@xcelsior/ui-spreadsheets 1.2.2 → 1.3.1

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.
Files changed (99) hide show
  1. package/.omc/state/agent-replay-0cead415-b3bd-40fd-b199-47371946c4db.jsonl +27 -0
  2. package/.omc/state/idle-notif-cooldown.json +3 -0
  3. package/.omc/state/last-tool-error.json +7 -0
  4. package/.omc/state/mission-state.json +189 -0
  5. package/.omc/state/subagent-tracking.json +125 -0
  6. package/.turbo/turbo-build.log +28 -28
  7. package/.turbo/turbo-lint.log +140 -0
  8. package/dist/index.d.mts +94 -4
  9. package/dist/index.d.ts +94 -4
  10. package/dist/index.js +2134 -1157
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +2024 -1049
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/styles/globals.css +156 -16
  15. package/dist/styles/globals.css.map +1 -1
  16. package/package.json +1 -1
  17. package/plans/20260330-1230-spreadsheet-features/phase-01-types-and-duplicates-hook.md +73 -0
  18. package/plans/20260330-1230-spreadsheet-features/phase-02-filter-dropdown-portal.md +90 -0
  19. package/plans/20260330-1230-spreadsheet-features/phase-03-header-overflow-menu.md +101 -0
  20. package/plans/20260330-1230-spreadsheet-features/phase-04-integration.md +193 -0
  21. package/plans/20260330-1230-spreadsheet-features/plan.md +59 -0
  22. package/src/components/ColorPickerPopover.tsx +77 -32
  23. package/src/components/ColumnHeaderActions.tsx +241 -1
  24. package/src/components/RowIndexColumnHeader.tsx +13 -17
  25. package/src/components/SelectionSummaryBar.tsx +103 -0
  26. package/src/components/Spreadsheet.stories.tsx +254 -0
  27. package/src/components/Spreadsheet.tsx +235 -190
  28. package/src/components/SpreadsheetCell.tsx +280 -42
  29. package/src/components/SpreadsheetFilterDropdown.tsx +178 -13
  30. package/src/components/SpreadsheetHeader.tsx +79 -24
  31. package/src/components/SpreadsheetSettingsModal.tsx +4 -0
  32. package/src/hooks/useSpreadsheetColumnResize.ts +143 -0
  33. package/src/hooks/useSpreadsheetDuplicates.ts +149 -0
  34. package/src/hooks/useSpreadsheetFiltering.ts +18 -1
  35. package/src/hooks/useSpreadsheetHighlighting.ts +23 -3
  36. package/src/hooks/useSpreadsheetKeyboardShortcuts.ts +16 -0
  37. package/src/hooks/useSpreadsheetPinning.ts +148 -134
  38. package/src/hooks/useSpreadsheetSelection.ts +10 -22
  39. package/src/hooks/useSpreadsheetSummary.ts +68 -0
  40. package/src/index.ts +4 -1
  41. package/src/styles/globals.css +51 -0
  42. package/src/types.ts +50 -2
  43. package/storybook-static/assets/Color-YHDXOIA2-CtQurLnT.js +1 -0
  44. package/storybook-static/assets/DocsRenderer-CFRXHY34-oxrW8Hvo.js +575 -0
  45. package/storybook-static/assets/Spreadsheet.stories-DvhhzuK4.js +1357 -0
  46. package/storybook-static/assets/chunk-XP5HYGXS-BpfKkqn7.js +1 -0
  47. package/storybook-static/assets/entry-preview-CkBGHCAN.js +2 -0
  48. package/storybook-static/assets/entry-preview-docs-ugJb6pa8.js +46 -0
  49. package/storybook-static/assets/iframe-CPp2u3vg.js +211 -0
  50. package/storybook-static/assets/index-BB9bPxRC.js +24 -0
  51. package/storybook-static/assets/index-BQFlzFLk.js +9 -0
  52. package/storybook-static/assets/index-CtvPRVHf.js +9 -0
  53. package/storybook-static/assets/index-DgH-xKnr.js +11 -0
  54. package/storybook-static/assets/index-DrFu-skq.js +6 -0
  55. package/storybook-static/assets/index-DrdPSA1J.js +240 -0
  56. package/storybook-static/assets/index-DzFBShOR.js +20 -0
  57. package/storybook-static/assets/index-v-1boR4t.js +1 -0
  58. package/storybook-static/assets/preview-B8lJiyuQ.js +34 -0
  59. package/storybook-static/assets/preview-BBWR9nbA.js +1 -0
  60. package/storybook-static/assets/preview-BWzBA1C2.js +396 -0
  61. package/storybook-static/assets/preview-Bm0S-uxO.css +1 -0
  62. package/storybook-static/assets/preview-CvbIS5ZJ.js +1 -0
  63. package/storybook-static/assets/preview-DD_OYowb.js +1 -0
  64. package/storybook-static/assets/preview-DGUiP6tS.js +7 -0
  65. package/storybook-static/assets/preview-DHQbi4pV.js +1 -0
  66. package/storybook-static/assets/preview-DwI0w3cI.js +1 -0
  67. package/storybook-static/assets/preview-DyR7iiFG.js +1 -0
  68. package/storybook-static/assets/preview-zxZ6Be2V.js +2 -0
  69. package/storybook-static/assets/react-18-Pj8skaX9.js +1 -0
  70. package/storybook-static/assets/test-utils-quxJ1Z79.js +9 -0
  71. package/storybook-static/favicon.svg +1 -0
  72. package/storybook-static/iframe.html +666 -0
  73. package/storybook-static/index.html +177 -0
  74. package/storybook-static/index.json +1 -0
  75. package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
  76. package/storybook-static/nunito-sans-bold.woff2 +0 -0
  77. package/storybook-static/nunito-sans-italic.woff2 +0 -0
  78. package/storybook-static/nunito-sans-regular.woff2 +0 -0
  79. package/storybook-static/project.json +1 -0
  80. package/storybook-static/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
  81. package/storybook-static/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
  82. package/storybook-static/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
  83. package/storybook-static/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
  84. package/storybook-static/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
  85. package/storybook-static/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
  86. package/storybook-static/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
  87. package/storybook-static/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
  88. package/storybook-static/sb-addons/interactions-10/manager-bundle.js +222 -0
  89. package/storybook-static/sb-addons/links-1/manager-bundle.js +3 -0
  90. package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
  91. package/storybook-static/sb-common-assets/favicon.svg +1 -0
  92. package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  93. package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  94. package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  95. package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  96. package/storybook-static/sb-manager/globals-module-info.js +1052 -0
  97. package/storybook-static/sb-manager/globals-runtime.js +42127 -0
  98. package/storybook-static/sb-manager/globals.js +48 -0
  99. package/storybook-static/sb-manager/runtime.js +12048 -0
