@dashadmin/dash-components 1.3.73 → 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.
Files changed (60) hide show
  1. package/package.json +24 -21
  2. package/src/components/AuditLog/AuditLog.tsx +0 -490
  3. package/src/components/AuditLog/index.ts +0 -2
  4. package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +0 -917
  5. package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +0 -39
  6. package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +0 -916
  7. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -42
  8. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +0 -44
  9. package/src/components/Json/Json.tsx +0 -312
  10. package/src/components/JsonColorSelector/JsonColorSelector.tsx +0 -1443
  11. package/src/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -746
  12. package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +0 -1135
  13. package/src/components/JsonColorSelector/JsonCssVarValues.tsx +0 -867
  14. package/src/components/JsonColorSelector/components/BasicColorSelector.tsx +0 -333
  15. package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +0 -495
  16. package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +0 -80
  17. package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +0 -420
  18. package/src/components/JsonColorSelector/helpers/functions.tsx +0 -123
  19. package/src/components/JsonColorSelector/helpers/paletteDerivation.ts +0 -367
  20. package/src/components/JsonColorSelector/interfaces/interfaces.tsx +0 -22
  21. package/src/components/JsonColorSelector/useAsyncColorThief.tsx +0 -97
  22. package/src/components/ListActive/ListActive.tsx +0 -106
  23. package/src/components/ListActive/ListBoolean.tsx +0 -96
  24. package/src/components/NotificationPreferences/NotificationPreferences.tsx +0 -480
  25. package/src/components/NotificationPreferences/index.tsx +0 -2
  26. package/src/components/SearchableSelects/RASearchableSelect.tsx +0 -303
  27. package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +0 -617
  28. package/src/components/SearchableSelects/RASearchableSelectChips.tsx +0 -561
  29. package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +0 -366
  30. package/src/components/SearchableSelects/SearchableSelect.tsx +0 -323
  31. package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +0 -358
  32. package/src/components/SortableDataGrid/README.md +0 -1
  33. package/src/components/SortableDataGrid/SortableDataGrid.tsx +0 -56
  34. package/src/components/SortableDataGrid/SortableDataGridBody.tsx +0 -175
  35. package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +0 -171
  36. package/src/components/SortableDataGrid/SortableDataGridRow.tsx +0 -247
  37. package/src/components/Upload/SingleImageUploader.tsx +0 -55
  38. package/src/components/custom/PackageCopyMethod.tsx +0 -109
  39. package/src/components/custom/PackageRadioButtonGroup.tsx +0 -49
  40. package/src/components/dialog/Basic.tsx +0 -11
  41. package/src/components/dialog/Correct.tsx +0 -16
  42. package/src/components/dialog/Error.tsx +0 -15
  43. package/src/components/dialog/Info.tsx +0 -15
  44. package/src/components/dialog/InfoV2.tsx +0 -35
  45. package/src/components/dialog/NotFound.tsx +0 -18
  46. package/src/components/dialog/QuickSearch.tsx +0 -115
  47. package/src/components/notifications/NotificationsCenter.tsx +0 -743
  48. package/src/components/notifications/index.ts +0 -42
  49. package/src/components/tables/PaginationComponent.tsx +0 -17
  50. package/src/components/theme/AppLoadingFallback.tsx +0 -42
  51. package/src/components/theme/ErrorBoundary.tsx +0 -57
  52. package/src/components/theme/InitialLoader.tsx +0 -16
  53. package/src/components/theme/NotFound.tsx +0 -124
  54. package/src/components/theme/NotResults.tsx +0 -56
  55. package/src/hooks/useDraggable.ts +0 -11
  56. package/src/hooks/useNotifications.ts +0 -14
  57. package/src/hooks/useQuickSearch.tsx +0 -29
  58. package/src/index.tsx +0 -77
  59. package/src/styles/index.less +0 -0
  60. package/src/utils/setNativeValue.tsx +0 -23
