@xcelsior/ui-spreadsheets 1.2.2 → 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 -1156
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2023 -1048
- package/dist/index.mjs.map +1 -1
- package/dist/styles/globals.css +156 -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 -17
- package/src/components/SelectionSummaryBar.tsx +103 -0
- package/src/components/Spreadsheet.stories.tsx +254 -0
- package/src/components/Spreadsheet.tsx +234 -189
- 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
|
|
@@ -985,12 +982,11 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
985
982
|
return (
|
|
986
983
|
<th
|
|
987
984
|
key={`${item.groupId}-placeholder`}
|
|
988
|
-
className="border border-gray-200 px-2 py-1 text-center text-gray-400
|
|
985
|
+
className="border border-gray-200 px-2 py-1 text-center text-gray-400"
|
|
989
986
|
style={{
|
|
990
987
|
backgroundColor:
|
|
991
988
|
item.headerColor || 'rgb(243 244 246)',
|
|
992
989
|
minWidth: '30px',
|
|
993
|
-
top: 0,
|
|
994
990
|
}}
|
|
995
991
|
>
|
|
996
992
|
...
|
|
@@ -1005,13 +1001,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1005
1001
|
isColumnPinned(column.id) &&
|
|
1006
1002
|
getColumnPinSide(column.id) === 'right';
|
|
1007
1003
|
return (
|
|
1008
|
-
<
|
|
1004
|
+
<MemoizedSpreadsheetHeader
|
|
1009
1005
|
key={column.id}
|
|
1010
1006
|
column={column}
|
|
1011
1007
|
sortConfig={sortConfig}
|
|
1012
1008
|
hasActiveFilter={!!filters[column.id]}
|
|
1013
1009
|
isPinned={isColumnPinned(column.id)}
|
|
1014
1010
|
pinSide={getColumnPinSide(column.id)}
|
|
1011
|
+
pinnedZIndex={isColumnPinned(column.id) ? getPinnedZIndex(column.id) : undefined}
|
|
1015
1012
|
leftOffset={
|
|
1016
1013
|
isPinnedLeft ? getColumnLeftOffset(column.id) : 0
|
|
1017
1014
|
}
|
|
@@ -1020,7 +1017,30 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1020
1017
|
}
|
|
1021
1018
|
highlightColor={getColumnHighlight(column.id)}
|
|
1022
1019
|
compactMode={effectiveCompactMode}
|
|
1023
|
-
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)}
|
|
1024
1044
|
onFilterClick={() =>
|
|
1025
1045
|
setActiveFilterColumn(
|
|
1026
1046
|
activeFilterColumn === column.id
|
|
@@ -1034,6 +1054,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1034
1054
|
? () => setHighlightPickerColumn(column.id)
|
|
1035
1055
|
: undefined
|
|
1036
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)}
|
|
1037
1065
|
>
|
|
1038
1066
|
{/* Filter dropdown */}
|
|
1039
1067
|
{activeFilterColumn === column.id && (
|
|
@@ -1047,9 +1075,10 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1047
1075
|
)
|
|
1048
1076
|
}
|
|
1049
1077
|
onClose={() => setActiveFilterColumn(null)}
|
|
1078
|
+
hasDuplicateCheck={duplicateCheckColumns.has(column.id)}
|
|
1050
1079
|
/>
|
|
1051
1080
|
)}
|
|
1052
|
-
</
|
|
1081
|
+
</MemoizedSpreadsheetHeader>
|
|
1053
1082
|
);
|
|
1054
1083
|
})}
|
|
1055
1084
|
</tr>
|
|
@@ -1081,7 +1110,6 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1081
1110
|
paginatedData.map((row, rowIndex) => {
|
|
1082
1111
|
const rowId = getRowId(row);
|
|
1083
1112
|
const isRowSelected = selectedRows.has(rowId);
|
|
1084
|
-
const isRowHovered = hoveredRow === rowId;
|
|
1085
1113
|
const rowHighlight = getRowHighlight(rowId);
|
|
1086
1114
|
const displayIndex =
|
|
1087
1115
|
rowIndex + 1 + (currentPage - 1) * pageSize;
|
|
@@ -1089,8 +1117,8 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1089
1117
|
return (
|
|
1090
1118
|
<tr
|
|
1091
1119
|
key={rowId}
|
|
1092
|
-
onMouseEnter={() =>
|
|
1093
|
-
onMouseLeave={() =>
|
|
1120
|
+
onMouseEnter={() => { hoveredRowRef.current = rowId; }}
|
|
1121
|
+
onMouseLeave={() => { hoveredRowRef.current = null; }}
|
|
1094
1122
|
onClick={() => {
|
|
1095
1123
|
onRowClick?.(row, rowIndex);
|
|
1096
1124
|
}}
|
|
@@ -1098,42 +1126,36 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1098
1126
|
>
|
|
1099
1127
|
{/* Row Index Column */}
|
|
1100
1128
|
<td
|
|
1129
|
+
data-column-id="__row_index__"
|
|
1101
1130
|
onClick={(e) => handleRowSelect(rowId, e)}
|
|
1102
1131
|
className={cn(
|
|
1103
1132
|
'border border-gray-200 text-center font-semibold cursor-pointer group',
|
|
1104
1133
|
effectiveCompactMode
|
|
1105
|
-
? 'text-
|
|
1106
|
-
: 'text-
|
|
1107
|
-
|
|
1134
|
+
? 'text-xs px-1.5 py-0.5'
|
|
1135
|
+
: 'text-sm px-2.5 py-1.5',
|
|
1136
|
+
'sticky',
|
|
1108
1137
|
isRowSelected && 'bg-blue-100',
|
|
1109
|
-
!isRowSelected && rowHighlight && ''
|
|
1110
|
-
isRowHovered &&
|
|
1111
|
-
!isRowSelected &&
|
|
1112
|
-
!rowHighlight &&
|
|
1113
|
-
'bg-gray-50'
|
|
1138
|
+
!isRowSelected && rowHighlight && ''
|
|
1114
1139
|
)}
|
|
1115
1140
|
style={{
|
|
1116
1141
|
backgroundColor:
|
|
1117
1142
|
rowHighlight?.color ||
|
|
1118
1143
|
(isRowSelected
|
|
1119
1144
|
? '#dbeafe'
|
|
1120
|
-
:
|
|
1121
|
-
? '#f9fafb'
|
|
1122
|
-
: rowIndexHighlightColor || 'white'),
|
|
1145
|
+
: rowIndexHighlightColor || (rowIndex % 2 !== 0 ? '#f9fafb' : 'white')),
|
|
1123
1146
|
minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
1124
1147
|
width: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}),
|
|
1148
|
+
position: 'sticky' as const,
|
|
1149
|
+
left: 0,
|
|
1150
|
+
zIndex: 40,
|
|
1129
1151
|
}}
|
|
1130
1152
|
>
|
|
1131
|
-
<div className="relative flex items-center
|
|
1132
|
-
{/* Row number -
|
|
1133
|
-
<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>
|
|
1134
1156
|
|
|
1135
|
-
{/* Action buttons - absolute
|
|
1136
|
-
<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">
|
|
1137
1159
|
{/* Context Menu (3-dot menu for row actions) */}
|
|
1138
1160
|
{rowContextMenuItems &&
|
|
1139
1161
|
rowContextMenuItems.length > 0 && (
|
|
@@ -1310,11 +1332,15 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1310
1332
|
isInSelection={isInSelection}
|
|
1311
1333
|
selectionEdge={selectionEdge}
|
|
1312
1334
|
isRowSelected={isRowSelected}
|
|
1313
|
-
isRowHovered={
|
|
1335
|
+
isRowHovered={false}
|
|
1314
1336
|
highlightColor={cellOrRowOrColumnHighlight}
|
|
1337
|
+
isDuplicate={isCellDuplicate(rowId, column.id)}
|
|
1315
1338
|
compactMode={effectiveCompactMode}
|
|
1339
|
+
isOddRow={rowIndex % 2 !== 0}
|
|
1340
|
+
resolvedWidth={getColumnWidth(column.id)}
|
|
1316
1341
|
isPinned={isColPinned}
|
|
1317
1342
|
pinSide={colPinSide}
|
|
1343
|
+
pinnedZIndex={isColPinned ? getPinnedZIndex(column.id) : undefined}
|
|
1318
1344
|
leftOffset={getColumnLeftOffset(column.id)}
|
|
1319
1345
|
rightOffset={getColumnRightOffset(
|
|
1320
1346
|
column.id
|
|
@@ -1322,6 +1348,12 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1322
1348
|
onClick={(e) =>
|
|
1323
1349
|
handleCellClick(rowId, column.id, e)
|
|
1324
1350
|
}
|
|
1351
|
+
onDoubleClick={() =>
|
|
1352
|
+
handleCellDoubleClick(rowId, column.id)
|
|
1353
|
+
}
|
|
1354
|
+
onMouseEnter={() =>
|
|
1355
|
+
handleCellMouseEnter(rowId, column.id)
|
|
1356
|
+
}
|
|
1325
1357
|
onConfirm={handleConfirmEdit}
|
|
1326
1358
|
onCancel={handleCancelEdit}
|
|
1327
1359
|
onHighlight={
|
|
@@ -1370,9 +1402,19 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1370
1402
|
)}
|
|
1371
1403
|
</tbody>
|
|
1372
1404
|
</table>
|
|
1373
|
-
</div>
|
|
1374
1405
|
</div>
|
|
1375
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
|
+
|
|
1376
1418
|
{/* Pagination */}
|
|
1377
1419
|
{showPagination && effectiveTotalItems > 0 && (
|
|
1378
1420
|
<Pagination
|
|
@@ -1442,6 +1484,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1442
1484
|
<ColorPickerPopover
|
|
1443
1485
|
title="Highlight Row"
|
|
1444
1486
|
paletteType="row"
|
|
1487
|
+
recentColors={recentColors}
|
|
1445
1488
|
onSelectColor={(color) => handleRowHighlightToggle(highlightPickerRow, color)}
|
|
1446
1489
|
onClose={() => setHighlightPickerRow(null)}
|
|
1447
1490
|
/>
|
|
@@ -1456,6 +1499,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1456
1499
|
: `Highlight Column: ${columns.find((c) => c.id === highlightPickerColumn)?.label || ''}`
|
|
1457
1500
|
}
|
|
1458
1501
|
paletteType="column"
|
|
1502
|
+
recentColors={recentColors}
|
|
1459
1503
|
onSelectColor={(color) =>
|
|
1460
1504
|
handleColumnHighlightToggle(highlightPickerColumn, color)
|
|
1461
1505
|
}
|
|
@@ -1468,6 +1512,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1468
1512
|
<ColorPickerPopover
|
|
1469
1513
|
title="Highlight Cell"
|
|
1470
1514
|
paletteType="row"
|
|
1515
|
+
recentColors={recentColors}
|
|
1471
1516
|
onSelectColor={(color) =>
|
|
1472
1517
|
handleCellHighlightToggle(
|
|
1473
1518
|
highlightPickerCell.rowId,
|