@@ -0,0 +1,149 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import type { SpreadsheetColumn } from '../types';
3
+
4
+ export interface UseSpreadsheetDuplicatesOptions<T> {
5
+ data: T[];
6
+ columns: SpreadsheetColumn<T>[];
7
+ duplicateCheckColumns: string[];
8
+ getRowId: (row: T) => string | number;
9
+ }
10
+
11
+ export interface UseSpreadsheetDuplicatesReturn {
12
+ /** Check if a specific cell value is a duplicate in its column */
13
+ isCellDuplicate: (rowId: string | number, columnId: string) => boolean;
14
+ /** Get the duplicate count for a value in a column */
15
+ getDuplicateCount: (columnId: string, value: any) => number;
16
+ /** Get the number of rows with duplicate values for a column */
17
+ getDuplicateColumnCount: (columnId: string) => number;
18
+ /** Map of columnId -> Set of rowIds that have duplicate values */
19
+ duplicateRowIds: Map<string, Set<string | number>>;
20
+ /** Set of column IDs with duplicate checking enabled */
21
+ duplicateCheckColumns: Set<string>;
22
+ /** Toggle duplicate check for a column */
23
+ toggleDuplicateCheck: (columnId: string) => void;
24
+ }
25
+
26
+ /** Normalize a cell value for duplicate comparison */
27
+ function normalizeValue(value: any): string {
28
+ if (value === null || value === undefined || value === '') {
29
+ return '__blank__';
30
+ }
31
+ if (typeof value === 'number') {
32
+ return String(value);
33
+ }
34
+ return String(value).trim().toLowerCase();
35
+ }
36
+
37
+ export function useSpreadsheetDuplicates<T>({
38
+ data,
39
+ columns,
40
+ duplicateCheckColumns,
41
+ getRowId,
42
+ }: UseSpreadsheetDuplicatesOptions<T>): UseSpreadsheetDuplicatesReturn {
43
+ const [localDuplicateCheckColumns, setLocalDuplicateCheckColumns] = useState<Set<string>>(
44
+ () => new Set(duplicateCheckColumns)
45
+ );
46
+
47
+ // Sync external prop changes into local state
48
+ const duplicateCheckColumnsSet = useMemo(() => {
49
+ return new Set(duplicateCheckColumns);
50
+ }, [duplicateCheckColumns]);
51
+
52
+ // Build Maps for O(1) lookup: columnId -> normalizedValue -> rowId[]
53
+ const { duplicateRowIds, valueCounts } = useMemo(() => {
54
+ const duplicateRowIds = new Map<string, Set<string | number>>();
55
+ const valueCounts = new Map<string, Map<string, number>>();
56
+
57
+ const activeColumns = duplicateCheckColumnsSet.size > 0
58
+ ? duplicateCheckColumnsSet
59
+ : localDuplicateCheckColumns;
60
+
61
+ for (const columnId of activeColumns) {
62
+ const column = columns.find((c) => c.id === columnId);
63
+ if (!column) continue;
64
+
65
+ // Single O(n) pass: build value -> rowId[] map
66
+ const valueToRowIds = new Map<string, (string | number)[]>();
67
+
68
+ for (const row of data) {
69
+ const rawValue = column.getValue ? column.getValue(row) : (row as any)[columnId];
70
+ // Skip empty/null/undefined values — they're not duplicates
71
+ if (rawValue === null || rawValue === undefined || rawValue === '') continue;
72
+ const normalized = normalizeValue(rawValue);
73
+ const rowId = getRowId(row);
74
+
75
+ const existing = valueToRowIds.get(normalized);
76
+ if (existing) {
77
+ existing.push(rowId);
78
+ } else {
79
+ valueToRowIds.set(normalized, [rowId]);
80
+ }
81
+ }
82
+
83
+ // Build duplicateRowIds set for this column
84
+ const dupSet = new Set<string | number>();
85
+ const countMap = new Map<string, number>();
86
+
87
+ for (const [normalizedVal, rowIds] of valueToRowIds) {
88
+ countMap.set(normalizedVal, rowIds.length);
89
+ if (rowIds.length >= 2) {
90
+ for (const rowId of rowIds) {
91
+ dupSet.add(rowId);
92
+ }
93
+ }
94
+ }
95
+
96
+ duplicateRowIds.set(columnId, dupSet);
97
+ valueCounts.set(columnId, countMap);
98
+ }
99
+
100
+ return { duplicateRowIds, valueCounts };
101
+ }, [data, columns, duplicateCheckColumnsSet, localDuplicateCheckColumns, getRowId]);
102
+
103
+ const isCellDuplicate = useCallback(
104
+ (rowId: string | number, columnId: string): boolean => {
105
+ return duplicateRowIds.get(columnId)?.has(rowId) ?? false;
106
+ },
107
+ [duplicateRowIds]
108
+ );
109
+
110
+ const getDuplicateCount = useCallback(
111
+ (columnId: string, value: any): number => {
112
+ const normalized = normalizeValue(value);
113
+ return valueCounts.get(columnId)?.get(normalized) ?? 0;
114
+ },
115
+ [valueCounts]
116
+ );
117
+
118
+ const getDuplicateColumnCount = useCallback(
119
+ (columnId: string): number => {
120
+ return duplicateRowIds.get(columnId)?.size ?? 0;
121
+ },
122
+ [duplicateRowIds]
123
+ );
124
+
125
+ const toggleDuplicateCheck = useCallback((columnId: string) => {
126
+ setLocalDuplicateCheckColumns((prev) => {
127
+ const next = new Set(prev);
128
+ if (next.has(columnId)) {
129
+ next.delete(columnId);
130
+ } else {
131
+ next.add(columnId);
132
+ }
133
+ return next;
134
+ });
135
+ }, []);
136
+
137
+ // Expose the effective set (external prop wins when non-empty, else local state)
138
+ const effectiveDuplicateCheckColumns =
139
+ duplicateCheckColumnsSet.size > 0 ? duplicateCheckColumnsSet : localDuplicateCheckColumns;
140
+
141
+ return {
142
+ isCellDuplicate,
143
+ getDuplicateCount,
144
+ getDuplicateColumnCount,
145
+ duplicateRowIds,
146
+ duplicateCheckColumns: effectiveDuplicateCheckColumns,
147
+ toggleDuplicateCheck,
148
+ };
149
+ }
@@ -25,6 +25,13 @@ export interface UseSpreadsheetFilteringOptions<T> {
25
25
  * Use with serverSide mode for server-side sorting.
26
26
  */
27
27
  controlledSortConfig?: SpreadsheetSortConfig | null;
28
+ /**
29
+ * Map of columnId -> Set of rowIds that have duplicate values.
30
+ * Used for showDuplicatesOnly filter option.
31
+ */
32
+ duplicateRowIds?: Map<string, Set<string | number>>;
33
+ /** Row ID accessor, required when using showDuplicatesOnly filter */
34
+ getRowId?: (row: T) => string | number;
28
35
  }
