@dashadmin/dash-components 1.3.72 → 1.3.74-srcfree.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -21
- package/src/components/AuditLog/AuditLog.tsx +0 -490
- package/src/components/AuditLog/index.ts +0 -2
- package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +0 -917
- package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +0 -39
- package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +0 -916
- package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -42
- package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +0 -44
- package/src/components/Json/Json.tsx +0 -312
- package/src/components/JsonColorSelector/JsonColorSelector.tsx +0 -1443
- package/src/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -746
- package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +0 -1135
- package/src/components/JsonColorSelector/JsonCssVarValues.tsx +0 -867
- package/src/components/JsonColorSelector/components/BasicColorSelector.tsx +0 -333
- package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +0 -495
- package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +0 -80
- package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +0 -420
- package/src/components/JsonColorSelector/helpers/functions.tsx +0 -123
- package/src/components/JsonColorSelector/helpers/paletteDerivation.ts +0 -367
- package/src/components/JsonColorSelector/interfaces/interfaces.tsx +0 -22
- package/src/components/JsonColorSelector/useAsyncColorThief.tsx +0 -97
- package/src/components/ListActive/ListActive.tsx +0 -106
- package/src/components/ListActive/ListBoolean.tsx +0 -96
- package/src/components/NotificationPreferences/NotificationPreferences.tsx +0 -480
- package/src/components/NotificationPreferences/index.tsx +0 -2
- package/src/components/SearchableSelects/RASearchableSelect.tsx +0 -303
- package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +0 -617
- package/src/components/SearchableSelects/RASearchableSelectChips.tsx +0 -561
- package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +0 -366
- package/src/components/SearchableSelects/SearchableSelect.tsx +0 -323
- package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +0 -358
- package/src/components/SortableDataGrid/README.md +0 -1
- package/src/components/SortableDataGrid/SortableDataGrid.tsx +0 -56
- package/src/components/SortableDataGrid/SortableDataGridBody.tsx +0 -175
- package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +0 -171
- package/src/components/SortableDataGrid/SortableDataGridRow.tsx +0 -247
- package/src/components/Upload/SingleImageUploader.tsx +0 -55
- package/src/components/custom/PackageCopyMethod.tsx +0 -109
- package/src/components/custom/PackageRadioButtonGroup.tsx +0 -49
- package/src/components/dialog/Basic.tsx +0 -11
- package/src/components/dialog/Correct.tsx +0 -16
- package/src/components/dialog/Error.tsx +0 -15
- package/src/components/dialog/Info.tsx +0 -15
- package/src/components/dialog/InfoV2.tsx +0 -35
- package/src/components/dialog/NotFound.tsx +0 -18
- package/src/components/dialog/QuickSearch.tsx +0 -115
- package/src/components/notifications/NotificationsCenter.tsx +0 -743
- package/src/components/notifications/index.ts +0 -42
- package/src/components/tables/PaginationComponent.tsx +0 -17
- package/src/components/theme/AppLoadingFallback.tsx +0 -42
- package/src/components/theme/ErrorBoundary.tsx +0 -57
- package/src/components/theme/InitialLoader.tsx +0 -16
- package/src/components/theme/NotFound.tsx +0 -124
- package/src/components/theme/NotResults.tsx +0 -56
- package/src/hooks/useDraggable.ts +0 -11
- package/src/hooks/useNotifications.ts +0 -14
- package/src/hooks/useQuickSearch.tsx +0 -29
- package/src/index.tsx +0 -77
- package/src/styles/index.less +0 -0
- package/src/utils/setNativeValue.tsx +0 -23
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Box,
|
|
3
|
-
Button,
|
|
4
|
-
IconButton,
|
|
5
|
-
Typography,
|
|
6
|
-
Card,
|
|
7
|
-
CardMedia,
|
|
8
|
-
CircularProgress,
|
|
9
|
-
Input,
|
|
10
|
-
Alert,
|
|
11
|
-
TextField,
|
|
12
|
-
Divider,
|
|
13
|
-
} from '@mui/material';
|
|
14
|
-
|
|
15
|
-
import DeleteIcon from '@mui/icons-material/Delete';
|
|
16
|
-
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
|
17
|
-
import PaletteIcon from '@mui/icons-material/Palette';
|
|
18
|
-
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
|
19
|
-
import UpdateIcon from '@mui/icons-material/Update';
|
|
20
|
-
import { useState, useEffect, memo, useCallback } from 'react';
|
|
21
|
-
import { useTranslate } from 'react-admin';
|
|
22
|
-
import { useAsyncColorThief } from '../useAsyncColorThief';
|
|
23
|
-
import { getContrastColor, rgbArrayToHex } from '../helpers/functions';
|
|
24
|
-
import { KeyValuePair } from '../interfaces/interfaces';
|
|
25
|
-
import { useAxios } from 'dash-axios-hook';
|
|
26
|
-
|
|
27
|
-
const MAX_IMAGE_MB = 5;
|
|
28
|
-
|
|
29
|
-
const ImageColorExtractor = memo<{
|
|
30
|
-
onColorsExtracted: (colors: number[][]) => void;
|
|
31
|
-
onColorsUpdate?: (newPairs: KeyValuePair[]) => void;
|
|
32
|
-
existingPairs?: KeyValuePair[];
|
|
33
|
-
/** When false, the OpenAI theme generation UI is hidden — only local extraction applies. */
|
|
34
|
-
aiEnabled?: boolean;
|
|
35
|
-
/** Local (no-AI) apply: receives the extracted RGB palette so the caller can map it to base colors. */
|
|
36
|
-
onApplyLocal?: (palette: number[][]) => void;
|
|
37
|
-
}>(({ onColorsExtracted, onColorsUpdate, existingPairs = [], aiEnabled = true, onApplyLocal }) => {
|
|
38
|
-
const translate = useTranslate();
|
|
39
|
-
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
40
|
-
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
|
41
|
-
const [themePrompt, setThemePrompt] = useState<string>('');
|
|
42
|
-
const [isGeneratingTheme, setIsGeneratingTheme] = useState<boolean>(false);
|
|
43
|
-
const [themeError, setThemeError] = useState<string | null>(null);
|
|
44
|
-
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
|
45
|
-
|
|
46
|
-
// Add state to preserve extracted colors independently
|
|
47
|
-
const [extractedPalette, setExtractedPalette] = useState<number[][]>([]);
|
|
48
|
-
|
|
49
|
-
const axios = useAxios();
|
|
50
|
-
const { dominantColor, palette, loading, error } = useAsyncColorThief(imageUrl, {
|
|
51
|
-
colorCount: 8,
|
|
52
|
-
quality: 10
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Update internal extracted palette state
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (palette && palette.length > 0) {
|
|
58
|
-
setExtractedPalette(palette);
|
|
59
|
-
onColorsExtracted(palette);
|
|
60
|
-
}
|
|
61
|
-
}, [palette, onColorsExtracted]);
|
|
62
|
-
|
|
63
|
-
// Shared validation + preview setup for a file coming from either the file
|
|
64
|
-
// input or a drag-and-drop, so the two paths can never drift apart.
|
|
65
|
-
const processFile = useCallback((file: File) => {
|
|
66
|
-
// Validate file type
|
|
67
|
-
if (!file.type.startsWith('image/')) {
|
|
68
|
-
alert(translate('colorSelector.imageExtractor.invalid_file_type'));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Validate file size
|
|
73
|
-
if (file.size > MAX_IMAGE_MB * 1024 * 1024) {
|
|
74
|
-
alert(translate('colorSelector.imageExtractor.file_too_large', { limit: MAX_IMAGE_MB }));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
setUploadedFile(file);
|
|
79
|
-
|
|
80
|
-
// Create preview URL
|
|
81
|
-
const previewUrl = URL.createObjectURL(file);
|
|
82
|
-
setImageUrl(previewUrl);
|
|
83
|
-
}, [translate]);
|
|
84
|
-
|
|
85
|
-
// Handle file selection for upload
|
|
86
|
-
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
87
|
-
const file = event.target.files?.[0];
|
|
88
|
-
if (file) {
|
|
89
|
-
processFile(file);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Drag-and-drop onto the upload dropzone
|
|
94
|
-
const handleDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
|
95
|
-
event.preventDefault();
|
|
96
|
-
event.stopPropagation();
|
|
97
|
-
setIsDragOver(true);
|
|
98
|
-
}, []);
|
|
99
|
-
|
|
100
|
-
const handleDragLeave = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
|
101
|
-
event.preventDefault();
|
|
102
|
-
event.stopPropagation();
|
|
103
|
-
setIsDragOver(false);
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
|
107
|
-
event.preventDefault();
|
|
108
|
-
event.stopPropagation();
|
|
109
|
-
setIsDragOver(false);
|
|
110
|
-
|
|
111
|
-
const file = event.dataTransfer.files?.[0];
|
|
112
|
-
if (file) {
|
|
113
|
-
processFile(file);
|
|
114
|
-
}
|
|
115
|
-
}, [processFile]);
|
|
116
|
-
|
|
117
|
-
// Handle image removal
|
|
118
|
-
const handleImageRemove = () => {
|
|
119
|
-
if (imageUrl && imageUrl.startsWith('blob:')) {
|
|
120
|
-
URL.revokeObjectURL(imageUrl);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
setImageUrl(null);
|
|
124
|
-
setUploadedFile(null);
|
|
125
|
-
setExtractedPalette([]); // Clear internal palette state
|
|
126
|
-
onColorsExtracted([]); // Clear extracted colors
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Generate theme using AI - Modified to merge with existing colors
|
|
130
|
-
const handleGenerateTheme = useCallback(async () => {
|
|
131
|
-
if (!extractedPalette || extractedPalette.length === 0) {
|
|
132
|
-
setThemeError(translate('colorSelector.imageExtractor.no_palette_error'));
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
setIsGeneratingTheme(true);
|
|
137
|
-
setThemeError(null);
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const response = await axios.post('/tenant/tenant/settings/theme-generator', {
|
|
141
|
-
colors: extractedPalette,
|
|
142
|
-
prompt: themePrompt,
|
|
143
|
-
output_format: 'json'
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (response.status !== 200) {
|
|
147
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const result = response.data;
|
|
151
|
-
|
|
152
|
-
if (result.success && result.data) {
|
|
153
|
-
// Convert the generated theme to KeyValuePair format
|
|
154
|
-
const newPairs: KeyValuePair[] = Object.entries(result.data).map(([key, value]) => ({
|
|
155
|
-
key,
|
|
156
|
-
value: String(value),
|
|
157
|
-
id: Math.random().toString(36).substring(2, 11)
|
|
158
|
-
}));
|
|
159
|
-
|
|
160
|
-
// Merge with existing pairs instead of replacing them
|
|
161
|
-
const mergedPairs = [...existingPairs];
|
|
162
|
-
|
|
163
|
-
newPairs.forEach(newPair => {
|
|
164
|
-
const existingIndex = mergedPairs.findIndex(existing => existing.key === newPair.key);
|
|
165
|
-
if (existingIndex >= 0) {
|
|
166
|
-
// Update existing pair
|
|
167
|
-
mergedPairs[existingIndex] = newPair;
|
|
168
|
-
} else {
|
|
169
|
-
// Add new pair
|
|
170
|
-
mergedPairs.push(newPair);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Update the colors in the parent component
|
|
175
|
-
if (onColorsUpdate) {
|
|
176
|
-
onColorsUpdate(mergedPairs);
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
throw new Error(result.message || translate('colorSelector.imageExtractor.theme_generation_failed'));
|
|
180
|
-
}
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error('Theme generation error:', error);
|
|
183
|
-
setThemeError(
|
|
184
|
-
error instanceof Error
|
|
185
|
-
? error.message
|
|
186
|
-
: translate('colorSelector.imageExtractor.theme_generation_failed')
|
|
187
|
-
);
|
|
188
|
-
} finally {
|
|
189
|
-
setIsGeneratingTheme(false);
|
|
190
|
-
}
|
|
191
|
-
}, [extractedPalette, themePrompt, existingPairs, onColorsUpdate, axios, translate]);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<Box sx={{ mt: 3, p: 3, borderRadius: 2 }}>
|
|
195
|
-
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
196
|
-
<PaletteIcon />
|
|
197
|
-
{aiEnabled
|
|
198
|
-
? translate('colorSelector.imageExtractor.title_ai')
|
|
199
|
-
: translate('colorSelector.imageExtractor.title_local')}
|
|
200
|
-
</Typography>
|
|
201
|
-
|
|
202
|
-
<Divider sx={{ mb: 3 }} />
|
|
203
|
-
|
|
204
|
-
{/* Image Upload Section */}
|
|
205
|
-
<Box sx={{ mb: 3 }}>
|
|
206
|
-
{!imageUrl ? (
|
|
207
|
-
<Box
|
|
208
|
-
onDragEnter={handleDragOver}
|
|
209
|
-
onDragOver={handleDragOver}
|
|
210
|
-
onDragLeave={handleDragLeave}
|
|
211
|
-
onDrop={handleDrop}
|
|
212
|
-
sx={{
|
|
213
|
-
textAlign: 'center',
|
|
214
|
-
py: 4,
|
|
215
|
-
backgroundColor: isDragOver ? 'rgba(25, 118, 210, 0.08)' : 'rgba(0,0,0,0.02)',
|
|
216
|
-
borderRadius: 1,
|
|
217
|
-
border: isDragOver ? '2px dashed #1976d2' : '2px dashed #e0e0e0',
|
|
218
|
-
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
|
219
|
-
}}
|
|
220
|
-
>
|
|
221
|
-
<Input
|
|
222
|
-
type="file"
|
|
223
|
-
inputProps={{ accept: 'image/*' }}
|
|
224
|
-
onChange={handleFileSelect}
|
|
225
|
-
sx={{ display: 'none' }}
|
|
226
|
-
id="color-extractor-upload"
|
|
227
|
-
/>
|
|
228
|
-
<Typography variant="body1" color="textSecondary" gutterBottom>
|
|
229
|
-
{translate('colorSelector.imageExtractor.upload_prompt')}
|
|
230
|
-
</Typography>
|
|
231
|
-
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
|
232
|
-
{translate('colorSelector.imageExtractor.upload_hint')}
|
|
233
|
-
</Typography>
|
|
234
|
-
<label htmlFor="color-extractor-upload">
|
|
235
|
-
<Button
|
|
236
|
-
variant="contained"
|
|
237
|
-
component="span"
|
|
238
|
-
startIcon={<CloudUploadIcon />}
|
|
239
|
-
size="large"
|
|
240
|
-
>
|
|
241
|
-
{translate('colorSelector.imageExtractor.upload_button')}
|
|
242
|
-
</Button>
|
|
243
|
-
</label>
|
|
244
|
-
<Typography variant="caption" color="textSecondary" sx={{ display: 'block', mt: 1.5 }}>
|
|
245
|
-
{translate('colorSelector.imageExtractor.drop_hint')}
|
|
246
|
-
</Typography>
|
|
247
|
-
</Box>
|
|
248
|
-
) : (
|
|
249
|
-
<Card sx={{ maxWidth: 400, mb: 3 }}>
|
|
250
|
-
<Box sx={{ position: 'relative' }}>
|
|
251
|
-
<CardMedia
|
|
252
|
-
component="img"
|
|
253
|
-
height="200"
|
|
254
|
-
image={imageUrl}
|
|
255
|
-
alt={translate('colorSelector.imageExtractor.uploaded_image_alt')}
|
|
256
|
-
sx={{ objectFit: 'contain' }}
|
|
257
|
-
/>
|
|
258
|
-
<IconButton
|
|
259
|
-
sx={{
|
|
260
|
-
position: 'absolute',
|
|
261
|
-
top: 8,
|
|
262
|
-
right: 8,
|
|
263
|
-
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
264
|
-
'&:hover': {
|
|
265
|
-
backgroundColor: 'rgba(255, 255, 255, 1)',
|
|
266
|
-
},
|
|
267
|
-
}}
|
|
268
|
-
onClick={handleImageRemove}
|
|
269
|
-
size="small"
|
|
270
|
-
>
|
|
271
|
-
<DeleteIcon />
|
|
272
|
-
</IconButton>
|
|
273
|
-
</Box>
|
|
274
|
-
</Card>
|
|
275
|
-
)}
|
|
276
|
-
</Box>
|
|
277
|
-
|
|
278
|
-
{/* Color Extraction Status */}
|
|
279
|
-
{loading && (
|
|
280
|
-
<Box
|
|
281
|
-
sx={{
|
|
282
|
-
display: "flex",
|
|
283
|
-
alignItems: "center",
|
|
284
|
-
gap: 2,
|
|
285
|
-
mb: 3,
|
|
286
|
-
p: 2,
|
|
287
|
-
backgroundColor: 'rgba(25, 118, 210, 0.04)',
|
|
288
|
-
borderRadius: 1
|
|
289
|
-
}}>
|
|
290
|
-
<CircularProgress size={20} />
|
|
291
|
-
<Typography>{translate('colorSelector.imageExtractor.extracting')}</Typography>
|
|
292
|
-
</Box>
|
|
293
|
-
)}
|
|
294
|
-
|
|
295
|
-
{error && (
|
|
296
|
-
<Alert severity="error" sx={{ mb: 3 }}>
|
|
297
|
-
{error}
|
|
298
|
-
</Alert>
|
|
299
|
-
)}
|
|
300
|
-
|
|
301
|
-
{themeError && (
|
|
302
|
-
<Alert severity="error" sx={{ mb: 3 }}>
|
|
303
|
-
{themeError}
|
|
304
|
-
</Alert>
|
|
305
|
-
)}
|
|
306
|
-
|
|
307
|
-
{/* Extracted Colors Display - Use internal state */}
|
|
308
|
-
{extractedPalette && extractedPalette.length > 0 && !loading && (
|
|
309
|
-
<Box sx={{ mb: 3 }}>
|
|
310
|
-
<Typography variant="subtitle1" gutterBottom>
|
|
311
|
-
{translate('colorSelector.imageExtractor.extracted_colors', { count: extractedPalette.length })}
|
|
312
|
-
</Typography>
|
|
313
|
-
|
|
314
|
-
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2, mb: 3 }}>
|
|
315
|
-
{extractedPalette.map((color, index) => {
|
|
316
|
-
const hexColor = rgbArrayToHex(color);
|
|
317
|
-
const isDominant = dominantColor &&
|
|
318
|
-
color[0] === dominantColor[0] &&
|
|
319
|
-
color[1] === dominantColor[1] &&
|
|
320
|
-
color[2] === dominantColor[2];
|
|
321
|
-
|
|
322
|
-
return (
|
|
323
|
-
<Box
|
|
324
|
-
key={index}
|
|
325
|
-
sx={{
|
|
326
|
-
width: 50,
|
|
327
|
-
height: 50,
|
|
328
|
-
backgroundColor: hexColor,
|
|
329
|
-
borderRadius: '50%',
|
|
330
|
-
display: 'flex',
|
|
331
|
-
alignItems: 'center',
|
|
332
|
-
justifyContent: 'center',
|
|
333
|
-
border: isDominant ? '3px solid #4caf50' : '2px solid #ccc',
|
|
334
|
-
cursor: 'pointer',
|
|
335
|
-
transition: 'transform 0.2s',
|
|
336
|
-
'&:hover': {
|
|
337
|
-
transform: 'scale(1.1)',
|
|
338
|
-
},
|
|
339
|
-
}}
|
|
340
|
-
title={`${hexColor} ${isDominant ? `(${translate('colorSelector.imageExtractor.dominant_label')})` : ''}`}
|
|
341
|
-
>
|
|
342
|
-
<Typography
|
|
343
|
-
variant="caption"
|
|
344
|
-
sx={{
|
|
345
|
-
color: getContrastColor(hexColor),
|
|
346
|
-
fontWeight: 'bold',
|
|
347
|
-
fontSize: '0.8rem',
|
|
348
|
-
}}
|
|
349
|
-
>
|
|
350
|
-
{index + 1}
|
|
351
|
-
</Typography>
|
|
352
|
-
</Box>
|
|
353
|
-
);
|
|
354
|
-
})}
|
|
355
|
-
</Box>
|
|
356
|
-
|
|
357
|
-
{/* Theme Generation Controls */}
|
|
358
|
-
<Box sx={{ mb: 3 }}>
|
|
359
|
-
{aiEnabled && (
|
|
360
|
-
<TextField
|
|
361
|
-
fullWidth
|
|
362
|
-
multiline
|
|
363
|
-
rows={2}
|
|
364
|
-
placeholder={translate('colorSelector.imageExtractor.theme_description_placeholder')}
|
|
365
|
-
value={themePrompt}
|
|
366
|
-
onChange={(e) => setThemePrompt(e.target.value)}
|
|
367
|
-
sx={{ mb: 2 }}
|
|
368
|
-
label={translate('colorSelector.imageExtractor.theme_description_label')}
|
|
369
|
-
/>
|
|
370
|
-
)}
|
|
371
|
-
|
|
372
|
-
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
|
373
|
-
{onApplyLocal && (
|
|
374
|
-
<Button
|
|
375
|
-
variant={aiEnabled ? 'outlined' : 'contained'}
|
|
376
|
-
startIcon={<UpdateIcon />}
|
|
377
|
-
onClick={() => onApplyLocal(extractedPalette)}
|
|
378
|
-
color="primary"
|
|
379
|
-
>
|
|
380
|
-
{translate('colorSelector.imageExtractor.apply_colors_button')}
|
|
381
|
-
</Button>
|
|
382
|
-
)}
|
|
383
|
-
{aiEnabled && (
|
|
384
|
-
<Button
|
|
385
|
-
variant="contained"
|
|
386
|
-
startIcon={isGeneratingTheme ? <CircularProgress size={16} /> : <AutoFixHighIcon />}
|
|
387
|
-
onClick={handleGenerateTheme}
|
|
388
|
-
disabled={isGeneratingTheme}
|
|
389
|
-
color="primary"
|
|
390
|
-
>
|
|
391
|
-
{isGeneratingTheme
|
|
392
|
-
? translate('colorSelector.imageExtractor.generating_button')
|
|
393
|
-
: translate('colorSelector.imageExtractor.generate_button')}
|
|
394
|
-
</Button>
|
|
395
|
-
)}
|
|
396
|
-
</Box>
|
|
397
|
-
</Box>
|
|
398
|
-
|
|
399
|
-
<Typography variant="body2" color="textSecondary" sx={{ fontStyle: 'italic' }}>
|
|
400
|
-
{onApplyLocal && (
|
|
401
|
-
<>
|
|
402
|
-
• <strong>{translate('colorSelector.imageExtractor.apply_colors_help_label')}</strong>{' '}
|
|
403
|
-
{translate('colorSelector.imageExtractor.apply_colors_help_desc')}
|
|
404
|
-
<br />
|
|
405
|
-
</>
|
|
406
|
-
)}
|
|
407
|
-
{aiEnabled && (
|
|
408
|
-
<>
|
|
409
|
-
• <strong>{translate('colorSelector.imageExtractor.generate_help_label')}</strong>{' '}
|
|
410
|
-
{translate('colorSelector.imageExtractor.generate_help_desc')}
|
|
411
|
-
</>
|
|
412
|
-
)}
|
|
413
|
-
</Typography>
|
|
414
|
-
</Box>
|
|
415
|
-
)}
|
|
416
|
-
</Box>
|
|
417
|
-
);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
export default ImageColorExtractor;
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { ColorFormat, ColorFormatType, KeyValuePair } from "../interfaces/interfaces";
|
|
2
|
-
|
|
3
|
-
// Helper function to determine text color based on background
|
|
4
|
-
export const getContrastColor = (color: string): string => {
|
|
5
|
-
// Handle different color formats
|
|
6
|
-
let r: number, g: number, b: number, a: number = 1;
|
|
7
|
-
|
|
8
|
-
if (color.startsWith('#')) {
|
|
9
|
-
// Hex color
|
|
10
|
-
const hex = color.replace('#', '');
|
|
11
|
-
if (hex.length === 8) {
|
|
12
|
-
// RGBA hex
|
|
13
|
-
r = parseInt(hex.substr(0, 2), 16);
|
|
14
|
-
g = parseInt(hex.substr(2, 2), 16);
|
|
15
|
-
b = parseInt(hex.substr(4, 2), 16);
|
|
16
|
-
a = parseInt(hex.substr(6, 2), 16) / 255;
|
|
17
|
-
} else {
|
|
18
|
-
// RGB hex
|
|
19
|
-
r = parseInt(hex.substr(0, 2), 16);
|
|
20
|
-
g = parseInt(hex.substr(2, 2), 16);
|
|
21
|
-
b = parseInt(hex.substr(4, 2), 16);
|
|
22
|
-
}
|
|
23
|
-
} else if (color.startsWith('rgb')) {
|
|
24
|
-
// RGB/RGBA color
|
|
25
|
-
const match = color.match(/rgba?\(([^)]+)\)/);
|
|
26
|
-
if (match) {
|
|
27
|
-
const values = match[1].split(',').map(v => parseFloat(v.trim()));
|
|
28
|
-
[r, g, b, a = 1] = values;
|
|
29
|
-
} else {
|
|
30
|
-
return '#000000';
|
|
31
|
-
}
|
|
32
|
-
} else if (color.startsWith('hsl')) {
|
|
33
|
-
// For HSL, we'll use a simple approach
|
|
34
|
-
return '#000000';
|
|
35
|
-
} else {
|
|
36
|
-
return '#000000';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Calculate luminance considering alpha
|
|
40
|
-
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
41
|
-
const effectiveLuminance = luminance * a + (1 - a); // Blend with white background
|
|
42
|
-
|
|
43
|
-
return effectiveLuminance > 0.5 ? '#000000' : '#ffffff';
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Helper function to convert color to different formats
|
|
47
|
-
// Helper function to convert color to different formats - Updated
|
|
48
|
-
export const convertColor = (color: ColorFormat, format: ColorFormatType): string => {
|
|
49
|
-
switch (format) {
|
|
50
|
-
case 'hex':
|
|
51
|
-
// For hex, we'll ignore alpha or convert to 8-digit hex if alpha < 1
|
|
52
|
-
if (color.rgb.a !== undefined && color.rgb.a < 1) {
|
|
53
|
-
const alpha = Math.round(color.rgb.a * 255);
|
|
54
|
-
const alphaHex = alpha.toString(16).padStart(2, '0');
|
|
55
|
-
return `${color.hex}${alphaHex}`;
|
|
56
|
-
}
|
|
57
|
-
return color.hex;
|
|
58
|
-
case 'rgb':
|
|
59
|
-
return `rgb(${Math.round(color.rgb.r)}, ${Math.round(color.rgb.g)}, ${Math.round(color.rgb.b)})`;
|
|
60
|
-
case 'rgba':
|
|
61
|
-
const alpha = color.rgb.a !== undefined ? Number(color.rgb.a.toFixed(2)) : 1;
|
|
62
|
-
return `rgba(${Math.round(color.rgb.r)}, ${Math.round(color.rgb.g)}, ${Math.round(color.rgb.b)}, ${alpha})`;
|
|
63
|
-
case 'hsl':
|
|
64
|
-
return `hsl(${Math.round(color.hsl.h)}, ${Math.round(color.hsl.s * 100)}%, ${Math.round(color.hsl.l * 100)}%)`;
|
|
65
|
-
case 'hsla':
|
|
66
|
-
const hslaAlpha = color.hsl.a !== undefined ? Number(color.hsl.a.toFixed(2)) : 1;
|
|
67
|
-
return `hsla(${Math.round(color.hsl.h)}, ${Math.round(color.hsl.s * 100)}%, ${Math.round(color.hsl.l * 100)}%, ${hslaAlpha})`;
|
|
68
|
-
default:
|
|
69
|
-
return color.hex;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
// Helper function to parse color key and create chips
|
|
73
|
-
export const parseColorKey = (key: string) => {
|
|
74
|
-
const parts = key.split('--');
|
|
75
|
-
const baseName = parts[0];
|
|
76
|
-
const mode = parts[1];
|
|
77
|
-
|
|
78
|
-
// Split base name by hyphens for individual chips
|
|
79
|
-
const baseChips = baseName.split('-').filter(part => part.length > 0);
|
|
80
|
-
|
|
81
|
-
return { baseChips, mode };
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Helper function to extract available modes from color keys
|
|
85
|
-
export const extractAvailableModes = (pairs: KeyValuePair[]): string[] => {
|
|
86
|
-
const modes = new Set<string>();
|
|
87
|
-
|
|
88
|
-
pairs.forEach(pair => {
|
|
89
|
-
const { mode } = parseColorKey(pair.key);
|
|
90
|
-
if (mode) {
|
|
91
|
-
modes.add(mode);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return Array.from(modes).sort();
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// Helper function to get mode icon
|
|
99
|
-
export const getModeIcon = (mode: string | undefined): string => {
|
|
100
|
-
if (!mode) return '';
|
|
101
|
-
|
|
102
|
-
switch (mode.toLowerCase()) {
|
|
103
|
-
case 'light':
|
|
104
|
-
return '☀️'; // Sun icon
|
|
105
|
-
case 'dark':
|
|
106
|
-
return '🌙'; // Moon icon
|
|
107
|
-
default:
|
|
108
|
-
return mode; // Plain text for other modes
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Helper function to convert RGB array to hex
|
|
113
|
-
export const rgbArrayToHex = (rgb: number[]): string => {
|
|
114
|
-
const [r, g, b] = rgb;
|
|
115
|
-
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Helper function to convert RGB array to rgb string
|
|
119
|
-
export const rgbArrayToRgbString = (rgb: number[]): string => {
|
|
120
|
-
const [r, g, b] = rgb;
|
|
121
|
-
return `rgb(${r}, ${g}, ${b})`;
|
|
122
|
-
};
|
|
123
|
-
|