@dashadmin/dash-components 1.3.25 → 1.3.28
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/README.md +35 -0
- package/dist/components/AuditLog/AuditLog.js +345 -1
- package/dist/components/AuditLog/index.js +4 -1
- package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
- package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
- package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
- package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
- package/dist/components/Json/Json.js +235 -1
- package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
- package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
- package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
- package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
- package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
- package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
- package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
- package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
- package/dist/components/ListActive/ListActive.js +126 -1
- package/dist/components/ListActive/ListBoolean.js +124 -1
- package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
- package/dist/components/NotificationPreferences/index.js +7 -1
- package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
- package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
- package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
- package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
- package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
- package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
- package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
- package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
- package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
- package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
- package/dist/components/Upload/SingleImageUploader.js +40 -1
- package/dist/components/custom/PackageCopyMethod.js +78 -1
- package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
- package/dist/components/dialog/Basic.js +8 -1
- package/dist/components/dialog/Correct.js +14 -1
- package/dist/components/dialog/Error.js +14 -1
- package/dist/components/dialog/Info.js +14 -1
- package/dist/components/dialog/InfoV2.js +21 -1
- package/dist/components/dialog/NotFound.js +14 -1
- package/dist/components/dialog/QuickSearch.js +117 -1
- package/dist/components/notifications/NotificationsCenter.js +597 -1
- package/dist/components/notifications/index.js +24 -1
- package/dist/components/tables/PaginationComponent.js +36 -1
- package/dist/components/theme/AppLoadingFallback.js +38 -2
- package/dist/components/theme/ErrorBoundary.js +52 -1
- package/dist/components/theme/InitialLoader.js +13 -1
- package/dist/components/theme/NotFound.js +136 -1
- package/dist/components/theme/NotResults.js +38 -1
- package/dist/hooks/useDraggable.js +6 -1
- package/dist/hooks/useNotifications.js +8 -1
- package/dist/hooks/useQuickSearch.js +26 -1
- package/dist/index.js +66 -5567
- package/dist/utils/setNativeValue.js +17 -1
- package/package.json +148 -132
- package/src/components/AuditLog/AuditLog.tsx +476 -0
- package/src/components/AuditLog/index.ts +2 -0
- package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
- package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
- package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
- package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
- package/src/components/Json/Json.tsx +312 -0
- package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
- package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
- package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
- package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
- package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
- package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
- package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
- package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
- package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
- package/src/components/ListActive/ListActive.tsx +106 -0
- package/src/components/ListActive/ListBoolean.tsx +96 -0
- package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
- package/src/components/NotificationPreferences/index.tsx +2 -0
- package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
- package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
- package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
- package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
- package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
- package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
- package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
- package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
- package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
- package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
- package/src/components/Upload/SingleImageUploader.tsx +55 -0
- package/src/components/custom/PackageCopyMethod.tsx +109 -0
- package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
- package/src/components/dialog/Basic.tsx +11 -0
- package/src/components/dialog/Correct.tsx +16 -0
- package/src/components/dialog/Error.tsx +15 -0
- package/src/components/dialog/Info.tsx +15 -0
- package/src/components/dialog/InfoV2.tsx +35 -0
- package/src/components/dialog/NotFound.tsx +18 -0
- package/src/components/dialog/QuickSearch.tsx +115 -0
- package/src/components/notifications/NotificationsCenter.tsx +792 -0
- package/src/components/notifications/index.ts +42 -0
- package/src/components/tables/PaginationComponent.tsx +17 -0
- package/src/components/theme/AppLoadingFallback.tsx +42 -0
- package/src/components/theme/ErrorBoundary.tsx +57 -0
- package/src/components/theme/InitialLoader.tsx +16 -0
- package/src/components/theme/NotFound.tsx +124 -0
- package/src/components/theme/NotResults.tsx +54 -0
- package/src/hooks/useDraggable.ts +11 -0
- package/src/hooks/useNotifications.ts +14 -0
- package/src/hooks/useQuickSearch.tsx +29 -0
- package/src/index.tsx +77 -0
- package/src/utils/setNativeValue.tsx +23 -0
- package/dist/color-thief-CTwGFhOB.js +0 -272
- /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
- /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
- /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
- /package/{dist → src}/styles/index.less +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
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 { useAsyncColorThief } from '../useAsyncColorThief';
|
|
22
|
+
import { getContrastColor, rgbArrayToHex } from '../helpers/functions';
|
|
23
|
+
import { KeyValuePair } from '../interfaces/interfaces';
|
|
24
|
+
import { useAxios } from 'dash-axios-hook';
|
|
25
|
+
|
|
26
|
+
const ImageColorExtractor = memo<{
|
|
27
|
+
onColorsExtracted: (colors: number[][]) => void;
|
|
28
|
+
onColorsUpdate?: (newPairs: KeyValuePair[]) => void;
|
|
29
|
+
existingPairs?: KeyValuePair[];
|
|
30
|
+
}>(({ onColorsExtracted, onColorsUpdate, existingPairs = [] }) => {
|
|
31
|
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
32
|
+
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
|
33
|
+
const [themePrompt, setThemePrompt] = useState<string>('');
|
|
34
|
+
const [isGeneratingTheme, setIsGeneratingTheme] = useState<boolean>(false);
|
|
35
|
+
const [themeError, setThemeError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
// Add state to preserve extracted colors independently
|
|
38
|
+
const [extractedPalette, setExtractedPalette] = useState<number[][]>([]);
|
|
39
|
+
|
|
40
|
+
const axios = useAxios();
|
|
41
|
+
const { dominantColor, palette, loading, error } = useAsyncColorThief(imageUrl, {
|
|
42
|
+
colorCount: 8,
|
|
43
|
+
quality: 10
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Update internal extracted palette state
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (palette && palette.length > 0) {
|
|
49
|
+
setExtractedPalette(palette);
|
|
50
|
+
onColorsExtracted(palette);
|
|
51
|
+
}
|
|
52
|
+
}, [palette, onColorsExtracted]);
|
|
53
|
+
|
|
54
|
+
// Handle file selection for upload
|
|
55
|
+
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
56
|
+
const file = event.target.files?.[0];
|
|
57
|
+
if (file) {
|
|
58
|
+
// Validate file type
|
|
59
|
+
if (!file.type.startsWith('image/')) {
|
|
60
|
+
alert('Please select a valid image file');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate file size (e.g., max 5MB)
|
|
65
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
66
|
+
alert('File size must be less than 5MB');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setUploadedFile(file);
|
|
71
|
+
|
|
72
|
+
// Create preview URL
|
|
73
|
+
const previewUrl = URL.createObjectURL(file);
|
|
74
|
+
setImageUrl(previewUrl);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Handle image removal
|
|
79
|
+
const handleImageRemove = () => {
|
|
80
|
+
if (imageUrl && imageUrl.startsWith('blob:')) {
|
|
81
|
+
URL.revokeObjectURL(imageUrl);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setImageUrl(null);
|
|
85
|
+
setUploadedFile(null);
|
|
86
|
+
setExtractedPalette([]); // Clear internal palette state
|
|
87
|
+
onColorsExtracted([]); // Clear extracted colors
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Generate theme using AI - Modified to merge with existing colors
|
|
91
|
+
const handleGenerateTheme = useCallback(async () => {
|
|
92
|
+
if (!extractedPalette || extractedPalette.length === 0) {
|
|
93
|
+
setThemeError('Please extract colors from an image first');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setIsGeneratingTheme(true);
|
|
98
|
+
setThemeError(null);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await axios.post('/tenant/tenant/settings/theme-generator', {
|
|
102
|
+
colors: extractedPalette,
|
|
103
|
+
prompt: themePrompt,
|
|
104
|
+
output_format: 'json'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (response.status !== 200) {
|
|
108
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = response.data;
|
|
112
|
+
|
|
113
|
+
if (result.success && result.data) {
|
|
114
|
+
// Convert the generated theme to KeyValuePair format
|
|
115
|
+
const newPairs: KeyValuePair[] = Object.entries(result.data).map(([key, value]) => ({
|
|
116
|
+
key,
|
|
117
|
+
value: String(value),
|
|
118
|
+
id: Math.random().toString(36).substring(2, 11)
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// Merge with existing pairs instead of replacing them
|
|
122
|
+
const mergedPairs = [...existingPairs];
|
|
123
|
+
|
|
124
|
+
newPairs.forEach(newPair => {
|
|
125
|
+
const existingIndex = mergedPairs.findIndex(existing => existing.key === newPair.key);
|
|
126
|
+
if (existingIndex >= 0) {
|
|
127
|
+
// Update existing pair
|
|
128
|
+
mergedPairs[existingIndex] = newPair;
|
|
129
|
+
} else {
|
|
130
|
+
// Add new pair
|
|
131
|
+
mergedPairs.push(newPair);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Update the colors in the parent component
|
|
136
|
+
if (onColorsUpdate) {
|
|
137
|
+
onColorsUpdate(mergedPairs);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
throw new Error(result.message || 'Failed to generate theme');
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Theme generation error:', error);
|
|
144
|
+
setThemeError(error instanceof Error ? error.message : 'Failed to generate theme');
|
|
145
|
+
} finally {
|
|
146
|
+
setIsGeneratingTheme(false);
|
|
147
|
+
}
|
|
148
|
+
}, [extractedPalette, themePrompt, existingPairs, onColorsUpdate, axios]);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Box sx={{ mt: 3, p: 3, borderRadius: 2 }}>
|
|
152
|
+
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
153
|
+
<PaletteIcon />
|
|
154
|
+
AI Theme Generator
|
|
155
|
+
</Typography>
|
|
156
|
+
|
|
157
|
+
<Divider sx={{ mb: 3 }} />
|
|
158
|
+
|
|
159
|
+
{/* Image Upload Section */}
|
|
160
|
+
<Box sx={{ mb: 3 }}>
|
|
161
|
+
{!imageUrl ? (
|
|
162
|
+
<Box sx={{
|
|
163
|
+
textAlign: 'center',
|
|
164
|
+
py: 4,
|
|
165
|
+
backgroundColor: 'rgba(0,0,0,0.02)',
|
|
166
|
+
borderRadius: 1,
|
|
167
|
+
border: '2px dashed #e0e0e0'
|
|
168
|
+
}}>
|
|
169
|
+
<Input
|
|
170
|
+
type="file"
|
|
171
|
+
inputProps={{ accept: 'image/*' }}
|
|
172
|
+
onChange={handleFileSelect}
|
|
173
|
+
sx={{ display: 'none' }}
|
|
174
|
+
id="color-extractor-upload"
|
|
175
|
+
/>
|
|
176
|
+
<Typography variant="body1" color="textSecondary" gutterBottom>
|
|
177
|
+
Upload an Image to Extract Colors
|
|
178
|
+
</Typography>
|
|
179
|
+
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
|
180
|
+
Select an image file and we'll automatically extract a color palette
|
|
181
|
+
</Typography>
|
|
182
|
+
<label htmlFor="color-extractor-upload">
|
|
183
|
+
<Button
|
|
184
|
+
variant="contained"
|
|
185
|
+
component="span"
|
|
186
|
+
startIcon={<CloudUploadIcon />}
|
|
187
|
+
size="large"
|
|
188
|
+
>
|
|
189
|
+
Choose Image
|
|
190
|
+
</Button>
|
|
191
|
+
</label>
|
|
192
|
+
</Box>
|
|
193
|
+
) : (
|
|
194
|
+
<Card sx={{ maxWidth: 400, mb: 3 }}>
|
|
195
|
+
<Box sx={{ position: 'relative' }}>
|
|
196
|
+
<CardMedia
|
|
197
|
+
component="img"
|
|
198
|
+
height="200"
|
|
199
|
+
image={imageUrl}
|
|
200
|
+
alt="Uploaded image"
|
|
201
|
+
sx={{ objectFit: 'contain' }}
|
|
202
|
+
/>
|
|
203
|
+
<IconButton
|
|
204
|
+
sx={{
|
|
205
|
+
position: 'absolute',
|
|
206
|
+
top: 8,
|
|
207
|
+
right: 8,
|
|
208
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
209
|
+
'&:hover': {
|
|
210
|
+
backgroundColor: 'rgba(255, 255, 255, 1)',
|
|
211
|
+
},
|
|
212
|
+
}}
|
|
213
|
+
onClick={handleImageRemove}
|
|
214
|
+
size="small"
|
|
215
|
+
>
|
|
216
|
+
<DeleteIcon />
|
|
217
|
+
</IconButton>
|
|
218
|
+
</Box>
|
|
219
|
+
</Card>
|
|
220
|
+
)}
|
|
221
|
+
</Box>
|
|
222
|
+
|
|
223
|
+
{/* Color Extraction Status */}
|
|
224
|
+
{loading && (
|
|
225
|
+
<Box display="flex" alignItems="center" gap={2} sx={{ mb: 3, p: 2, backgroundColor: 'rgba(25, 118, 210, 0.04)', borderRadius: 1 }}>
|
|
226
|
+
<CircularProgress size={20} />
|
|
227
|
+
<Typography>Extracting colors from image...</Typography>
|
|
228
|
+
</Box>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{error && (
|
|
232
|
+
<Alert severity="error" sx={{ mb: 3 }}>
|
|
233
|
+
{error}
|
|
234
|
+
</Alert>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{themeError && (
|
|
238
|
+
<Alert severity="error" sx={{ mb: 3 }}>
|
|
239
|
+
{themeError}
|
|
240
|
+
</Alert>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Extracted Colors Display - Use internal state */}
|
|
244
|
+
{extractedPalette && extractedPalette.length > 0 && !loading && (
|
|
245
|
+
<Box sx={{ mb: 3 }}>
|
|
246
|
+
<Typography variant="subtitle1" gutterBottom>
|
|
247
|
+
Extracted Colors ({extractedPalette.length} colors found)
|
|
248
|
+
</Typography>
|
|
249
|
+
|
|
250
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2, mb: 3 }}>
|
|
251
|
+
{extractedPalette.map((color, index) => {
|
|
252
|
+
const hexColor = rgbArrayToHex(color);
|
|
253
|
+
const isDominant = dominantColor &&
|
|
254
|
+
color[0] === dominantColor[0] &&
|
|
255
|
+
color[1] === dominantColor[1] &&
|
|
256
|
+
color[2] === dominantColor[2];
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<Box
|
|
260
|
+
key={index}
|
|
261
|
+
sx={{
|
|
262
|
+
width: 50,
|
|
263
|
+
height: 50,
|
|
264
|
+
backgroundColor: hexColor,
|
|
265
|
+
borderRadius: '50%',
|
|
266
|
+
display: 'flex',
|
|
267
|
+
alignItems: 'center',
|
|
268
|
+
justifyContent: 'center',
|
|
269
|
+
border: isDominant ? '3px solid #4caf50' : '2px solid #ccc',
|
|
270
|
+
cursor: 'pointer',
|
|
271
|
+
transition: 'transform 0.2s',
|
|
272
|
+
'&:hover': {
|
|
273
|
+
transform: 'scale(1.1)',
|
|
274
|
+
},
|
|
275
|
+
}}
|
|
276
|
+
title={`${hexColor} ${isDominant ? '(Dominant)' : ''}`}
|
|
277
|
+
>
|
|
278
|
+
<Typography
|
|
279
|
+
variant="caption"
|
|
280
|
+
sx={{
|
|
281
|
+
color: getContrastColor(hexColor),
|
|
282
|
+
fontWeight: 'bold',
|
|
283
|
+
fontSize: '0.8rem',
|
|
284
|
+
}}
|
|
285
|
+
>
|
|
286
|
+
{index + 1}
|
|
287
|
+
</Typography>
|
|
288
|
+
</Box>
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
291
|
+
</Box>
|
|
292
|
+
|
|
293
|
+
{/* Theme Generation Controls */}
|
|
294
|
+
<Box sx={{ mb: 3 }}>
|
|
295
|
+
<TextField
|
|
296
|
+
fullWidth
|
|
297
|
+
multiline
|
|
298
|
+
rows={2}
|
|
299
|
+
placeholder="Optional: Describe the theme style you want (e.g., 'modern and minimalist', 'warm and cozy', 'professional corporate')"
|
|
300
|
+
value={themePrompt}
|
|
301
|
+
onChange={(e) => setThemePrompt(e.target.value)}
|
|
302
|
+
sx={{ mb: 2 }}
|
|
303
|
+
label="Theme Description (Optional)"
|
|
304
|
+
/>
|
|
305
|
+
|
|
306
|
+
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
|
307
|
+
<Button
|
|
308
|
+
variant="contained"
|
|
309
|
+
startIcon={isGeneratingTheme ? <CircularProgress size={16} /> : <AutoFixHighIcon />}
|
|
310
|
+
onClick={handleGenerateTheme}
|
|
311
|
+
disabled={isGeneratingTheme}
|
|
312
|
+
color="primary"
|
|
313
|
+
>
|
|
314
|
+
{isGeneratingTheme ? 'Generating...' : 'Generate AI Theme'}
|
|
315
|
+
</Button>
|
|
316
|
+
</Box>
|
|
317
|
+
</Box>
|
|
318
|
+
|
|
319
|
+
<Typography variant="body2" color="textSecondary" sx={{ fontStyle: 'italic' }}>
|
|
320
|
+
• <strong>Generate AI Theme:</strong> Uses AI to create a complete color scheme based on extracted colors and your description
|
|
321
|
+
</Typography>
|
|
322
|
+
</Box>
|
|
323
|
+
)}
|
|
324
|
+
</Box>
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
export default ImageColorExtractor;
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import { RGBColor } from 'react-color';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export interface KeyValuePair {
|
|
6
|
+
key: string;
|
|
7
|
+
value: string;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ColorFormat {
|
|
12
|
+
hex: string;
|
|
13
|
+
rgb: RGBColor;
|
|
14
|
+
hsl: { h: number; s: number; l: number; a?: number };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ColorFormatType = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla';
|
|
18
|
+
|
|
19
|
+
// Color mapping interface
|
|
20
|
+
export interface ColorMapping {
|
|
21
|
+
[key: string]: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ColorThiefResult {
|
|
4
|
+
dominantColor: number[] | null;
|
|
5
|
+
palette: number[][] | null;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Custom hook for async loading and using Color Thief functionality
|
|
12
|
+
* @param imageSource - The image source (URL, base64, or ref)
|
|
13
|
+
* @param options - Configuration options for color extraction
|
|
14
|
+
* @returns Color thief result with dominant color and palette
|
|
15
|
+
*/
|
|
16
|
+
export const useAsyncColorThief = (
|
|
17
|
+
imageSource: string | null,
|
|
18
|
+
options: { colorCount?: number; quality?: number } = {}
|
|
19
|
+
): ColorThiefResult => {
|
|
20
|
+
const [result, setResult] = useState<ColorThiefResult>({
|
|
21
|
+
dominantColor: null,
|
|
22
|
+
palette: null,
|
|
23
|
+
loading: false,
|
|
24
|
+
error: null
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const extractColors = useCallback(async (src: string) => {
|
|
28
|
+
if (!src) return;
|
|
29
|
+
|
|
30
|
+
setResult(prev => ({ ...prev, loading: true, error: null }));
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Dynamically import ColorThief
|
|
34
|
+
const ColorThief = (await import('colorthief')).default;
|
|
35
|
+
|
|
36
|
+
// Create a temporary image element for color extraction
|
|
37
|
+
const img = new Image();
|
|
38
|
+
img.crossOrigin = 'anonymous';
|
|
39
|
+
|
|
40
|
+
img.onload = () => {
|
|
41
|
+
try {
|
|
42
|
+
const colorThief = new ColorThief();
|
|
43
|
+
|
|
44
|
+
const dominantColor = colorThief.getColor(img);
|
|
45
|
+
const palette = colorThief.getPalette(img, options.colorCount || 5, options.quality || 10);
|
|
46
|
+
|
|
47
|
+
setResult({
|
|
48
|
+
dominantColor,
|
|
49
|
+
palette,
|
|
50
|
+
loading: false,
|
|
51
|
+
error: null
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Color extraction error:', error);
|
|
55
|
+
setResult(prev => ({
|
|
56
|
+
...prev,
|
|
57
|
+
loading: false,
|
|
58
|
+
error: `Failed to extract colors: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
img.onerror = () => {
|
|
64
|
+
setResult(prev => ({
|
|
65
|
+
...prev,
|
|
66
|
+
loading: false,
|
|
67
|
+
error: 'Failed to load image'
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
img.src = src;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('ColorThief import error:', error);
|
|
74
|
+
setResult(prev => ({
|
|
75
|
+
...prev,
|
|
76
|
+
loading: false,
|
|
77
|
+
error: `Failed to load Color Thief: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}, [options.colorCount, options.quality]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (imageSource) {
|
|
84
|
+
extractColors(imageSource);
|
|
85
|
+
} else {
|
|
86
|
+
// Reset state when no image source
|
|
87
|
+
setResult({
|
|
88
|
+
dominantColor: null,
|
|
89
|
+
palette: null,
|
|
90
|
+
loading: false,
|
|
91
|
+
error: null
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}, [imageSource, extractColors]);
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { act, useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { IDashAutoAdminCustomFieldComponent } from 'dash-auto-admin';
|
|
3
|
+
/*import { ICommunes } from '@app/interfaces/ICommunes'; */
|
|
4
|
+
import { useRecordContext, useUpdate } from 'react-admin';
|
|
5
|
+
import { Switch, Box } from '@mui/material';
|
|
6
|
+
import { FormControlLabel } from '@mui/material';
|
|
7
|
+
import { AttributeToField } from 'dash-auto-admin/src/mui/AttributeToField';
|
|
8
|
+
import AttributeToInput from 'dash-auto-admin/src/mui/AttributeToInput';
|
|
9
|
+
|
|
10
|
+
export interface IListActive extends IDashAutoAdminCustomFieldComponent {
|
|
11
|
+
activeLabel?: string;
|
|
12
|
+
inactiveLabel?: string;
|
|
13
|
+
enableLabel?: string;
|
|
14
|
+
disableLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ListActive = ({
|
|
18
|
+
method,
|
|
19
|
+
attribute,
|
|
20
|
+
resourceConfig,
|
|
21
|
+
activeLabel = 'Activo',
|
|
22
|
+
inactiveLabel = 'Inactivo',
|
|
23
|
+
enableLabel = 'Activar',
|
|
24
|
+
disableLabel = 'Desactivar',
|
|
25
|
+
showLabel = false,
|
|
26
|
+
}: IListActive) => {
|
|
27
|
+
const [ ,setLoading] = useState<boolean>(true);
|
|
28
|
+
const [update, { isLoading: updateLoading }] = useUpdate();
|
|
29
|
+
const record: any = useRecordContext();
|
|
30
|
+
const [is_active, setIsActive] = useState<boolean>(false);
|
|
31
|
+
const onError = (error: any) => {
|
|
32
|
+
console.error(error);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onChange = useCallback(
|
|
36
|
+
async (value: boolean) => {
|
|
37
|
+
/*window.dispatchEvent(
|
|
38
|
+
new MessageEvent('dash-global-loader', { data: true })
|
|
39
|
+
);*/
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await update(
|
|
43
|
+
resourceConfig.model + '/partial/' + record.id,
|
|
44
|
+
{
|
|
45
|
+
id: record.id,
|
|
46
|
+
data: { [attribute.attribute]: value },
|
|
47
|
+
previousData: record,
|
|
48
|
+
meta: {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
onSuccess: () => {
|
|
55
|
+
// Force a refresh of the record context
|
|
56
|
+
setIsActive(value);
|
|
57
|
+
},
|
|
58
|
+
onSettled: (data, error) => {
|
|
59
|
+
if (error) onError(error);
|
|
60
|
+
/* window.dispatchEvent(
|
|
61
|
+
new MessageEvent('dash-global-loader', { data: false })
|
|
62
|
+
);*/
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
onError(error);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[update, record]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setLoading(false);
|
|
75
|
+
|
|
76
|
+
if (record && attribute && attribute.attribute in record) {
|
|
77
|
+
setIsActive(!!record[attribute.attribute]);
|
|
78
|
+
}
|
|
79
|
+
}, [record, attribute]);
|
|
80
|
+
|
|
81
|
+
//const textSwitch = record[attribute.attribute] ? disableLabel : enableLabel;
|
|
82
|
+
|
|
83
|
+
switch (method) {
|
|
84
|
+
case 'create':
|
|
85
|
+
case 'edit':
|
|
86
|
+
return AttributeToInput(method, resourceConfig, {...attribute,custom:false,type:Boolean})
|
|
87
|
+
case 'view':
|
|
88
|
+
return AttributeToField(method, resourceConfig, {...attribute,custom:false,type:Boolean})
|
|
89
|
+
case 'list':
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<Box>
|
|
93
|
+
<Switch
|
|
94
|
+
size='small'
|
|
95
|
+
onChange={(e, value) => onChange(value)}
|
|
96
|
+
checked={is_active}
|
|
97
|
+
/>{showLabel ? (is_active ? activeLabel : inactiveLabel) : <></>}
|
|
98
|
+
</Box>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
default:
|
|
102
|
+
return <>{attribute.label}: {is_active ? activeLabel : inactiveLabel}</>;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default ListActive;
|