@visns-studio/visns-components 5.10.2 → 5.10.4
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 +42 -1
- package/package.json +3 -3
- package/src/components/crm/DataGrid.jsx +707 -2686
- package/src/components/crm/Field.jsx +0 -2
- package/src/components/crm/cells/CellWithTooltip.jsx +138 -0
- package/src/components/crm/columns/ColumnRenderers.jsx +1358 -0
- package/src/components/crm/controls/AudioPlayer.jsx +19 -0
- package/src/components/crm/controls/AutoRefreshControls.jsx +37 -0
- package/src/components/crm/controls/DataGridSearch.jsx +238 -0
- package/src/components/crm/modals/GalleryModal.jsx +405 -0
- package/src/components/crm/styles/DataGrid.module.scss +296 -0
- package/src/components/crm/styles/global-datagrid.css +21 -0
- package/src/index.js +11 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import Popup from 'reactjs-popup';
|
|
3
|
+
import { toast } from 'react-toastify';
|
|
4
|
+
import imageCompression from 'browser-image-compression';
|
|
5
|
+
import Vapor from 'laravel-vapor';
|
|
6
|
+
import Lightbox from 'yet-another-react-lightbox';
|
|
7
|
+
import { CircleX, Image as ImageIcon, CloudUpload } from 'akar-icons';
|
|
8
|
+
import styles from '../styles/DataGrid.module.scss';
|
|
9
|
+
import CustomFetch from '../Fetch';
|
|
10
|
+
|
|
11
|
+
const GalleryModal = ({
|
|
12
|
+
modalGalleryShow,
|
|
13
|
+
modalGalleryClose,
|
|
14
|
+
galleryData,
|
|
15
|
+
setGalleryData,
|
|
16
|
+
currentItem,
|
|
17
|
+
form,
|
|
18
|
+
lightboxOpen,
|
|
19
|
+
setLightboxOpen,
|
|
20
|
+
lightboxIndex,
|
|
21
|
+
setLightboxIndex,
|
|
22
|
+
}) => {
|
|
23
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
24
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
25
|
+
|
|
26
|
+
const handleFileUpload = async (files) => {
|
|
27
|
+
if (!currentItem || !currentItem.settings.upload) return;
|
|
28
|
+
|
|
29
|
+
setIsUploading(true);
|
|
30
|
+
const uploadPromises = [];
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < files.length; i++) {
|
|
33
|
+
const file = files[i];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Optimize image if needed
|
|
37
|
+
let optimizedFile = file;
|
|
38
|
+
if (file.type.startsWith('image/')) {
|
|
39
|
+
const options = {
|
|
40
|
+
maxSizeMB: 1,
|
|
41
|
+
maxWidthOrHeight: 1920,
|
|
42
|
+
useWebWorker: true,
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
optimizedFile = await imageCompression(
|
|
46
|
+
file,
|
|
47
|
+
options
|
|
48
|
+
);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('Image compression failed:', err);
|
|
51
|
+
// Continue with original file if compression fails
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Upload to Vapor
|
|
56
|
+
const uploadPromise = new Promise((resolve, reject) => {
|
|
57
|
+
Vapor.store(optimizedFile, {
|
|
58
|
+
progress: (progress) => {
|
|
59
|
+
setUploadProgress(progress * 100);
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.then((response) => {
|
|
63
|
+
// Construct the URL with the key parameter
|
|
64
|
+
const uploadSettings =
|
|
65
|
+
currentItem.settings.upload;
|
|
66
|
+
const itemData = currentItem.data;
|
|
67
|
+
const keyValue = itemData[uploadSettings.key];
|
|
68
|
+
const url = keyValue
|
|
69
|
+
? `${uploadSettings.url}/${keyValue}`
|
|
70
|
+
: uploadSettings.url;
|
|
71
|
+
|
|
72
|
+
// Make the PUT request
|
|
73
|
+
CustomFetch(
|
|
74
|
+
url,
|
|
75
|
+
uploadSettings.method || 'POST',
|
|
76
|
+
{
|
|
77
|
+
uuid: response.uuid,
|
|
78
|
+
key: response.key,
|
|
79
|
+
bucket: response.bucket,
|
|
80
|
+
filename: file.name,
|
|
81
|
+
filesize: optimizedFile.size,
|
|
82
|
+
extension: response.extension,
|
|
83
|
+
file_relationship: uploadSettings.id,
|
|
84
|
+
},
|
|
85
|
+
(result) => {
|
|
86
|
+
if (result.error === '') {
|
|
87
|
+
resolve();
|
|
88
|
+
} else {
|
|
89
|
+
toast.error(String(result.error));
|
|
90
|
+
reject(result.error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
})
|
|
95
|
+
.catch(reject);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
uploadPromises.push(uploadPromise);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Error uploading file:', error);
|
|
101
|
+
toast.error(
|
|
102
|
+
`Error uploading ${file.name}: ${
|
|
103
|
+
error.message || 'Unknown error'
|
|
104
|
+
}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await Promise.all(uploadPromises);
|
|
111
|
+
toast.success('Files uploaded successfully');
|
|
112
|
+
|
|
113
|
+
// Refresh the gallery data
|
|
114
|
+
if (currentItem && currentItem.settings && currentItem.data) {
|
|
115
|
+
const s = currentItem.settings;
|
|
116
|
+
const d = currentItem.data;
|
|
117
|
+
|
|
118
|
+
// Function to access nested property using a series of keys
|
|
119
|
+
const getNestedValue = (data, keys) =>
|
|
120
|
+
keys.reduce(
|
|
121
|
+
(currentValue, key) =>
|
|
122
|
+
currentValue && currentValue[key] !== undefined
|
|
123
|
+
? currentValue[key]
|
|
124
|
+
: undefined,
|
|
125
|
+
data
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Fetch updated data
|
|
129
|
+
try {
|
|
130
|
+
const res = await CustomFetch(
|
|
131
|
+
`${form.url}/${d[form.primaryKey]}`,
|
|
132
|
+
'GET',
|
|
133
|
+
{}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (res.data) {
|
|
137
|
+
// Extract the updated nested value
|
|
138
|
+
const updatedNestedValue = getNestedValue(
|
|
139
|
+
res.data,
|
|
140
|
+
s.relation
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Update gallery data
|
|
144
|
+
if (
|
|
145
|
+
updatedNestedValue &&
|
|
146
|
+
Array.isArray(updatedNestedValue)
|
|
147
|
+
) {
|
|
148
|
+
const updatedGalleryArray =
|
|
149
|
+
updatedNestedValue.map((item) => ({
|
|
150
|
+
src: item[s.key],
|
|
151
|
+
caption: item.file_name,
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
setGalleryData(updatedGalleryArray);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error refreshing gallery data:', error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error in upload process:', error);
|
|
163
|
+
} finally {
|
|
164
|
+
setIsUploading(false);
|
|
165
|
+
setUploadProgress(0);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<>
|
|
171
|
+
<Popup
|
|
172
|
+
open={modalGalleryShow}
|
|
173
|
+
onClose={modalGalleryClose}
|
|
174
|
+
closeOnDocumentClick={false}
|
|
175
|
+
contentStyle={{
|
|
176
|
+
width: '90vw',
|
|
177
|
+
maxWidth: '1400px',
|
|
178
|
+
minWidth: '600px',
|
|
179
|
+
padding: 0,
|
|
180
|
+
border: 'none',
|
|
181
|
+
borderRadius: '8px',
|
|
182
|
+
overflow: 'hidden',
|
|
183
|
+
maxHeight: '90vh',
|
|
184
|
+
}}
|
|
185
|
+
overlayStyle={{
|
|
186
|
+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<div className={styles.modalwrap}>
|
|
190
|
+
<div className={styles.modal}>
|
|
191
|
+
<div className={styles.modal__header}>
|
|
192
|
+
<h1>Gallery</h1>
|
|
193
|
+
<button
|
|
194
|
+
className={styles.modal__close}
|
|
195
|
+
onClick={modalGalleryClose}
|
|
196
|
+
>
|
|
197
|
+
<CircleX strokeWidth={1} size={24} />
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
<div
|
|
201
|
+
className={`${styles.modal__content} ${styles['modal__content--gallery']}`}
|
|
202
|
+
>
|
|
203
|
+
{currentItem?.settings?.upload && (
|
|
204
|
+
<div className={styles.uploadSection}>
|
|
205
|
+
<h3>
|
|
206
|
+
<ImageIcon
|
|
207
|
+
strokeWidth={1.5}
|
|
208
|
+
size={18}
|
|
209
|
+
/>
|
|
210
|
+
Upload New Images
|
|
211
|
+
</h3>
|
|
212
|
+
<div
|
|
213
|
+
className={styles.dropzoneContainer}
|
|
214
|
+
>
|
|
215
|
+
{isUploading && (
|
|
216
|
+
<div
|
|
217
|
+
className={
|
|
218
|
+
styles.progressBar
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
<div
|
|
222
|
+
className={
|
|
223
|
+
styles.progressBarInner
|
|
224
|
+
}
|
|
225
|
+
style={{
|
|
226
|
+
width: `${uploadProgress}%`,
|
|
227
|
+
}}
|
|
228
|
+
></div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
<div
|
|
232
|
+
className={styles.dropzone}
|
|
233
|
+
onClick={() =>
|
|
234
|
+
document
|
|
235
|
+
.getElementById(
|
|
236
|
+
'gallery-file-upload'
|
|
237
|
+
)
|
|
238
|
+
.click()
|
|
239
|
+
}
|
|
240
|
+
onDragOver={(e) => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
e.stopPropagation();
|
|
243
|
+
}}
|
|
244
|
+
onDragEnter={(e) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
}}
|
|
248
|
+
onDragLeave={(e) => {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
}}
|
|
252
|
+
onDrop={(e) => {
|
|
253
|
+
e.preventDefault();
|
|
254
|
+
e.stopPropagation();
|
|
255
|
+
const files =
|
|
256
|
+
e.dataTransfer.files;
|
|
257
|
+
if (files.length > 0) {
|
|
258
|
+
handleFileUpload(files);
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<CloudUpload
|
|
263
|
+
strokeWidth={1.5}
|
|
264
|
+
size={24}
|
|
265
|
+
/>
|
|
266
|
+
<p>
|
|
267
|
+
Drag and drop files here or
|
|
268
|
+
click to select files
|
|
269
|
+
</p>
|
|
270
|
+
<input
|
|
271
|
+
id="gallery-file-upload"
|
|
272
|
+
type="file"
|
|
273
|
+
multiple
|
|
274
|
+
style={{ display: 'none' }}
|
|
275
|
+
onChange={(e) => {
|
|
276
|
+
if (
|
|
277
|
+
e.target.files
|
|
278
|
+
.length > 0
|
|
279
|
+
) {
|
|
280
|
+
handleFileUpload(
|
|
281
|
+
e.target.files
|
|
282
|
+
);
|
|
283
|
+
e.target.value =
|
|
284
|
+
null; // Reset input
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
287
|
+
accept="image/*"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
<div className={styles.formcontainer}>
|
|
294
|
+
{galleryData.length > 0 ? (
|
|
295
|
+
<div
|
|
296
|
+
className={styles.galleryContainer}
|
|
297
|
+
>
|
|
298
|
+
<div
|
|
299
|
+
className={
|
|
300
|
+
styles.galleryInstructions
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
<ImageIcon
|
|
304
|
+
strokeWidth={1.5}
|
|
305
|
+
size={16}
|
|
306
|
+
/>
|
|
307
|
+
<span>
|
|
308
|
+
Tap any image to enlarge
|
|
309
|
+
</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div
|
|
312
|
+
className={
|
|
313
|
+
styles.customGalleryGrid
|
|
314
|
+
}
|
|
315
|
+
>
|
|
316
|
+
{galleryData.map(
|
|
317
|
+
(img, index) => (
|
|
318
|
+
<div
|
|
319
|
+
key={`gallery-item-${index}`}
|
|
320
|
+
className={
|
|
321
|
+
styles.galleryItem
|
|
322
|
+
}
|
|
323
|
+
onClick={() => {
|
|
324
|
+
setLightboxIndex(
|
|
325
|
+
index
|
|
326
|
+
);
|
|
327
|
+
setLightboxOpen(
|
|
328
|
+
true
|
|
329
|
+
);
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<img
|
|
333
|
+
src={img.src}
|
|
334
|
+
alt={
|
|
335
|
+
img.caption ||
|
|
336
|
+
`Image ${
|
|
337
|
+
index +
|
|
338
|
+
1
|
|
339
|
+
}`
|
|
340
|
+
}
|
|
341
|
+
/>
|
|
342
|
+
{img.caption && (
|
|
343
|
+
<div
|
|
344
|
+
className={
|
|
345
|
+
styles.imageCaption
|
|
346
|
+
}
|
|
347
|
+
>
|
|
348
|
+
{
|
|
349
|
+
img.caption
|
|
350
|
+
}
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
<div
|
|
354
|
+
className={
|
|
355
|
+
styles.galleryImageOverlay
|
|
356
|
+
}
|
|
357
|
+
>
|
|
358
|
+
<span>
|
|
359
|
+
<ImageIcon
|
|
360
|
+
strokeWidth={
|
|
361
|
+
1.5
|
|
362
|
+
}
|
|
363
|
+
size={
|
|
364
|
+
16
|
|
365
|
+
}
|
|
366
|
+
style={{
|
|
367
|
+
marginRight:
|
|
368
|
+
'6px',
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
Enlarge
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
) : (
|
|
380
|
+
<div className={styles.noDataMessage}>
|
|
381
|
+
<ImageIcon
|
|
382
|
+
strokeWidth={1.5}
|
|
383
|
+
size={20}
|
|
384
|
+
/>
|
|
385
|
+
No images available in the gallery
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
</Popup>
|
|
393
|
+
|
|
394
|
+
{/* Lightbox for full-size image viewing */}
|
|
395
|
+
<Lightbox
|
|
396
|
+
open={lightboxOpen}
|
|
397
|
+
close={() => setLightboxOpen(false)}
|
|
398
|
+
index={lightboxIndex}
|
|
399
|
+
slides={galleryData.map((img) => ({ src: img.src }))}
|
|
400
|
+
/>
|
|
401
|
+
</>
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export default GalleryModal;
|
|
@@ -868,3 +868,299 @@
|
|
|
868
868
|
user-select: none;
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
|
+
|
|
872
|
+
/* Group action buttons */
|
|
873
|
+
.groupActionContainer {
|
|
874
|
+
display: flex;
|
|
875
|
+
align-items: center;
|
|
876
|
+
flex: 1;
|
|
877
|
+
z-index: 2;
|
|
878
|
+
position: relative;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.groupExpandBtn {
|
|
882
|
+
background: var(--primary-color, #3b82f6);
|
|
883
|
+
border: none;
|
|
884
|
+
border-radius: 6px;
|
|
885
|
+
color: white;
|
|
886
|
+
min-width: 28px;
|
|
887
|
+
min-height: 28px;
|
|
888
|
+
width: 28px;
|
|
889
|
+
height: 28px;
|
|
890
|
+
display: flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
justify-content: center;
|
|
893
|
+
margin-right: 8px;
|
|
894
|
+
cursor: pointer;
|
|
895
|
+
font-size: 12px;
|
|
896
|
+
font-weight: bold;
|
|
897
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
898
|
+
transition: all 0.2s ease;
|
|
899
|
+
position: relative;
|
|
900
|
+
|
|
901
|
+
&:hover {
|
|
902
|
+
transform: scale(1.05);
|
|
903
|
+
background: var(--secondary-color, #2563eb);
|
|
904
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
&:active {
|
|
908
|
+
transform: scale(0.95);
|
|
909
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Mobile responsive
|
|
913
|
+
@media (max-width: 768px) {
|
|
914
|
+
min-width: 32px;
|
|
915
|
+
min-height: 32px;
|
|
916
|
+
width: 32px;
|
|
917
|
+
height: 32px;
|
|
918
|
+
margin-right: 12px;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Visual indicator for touch devices
|
|
922
|
+
@media (hover: none) and (pointer: coarse) {
|
|
923
|
+
&::after {
|
|
924
|
+
content: '';
|
|
925
|
+
position: absolute;
|
|
926
|
+
top: -2px;
|
|
927
|
+
right: -2px;
|
|
928
|
+
width: 6px;
|
|
929
|
+
height: 6px;
|
|
930
|
+
background: rgba(255, 255, 255, 0.8);
|
|
931
|
+
border-radius: 50%;
|
|
932
|
+
box-shadow: 0 0 4px rgba(255, 255, 255, 0.6);
|
|
933
|
+
animation: touchPulse 2s infinite;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.groupActionBtn {
|
|
939
|
+
border: none;
|
|
940
|
+
border-radius: 6px;
|
|
941
|
+
color: white;
|
|
942
|
+
min-width: 28px;
|
|
943
|
+
min-height: 28px;
|
|
944
|
+
width: 28px;
|
|
945
|
+
height: 28px;
|
|
946
|
+
display: flex;
|
|
947
|
+
align-items: center;
|
|
948
|
+
justify-content: center;
|
|
949
|
+
margin-right: 8px;
|
|
950
|
+
cursor: pointer;
|
|
951
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
952
|
+
transition: all 0.2s ease;
|
|
953
|
+
position: relative;
|
|
954
|
+
|
|
955
|
+
&:hover {
|
|
956
|
+
transform: scale(1.05);
|
|
957
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
&:active {
|
|
961
|
+
transform: scale(0.95);
|
|
962
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Add touch-friendly spacing on mobile
|
|
966
|
+
@media (max-width: 768px) {
|
|
967
|
+
margin-right: 12px;
|
|
968
|
+
min-width: 32px;
|
|
969
|
+
min-height: 32px;
|
|
970
|
+
width: 32px;
|
|
971
|
+
height: 32px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Visual indicator for touch devices - subtle glow
|
|
975
|
+
@media (hover: none) and (pointer: coarse) {
|
|
976
|
+
&::after {
|
|
977
|
+
content: '';
|
|
978
|
+
position: absolute;
|
|
979
|
+
top: -2px;
|
|
980
|
+
right: -2px;
|
|
981
|
+
width: 6px;
|
|
982
|
+
height: 6px;
|
|
983
|
+
background: rgba(255, 255, 255, 0.8);
|
|
984
|
+
border-radius: 50%;
|
|
985
|
+
box-shadow: 0 0 4px rgba(255, 255, 255, 0.6);
|
|
986
|
+
animation: touchPulse 2s infinite;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Color variants
|
|
991
|
+
&.success {
|
|
992
|
+
background: var(--success-color, #10b981);
|
|
993
|
+
&:hover {
|
|
994
|
+
background: var(--success-hover-color, #059669);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
&.warning {
|
|
999
|
+
background: var(--warning-color, #f59e0b);
|
|
1000
|
+
&:hover {
|
|
1001
|
+
background: var(--warning-hover-color, #d97706);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
&.info {
|
|
1006
|
+
background: var(--info-color, #3b82f6);
|
|
1007
|
+
&:hover {
|
|
1008
|
+
background: var(--info-hover-color, #2563eb);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
&.purple {
|
|
1013
|
+
background: var(--purple-color, #8b5cf6);
|
|
1014
|
+
&:hover {
|
|
1015
|
+
background: var(--purple-hover-color, #7c3aed);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
&.pink {
|
|
1020
|
+
background: var(--pink-color, #ec4899);
|
|
1021
|
+
&:hover {
|
|
1022
|
+
background: var(--pink-hover-color, #db2777);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.groupInfo {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
align-items: center;
|
|
1030
|
+
gap: 8px;
|
|
1031
|
+
flex: 1;
|
|
1032
|
+
margin-left: 4px;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.groupValue {
|
|
1036
|
+
font-size: 0.85rem;
|
|
1037
|
+
color: var(--secondary-color, #1f2937);
|
|
1038
|
+
font-weight: 600;
|
|
1039
|
+
text-transform: uppercase;
|
|
1040
|
+
letter-spacing: 0.025em;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.groupClickIndicator {
|
|
1044
|
+
color: #9ca3af;
|
|
1045
|
+
font-size: 0.7rem;
|
|
1046
|
+
font-style: italic;
|
|
1047
|
+
opacity: 0.8;
|
|
1048
|
+
margin-left: auto;
|
|
1049
|
+
user-select: none;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.groupHeaderContainer {
|
|
1053
|
+
display: flex;
|
|
1054
|
+
align-items: center;
|
|
1055
|
+
justify-content: space-between;
|
|
1056
|
+
width: 100%;
|
|
1057
|
+
height: 34px;
|
|
1058
|
+
padding: 6px 12px;
|
|
1059
|
+
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
1060
|
+
border-left: 4px solid var(--primary-color, #3b82f6);
|
|
1061
|
+
position: relative;
|
|
1062
|
+
overflow: hidden;
|
|
1063
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
1064
|
+
border-radius: 0 4px 4px 0;
|
|
1065
|
+
cursor: pointer;
|
|
1066
|
+
box-sizing: border-box;
|
|
1067
|
+
line-height: 1.1;
|
|
1068
|
+
flex-shrink: 0;
|
|
1069
|
+
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
1070
|
+
|
|
1071
|
+
&:hover {
|
|
1072
|
+
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
|
1073
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
1074
|
+
transform: translateY(-1px);
|
|
1075
|
+
|
|
1076
|
+
&::after {
|
|
1077
|
+
opacity: 1;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
&:active {
|
|
1082
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
1083
|
+
transform: translateY(0);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
&::after {
|
|
1087
|
+
content: '';
|
|
1088
|
+
position: absolute;
|
|
1089
|
+
top: 0;
|
|
1090
|
+
left: 0;
|
|
1091
|
+
right: 0;
|
|
1092
|
+
bottom: 0;
|
|
1093
|
+
background: rgba(59, 130, 246, 0.05);
|
|
1094
|
+
opacity: 0;
|
|
1095
|
+
transition: opacity 0.2s ease;
|
|
1096
|
+
pointer-events: none;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.groupHeaderPattern {
|
|
1101
|
+
position: absolute;
|
|
1102
|
+
top: 0;
|
|
1103
|
+
left: 0;
|
|
1104
|
+
right: 0;
|
|
1105
|
+
bottom: 0;
|
|
1106
|
+
background: repeating-linear-gradient(
|
|
1107
|
+
45deg,
|
|
1108
|
+
transparent,
|
|
1109
|
+
transparent 2px,
|
|
1110
|
+
rgba(59, 130, 246, 0.03) 2px,
|
|
1111
|
+
rgba(59, 130, 246, 0.03) 4px
|
|
1112
|
+
);
|
|
1113
|
+
z-index: 1;
|
|
1114
|
+
pointer-events: none;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Touch tooltip styles
|
|
1118
|
+
.touchTooltip {
|
|
1119
|
+
animation: fadeInTooltip 0.2s ease-out;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.touchTooltipContent {
|
|
1123
|
+
background: rgba(0, 0, 0, 0.9);
|
|
1124
|
+
color: white;
|
|
1125
|
+
padding: 8px 12px;
|
|
1126
|
+
border-radius: 6px;
|
|
1127
|
+
font-size: 0.875rem;
|
|
1128
|
+
font-weight: 500;
|
|
1129
|
+
white-space: nowrap;
|
|
1130
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
1131
|
+
position: relative;
|
|
1132
|
+
|
|
1133
|
+
// Arrow pointing down
|
|
1134
|
+
&::after {
|
|
1135
|
+
content: '';
|
|
1136
|
+
position: absolute;
|
|
1137
|
+
top: 100%;
|
|
1138
|
+
left: 50%;
|
|
1139
|
+
transform: translateX(-50%);
|
|
1140
|
+
border: 6px solid transparent;
|
|
1141
|
+
border-top-color: rgba(0, 0, 0, 0.9);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Animations
|
|
1146
|
+
@keyframes fadeInTooltip {
|
|
1147
|
+
from {
|
|
1148
|
+
opacity: 0;
|
|
1149
|
+
transform: translateY(-5px);
|
|
1150
|
+
}
|
|
1151
|
+
to {
|
|
1152
|
+
opacity: 1;
|
|
1153
|
+
transform: translateY(0);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
@keyframes touchPulse {
|
|
1158
|
+
0%, 100% {
|
|
1159
|
+
opacity: 0.8;
|
|
1160
|
+
transform: scale(1);
|
|
1161
|
+
}
|
|
1162
|
+
50% {
|
|
1163
|
+
opacity: 1;
|
|
1164
|
+
transform: scale(1.2);
|
|
1165
|
+
}
|
|
1166
|
+
}
|