29
36
 
30
37
  export interface UseSpreadsheetFilteringReturn<T> {
@@ -54,6 +61,8 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
54
61
  controlledFilters,
55
62
  controlledSortConfig,
56
63
  defaultSortConfig,
64
+ duplicateRowIds,
65
+ getRowId,
57
66
  }: UseSpreadsheetFilteringOptions<T>): UseSpreadsheetFilteringReturn<T> {
58
67
  // Internal state for uncontrolled mode
59
68
  const [internalFilters, setInternalFilters] = useState<Record<string, SpreadsheetColumnFilter>>(
@@ -337,6 +346,14 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
337
346
  if (!column) continue;
338
347
 
339
348
  filterChain.add(buildFilterPredicate(column, filter));
349
+
350
+ // Handle showDuplicatesOnly as an additional predicate
351
+ if (filter.showDuplicatesOnly && duplicateRowIds && getRowId) {
352
+ const dupSet = duplicateRowIds.get(columnId);
353
+ if (dupSet) {
354
+ filterChain.add((row: T) => dupSet.has(getRowId(row)));
355
+ }
356
+ }
340
357
  }
341
358
 
342
359
  // Apply combined filter if any filters are active
@@ -351,7 +368,7 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
351
368
  }
352
369
 
353
370
  return lazyResult;
