@visns-studio/visns-components 5.15.26 → 5.15.27

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.
@@ -0,0 +1,789 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import moment from 'moment';
3
+
4
+ const ExcelStyleGrid = ({
5
+ gridData = [],
6
+ dateColumns = [],
7
+ facilitiesByCategory = {},
8
+ facilityColors = {},
9
+ showScrollHint = true,
10
+ setShowScrollHint,
11
+ maxWidth = '1150px',
12
+ maxHeight = '400px',
13
+ onProjectClick,
14
+ }) => {
15
+ const scrollContainerRef = useRef(null);
16
+ const [tooltip, setTooltip] = useState({
17
+ visible: false,
18
+ data: null,
19
+ position: { x: 0, y: 0 },
20
+ });
21
+ const [hoveredColumn, setHoveredColumn] = useState(null);
22
+ const [hoveredRow, setHoveredRow] = useState(null);
23
+
24
+ // Helper function to darken hex color for borders
25
+ const darkenColor = (hex, percent) => {
26
+ const num = parseInt(hex.replace('#', ''), 16);
27
+ const amt = Math.round(2.55 * percent);
28
+ const R = (num >> 16) - amt;
29
+ const G = ((num >> 8) & 0x00ff) - amt;
30
+ const B = (num & 0x0000ff) - amt;
31
+ return (
32
+ '#' +
33
+ (
34
+ 0x1000000 +
35
+ (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
36
+ (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
37
+ (B < 255 ? (B < 1 ? 0 : B) : 255)
38
+ )
39
+ .toString(16)
40
+ .slice(1)
41
+ );
42
+ };
43
+
44
+ // Helper function to get intelligent text color based on background
45
+ const getContrastTextColor = (backgroundColor) => {
46
+ // Convert hex to RGB
47
+ const hex = backgroundColor.replace('#', '');
48
+ const r = parseInt(hex.substr(0, 2), 16);
49
+ const g = parseInt(hex.substr(2, 2), 16);
50
+ const b = parseInt(hex.substr(4, 2), 16);
51
+
52
+ // Calculate relative luminance
53
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
54
+
55
+ // Return black for light backgrounds, white for dark backgrounds
56
+ return luminance > 0.5 ? '#333333' : '#ffffff';
57
+ };
58
+
59
+ // Helper function to get cell color based on confirmation status
60
+ const getCellColor = (confirmed, isHovered = false) => {
61
+ const baseColors = {
62
+ confirmed: '#c8e6c9', // Soft mint green for confirmed
63
+ unconfirmed: '#ffe0b2', // Soft peach for unconfirmed
64
+ };
65
+
66
+ const color = Boolean(confirmed) ? baseColors.confirmed : baseColors.unconfirmed;
67
+
68
+ // Add slight transparency for hover effect
69
+ return isHovered ? `${color}DD` : color;
70
+ };
71
+
72
+ // Find date range for a booking
73
+ const getDateRangeForCell = (row, currentDate) => {
74
+ const currentCell = row.cells[currentDate];
75
+ if (!currentCell.hasData) return null;
76
+
77
+ let startDate = currentDate;
78
+ let endDate = currentDate;
79
+
80
+ // Find start of booking by going backwards
81
+ const sortedDates = Object.keys(row.cells).sort();
82
+ const currentIndex = sortedDates.indexOf(currentDate);
83
+
84
+ for (let i = currentIndex - 1; i >= 0; i--) {
85
+ const date = sortedDates[i];
86
+ const cell = row.cells[date];
87
+ if (
88
+ cell.hasData &&
89
+ cell.data?.resource === currentCell.data?.resource &&
90
+ cell.data?.client === currentCell.data?.client
91
+ ) {
92
+ startDate = date;
93
+ } else {
94
+ break;
95
+ }
96
+ }
97
+
98
+ // Find end of booking by going forwards
99
+ for (let i = currentIndex + 1; i < sortedDates.length; i++) {
100
+ const date = sortedDates[i];
101
+ const cell = row.cells[date];
102
+ if (
103
+ cell.hasData &&
104
+ cell.data?.resource === currentCell.data?.resource &&
105
+ cell.data?.client === currentCell.data?.client
106
+ ) {
107
+ endDate = date;
108
+ } else {
109
+ break;
110
+ }
111
+ }
112
+
113
+ return { startDate, endDate };
114
+ };
115
+
116
+ // Grid cell tooltip component
117
+ const GridTooltip = ({ data, visible, position }) => {
118
+ if (!visible || !data) return null;
119
+
120
+ return (
121
+ <div
122
+ style={{
123
+ position: 'fixed',
124
+ left: position.x + 10,
125
+ top: position.y - 10,
126
+ background: 'white',
127
+ padding: '10px',
128
+ border: '1px solid #ccc',
129
+ borderRadius: '4px',
130
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
131
+ zIndex: 1000,
132
+ fontSize: '12px',
133
+ minWidth: '200px',
134
+ }}
135
+ >
136
+ <div>
137
+ <strong>Resource:</strong> {data.resource}
138
+ </div>
139
+ {data.dateRange ? (
140
+ <div>
141
+ <strong>Date Range:</strong> {data.dateRange}
142
+ </div>
143
+ ) : (
144
+ <div>
145
+ <strong>Date:</strong> {data.date}
146
+ </div>
147
+ )}
148
+ {(data.client || data.resource) && (
149
+ <div>
150
+ <strong>User:</strong> {data.client || 'Not specified'}
151
+ </div>
152
+ )}
153
+ {data.status && (
154
+ <div>
155
+ <strong>Status:</strong> {data.status}
156
+ </div>
157
+ )}
158
+ </div>
159
+ );
160
+ };
161
+
162
+ if (gridData.length === 0) {
163
+ return (
164
+ <div style={{ padding: '20px', textAlign: 'center' }}>
165
+ No data available to display in the grid.
166
+ </div>
167
+ );
168
+ }
169
+
170
+ return (
171
+ <div
172
+ style={{
173
+ position: 'relative',
174
+ width: '100%',
175
+ maxWidth: maxWidth,
176
+ overflow: 'hidden',
177
+ }}
178
+ >
179
+ <div
180
+ className="scroll-indicators"
181
+ style={{
182
+ position: 'absolute',
183
+ width: '100%',
184
+ height: '100%',
185
+ pointerEvents: 'none',
186
+ zIndex: 1001,
187
+ }}
188
+ >
189
+ <div
190
+ className="scroll-left"
191
+ style={{
192
+ display: 'none',
193
+ position: 'absolute',
194
+ left: '8px',
195
+ top: '50%',
196
+ transform: 'translateY(-50%)',
197
+ pointerEvents: 'auto',
198
+ }}
199
+ >
200
+ <button
201
+ onClick={() => {
202
+ if (scrollContainerRef.current) {
203
+ scrollContainerRef.current.scrollLeft -= 300;
204
+ scrollContainerRef.current.focus();
205
+ }
206
+ }}
207
+ aria-label="Scroll left"
208
+ style={{
209
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
210
+ color: '#007bff',
211
+ border: '2px solid #007bff',
212
+ borderRadius: '50%',
213
+ width: '40px',
214
+ height: '40px',
215
+ fontSize: '16px',
216
+ cursor: 'pointer',
217
+ display: 'flex',
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ boxShadow: '0 4px 12px rgba(0, 123, 255, 0.3)',
221
+ transition: 'all 0.2s ease',
222
+ fontWeight: 'bold',
223
+ }}
224
+ onMouseEnter={(e) => {
225
+ e.target.style.backgroundColor = '#007bff';
226
+ e.target.style.color = 'white';
227
+ e.target.style.transform = 'scale(1.1)';
228
+ }}
229
+ onMouseLeave={(e) => {
230
+ e.target.style.backgroundColor =
231
+ 'rgba(255, 255, 255, 0.95)';
232
+ e.target.style.color = '#007bff';
233
+ e.target.style.transform = 'scale(1)';
234
+ }}
235
+ >
236
+
237
+ </button>
238
+ </div>
239
+ <div
240
+ className="scroll-right"
241
+ style={{
242
+ position: 'absolute',
243
+ right: '8px',
244
+ top: '50%',
245
+ transform: 'translateY(-50%)',
246
+ pointerEvents: 'auto',
247
+ }}
248
+ >
249
+ <button
250
+ onClick={() => {
251
+ if (scrollContainerRef.current) {
252
+ scrollContainerRef.current.scrollLeft += 300;
253
+ scrollContainerRef.current.focus();
254
+ }
255
+ }}
256
+ aria-label="Scroll right"
257
+ style={{
258
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
259
+ color: '#007bff',
260
+ border: '2px solid #007bff',
261
+ borderRadius: '50%',
262
+ width: '40px',
263
+ height: '40px',
264
+ fontSize: '16px',
265
+ cursor: 'pointer',
266
+ display: 'flex',
267
+ alignItems: 'center',
268
+ justifyContent: 'center',
269
+ boxShadow: '0 4px 12px rgba(0, 123, 255, 0.3)',
270
+ transition: 'all 0.2s ease',
271
+ fontWeight: 'bold',
272
+ }}
273
+ onMouseEnter={(e) => {
274
+ e.target.style.backgroundColor = '#007bff';
275
+ e.target.style.color = 'white';
276
+ e.target.style.transform = 'scale(1.1)';
277
+ }}
278
+ onMouseLeave={(e) => {
279
+ e.target.style.backgroundColor =
280
+ 'rgba(255, 255, 255, 0.95)';
281
+ e.target.style.color = '#007bff';
282
+ e.target.style.transform = 'scale(1)';
283
+ }}
284
+ >
285
+
286
+ </button>
287
+ </div>
288
+ </div>
289
+
290
+ {/* Excel-style Grid */}
291
+ <div
292
+ className="dock-usage-grid"
293
+ ref={scrollContainerRef}
294
+ style={{
295
+ overflowX: 'auto',
296
+ overflowY: 'auto',
297
+ maxHeight: maxHeight,
298
+ border: '1px solid #dee2e6',
299
+ backgroundColor: '#fff',
300
+ }}
301
+ tabIndex="0"
302
+ onKeyDown={(e) => {
303
+ if (e.key === 'ArrowLeft') {
304
+ e.preventDefault();
305
+ if (scrollContainerRef.current) {
306
+ scrollContainerRef.current.scrollLeft -= 100;
307
+ }
308
+ } else if (e.key === 'ArrowRight') {
309
+ e.preventDefault();
310
+ if (scrollContainerRef.current) {
311
+ scrollContainerRef.current.scrollLeft += 100;
312
+ }
313
+ }
314
+ }}
315
+ onScroll={(e) => {
316
+ const container = e.target;
317
+ const scrollLeft = container.scrollLeft;
318
+ const maxScroll =
319
+ container.scrollWidth - container.clientWidth;
320
+
321
+ const leftButton = document.querySelector('.scroll-left');
322
+ const rightButton = document.querySelector('.scroll-right');
323
+
324
+ if (leftButton)
325
+ leftButton.style.display =
326
+ scrollLeft > 20 ? 'block' : 'none';
327
+ if (rightButton)
328
+ rightButton.style.display =
329
+ scrollLeft < maxScroll - 20 ? 'block' : 'none';
330
+
331
+ if (scrollLeft > 0) {
332
+ if (rightButton)
333
+ rightButton.classList.add('has-scrolled');
334
+ if (setShowScrollHint) setShowScrollHint(false);
335
+ } else {
336
+ if (rightButton)
337
+ rightButton.classList.remove('has-scrolled');
338
+ }
339
+ }}
340
+ onMouseLeave={() => {
341
+ setHoveredColumn(null);
342
+ setHoveredRow(null);
343
+ setTooltip({
344
+ visible: false,
345
+ data: null,
346
+ position: { x: 0, y: 0 },
347
+ });
348
+ }}
349
+ >
350
+ <div
351
+ className="grid-container"
352
+ style={{
353
+ minWidth: `${Math.max(600, dateColumns.length * 45 + 200)}px`,
354
+ }}
355
+ >
356
+ {/* Header Row */}
357
+ <div
358
+ className="grid-header"
359
+ style={{
360
+ display: 'grid',
361
+ gridTemplateColumns: `200px repeat(${dateColumns.length}, 45px)`,
362
+ backgroundColor: '#f8f9fa',
363
+ borderBottom: '2px solid #dee2e6',
364
+ position: 'sticky',
365
+ top: 0,
366
+ zIndex: 20,
367
+ }}
368
+ >
369
+ <div
370
+ className="header-cell resource-header"
371
+ style={{
372
+ padding: '4px 8px',
373
+ fontWeight: 'bold',
374
+ border: '1px solid #dee2e6',
375
+ backgroundColor: '#e9ecef',
376
+ fontSize: '11px',
377
+ }}
378
+ >
379
+ Project
380
+ </div>
381
+ {dateColumns.map((dateCol) => (
382
+ <div
383
+ key={`header-${dateCol.date}`}
384
+ className="header-cell date-header"
385
+ style={{
386
+ padding: '2px 1px',
387
+ fontWeight: 'bold',
388
+ border: '1px solid #dee2e6',
389
+ fontSize: '9px',
390
+ textAlign: 'center',
391
+ backgroundColor:
392
+ hoveredColumn === dateCol.date
393
+ ? '#e1f0ff'
394
+ : '#e9ecef',
395
+ writingMode: 'horizontal-tb',
396
+ lineHeight: 1.1,
397
+ transition: 'background-color 0.2s ease',
398
+ display: 'flex',
399
+ flexDirection: 'column',
400
+ justifyContent: 'center',
401
+ }}
402
+ onMouseEnter={() =>
403
+ setHoveredColumn(dateCol.date)
404
+ }
405
+ >
406
+ <div
407
+ style={{
408
+ fontSize: '8px',
409
+ marginBottom: '1px',
410
+ }}
411
+ >
412
+ {moment(dateCol.date).format('ddd')}
413
+ </div>
414
+ <div style={{ fontSize: '9px' }}>
415
+ {dateCol.display}
416
+ </div>
417
+ </div>
418
+ ))}
419
+ </div>
420
+
421
+ {/* Grid Body - Group by Facility */}
422
+ {Object.keys(facilitiesByCategory).sort((a, b) => {
423
+ // Custom sorting function for wharf facilities (Jetty first, then AMC 1-6)
424
+ // Jetty facilities first
425
+ const isAJetty = a.toLowerCase().includes('jetty');
426
+ const isBJetty = b.toLowerCase().includes('jetty');
427
+
428
+ if (isAJetty && !isBJetty) return -1;
429
+ if (!isAJetty && isBJetty) return 1;
430
+
431
+ // Both are jetties, sort alphabetically
432
+ if (isAJetty && isBJetty) {
433
+ return a.localeCompare(b);
434
+ }
435
+
436
+ // AMC facilities sorting (AMC 1-6)
437
+ const isAAMC = a.startsWith('AMC ');
438
+ const isBAMC = b.startsWith('AMC ');
439
+
440
+ if (isAAMC && !isBAMC) return -1;
441
+ if (!isAAMC && isBAMC) return 1;
442
+
443
+ // Both are AMC, sort numerically
444
+ if (isAAMC && isBAMC) {
445
+ const numA = parseInt(a.split(' ')[1]) || 0;
446
+ const numB = parseInt(b.split(' ')[1]) || 0;
447
+ return numA - numB;
448
+ }
449
+
450
+ // Default alphabetical sort for other facilities
451
+ return a.localeCompare(b);
452
+ }).map((facility) => (
453
+ <div key={facility} className="facility-group">
454
+ {/* Facility Header - Only show if there are multiple facility categories */}
455
+ {Object.keys(facilitiesByCategory).length > 1 && (
456
+ <div
457
+ className="facility-header"
458
+ style={{
459
+ display: 'grid',
460
+ gridTemplateColumns: `200px repeat(${dateColumns.length}, 45px)`,
461
+ backgroundColor:
462
+ facilityColors[facility] || '#a8c8ec',
463
+ color: getContrastTextColor(
464
+ facilityColors[facility] || '#a8c8ec'
465
+ ),
466
+ fontWeight: 'bold',
467
+ position: 'relative',
468
+ zIndex: 15,
469
+ }}
470
+ >
471
+ <div
472
+ style={{
473
+ padding: '3px 8px',
474
+ border: `1px solid ${facilityColors[facility] ? darkenColor(facilityColors[facility], 20) : '#0056b3'}`,
475
+ fontSize: '11px',
476
+ }}
477
+ >
478
+ {facility}
479
+ </div>
480
+ {dateColumns.map((dateCol) => (
481
+ <div
482
+ key={`${facility}-header-${dateCol.date}`}
483
+ style={{
484
+ border: `1px solid ${facilityColors[facility] ? darkenColor(facilityColors[facility], 20) : '#0056b3'}`,
485
+ }}
486
+ ></div>
487
+ ))}
488
+ </div>
489
+ )}
490
+
491
+ {/* Facility Rows */}
492
+ {gridData
493
+ .filter((row) => row.facility === facility)
494
+ .map((row, rowIndex) => (
495
+ <div
496
+ key={`${facility}-${row.id}`}
497
+ className="grid-row"
498
+ style={{
499
+ display: 'grid',
500
+ gridTemplateColumns: `200px repeat(${dateColumns.length}, 45px)`,
501
+ backgroundColor:
502
+ hoveredRow === row.id
503
+ ? '#f0f8ff'
504
+ : rowIndex % 2 === 0
505
+ ? '#fff'
506
+ : '#f8f9fa',
507
+ transition:
508
+ 'background-color 0.2s ease',
509
+ }}
510
+ >
511
+ <div
512
+ className="resource-cell"
513
+ style={{
514
+ padding: '3px 8px',
515
+ border: '1px solid #dee2e6',
516
+ fontSize: '10px',
517
+ backgroundColor:
518
+ hoveredRow === row.id
519
+ ? '#e1f0ff'
520
+ : '#fff',
521
+ fontWeight: '500',
522
+ lineHeight: 1.2,
523
+ transition:
524
+ 'background-color 0.2s ease',
525
+ cursor: row.lead_id ? 'pointer' : 'default',
526
+ }}
527
+ onMouseEnter={() =>
528
+ setHoveredRow(row.id)
529
+ }
530
+ onClick={() => {
531
+ if (row.lead_id && onProjectClick) {
532
+ onProjectClick(row.lead_id);
533
+ }
534
+ }}
535
+ >
536
+ <span style={{
537
+ color: row.lead_id ? '#007bff' : 'inherit',
538
+ textDecoration: row.lead_id ? 'underline' : 'none'
539
+ }}>
540
+ {row.resourceName}
541
+ </span>
542
+ </div>
543
+ {dateColumns.map(
544
+ (dateCol, colIndex) => {
545
+ const cell =
546
+ row.cells[dateCol.date];
547
+ const prevCell =
548
+ colIndex > 0
549
+ ? row.cells[
550
+ dateColumns[
551
+ colIndex - 1
552
+ ].date
553
+ ]
554
+ : null;
555
+ const nextCell =
556
+ colIndex <
557
+ dateColumns.length - 1
558
+ ? row.cells[
559
+ dateColumns[
560
+ colIndex + 1
561
+ ].date
562
+ ]
563
+ : null;
564
+
565
+ // Check if this cell and adjacent cells have the same data/booking
566
+ const hasSamePrev =
567
+ prevCell &&
568
+ prevCell.hasData &&
569
+ cell.hasData &&
570
+ prevCell.data?.resource ===
571
+ cell.data?.resource &&
572
+ prevCell.data?.client ===
573
+ cell.data?.client;
574
+ const hasSameNext =
575
+ nextCell &&
576
+ nextCell.hasData &&
577
+ cell.hasData &&
578
+ nextCell.data?.resource ===
579
+ cell.data?.resource &&
580
+ nextCell.data?.client ===
581
+ cell.data?.client;
582
+
583
+ const isHovered =
584
+ hoveredColumn ===
585
+ dateCol.date &&
586
+ hoveredRow === row.id;
587
+
588
+ return (
589
+ <div
590
+ key={`${row.id}-${dateCol.date}`}
591
+ className="data-cell"
592
+ style={{
593
+ borderTop: isHovered
594
+ ? '2px solid #007bff'
595
+ : cell.hasData
596
+ ? '2px solid #333'
597
+ : '1px solid #dee2e6',
598
+ borderBottom:
599
+ isHovered
600
+ ? '2px solid #007bff'
601
+ : cell.hasData
602
+ ? '2px solid #333'
603
+ : '1px solid #dee2e6',
604
+ borderLeft:
605
+ isHovered
606
+ ? '2px solid #007bff'
607
+ : hasSamePrev
608
+ ? 'none'
609
+ : cell.hasData
610
+ ? '2px solid #333'
611
+ : '1px solid #dee2e6',
612
+ borderRight:
613
+ isHovered
614
+ ? '2px solid #007bff'
615
+ : hasSameNext
616
+ ? 'none'
617
+ : cell.hasData
618
+ ? '2px solid #333'
619
+ : '1px solid #dee2e6',
620
+ backgroundColor:
621
+ isHovered
622
+ ? cell.hasData
623
+ ? getCellColor(cell.data?.confirmed, true)
624
+ : '#e6f3ff'
625
+ : cell.hasData
626
+ ? getCellColor(cell.data?.confirmed, false)
627
+ : '#fff',
628
+ minHeight: '22px',
629
+ cursor: cell.hasData
630
+ ? 'pointer'
631
+ : 'default',
632
+ opacity: 1,
633
+ position:
634
+ 'relative',
635
+ boxShadow: isHovered
636
+ ? '0 0 6px rgba(0, 123, 255, 0.4)'
637
+ : cell.hasData
638
+ ? '0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 -1px 0 rgba(0, 0, 0, 0.1)'
639
+ : 'none',
640
+ zIndex: isHovered
641
+ ? 5
642
+ : cell.hasData
643
+ ? 2
644
+ : 1,
645
+ transition:
646
+ 'all 0.15s ease',
647
+ }}
648
+ onMouseEnter={(e) => {
649
+ setHoveredRow(
650
+ row.id
651
+ );
652
+ setHoveredColumn(
653
+ dateCol.date
654
+ );
655
+
656
+ if (
657
+ cell.hasData &&
658
+ cell.data
659
+ ) {
660
+ const dateRange =
661
+ getDateRangeForCell(
662
+ row,
663
+ dateCol.date
664
+ );
665
+ const tooltipData =
666
+ {
667
+ ...cell.data,
668
+ dateRange:
669
+ dateRange
670
+ ? `${moment(dateRange.startDate).format('DD/MM/YYYY')} - ${moment(dateRange.endDate).format('DD/MM/YYYY')}`
671
+ : null,
672
+ };
673
+
674
+ setTooltip({
675
+ visible: true,
676
+ data: tooltipData,
677
+ position: {
678
+ x: e.clientX,
679
+ y: e.clientY,
680
+ },
681
+ });
682
+ }
683
+ }}
684
+ onMouseLeave={() => {
685
+ // Don't clear hover states here - let the container handle it
686
+ }}
687
+ onMouseMove={(e) => {
688
+ if (
689
+ cell.hasData &&
690
+ tooltip.visible
691
+ ) {
692
+ setTooltip(
693
+ (prev) => ({
694
+ ...prev,
695
+ position:
696
+ {
697
+ x: e.clientX,
698
+ y: e.clientY,
699
+ },
700
+ })
701
+ );
702
+ }
703
+ }}
704
+ ></div>
705
+ );
706
+ }
707
+ )}
708
+ </div>
709
+ ))}
710
+ </div>
711
+ ))}
712
+ </div>
713
+ </div>
714
+
715
+ {/* Color Legend */}
716
+ <div
717
+ className="color-legend"
718
+ style={{
719
+ display: 'flex',
720
+ alignItems: 'center',
721
+ justifyContent: 'center',
722
+ marginTop: '15px',
723
+ padding: '10px',
724
+ backgroundColor: '#f8f9fa',
725
+ borderRadius: '6px',
726
+ fontSize: '12px',
727
+ fontWeight: '500',
728
+ gap: '20px',
729
+ }}
730
+ >
731
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
732
+ <div
733
+ style={{
734
+ width: '16px',
735
+ height: '16px',
736
+ backgroundColor: getCellColor(true),
737
+ borderRadius: '3px',
738
+ border: '1px solid #dee2e6',
739
+ }}
740
+ ></div>
741
+ <span>Confirmed</span>
742
+ </div>
743
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
744
+ <div
745
+ style={{
746
+ width: '16px',
747
+ height: '16px',
748
+ backgroundColor: getCellColor(false),
749
+ borderRadius: '3px',
750
+ border: '1px solid #dee2e6',
751
+ }}
752
+ ></div>
753
+ <span>Unconfirmed</span>
754
+ </div>
755
+ </div>
756
+
757
+ {/* Visual indicator that shows there's more content to scroll */}
758
+ {showScrollHint && (
759
+ <div
760
+ className="scroll-hint"
761
+ style={{
762
+ textAlign: 'center',
763
+ marginTop: '10px',
764
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
765
+ color: '#666',
766
+ padding: '8px 12px',
767
+ borderRadius: '6px',
768
+ fontSize: '12px',
769
+ fontWeight: '500',
770
+ transition: 'opacity 0.5s ease',
771
+ opacity: 0.8,
772
+ whiteSpace: 'nowrap',
773
+ }}
774
+ >
775
+ Use arrow keys or scroll buttons to navigate horizontally
776
+ </div>
777
+ )}
778
+
779
+ {/* Tooltip */}
780
+ <GridTooltip
781
+ data={tooltip.data}
782
+ visible={tooltip.visible}
783
+ position={tooltip.position}
784
+ />
785
+ </div>
786
+ );
787
+ };
788
+
789
+ export default ExcelStyleGrid;