@xcelsior/ui-spreadsheets 1.2.1 → 1.3.0
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/.omc/state/agent-replay-0cead415-b3bd-40fd-b199-47371946c4db.jsonl +25 -0
- package/.omc/state/idle-notif-cooldown.json +3 -0
- package/.omc/state/last-tool-error.json +7 -0
- package/.omc/state/mission-state.json +179 -0
- package/.omc/state/subagent-tracking.json +116 -0
- package/.turbo/turbo-build.log +28 -28
- package/.turbo/turbo-lint.log +140 -0
- package/dist/index.d.mts +94 -4
- package/dist/index.d.ts +94 -4
- package/dist/index.js +2133 -1155
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2023 -1047
- package/dist/index.mjs.map +1 -1
- package/dist/styles/globals.css +159 -16
- package/dist/styles/globals.css.map +1 -1
- package/package.json +1 -1
- package/plans/20260330-1230-spreadsheet-features/phase-01-types-and-duplicates-hook.md +73 -0
- package/plans/20260330-1230-spreadsheet-features/phase-02-filter-dropdown-portal.md +90 -0
- package/plans/20260330-1230-spreadsheet-features/phase-03-header-overflow-menu.md +101 -0
- package/plans/20260330-1230-spreadsheet-features/phase-04-integration.md +193 -0
- package/plans/20260330-1230-spreadsheet-features/plan.md +59 -0
- package/src/components/ColorPickerPopover.tsx +77 -32
- package/src/components/ColumnHeaderActions.tsx +241 -1
- package/src/components/RowIndexColumnHeader.tsx +13 -16
- package/src/components/SelectionSummaryBar.tsx +103 -0
- package/src/components/Spreadsheet.stories.tsx +396 -0
- package/src/components/Spreadsheet.tsx +233 -187
- package/src/components/SpreadsheetCell.tsx +280 -42
- package/src/components/SpreadsheetFilterDropdown.tsx +178 -13
- package/src/components/SpreadsheetHeader.tsx +79 -24
- package/src/components/SpreadsheetSettingsModal.tsx +4 -0
- package/src/hooks/useSpreadsheetColumnResize.ts +143 -0
- package/src/hooks/useSpreadsheetDuplicates.ts +149 -0
- package/src/hooks/useSpreadsheetFiltering.ts +18 -1
- package/src/hooks/useSpreadsheetHighlighting.ts +23 -3
- package/src/hooks/useSpreadsheetKeyboardShortcuts.ts +16 -0
- package/src/hooks/useSpreadsheetPinning.ts +148 -134
- package/src/hooks/useSpreadsheetSelection.ts +10 -22
- package/src/hooks/useSpreadsheetSummary.ts +68 -0
- package/src/index.ts +4 -1
- package/src/styles/globals.css +51 -0
- package/src/types.ts +50 -2
- package/storybook-static/assets/Color-YHDXOIA2-CtQurLnT.js +1 -0
- package/storybook-static/assets/DocsRenderer-CFRXHY34-oxrW8Hvo.js +575 -0
- package/storybook-static/assets/Spreadsheet.stories-DvhhzuK4.js +1357 -0
- package/storybook-static/assets/chunk-XP5HYGXS-BpfKkqn7.js +1 -0
- package/storybook-static/assets/entry-preview-CkBGHCAN.js +2 -0
- package/storybook-static/assets/entry-preview-docs-ugJb6pa8.js +46 -0
- package/storybook-static/assets/iframe-CPp2u3vg.js +211 -0
- package/storybook-static/assets/index-BB9bPxRC.js +24 -0
- package/storybook-static/assets/index-BQFlzFLk.js +9 -0
- package/storybook-static/assets/index-CtvPRVHf.js +9 -0
- package/storybook-static/assets/index-DgH-xKnr.js +11 -0
- package/storybook-static/assets/index-DrFu-skq.js +6 -0
- package/storybook-static/assets/index-DrdPSA1J.js +240 -0
- package/storybook-static/assets/index-DzFBShOR.js +20 -0
- package/storybook-static/assets/index-v-1boR4t.js +1 -0
- package/storybook-static/assets/preview-B8lJiyuQ.js +34 -0
- package/storybook-static/assets/preview-BBWR9nbA.js +1 -0
- package/storybook-static/assets/preview-BWzBA1C2.js +396 -0
- package/storybook-static/assets/preview-Bm0S-uxO.css +1 -0
- package/storybook-static/assets/preview-CvbIS5ZJ.js +1 -0
- package/storybook-static/assets/preview-DD_OYowb.js +1 -0
- package/storybook-static/assets/preview-DGUiP6tS.js +7 -0
- package/storybook-static/assets/preview-DHQbi4pV.js +1 -0
- package/storybook-static/assets/preview-DwI0w3cI.js +1 -0
- package/storybook-static/assets/preview-DyR7iiFG.js +1 -0
- package/storybook-static/assets/preview-zxZ6Be2V.js +2 -0
- package/storybook-static/assets/react-18-Pj8skaX9.js +1 -0
- package/storybook-static/assets/test-utils-quxJ1Z79.js +9 -0
- package/storybook-static/favicon.svg +1 -0
- package/storybook-static/iframe.html +666 -0
- package/storybook-static/index.html +177 -0
- package/storybook-static/index.json +1 -0
- package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/project.json +1 -0
- package/storybook-static/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
- package/storybook-static/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
- package/storybook-static/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
- package/storybook-static/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/interactions-10/manager-bundle.js +222 -0
- package/storybook-static/sb-addons/links-1/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
- package/storybook-static/sb-common-assets/favicon.svg +1 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/sb-manager/globals-module-info.js +1052 -0
- package/storybook-static/sb-manager/globals-runtime.js +42127 -0
- package/storybook-static/sb-manager/globals.js +48 -0
- package/storybook-static/sb-manager/runtime.js +12048 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, startTransition } from 'react';
|
|
3
3
|
import { HiChevronDown, HiChevronRight } from 'react-icons/hi';
|
|
4
4
|
import { AiFillHighlight } from 'react-icons/ai';
|
|
5
5
|
import { FaComment, FaRegComment } from 'react-icons/fa';
|
|
@@ -7,7 +7,7 @@ import { cn } from '../utils';
|
|
|
7
7
|
import { SpreadsheetCell } from './SpreadsheetCell';
|
|
8
8
|
import { SpreadsheetFilterDropdown } from './SpreadsheetFilterDropdown';
|
|
9
9
|
import { SpreadsheetToolbar } from './SpreadsheetToolbar';
|
|
10
|
-
import {
|
|
10
|
+
import { MemoizedSpreadsheetHeader } from './SpreadsheetHeader';
|
|
11
11
|
import { RowIndexColumnHeader } from './RowIndexColumnHeader';
|
|
12
12
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
|
13
13
|
import { type SpreadsheetSettings, SpreadsheetSettingsModal } from './SpreadsheetSettingsModal';
|
|
@@ -16,17 +16,20 @@ import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
|
|
|
16
16
|
import { RowContextMenu } from './RowContextMenu';
|
|
17
17
|
import { Pagination } from '@xcelsior/design-system';
|
|
18
18
|
import { useSpreadsheetFiltering } from '../hooks/useSpreadsheetFiltering';
|
|
19
|
+
import { useSpreadsheetDuplicates } from '../hooks/useSpreadsheetDuplicates';
|
|
19
20
|
import { useSpreadsheetHighlighting } from '../hooks/useSpreadsheetHighlighting';
|
|
20
21
|
import {
|
|
21
22
|
useSpreadsheetPinning,
|
|
22
23
|
ROW_INDEX_COLUMN_WIDTH,
|
|
23
24
|
ROW_INDEX_COLUMN_ID,
|
|
24
|
-
MIN_PINNED_COLUMN_WIDTH,
|
|
25
25
|
} from '../hooks/useSpreadsheetPinning';
|
|
26
26
|
import { useSpreadsheetComments } from '../hooks/useSpreadsheetComments';
|
|
27
27
|
import { useSpreadsheetUndoRedo } from '../hooks/useSpreadsheetUndoRedo';
|
|
28
28
|
import { useSpreadsheetKeyboardShortcuts } from '../hooks/useSpreadsheetKeyboardShortcuts';
|
|
29
29
|
import { useSpreadsheetSelection } from '../hooks/useSpreadsheetSelection';
|
|
30
|
+
import { useSpreadsheetSummary } from '../hooks/useSpreadsheetSummary';
|
|
31
|
+
import { useSpreadsheetColumnResize } from '../hooks/useSpreadsheetColumnResize';
|
|
32
|
+
import { SelectionSummaryBar } from './SelectionSummaryBar';
|
|
30
33
|
import type {
|
|
31
34
|
CellEdit,
|
|
32
35
|
SpreadsheetColumn,
|
|
@@ -126,6 +129,8 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
126
129
|
sortConfig: controlledSortConfig,
|
|
127
130
|
onPageChange,
|
|
128
131
|
filters: controlledFilters,
|
|
132
|
+
duplicateCheckColumns: propDuplicateCheckColumns,
|
|
133
|
+
onDuplicateCheckChange,
|
|
129
134
|
}: SpreadsheetProps<T>) {
|
|
130
135
|
// ==================== HOOKS ====================
|
|
131
136
|
|
|
@@ -139,6 +144,41 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
139
144
|
compactView: initialSettings?.compactView ?? false,
|
|
140
145
|
});
|
|
141
146
|
|
|
147
|
+
// Duplicate detection hook
|
|
148
|
+
const {
|
|
149
|
+
isCellDuplicate,
|
|
150
|
+
toggleDuplicateCheck,
|
|
151
|
+
duplicateCheckColumns,
|
|
152
|
+
duplicateRowIds,
|
|
153
|
+
getDuplicateColumnCount,
|
|
154
|
+
} = useSpreadsheetDuplicates({
|
|
155
|
+
data,
|
|
156
|
+
columns,
|
|
157
|
+
duplicateCheckColumns: propDuplicateCheckColumns ?? [],
|
|
158
|
+
getRowId,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Callback to handle toggling duplicate check with external notification
|
|
162
|
+
const handleDuplicateCheckToggle = useCallback(
|
|
163
|
+
(columnId: string) => {
|
|
164
|
+
setIsProcessing(true);
|
|
165
|
+
startTransition(() => {
|
|
166
|
+
toggleDuplicateCheck(columnId);
|
|
167
|
+
// Build updated list for external callback
|
|
168
|
+
const currentCols = Array.from(duplicateCheckColumns);
|
|
169
|
+
const next = currentCols.includes(columnId)
|
|
170
|
+
? currentCols.filter((id) => id !== columnId)
|
|
171
|
+
: [...currentCols, columnId];
|
|
172
|
+
onDuplicateCheckChange?.(next);
|
|
173
|
+
setIsProcessing(false);
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
[toggleDuplicateCheck, duplicateCheckColumns, onDuplicateCheckChange]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Processing overlay state
|
|
180
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
181
|
+
|
|
142
182
|
// Filtering and sorting hook
|
|
143
183
|
const {
|
|
144
184
|
filters,
|
|
@@ -160,6 +200,8 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
160
200
|
controlledFilters,
|
|
161
201
|
controlledSortConfig,
|
|
162
202
|
defaultSortConfig: spreadsheetSettings.defaultSort,
|
|
203
|
+
duplicateRowIds,
|
|
204
|
+
getRowId,
|
|
163
205
|
});
|
|
164
206
|
|
|
165
207
|
// Highlighting hook
|
|
@@ -176,6 +218,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
176
218
|
setHighlightPickerColumn,
|
|
177
219
|
highlightPickerCell,
|
|
178
220
|
setHighlightPickerCell,
|
|
221
|
+
recentColors,
|
|
179
222
|
} = useSpreadsheetHighlighting({
|
|
180
223
|
externalRowHighlights,
|
|
181
224
|
onRowHighlight,
|
|
@@ -185,6 +228,17 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
185
228
|
onCellHighlight,
|
|
186
229
|
});
|
|
187
230
|
|
|
231
|
+
// Column resize hook (must be before pinning hook so getColumnWidth is available for offset calculations)
|
|
232
|
+
const { getColumnWidth, getResizeHandleProps, isResizing, columnWidths } = useSpreadsheetColumnResize({
|
|
233
|
+
initialColumnWidths: initialSettings?.columnWidths,
|
|
234
|
+
onColumnResize: (columnId, width) => {
|
|
235
|
+
setSpreadsheetSettings((prev) => ({
|
|
236
|
+
...prev,
|
|
237
|
+
columnWidths: { ...prev.columnWidths, [columnId]: width },
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
188
242
|
// Pinning hook
|
|
189
243
|
const {
|
|
190
244
|
pinnedColumns,
|
|
@@ -198,11 +252,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
198
252
|
getColumnRightOffset,
|
|
199
253
|
isColumnPinned,
|
|
200
254
|
getColumnPinSide,
|
|
255
|
+
getPinnedZIndex,
|
|
256
|
+
measureRef,
|
|
201
257
|
} = useSpreadsheetPinning({
|
|
202
258
|
columns,
|
|
203
259
|
columnGroups,
|
|
204
260
|
defaultPinnedColumns: initialSettings?.defaultPinnedColumns,
|
|
205
261
|
defaultPinnedRightColumns: initialSettings?.defaultPinnedRightColumns,
|
|
262
|
+
getColumnWidth,
|
|
206
263
|
});
|
|
207
264
|
|
|
208
265
|
// Comments hook
|
|
@@ -252,7 +309,9 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
252
309
|
// Row selection state
|
|
253
310
|
const [selectedRows, setSelectedRows] = useState<Set<string | number>>(new Set());
|
|
254
311
|
const [lastSelectedRow, setLastSelectedRow] = useState<string | number | null>(null);
|
|
255
|
-
|
|
312
|
+
// hoveredRow uses a ref instead of state to avoid full re-renders on mouse move
|
|
313
|
+
// Hover styling is handled via CSS tr:hover in the stylesheet
|
|
314
|
+
const hoveredRowRef = useRef<string | number | null>(null);
|
|
256
315
|
|
|
257
316
|
// Pagination state (supports both controlled and uncontrolled modes)
|
|
258
317
|
const [internalCurrentPage, setInternalCurrentPage] = useState(1);
|
|
@@ -318,6 +377,23 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
318
377
|
}));
|
|
319
378
|
}, [sortConfig]);
|
|
320
379
|
|
|
380
|
+
// Sync pinned columns from external settings (e.g., after hydration from localStorage)
|
|
381
|
+
// Only fires once when settings arrive with pinned columns that differ from current state
|
|
382
|
+
const hasSyncedPinnedFromSettings = useRef(false);
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (hasSyncedPinnedFromSettings.current) return;
|
|
385
|
+
const settingsPinned = initialSettings?.defaultPinnedColumns;
|
|
386
|
+
if (settingsPinned && settingsPinned.length > 0) {
|
|
387
|
+
// Check if current pinnedColumns already matches (initialized correctly)
|
|
388
|
+
const currentIds = Array.from(pinnedColumns.keys());
|
|
389
|
+
const hasAllSettingsPinned = settingsPinned.every((id) => pinnedColumns.has(id));
|
|
390
|
+
if (!hasAllSettingsPinned) {
|
|
391
|
+
setPinnedColumnsFromIds(settingsPinned);
|
|
392
|
+
}
|
|
393
|
+
hasSyncedPinnedFromSettings.current = true;
|
|
394
|
+
}
|
|
395
|
+
}, [initialSettings?.defaultPinnedColumns, pinnedColumns, setPinnedColumnsFromIds]);
|
|
396
|
+
|
|
321
397
|
// Sync pinned columns to spreadsheetSettings when pinning changes
|
|
322
398
|
useEffect(() => {
|
|
323
399
|
const pinnedColumnIds = Array.from(pinnedColumns.keys());
|
|
@@ -398,9 +474,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
398
474
|
// Cell selection hook (for multi-cell selection, copy/paste)
|
|
399
475
|
const {
|
|
400
476
|
focusedCell,
|
|
477
|
+
setFocusedCell,
|
|
401
478
|
editingCell,
|
|
402
479
|
setEditingCell,
|
|
480
|
+
selectedCellRange,
|
|
481
|
+
setSelectedCellRange,
|
|
403
482
|
handleCellMouseDown,
|
|
483
|
+
handleCellMouseEnter,
|
|
484
|
+
handleMouseUp,
|
|
404
485
|
isCellInSelection,
|
|
405
486
|
getCellSelectionEdge,
|
|
406
487
|
clearSelection,
|
|
@@ -409,6 +490,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
409
490
|
enterEditMode,
|
|
410
491
|
copySelectedCells,
|
|
411
492
|
pasteFromSystemClipboard,
|
|
493
|
+
getSelectedCellValues,
|
|
412
494
|
} = useSpreadsheetSelection({
|
|
413
495
|
data: paginatedData as T[],
|
|
414
496
|
columns: visibleColumns,
|
|
@@ -416,6 +498,13 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
416
498
|
enableCellEditing,
|
|
417
499
|
});
|
|
418
500
|
|
|
501
|
+
// Summary hook for selection statistics
|
|
502
|
+
const selectedCellValues = useMemo(() => getSelectedCellValues(), [getSelectedCellValues]);
|
|
503
|
+
const { summary: selectionSummary, hasNumericValues } = useSpreadsheetSummary({
|
|
504
|
+
selectedCellValues,
|
|
505
|
+
columns: visibleColumns,
|
|
506
|
+
});
|
|
507
|
+
|
|
419
508
|
// Escape handler for clearing modals and selection
|
|
420
509
|
const handleEscapeCallback = useCallback(() => {
|
|
421
510
|
if (commentModalCell !== null) {
|
|
@@ -523,6 +612,19 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
523
612
|
onTabNavigation: handleTabNavigation,
|
|
524
613
|
onCopy: copySelectedCells,
|
|
525
614
|
onPaste: handlePaste,
|
|
615
|
+
onSelectAll: () => {
|
|
616
|
+
if (paginatedData.length > 0 && visibleColumns.length > 0) {
|
|
617
|
+
const firstRow = paginatedData[0];
|
|
618
|
+
const lastRow = paginatedData[paginatedData.length - 1];
|
|
619
|
+
const firstCol = visibleColumns[0];
|
|
620
|
+
const lastCol = visibleColumns[visibleColumns.length - 1];
|
|
621
|
+
setSelectedCellRange({
|
|
622
|
+
start: { rowId: getRowId(firstRow), columnId: firstCol.id },
|
|
623
|
+
end: { rowId: getRowId(lastRow), columnId: lastCol.id },
|
|
624
|
+
});
|
|
625
|
+
setFocusedCell({ rowId: getRowId(firstRow), columnId: firstCol.id });
|
|
626
|
+
}
|
|
627
|
+
},
|
|
526
628
|
hasFocusedCell: focusedCell !== null,
|
|
527
629
|
isEditing: editingCell !== null,
|
|
528
630
|
enabled: true,
|
|
@@ -616,19 +718,26 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
616
718
|
]
|
|
617
719
|
);
|
|
618
720
|
|
|
619
|
-
// Handle cell click -
|
|
620
|
-
// but also handle entering edit mode for editable cells
|
|
721
|
+
// Handle cell click - focus only (edit mode requires double-click or F2)
|
|
621
722
|
const handleCellClick = useCallback(
|
|
622
723
|
(rowId: string | number, columnId: string, event: React.MouseEvent) => {
|
|
623
724
|
event.stopPropagation();
|
|
624
725
|
handleCellMouseDown(rowId, columnId, event);
|
|
625
|
-
|
|
626
|
-
// Double-click to edit is handled by the selection hook
|
|
627
|
-
// For single click, we just focus. Edit mode is entered via handleCellMouseDown for editable cells
|
|
628
726
|
},
|
|
629
727
|
[handleCellMouseDown]
|
|
630
728
|
);
|
|
631
729
|
|
|
730
|
+
// Handle cell double-click - enter edit mode
|
|
731
|
+
const handleCellDoubleClick = useCallback(
|
|
732
|
+
(rowId: string | number, columnId: string) => {
|
|
733
|
+
const column = columns.find((c) => c.id === columnId);
|
|
734
|
+
if (column?.editable && enableCellEditing) {
|
|
735
|
+
setEditingCell({ rowId, columnId });
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
[columns, enableCellEditing, setEditingCell]
|
|
739
|
+
);
|
|
740
|
+
|
|
632
741
|
const handleCellChange = useCallback(
|
|
633
742
|
(rowId: string | number, columnId: string, newValue: any) => {
|
|
634
743
|
const row = data.find((r) => getRowId(r) === rowId);
|
|
@@ -676,8 +785,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
676
785
|
setHighlightPickerColumn(ROW_INDEX_COLUMN_ID);
|
|
677
786
|
}, [setHighlightPickerColumn]);
|
|
678
787
|
|
|
679
|
-
// Build render items
|
|
680
|
-
// Pinned columns are moved to the edges: left-pinned first, then unpinned, then right-pinned
|
|
788
|
+
// Build render items in group order — collapsed groups get a placeholder, columns stay in natural order
|
|
681
789
|
const columnRenderItems = useMemo(() => {
|
|
682
790
|
if (!columnGroups || columnGroups.length === 0) {
|
|
683
791
|
return visibleColumns.map((col) => ({
|
|
@@ -694,83 +802,43 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
694
802
|
};
|
|
695
803
|
type RenderItem = ColumnItem | PlaceholderItem;
|
|
696
804
|
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
const rightPinnedItems: ColumnItem[] = [];
|
|
805
|
+
const items: RenderItem[] = [];
|
|
806
|
+
const allGroupedIds = new Set(columnGroups.flatMap((g) => g.columns));
|
|
700
807
|
|
|
808
|
+
// Add ungrouped columns first (if any appear before the first group)
|
|
809
|
+
for (const col of visibleColumns) {
|
|
810
|
+
if (!allGroupedIds.has(col.id)) {
|
|
811
|
+
items.push({ type: 'column', column: col });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Add columns by group order
|
|
701
816
|
for (const group of columnGroups) {
|
|
702
817
|
const isCollapsed = collapsedGroups.has(group.id);
|
|
818
|
+
const groupVisibleCols = visibleColumns.filter((c) => group.columns.includes(c.id));
|
|
703
819
|
|
|
704
820
|
if (isCollapsed) {
|
|
705
|
-
|
|
821
|
+
// Add collapsed placeholder
|
|
822
|
+
items.push({
|
|
706
823
|
type: 'collapsed-placeholder',
|
|
707
824
|
groupId: group.id,
|
|
708
825
|
headerColor: group.headerColor,
|
|
709
826
|
});
|
|
710
827
|
}
|
|
711
828
|
|
|
712
|
-
//
|
|
713
|
-
const groupVisibleCols = (columns || []).filter((c) => {
|
|
714
|
-
if (!group.columns.includes(c.id)) return false;
|
|
715
|
-
if (isCollapsed) return pinnedColumns.has(c.id);
|
|
716
|
-
return true;
|
|
717
|
-
});
|
|
718
|
-
|
|
829
|
+
// Add visible columns from this group
|
|
719
830
|
for (const col of groupVisibleCols) {
|
|
720
|
-
|
|
721
|
-
if (pinSide === 'left') {
|
|
722
|
-
leftPinnedItems.push({ type: 'column', column: col });
|
|
723
|
-
} else if (pinSide === 'right') {
|
|
724
|
-
rightPinnedItems.push({ type: 'column', column: col });
|
|
725
|
-
} else {
|
|
726
|
-
middleItems.push({ type: 'column', column: col });
|
|
727
|
-
}
|
|
831
|
+
items.push({ type: 'column', column: col });
|
|
728
832
|
}
|
|
729
833
|
}
|
|
730
834
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
for (const col of visibleColumns) {
|
|
734
|
-
if (!allGroupedIds.has(col.id)) {
|
|
735
|
-
const pinSide = pinnedColumns.get(col.id);
|
|
736
|
-
if (pinSide === 'left') {
|
|
737
|
-
leftPinnedItems.push({ type: 'column', column: col });
|
|
738
|
-
} else if (pinSide === 'right') {
|
|
739
|
-
rightPinnedItems.push({ type: 'column', column: col });
|
|
740
|
-
} else {
|
|
741
|
-
middleItems.push({ type: 'column', column: col });
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
835
|
+
return items;
|
|
836
|
+
}, [columnGroups, collapsedGroups, visibleColumns]);
|
|
745
837
|
|
|
746
|
-
|
|
747
|
-
const pinnedLeftOrder = Array.from(pinnedColumns.entries())
|
|
748
|
-
.filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
|
|
749
|
-
.map(([id]) => id);
|
|
750
|
-
const pinnedRightOrder = Array.from(pinnedColumns.entries())
|
|
751
|
-
.filter(([, side]) => side === 'right')
|
|
752
|
-
.map(([id]) => id);
|
|
753
|
-
|
|
754
|
-
leftPinnedItems.sort(
|
|
755
|
-
(a, b) => pinnedLeftOrder.indexOf(a.column.id) - pinnedLeftOrder.indexOf(b.column.id)
|
|
756
|
-
);
|
|
757
|
-
rightPinnedItems.sort(
|
|
758
|
-
(a, b) => pinnedRightOrder.indexOf(a.column.id) - pinnedRightOrder.indexOf(b.column.id)
|
|
759
|
-
);
|
|
760
|
-
|
|
761
|
-
return [...leftPinnedItems, ...middleItems, ...rightPinnedItems];
|
|
762
|
-
}, [columnGroups, collapsedGroups, columns, pinnedColumns, visibleColumns]);
|
|
763
|
-
|
|
764
|
-
// Build group header items that account for pinned columns being moved to edges
|
|
838
|
+
// Build group header items — columns stay in their groups (freeze panes, no splitting)
|
|
765
839
|
const groupHeaderItems = useMemo(() => {
|
|
766
840
|
if (!columnGroups || columnGroups.length === 0) return null;
|
|
767
841
|
|
|
768
|
-
type PinnedGroupHeaderItem = {
|
|
769
|
-
type: 'pinned-column';
|
|
770
|
-
columnId: string;
|
|
771
|
-
headerColor?: string;
|
|
772
|
-
pinSide: 'left' | 'right';
|
|
773
|
-
};
|
|
774
842
|
type GroupHeaderItem = {
|
|
775
843
|
type: 'group';
|
|
776
844
|
group: SpreadsheetColumnGroup;
|
|
@@ -778,46 +846,16 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
778
846
|
isCollapsed: boolean;
|
|
779
847
|
};
|
|
780
848
|
|
|
781
|
-
const
|
|
782
|
-
const groups: GroupHeaderItem[] = [];
|
|
783
|
-
const rightPinned: PinnedGroupHeaderItem[] = [];
|
|
849
|
+
const items: GroupHeaderItem[] = [];
|
|
784
850
|
|
|
785
851
|
for (const group of columnGroups) {
|
|
786
852
|
const isCollapsed = collapsedGroups.has(group.id);
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
: groupColumns;
|
|
791
|
-
|
|
792
|
-
let movedLeftCount = 0;
|
|
793
|
-
let movedRightCount = 0;
|
|
794
|
-
|
|
795
|
-
for (const col of visibleGroupColumns) {
|
|
796
|
-
const pinSide = pinnedColumns.get(col.id);
|
|
797
|
-
if (pinSide === 'left') {
|
|
798
|
-
movedLeftCount++;
|
|
799
|
-
leftPinned.push({
|
|
800
|
-
type: 'pinned-column',
|
|
801
|
-
columnId: col.id,
|
|
802
|
-
headerColor: group.headerColor,
|
|
803
|
-
pinSide: 'left',
|
|
804
|
-
});
|
|
805
|
-
} else if (pinSide === 'right') {
|
|
806
|
-
movedRightCount++;
|
|
807
|
-
rightPinned.push({
|
|
808
|
-
type: 'pinned-column',
|
|
809
|
-
columnId: col.id,
|
|
810
|
-
headerColor: group.headerColor,
|
|
811
|
-
pinSide: 'right',
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const remainingCols = visibleGroupColumns.length - movedLeftCount - movedRightCount;
|
|
817
|
-
const colSpan = remainingCols + (isCollapsed ? 1 : 0);
|
|
853
|
+
// Count visible columns in this group (from visibleColumns which respects collapse)
|
|
854
|
+
const visibleCount = visibleColumns.filter((c) => group.columns.includes(c.id)).length;
|
|
855
|
+
const colSpan = visibleCount + (isCollapsed ? 1 : 0); // +1 for collapsed placeholder
|
|
818
856
|
|
|
819
857
|
if (colSpan > 0) {
|
|
820
|
-
|
|
858
|
+
items.push({
|
|
821
859
|
type: 'group',
|
|
822
860
|
group,
|
|
823
861
|
colSpan,
|
|
@@ -826,23 +864,9 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
826
864
|
}
|
|
827
865
|
}
|
|
828
866
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
.filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
|
|
832
|
-
.map(([id]) => id);
|
|
833
|
-
const pinnedRightOrder = Array.from(pinnedColumns.entries())
|
|
834
|
-
.filter(([, side]) => side === 'right')
|
|
835
|
-
.map(([id]) => id);
|
|
867
|
+
return items;
|
|
868
|
+
}, [columnGroups, collapsedGroups, visibleColumns]);
|
|
836
869
|
|
|
837
|
-
leftPinned.sort(
|
|
838
|
-
(a, b) => pinnedLeftOrder.indexOf(a.columnId) - pinnedLeftOrder.indexOf(b.columnId)
|
|
839
|
-
);
|
|
840
|
-
rightPinned.sort(
|
|
841
|
-
(a, b) => pinnedRightOrder.indexOf(a.columnId) - pinnedRightOrder.indexOf(b.columnId)
|
|
842
|
-
);
|
|
843
|
-
|
|
844
|
-
return [...leftPinned, ...groups, ...rightPinned];
|
|
845
|
-
}, [columnGroups, collapsedGroups, columns, pinnedColumns]);
|
|
846
870
|
|
|
847
871
|
// ==================== RENDER ====================
|
|
848
872
|
|
|
@@ -885,14 +909,18 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
885
909
|
)}
|
|
886
910
|
|
|
887
911
|
{/* Table Container */}
|
|
888
|
-
<div ref={tableRef} className=
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
912
|
+
<div ref={tableRef} className={cn('flex-1 overflow-auto border border-gray-200 rounded spreadsheet-scroll-container relative', isResizing && 'select-none')} onMouseUp={handleMouseUp}>
|
|
913
|
+
{/* Processing overlay */}
|
|
914
|
+
{isProcessing && (
|
|
915
|
+
<div className="spreadsheet-processing-overlay">
|
|
916
|
+
<div className="flex items-center gap-2 text-gray-500">
|
|
917
|
+
<div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
|
|
918
|
+
Processing...
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
)}
|
|
922
|
+
<table ref={measureRef as React.Ref<HTMLTableElement>} className="border-separate border-spacing-0 text-sm select-none" style={zoom !== 100 ? { zoom: zoom / 100 } : undefined}>
|
|
923
|
+
<thead className="sticky top-0" style={{ zIndex: 50 }}>
|
|
896
924
|
{/* Column Group Headers */}
|
|
897
925
|
{columnGroups && groupHeaderItems && (
|
|
898
926
|
<tr>
|
|
@@ -904,37 +932,6 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
904
932
|
compactMode={effectiveCompactMode}
|
|
905
933
|
/>
|
|
906
934
|
{groupHeaderItems.map((item) => {
|
|
907
|
-
if (item.type === 'pinned-column') {
|
|
908
|
-
const col = columns.find((c) => c.id === item.columnId);
|
|
909
|
-
const isPinnedLeft = item.pinSide === 'left';
|
|
910
|
-
const pinnedWidth = Math.max(
|
|
911
|
-
col?.minWidth ||
|
|
912
|
-
col?.width ||
|
|
913
|
-
MIN_PINNED_COLUMN_WIDTH,
|
|
914
|
-
MIN_PINNED_COLUMN_WIDTH
|
|
915
|
-
);
|
|
916
|
-
return (
|
|
917
|
-
<th
|
|
918
|
-
key={`pinned-group-${item.columnId}`}
|
|
919
|
-
className={cn(
|
|
920
|
-
'border border-gray-200 px-2 py-1.5 text-center font-bold text-gray-700',
|
|
921
|
-
'z-30'
|
|
922
|
-
)}
|
|
923
|
-
style={{
|
|
924
|
-
backgroundColor:
|
|
925
|
-
item.headerColor || 'rgb(243 244 246)',
|
|
926
|
-
position: 'sticky',
|
|
927
|
-
left: isPinnedLeft
|
|
928
|
-
? `${getColumnLeftOffset(item.columnId)}px`
|
|
929
|
-
: undefined,
|
|
930
|
-
right: !isPinnedLeft
|
|
931
|
-
? `${getColumnRightOffset(item.columnId)}px`
|
|
932
|
-
: undefined,
|
|
933
|
-
minWidth: pinnedWidth,
|
|
934
|
-
}}
|
|
935
|
-
/>
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
935
|
const { group, colSpan, isCollapsed } = item;
|
|
939
936
|
return (
|
|
940
937
|
<th
|
|
@@ -1004,13 +1001,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1004
1001
|
isColumnPinned(column.id) &&
|
|
1005
1002
|
getColumnPinSide(column.id) === 'right';
|
|
1006
1003
|
return (
|
|
1007
|
-
<
|
|
1004
|
+
<MemoizedSpreadsheetHeader
|
|
1008
1005
|
key={column.id}
|
|
1009
1006
|
column={column}
|
|
1010
1007
|
sortConfig={sortConfig}
|
|
1011
1008
|
hasActiveFilter={!!filters[column.id]}
|
|
1012
1009
|
isPinned={isColumnPinned(column.id)}
|
|
1013
1010
|
pinSide={getColumnPinSide(column.id)}
|
|
1011
|
+
pinnedZIndex={isColumnPinned(column.id) ? getPinnedZIndex(column.id) : undefined}
|
|
1014
1012
|
leftOffset={
|
|
1015
1013
|
isPinnedLeft ? getColumnLeftOffset(column.id) : 0
|
|
1016
1014
|
}
|
|
@@ -1019,7 +1017,30 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1019
1017
|
}
|
|
1020
1018
|
highlightColor={getColumnHighlight(column.id)}
|
|
1021
1019
|
compactMode={effectiveCompactMode}
|
|
1022
|
-
onClick={() =>
|
|
1020
|
+
onClick={() => {
|
|
1021
|
+
// Toggle column selection
|
|
1022
|
+
if (paginatedData.length > 0) {
|
|
1023
|
+
const firstRowId = getRowId(paginatedData[0]);
|
|
1024
|
+
const lastRowId = getRowId(paginatedData[paginatedData.length - 1]);
|
|
1025
|
+
// Check if this column is already fully selected — deselect if so
|
|
1026
|
+
const isAlreadySelected =
|
|
1027
|
+
selectedCellRange?.start.columnId === column.id &&
|
|
1028
|
+
selectedCellRange?.end.columnId === column.id &&
|
|
1029
|
+
selectedCellRange?.start.rowId === firstRowId &&
|
|
1030
|
+
selectedCellRange?.end.rowId === lastRowId;
|
|
1031
|
+
if (isAlreadySelected) {
|
|
1032
|
+
setSelectedCellRange(null);
|
|
1033
|
+
setFocusedCell(null);
|
|
1034
|
+
} else {
|
|
1035
|
+
setSelectedCellRange({
|
|
1036
|
+
start: { rowId: firstRowId, columnId: column.id },
|
|
1037
|
+
end: { rowId: lastRowId, columnId: column.id },
|
|
1038
|
+
});
|
|
1039
|
+
setFocusedCell({ rowId: firstRowId, columnId: column.id });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}}
|
|
1043
|
+
onSortClick={() => handleSort(column.id)}
|
|
1023
1044
|
onFilterClick={() =>
|
|
1024
1045
|
setActiveFilterColumn(
|
|
1025
1046
|
activeFilterColumn === column.id
|
|
@@ -1033,6 +1054,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1033
1054
|
? () => setHighlightPickerColumn(column.id)
|
|
1034
1055
|
: undefined
|
|
1035
1056
|
}
|
|
1057
|
+
resizeHandleProps={getResizeHandleProps(
|
|
1058
|
+
column.id,
|
|
1059
|
+
column.width || column.minWidth || 100
|
|
1060
|
+
)}
|
|
1061
|
+
resolvedWidth={getColumnWidth(column.id)}
|
|
1062
|
+
hasDuplicateCheck={duplicateCheckColumns.has(column.id)}
|
|
1063
|
+
onDuplicateCheckClick={() => handleDuplicateCheckToggle(column.id)}
|
|
1064
|
+
duplicateCount={getDuplicateColumnCount(column.id)}
|
|
1036
1065
|
>
|
|
1037
1066
|
{/* Filter dropdown */}
|
|
1038
1067
|
{activeFilterColumn === column.id && (
|
|
@@ -1046,9 +1075,10 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1046
1075
|
)
|
|
1047
1076
|
}
|
|
1048
1077
|
onClose={() => setActiveFilterColumn(null)}
|
|
1078
|
+
hasDuplicateCheck={duplicateCheckColumns.has(column.id)}
|
|
1049
1079
|
/>
|
|
1050
1080
|
)}
|
|
1051
|
-
</
|
|
1081
|
+
</MemoizedSpreadsheetHeader>
|
|
1052
1082
|
);
|
|
1053
1083
|
})}
|
|
1054
1084
|
</tr>
|
|
@@ -1080,7 +1110,6 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1080
1110
|
paginatedData.map((row, rowIndex) => {
|
|
1081
1111
|
const rowId = getRowId(row);
|
|
1082
1112
|
const isRowSelected = selectedRows.has(rowId);
|
|
1083
|
-
const isRowHovered = hoveredRow === rowId;
|
|
1084
1113
|
const rowHighlight = getRowHighlight(rowId);
|
|
1085
1114
|
const displayIndex =
|
|
1086
1115
|
rowIndex + 1 + (currentPage - 1) * pageSize;
|
|
@@ -1088,8 +1117,8 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1088
1117
|
return (
|
|
1089
1118
|
<tr
|
|
1090
1119
|
key={rowId}
|
|
1091
|
-
onMouseEnter={() =>
|
|
1092
|
-
onMouseLeave={() =>
|
|
1120
|
+
onMouseEnter={() => { hoveredRowRef.current = rowId; }}
|
|
1121
|
+
onMouseLeave={() => { hoveredRowRef.current = null; }}
|
|
1093
1122
|
onClick={() => {
|
|
1094
1123
|
onRowClick?.(row, rowIndex);
|
|
1095
1124
|
}}
|
|
@@ -1097,42 +1126,36 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1097
1126
|
>
|
|
1098
1127
|
{/* Row Index Column */}
|
|
1099
1128
|
<td
|
|
1129
|
+
data-column-id="__row_index__"
|
|
1100
1130
|
onClick={(e) => handleRowSelect(rowId, e)}
|
|
1101
1131
|
className={cn(
|
|
1102
1132
|
'border border-gray-200 text-center font-semibold cursor-pointer group',
|
|
1103
1133
|
effectiveCompactMode
|
|
1104
|
-
? 'text-
|
|
1105
|
-
: 'text-
|
|
1106
|
-
|
|
1134
|
+
? 'text-xs px-1.5 py-0.5'
|
|
1135
|
+
: 'text-sm px-2.5 py-1.5',
|
|
1136
|
+
'sticky',
|
|
1107
1137
|
isRowSelected && 'bg-blue-100',
|
|
1108
|
-
!isRowSelected && rowHighlight && ''
|
|
1109
|
-
isRowHovered &&
|
|
1110
|
-
!isRowSelected &&
|
|
1111
|
-
!rowHighlight &&
|
|
1112
|
-
'bg-gray-50'
|
|
1138
|
+
!isRowSelected && rowHighlight && ''
|
|
1113
1139
|
)}
|
|
1114
1140
|
style={{
|
|
1115
1141
|
backgroundColor:
|
|
1116
1142
|
rowHighlight?.color ||
|
|
1117
1143
|
(isRowSelected
|
|
1118
1144
|
? '#dbeafe'
|
|
1119
|
-
:
|
|
1120
|
-
? '#f9fafb'
|
|
1121
|
-
: rowIndexHighlightColor || 'white'),
|
|
1145
|
+
: rowIndexHighlightColor || (rowIndex % 2 !== 0 ? '#f9fafb' : 'white')),
|
|
1122
1146
|
minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
1123
1147
|
width: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
}),
|
|
1148
|
+
position: 'sticky' as const,
|
|
1149
|
+
left: 0,
|
|
1150
|
+
zIndex: 40,
|
|
1128
1151
|
}}
|
|
1129
1152
|
>
|
|
1130
|
-
<div className="relative flex items-center
|
|
1131
|
-
{/* Row number -
|
|
1132
|
-
<span>{displayIndex}</span>
|
|
1153
|
+
<div className="relative flex items-center w-full h-full">
|
|
1154
|
+
{/* Row number - left aligned to leave room for hover icons */}
|
|
1155
|
+
<span className="pl-1">{displayIndex}</span>
|
|
1133
1156
|
|
|
1134
|
-
{/* Action buttons - absolute
|
|
1135
|
-
<div className="absolute inset-0 flex items-center
|
|
1157
|
+
{/* Action buttons - absolute right side to avoid blocking row select */}
|
|
1158
|
+
<div className="absolute inset-y-0 right-0 flex items-center gap-0.5 pr-0.5">
|
|
1136
1159
|
{/* Context Menu (3-dot menu for row actions) */}
|
|
1137
1160
|
{rowContextMenuItems &&
|
|
1138
1161
|
rowContextMenuItems.length > 0 && (
|
|
@@ -1309,11 +1332,15 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1309
1332
|
isInSelection={isInSelection}
|
|
1310
1333
|
selectionEdge={selectionEdge}
|
|
1311
1334
|
isRowSelected={isRowSelected}
|
|
1312
|
-
isRowHovered={
|
|
1335
|
+
isRowHovered={false}
|
|
1313
1336
|
highlightColor={cellOrRowOrColumnHighlight}
|
|
1337
|
+
isDuplicate={isCellDuplicate(rowId, column.id)}
|
|
1314
1338
|
compactMode={effectiveCompactMode}
|
|
1339
|
+
isOddRow={rowIndex % 2 !== 0}
|
|
1340
|
+
resolvedWidth={getColumnWidth(column.id)}
|
|
1315
1341
|
isPinned={isColPinned}
|
|
1316
1342
|
pinSide={colPinSide}
|
|
1343
|
+
pinnedZIndex={isColPinned ? getPinnedZIndex(column.id) : undefined}
|
|
1317
1344
|
leftOffset={getColumnLeftOffset(column.id)}
|
|
1318
1345
|
rightOffset={getColumnRightOffset(
|
|
1319
1346
|
column.id
|
|
@@ -1321,6 +1348,12 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1321
1348
|
onClick={(e) =>
|
|
1322
1349
|
handleCellClick(rowId, column.id, e)
|
|
1323
1350
|
}
|
|
1351
|
+
onDoubleClick={() =>
|
|
1352
|
+
handleCellDoubleClick(rowId, column.id)
|
|
1353
|
+
}
|
|
1354
|
+
onMouseEnter={() =>
|
|
1355
|
+
handleCellMouseEnter(rowId, column.id)
|
|
1356
|
+
}
|
|
1324
1357
|
onConfirm={handleConfirmEdit}
|
|
1325
1358
|
onCancel={handleCancelEdit}
|
|
1326
1359
|
onHighlight={
|
|
@@ -1369,9 +1402,19 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1369
1402
|
)}
|
|
1370
1403
|
</tbody>
|
|
1371
1404
|
</table>
|
|
1372
|
-
</div>
|
|
1373
1405
|
</div>
|
|
1374
1406
|
|
|
1407
|
+
{/* Status Bar (address + selection summary) */}
|
|
1408
|
+
<SelectionSummaryBar
|
|
1409
|
+
summary={selectionSummary}
|
|
1410
|
+
focusedCell={focusedCell}
|
|
1411
|
+
columns={visibleColumns}
|
|
1412
|
+
data={paginatedData as T[]}
|
|
1413
|
+
getRowId={getRowId}
|
|
1414
|
+
currentPage={currentPage}
|
|
1415
|
+
pageSize={pageSize}
|
|
1416
|
+
/>
|
|
1417
|
+
|
|
1375
1418
|
{/* Pagination */}
|
|
1376
1419
|
{showPagination && effectiveTotalItems > 0 && (
|
|
1377
1420
|
<Pagination
|
|
@@ -1441,6 +1484,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1441
1484
|
<ColorPickerPopover
|
|
1442
1485
|
title="Highlight Row"
|
|
1443
1486
|
paletteType="row"
|
|
1487
|
+
recentColors={recentColors}
|
|
1444
1488
|
onSelectColor={(color) => handleRowHighlightToggle(highlightPickerRow, color)}
|
|
1445
1489
|
onClose={() => setHighlightPickerRow(null)}
|
|
1446
1490
|
/>
|
|
@@ -1455,6 +1499,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1455
1499
|
: `Highlight Column: ${columns.find((c) => c.id === highlightPickerColumn)?.label || ''}`
|
|
1456
1500
|
}
|
|
1457
1501
|
paletteType="column"
|
|
1502
|
+
recentColors={recentColors}
|
|
1458
1503
|
onSelectColor={(color) =>
|
|
1459
1504
|
handleColumnHighlightToggle(highlightPickerColumn, color)
|
|
1460
1505
|
}
|
|
@@ -1467,6 +1512,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1467
1512
|
<ColorPickerPopover
|
|
1468
1513
|
title="Highlight Cell"
|
|
1469
1514
|
paletteType="row"
|
|
1515
|
+
recentColors={recentColors}
|
|
1470
1516
|
onSelectColor={(color) =>
|
|
1471
1517
|
handleCellHighlightToggle(
|
|
1472
1518
|
highlightPickerCell.rowId,
|