354
- }, [data, filters, sortConfig, columns, serverSide, buildFilterPredicate, buildSortComparator]);
371
+ }, [data, filters, sortConfig, columns, serverSide, buildFilterPredicate, buildSortComparator, duplicateRowIds, getRowId]);
355
372
 
356
373
  const handleFilterChange = useCallback(
357
374
  (columnId: string, filter: SpreadsheetColumnFilter | undefined) => {
@@ -72,6 +72,9 @@ export interface UseSpreadsheetHighlightingReturn {
72
72
  highlightPickerCell: { rowId: string | number; columnId: string } | null;
73
73
  setHighlightPickerCell: (cell: { rowId: string | number; columnId: string } | null) => void;
74
74
 
75
+ // Recent colors
76
+ recentColors: string[];
77
+
75
78
  // Utility
76
79
  clearAllHighlights: () => void;
77
80
  }
@@ -95,6 +98,17 @@ export function useSpreadsheetHighlighting({
95
98
  Record<string, string>
96
99
  >({});
97
100
 
101
+ // Recent colors (track last 8 used colors)
102
+ const [recentColors, setRecentColors] = useState<string[]>([]);
103
+
104
+ const addRecentColor = useCallback((color: string | null) => {
105
+ if (!color) return;
106
+ setRecentColors((prev) => {
107
+ const filtered = prev.filter((c) => c !== color);
108
+ return [color, ...filtered].slice(0, 8);
109
+ });
110
+ }, []);
111
+
98
112
  // Picker states
99
113
  const [highlightPickerRow, setHighlightPickerRow] = useState<string | number | null>(null);
100
114
  const [highlightPickerColumn, setHighlightPickerColumn] = useState<string | null>(null);
@@ -144,9 +158,10 @@ export function useSpreadsheetHighlighting({
144
158
  return prev;
145
159
  });
146
160
  }
161
+ addRecentColor(color);
147
162
  setHighlightPickerCell(null);
148
163
  },
149
- [onCellHighlight]
164
+ [onCellHighlight, addRecentColor]
150
165
  );
151
166
 
152
167
  // Get row-level highlight
@@ -181,9 +196,10 @@ export function useSpreadsheetHighlighting({
181
196
  return prev;
182
197
  });
183
198
  }