@@ -1,1135 +0,0 @@
1
- import { IDashAutoAdminCustomFieldComponent } from 'dash-auto-admin';
2
- import { useRecordContext } from 'react-admin';
3
- import { useFormContext } from 'react-hook-form';
4
- import {
5
- Box,
6
- Button,
7
- IconButton,
8
- TextField,
9
- Typography,
10
- Chip,
11
- InputAdornment,
12
- FormControl,
13
- InputLabel,
14
- Select,
15
- MenuItem,
16
- Grid,
17
- Pagination,
18
- Card,
19
- Link,
20
- ToggleButton,
21
- ToggleButtonGroup,
22
- } from '@mui/material';
23
- import AddIcon from '@mui/icons-material/Add';
24
- import EditIcon from '@mui/icons-material/Edit';
25
- import PreviewIcon from '@mui/icons-material/Preview';
26
- import SearchIcon from '@mui/icons-material/Search';
27
- import ClearIcon from '@mui/icons-material/Clear';
28
- import FilterListIcon from '@mui/icons-material/FilterList';
29
- import DownloadIcon from '@mui/icons-material/Download';
30
- import UploadIcon from '@mui/icons-material/Upload';
31
- import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
32
- import { ColorMapping, KeyValuePair } from './interfaces/interfaces';
33
- import { extractAvailableModes, getContrastColor, getModeIcon, parseColorKey } from './helpers/functions';
34
- import ImageColorExtractor from './components/ImageColorExtractor';
35
- import ColorEditDialog from './components/ColorEditDialog';
36
- import BasicColorSelector from './components/BasicColorSelector';
37
- import {
38
- BasicPalette,
39
- ThemeMode,
40
- basesFromImagePalette,
41
- deriveBrandPairs,
42
- extractBases,
43
- } from './helpers/paletteDerivation';
44
- import { updateDomCssVariables } from 'dash-utils';
45
- import { AuthPersistenceService } from 'dash-auth';
46
-
47
-
48
- // Feature flags for pagination behavior
49
- const JSON_COLOR_SELECTOR_PAGINATION_ENABLED: boolean = false;
50
- const JSON_COLOR_SELECTOR_PAGINATION_SHOW_ALL: boolean = false;
51
-
52
- // AI theme generation (OpenAI via backend theme-generator endpoint) is opt-in.
53
- // Enable per-field via the setting format's componentProps: { aiThemeEnabled: true }.
54
- const JSON_COLOR_SELECTOR_AI_ENABLED_DEFAULT: boolean = false;
55
-
56
- // Default color mappings (can be modified later)
57
- const DEFAULT_COLOR_MAPPINGS: ColorMapping = {
58
- "primary-color": "Primary Color",
59
- "secondary-color": "Secondary Color",
60
- "highlight-color": "Highlight Color",
61
- "contrast-color": "Contrast Color",
62
- };
63
-
64
-
65
- // Local Color Palette Item Component with new design
66
- const LocalColorPaletteItem: React.FC<{
67
- pair: KeyValuePair;
68
- onEdit: (pair: KeyValuePair) => void;
69
- onDelete: (id: string) => void;
70
- }> = ({ pair, onEdit, onDelete }) => {
71
- const textColor = getContrastColor(pair.value);
72
- const { baseChips, mode } = parseColorKey(pair.key);
73
- const modeIcon = getModeIcon(mode);
74
-
75
- return (
76
- <Card
77
- onClick={() => onEdit(pair)}
78
- elevation={1}
79
- sx={{
80
- width: '100%',
81
- display: 'flex',
82
- alignItems: 'stretch', // Ensure stretch for full height
83
- cursor: 'pointer',
84
- transition: 'all 0.2s ease-in-out',
85
- '&:hover': {
86
- transform: 'translateY(-2px)',
87
- boxShadow: 3,
88
- },
89
- borderRadius: 2,
90
- overflow: 'hidden',
91
- position: 'relative',
92
- height: 80, // Fixed height for consistency
93
- backgroundColor: 'background.paper',
94
- }}
95
- >
96
- {/* Left Color Box */}
97
- <Box
98
- sx={{
99
- width: 80, // Fixed square width
100
- backgroundColor: pair.value,
101
- flexShrink: 0,
102
- display: 'flex',
103
- alignItems: 'center',
104
- justifyContent: 'center',
105
- position: 'relative',
106
- }}
107
- >
108
- {/* Hover Edit Icon Overlay */}
109
- <Box
110
- sx={{
111
- position: 'absolute',
112
- top: 0,
113
- left: 0,
114
- right: 0,
115
- bottom: 0,
116
- backgroundColor: 'rgba(0,0,0,0.3)',
117
- opacity: 0,
118
- transition: 'opacity 0.2s',
119
- '&:hover': { opacity: 1 },
120
- display: 'flex',
121
- alignItems: 'center',
122
- justifyContent: 'center',
123
- }}
124
- >
125
- <EditIcon sx={{ color: '#fff' }} />
126
- </Box>
127
- </Box>
128
-
129
- {/* Right Details Section */}
130
- <Box sx={{
131
- flexGrow: 1,
132
- p: 1.5,
133
- display: 'flex',
134
- flexDirection: 'column',
135
- justifyContent: 'space-between',
136
- overflow: 'hidden'
137
- }}>
138
- <Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 0.5 }}>
139
- <Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 'bold', color: 'text.secondary' }}>
140
- {pair.value}
141
- </Typography>
142
- {mode && (
143
- <Chip
144
- label={mode}
145
- size="small"
146
- icon={<span style={{ marginLeft: 6, fontSize: '0.8rem' }}>{modeIcon}</span>}
147
- sx={{
148
- height: 18,
149
- fontSize: '0.65rem',
150
- '& .MuiChip-label': { padding: '0 6px' },
151
- }}
152
- />
153
- )}
154
- </Box>
155
-
156
- <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
157
- {baseChips.map((chip, index) => (
158
- <Chip
159
- key={index}
160
- label={chip}
161
- size="small"
162
- variant="outlined"
163
- sx={{
164
- height: 20,
165
- fontSize: '0.7rem',
166
- maxWidth: '100%',
167
- '& .MuiChip-label': {
168
- padding: '0 6px',
169
- overflow: 'hidden',
170
- textOverflow: 'ellipsis',
171
- },
172
- }}
173
- />
174
- ))}
175
- </Box>
176
- </Box>
177
- </Card>
178
- );
179
- };
180
-
181
- export const JsonEdit: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
182
- const { method, attribute, resourceConfig } = props;
183
-
184
- // AI theme generation is opt-in: settable per-field via componentProps.aiThemeEnabled
185
- const aiThemeEnabled: boolean =
186
- (props as any).aiThemeEnabled ??
187
- (attribute as any)?.componentProps?.aiThemeEnabled ??
188
- JSON_COLOR_SELECTOR_AI_ENABLED_DEFAULT;
189
-
190
- const record = useRecordContext();
191
- const { setValue, getValues } = useFormContext();
192
-
193
- const attributePath = attribute.attribute;
194
- const isNestedSetting = attributePath.startsWith('settings.');
195
- const settingsPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
196
-
197
- const [viewMode, setViewMode] = useState<'basic' | 'advanced'>('basic');
198
- const [pairs, setPairs] = useState<KeyValuePair[]>([]);
199
- const [searchTerm, setSearchTerm] = useState<string>('');
200
- const [selectedMode, setSelectedMode] = useState<string>('');
201
- const [isLoading, setIsLoading] = useState<boolean>(true);
202
- const [isRendering, setIsRendering] = useState<boolean>(true);
203
- const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
204
- const [editingPair, setEditingPair] = useState<KeyValuePair | null>(null);
205
- const [extractedColors, setExtractedColors] = useState<number[][]>([]); // Add this state
206
-
207
- // Pagination state
208
- const [page, setPage] = useState<number>(1);
209
- const [pageSize] = useState<number>(50);
210
- const [showAll, setShowAll] = useState<boolean>(!JSON_COLOR_SELECTOR_PAGINATION_ENABLED);
211
-
212
- // File input ref for CSS import
213
- const fileInputRef = useRef<HTMLInputElement>(null);
214
-
215
- // Throttling refs
216
- const throttleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
217
- const pendingUpdatesRef = useRef<Map<string, { key: string; color: string }>>(new Map());
218
-
219
- // Dispatch loading events
220
- const dispatchLoadingEvent = useCallback((loading: boolean) => {
221
- window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: loading }));
222
- }, []);
223
-
224
- const handleColorsExtracted = useCallback((colors: number[][]) => {
225
- setExtractedColors(colors);
226
- }, []);
227
-
228
- // Extract available modes from pairs
229
- const availableModes = useMemo(() => {
230
- return extractAvailableModes(pairs);
231
- }, [pairs]);
232
-
233
- // Filter pairs based on search term and selected mode
234
- const filteredPairs = useMemo(() => {
235
- let filtered = pairs;
236
-
237
- // Filter by search term
238
- if (searchTerm.trim()) {
239
- filtered = filtered.filter(pair =>
240
- pair.key.toLowerCase().includes(searchTerm.toLowerCase())
241
- );
242
- }
243
-
244
- // Filter by selected mode
245
- if (selectedMode) {
246
- filtered = filtered.filter(pair => {
247
- const { mode } = parseColorKey(pair.key);
248
- return mode === selectedMode;
249
- });
250
- }
251
-
252
- return filtered;
253
- }, [pairs, searchTerm, selectedMode]);
254
-
255
- // Export colors as CSS variables
256
- const handleExportCSS = useCallback(() => {
257
- try {
258
- const cssContent = generateCSSContent(pairs);
259
- const blob = new Blob([cssContent], { type: 'text/css' });
260
- const url = URL.createObjectURL(blob);
261
-
262
- const link = document.createElement('a');
263
- link.href = url;
264
- link.download = 'color-variables.css';
265
- document.body.appendChild(link);
266
- link.click();
267
- document.body.removeChild(link);
268
- URL.revokeObjectURL(url);
269
-
270
- console.log('CSS variables exported successfully');
271
- } catch (error) {
272
- console.error('Error exporting CSS variables:', error);
273
- }
274
- }, [pairs]);
275
-
276
- // Import colors from CSS file
277
- const handleImportCSS = useCallback(() => {
278
- fileInputRef.current?.click();
279
- }, []);
280
-
281
- // Handle file selection for CSS import
282
- const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
283
- const file = event.target.files?.[0];
284
- if (!file) return;
285
-
286
- const reader = new FileReader();
287
- reader.onload = (e) => {
288
- try {
289
- const cssContent = e.target?.result as string;
290
- const importedPairs = parseCSSContent(cssContent);
291
-
292
- if (importedPairs.length > 0) {
293
- // Merge with existing pairs, with imported pairs taking precedence
294
- const mergedPairs = mergePairs(pairs, importedPairs);
295
- setPairs(mergedPairs);
296
- updateFormValue(mergedPairs);
297
- console.log(`Imported ${importedPairs.length} color variables from CSS`);
298
- } else {
299
- console.warn('No valid CSS variables found in the imported file');
300
- }
301
- } catch (error) {
302
- console.error('Error importing CSS file:', error);
303
- }
304
- };
305
-
306
- reader.readAsText(file);
307
- // Reset file input
308
- event.target.value = '';
309
- }, [pairs]);
310
-
311
- // Generate CSS content from pairs
312
- const generateCSSContent = (colorPairs: KeyValuePair[]): string => {
313
- const cssLines: string[] = [
314
- '/* Color Variables - Generated by JsonColorSelector */',
315
- '/* Import this file or copy these variables to your CSS */',
316
- '',
317
- ':root {'
318
- ];
319
-
320
- // Group by mode for better organization
321
- const groupedPairs = colorPairs.reduce((acc, pair) => {
322
- if (!pair.key.trim()) return acc;
323
-
324
- const { mode } = parseColorKey(pair.key);
325
- const groupKey = mode || 'default';
326
-
327
- if (!acc[groupKey]) {
328
- acc[groupKey] = [];
329
- }
330
- acc[groupKey].push(pair);
331
- return acc;
332
- }, {} as Record<string, KeyValuePair[]>);
333
-
334
- // Add CSS variables grouped by mode
335
- Object.entries(groupedPairs).forEach(([mode, modePairs]) => {
336
- if (modePairs.length > 0) {
337
- cssLines.push(` /* ${mode} mode colors */`);
338
- modePairs.forEach(pair => {
339
- const cssVarName = pair.key.startsWith('--') ? pair.key : `--${pair.key}`;
340
- cssLines.push(` ${cssVarName}: ${pair.value};`);
341
- });
342
- cssLines.push('');
343
- }
344
- });
345
-
346
- cssLines.push('}');
347
- cssLines.push('');
348
- cssLines.push('/* Usage example: */');
349
- cssLines.push('/* .my-element { background-color: var(--primary-color); } */');
350
-
351
- return cssLines.join('\n');
352
- };
353
-
354
- // Parse CSS content to extract color variables
355
- const parseCSSContent = (cssContent: string): KeyValuePair[] => {
356
- const pairs: KeyValuePair[] = [];
357
-
358
- // Remove comments and normalize whitespace
359
- const cleanCSS = cssContent
360
- .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
361
- .replace(/\s+/g, ' ') // Normalize whitespace
362
- .trim();
363
-
364
- // Match CSS custom properties (variables)
365
- const variableRegex = /--([^:]+):\s*([^;]+);/g;
366
- let match;
367
-
368
- while ((match = variableRegex.exec(cleanCSS)) !== null) {
369
- const [, name, value] = match;
370
- const cleanName = name.trim();
371
- const cleanValue = value.trim();
372
-
373
- // Only import if it looks like a color value
374
- if (isColorValue(cleanValue)) {
375
- pairs.push({
376
- key: cleanName,
377
- value: cleanValue,
378
- id: generateId()
379
- });
380
- }
381
- }
382
-
383
- return pairs;
384
- };
385
-
386
- // Check if a value looks like a color
387
- const isColorValue = (value: string): boolean => {
388
- const colorPatterns = [
389
- /^#[0-9a-fA-F]{3,8}$/, // Hex colors
390
- /^rgb\(/i, // RGB colors
391
- /^rgba\(/i, // RGBA colors
392
- /^hsl\(/i, // HSL colors
393
- /^hsla\(/i, // HSLA colors
394
- /^(red|blue|green|yellow|purple|orange|pink|brown|black|white|gray|grey)$/i // Named colors
395
- ];
396
-
397
- return colorPatterns.some(pattern => pattern.test(value.trim()));
398
- };
399
-
400
- // Merge imported pairs with existing pairs
401
- const mergePairs = (existingPairs: KeyValuePair[], importedPairs: KeyValuePair[]): KeyValuePair[] => {
402
- const merged = [...existingPairs];
403
-
404
- importedPairs.forEach(importedPair => {
405
- const existingIndex = merged.findIndex(pair => pair.key === importedPair.key);
406
- if (existingIndex >= 0) {
407
- // Update existing pair
408
- merged[existingIndex] = { ...merged[existingIndex], value: importedPair.value };
409
- } else {
410
- // Add new pair
411
- merged.push(importedPair);
412
- }
413
- });
414
-
415
- return merged;
416
- };
417
-
418
- const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
419
- setSearchTerm(event.target.value);
420
- setPage(1);
421
- };
422
-
423
- const handleClearSearch = () => {
424
- setSearchTerm('');
425
- setPage(1);
426
- };
427
-
428
- const handleModeChange = (event: any) => {
429
- setSelectedMode(event.target.value);
430
- setPage(1);
431
- };
432
-
433
- const handleClearMode = () => {
434
- setSelectedMode('');
435
- setPage(1);
436
- };
437
-
438
- // Initial data loading effect
439
- useEffect(() => {
440
- const loadInitialData = async () => {
441
- setIsLoading(true);
442
- dispatchLoadingEvent(true);
443
-
444
- try {
445
- let initialValue: Record<string, any> = {};
446
- let defaultValues: Record<string, any> = {};
447
-
448
- try {
449
- defaultValues = attribute?.default_value || {};
450
- } catch (e) {
451
- defaultValues = {};
452
- }
453
-
454
- if (isNestedSetting && record?.settings) {
455
- const settings = record.settings || {};
456
- initialValue = { ...defaultValues, ...getNestedValue(settings, settingsPath) };
457
- } else if (record?.[attributePath]) {
458
- initialValue = { ...defaultValues, ...record[attributePath] };
459
- } else {
460
- initialValue = defaultValues;
461
- }
462
-
463
- const initialPairs = Object.entries(initialValue).map(([key, value]) => ({
464
- key,
465
- value: String(value),
466
- id: generateId()
467
- }));
468
-
469
- setPairs(initialPairs);
470
-
471
- // Simulate processing time for large datasets
472
- if (initialPairs.length > 50) {
473
- await new Promise(resolve => setTimeout(resolve, 100));
474
- }
475
-
476
- } catch (error) {
477
- console.error('Error loading initial data:', error);
478
- } finally {
479
- setIsLoading(false);
480
- }
481
- };
482
-
483
- loadInitialData();
484
- }, [record, attributePath]);
485
-
486
- // Rendering completion effect
487
- useEffect(() => {
488
- if (!isLoading && pairs.length >= 0) {
489
- setIsRendering(true);
490
-
491
- requestAnimationFrame(() => {
492
- requestAnimationFrame(() => {
493
- setIsRendering(false);
494
- dispatchLoadingEvent(false);
495
- });
496
- });
497
- }
498
- }, [isLoading, pairs.length, dispatchLoadingEvent]);
499
-
500
- // Cleanup throttle timeout on unmount
501
- useEffect(() => {
502
- return () => {
503
- if (throttleTimeoutRef.current) {
504
- clearTimeout(throttleTimeoutRef.current);
505
- }
506
- dispatchLoadingEvent(false);
507
- };
508
- }, [dispatchLoadingEvent]);
509
-
510
- const getNestedValue = (obj: any, path: string): any => {
511
- if (!path) return obj;
512
- const keys = path.split('.');
513
- return keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : {}), obj);
514
- };
515
-
516
- const setNestedValue = (obj: any, path: string, value: any): any => {
517
- if (!path) return value;
518
-
519
- const keys = path.split('.');
520
- const lastKey = keys.pop();
521
- const lastObj = keys.reduce((o, key) => {
522
- if (o[key] === undefined) o[key] = {};
523
- return o[key];
524
- }, obj);
525
-
526
- if (lastKey) {
527
- lastObj[lastKey] = value;
528
- }
529
- return obj;
530
- };
531
-
532
- const generateId = (): string => {
533
- return Math.random().toString(36).substring(2, 11);
534
- };
535
-
536
- const addPair = () => {
537
- const newPair = { key: '', value: '#000000', id: generateId() };
538
- setEditingPair(newPair);
539
- setEditDialogOpen(true);
540
- };
541
-
542
- const handleEditPair = (pair: KeyValuePair) => {
543
- setEditingPair(pair);
544
- setEditDialogOpen(true);
545
- };
546
-
547
- const handleSavePair = (editedPair: KeyValuePair) => {
548
- const existingIndex = pairs.findIndex(p => p.id === editedPair.id);
549
- let updatedPairs: KeyValuePair[];
550
-
551
- if (existingIndex >= 0) {
552
- // Update existing pair
553
- updatedPairs = pairs.map(p => p.id === editedPair.id ? editedPair : p);
554
- } else {
555
- // Add new pair
556
- updatedPairs = [...pairs, editedPair];
557
- }
558
-
559
- setPairs(updatedPairs);
560
- updateFormValue(updatedPairs);
561
- updateSingleDomColor(editedPair.key, editedPair.value);
562
- };
563
-
564
- const handleDeletePair = (id: string) => {
565
- const updatedPairs = pairs.filter(pair => pair.id !== id);
566
- setPairs(updatedPairs);
567
- updateFormValue(updatedPairs);
568
- };
569
-
570
- // Handle colors update from ImageColorExtractor
571
- const handleColorsUpdate = (newPairs: KeyValuePair[]) => {
572
- setPairs(newPairs);
573
- updateFormValue(newPairs);
574
- };
575
-
576
- // ------------------------------------------------------------------
577
- // Basic mode: 5 base colors per mode, full brand palette derived locally
578
- // ------------------------------------------------------------------
579
-
580
- const colorsRecord = useMemo(() => {
581
- return pairs.reduce((acc, pair) => {
582
- if (pair.key.trim()) acc[pair.key] = pair.value;
583
- return acc;
584
- }, {} as Record<string, string>);
585
- }, [pairs]);
586
-
587
- const lightBases = useMemo(() => extractBases(colorsRecord, 'light'), [colorsRecord]);
588
- const darkBases = useMemo(() => extractBases(colorsRecord, 'dark'), [colorsRecord]);
589
-
590
- // Derive the brand palette from the given bases and merge it over current pairs.
591
- // Neutral greyscale and semantic alert keys are untouched by design.
592
- const applyBases = useCallback((light: BasicPalette, dark: BasicPalette) => {
593
- const derived = deriveBrandPairs(light, dark);
594
- const derivedPairs: KeyValuePair[] = Object.entries(derived).map(([key, value]) => ({
595
- key,
596
- value,
597
- id: generateId(),
598
- }));
599
- const merged = mergePairs(pairs, derivedPairs);
600
- setPairs(merged);
601
- updateFormValue(merged);
602
- }, [pairs]);
603
-
604
- const handleBaseChange = useCallback((mode: ThemeMode, key: keyof BasicPalette, color: string) => {
605
- const nextLight = mode === 'light' ? { ...lightBases, [key]: color } : lightBases;
606
- const nextDark = mode === 'dark' ? { ...darkBases, [key]: color } : darkBases;
607
- applyBases(nextLight, nextDark);
608
- }, [lightBases, darkBases, applyBases]);
609
-
610
- // Local (no-AI) image palette → base colors prefill
611
- const handleApplyLocalPalette = useCallback((palette: number[][]) => {
612
- if (!palette || palette.length === 0) return;
613
- const { light, dark } = basesFromImagePalette(palette);
614
- applyBases(light, dark);
615
- }, [applyBases]);
616
-
617
- // Update only a single DOM color property
618
- const updateSingleDomColor = useCallback((key: string, color: string) => {
619
- if (!key.trim()) return;
620
-
621
- requestAnimationFrame(() => {
622
- const currentTheme = document.documentElement.getAttribute('data-theme') || '';
623
- const themeSuffix = currentTheme ? `--${currentTheme}` : '';
624
-
625
- if (themeSuffix && key.endsWith(themeSuffix)) {
626
- const baseKey = key.slice(0, -themeSuffix.length);
627
- document.documentElement.style.setProperty(`--${baseKey}`, color);
628
- document.documentElement.style.setProperty(`--${baseKey}${themeSuffix}`, color);
629
- } else {
630
- // Fallback: update the variable directly if it doesn't match the current theme suffix
631
- // This handles global variables or keys being edited that don't adhere to the specific suffix
632
- const varName = key.startsWith('--') ? key : `--${key}`;
633
- document.documentElement.style.setProperty(varName, color);
634
- }
635
- });
636
- }, []);
637
-
638
- // Batch update for preview only - processes all colors at once
639
- const updateDomColors = (colors: Record<string, any>) => {
640
- const currentTheme = document.documentElement.getAttribute('data-theme');
641
- const themeSuffix = `--${currentTheme}`;
642
-
643
- requestAnimationFrame(() => {
644
- Object.entries(colors).forEach(([key, value]) => {
645
- if (key.endsWith(themeSuffix)) {
646
- const baseKey = key.slice(0, -themeSuffix.length);
647
- document.documentElement.style.setProperty(`--${baseKey}`, String(value));
648
- }
649
- });
650
- });
651
- };
652
-
653
- const handlePreview = useCallback((m: string) => {
654
- const colorsObj = pairs.reduce((acc, pair) => {
655
- if (pair.key.trim()) {
656
- acc[pair.key] = pair.value;
657
- }
658
- return acc;
659
- }, {} as Record<string, any>);
660
-
661
- // Update CSS variables first
662
- updateDomCssVariables(m, colorsObj,getTenantSettingsValues());
663
- }, [pairs]); // Make sure pairs is in the dependency array
664
-
665
-
666
- const syncPairsFromForm = useCallback(() => {
667
- try {
668
- let currentFormValue: Record<string, any> = {};
669
-
670
- if (isNestedSetting) {
671
- const currentSettings = getValues('settings') || {};
672
- currentFormValue = getNestedValue(currentSettings, settingsPath) || {};
673
- } else {
674
- currentFormValue = getValues(attributePath) || {};
675
- }
676
-
677
- return currentFormValue;
678
- } catch (error) {
679
- console.error('Error syncing pairs from form values:', error);
680
- return {};
681
- }
682
- }, [getValues, isNestedSetting, settingsPath, attributePath]);
683
-
684
-
685
- const getTenantSettingsValues = () => {
686
- const persistedTenantSettings = AuthPersistenceService.getTenantSettings();
687
- if (persistedTenantSettings) {
688
- return persistedTenantSettings?.values || {};
689
- }
690
- return {}
691
- };
692
-
693
-
694
- useEffect(() => {
695
- const handleThemeSwitch = (event: CustomEvent<{ mode: string }>) => {
696
- console.log('Theme switch event received:', event.detail.mode);
697
-
698
- // Get current colors directly from form instead of relying on pairs state
699
- const currentColors = syncPairsFromForm();
700
-
701
- console.log('Current colors from form:', currentColors); // Debug log
702
-
703
- // Update CSS variables with current form values
704
- updateDomCssVariables(event.detail.mode, currentColors,getTenantSettingsValues());
705
- };
706
-
707
- window.addEventListener('dash-theme-mode-switched', handleThemeSwitch as EventListener);
708
-
709
- return () => {
710
- window.removeEventListener('dash-theme-mode-switched', handleThemeSwitch as EventListener);
711
- };
712
- }, [syncPairsFromForm]);
713
-
714
-
715
- const updateFormValue = (currentPairs: KeyValuePair[]) => {
716
- const obj = currentPairs.reduce((acc, pair) => {
717
- if (pair.key.trim()) {
718
- acc[pair.key] = pair.value;
719
- }
720
- return acc;
721
- }, {} as Record<string, any>);
722
-
723
- if (isNestedSetting) {
724
- const currentSettings = getValues('settings') || {};
725
- const updatedSettings = setNestedValue({ ...currentSettings }, settingsPath, obj);
726
- setValue('settings', updatedSettings, { shouldDirty: true });
727
- } else {
728
- setValue(attributePath, obj, { shouldDirty: true });
729
- }
730
- };
731
-
732
- // Show loading state while component is initializing or rendering
733
- if (isLoading || isRendering) {
734
- return (
735
- <Box sx={{ mt: 1, mb: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
736
- <Typography variant="body2" color="textSecondary">
737
- {isLoading ? 'Loading color settings...' : 'Rendering colors...'}
738
- </Typography>
739
- </Box>
740
- );
741
- }
742
-
743
- return (
744
- <Box sx={{ mt: 1, mb: 2 }}>
745
-
746
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2, flexWrap: 'wrap', gap: 1 }}>
747
- <Typography variant="subtitle1">
748
- {attribute.label || 'Color Palette'}
749
- </Typography>
750
- <ToggleButtonGroup
751
- value={viewMode}
752
- exclusive
753
- size="small"
754
- onChange={(_, value) => value && setViewMode(value)}
755
- >
756
- <ToggleButton value="basic">Basic</ToggleButton>
757
- <ToggleButton value="advanced">Advanced</ToggleButton>
758
- </ToggleButtonGroup>
759
- </Box>
760
-
761
- {/* Hidden file input for CSS import */}
762
- <input
763
- type="file"
764
- ref={fileInputRef}
765
- accept=".css"
766
- style={{ display: 'none' }}
767
- onChange={handleFileSelect}
768
- />
769
-
770
- {/* Image Color Extractor — always sits above the preview cards, so the
771
- workflow reads top-to-bottom: source image first, derived theme below. */}
772
- <ImageColorExtractor
773
- onColorsExtracted={handleColorsExtracted}
774
- onColorsUpdate={handleColorsUpdate}
775
- existingPairs={pairs}
776
- aiEnabled={aiThemeEnabled}
777
- onApplyLocal={handleApplyLocalPalette}
778
- />
779
-
780
- {/* Basic mode: 5 base colors per mode, brand palette derived locally */}
781
- {viewMode === 'basic' && (
782
- <>
783
- <BasicColorSelector
784
- lightBases={lightBases}
785
- darkBases={darkBases}
786
- onBaseChange={handleBaseChange}
787
- />
788
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mt: 2, flexWrap: 'wrap', gap: 1 }}>
789
- <Typography variant="caption" sx={{ color: 'text.secondary', maxWidth: 480 }}>
790
- Changing a base color regenerates the derived brand palette (sidebar, buttons, links, tables).
791
- Neutral backgrounds and alert colors are preserved — fine-tune any specific value in Advanced mode.
792
- </Typography>
793
- <Button
794
- startIcon={<PreviewIcon />}
795
- variant="contained"
796
- size="small"
797
- color="primary"
798
- onClick={() => handlePreview(document.documentElement.getAttribute('data-theme'))}
799
- >
800
- Preview Colors
801
- </Button>
802
- </Box>
803
- </>
804
- )}
805
-
806
- {viewMode === 'advanced' && (<>
807
- {/* Search and Filter Controls */}
808
- <Box sx={{ mb: 3, display: 'flex', gap: 2, alignItems: 'flex-end' }}>
809
- {/* Search Field */}
810
- <TextField
811
- fullWidth
812
- size="small"
813
- placeholder="Search colors by name..."
814
- value={searchTerm}
815
- onChange={handleSearchChange}
816
- slotProps={{
817
- input: {
818
- startAdornment: (
819
- <InputAdornment position="start">
820
- <SearchIcon />
821
- </InputAdornment>
822
- ),
823
- endAdornment: searchTerm && (
824
- <InputAdornment position="end">
825
- <IconButton
826
- size="small"
827
- onClick={handleClearSearch}
828
- edge="end"
829
- >
830
- <ClearIcon />
831
- </IconButton>
832
- </InputAdornment>
833
- ),
834
- }
835
- }}
836
- />
837
-
838
- {/* Mode Selector */}
839
- <FormControl size="small" sx={{ minWidth: 150 }}>
840
- <InputLabel>Mode</InputLabel>
841
- <Select
842
- value={selectedMode}
843
- label="Mode"
844
- onChange={handleModeChange}
845
- startAdornment={
846
- <InputAdornment position="start">
847
- <FilterListIcon fontSize="small" />
848
- </InputAdornment>
849
- }
850
- endAdornment={selectedMode && (
851
- <InputAdornment position="end">
852
- <IconButton
853
- size="small"
854
- onClick={handleClearMode}
855
- edge="end"
856
- sx={{ mr: 1 }}
857
- >
858
- <ClearIcon fontSize="small" />
859
- </IconButton>
860
- </InputAdornment>
861
- )}
862
- >
863
- <MenuItem value="">
864
- <em>All Modes</em>
865
- </MenuItem>
866
- {availableModes.map((mode) => (
867
- <MenuItem key={mode} value={mode}>
868
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
869
- <span>{getModeIcon(mode)}</span>
870
- <span>{mode}</span>
871
- </Box>
872
- </MenuItem>
873
- ))}
874
- </Select>
875
- </FormControl>
876
- </Box>
877
-
878
- {/* Header Actions */}
879
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3, flexWrap: 'wrap', gap: 1 }}>
880
- <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
881
- <Button
882
- startIcon={<PreviewIcon />}
883
- variant="contained"
884
- size="small"
885
- onClick={() => handlePreview(document.documentElement.getAttribute('data-theme'))}
886
- color="primary"
887
- >
888
- Preview Colors
889
- </Button>
890
- <Button
891
- startIcon={<DownloadIcon />}
892
- variant="outlined"
893
- size="small"
894
- onClick={handleExportCSS}
895
- color="secondary"
896
- disabled={pairs.length === 0}
897
- >
898
- Export CSS
899
- </Button>
900
- <Button
901
- startIcon={<UploadIcon />}
902
- variant="outlined"
903
- size="small"
904
- onClick={handleImportCSS}
905
- color="secondary"
906
- >
907
- Import CSS
908
- </Button>
909
- </Box>
910
- <Button
911
- startIcon={<AddIcon />}
912
- variant="outlined"
913
- size="small"
914
- onClick={addPair}
915
- >
916
- Add Color
917
- </Button>
918
- </Box>
919
-
920
- {/* Results count when filtering */}
921
- {(searchTerm || selectedMode) && (
922
- <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
923
- <Chip
924
- label={`${filteredPairs.length} of ${pairs.length} colors`}
925
- size="small"
926
- variant="outlined"
927
- />
928
- {selectedMode && (
929
- <Chip
930
- label={`Mode: ${selectedMode}`}
931
- size="small"
932
- variant="filled"
933
- color="primary"
934
- onDelete={handleClearMode}
935
- avatar={<span style={{ paddingLeft: 6 }}>{getModeIcon(selectedMode)}</span>}
936
- />
937
- )}
938
- {searchTerm && (
939
- <Chip
940
- label={`Search: "${searchTerm}"`}
941
- size="small"
942
- variant="filled"
943
- color="secondary"
944
- onDelete={handleClearSearch}
945
- />
946
- )}
947
- </Box>
948
- )}
949
-
950
- {/* Color Payload Grid with Pagination */}
951
- <Box sx={{ flexGrow: 1, my: 2 }}>
952
- {isRendering ? (
953
- <Box sx={{ display: 'flex', justifyContent: 'center', p: 4, alignItems: 'center', flexDirection: 'column', gap: 2 }}>
954
- <Typography>Rendering Grid...</Typography>
955
- </Box>
956
- ) : filteredPairs.length > 0 ? (
957
- <>
958
- <Box sx={{
959
- display: 'grid',
960
- gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
961
- gap: 2,
962
- width: '100%'
963
- }}>
964
- {(showAll
965
- ? filteredPairs
966
- : filteredPairs.slice((page - 1) * pageSize, page * pageSize)
967
- ).map((pair) => (
968
- <Box key={pair.id} sx={{ minWidth: 0 }}>
969
- <LocalColorPaletteItem
970
- pair={pair}
971
- onEdit={handleEditPair}
972
- onDelete={handleDeletePair}
973
- />
974
- </Box>
975
- ))}
976
- </Box>
977
-
978
- {/* Pagination Controls - only when pagination is enabled and not showing all */}
979
- {JSON_COLOR_SELECTOR_PAGINATION_ENABLED && !showAll && filteredPairs.length > pageSize && (
980
- <Box sx={{ mt: 4, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
981
- <Pagination
982
- count={Math.ceil(filteredPairs.length / pageSize)}
983
- page={page}
984
- onChange={(_, value) => setPage(value)}
985
- color="primary"
986
- size="large"
987
- showFirstButton
988
- showLastButton
989
- />
990
-
991
- {/* Show All button */}
992
- {JSON_COLOR_SELECTOR_PAGINATION_SHOW_ALL && (
993
- <Link
994
- component="button"
995
- variant="body2"
996
- underline="always"
997
- onClick={() => setShowAll(true)}
998
- sx={{ mt: 0.5, fontSize: '0.8rem', cursor: 'pointer' }}
999
- >
1000
- Show all
1001
- </Link>
1002
- )}
1003
- </Box>
1004
- )}
1005
-
1006
- <Typography variant="caption" sx={{ display: 'block', textAlign: 'center', mt: 2, color: 'text.secondary' }}>
1007
- {showAll
1008
- ? `Showing all ${filteredPairs.length} colors`
1009
- : `Showing ${Math.min((page - 1) * pageSize + 1, filteredPairs.length)} - ${Math.min(page * pageSize, filteredPairs.length)} of ${filteredPairs.length} colors`
1010
- }
1011
- </Typography>
1012
-
1013
- {/* Show paginated link when showing all and pagination is enabled */}
1014
- {JSON_COLOR_SELECTOR_PAGINATION_ENABLED && showAll && filteredPairs.length > pageSize && (
1015
- <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
1016
- <Link
1017
- component="button"
1018
- variant="body2"
1019
- underline="always"
1020
- onClick={() => { setShowAll(false); setPage(1); }}
1021
- sx={{ fontSize: '0.8rem', cursor: 'pointer' }}
1022
- >
1023
- Show paginated
1024
- </Link>
1025
- </Box>
1026
- )}
1027
- </>
1028
- ) : (
1029
- <Box sx={{
1030
- p: 4,
1031
- textAlign: 'center',
1032
- bgcolor: 'background.paper',
1033
- borderRadius: 1,
1034
- border: '1px dashed #ccc'
1035
- }}>
1036
- <Typography color="textSecondary">
1037
- No colors found using current filters.
1038
- </Typography>
1039
- <Box sx={{ mt: 1 }}>
1040
- <Link
1041
- component="button"
1042
- variant="body2"
1043
- onClick={() => {
1044
- setSearchTerm('');
1045
- setSelectedMode('');
1046
- setPage(1);
1047
- }}
1048
- >
1049
- Clear filters
1050
- </Link>
1051
- </Box>
1052
- </Box>
1053
- )}
1054
- </Box>
1055
-
1056
-
1057
- {/* Show message when no colors exist */}
1058
- {pairs.length === 0 && (
1059
- <Box sx={{ textAlign: 'center', py: 4 }}>
1060
- <Typography variant="body2" color="textSecondary" gutterBottom>
1061
- No colors in your palette yet
1062
- </Typography>
1063
- <Button
1064
- startIcon={<AddIcon />}
1065
- variant="contained"
1066
- onClick={addPair}
1067
- >
1068
- Add Your First Color
1069
- </Button>
1070
- </Box>
1071
- )}
1072
- </>)}
1073
-
1074
- {/* Edit Dialog - Updated with extracted colors */}
1075
- <ColorEditDialog
1076
- open={editDialogOpen}
1077
- pair={editingPair}
1078
- onClose={() => {
1079
- setEditDialogOpen(false);
1080
- setEditingPair(null);
1081
- }}
1082
- onSave={handleSavePair}
1083
- onDelete={handleDeletePair}
1084
- existingKeys={pairs.map(p => p.key)}
1085
- updateSingleDomColor={updateSingleDomColor}
1086
- extractedColors={extractedColors} // Pass extracted colors
1087
- />
1088
- </Box>
1089
- );
1090
- };
1091
-
1092
-
1093
- const JsonColorSelector = ({ method, attribute, resourceConfig }: IDashAutoAdminCustomFieldComponent) => {
1094
- const [componentLoading, setComponentLoading] = useState(true);
1095
-
1096
- // Dispatch loading event when component mounts
1097
- useEffect(() => {
1098
- window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: true }));
1099
-
1100
- // Set a small timeout to allow component to initialize
1101
- const timer = setTimeout(() => {
1102
- setComponentLoading(false);
1103
- }, 50);
1104
-
1105
- return () => {
1106
- clearTimeout(timer);
1107
- // Ensure loading state is cleared on unmount
1108
- window.dispatchEvent(new MessageEvent('auto-admin-loading-state', { data: false }));
1109
- };
1110
- }, []);
1111
-
1112
- // Show initial loading state
1113
- if (componentLoading) {
1114
- return (
1115
- <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 100 }}>
1116
- <Typography variant="body2" color="textSecondary">
1117
- Initializing color palette...
1118
- </Typography>
1119
- </Box>
1120
- );
1121
- }
1122
-
1123
- switch (method) {
1124
- case 'edit':
1125
- return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1126
- case 'create':
1127
- return <JsonEdit attribute={attribute} method={method} resourceConfig={resourceConfig} />;
1128
- case 'view':
1129
- case 'list':
1130
- default:
1131
- return <>Not implemented</>;
1132
- }
1133
- };
1134
-
1135
- export default JsonColorSelector;