|
| 1 | +/* |
| 2 | + * Copyright 2025 The Kubernetes Authors |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +import Box from '@mui/material/Box'; |
| 18 | +import Button from '@mui/material/Button'; |
| 19 | +import Checkbox from '@mui/material/Checkbox'; |
| 20 | +import Dialog from '@mui/material/Dialog'; |
| 21 | +import DialogActions from '@mui/material/DialogActions'; |
| 22 | +import DialogContent from '@mui/material/DialogContent'; |
| 23 | +import DialogTitle from '@mui/material/DialogTitle'; |
| 24 | +import FormControlLabel from '@mui/material/FormControlLabel'; |
| 25 | +import { useTheme } from '@mui/material/styles'; |
| 26 | +import TextField from '@mui/material/TextField'; |
| 27 | +import ToggleButton from '@mui/material/ToggleButton'; |
| 28 | +import Tooltip from '@mui/material/Tooltip'; |
| 29 | +import React from 'react'; |
| 30 | +import { useTranslation } from 'react-i18next'; |
| 31 | +import { isValidCssColor } from '../../../helpers/clusterAppearance'; |
| 32 | + |
| 33 | +// Preset colors for cluster appearance |
| 34 | +export const PRESET_COLORS = [ |
| 35 | + { name: 'Red', value: '#f44336' }, |
| 36 | + { name: 'Pink', value: '#e91e63' }, |
| 37 | + { name: 'Purple', value: '#9c27b0' }, |
| 38 | + { name: 'Deep Purple', value: '#673ab7' }, |
| 39 | + { name: 'Indigo', value: '#3f51b5' }, |
| 40 | + { name: 'Blue', value: '#2196f3' }, |
| 41 | + { name: 'Light Blue', value: '#03a9f4' }, |
| 42 | + { name: 'Cyan', value: '#00bcd4' }, |
| 43 | + { name: 'Teal', value: '#009688' }, |
| 44 | + { name: 'Green', value: '#4caf50' }, |
| 45 | + { name: 'Light Green', value: '#8bc34a' }, |
| 46 | + { name: 'Lime', value: '#cddc39' }, |
| 47 | + { name: 'Yellow', value: '#ffeb3b' }, |
| 48 | + { name: 'Amber', value: '#ffc107' }, |
| 49 | + { name: 'Orange', value: '#ff9800' }, |
| 50 | + { name: 'Deep Orange', value: '#ff5722' }, |
| 51 | +]; |
| 52 | + |
| 53 | +interface ColorPickerProps { |
| 54 | + open: boolean; |
| 55 | + currentColor: string; |
| 56 | + onClose: () => void; |
| 57 | + onSelectColor: (color: string) => void; |
| 58 | + onError: (error: string) => void; |
| 59 | +} |
| 60 | + |
| 61 | +export default function ColorPicker({ |
| 62 | + open, |
| 63 | + currentColor, |
| 64 | + onClose, |
| 65 | + onSelectColor, |
| 66 | + onError, |
| 67 | +}: ColorPickerProps) { |
| 68 | + const { t } = useTranslation(['translation']); |
| 69 | + const theme = useTheme(); |
| 70 | + |
| 71 | + const [customColorInput, setCustomColorInput] = React.useState(''); |
| 72 | + const [useCustomColor, setUseCustomColor] = React.useState(false); |
| 73 | + |
| 74 | + const isValidAccentColor = (color: string): boolean => !color || isValidCssColor(color); |
| 75 | + |
| 76 | + const handlePresetColorClick = (color: string) => { |
| 77 | + setUseCustomColor(false); |
| 78 | + onSelectColor(color); |
| 79 | + onError(''); |
| 80 | + onClose(); |
| 81 | + }; |
| 82 | + |
| 83 | + const handleApplyCustomColor = () => { |
| 84 | + if (isValidAccentColor(customColorInput)) { |
| 85 | + onSelectColor(customColorInput); |
| 86 | + onError(''); |
| 87 | + onClose(); |
| 88 | + } else { |
| 89 | + onError( |
| 90 | + t('translation|Accent color format is invalid. Use hex (#ff0000), rgb(), or rgba().') |
| 91 | + ); |
| 92 | + } |
| 93 | + }; |
| 94 | + |
| 95 | + return ( |
| 96 | + <Dialog open={open} onClose={onClose} maxWidth="sm"> |
| 97 | + <DialogTitle>{t('translation|Choose Accent Color')}</DialogTitle> |
| 98 | + <DialogContent> |
| 99 | + <Box sx={{ pt: 1 }}> |
| 100 | + <Box display="flex" flexWrap="wrap" gap={1}> |
| 101 | + {PRESET_COLORS.map(color => ( |
| 102 | + <Tooltip key={color.value} title={color.name}> |
| 103 | + <ToggleButton |
| 104 | + value={color.value} |
| 105 | + selected={currentColor === color.value && !useCustomColor} |
| 106 | + onChange={() => handlePresetColorClick(color.value)} |
| 107 | + disabled={useCustomColor} |
| 108 | + sx={{ |
| 109 | + width: 48, |
| 110 | + height: 48, |
| 111 | + backgroundColor: color.value, |
| 112 | + '&:hover': { |
| 113 | + backgroundColor: color.value, |
| 114 | + opacity: 0.8, |
| 115 | + }, |
| 116 | + '&.Mui-selected': { |
| 117 | + backgroundColor: color.value, |
| 118 | + border: `3px solid ${theme.palette.text.primary}`, |
| 119 | + '&:hover': { |
| 120 | + backgroundColor: color.value, |
| 121 | + }, |
| 122 | + }, |
| 123 | + '&.Mui-disabled': { |
| 124 | + opacity: 0.5, |
| 125 | + }, |
| 126 | + }} |
| 127 | + /> |
| 128 | + </Tooltip> |
| 129 | + ))} |
| 130 | + </Box> |
| 131 | + </Box> |
| 132 | + <Box sx={{ mt: 2 }}> |
| 133 | + <FormControlLabel |
| 134 | + control={ |
| 135 | + <Checkbox |
| 136 | + checked={useCustomColor} |
| 137 | + onChange={e => setUseCustomColor(e.target.checked)} |
| 138 | + /> |
| 139 | + } |
| 140 | + label={t('translation|Use custom color')} |
| 141 | + /> |
| 142 | + {useCustomColor && ( |
| 143 | + <TextField |
| 144 | + label={t('translation|Custom color')} |
| 145 | + placeholder="#ff0000" |
| 146 | + value={customColorInput} |
| 147 | + onChange={e => setCustomColorInput(e.target.value)} |
| 148 | + fullWidth |
| 149 | + helperText={t('translation|Hex, rgb(), or rgba()')} |
| 150 | + error={!!customColorInput && !isValidAccentColor(customColorInput)} |
| 151 | + sx={{ mt: 1 }} |
| 152 | + /> |
| 153 | + )} |
| 154 | + </Box> |
| 155 | + </DialogContent> |
| 156 | + <DialogActions> |
| 157 | + <Button onClick={onClose}>{t('translation|Cancel')}</Button> |
| 158 | + {useCustomColor && ( |
| 159 | + <Button |
| 160 | + onClick={handleApplyCustomColor} |
| 161 | + disabled={!customColorInput || !isValidAccentColor(customColorInput)} |
| 162 | + > |
| 163 | + {t('translation|Apply')} |
| 164 | + </Button> |
| 165 | + )} |
| 166 | + </DialogActions> |
| 167 | + </Dialog> |
| 168 | + ); |
| 169 | +} |
0 commit comments