199
+ addRecentColor(color);
184
200
  setHighlightPickerRow(null);
185
201
  },
186
- [onRowHighlight]
202
+ [onRowHighlight, addRecentColor]
187
203
  );
188
204
 
189
205
  // Get column highlight (works for both regular columns and row index)
@@ -212,9 +228,10 @@ export function useSpreadsheetHighlighting({
212
228
  return newHighlights;
213
229
  });
214
230
  }
231
+ addRecentColor(color);
215
232
  setHighlightPickerColumn(null);
216
233
  },
217
- [onColumnHighlight]
234
+ [onColumnHighlight, addRecentColor]
218
235
  );
219
236
 
220
237
  // Clear all highlights
@@ -240,6 +257,9 @@ export function useSpreadsheetHighlighting({
240
257
  getColumnHighlight,
241
258
  handleColumnHighlightToggle,
242
259
 
260
+ // Recent colors
261
+ recentColors,
262
+
243
263
  // Picker state
244
264
  highlightPickerRow,
245
265
  setHighlightPickerRow,
@@ -29,6 +29,8 @@ export interface UseSpreadsheetKeyboardShortcutsOptions {
29
29
  onCopy?: () => void;
30
30
  /** Handler for paste (Ctrl/Cmd+V) */
31
31
  onPaste?: () => void;
32
+ /** Handler for select all (Ctrl/Cmd+A) */
33
+ onSelectAll?: () => void;
32
34
  /** Whether there is a focused cell */
33
35
  hasFocusedCell?: boolean;
34
36
  /** Whether a cell is currently being edited */
@@ -69,6 +71,7 @@ export function useSpreadsheetKeyboardShortcuts({
69
71
  onTabNavigation,
70
72
  onCopy,
71
73
  onPaste,
74
+ onSelectAll,
72
75
  hasFocusedCell = false,
73
76
  isEditing = false,
74
77
  customShortcuts = [],
@@ -119,6 +122,17 @@ export function useSpreadsheetKeyboardShortcuts({
119
122
  return;
120
123
  }
121
124
 
125
+ // Cmd/Ctrl+A: Select all (only when not editing)
126
+ if (
127
+ (event.metaKey || event.ctrlKey) &&
128
+ (event.key === 'a' || event.key === 'A') &&
129
+ !isEditing
130
+ ) {
131
+ event.preventDefault();
132
+ onSelectAll?.();
133
+ return;
134
+ }
135
+
122
136
  // Cmd/Ctrl+C: Copy (only when not editing and has focused cell)
123
137
  if (
124
138
  (event.metaKey || event.ctrlKey) &&
@@ -211,6 +225,7 @@ export function useSpreadsheetKeyboardShortcuts({
211
225
  onTabNavigation,
212
226
  onCopy,
213
227
  onPaste,
228
+ onSelectAll,
214
229
  hasFocusedCell,
215
230
  isEditing,
216
231
  customShortcuts,
@@ -230,6 +245,7 @@ export function useSpreadsheetKeyboardShortcuts({
230
245
  editing: [
231
246
  { label: 'Undo', keys: [modifierKey, 'Z'] },
232
247
  { label: 'Redo', keys: [modifierKey, 'Shift', 'Z'] },
248
+ { label: 'Select all', keys: [modifierKey, 'A'] },
233
249
  { label: 'Copy cells', keys: [modifierKey, 'C'] },
234
250
  { label: 'Paste cells', keys: [modifierKey, 'V'] },
235
251
  { label: 'Confirm cell edit', keys: ['Enter'] },