@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
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/Spreadsheet.tsx
2
- import { useCallback as useCallback7, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef5, useState as useState12 } from "react";
2
+ import { useCallback as useCallback11, useEffect as useEffect8, useMemo as useMemo7, useRef as useRef10, useState as useState16, startTransition } from "react";
3
3
 
4
4
  // ../../../node_modules/.pnpm/react-icons@4.12.0_react@18.3.1/node_modules/react-icons/lib/esm/iconBase.js
5
5
  import React2 from "react";
@@ -93,6 +93,9 @@ function HiCog(props) {
93
93
  function HiDotsVertical(props) {
94
94
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 20 20", "fill": "currentColor", "aria-hidden": "true" }, "child": [{ "tag": "path", "attr": { "d": "M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" } }] })(props);
95
95
  }
96
+ function HiExclamation(props) {
97
+ return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 20 20", "fill": "currentColor", "aria-hidden": "true" }, "child": [{ "tag": "path", "attr": { "fillRule": "evenodd", "d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", "clipRule": "evenodd" } }] })(props);
98
+ }
96
99
  function HiEye(props) {
97
100
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 20 20", "fill": "currentColor", "aria-hidden": "true" }, "child": [{ "tag": "path", "attr": { "d": "M10 12a2 2 0 100-4 2 2 0 000 4z" } }, { "tag": "path", "attr": { "fillRule": "evenodd", "d": "M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z", "clipRule": "evenodd" } }] })(props);
98
101
  }
@@ -105,6 +108,9 @@ function HiReply(props) {
105
108
  function HiSortAscending(props) {
106
109
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 20 20", "fill": "currentColor", "aria-hidden": "true" }, "child": [{ "tag": "path", "attr": { "d": "M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h5a1 1 0 000-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM13 16a1 1 0 102 0v-5.586l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 101.414 1.414L13 10.414V16z" } }] })(props);
107
110
  }
111
+ function HiSortDescending(props) {
112
+ return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 20 20", "fill": "currentColor", "aria-hidden": "true" }, "child": [{ "tag": "path", "attr": { "d": "M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" } }] })(props);
113
+ }
108
114
  function HiViewBoards(props) {
109
115
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 20 20", "fill": "currentColor", "aria-hidden": "true" }, "child": [{ "tag": "path", "attr": { "d": "M2 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1H3a1 1 0 01-1-1V4zM8 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1H9a1 1 0 01-1-1V4zM15 3a1 1 0 00-1 1v12a1 1 0 001 1h2a1 1 0 001-1V4a1 1 0 00-1-1h-2z" } }] })(props);
110
116
  }
@@ -145,165 +151,186 @@ function cn(...inputs) {
145
151
  }
146
152
 
147
153
  // src/components/SpreadsheetCell.tsx
148
- import { useState as useState2, useRef, useEffect, memo } from "react";
149
-
150
- // src/hooks/useSpreadsheetPinning.ts
151
- import { useCallback, useMemo, useState } from "react";
152
- var ROW_INDEX_COLUMN_ID = "__row_index__";
153
- var ROW_INDEX_COLUMN_WIDTH = 80;
154
- var MIN_PINNED_COLUMN_WIDTH = 150;
155
- function useSpreadsheetPinning({
156
- columns,
157
- columnGroups,
158
- showRowIndex = true,
159
- defaultPinnedColumns = [],
160
- defaultPinnedRightColumns = []
161
- }) {
162
- const [pinnedColumns, setPinnedColumns] = useState(() => {
163
- const map = /* @__PURE__ */ new Map();
164
- defaultPinnedColumns.forEach((col) => {
165
- map.set(col, "left");
166
- });
167
- defaultPinnedRightColumns.forEach((col) => {
168
- map.set(col, "right");
169
- });
170
- return map;
171
- });
172
- const [collapsedGroups, setCollapsedGroups] = useState(/* @__PURE__ */ new Set());
173
- const isRowIndexPinned = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
174
- const handleTogglePin = useCallback((columnId) => {
175
- setPinnedColumns((prev) => {
176
- const newMap = new Map(prev);
177
- if (newMap.has(columnId)) {
178
- newMap.delete(columnId);
179
- } else {
180
- newMap.set(columnId, "left");
181
- }
182
- return newMap;
183
- });
184
- }, []);
185
- const setPinnedColumnsFromIds = useCallback((columnIds) => {
186
- const map = /* @__PURE__ */ new Map();
187
- columnIds.forEach((col) => {
188
- map.set(col, "left");
189
- });
190
- setPinnedColumns(map);
191
- }, []);
192
- const handleToggleGroupCollapse = useCallback((groupId) => {
193
- setCollapsedGroups((prev) => {
194
- const newSet = new Set(prev);
195
- if (newSet.has(groupId)) {
196
- newSet.delete(groupId);
197
- } else {
198
- newSet.add(groupId);
199
- }
200
- return newSet;
201
- });
202
- }, []);
203
- const visibleColumns = useMemo(() => {
204
- if (!columns || !Array.isArray(columns) || columns.length === 0) {
205
- return [];
206
- }
207
- let result = [...columns];
208
- if (columnGroups && Array.isArray(columnGroups)) {
209
- result = result.filter((column) => {
210
- const group = columnGroups.find((g) => g.columns.includes(column.id));
211
- if (!group) return true;
212
- if (!collapsedGroups.has(group.id)) return true;
213
- return pinnedColumns.has(column.id);
154
+ import { useState, useRef, useEffect, useCallback, memo } from "react";
155
+ import { createPortal } from "react-dom";
156
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
157
+ var cellPaddingCompact = "px-1.5 py-0.5";
158
+ var cellPaddingNormal = "px-2.5 py-1.5";
159
+ var AutocompleteEditor = ({ value, column, compactMode, onConfirm, onCancel }) => {
160
+ const getLabel = useCallback(
161
+ (val) => {
162
+ if (val === null || val === void 0 || val === "") return "";
163
+ if (column.getOptionLabel) return column.getOptionLabel(val);
164
+ const match = column.autocompleteOptions?.find((o) => o.value === val);
165
+ return match ? match.label : String(val);
166
+ },
167
+ [column]
168
+ );
169
+ const [searchText, setSearchText] = useState(() => getLabel(value));
170
+ const [filteredOptions, setFilteredOptions] = useState(
171
+ column.autocompleteOptions ?? []
172
+ );
173
+ const [focusedIndex, setFocusedIndex] = useState(-1);
174
+ const [isOpen, setIsOpen] = useState(true);
175
+ const [dropdownPos, setDropdownPos] = useState(null);
176
+ const inputRef = useRef(null);
177
+ const dropdownRef = useRef(null);
178
+ const debounceRef = useRef(null);
179
+ const updateDropdownPos = useCallback(() => {
180
+ if (inputRef.current) {
181
+ const rect = inputRef.current.getBoundingClientRect();
182
+ setDropdownPos({
183
+ top: rect.bottom + 2,
184
+ left: rect.left,
185
+ width: rect.width
214
186
  });
215
187
  }
216
- const nonRowIndexPinned = Array.from(pinnedColumns.keys()).filter(
217
- (id) => id !== ROW_INDEX_COLUMN_ID
218
- );
219
- if (nonRowIndexPinned.length === 0) {
220
- return result;
221
- }
222
- const leftPinned = [];
223
- const unpinned = [];
224
- const rightPinned = [];
225
- const pinnedLeftIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
226
- const pinnedRightIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "right" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
227
- for (const column of result) {
228
- const pinSide = pinnedColumns.get(column.id);
229
- if (pinSide === "left") {
230
- leftPinned.push(column);
231
- } else if (pinSide === "right") {
232
- rightPinned.push(column);
233
- } else {
234
- unpinned.push(column);
235
- }
236
- }
237
- leftPinned.sort((a, b) => pinnedLeftIds.indexOf(a.id) - pinnedLeftIds.indexOf(b.id));
238
- rightPinned.sort((a, b) => pinnedRightIds.indexOf(a.id) - pinnedRightIds.indexOf(b.id));
239
- return [...leftPinned, ...unpinned, ...rightPinned];
240
- }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
241
- const getColumnLeftOffset = useCallback(
242
- (columnId) => {
243
- if (columnId === ROW_INDEX_COLUMN_ID) {
244
- return 0;
245
- }
246
- const pinnedLeft = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
247
- const index = pinnedLeft.indexOf(columnId);
248
- const isRowIndexPinnedNow = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
249
- const baseOffset = showRowIndex && isRowIndexPinnedNow ? ROW_INDEX_COLUMN_WIDTH : 0;
250
- if (index === -1) return baseOffset;
251
- let offset = baseOffset;
252
- for (let i = 0; i < index; i++) {
253
- const col = columns.find((c) => c.id === pinnedLeft[i]);
254
- const configuredWidth = col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH;
255
- offset += Math.max(configuredWidth, MIN_PINNED_COLUMN_WIDTH);
256
- }
257
- return offset;
188
+ }, []);
189
+ useEffect(() => {
190
+ inputRef.current?.focus();
191
+ inputRef.current?.select();
192
+ updateDropdownPos();
193
+ }, [updateDropdownPos]);
194
+ const filterClientSide = useCallback(
195
+ (term) => {
196
+ const opts = column.autocompleteOptions ?? [];
197
+ if (!term.trim()) return opts;
198
+ const lower = term.toLowerCase();
199
+ return opts.filter((o) => o.label.toLowerCase().includes(lower));
258
200
  },
259
- [pinnedColumns, columns, showRowIndex]
201
+ [column.autocompleteOptions]
260
202
  );
261
- const isColumnPinned = useCallback(
262
- (columnId) => {
263
- return pinnedColumns.has(columnId);
203
+ const handleSearchChange = useCallback(
204
+ (e) => {
205
+ const term = e.target.value;
206
+ setSearchText(term);
207
+ setFocusedIndex(-1);
208
+ setIsOpen(true);
209
+ updateDropdownPos();
210
+ if (debounceRef.current) clearTimeout(debounceRef.current);
211
+ debounceRef.current = setTimeout(async () => {
212
+ if (column.onAutocompleteSearch) {
213
+ try {
214
+ const results = await column.onAutocompleteSearch(term);
215
+ setFilteredOptions(results);
216
+ } catch {
217
+ setFilteredOptions([]);
218
+ }
219
+ } else {
220
+ setFilteredOptions(filterClientSide(term));
221
+ }
222
+ }, 300);
264
223
  },
265
- [pinnedColumns]
224
+ [column, filterClientSide, updateDropdownPos]
266
225
  );
267
- const getColumnPinSide = useCallback(
268
- (columnId) => {
269
- return pinnedColumns.get(columnId);
226
+ useEffect(() => {
227
+ return () => {
228
+ if (debounceRef.current) clearTimeout(debounceRef.current);
229
+ };
230
+ }, []);
231
+ const selectOption = useCallback(
232
+ (option) => {
233
+ setSearchText(option.label);
234
+ setIsOpen(false);
235
+ onConfirm?.(option.value);
270
236
  },
271
- [pinnedColumns]
237
+ [onConfirm]
272
238
  );
273
- const getColumnRightOffset = useCallback(
274
- (columnId) => {
275
- const pinnedRight = Array.from(pinnedColumns.entries()).filter(([, side]) => side === "right").map(([id]) => id);
276
- const index = pinnedRight.indexOf(columnId);
277
- if (index === -1) return 0;
278
- let offset = 0;
279
- for (let i = pinnedRight.length - 1; i > index; i--) {
280
- const col = columns.find((c) => c.id === pinnedRight[i]);
281
- const configuredWidth = col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH;
282
- offset += Math.max(configuredWidth, MIN_PINNED_COLUMN_WIDTH);
239
+ const handleKeyDown = useCallback(
240
+ (e) => {
241
+ const visibleOptions2 = filteredOptions.slice(0, 8);
242
+ if (e.key === "ArrowDown") {
243
+ e.preventDefault();
244
+ setFocusedIndex((prev) => Math.min(prev + 1, visibleOptions2.length - 1));
245
+ } else if (e.key === "ArrowUp") {
246
+ e.preventDefault();
247
+ setFocusedIndex((prev) => Math.max(prev - 1, -1));
248
+ } else if (e.key === "Enter") {
249
+ e.preventDefault();
250
+ if (focusedIndex >= 0 && visibleOptions2[focusedIndex]) {
251
+ selectOption(visibleOptions2[focusedIndex]);
252
+ } else {
253
+ onConfirm?.(value);
254
+ }
255
+ } else if (e.key === "Escape") {
256
+ e.preventDefault();
257
+ e.stopPropagation();
258
+ setIsOpen(false);
259
+ onCancel?.();
283
260
  }
284
- return offset;
285
261
  },
286
- [pinnedColumns, columns]
262
+ [filteredOptions, focusedIndex, selectOption, onConfirm, onCancel, value]
287
263
  );
288
- return {
289
- pinnedColumns,
290
- isRowIndexPinned,
291
- collapsedGroups,
292
- visibleColumns,
293
- handleTogglePin,
294
- handleToggleGroupCollapse,
295
- setPinnedColumnsFromIds,
296
- getColumnLeftOffset,
297
- getColumnRightOffset,
298
- isColumnPinned,
299
- getColumnPinSide
300
- };
301
- }
302
-
303
- // src/components/SpreadsheetCell.tsx
304
- import { jsx, jsxs } from "react/jsx-runtime";
305
- var cellPaddingCompact = "px-1 py-px";
306
- var cellPaddingNormal = "px-2 py-1";
264
+ const handleBlur = useCallback(
265
+ (e) => {
266
+ setTimeout(() => {
267
+ if (dropdownRef.current && dropdownRef.current.contains(document.activeElement)) {
268
+ return;
269
+ }
270
+ setIsOpen(false);
271
+ onConfirm?.(value);
272
+ }, 150);
273
+ },
274
+ [onConfirm, value]
275
+ );
276
+ const visibleOptions = filteredOptions.slice(0, 8);
277
+ const dropdown = isOpen && visibleOptions.length > 0 && dropdownPos ? createPortal(
278
+ /* @__PURE__ */ jsx(
279
+ "div",
280
+ {
281
+ ref: dropdownRef,
282
+ style: {
283
+ position: "fixed",
284
+ top: dropdownPos.top,
285
+ left: dropdownPos.left,
286
+ width: Math.max(dropdownPos.width, 180),
287
+ zIndex: 50
288
+ },
289
+ className: "bg-white border border-gray-200 rounded-md shadow-lg max-h-48 overflow-y-auto",
290
+ onMouseDown: (e) => e.preventDefault(),
291
+ children: visibleOptions.map((option, index) => /* @__PURE__ */ jsx(
292
+ "div",
293
+ {
294
+ onMouseDown: (e) => {
295
+ e.preventDefault();
296
+ selectOption(option);
297
+ },
298
+ className: cn(
299
+ "px-3 py-1.5 cursor-pointer",
300
+ compactMode ? "text-xs" : "text-sm",
301
+ index === focusedIndex ? "bg-blue-100" : "hover:bg-blue-50"
302
+ ),
303
+ children: option.label
304
+ },
305
+ `${option.value}-${index}`
306
+ ))
307
+ }
308
+ ),
309
+ document.body
310
+ ) : null;
311
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
312
+ /* @__PURE__ */ jsx(
313
+ "input",
314
+ {
315
+ ref: inputRef,
316
+ type: "text",
317
+ value: searchText,
318
+ onChange: handleSearchChange,
319
+ onKeyDown: handleKeyDown,
320
+ onBlur: handleBlur,
321
+ autoComplete: "off",
322
+ autoCorrect: "off",
323
+ autoCapitalize: "off",
324
+ spellCheck: false,
325
+ className: cn(
326
+ "w-full border-0 bg-blue-50 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-sm",
327
+ compactMode ? "text-xs" : "text-sm"
328
+ )
329
+ }
330
+ ),
331
+ dropdown
332
+ ] });
333
+ };
307
334
  var SpreadsheetCell = ({
308
335
  value,
309
336
  column,
@@ -318,6 +345,7 @@ var SpreadsheetCell = ({
318
345
  isRowSelected = false,
319
346
  isRowHovered = false,
320
347
  highlightColor,
348
+ isDuplicate = false,
321
349
  hasComments = false,
322
350
  unresolvedCommentCount = 0,
323
351
  isCopied = false,
@@ -326,7 +354,11 @@ var SpreadsheetCell = ({
326
354
  pinSide,
327
355
  leftOffset = 0,
328
356
  rightOffset = 0,
357
+ isOddRow = false,
358
+ resolvedWidth,
359
+ pinnedZIndex,
329
360
  onClick,
361
+ onDoubleClick,
330
362
  onMouseDown,
331
363
  onMouseEnter,
332
364
  onChange,
@@ -337,7 +369,7 @@ var SpreadsheetCell = ({
337
369
  onViewComments,
338
370
  className
339
371
  }) => {
340
- const [localValue, setLocalValue] = useState2(value);
372
+ const [localValue, setLocalValue] = useState(value);
341
373
  const inputRef = useRef(null);
342
374
  const selectRef = useRef(null);
343
375
  useEffect(() => {
@@ -347,7 +379,7 @@ var SpreadsheetCell = ({
347
379
  if (isEditing) {
348
380
  if (column.type === "select") {
349
381
  selectRef.current?.focus();
350
- } else {
382
+ } else if (column.type !== "autocomplete") {
351
383
  inputRef.current?.focus();
352
384
  inputRef.current?.select();
353
385
  }
@@ -366,8 +398,10 @@ var SpreadsheetCell = ({
366
398
  };
367
399
  const getBackgroundColor = () => {
368
400
  if (highlightColor) return highlightColor;
401
+ if (isDuplicate) return "rgb(254 202 202)";
369
402
  if (isRowSelected) return "rgb(219 234 254)";
370
403
  if (isRowHovered) return "rgb(243 244 246)";
404
+ if (isOddRow) return "rgb(249 250 251)";
371
405
  return "white";
372
406
  };
373
407
  const handleCheckboxChange = (e) => {
@@ -404,12 +438,29 @@ var SpreadsheetCell = ({
404
438
  if (column.type === "number") {
405
439
  return typeof value === "number" ? value.toLocaleString() : value;
406
440
  }
441
+ if (column.type === "autocomplete") {
442
+ if (column.getOptionLabel) return column.getOptionLabel(value, row);
443
+ const match = column.autocompleteOptions?.find((o) => o.value === value);
444
+ return match ? match.label : String(value);
445
+ }
407
446
  return String(value);
408
447
  };
409
448
  const renderEditInput = () => {
410
449
  if (column.type === "checkbox") {
411
450
  return renderContent();
412
451
  }
452
+ if (column.type === "autocomplete") {
453
+ return /* @__PURE__ */ jsx(
454
+ AutocompleteEditor,
455
+ {
456
+ value: localValue,
457
+ column,
458
+ compactMode,
459
+ onConfirm,
460
+ onCancel
461
+ }
462
+ );
463
+ }
413
464
  if (column.type === "select" && column.options) {
414
465
  return /* @__PURE__ */ jsx(
415
466
  "select",
@@ -422,10 +473,11 @@ var SpreadsheetCell = ({
422
473
  onConfirm?.(newValue);
423
474
  },
424
475
  onKeyDown: handleKeyDown,
425
- onBlur: () => onConfirm?.(localValue),
476
+ onClick: (e) => e.stopPropagation(),
477
+ onMouseDown: (e) => e.stopPropagation(),
426
478
  className: cn(
427
479
  "w-full border-0 bg-blue-50 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-sm",
428
- compactMode ? "text-[10px]" : "text-xs"
480
+ compactMode ? "text-xs" : "text-sm"
429
481
  ),
430
482
  children: column.options.map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, option))
431
483
  }
@@ -450,7 +502,7 @@ var SpreadsheetCell = ({
450
502
  spellCheck: false,
451
503
  className: cn(
452
504
  "w-full border-0 bg-blue-50 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-sm",
453
- compactMode ? "text-[10px]" : "text-xs"
505
+ compactMode ? "text-xs" : "text-sm"
454
506
  )
455
507
  }
456
508
  );
@@ -474,60 +526,50 @@ var SpreadsheetCell = ({
474
526
  }
475
527
  const selectionBorderStyles = {};
476
528
  if (isInSelection && selectionEdge) {
477
- const borderColor = "rgb(59 130 246)";
478
- const borderWidth = "2px";
479
- if (selectionEdge.top) {
480
- selectionBorderStyles.borderTopColor = borderColor;
481
- selectionBorderStyles.borderTopWidth = borderWidth;
482
- }
483
- if (selectionEdge.right) {
484
- selectionBorderStyles.borderRightColor = borderColor;
485
- selectionBorderStyles.borderRightWidth = borderWidth;
486
- }
487
- if (selectionEdge.bottom) {
488
- selectionBorderStyles.borderBottomColor = borderColor;
489
- selectionBorderStyles.borderBottomWidth = borderWidth;
490
- }
491
- if (selectionEdge.left) {
492
- selectionBorderStyles.borderLeftColor = borderColor;
493
- selectionBorderStyles.borderLeftWidth = borderWidth;
529
+ const color = "rgb(59 130 246)";
530
+ const w = "2px";
531
+ const shadows = [];
532
+ if (selectionEdge.top) shadows.push(`inset 0 ${w} 0 0 ${color}`);
533
+ if (selectionEdge.bottom) shadows.push(`inset 0 -${w} 0 0 ${color}`);
534
+ if (selectionEdge.left) shadows.push(`inset ${w} 0 0 0 ${color}`);
535
+ if (selectionEdge.right) shadows.push(`inset -${w} 0 0 0 ${color}`);
536
+ if (shadows.length > 0) {
537
+ selectionBorderStyles.boxShadow = shadows.join(", ");
494
538
  }
495
539
  }
496
540
  return /* @__PURE__ */ jsx(
497
541
  "td",
498
542
  {
499
543
  onClick,
544
+ onDoubleClick,
500
545
  onMouseDown,
501
546
  onMouseEnter,
502
547
  onKeyDown: handleCellKeyDown,
503
548
  "data-cell-id": `${rowId}-${column.id}`,
549
+ "data-column-id": column.id,
504
550
  className: cn(
505
- "border border-gray-200 group cursor-pointer transition-colors select-none",
506
- compactMode ? "text-[10px]" : "text-xs",
551
+ "border border-gray-200 group cursor-pointer transition-colors select-none relative",
552
+ compactMode ? "text-xs" : "text-sm",
507
553
  cellPadding,
508
554
  column.align === "right" && "text-right",
509
555
  column.align === "center" && "text-center",
510
556
  isCopied && "animate-pulse",
511
557
  isFocused && !isInSelection && "ring-2 ring-blue-500 ring-inset",
512
558
  isInSelection && "bg-blue-50",
513
- isPinned ? "z-20" : "z-0",
559
+ !isPinned && "z-0",
514
560
  className
515
561
  ),
516
562
  style: {
517
563
  backgroundColor: isInSelection ? "rgb(239 246 255)" : getBackgroundColor(),
518
- minWidth: column.minWidth || column.width,
519
- // Pinned columns must have a fixed width so sticky offsets stay correct.
520
- // Enforce MIN_PINNED_COLUMN_WIDTH so header actions always fit.
521
- ...isPinned && {
522
- width: Math.max(
523
- column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
524
- MIN_PINNED_COLUMN_WIDTH
525
- ),
526
- maxWidth: Math.max(
527
- column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
528
- MIN_PINNED_COLUMN_WIDTH
529
- )
564
+ // Always set explicit width to prevent layout shift on selection/re-render
565
+ ...resolvedWidth ? {
566
+ width: resolvedWidth,
567
+ minWidth: resolvedWidth
568
+ } : {
569
+ width: column.width || column.minWidth,
570
+ minWidth: column.minWidth || column.width
530
571
  },
572
+ ...isPinned && pinnedZIndex !== void 0 && { zIndex: pinnedZIndex },
531
573
  ...positionStyles,
532
574
  ...selectionBorderStyles
533
575
  },
@@ -537,12 +579,13 @@ var SpreadsheetCell = ({
537
579
  {
538
580
  className: cn(
539
581
  "flex-1 truncate",
540
- isEditable && "cursor-text bg-blue-50/50 rounded"
582
+ isEditable && "cursor-text bg-blue-50 rounded px-1"
541
583
  ),
542
584
  title: String(value ?? ""),
543
585
  children: renderContent()
544
586
  }
545
587
  ),
588
+ isEditable && (column.type === "select" || column.type === "autocomplete") && !isEditing && /* @__PURE__ */ jsx(HiChevronDown, { className: "h-3 w-3 shrink-0 text-gray-400" }),
546
589
  !isInSelection && !isFocused && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0", children: [
547
590
  column.highlightable !== false && onHighlight && /* @__PURE__ */ jsx(
548
591
  "button",
@@ -607,6 +650,7 @@ var MemoizedSpreadsheetCell = memo(SpreadsheetCell, (prevProps, nextProps) => {
607
650
  if (prevProps.isRowSelected !== nextProps.isRowSelected) return false;
608
651
  if (prevProps.isRowHovered !== nextProps.isRowHovered) return false;
609
652
  if (prevProps.highlightColor !== nextProps.highlightColor) return false;
653
+ if (prevProps.isDuplicate !== nextProps.isDuplicate) return false;
610
654
  if (prevProps.hasComments !== nextProps.hasComments) return false;
611
655
  if (prevProps.unresolvedCommentCount !== nextProps.unresolvedCommentCount) return false;
612
656
  if (prevProps.isCopied !== nextProps.isCopied) return false;
@@ -614,6 +658,7 @@ var MemoizedSpreadsheetCell = memo(SpreadsheetCell, (prevProps, nextProps) => {
614
658
  if (prevProps.leftOffset !== nextProps.leftOffset) return false;
615
659
  if (prevProps.rightOffset !== nextProps.rightOffset) return false;
616
660
  if (prevProps.compactMode !== nextProps.compactMode) return false;
661
+ if (prevProps.isOddRow !== nextProps.isOddRow) return false;
617
662
  const prevEdge = prevProps.selectionEdge;
618
663
  const nextEdge = nextProps.selectionEdge;
619
664
  if (prevEdge !== nextEdge) {
@@ -628,8 +673,9 @@ var MemoizedSpreadsheetCell = memo(SpreadsheetCell, (prevProps, nextProps) => {
628
673
  MemoizedSpreadsheetCell.displayName = "MemoizedSpreadsheetCell";
629
674
 
630
675
  // src/components/SpreadsheetFilterDropdown.tsx
631
- import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
632
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
676
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
677
+ import { createPortal as createPortal2 } from "react-dom";
678
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
633
679
  var TEXT_OPERATORS = [
634
680
  { value: "contains", label: "Contains" },
635
681
  { value: "notContains", label: "Does not contain" },
@@ -667,59 +713,121 @@ var DATE_OPERATORS = [
667
713
  { value: "isEmpty", label: "Is empty" },
668
714
  { value: "isNotEmpty", label: "Is not empty" }
669
715
  ];
716
+ var DROPDOWN_WIDTH = 256;
717
+ var DROPDOWN_HEIGHT = 340;
718
+ var ARROW_SIZE = 6;
670
719
  var SpreadsheetFilterDropdown = ({
671
720
  column,
672
721
  filter,
673
722
  onFilterChange,
674
723
  onClose,
675
- className
724
+ className,
725
+ triggerRef,
726
+ hasDuplicateCheck = false
676
727
  }) => {
677
- const [textOperator, setTextOperator] = useState3(
728
+ const [showDuplicatesOnly, setShowDuplicatesOnly] = useState2(
729
+ filter?.showDuplicatesOnly ?? false
730
+ );
731
+ const [textOperator, setTextOperator] = useState2(
678
732
  filter?.textCondition?.operator || "contains"
679
733
  );
680
- const [textValue, setTextValue] = useState3(filter?.textCondition?.value || "");
681
- const [numberOperator, setNumberOperator] = useState3(
734
+ const [textValue, setTextValue] = useState2(filter?.textCondition?.value || "");
735
+ const [numberOperator, setNumberOperator] = useState2(
682
736
  filter?.numberCondition?.operator || "equals"
683
737
  );
684
- const [numberValue, setNumberValue] = useState3(
738
+ const [numberValue, setNumberValue] = useState2(
685
739
  filter?.numberCondition?.value?.toString() || ""
686
740
  );
687
- const [numberValueTo, setNumberValueTo] = useState3(
741
+ const [numberValueTo, setNumberValueTo] = useState2(
688
742
  filter?.numberCondition?.valueTo?.toString() || ""
689
743
  );
690
- const [dateOperator, setDateOperator] = useState3(
744
+ const [dateOperator, setDateOperator] = useState2(
691
745
  filter?.dateCondition?.operator || "equals"
692
746
  );
693
- const [dateValue, setDateValue] = useState3(filter?.dateCondition?.value || "");
694
- const [dateValueTo, setDateValueTo] = useState3(filter?.dateCondition?.valueTo || "");
747
+ const [dateValue, setDateValue] = useState2(filter?.dateCondition?.value || "");
748
+ const [dateValueTo, setDateValueTo] = useState2(filter?.dateCondition?.valueTo || "");
695
749
  const dropdownRef = useRef2(null);
750
+ const [position, setPosition] = useState2({ top: 0, left: 0, arrowLeft: DROPDOWN_WIDTH / 2, flippedUp: false });
696
751
  const isNumeric = column.type === "number";
697
752
  const isDate = column.type === "date";
753
+ const updatePosition = useCallback2(() => {
754
+ if (!triggerRef?.current) return;
755
+ const rect = triggerRef.current.getBoundingClientRect();
756
+ let top = rect.bottom + 4;
757
+ let flippedUp = false;
758
+ if (top + DROPDOWN_HEIGHT > window.innerHeight) {
759
+ top = rect.top - DROPDOWN_HEIGHT - 4;
760
+ flippedUp = true;
761
+ }
762
+ let left = rect.left;
763
+ if (left + DROPDOWN_WIDTH > window.innerWidth) {
764
+ left = window.innerWidth - DROPDOWN_WIDTH - 8;
765
+ }
766
+ if (left < 8) {
767
+ left = 8;
768
+ }
769
+ const anchorCenter = rect.left + rect.width / 2;
770
+ const arrowLeft = Math.min(
771
+ Math.max(anchorCenter - left, ARROW_SIZE + 4),
772
+ DROPDOWN_WIDTH - ARROW_SIZE - 4
773
+ );
774
+ setPosition({ top, left, arrowLeft, flippedUp });
775
+ }, [triggerRef]);
776
+ useEffect2(() => {
777
+ if (!triggerRef?.current) return;
778
+ updatePosition();
779
+ window.addEventListener("scroll", updatePosition, true);
780
+ window.addEventListener("resize", updatePosition);
781
+ return () => {
782
+ window.removeEventListener("scroll", updatePosition, true);
783
+ window.removeEventListener("resize", updatePosition);
784
+ };
785
+ }, [updatePosition, triggerRef]);
698
786
  useEffect2(() => {
699
787
  const handleClickOutside = (event) => {
700
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
788
+ const target = event.target;
789
+ if (dropdownRef.current && !dropdownRef.current.contains(target)) {
790
+ if (triggerRef?.current?.contains(target)) return;
701
791
  onClose();
702
792
  }
703
793
  };
704
- document.addEventListener("mousedown", handleClickOutside);
705
- return () => document.removeEventListener("mousedown", handleClickOutside);
794
+ const rafId = requestAnimationFrame(() => {
795
+ document.addEventListener("mousedown", handleClickOutside);
796
+ });
797
+ return () => {
798
+ cancelAnimationFrame(rafId);
799
+ document.removeEventListener("mousedown", handleClickOutside);
800
+ };
801
+ }, [onClose, triggerRef]);
802
+ useEffect2(() => {
803
+ const handleKeyDown = (event) => {
804
+ if (event.key === "Escape") {
805
+ onClose();
806
+ }
807
+ };
808
+ document.addEventListener("keydown", handleKeyDown);
809
+ return () => document.removeEventListener("keydown", handleKeyDown);
706
810
  }, [onClose]);
707
811
  const handleApplyFilter = () => {
708
812
  let newFilter;
709
813
  if (isNumeric) {
710
814
  const needsValue = !["isEmpty", "isNotEmpty"].includes(numberOperator);
711
- if (needsValue && !numberValue) {
815
+ if (needsValue && !numberValue && !showDuplicatesOnly) {
712
816
  onFilterChange(void 0);
713
817
  onClose();
714
818
  return;
715
819
  }
716
820
  newFilter = {
717
- numberCondition: {
821
+ numberCondition: needsValue || !numberValue ? {
718
822
  operator: numberOperator,
719
823
  value: numberValue ? parseFloat(numberValue) : void 0,
720
824
  valueTo: numberValueTo ? parseFloat(numberValueTo) : void 0
721
- }
825
+ } : void 0,
826
+ ...showDuplicatesOnly && { showDuplicatesOnly: true }
722
827
  };
828
+ if (!newFilter.numberCondition && !newFilter.showDuplicatesOnly) {
829
+ newFilter = void 0;
830
+ }
723
831
  } else if (isDate) {
724
832
  const needsValue = ![
725
833
  "isEmpty",
@@ -732,7 +840,7 @@ var SpreadsheetFilterDropdown = ({
732
840
  "lastMonth",
733
841
  "thisYear"
734
842
  ].includes(dateOperator);
735
- if (needsValue && !dateValue) {
843
+ if (needsValue && !dateValue && !showDuplicatesOnly) {
736
844
  onFilterChange(void 0);
737
845
  onClose();
738
846
  return;
@@ -742,26 +850,32 @@ var SpreadsheetFilterDropdown = ({
742
850
  operator: dateOperator,
743
851
  value: dateValue || void 0,
744
852
  valueTo: dateValueTo || void 0
745
- }
853
+ },
854
+ ...showDuplicatesOnly && { showDuplicatesOnly: true }
746
855
  };
747
856
  } else {
748
857
  const needsValue = !["isEmpty", "isNotEmpty"].includes(textOperator);
749
- if (needsValue && !textValue) {
858
+ if (needsValue && !textValue && !showDuplicatesOnly) {
750
859
  onFilterChange(void 0);
751
860
  onClose();
752
861
  return;
753
862
  }
754
863
  newFilter = {
755
- textCondition: {
864
+ textCondition: needsValue || !textValue ? {
756
865
  operator: textOperator,
757
866
  value: textValue || void 0
758
- }
867
+ } : void 0,
868
+ ...showDuplicatesOnly && { showDuplicatesOnly: true }
759
869
  };
870
+ if (!newFilter.textCondition && !newFilter.showDuplicatesOnly) {
871
+ newFilter = void 0;
872
+ }
760
873
  }
761
874
  onFilterChange(newFilter);
762
875
  onClose();
763
876
  };
764
877
  const handleClearFilter = () => {
878
+ setShowDuplicatesOnly(false);
765
879
  setTextValue("");
766
880
  setNumberValue("");
767
881
  setNumberValueTo("");
@@ -770,6 +884,12 @@ var SpreadsheetFilterDropdown = ({
770
884
  onFilterChange(void 0);
771
885
  onClose();
772
886
  };
887
+ const handleFilterKeyDown = (e) => {
888
+ if (e.key === "Enter") {
889
+ e.preventDefault();
890
+ handleApplyFilter();
891
+ }
892
+ };
773
893
  const textNeedsValue = !["isEmpty", "isNotEmpty"].includes(textOperator);
774
894
  const numberNeedsValue = !["isEmpty", "isNotEmpty"].includes(numberOperator);
775
895
  const dateNeedsValue = ![
@@ -783,21 +903,58 @@ var SpreadsheetFilterDropdown = ({
783
903
  "lastMonth",
784
904
  "thisYear"
785
905
  ].includes(dateOperator);
786
- return /* @__PURE__ */ jsxs2(
906
+ const dropdownContent = /* @__PURE__ */ jsxs2(
787
907
  "div",
788
908
  {
789
909
  ref: dropdownRef,
790
910
  className: cn(
791
- "absolute top-full left-0 mt-1 bg-white border border-gray-200 shadow-lg rounded-lg w-64 overflow-hidden flex flex-col z-[100]",
911
+ "bg-white border border-gray-200 shadow-lg rounded-lg w-64 overflow-hidden flex flex-col",
912
+ !triggerRef && "absolute top-full left-0 mt-1 z-[100]",
792
913
  className
793
914
  ),
915
+ style: triggerRef ? { position: "fixed", top: position.top, left: position.left, zIndex: 9999 } : void 0,
794
916
  onClick: (e) => e.stopPropagation(),
795
917
  children: [
918
+ triggerRef && /* @__PURE__ */ jsx2(
919
+ "div",
920
+ {
921
+ className: "absolute pointer-events-none",
922
+ style: position.flippedUp ? {
923
+ bottom: -ARROW_SIZE,
924
+ left: position.arrowLeft - ARROW_SIZE,
925
+ width: 0,
926
+ height: 0,
927
+ borderLeft: `${ARROW_SIZE}px solid transparent`,
928
+ borderRight: `${ARROW_SIZE}px solid transparent`,
929
+ borderTop: `${ARROW_SIZE}px solid #e5e7eb`
930
+ } : {
931
+ top: -ARROW_SIZE,
932
+ left: position.arrowLeft - ARROW_SIZE,
933
+ width: 0,
934
+ height: 0,
935
+ borderLeft: `${ARROW_SIZE}px solid transparent`,
936
+ borderRight: `${ARROW_SIZE}px solid transparent`,
937
+ borderBottom: `${ARROW_SIZE}px solid #e5e7eb`
938
+ }
939
+ }
940
+ ),
796
941
  /* @__PURE__ */ jsx2("div", { className: "px-3 py-2 border-b border-gray-200 bg-gray-50", children: /* @__PURE__ */ jsxs2("span", { className: "text-xs font-medium text-gray-700", children: [
797
942
  "Filter: ",
798
943
  column.label
799
944
  ] }) }),
800
- /* @__PURE__ */ jsx2("div", { className: "p-3 space-y-3", children: isNumeric ? /* @__PURE__ */ jsxs2(Fragment, { children: [
945
+ hasDuplicateCheck && /* @__PURE__ */ jsx2("div", { className: "px-3 pt-2 pb-2 border-b border-gray-200", children: /* @__PURE__ */ jsxs2("label", { className: "flex items-center gap-2 cursor-pointer select-none", children: [
946
+ /* @__PURE__ */ jsx2(
947
+ "input",
948
+ {
949
+ type: "checkbox",
950
+ checked: showDuplicatesOnly,
951
+ onChange: (e) => setShowDuplicatesOnly(e.target.checked),
952
+ className: "h-3 w-3 rounded border-gray-300 text-red-600 focus:ring-red-500"
953
+ }
954
+ ),
955
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-gray-700", children: "Show only duplicates" })
956
+ ] }) }),
957
+ /* @__PURE__ */ jsx2("div", { className: "p-3 space-y-3", children: isNumeric ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
801
958
  /* @__PURE__ */ jsxs2("div", { children: [
802
959
  /* @__PURE__ */ jsx2("label", { className: "text-xs text-gray-500 mb-1 block", children: "Condition" }),
803
960
  /* @__PURE__ */ jsx2(
@@ -819,6 +976,7 @@ var SpreadsheetFilterDropdown = ({
819
976
  placeholder: "Enter value",
820
977
  value: numberValue,
821
978
  onChange: (e) => setNumberValue(e.target.value),
979
+ onKeyDown: handleFilterKeyDown,
822
980
  className: "w-full px-2 py-1.5 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
823
981
  }
824
982
  )
@@ -832,11 +990,12 @@ var SpreadsheetFilterDropdown = ({
832
990
  placeholder: "Enter end value",
833
991
  value: numberValueTo,
834
992
  onChange: (e) => setNumberValueTo(e.target.value),
993
+ onKeyDown: handleFilterKeyDown,
835
994
  className: "w-full px-2 py-1.5 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
836
995
  }
837
996
  )
838
997
  ] })
839
- ] }) : isDate ? /* @__PURE__ */ jsxs2(Fragment, { children: [
998
+ ] }) : isDate ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
840
999
  /* @__PURE__ */ jsxs2("div", { children: [
841
1000
  /* @__PURE__ */ jsx2("label", { className: "text-xs text-gray-500 mb-1 block", children: "Condition" }),
842
1001
  /* @__PURE__ */ jsx2(
@@ -857,6 +1016,7 @@ var SpreadsheetFilterDropdown = ({
857
1016
  type: "date",
858
1017
  value: dateValue,
859
1018
  onChange: (e) => setDateValue(e.target.value),
1019
+ onKeyDown: handleFilterKeyDown,
860
1020
  className: "w-full px-2 py-1.5 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
861
1021
  }
862
1022
  )
@@ -869,11 +1029,12 @@ var SpreadsheetFilterDropdown = ({
869
1029
  type: "date",
870
1030
  value: dateValueTo,
871
1031
  onChange: (e) => setDateValueTo(e.target.value),
1032
+ onKeyDown: handleFilterKeyDown,
872
1033
  className: "w-full px-2 py-1.5 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
873
1034
  }
874
1035
  )
875
1036
  ] })
876
- ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1037
+ ] }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
877
1038
  /* @__PURE__ */ jsxs2("div", { children: [
878
1039
  /* @__PURE__ */ jsx2("label", { className: "text-xs text-gray-500 mb-1 block", children: "Condition" }),
879
1040
  /* @__PURE__ */ jsx2(
@@ -895,6 +1056,7 @@ var SpreadsheetFilterDropdown = ({
895
1056
  placeholder: "Enter text",
896
1057
  value: textValue,
897
1058
  onChange: (e) => setTextValue(e.target.value),
1059
+ onKeyDown: handleFilterKeyDown,
898
1060
  className: "w-full px-2 py-1.5 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
899
1061
  }
900
1062
  )
@@ -929,6 +1091,10 @@ var SpreadsheetFilterDropdown = ({
929
1091
  ]
930
1092
  }
931
1093
  );
1094
+ if (triggerRef) {
1095
+ return createPortal2(dropdownContent, document.body);
1096
+ }
1097
+ return dropdownContent;
932
1098
  };
933
1099
  SpreadsheetFilterDropdown.displayName = "SpreadsheetFilterDropdown";
934
1100
 
@@ -1458,6 +1624,13 @@ var SpreadsheetToolbar = ({
1458
1624
  };
1459
1625
  SpreadsheetToolbar.displayName = "SpreadsheetToolbar";
1460
1626
 
1627
+ // src/components/SpreadsheetHeader.tsx
1628
+ import React4, { memo as memo2, useRef as useRef4 } from "react";
1629
+
1630
+ // src/components/ColumnHeaderActions.tsx
1631
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
1632
+ import { createPortal as createPortal3 } from "react-dom";
1633
+
1461
1634
  // ../../../node_modules/.pnpm/react-icons@4.12.0_react@18.3.1/node_modules/react-icons/md/index.esm.js
1462
1635
  function MdPushPin(props) {
1463
1636
  return GenIcon({ "tag": "svg", "attr": { "viewBox": "0 0 24 24" }, "child": [{ "tag": "path", "attr": { "fill": "none", "d": "M0 0h24v24H0z" } }, { "tag": "path", "attr": { "fillRule": "evenodd", "d": "M16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z" } }] })(props);
@@ -1475,6 +1648,9 @@ function ColumnHeaderActions({
1475
1648
  hasActiveFilter = false,
1476
1649
  hasActiveHighlight = false,
1477
1650
  isPinned = false,
1651
+ enableSorting = false,
1652
+ sortDirection = null,
1653
+ onSortClick,
1478
1654
  onFilterClick,
1479
1655
  onHighlightClick,
1480
1656
  onPinClick,
@@ -1482,20 +1658,194 @@ function ColumnHeaderActions({
1482
1658
  highlightTitle = "Highlight column",
1483
1659
  pinnedTitle = "Unpin column",
1484
1660
  unpinnedTitle = "Pin column",
1485
- className
1661
+ className,
1662
+ resolvedWidth,
1663
+ enableDuplicateCheck = false,
1664
+ hasDuplicateCheck = false,
1665
+ onDuplicateCheckClick
1486
1666
  }) {
1667
+ const [overflowOpen, setOverflowOpen] = useState3(false);
1668
+ const overflowRef = useRef3(null);
1669
+ const triggerRef = useRef3(null);
1670
+ const [dropdownPos, setDropdownPos] = useState3(null);
1671
+ const isCompact = resolvedWidth !== void 0 && resolvedWidth < 80;
1672
+ useEffect3(() => {
1673
+ if (!overflowOpen) return;
1674
+ const handler = (e) => {
1675
+ if (overflowRef.current && !overflowRef.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target)) {
1676
+ setOverflowOpen(false);
1677
+ }
1678
+ };
1679
+ document.addEventListener("mousedown", handler);
1680
+ return () => document.removeEventListener("mousedown", handler);
1681
+ }, [overflowOpen]);
1682
+ const openOverflow = (e) => {
1683
+ e.stopPropagation();
1684
+ if (!overflowOpen && triggerRef.current) {
1685
+ const rect = triggerRef.current.getBoundingClientRect();
1686
+ setDropdownPos({
1687
+ top: rect.bottom + window.scrollY + 4,
1688
+ right: window.innerWidth - rect.right - window.scrollX
1689
+ });
1690
+ }
1691
+ setOverflowOpen((prev) => !prev);
1692
+ };
1693
+ if (isCompact) {
1694
+ return /* @__PURE__ */ jsxs5("div", { className: cn("flex items-center", className), children: [
1695
+ /* @__PURE__ */ jsx5(
1696
+ "button",
1697
+ {
1698
+ ref: triggerRef,
1699
+ type: "button",
1700
+ onClick: openOverflow,
1701
+ className: "p-0.5 hover:bg-gray-200 rounded text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity",
1702
+ title: "Column actions",
1703
+ children: /* @__PURE__ */ jsx5(HiDotsVertical, { className: "h-3 w-3" })
1704
+ }
1705
+ ),
1706
+ overflowOpen && dropdownPos && createPortal3(
1707
+ /* @__PURE__ */ jsxs5(
1708
+ "div",
1709
+ {
1710
+ ref: overflowRef,
1711
+ style: {
1712
+ position: "absolute",
1713
+ top: dropdownPos.top,
1714
+ right: dropdownPos.right
1715
+ },
1716
+ className: "bg-white border border-gray-200 shadow-lg rounded-md py-1 w-44 z-[9999]",
1717
+ children: [
1718
+ enableSorting && onSortClick && /* @__PURE__ */ jsxs5(
1719
+ "button",
1720
+ {
1721
+ type: "button",
1722
+ onClick: (e) => {
1723
+ e.stopPropagation();
1724
+ onSortClick();
1725
+ setOverflowOpen(false);
1726
+ },
1727
+ className: cn(
1728
+ "flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 text-left",
1729
+ sortDirection ? "text-blue-600" : "text-gray-700"
1730
+ ),
1731
+ children: [
1732
+ sortDirection === "desc" ? /* @__PURE__ */ jsx5(HiSortDescending, { className: "h-3.5 w-3.5 shrink-0" }) : /* @__PURE__ */ jsx5(HiSortAscending, { className: "h-3.5 w-3.5 shrink-0" }),
1733
+ sortDirection === "asc" ? "Sorted ascending" : sortDirection === "desc" ? "Sorted descending" : "Sort"
1734
+ ]
1735
+ }
1736
+ ),
1737
+ enableFiltering && onFilterClick && /* @__PURE__ */ jsxs5(
1738
+ "button",
1739
+ {
1740
+ type: "button",
1741
+ onClick: (e) => {
1742
+ e.stopPropagation();
1743
+ onFilterClick();
1744
+ setOverflowOpen(false);
1745
+ },
1746
+ className: cn(
1747
+ "flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 text-left",
1748
+ hasActiveFilter ? "text-blue-600" : "text-gray-700"
1749
+ ),
1750
+ children: [
1751
+ /* @__PURE__ */ jsx5(HiFilter, { className: "h-3.5 w-3.5 shrink-0" }),
1752
+ "Filter",
1753
+ hasActiveFilter && /* @__PURE__ */ jsx5("span", { className: "ml-auto h-1.5 w-1.5 rounded-full bg-blue-500 shrink-0" })
1754
+ ]
1755
+ }
1756
+ ),
1757
+ enableHighlighting && onHighlightClick && /* @__PURE__ */ jsxs5(
1758
+ "button",
1759
+ {
1760
+ type: "button",
1761
+ onClick: (e) => {
1762
+ e.stopPropagation();
1763
+ onHighlightClick();
1764
+ setOverflowOpen(false);
1765
+ },
1766
+ className: cn(
1767
+ "flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 text-left",
1768
+ hasActiveHighlight ? "text-amber-500" : "text-gray-700"
1769
+ ),
1770
+ children: [
1771
+ /* @__PURE__ */ jsx5(AiFillHighlight, { className: "h-3.5 w-3.5 shrink-0" }),
1772
+ "Highlight"
1773
+ ]
1774
+ }
1775
+ ),
1776
+ enablePinning && onPinClick && /* @__PURE__ */ jsxs5(
1777
+ "button",
1778
+ {
1779
+ type: "button",
1780
+ onClick: (e) => {
1781
+ e.stopPropagation();
1782
+ onPinClick();
1783
+ setOverflowOpen(false);
1784
+ },
1785
+ className: cn(
1786
+ "flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 text-left",
1787
+ isPinned ? "text-blue-600" : "text-gray-700"
1788
+ ),
1789
+ children: [
1790
+ isPinned ? /* @__PURE__ */ jsx5(MdPushPin, { className: "h-3.5 w-3.5 shrink-0" }) : /* @__PURE__ */ jsx5(MdOutlinePushPin, { className: "h-3.5 w-3.5 shrink-0" }),
1791
+ isPinned ? "Unpin" : "Pin"
1792
+ ]
1793
+ }
1794
+ ),
1795
+ enableDuplicateCheck && onDuplicateCheckClick && /* @__PURE__ */ jsxs5(
1796
+ "button",
1797
+ {
1798
+ type: "button",
1799
+ onClick: (e) => {
1800
+ e.stopPropagation();
1801
+ onDuplicateCheckClick();
1802
+ setOverflowOpen(false);
1803
+ },
1804
+ className: cn(
1805
+ "flex items-center gap-2 w-full px-3 py-1.5 text-xs hover:bg-gray-100 text-left",
1806
+ hasDuplicateCheck ? "text-purple-600" : "text-gray-700"
1807
+ ),
1808
+ children: [
1809
+ /* @__PURE__ */ jsx5(HiExclamation, { className: "h-3.5 w-3.5 shrink-0" }),
1810
+ "Check Duplicates"
1811
+ ]
1812
+ }
1813
+ )
1814
+ ]
1815
+ }
1816
+ ),
1817
+ document.body
1818
+ )
1819
+ ] });
1820
+ }
1487
1821
  return /* @__PURE__ */ jsxs5("div", { className: cn("flex items-center gap-0.5", className), children: [
1488
- enableFiltering && onFilterClick && /* @__PURE__ */ jsx5(
1822
+ enableSorting && onSortClick && /* @__PURE__ */ jsx5(
1489
1823
  "button",
1490
1824
  {
1491
1825
  type: "button",
1492
1826
  onClick: (e) => {
1493
1827
  e.stopPropagation();
1494
- onFilterClick();
1828
+ onSortClick();
1495
1829
  },
1496
1830
  className: cn(
1497
1831
  "p-0.5 hover:bg-gray-200 rounded transition-opacity",
1498
- hasActiveFilter ? "text-blue-600 opacity-100" : "text-gray-400 opacity-0 group-hover:opacity-100"
1832
+ sortDirection ? "text-blue-600 opacity-100" : "text-gray-400 opacity-0 group-hover:opacity-100"
1833
+ ),
1834
+ title: sortDirection === "asc" ? "Sorted ascending" : sortDirection === "desc" ? "Sorted descending" : "Sort column",
1835
+ children: sortDirection === "desc" ? /* @__PURE__ */ jsx5(HiSortDescending, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx5(HiSortAscending, { className: "h-3 w-3" })
1836
+ }
1837
+ ),
1838
+ enableFiltering && onFilterClick && /* @__PURE__ */ jsx5(
1839
+ "button",
1840
+ {
1841
+ type: "button",
1842
+ onClick: (e) => {
1843
+ e.stopPropagation();
1844
+ onFilterClick();
1845
+ },
1846
+ className: cn(
1847
+ "p-0.5 hover:bg-gray-200 rounded transition-opacity",
1848
+ hasActiveFilter ? "text-blue-600 opacity-100" : "text-gray-400 opacity-0 group-hover:opacity-100"
1499
1849
  ),
1500
1850
  title: filterTitle,
1501
1851
  children: /* @__PURE__ */ jsx5(HiFilter, { className: "h-3 w-3" })
@@ -1532,6 +1882,22 @@ function ColumnHeaderActions({
1532
1882
  title: isPinned ? pinnedTitle : unpinnedTitle,
1533
1883
  children: isPinned ? /* @__PURE__ */ jsx5(MdPushPin, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx5(MdOutlinePushPin, { className: "h-3 w-3" })
1534
1884
  }
1885
+ ),
1886
+ enableDuplicateCheck && onDuplicateCheckClick && /* @__PURE__ */ jsx5(
1887
+ "button",
1888
+ {
1889
+ type: "button",
1890
+ onClick: (e) => {
1891
+ e.stopPropagation();
1892
+ onDuplicateCheckClick();
1893
+ },
1894
+ className: cn(
1895
+ "p-0.5 hover:bg-gray-200 rounded transition-opacity",
1896
+ hasDuplicateCheck ? "text-purple-600 opacity-100" : "text-gray-400 opacity-0 group-hover:opacity-100"
1897
+ ),
1898
+ title: "Check duplicates",
1899
+ children: /* @__PURE__ */ jsx5(HiExclamation, { className: "h-3 w-3" })
1900
+ }
1535
1901
  )
1536
1902
  ] });
1537
1903
  }
@@ -1539,8 +1905,8 @@ ColumnHeaderActions.displayName = "ColumnHeaderActions";
1539
1905
 
1540
1906
  // src/components/SpreadsheetHeader.tsx
1541
1907
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1542
- var cellPaddingCompact2 = "px-1 py-0.5";
1543
- var cellPaddingNormal2 = "px-2 py-1.5";
1908
+ var cellPaddingCompact2 = "px-1.5 py-1";
1909
+ var cellPaddingNormal2 = "px-2 py-2";
1544
1910
  var SpreadsheetHeader = ({
1545
1911
  column,
1546
1912
  sortConfig,
@@ -1552,12 +1918,21 @@ var SpreadsheetHeader = ({
1552
1918
  highlightColor,
1553
1919
  compactMode = false,
1554
1920
  onClick,
1921
+ pinnedZIndex,
1922
+ onSortClick,
1555
1923
  onFilterClick,
1556
1924
  onPinClick,
1557
1925
  onHighlightClick,
1926
+ hasDuplicateCheck = false,
1927
+ onDuplicateCheckClick,
1928
+ duplicateCount,
1929
+ resizeHandleProps,
1930
+ resolvedWidth,
1931
+ topOffset = 0,
1558
1932
  className,
1559
1933
  children
1560
1934
  }) => {
1935
+ const thRef = useRef4(null);
1561
1936
  const isSorted = sortConfig?.columnId === column.id;
1562
1937
  const sortDirection = isSorted ? sortConfig.direction : null;
1563
1938
  const cellPadding = compactMode ? cellPaddingCompact2 : cellPaddingNormal2;
@@ -1573,46 +1948,55 @@ var SpreadsheetHeader = ({
1573
1948
  return /* @__PURE__ */ jsxs6(
1574
1949
  "th",
1575
1950
  {
1576
- onClick: column.sortable ? onClick : void 0,
1951
+ ref: thRef,
1952
+ "data-column-id": column.id,
1953
+ onClick,
1577
1954
  className: cn(
1578
- "border border-gray-200 font-semibold text-gray-700 sticky group",
1579
- compactMode ? "text-[10px]" : "text-xs",
1955
+ "border border-gray-200 font-semibold text-gray-700 group relative cursor-pointer",
1956
+ isPinned && "sticky",
1957
+ compactMode ? "text-xs" : "text-sm",
1580
1958
  cellPadding,
1581
1959
  column.align === "right" && "text-right",
1582
1960
  column.align === "center" && "text-center",
1583
- column.sortable && "cursor-pointer hover:bg-gray-100",
1584
- isPinned ? "z-30" : "z-20",
1961
+ "hover:bg-gray-100",
1962
+ !isPinned && "z-20",
1585
1963
  className
1586
1964
  ),
1587
1965
  style: {
1588
1966
  backgroundColor: highlightColor || "rgb(243 244 246)",
1589
1967
  // gray-100
1590
- minWidth: column.minWidth || column.width,
1591
- // Pinned columns must have a fixed width so sticky offsets stay correct.
1592
- // Enforce MIN_PINNED_COLUMN_WIDTH so header actions (pin/filter/highlight) always fit.
1593
- ...isPinned && {
1594
- width: Math.max(
1595
- column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
1596
- MIN_PINNED_COLUMN_WIDTH
1597
- ),
1598
- maxWidth: Math.max(
1599
- column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH,
1600
- MIN_PINNED_COLUMN_WIDTH
1601
- )
1968
+ // Always set explicit width to prevent layout shift on selection/re-render
1969
+ ...resolvedWidth ? {
1970
+ width: resolvedWidth,
1971
+ minWidth: resolvedWidth
1972
+ } : {
1973
+ width: column.width || column.minWidth,
1974
+ minWidth: column.minWidth || column.width
1602
1975
  },
1603
- top: 0,
1604
- // For sticky header
1976
+ ...isPinned && pinnedZIndex !== void 0 && { zIndex: pinnedZIndex + 10 },
1977
+ // +10 so headers are above cells
1605
1978
  ...positionStyles
1606
1979
  },
1607
1980
  children: [
1608
1981
  /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between gap-1", children: [
1609
1982
  /* @__PURE__ */ jsxs6("span", { className: "flex-1 flex items-center gap-1", children: [
1610
1983
  column.label,
1611
- isSorted && /* @__PURE__ */ jsx6("span", { className: "text-blue-600", children: sortDirection === "asc" ? /* @__PURE__ */ jsx6(HiChevronUp, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx6(HiChevronDown, { className: "h-3 w-3" }) })
1984
+ isSorted && /* @__PURE__ */ jsx6("span", { className: "text-blue-600", children: sortDirection === "asc" ? /* @__PURE__ */ jsx6(HiChevronUp, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx6(HiChevronDown, { className: "h-3 w-3" }) }),
1985
+ hasDuplicateCheck && duplicateCount != null && duplicateCount > 0 && /* @__PURE__ */ jsx6(
1986
+ "span",
1987
+ {
1988
+ className: "inline-flex items-center px-1 py-0.5 rounded-full bg-red-100 text-red-600 text-[10px] font-medium leading-none",
1989
+ title: `${duplicateCount} duplicate values found`,
1990
+ children: duplicateCount
1991
+ }
1992
+ )
1612
1993
  ] }),
1613
1994
  /* @__PURE__ */ jsx6(
1614
1995
  ColumnHeaderActions,
1615
1996
  {
1997
+ enableSorting: column.sortable,
1998
+ sortDirection: isSorted ? sortDirection : null,
1999
+ onSortClick,
1616
2000
  enableFiltering: column.filterable,
1617
2001
  enableHighlighting: column.highlightable !== false && !!onHighlightClick,
1618
2002
  enablePinning: column.pinnable !== false,
@@ -1621,21 +2005,249 @@ var SpreadsheetHeader = ({
1621
2005
  isPinned,
1622
2006
  onFilterClick,
1623
2007
  onHighlightClick,
1624
- onPinClick
2008
+ onPinClick,
2009
+ resolvedWidth,
2010
+ enableDuplicateCheck: true,
2011
+ hasDuplicateCheck,
2012
+ onDuplicateCheckClick
1625
2013
  }
1626
2014
  )
1627
2015
  ] }),
1628
- children
2016
+ children && React4.Children.map(
2017
+ children,
2018
+ (child) => React4.isValidElement(child) ? React4.cloneElement(child, { triggerRef: thRef }) : child
2019
+ ),
2020
+ resizeHandleProps && /* @__PURE__ */ jsx6(
2021
+ "div",
2022
+ {
2023
+ onMouseDown: resizeHandleProps.onMouseDown,
2024
+ style: resizeHandleProps.style,
2025
+ className: resizeHandleProps.className
2026
+ }
2027
+ )
1629
2028
  ]
1630
2029
  }
1631
2030
  );
1632
2031
  };
1633
2032
  SpreadsheetHeader.displayName = "SpreadsheetHeader";
2033
+ var MemoizedSpreadsheetHeader = memo2(SpreadsheetHeader, (prevProps, nextProps) => {
2034
+ if (prevProps.column.id !== nextProps.column.id) return false;
2035
+ if (prevProps.sortConfig?.columnId !== nextProps.sortConfig?.columnId) return false;
2036
+ if (prevProps.sortConfig?.direction !== nextProps.sortConfig?.direction) return false;
2037
+ if (prevProps.hasActiveFilter !== nextProps.hasActiveFilter) return false;
2038
+ if (prevProps.isPinned !== nextProps.isPinned) return false;
2039
+ if (prevProps.pinSide !== nextProps.pinSide) return false;
2040
+ if (prevProps.leftOffset !== nextProps.leftOffset) return false;
2041
+ if (prevProps.rightOffset !== nextProps.rightOffset) return false;
2042
+ if (prevProps.highlightColor !== nextProps.highlightColor) return false;
2043
+ if (prevProps.compactMode !== nextProps.compactMode) return false;
2044
+ if (prevProps.pinnedZIndex !== nextProps.pinnedZIndex) return false;
2045
+ if (prevProps.resolvedWidth !== nextProps.resolvedWidth) return false;
2046
+ if (prevProps.topOffset !== nextProps.topOffset) return false;
2047
+ if (prevProps.hasDuplicateCheck !== nextProps.hasDuplicateCheck) return false;
2048
+ if (prevProps.duplicateCount !== nextProps.duplicateCount) return false;
2049
+ if (prevProps.children !== nextProps.children) return false;
2050
+ return true;
2051
+ });
2052
+ MemoizedSpreadsheetHeader.displayName = "MemoizedSpreadsheetHeader";
2053
+
2054
+ // src/hooks/useSpreadsheetPinning.ts
2055
+ import { useCallback as useCallback3, useEffect as useEffect4, useMemo, useRef as useRef5, useState as useState4 } from "react";
2056
+ var ROW_INDEX_COLUMN_ID = "__row_index__";
2057
+ var ROW_INDEX_COLUMN_WIDTH = 90;
2058
+ function useSpreadsheetPinning({
2059
+ columns,
2060
+ columnGroups,
2061
+ showRowIndex = true,
2062
+ defaultPinnedColumns = [],
2063
+ defaultPinnedRightColumns = [],
2064
+ getColumnWidth
2065
+ }) {
2066
+ const [pinnedColumns, setPinnedColumns] = useState4(() => {
2067
+ const map = /* @__PURE__ */ new Map();
2068
+ if (showRowIndex) {
2069
+ map.set(ROW_INDEX_COLUMN_ID, "left");
2070
+ }
2071
+ const midpoint = Math.floor(columns.length / 2);
2072
+ defaultPinnedColumns.forEach((col) => {
2073
+ if (col === ROW_INDEX_COLUMN_ID) return;
2074
+ const colIndex = columns.findIndex((c) => c.id === col);
2075
+ map.set(col, colIndex >= 0 && colIndex <= midpoint ? "left" : "right");
2076
+ });
2077
+ defaultPinnedRightColumns.forEach((col) => map.set(col, "right"));
2078
+ return map;
2079
+ });
2080
+ const [collapsedGroups, setCollapsedGroups] = useState4(/* @__PURE__ */ new Set());
2081
+ const measuredWidthsRef = useRef5(/* @__PURE__ */ new Map());
2082
+ const tableElRef = useRef5(null);
2083
+ const [measureVersion, setMeasureVersion] = useState4(0);
2084
+ const measureColumnWidths = useCallback3(() => {
2085
+ const el = tableElRef.current;
2086
+ if (!el) return;
2087
+ const newMap = /* @__PURE__ */ new Map();
2088
+ const headers = el.querySelectorAll("th[data-column-id]");
2089
+ headers.forEach((th) => {
2090
+ const colId = th.getAttribute("data-column-id");
2091
+ if (colId) {
2092
+ newMap.set(colId, th.offsetWidth);
2093
+ }
2094
+ });
2095
+ const rowIndexTd = el.querySelector('td[data-column-id="__row_index__"]');
2096
+ if (rowIndexTd) {
2097
+ newMap.set(ROW_INDEX_COLUMN_ID, rowIndexTd.offsetWidth);
2098
+ }
2099
+ measuredWidthsRef.current = newMap;
2100
+ setMeasureVersion((v) => v + 1);
2101
+ }, []);
2102
+ const measureRef = useCallback3((el) => {
2103
+ tableElRef.current = el;
2104
+ }, []);
2105
+ const isRowIndexPinned = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
2106
+ const handleTogglePin = useCallback3(
2107
+ (columnId) => {
2108
+ setPinnedColumns((prev) => {
2109
+ const newMap = new Map(prev);
2110
+ if (newMap.has(columnId)) {
2111
+ newMap.delete(columnId);
2112
+ } else {
2113
+ if (columnId === ROW_INDEX_COLUMN_ID) {
2114
+ newMap.set(columnId, "left");
2115
+ } else {
2116
+ const colIndex = columns.findIndex((c) => c.id === columnId);
2117
+ const midpoint = Math.floor(columns.length / 2);
2118
+ newMap.set(columnId, colIndex <= midpoint ? "left" : "right");
2119
+ }
2120
+ }
2121
+ return newMap;
2122
+ });
2123
+ },
2124
+ [columns]
2125
+ );
2126
+ const setPinnedColumnsFromIds = useCallback3((columnIds) => {
2127
+ const map = /* @__PURE__ */ new Map();
2128
+ if (showRowIndex) {
2129
+ map.set(ROW_INDEX_COLUMN_ID, "left");
2130
+ }
2131
+ const midpoint = Math.floor(columns.length / 2);
2132
+ columnIds.forEach((col) => {
2133
+ if (col !== ROW_INDEX_COLUMN_ID) {
2134
+ const colIndex = columns.findIndex((c) => c.id === col);
2135
+ map.set(col, colIndex >= 0 && colIndex <= midpoint ? "left" : "right");
2136
+ }
2137
+ });
2138
+ setPinnedColumns(map);
2139
+ }, [showRowIndex, columns]);
2140
+ const handleToggleGroupCollapse = useCallback3((groupId) => {
2141
+ setCollapsedGroups((prev) => {
2142
+ const newSet = new Set(prev);
2143
+ if (newSet.has(groupId)) {
2144
+ newSet.delete(groupId);
2145
+ } else {
2146
+ newSet.add(groupId);
2147
+ }
2148
+ return newSet;
2149
+ });
2150
+ }, []);
2151
+ const visibleColumns = useMemo(() => {
2152
+ if (!columns || !Array.isArray(columns) || columns.length === 0) return [];
2153
+ let result = [...columns];
2154
+ if (columnGroups && Array.isArray(columnGroups)) {
2155
+ result = result.filter((column) => {
2156
+ const group = columnGroups.find((g) => g.columns.includes(column.id));
2157
+ if (!group) return true;
2158
+ if (!collapsedGroups.has(group.id)) return true;
2159
+ return pinnedColumns.has(column.id);
2160
+ });
2161
+ }
2162
+ return result;
2163
+ }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
2164
+ useEffect4(() => {
2165
+ requestAnimationFrame(() => measureColumnWidths());
2166
+ }, [measureColumnWidths, visibleColumns, pinnedColumns]);
2167
+ const widthMap = useMemo(() => {
2168
+ void measureVersion;
2169
+ const map = /* @__PURE__ */ new Map();
2170
+ for (const col of columns) {
2171
+ const measured = measuredWidthsRef.current.get(col.id);
2172
+ if (measured) {
2173
+ map.set(col.id, measured);
2174
+ continue;
2175
+ }
2176
+ const resized = getColumnWidth?.(col.id);
2177
+ if (resized) {
2178
+ map.set(col.id, resized);
2179
+ continue;
2180
+ }
2181
+ map.set(col.id, col.width || col.minWidth || 100);
2182
+ }
2183
+ const measuredRI = measuredWidthsRef.current.get(ROW_INDEX_COLUMN_ID);
2184
+ map.set(ROW_INDEX_COLUMN_ID, measuredRI || ROW_INDEX_COLUMN_WIDTH);
2185
+ return map;
2186
+ }, [columns, getColumnWidth, measureVersion]);
2187
+ const { leftOffsets, rightOffsets, zIndices } = useMemo(() => {
2188
+ const leftOffsets2 = /* @__PURE__ */ new Map();
2189
+ const rightOffsets2 = /* @__PURE__ */ new Map();
2190
+ const zIndices2 = /* @__PURE__ */ new Map();
2191
+ leftOffsets2.set(ROW_INDEX_COLUMN_ID, 0);
2192
+ zIndices2.set(ROW_INDEX_COLUMN_ID, 40);
2193
+ const leftPinned = visibleColumns.filter((c) => pinnedColumns.get(c.id) === "left");
2194
+ const rightPinned = visibleColumns.filter((c) => pinnedColumns.get(c.id) === "right");
2195
+ const isRowIndexPinnedNow = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
2196
+ let leftOffset = showRowIndex && isRowIndexPinnedNow ? widthMap.get(ROW_INDEX_COLUMN_ID) || ROW_INDEX_COLUMN_WIDTH : 0;
2197
+ for (let i = 0; i < leftPinned.length; i++) {
2198
+ leftOffsets2.set(leftPinned[i].id, leftOffset);
2199
+ zIndices2.set(leftPinned[i].id, 20 + (leftPinned.length - i));
2200
+ leftOffset += widthMap.get(leftPinned[i].id) || 100;
2201
+ }
2202
+ let rightOffset = 0;
2203
+ for (let i = rightPinned.length - 1; i >= 0; i--) {
2204
+ rightOffsets2.set(rightPinned[i].id, rightOffset);
2205
+ zIndices2.set(rightPinned[i].id, 20 + (rightPinned.length - i));
2206
+ rightOffset += widthMap.get(rightPinned[i].id) || 100;
2207
+ }
2208
+ return { leftOffsets: leftOffsets2, rightOffsets: rightOffsets2, zIndices: zIndices2 };
2209
+ }, [pinnedColumns, visibleColumns, showRowIndex, widthMap]);
2210
+ const getColumnLeftOffset = useCallback3(
2211
+ (columnId) => leftOffsets.get(columnId) ?? 0,
2212
+ [leftOffsets]
2213
+ );
2214
+ const getColumnRightOffset = useCallback3(
2215
+ (columnId) => rightOffsets.get(columnId) ?? 0,
2216
+ [rightOffsets]
2217
+ );
2218
+ const isColumnPinned = useCallback3(
2219
+ (columnId) => pinnedColumns.has(columnId),
2220
+ [pinnedColumns]
2221
+ );
2222
+ const getColumnPinSide = useCallback3(
2223
+ (columnId) => pinnedColumns.get(columnId),
2224
+ [pinnedColumns]
2225
+ );
2226
+ const getPinnedZIndex = useCallback3(
2227
+ (columnId) => zIndices.get(columnId) ?? 0,
2228
+ [zIndices]
2229
+ );
2230
+ return {
2231
+ pinnedColumns,
2232
+ isRowIndexPinned,
2233
+ collapsedGroups,
2234
+ visibleColumns,
2235
+ handleTogglePin,
2236
+ handleToggleGroupCollapse,
2237
+ setPinnedColumnsFromIds,
2238
+ getColumnLeftOffset,
2239
+ getColumnRightOffset,
2240
+ isColumnPinned,
2241
+ getColumnPinSide,
2242
+ getPinnedZIndex,
2243
+ measureRef
2244
+ };
2245
+ }
1634
2246
 
1635
2247
  // src/components/RowIndexColumnHeader.tsx
1636
2248
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1637
- var cellPaddingCompact3 = "px-1 py-0.5";
1638
- var cellPaddingNormal3 = "px-2 py-1.5";
2249
+ var cellPaddingCompact3 = "px-1.5 py-1";
2250
+ var cellPaddingNormal3 = "px-2 py-2";
1639
2251
  function RowIndexColumnHeader({
1640
2252
  enableHighlighting = false,
1641
2253
  highlightColor,
@@ -1652,18 +2264,16 @@ function RowIndexColumnHeader({
1652
2264
  "th",
1653
2265
  {
1654
2266
  className: cn(
1655
- "border border-gray-200 text-center font-bold text-gray-700",
1656
- compactMode ? "text-[10px]" : "text-xs",
2267
+ "border border-gray-200 text-center font-bold text-gray-700 sticky",
2268
+ compactMode ? "text-xs" : "text-sm",
1657
2269
  cellPadding,
1658
- isPinned ? "z-30" : "z-20",
1659
2270
  className
1660
2271
  ),
1661
2272
  style: {
1662
2273
  minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
1663
2274
  width: `${ROW_INDEX_COLUMN_WIDTH}px`,
1664
- // Only sticky horizontally if pinned, NOT vertically
1665
- position: isPinned ? "sticky" : void 0,
1666
- left: isPinned ? 0 : void 0,
2275
+ left: 0,
2276
+ zIndex: 50,
1667
2277
  backgroundColor: highlightColor || "rgb(243 244 246)"
1668
2278
  }
1669
2279
  }
@@ -1673,18 +2283,16 @@ function RowIndexColumnHeader({
1673
2283
  "th",
1674
2284
  {
1675
2285
  className: cn(
1676
- "border border-gray-200 text-center font-bold text-gray-700 group",
1677
- compactMode ? "text-[10px]" : "text-xs",
2286
+ "border border-gray-200 text-center font-bold text-gray-700 group sticky",
2287
+ compactMode ? "text-xs" : "text-sm",
1678
2288
  cellPadding,
1679
- isPinned ? "z-30" : "z-20",
1680
2289
  className
1681
2290
  ),
1682
2291
  style: {
1683
2292
  minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
1684
2293
  width: `${ROW_INDEX_COLUMN_WIDTH}px`,
1685
- position: "sticky",
1686
- top: 0,
1687
- left: isPinned ? 0 : void 0,
2294
+ left: 0,
2295
+ zIndex: 50,
1688
2296
  backgroundColor: highlightColor || "rgb(243 244 246)"
1689
2297
  },
1690
2298
  children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-center gap-1", children: [
@@ -1709,231 +2317,104 @@ function RowIndexColumnHeader({
1709
2317
  }
1710
2318
  RowIndexColumnHeader.displayName = "RowIndexColumnHeader";
1711
2319
 
1712
- // src/hooks/useSpreadsheetHighlighting.ts
1713
- import { useCallback as useCallback2, useState as useState4 } from "react";
1714
- var HIGHLIGHT_COLORS = {
1715
- // Darker colors for rows (more visible)
1716
- row: [
1717
- "#fef08a",
1718
- // yellow
1719
- "#bbf7d0",
1720
- // green
1721
- "#bfdbfe",
1722
- // blue
1723
- "#fecaca",
1724
- // red
1725
- "#e9d5ff",
1726
- // purple
1727
- "#fed7aa",
1728
- // orange
1729
- "#a5f3fc",
1730
- // cyan
1731
- "#fce7f3",
1732
- // pink
1733
- "#d1d5db"
1734
- // gray
1735
- ],
1736
- // Lighter colors for columns/headers (subtle background)
1737
- column: [
1738
- "#fef9c3",
1739
- // yellow-100
1740
- "#dcfce7",
1741
- // green-100
1742
- "#dbeafe",
1743
- // blue-100
1744
- "#fee2e2",
1745
- // red-100
1746
- "#f3e8ff",
1747
- // purple-100
1748
- "#ffedd5",
1749
- // orange-100
1750
- "#cffafe",
1751
- // cyan-100
1752
- "#fce7f3",
1753
- // pink-100
1754
- "#e5e7eb"
1755
- // gray-200
1756
- ]
1757
- };
1758
- function useSpreadsheetHighlighting({
1759
- externalRowHighlights,
1760
- onRowHighlight,
1761
- externalColumnHighlights,
1762
- onColumnHighlight,
1763
- externalCellHighlights,
1764
- onCellHighlight
1765
- } = {}) {
1766
- const [cellHighlightsInternal, setCellHighlightsInternal] = useState4([]);
1767
- const [rowHighlightsInternal, setRowHighlightsInternal] = useState4([]);
1768
- const [columnHighlightsInternal, setColumnHighlightsInternal] = useState4({});
1769
- const [highlightPickerRow, setHighlightPickerRow] = useState4(null);
1770
- const [highlightPickerColumn, setHighlightPickerColumn] = useState4(null);
1771
- const [highlightPickerCell, setHighlightPickerCell] = useState4(null);
1772
- const cellHighlights = externalCellHighlights || cellHighlightsInternal;
1773
- const rowHighlights = externalRowHighlights || rowHighlightsInternal;
1774
- const columnHighlights = externalColumnHighlights || columnHighlightsInternal;
1775
- const getCellHighlight = useCallback2(
1776
- (rowId, columnId) => {
1777
- return cellHighlights.find((h) => h.rowId === rowId && h.columnId === columnId)?.color;
1778
- },
1779
- [cellHighlights]
1780
- );
1781
- const handleCellHighlightToggle = useCallback2(
1782
- (rowId, columnId, color = "#fef08a") => {
1783
- if (onCellHighlight) {
1784
- onCellHighlight(rowId, columnId, color);
1785
- } else {
1786
- setCellHighlightsInternal((prev) => {
1787
- const existing = prev.find((h) => h.rowId === rowId && h.columnId === columnId);
1788
- if (existing) {
1789
- if (color === null) {
1790
- return prev.filter(
1791
- (h) => !(h.rowId === rowId && h.columnId === columnId)
1792
- );
1793
- }
1794
- return prev.map(
1795
- (h) => h.rowId === rowId && h.columnId === columnId ? { ...h, color } : h
1796
- );
1797
- }
1798
- if (color) {
1799
- return [...prev, { rowId, columnId, color }];
1800
- }
1801
- return prev;
1802
- });
1803
- }
1804
- setHighlightPickerCell(null);
1805
- },
1806
- [onCellHighlight]
1807
- );
1808
- const getRowHighlight = useCallback2(
1809
- (rowId) => {
1810
- return rowHighlights.find((h) => h.rowId === rowId && !h.columnId);
1811
- },
1812
- [rowHighlights]
1813
- );
1814
- const handleRowHighlightToggle = useCallback2(
1815
- (rowId, color) => {
1816
- if (onRowHighlight) {
1817
- onRowHighlight(rowId, color);
1818
- } else {
1819
- setRowHighlightsInternal((prev) => {
1820
- const existing = prev.find((h) => h.rowId === rowId && !h.columnId);
1821
- if (existing) {
1822
- if (color === null) {
1823
- return prev.filter((h) => !(h.rowId === rowId && !h.columnId));
1824
- }
1825
- return prev.map(
1826
- (h) => h.rowId === rowId && !h.columnId ? { ...h, color } : h
1827
- );
1828
- }
1829
- if (color) {
1830
- return [...prev, { rowId, color }];
1831
- }
1832
- return prev;
1833
- });
1834
- }
1835
- setHighlightPickerRow(null);
1836
- },
1837
- [onRowHighlight]
1838
- );
1839
- const getColumnHighlight = useCallback2(
1840
- (columnId) => {
1841
- return columnHighlights[columnId];
1842
- },
1843
- [columnHighlights]
1844
- );
1845
- const handleColumnHighlightToggle = useCallback2(
1846
- (columnId, color) => {
1847
- if (onColumnHighlight) {
1848
- onColumnHighlight(columnId, color);
1849
- } else {
1850
- setColumnHighlightsInternal((prev) => {
1851
- const newHighlights = { ...prev };
1852
- if (color === null) {
1853
- delete newHighlights[columnId];
1854
- } else {
1855
- newHighlights[columnId] = color;
1856
- }
1857
- return newHighlights;
1858
- });
1859
- }
1860
- setHighlightPickerColumn(null);
1861
- },
1862
- [onColumnHighlight]
1863
- );
1864
- const clearAllHighlights = useCallback2(() => {
1865
- setCellHighlightsInternal([]);
1866
- setRowHighlightsInternal([]);
1867
- setColumnHighlightsInternal({});
1868
- }, []);
1869
- return {
1870
- // Cell highlights
1871
- cellHighlights,
1872
- getCellHighlight,
1873
- handleCellHighlightToggle,
1874
- // Row highlights
1875
- rowHighlights,
1876
- getRowHighlight,
1877
- handleRowHighlightToggle,
1878
- // Column highlights
1879
- columnHighlights,
1880
- getColumnHighlight,
1881
- handleColumnHighlightToggle,
1882
- // Picker state
1883
- highlightPickerRow,
1884
- setHighlightPickerRow,
1885
- highlightPickerColumn,
1886
- setHighlightPickerColumn,
1887
- highlightPickerCell,
1888
- setHighlightPickerCell,
1889
- // Utility
1890
- clearAllHighlights
1891
- };
1892
- }
1893
-
1894
2320
  // src/components/ColorPickerPopover.tsx
2321
+ import { useState as useState5, useRef as useRef6 } from "react";
1895
2322
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1896
2323
  function ColorPickerPopover({
1897
2324
  title,
1898
- paletteType = "column",
1899
- colors,
2325
+ recentColors = [],
1900
2326
  onSelectColor,
1901
2327
  onClose,
1902
2328
  className
1903
2329
  }) {
1904
- const colorPalette = colors ?? [...HIGHLIGHT_COLORS[paletteType], null];
1905
- return /* @__PURE__ */ jsx8("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsxs8("div", { className: cn("bg-white rounded-lg shadow-xl p-4 w-64", className), children: [
1906
- /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold mb-3", children: title }),
1907
- /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-5 gap-2", children: colorPalette.map((color) => /* @__PURE__ */ jsx8(
1908
- "button",
1909
- {
1910
- type: "button",
1911
- onClick: () => onSelectColor(color),
1912
- className: cn(
1913
- "w-8 h-8 rounded border-2 transition-transform hover:scale-110",
1914
- color ? "border-gray-300" : "border-gray-300 bg-white flex items-center justify-center text-gray-400 text-xs"
1915
- ),
1916
- style: color ? { backgroundColor: color } : void 0,
1917
- title: color || "Clear highlight",
1918
- children: !color && "\u2715"
1919
- },
1920
- color || "clear"
1921
- )) }),
1922
- /* @__PURE__ */ jsx8("div", { className: "flex justify-end mt-4", children: /* @__PURE__ */ jsx8(
1923
- "button",
1924
- {
1925
- type: "button",
1926
- onClick: onClose,
1927
- className: "px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 rounded transition-colors",
1928
- children: "Cancel"
1929
- }
1930
- ) })
1931
- ] }) });
2330
+ const [selectedColor, setSelectedColor] = useState5("#fef08a");
2331
+ const colorInputRef = useRef6(null);
2332
+ const handleConfirm = () => {
2333
+ onSelectColor(selectedColor);
2334
+ };
2335
+ return /* @__PURE__ */ jsx8("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick: onClose, children: /* @__PURE__ */ jsxs8(
2336
+ "div",
2337
+ {
2338
+ className: cn("bg-white rounded-lg shadow-xl p-4 w-72", className),
2339
+ onClick: (e) => e.stopPropagation(),
2340
+ children: [
2341
+ /* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold mb-3", children: title }),
2342
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-3 mb-3", children: [
2343
+ /* @__PURE__ */ jsx8(
2344
+ "input",
2345
+ {
2346
+ ref: colorInputRef,
2347
+ type: "color",
2348
+ value: selectedColor,
2349
+ onChange: (e) => setSelectedColor(e.target.value),
2350
+ className: "w-10 h-10 rounded border border-gray-200 cursor-pointer p-0.5"
2351
+ }
2352
+ ),
2353
+ /* @__PURE__ */ jsxs8("div", { className: "flex-1", children: [
2354
+ /* @__PURE__ */ jsx8(
2355
+ "div",
2356
+ {
2357
+ className: "w-full h-8 rounded border border-gray-200",
2358
+ style: { backgroundColor: selectedColor }
2359
+ }
2360
+ ),
2361
+ /* @__PURE__ */ jsx8("span", { className: "text-xs text-gray-500 mt-1 block", children: selectedColor })
2362
+ ] })
2363
+ ] }),
2364
+ recentColors.length > 0 && /* @__PURE__ */ jsxs8("div", { className: "mb-3", children: [
2365
+ /* @__PURE__ */ jsx8("span", { className: "text-xs text-gray-500 mb-1.5 block", children: "Recent" }),
2366
+ /* @__PURE__ */ jsx8("div", { className: "flex gap-1.5", children: recentColors.map((color) => /* @__PURE__ */ jsx8(
2367
+ "button",
2368
+ {
2369
+ type: "button",
2370
+ onClick: () => setSelectedColor(color),
2371
+ className: cn(
2372
+ "w-7 h-7 rounded border-2 transition-transform hover:scale-110",
2373
+ selectedColor === color ? "border-blue-500" : "border-gray-200 hover:border-gray-400"
2374
+ ),
2375
+ style: { backgroundColor: color },
2376
+ title: color
2377
+ },
2378
+ color
2379
+ )) })
2380
+ ] }),
2381
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 pt-2 border-t border-gray-100", children: [
2382
+ /* @__PURE__ */ jsx8(
2383
+ "button",
2384
+ {
2385
+ type: "button",
2386
+ onClick: handleConfirm,
2387
+ className: "flex-1 px-2.5 py-1.5 text-xs text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors font-medium",
2388
+ children: "Apply"
2389
+ }
2390
+ ),
2391
+ /* @__PURE__ */ jsx8(
2392
+ "button",
2393
+ {
2394
+ type: "button",
2395
+ onClick: () => onSelectColor(null),
2396
+ className: "px-2.5 py-1.5 text-xs text-red-600 hover:bg-red-50 rounded border border-red-200 transition-colors",
2397
+ children: "Clear"
2398
+ }
2399
+ ),
2400
+ /* @__PURE__ */ jsx8(
2401
+ "button",
2402
+ {
2403
+ type: "button",
2404
+ onClick: onClose,
2405
+ className: "px-2.5 py-1.5 text-xs text-gray-600 hover:bg-gray-100 rounded transition-colors",
2406
+ children: "Cancel"
2407
+ }
2408
+ )
2409
+ ] })
2410
+ ]
2411
+ }
2412
+ ) });
1932
2413
  }
1933
2414
  ColorPickerPopover.displayName = "ColorPickerPopover";
1934
2415
 
1935
2416
  // src/components/SpreadsheetSettingsModal.tsx
1936
- import { useState as useState5, useEffect as useEffect3 } from "react";
2417
+ import { useState as useState6, useEffect as useEffect5 } from "react";
1937
2418
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1938
2419
  var DEFAULT_SETTINGS = {
1939
2420
  defaultPinnedColumns: [],
@@ -1952,9 +2433,9 @@ var SpreadsheetSettingsModal = ({
1952
2433
  title = "Spreadsheet Settings",
1953
2434
  pageSizeOptions = [25, 50, 100, 200]
1954
2435
  }) => {
1955
- const [activeTab, setActiveTab] = useState5("columns");
1956
- const [localSettings, setLocalSettings] = useState5(settings);
1957
- useEffect3(() => {
2436
+ const [activeTab, setActiveTab] = useState6("columns");
2437
+ const [localSettings, setLocalSettings] = useState6(settings);
2438
+ useEffect5(() => {
1958
2439
  setLocalSettings(settings);
1959
2440
  }, [settings]);
1960
2441
  if (!isOpen) return null;
@@ -2262,11 +2743,11 @@ var SpreadsheetSettingsModal = ({
2262
2743
  SpreadsheetSettingsModal.displayName = "SpreadsheetSettingsModal";
2263
2744
 
2264
2745
  // src/components/CommentModals.tsx
2265
- import { useState as useState6, useEffect as useEffect4 } from "react";
2746
+ import { useState as useState7, useEffect as useEffect6 } from "react";
2266
2747
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2267
2748
  function AddCommentModal({ isOpen, columnLabel, onAdd, onClose }) {
2268
- const [commentText, setCommentText] = useState6("");
2269
- useEffect4(() => {
2749
+ const [commentText, setCommentText] = useState7("");
2750
+ useEffect6(() => {
2270
2751
  if (!isOpen) {
2271
2752
  setCommentText("");
2272
2753
  }
@@ -2436,7 +2917,7 @@ function DeleteConfirmationModal({
2436
2917
  DeleteConfirmationModal.displayName = "DeleteConfirmationModal";
2437
2918
 
2438
2919
  // src/components/KeyboardShortcutsModal.tsx
2439
- import React4 from "react";
2920
+ import React5 from "react";
2440
2921
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2441
2922
  function KeyboardShortcutsModal({
2442
2923
  isOpen,
@@ -2478,7 +2959,7 @@ function ShortcutSection({ title, children }) {
2478
2959
  function ShortcutRow({ label, keys }) {
2479
2960
  return /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between", children: [
2480
2961
  /* @__PURE__ */ jsx11("span", { className: "text-gray-600 text-sm", children: label }),
2481
- /* @__PURE__ */ jsx11("div", { className: "flex items-center gap-1", children: keys.map((key, index) => /* @__PURE__ */ jsxs11(React4.Fragment, { children: [
2962
+ /* @__PURE__ */ jsx11("div", { className: "flex items-center gap-1", children: keys.map((key, index) => /* @__PURE__ */ jsxs11(React5.Fragment, { children: [
2482
2963
  index > 0 && /* @__PURE__ */ jsx11("span", { className: "text-gray-400", children: "+" }),
2483
2964
  key.includes("Click") ? /* @__PURE__ */ jsx11("span", { className: "text-gray-500 text-xs", children: key }) : /* @__PURE__ */ jsx11("kbd", { className: "px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs border border-gray-200", children: key })
2484
2965
  ] }, index)) })
@@ -2553,7 +3034,7 @@ RowContextMenu.displayName = "RowContextMenu";
2553
3034
  import { Pagination } from "@xcelsior/design-system";
2554
3035
 
2555
3036
  // src/hooks/useSpreadsheetFiltering.ts
2556
- import { useCallback as useCallback3, useMemo as useMemo3, useState as useState7 } from "react";
3037
+ import { useCallback as useCallback4, useMemo as useMemo3, useState as useState8 } from "react";
2557
3038
  import { FilterChain, LazyArray } from "@xcelsior/utils";
2558
3039
  function useSpreadsheetFiltering({
2559
3040
  data,
@@ -2563,18 +3044,20 @@ function useSpreadsheetFiltering({
2563
3044
  serverSide = false,
2564
3045
  controlledFilters,
2565
3046
  controlledSortConfig,
2566
- defaultSortConfig
3047
+ defaultSortConfig,
3048
+ duplicateRowIds,
3049
+ getRowId
2567
3050
  }) {
2568
- const [internalFilters, setInternalFilters] = useState7(
3051
+ const [internalFilters, setInternalFilters] = useState8(
2569
3052
  {}
2570
3053
  );
2571
- const [internalSortConfig, setInternalSortConfig] = useState7(
3054
+ const [internalSortConfig, setInternalSortConfig] = useState8(
2572
3055
  defaultSortConfig ?? null
2573
3056
  );
2574
- const [activeFilterColumn, setActiveFilterColumn] = useState7(null);
3057
+ const [activeFilterColumn, setActiveFilterColumn] = useState8(null);
2575
3058
  const filters = controlledFilters ?? internalFilters;
2576
3059
  const sortConfig = controlledSortConfig !== void 0 ? controlledSortConfig : internalSortConfig;
2577
- const applyTextCondition = useCallback3(
3060
+ const applyTextCondition = useCallback4(
2578
3061
  (value, condition) => {
2579
3062
  const strValue = String(value ?? "").toLowerCase();
2580
3063
  const filterValue = (condition.value ?? "").toLowerCase();
@@ -2601,7 +3084,7 @@ function useSpreadsheetFiltering({
2601
3084
  },
2602
3085
  []
2603
3086
  );
2604
- const applyNumberCondition = useCallback3(
3087
+ const applyNumberCondition = useCallback4(
2605
3088
  (value, condition) => {
2606
3089
  if (condition.operator === "isEmpty") return isBlankValue(value);
2607
3090
  if (condition.operator === "isNotEmpty") return !isBlankValue(value);
@@ -2630,7 +3113,7 @@ function useSpreadsheetFiltering({
2630
3113
  },
2631
3114
  []
2632
3115
  );
2633
- const applyDateCondition = useCallback3(
3116
+ const applyDateCondition = useCallback4(
2634
3117
  (value, condition) => {
2635
3118
  if (condition.operator === "isEmpty") return isBlankValue(value);
2636
3119
  if (condition.operator === "isNotEmpty") return !isBlankValue(value);
@@ -2702,7 +3185,7 @@ function useSpreadsheetFiltering({
2702
3185
  },
2703
3186
  []
2704
3187
  );
2705
- const buildFilterPredicate = useCallback3(
3188
+ const buildFilterPredicate = useCallback4(
2706
3189
  (column, filter) => {
2707
3190
  return (row) => {
2708
3191
  const value = column.getValue ? column.getValue(row) : row[column.id];
@@ -2736,7 +3219,7 @@ function useSpreadsheetFiltering({
2736
3219
  },
2737
3220
  [applyTextCondition, applyNumberCondition, applyDateCondition]
2738
3221
  );
2739
- const buildSortComparator = useCallback3(
3222
+ const buildSortComparator = useCallback4(
2740
3223
  (column, direction) => {
2741
3224
  return (a, b) => {
2742
3225
  const aValue = column?.getValue ? column.getValue(a) : a[sortConfig?.columnId];
@@ -2764,6 +3247,12 @@ function useSpreadsheetFiltering({
2764
3247
  const column = columns.find((c) => c.id === columnId);
2765
3248
  if (!column) continue;
2766
3249
  filterChain.add(buildFilterPredicate(column, filter));
3250
+ if (filter.showDuplicatesOnly && duplicateRowIds && getRowId) {
3251
+ const dupSet = duplicateRowIds.get(columnId);
3252
+ if (dupSet) {
3253
+ filterChain.add((row) => dupSet.has(getRowId(row)));
3254
+ }
3255
+ }
2767
3256
  }
2768
3257
  if (!filterChain.isEmpty) {
2769
3258
  lazyResult = filterChain.applyTo(lazyResult);
@@ -2773,8 +3262,8 @@ function useSpreadsheetFiltering({
2773
3262
  lazyResult = lazyResult.sort(buildSortComparator(column, sortConfig.direction));
2774
3263
  }
2775
3264
  return lazyResult;
2776
- }, [data, filters, sortConfig, columns, serverSide, buildFilterPredicate, buildSortComparator]);
2777
- const handleFilterChange = useCallback3(
3265
+ }, [data, filters, sortConfig, columns, serverSide, buildFilterPredicate, buildSortComparator, duplicateRowIds, getRowId]);
3266
+ const handleFilterChange = useCallback4(
2778
3267
  (columnId, filter) => {
2779
3268
  const newFilters = { ...filters };
2780
3269
  if (filter) {
@@ -2789,7 +3278,7 @@ function useSpreadsheetFiltering({
2789
3278
  },
2790
3279
  [filters, onFilterChange, controlledFilters]
2791
3280
  );
2792
- const handleSort = useCallback3(
3281
+ const handleSort = useCallback4(
2793
3282
  (columnId) => {
2794
3283
  let newSortConfig;
2795
3284
  if (sortConfig?.columnId === columnId) {
@@ -2808,13 +3297,13 @@ function useSpreadsheetFiltering({
2808
3297
  },
2809
3298
  [sortConfig, onSortChange, controlledSortConfig]
2810
3299
  );
2811
- const clearSort = useCallback3(() => {
3300
+ const clearSort = useCallback4(() => {
2812
3301
  if (controlledSortConfig === void 0) {
2813
3302
  setInternalSortConfig(null);
2814
3303
  }
2815
3304
  onSortChange?.(null);
2816
3305
  }, [onSortChange, controlledSortConfig]);
2817
- const setSortConfig = useCallback3(
3306
+ const setSortConfig = useCallback4(
2818
3307
  (config) => {
2819
3308
  if (controlledSortConfig === void 0) {
2820
3309
  setInternalSortConfig(config);
@@ -2823,7 +3312,7 @@ function useSpreadsheetFiltering({
2823
3312
  },
2824
3313
  [onSortChange, controlledSortConfig]
2825
3314
  );
2826
- const clearAllFilters = useCallback3(() => {
3315
+ const clearAllFilters = useCallback4(() => {
2827
3316
  if (controlledFilters === void 0) {
2828
3317
  setInternalFilters({});
2829
3318
  }
@@ -2845,24 +3334,274 @@ function useSpreadsheetFiltering({
2845
3334
  };
2846
3335
  }
2847
3336
 
3337
+ // src/hooks/useSpreadsheetDuplicates.ts
3338
+ import { useCallback as useCallback5, useMemo as useMemo4, useState as useState9 } from "react";
3339
+ function normalizeValue(value) {
3340
+ if (value === null || value === void 0 || value === "") {
3341
+ return "__blank__";
3342
+ }
3343
+ if (typeof value === "number") {
3344
+ return String(value);
3345
+ }
3346
+ return String(value).trim().toLowerCase();
3347
+ }
3348
+ function useSpreadsheetDuplicates({
3349
+ data,
3350
+ columns,
3351
+ duplicateCheckColumns,
3352
+ getRowId
3353
+ }) {
3354
+ const [localDuplicateCheckColumns, setLocalDuplicateCheckColumns] = useState9(
3355
+ () => new Set(duplicateCheckColumns)
3356
+ );
3357
+ const duplicateCheckColumnsSet = useMemo4(() => {
3358
+ return new Set(duplicateCheckColumns);
3359
+ }, [duplicateCheckColumns]);
3360
+ const { duplicateRowIds, valueCounts } = useMemo4(() => {
3361
+ const duplicateRowIds2 = /* @__PURE__ */ new Map();
3362
+ const valueCounts2 = /* @__PURE__ */ new Map();
3363
+ const activeColumns = duplicateCheckColumnsSet.size > 0 ? duplicateCheckColumnsSet : localDuplicateCheckColumns;
3364
+ for (const columnId of activeColumns) {
3365
+ const column = columns.find((c) => c.id === columnId);
3366
+ if (!column) continue;
3367
+ const valueToRowIds = /* @__PURE__ */ new Map();
3368
+ for (const row of data) {
3369
+ const rawValue = column.getValue ? column.getValue(row) : row[columnId];
3370
+ if (rawValue === null || rawValue === void 0 || rawValue === "") continue;
3371
+ const normalized = normalizeValue(rawValue);
3372
+ const rowId = getRowId(row);
3373
+ const existing = valueToRowIds.get(normalized);
3374
+ if (existing) {
3375
+ existing.push(rowId);
3376
+ } else {
3377
+ valueToRowIds.set(normalized, [rowId]);
3378
+ }
3379
+ }
3380
+ const dupSet = /* @__PURE__ */ new Set();
3381
+ const countMap = /* @__PURE__ */ new Map();
3382
+ for (const [normalizedVal, rowIds] of valueToRowIds) {
3383
+ countMap.set(normalizedVal, rowIds.length);
3384
+ if (rowIds.length >= 2) {
3385
+ for (const rowId of rowIds) {
3386
+ dupSet.add(rowId);
3387
+ }
3388
+ }
3389
+ }
3390
+ duplicateRowIds2.set(columnId, dupSet);
3391
+ valueCounts2.set(columnId, countMap);
3392
+ }
3393
+ return { duplicateRowIds: duplicateRowIds2, valueCounts: valueCounts2 };
3394
+ }, [data, columns, duplicateCheckColumnsSet, localDuplicateCheckColumns, getRowId]);
3395
+ const isCellDuplicate = useCallback5(
3396
+ (rowId, columnId) => {
3397
+ return duplicateRowIds.get(columnId)?.has(rowId) ?? false;
3398
+ },
3399
+ [duplicateRowIds]
3400
+ );
3401
+ const getDuplicateCount = useCallback5(
3402
+ (columnId, value) => {
3403
+ const normalized = normalizeValue(value);
3404
+ return valueCounts.get(columnId)?.get(normalized) ?? 0;
3405
+ },
3406
+ [valueCounts]
3407
+ );
3408
+ const getDuplicateColumnCount = useCallback5(
3409
+ (columnId) => {
3410
+ return duplicateRowIds.get(columnId)?.size ?? 0;
3411
+ },
3412
+ [duplicateRowIds]
3413
+ );
3414
+ const toggleDuplicateCheck = useCallback5((columnId) => {
3415
+ setLocalDuplicateCheckColumns((prev) => {
3416
+ const next = new Set(prev);
3417
+ if (next.has(columnId)) {
3418
+ next.delete(columnId);
3419
+ } else {
3420
+ next.add(columnId);
3421
+ }
3422
+ return next;
3423
+ });
3424
+ }, []);
3425
+ const effectiveDuplicateCheckColumns = duplicateCheckColumnsSet.size > 0 ? duplicateCheckColumnsSet : localDuplicateCheckColumns;
3426
+ return {
3427
+ isCellDuplicate,
3428
+ getDuplicateCount,
3429
+ getDuplicateColumnCount,
3430
+ duplicateRowIds,
3431
+ duplicateCheckColumns: effectiveDuplicateCheckColumns,
3432
+ toggleDuplicateCheck
3433
+ };
3434
+ }
3435
+
3436
+ // src/hooks/useSpreadsheetHighlighting.ts
3437
+ import { useCallback as useCallback6, useState as useState10 } from "react";
3438
+ function useSpreadsheetHighlighting({
3439
+ externalRowHighlights,
3440
+ onRowHighlight,
3441
+ externalColumnHighlights,
3442
+ onColumnHighlight,
3443
+ externalCellHighlights,
3444
+ onCellHighlight
3445
+ } = {}) {
3446
+ const [cellHighlightsInternal, setCellHighlightsInternal] = useState10([]);
3447
+ const [rowHighlightsInternal, setRowHighlightsInternal] = useState10([]);
3448
+ const [columnHighlightsInternal, setColumnHighlightsInternal] = useState10({});
3449
+ const [recentColors, setRecentColors] = useState10([]);
3450
+ const addRecentColor = useCallback6((color) => {
3451
+ if (!color) return;
3452
+ setRecentColors((prev) => {
3453
+ const filtered = prev.filter((c) => c !== color);
3454
+ return [color, ...filtered].slice(0, 8);
3455
+ });
3456
+ }, []);
3457
+ const [highlightPickerRow, setHighlightPickerRow] = useState10(null);
3458
+ const [highlightPickerColumn, setHighlightPickerColumn] = useState10(null);
3459
+ const [highlightPickerCell, setHighlightPickerCell] = useState10(null);
3460
+ const cellHighlights = externalCellHighlights || cellHighlightsInternal;
3461
+ const rowHighlights = externalRowHighlights || rowHighlightsInternal;
3462
+ const columnHighlights = externalColumnHighlights || columnHighlightsInternal;
3463
+ const getCellHighlight = useCallback6(
3464
+ (rowId, columnId) => {
3465
+ return cellHighlights.find((h) => h.rowId === rowId && h.columnId === columnId)?.color;
3466
+ },
3467
+ [cellHighlights]
3468
+ );
3469
+ const handleCellHighlightToggle = useCallback6(
3470
+ (rowId, columnId, color = "#fef08a") => {
3471
+ if (onCellHighlight) {
3472
+ onCellHighlight(rowId, columnId, color);
3473
+ } else {
3474
+ setCellHighlightsInternal((prev) => {
3475
+ const existing = prev.find((h) => h.rowId === rowId && h.columnId === columnId);
3476
+ if (existing) {
3477
+ if (color === null) {
3478
+ return prev.filter(
3479
+ (h) => !(h.rowId === rowId && h.columnId === columnId)
3480
+ );
3481
+ }
3482
+ return prev.map(
3483
+ (h) => h.rowId === rowId && h.columnId === columnId ? { ...h, color } : h
3484
+ );
3485
+ }
3486
+ if (color) {
3487
+ return [...prev, { rowId, columnId, color }];
3488
+ }
3489
+ return prev;
3490
+ });
3491
+ }
3492
+ addRecentColor(color);
3493
+ setHighlightPickerCell(null);
3494
+ },
3495
+ [onCellHighlight, addRecentColor]
3496
+ );
3497
+ const getRowHighlight = useCallback6(
3498
+ (rowId) => {
3499
+ return rowHighlights.find((h) => h.rowId === rowId && !h.columnId);
3500
+ },
3501
+ [rowHighlights]
3502
+ );
3503
+ const handleRowHighlightToggle = useCallback6(
3504
+ (rowId, color) => {
3505
+ if (onRowHighlight) {
3506
+ onRowHighlight(rowId, color);
3507
+ } else {
3508
+ setRowHighlightsInternal((prev) => {
3509
+ const existing = prev.find((h) => h.rowId === rowId && !h.columnId);
3510
+ if (existing) {
3511
+ if (color === null) {
3512
+ return prev.filter((h) => !(h.rowId === rowId && !h.columnId));
3513
+ }
3514
+ return prev.map(
3515
+ (h) => h.rowId === rowId && !h.columnId ? { ...h, color } : h
3516
+ );
3517
+ }
3518
+ if (color) {
3519
+ return [...prev, { rowId, color }];
3520
+ }
3521
+ return prev;
3522
+ });
3523
+ }
3524
+ addRecentColor(color);
3525
+ setHighlightPickerRow(null);
3526
+ },
3527
+ [onRowHighlight, addRecentColor]
3528
+ );
3529
+ const getColumnHighlight = useCallback6(
3530
+ (columnId) => {
3531
+ return columnHighlights[columnId];
3532
+ },
3533
+ [columnHighlights]
3534
+ );
3535
+ const handleColumnHighlightToggle = useCallback6(
3536
+ (columnId, color) => {
3537
+ if (onColumnHighlight) {
3538
+ onColumnHighlight(columnId, color);
3539
+ } else {
3540
+ setColumnHighlightsInternal((prev) => {
3541
+ const newHighlights = { ...prev };
3542
+ if (color === null) {
3543
+ delete newHighlights[columnId];
3544
+ } else {
3545
+ newHighlights[columnId] = color;
3546
+ }
3547
+ return newHighlights;
3548
+ });
3549
+ }
3550
+ addRecentColor(color);
3551
+ setHighlightPickerColumn(null);
3552
+ },
3553
+ [onColumnHighlight, addRecentColor]
3554
+ );
3555
+ const clearAllHighlights = useCallback6(() => {
3556
+ setCellHighlightsInternal([]);
3557
+ setRowHighlightsInternal([]);
3558
+ setColumnHighlightsInternal({});
3559
+ }, []);
3560
+ return {
3561
+ // Cell highlights
3562
+ cellHighlights,
3563
+ getCellHighlight,
3564
+ handleCellHighlightToggle,
3565
+ // Row highlights
3566
+ rowHighlights,
3567
+ getRowHighlight,
3568
+ handleRowHighlightToggle,
3569
+ // Column highlights
3570
+ columnHighlights,
3571
+ getColumnHighlight,
3572
+ handleColumnHighlightToggle,
3573
+ // Recent colors
3574
+ recentColors,
3575
+ // Picker state
3576
+ highlightPickerRow,
3577
+ setHighlightPickerRow,
3578
+ highlightPickerColumn,
3579
+ setHighlightPickerColumn,
3580
+ highlightPickerCell,
3581
+ setHighlightPickerCell,
3582
+ // Utility
3583
+ clearAllHighlights
3584
+ };
3585
+ }
3586
+
2848
3587
  // src/hooks/useSpreadsheetComments.ts
2849
- import { useCallback as useCallback4, useState as useState8 } from "react";
3588
+ import { useCallback as useCallback7, useState as useState11 } from "react";
2850
3589
  function useSpreadsheetComments({
2851
3590
  externalCellComments,
2852
3591
  onAddCellComment,
2853
3592
  onToggleCommentResolved
2854
3593
  } = {}) {
2855
- const [cellCommentsInternal, setCellCommentsInternal] = useState8([]);
2856
- const [commentModalCell, setCommentModalCell] = useState8(null);
2857
- const [viewCommentsCell, setViewCommentsCell] = useState8(null);
3594
+ const [cellCommentsInternal, setCellCommentsInternal] = useState11([]);
3595
+ const [commentModalCell, setCommentModalCell] = useState11(null);
3596
+ const [viewCommentsCell, setViewCommentsCell] = useState11(null);
2858
3597
  const cellComments = externalCellComments || cellCommentsInternal;
2859
- const getCellComments = useCallback4(
3598
+ const getCellComments = useCallback7(
2860
3599
  (rowId, columnId) => {
2861
3600
  return cellComments.filter((c) => c.rowId === rowId && c.columnId === columnId);
2862
3601
  },
2863
3602
  [cellComments]
2864
3603
  );
2865
- const getCellUnresolvedCommentCount = useCallback4(
3604
+ const getCellUnresolvedCommentCount = useCallback7(
2866
3605
  (rowId, columnId) => {
2867
3606
  return cellComments.filter(
2868
3607
  (c) => c.rowId === rowId && c.columnId === columnId && !c.resolved
@@ -2870,13 +3609,13 @@ function useSpreadsheetComments({
2870
3609
  },
2871
3610
  [cellComments]
2872
3611
  );
2873
- const cellHasComments = useCallback4(
3612
+ const cellHasComments = useCallback7(
2874
3613
  (rowId, columnId) => {
2875
3614
  return cellComments.some((c) => c.rowId === rowId && c.columnId === columnId);
2876
3615
  },
2877
3616
  [cellComments]
2878
3617
  );
2879
- const handleAddCellComment = useCallback4(
3618
+ const handleAddCellComment = useCallback7(
2880
3619
  (rowId, columnId, text) => {
2881
3620
  if (!text.trim()) return;
2882
3621
  if (onAddCellComment) {
@@ -2898,7 +3637,7 @@ function useSpreadsheetComments({
2898
3637
  },
2899
3638
  [onAddCellComment]
2900
3639
  );
2901
- const handleToggleCommentResolved = useCallback4(
3640
+ const handleToggleCommentResolved = useCallback7(
2902
3641
  (commentId) => {
2903
3642
  if (onToggleCommentResolved) {
2904
3643
  onToggleCommentResolved(commentId);
@@ -2930,20 +3669,20 @@ function useSpreadsheetComments({
2930
3669
  }
2931
3670
 
2932
3671
  // src/hooks/useSpreadsheetUndoRedo.ts
2933
- import { useCallback as useCallback5, useRef as useRef3, useState as useState9 } from "react";
3672
+ import { useCallback as useCallback8, useRef as useRef7, useState as useState12 } from "react";
2934
3673
  function useSpreadsheetUndoRedo({
2935
3674
  enabled = true,
2936
3675
  maxStackSize = 50,
2937
3676
  autoSave = true,
2938
3677
  onSave
2939
3678
  }) {
2940
- const [undoStack, setUndoStack] = useState9([]);
2941
- const [redoStack, setRedoStack] = useState9([]);
2942
- const [hasUnsavedChanges, setHasUnsavedChanges] = useState9(false);
2943
- const [saveStatus, setSaveStatus] = useState9("saved");
2944
- const onSaveRef = useRef3(onSave);
3679
+ const [undoStack, setUndoStack] = useState12([]);
3680
+ const [redoStack, setRedoStack] = useState12([]);
3681
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState12(false);
3682
+ const [saveStatus, setSaveStatus] = useState12("saved");
3683
+ const onSaveRef = useRef7(onSave);
2945
3684
  onSaveRef.current = onSave;
2946
- const pushToUndoStack = useCallback5(
3685
+ const pushToUndoStack = useCallback8(
2947
3686
  (snapshot) => {
2948
3687
  if (!enabled) return;
2949
3688
  setUndoStack((prev) => {
@@ -2957,7 +3696,7 @@ function useSpreadsheetUndoRedo({
2957
3696
  },
2958
3697
  [enabled, maxStackSize]
2959
3698
  );
2960
- const handleUndo = useCallback5(() => {
3699
+ const handleUndo = useCallback8(() => {
2961
3700
  if (!enabled || undoStack.length === 0) return null;
2962
3701
  const previousSnapshot = undoStack[undoStack.length - 1];
2963
3702
  setUndoStack((prev) => prev.slice(0, -1));
@@ -2970,7 +3709,7 @@ function useSpreadsheetUndoRedo({
2970
3709
  });
2971
3710
  return previousSnapshot;
2972
3711
  }, [enabled, undoStack, maxStackSize]);
2973
- const handleRedo = useCallback5(() => {
3712
+ const handleRedo = useCallback8(() => {
2974
3713
  if (!enabled || redoStack.length === 0) return null;
2975
3714
  const nextSnapshot = redoStack[redoStack.length - 1];
2976
3715
  setRedoStack((prev) => prev.slice(0, -1));
@@ -2983,7 +3722,7 @@ function useSpreadsheetUndoRedo({
2983
3722
  });
2984
3723
  return nextSnapshot;
2985
3724
  }, [enabled, redoStack, maxStackSize]);
2986
- const handleSave = useCallback5(async () => {
3725
+ const handleSave = useCallback8(async () => {
2987
3726
  if (!hasUnsavedChanges && !autoSave) return;
2988
3727
  setSaveStatus("saving");
2989
3728
  try {
@@ -2996,7 +3735,7 @@ function useSpreadsheetUndoRedo({
2996
3735
  setSaveStatus("error");
2997
3736
  }
2998
3737
  }, [hasUnsavedChanges, autoSave]);
2999
- const markAsChanged = useCallback5(() => {
3738
+ const markAsChanged = useCallback8(() => {
3000
3739
  setHasUnsavedChanges(true);
3001
3740
  if (autoSave) {
3002
3741
  setSaveStatus("saving");
@@ -3017,11 +3756,11 @@ function useSpreadsheetUndoRedo({
3017
3756
  setSaveStatus("unsaved");
3018
3757
  }
3019
3758
  }, [autoSave]);
3020
- const markAsSaved = useCallback5(() => {
3759
+ const markAsSaved = useCallback8(() => {
3021
3760
  setHasUnsavedChanges(false);
3022
3761
  setSaveStatus("saved");
3023
3762
  }, []);
3024
- const clearStacks = useCallback5(() => {
3763
+ const clearStacks = useCallback8(() => {
3025
3764
  setUndoStack([]);
3026
3765
  setRedoStack([]);
3027
3766
  }, []);
@@ -3049,7 +3788,7 @@ function useSpreadsheetUndoRedo({
3049
3788
  }
3050
3789
 
3051
3790
  // src/hooks/useSpreadsheetKeyboardShortcuts.ts
3052
- import { useEffect as useEffect5, useState as useState10 } from "react";
3791
+ import { useEffect as useEffect7, useState as useState13 } from "react";
3053
3792
  function useSpreadsheetKeyboardShortcuts({
3054
3793
  onUndo,
3055
3794
  onRedo,
@@ -3059,15 +3798,16 @@ function useSpreadsheetKeyboardShortcuts({
3059
3798
  onTabNavigation,
3060
3799
  onCopy,
3061
3800
  onPaste,
3801
+ onSelectAll,
3062
3802
  hasFocusedCell = false,
3063
3803
  isEditing = false,
3064
3804
  customShortcuts = [],
3065
3805
  enabled = true
3066
3806
  } = {}) {
3067
- const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState10(false);
3807
+ const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState13(false);
3068
3808
  const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPod|iPad/.test(navigator.platform);
3069
3809
  const modifierKey = isMac ? "\u2318" : "Ctrl";
3070
- useEffect5(() => {
3810
+ useEffect7(() => {
3071
3811
  if (!enabled) return;
3072
3812
  const handleKeyDown = (event) => {
3073
3813
  if (event.key === "Escape") {
@@ -3093,6 +3833,11 @@ function useSpreadsheetKeyboardShortcuts({
3093
3833
  onRedo?.();
3094
3834
  return;
3095
3835
  }
3836
+ if ((event.metaKey || event.ctrlKey) && (event.key === "a" || event.key === "A") && !isEditing) {
3837
+ event.preventDefault();
3838
+ onSelectAll?.();
3839
+ return;
3840
+ }
3096
3841
  if ((event.metaKey || event.ctrlKey) && event.key === "c" && !isEditing && hasFocusedCell) {
3097
3842
  event.preventDefault();
3098
3843
  onCopy?.();
@@ -3157,6 +3902,7 @@ function useSpreadsheetKeyboardShortcuts({
3157
3902
  onTabNavigation,
3158
3903
  onCopy,
3159
3904
  onPaste,
3905
+ onSelectAll,
3160
3906
  hasFocusedCell,
3161
3907
  isEditing,
3162
3908
  customShortcuts
@@ -3174,6 +3920,7 @@ function useSpreadsheetKeyboardShortcuts({
3174
3920
  editing: [
3175
3921
  { label: "Undo", keys: [modifierKey, "Z"] },
3176
3922
  { label: "Redo", keys: [modifierKey, "Shift", "Z"] },
3923
+ { label: "Select all", keys: [modifierKey, "A"] },
3177
3924
  { label: "Copy cells", keys: [modifierKey, "C"] },
3178
3925
  { label: "Paste cells", keys: [modifierKey, "V"] },
3179
3926
  { label: "Confirm cell edit", keys: ["Enter"] },
@@ -3208,7 +3955,7 @@ function useSpreadsheetKeyboardShortcuts({
3208
3955
  }
3209
3956
 
3210
3957
  // src/hooks/useSpreadsheetSelection.ts
3211
- import { useCallback as useCallback6, useState as useState11, useRef as useRef4, useMemo as useMemo4 } from "react";
3958
+ import { useCallback as useCallback9, useState as useState14, useRef as useRef8, useMemo as useMemo5 } from "react";
3212
3959
  function useSpreadsheetSelection({
3213
3960
  data,
3214
3961
  columns,
@@ -3216,34 +3963,34 @@ function useSpreadsheetSelection({
3216
3963
  onCellRangeSelectionChange,
3217
3964
  enableCellEditing = true
3218
3965
  }) {
3219
- const [focusedCell, setFocusedCell] = useState11(null);
3220
- const [editingCell, setEditingCell] = useState11(null);
3221
- const [selectedCellRange, setSelectedCellRangeState] = useState11(null);
3222
- const [isDragging, setIsDragging] = useState11(false);
3223
- const [clipboardData, setClipboardData] = useState11(null);
3224
- const anchorCell = useRef4(null);
3225
- const rowIndexMap = useMemo4(() => {
3966
+ const [focusedCell, setFocusedCell] = useState14(null);
3967
+ const [editingCell, setEditingCell] = useState14(null);
3968
+ const [selectedCellRange, setSelectedCellRangeState] = useState14(null);
3969
+ const [isDragging, setIsDragging] = useState14(false);
3970
+ const [clipboardData, setClipboardData] = useState14(null);
3971
+ const anchorCell = useRef8(null);
3972
+ const rowIndexMap = useMemo5(() => {
3226
3973
  const map = /* @__PURE__ */ new Map();
3227
3974
  data.forEach((row, index) => {
3228
3975
  map.set(getRowId(row), index);
3229
3976
  });
3230
3977
  return map;
3231
3978
  }, [data, getRowId]);
3232
- const columnIndexMap = useMemo4(() => {
3979
+ const columnIndexMap = useMemo5(() => {
3233
3980
  const map = /* @__PURE__ */ new Map();
3234
3981
  columns.forEach((col, index) => {
3235
3982
  map.set(col.id, index);
3236
3983
  });
3237
3984
  return map;
3238
3985
  }, [columns]);
3239
- const setSelectedCellRange = useCallback6(
3986
+ const setSelectedCellRange = useCallback9(
3240
3987
  (range) => {
3241
3988
  setSelectedCellRangeState(range);
3242
3989
  onCellRangeSelectionChange?.(range);
3243
3990
  },
3244
3991
  [onCellRangeSelectionChange]
3245
3992
  );
3246
- const getNormalizedRange = useCallback6(
3993
+ const getNormalizedRange = useCallback9(
3247
3994
  (range) => {
3248
3995
  if (!range) return null;
3249
3996
  const startRowIdx = rowIndexMap.get(range.start.rowId);
@@ -3262,7 +4009,7 @@ function useSpreadsheetSelection({
3262
4009
  },
3263
4010
  [rowIndexMap, columnIndexMap]
3264
4011
  );
3265
- const isCellInSelection = useCallback6(
4012
+ const isCellInSelection = useCallback9(
3266
4013
  (rowId, columnId) => {
3267
4014
  const normalizedRange = getNormalizedRange(selectedCellRange);
3268
4015
  if (!normalizedRange) return false;
@@ -3273,7 +4020,7 @@ function useSpreadsheetSelection({
3273
4020
  },
3274
4021
  [selectedCellRange, rowIndexMap, columnIndexMap, getNormalizedRange]
3275
4022
  );
3276
- const getCellSelectionEdge = useCallback6(
4023
+ const getCellSelectionEdge = useCallback9(
3277
4024
  (rowId, columnId) => {
3278
4025
  if (!isCellInSelection(rowId, columnId)) return void 0;
3279
4026
  const normalizedRange = getNormalizedRange(selectedCellRange);
@@ -3290,7 +4037,7 @@ function useSpreadsheetSelection({
3290
4037
  },
3291
4038
  [isCellInSelection, selectedCellRange, rowIndexMap, columnIndexMap, getNormalizedRange]
3292
4039
  );
3293
- const getSelectedCells = useCallback6(() => {
4040
+ const getSelectedCells = useCallback9(() => {
3294
4041
  const normalizedRange = getNormalizedRange(selectedCellRange);
3295
4042
  if (!normalizedRange) {
3296
4043
  return focusedCell ? [focusedCell] : [];
@@ -3308,7 +4055,7 @@ function useSpreadsheetSelection({
3308
4055
  }
3309
4056
  return cells;
3310
4057
  }, [selectedCellRange, focusedCell, data, columns, getRowId, getNormalizedRange]);
3311
- const getSelectedCellValues = useCallback6(() => {
4058
+ const getSelectedCellValues = useCallback9(() => {
3312
4059
  const cells = getSelectedCells();
3313
4060
  return cells.map((cell) => {
3314
4061
  const row = data.find((r) => getRowId(r) === cell.rowId);
@@ -3317,7 +4064,7 @@ function useSpreadsheetSelection({
3317
4064
  return { position: cell, value };
3318
4065
  });
3319
4066
  }, [getSelectedCells, data, columns, getRowId]);
3320
- const handleCellClick = useCallback6(
4067
+ const handleCellClick = useCallback9(
3321
4068
  (rowId, columnId, event) => {
3322
4069
  event.stopPropagation();
3323
4070
  const newCell = { rowId, columnId };
@@ -3331,15 +4078,12 @@ function useSpreadsheetSelection({
3331
4078
  anchorCell.current = newCell;
3332
4079
  setFocusedCell(newCell);
3333
4080
  setSelectedCellRange(null);
3334
- const column = columns.find((c) => c.id === columnId);
3335
- if (column?.editable && enableCellEditing) {
3336
- setEditingCell(newCell);
3337
- }
4081
+ setEditingCell(null);
3338
4082
  }
3339
4083
  },
3340
- [columns, enableCellEditing, setSelectedCellRange]
4084
+ [setSelectedCellRange]
3341
4085
  );
3342
- const handleCellMouseDown = useCallback6(
4086
+ const handleCellMouseDown = useCallback9(
3343
4087
  (rowId, columnId, event) => {
3344
4088
  if (event.button !== 0) return;
3345
4089
  const newCell = { rowId, columnId };
@@ -3355,28 +4099,23 @@ function useSpreadsheetSelection({
3355
4099
  anchorCell.current = newCell;
3356
4100
  setFocusedCell(newCell);
3357
4101
  setSelectedCellRange(null);
3358
- const column = columns.find((c) => c.id === columnId);
3359
- if (column?.editable && enableCellEditing) {
3360
- setEditingCell(newCell);
3361
- } else {
3362
- setEditingCell(null);
3363
- }
4102
+ setEditingCell(null);
3364
4103
  }
3365
4104
  },
3366
- [columns, enableCellEditing, setSelectedCellRange]
4105
+ [setSelectedCellRange]
3367
4106
  );
3368
- const handleCellMouseEnter = useCallback6((_rowId, _columnId) => {
4107
+ const handleCellMouseEnter = useCallback9((_rowId, _columnId) => {
3369
4108
  }, []);
3370
- const handleMouseUp = useCallback6(() => {
4109
+ const handleMouseUp = useCallback9(() => {
3371
4110
  setIsDragging(false);
3372
4111
  }, []);
3373
- const clearSelection = useCallback6(() => {
4112
+ const clearSelection = useCallback9(() => {
3374
4113
  setFocusedCell(null);
3375
4114
  setEditingCell(null);
3376
4115
  setSelectedCellRange(null);
3377
4116
  anchorCell.current = null;
3378
4117
  }, [setSelectedCellRange]);
3379
- const navigateCell = useCallback6(
4118
+ const navigateCell = useCallback9(
3380
4119
  (direction, extendSelection = false) => {
3381
4120
  const currentCell = focusedCell;
3382
4121
  if (!currentCell) return;
@@ -3423,7 +4162,7 @@ function useSpreadsheetSelection({
3423
4162
  },
3424
4163
  [focusedCell, data, columns, getRowId, rowIndexMap, columnIndexMap, setSelectedCellRange]
3425
4164
  );
3426
- const handleTabNavigation = useCallback6(
4165
+ const handleTabNavigation = useCallback9(
3427
4166
  (shiftKey) => {
3428
4167
  const currentCell = focusedCell;
3429
4168
  if (!currentCell) return;
@@ -3464,17 +4203,17 @@ function useSpreadsheetSelection({
3464
4203
  },
3465
4204
  [focusedCell, data, columns, getRowId, rowIndexMap, columnIndexMap, setSelectedCellRange]
3466
4205
  );
3467
- const enterEditMode = useCallback6(() => {
4206
+ const enterEditMode = useCallback9(() => {
3468
4207
  if (!focusedCell || !enableCellEditing) return;
3469
4208
  const column = columns.find((c) => c.id === focusedCell.columnId);
3470
4209
  if (column?.editable) {
3471
4210
  setEditingCell(focusedCell);
3472
4211
  }
3473
4212
  }, [focusedCell, columns, enableCellEditing]);
3474
- const exitEditMode = useCallback6(() => {
4213
+ const exitEditMode = useCallback9(() => {
3475
4214
  setEditingCell(null);
3476
4215
  }, []);
3477
- const copySelectedCells = useCallback6(() => {
4216
+ const copySelectedCells = useCallback9(() => {
3478
4217
  const normalizedRange = getNormalizedRange(selectedCellRange);
3479
4218
  if (!normalizedRange && !focusedCell) return;
3480
4219
  const startRowIdx = normalizedRange?.startRowIdx ?? rowIndexMap.get(focusedCell.rowId) ?? 0;
@@ -3523,14 +4262,14 @@ function useSpreadsheetSelection({
3523
4262
  rowIndexMap,
3524
4263
  columnIndexMap
3525
4264
  ]);
3526
- const parseClipboardText = useCallback6((text) => {
4265
+ const parseClipboardText = useCallback9((text) => {
3527
4266
  const lines = text.split(/\r?\n/);
3528
4267
  if (lines.length > 0 && lines[lines.length - 1] === "") {
3529
4268
  lines.pop();
3530
4269
  }
3531
4270
  return lines.map((line) => line.split(" "));
3532
4271
  }, []);
3533
- const createEditsFromValues = useCallback6(
4272
+ const createEditsFromValues = useCallback9(
3534
4273
  (values) => {
3535
4274
  if (!focusedCell) return [];
3536
4275
  const edits = [];
@@ -3590,11 +4329,11 @@ function useSpreadsheetSelection({
3590
4329
  getNormalizedRange
3591
4330
  ]
3592
4331
  );
3593
- const pasteClipboard = useCallback6(() => {
4332
+ const pasteClipboard = useCallback9(() => {
3594
4333
  if (!clipboardData?.values) return [];
3595
4334
  return createEditsFromValues(clipboardData.values);
3596
4335
  }, [clipboardData, createEditsFromValues]);
3597
- const pasteFromSystemClipboard = useCallback6(async () => {
4336
+ const pasteFromSystemClipboard = useCallback9(async () => {
3598
4337
  if (!focusedCell) return [];
3599
4338
  try {
3600
4339
  const text = await navigator.clipboard.readText();
@@ -3635,8 +4374,193 @@ function useSpreadsheetSelection({
3635
4374
  };
3636
4375
  }
3637
4376
 
4377
+ // src/hooks/useSpreadsheetSummary.ts
4378
+ import { useMemo as useMemo6 } from "react";
4379
+ function useSpreadsheetSummary({
4380
+ selectedCellValues,
4381
+ columns
4382
+ }) {
4383
+ const summary = useMemo6(() => {
4384
+ if (selectedCellValues.length === 0) return null;
4385
+ const numericValues = [];
4386
+ for (const { position, value } of selectedCellValues) {
4387
+ const column = columns.find((c) => c.id === position.columnId);
4388
+ if (column?.type === "number" || typeof value === "number") {
4389
+ const num = typeof value === "number" ? value : parseFloat(value);
4390
+ if (!isNaN(num)) {
4391
+ numericValues.push(num);
4392
+ }
4393
+ }
4394
+ }
4395
+ if (numericValues.length === 0) return null;
4396
+ const sum = numericValues.reduce((a, b) => a + b, 0);
4397
+ return {
4398
+ sum: Math.round(sum * 100) / 100,
4399
+ avg: Math.round(sum / numericValues.length * 100) / 100,
4400
+ count: selectedCellValues.length,
4401
+ numericCount: numericValues.length,
4402
+ min: Math.round(Math.min(...numericValues) * 100) / 100,
4403
+ max: Math.round(Math.max(...numericValues) * 100) / 100
4404
+ };
4405
+ }, [selectedCellValues, columns]);
4406
+ return {
4407
+ summary,
4408
+ hasNumericValues: summary !== null
4409
+ };
4410
+ }
4411
+
4412
+ // src/hooks/useSpreadsheetColumnResize.ts
4413
+ import { useCallback as useCallback10, useState as useState15, useRef as useRef9 } from "react";
4414
+ var DEFAULT_MIN_WIDTH = 40;
4415
+ function useSpreadsheetColumnResize({
4416
+ minWidth = DEFAULT_MIN_WIDTH,
4417
+ initialColumnWidths,
4418
+ onColumnResize
4419
+ } = {}) {
4420
+ const [columnWidths, setColumnWidths] = useState15(() => {
4421
+ if (initialColumnWidths) {
4422
+ return new Map(Object.entries(initialColumnWidths));
4423
+ }
4424
+ return /* @__PURE__ */ new Map();
4425
+ });
4426
+ const [resizingColumnId, setResizingColumnId] = useState15(null);
4427
+ const startXRef = useRef9(0);
4428
+ const startWidthRef = useRef9(0);
4429
+ const rafRef = useRef9(null);
4430
+ const getColumnWidth = useCallback10(
4431
+ (columnId, defaultWidth) => {
4432
+ return columnWidths.get(columnId) ?? defaultWidth;
4433
+ },
4434
+ [columnWidths]
4435
+ );
4436
+ const updateColumnDom = useCallback10((columnId, width) => {
4437
+ const widthPx = `${width}px`;
4438
+ const cells = document.querySelectorAll(`[data-column-id="${columnId}"]`);
4439
+ for (let i = 0; i < cells.length; i++) {
4440
+ const el = cells[i];
4441
+ el.style.width = widthPx;
4442
+ el.style.minWidth = widthPx;
4443
+ }
4444
+ }, []);
4445
+ const getResizeHandleProps = useCallback10(
4446
+ (columnId, currentWidth) => {
4447
+ return {
4448
+ onMouseDown: (e) => {
4449
+ e.preventDefault();
4450
+ e.stopPropagation();
4451
+ startXRef.current = e.clientX;
4452
+ startWidthRef.current = columnWidths.get(columnId) ?? currentWidth;
4453
+ setResizingColumnId(columnId);
4454
+ document.body.style.cursor = "col-resize";
4455
+ document.body.style.userSelect = "none";
4456
+ let latestWidth = startWidthRef.current;
4457
+ const moveHandler = (ev) => {
4458
+ const diff = ev.clientX - startXRef.current;
4459
+ latestWidth = Math.max(minWidth, startWidthRef.current + diff);
4460
+ if (rafRef.current === null) {
4461
+ rafRef.current = requestAnimationFrame(() => {
4462
+ rafRef.current = null;
4463
+ updateColumnDom(columnId, latestWidth);
4464
+ });
4465
+ }
4466
+ };
4467
+ const upHandler = () => {
4468
+ document.removeEventListener("mousemove", moveHandler);
4469
+ document.removeEventListener("mouseup", upHandler);
4470
+ if (rafRef.current !== null) {
4471
+ cancelAnimationFrame(rafRef.current);
4472
+ rafRef.current = null;
4473
+ }
4474
+ setColumnWidths((prev) => {
4475
+ const next = new Map(prev);
4476
+ next.set(columnId, latestWidth);
4477
+ return next;
4478
+ });
4479
+ onColumnResize?.(columnId, latestWidth);
4480
+ setResizingColumnId(null);
4481
+ document.body.style.cursor = "";
4482
+ document.body.style.userSelect = "";
4483
+ };
4484
+ document.addEventListener("mousemove", moveHandler);
4485
+ document.addEventListener("mouseup", upHandler);
4486
+ },
4487
+ style: {
4488
+ cursor: "col-resize"
4489
+ },
4490
+ className: "absolute top-0 right-0 w-1 h-full hover:bg-blue-400 transition-colors z-10"
4491
+ };
4492
+ },
4493
+ [columnWidths, minWidth, onColumnResize, updateColumnDom]
4494
+ );
4495
+ return {
4496
+ columnWidths,
4497
+ getColumnWidth,
4498
+ getResizeHandleProps,
4499
+ isResizing: resizingColumnId !== null,
4500
+ resizingColumnId
4501
+ };
4502
+ }
4503
+
4504
+ // src/components/SelectionSummaryBar.tsx
4505
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
4506
+ function SelectionSummaryBar({
4507
+ summary,
4508
+ focusedCell,
4509
+ columns,
4510
+ data,
4511
+ getRowId,
4512
+ currentPage,
4513
+ pageSize
4514
+ }) {
4515
+ let addressDisplay = null;
4516
+ let valueDisplay = null;
4517
+ if (focusedCell) {
4518
+ const column = columns.find((c) => c.id === focusedCell.columnId);
4519
+ const rowIndex = data.findIndex((r) => getRowId(r) === focusedCell.rowId);
4520
+ const row = rowIndex !== -1 ? data[rowIndex] : null;
4521
+ if (column && row) {
4522
+ const displayRowIndex = rowIndex + 1 + (currentPage - 1) * pageSize;
4523
+ addressDisplay = `Row ${displayRowIndex} / ${column.label}`;
4524
+ const value = column.getValue ? column.getValue(row) : row[focusedCell.columnId];
4525
+ if (value !== null && value !== void 0 && value !== "") {
4526
+ valueDisplay = String(value);
4527
+ }
4528
+ }
4529
+ }
4530
+ if (!addressDisplay && !summary) return null;
4531
+ return /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between px-3 py-1.5 bg-gray-50 border-t border-gray-200 text-xs text-gray-600", children: [
4532
+ /* @__PURE__ */ jsx13("div", { className: "flex items-center gap-2 min-w-0", children: addressDisplay && /* @__PURE__ */ jsxs13(Fragment3, { children: [
4533
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-gray-500 bg-white px-2 py-0.5 rounded border border-gray-200 shrink-0", children: addressDisplay }),
4534
+ valueDisplay && /* @__PURE__ */ jsx13("span", { className: "text-gray-700 truncate", title: valueDisplay, children: valueDisplay })
4535
+ ] }) }),
4536
+ summary && /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-4 shrink-0", children: [
4537
+ /* @__PURE__ */ jsxs13("span", { children: [
4538
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-400 mr-1", children: "Count:" }),
4539
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-gray-700", children: summary.numericCount })
4540
+ ] }),
4541
+ /* @__PURE__ */ jsxs13("span", { children: [
4542
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-400 mr-1", children: "Sum:" }),
4543
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-gray-700", children: summary.sum.toLocaleString() })
4544
+ ] }),
4545
+ /* @__PURE__ */ jsxs13("span", { children: [
4546
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-400 mr-1", children: "Avg:" }),
4547
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-gray-700", children: summary.avg.toLocaleString() })
4548
+ ] }),
4549
+ /* @__PURE__ */ jsxs13("span", { children: [
4550
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-400 mr-1", children: "Min:" }),
4551
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-gray-700", children: summary.min.toLocaleString() })
4552
+ ] }),
4553
+ /* @__PURE__ */ jsxs13("span", { children: [
4554
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-400 mr-1", children: "Max:" }),
4555
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-gray-700", children: summary.max.toLocaleString() })
4556
+ ] })
4557
+ ] })
4558
+ ] });
4559
+ }
4560
+ SelectionSummaryBar.displayName = "SelectionSummaryBar";
4561
+
3638
4562
  // src/components/Spreadsheet.tsx
3639
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
4563
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
3640
4564
  function Spreadsheet({
3641
4565
  data,
3642
4566
  columns,
@@ -3682,9 +4606,11 @@ function Spreadsheet({
3682
4606
  pageSize: controlledPageSize,
3683
4607
  sortConfig: controlledSortConfig,
3684
4608
  onPageChange,
3685
- filters: controlledFilters
4609
+ filters: controlledFilters,
4610
+ duplicateCheckColumns: propDuplicateCheckColumns,
4611
+ onDuplicateCheckChange
3686
4612
  }) {
3687
- const [spreadsheetSettings, setSpreadsheetSettings] = useState12({
4613
+ const [spreadsheetSettings, setSpreadsheetSettings] = useState16({
3688
4614
  defaultPinnedColumns: initialSettings?.defaultPinnedColumns ?? [],
3689
4615
  defaultSort: initialSettings?.defaultSort ?? null,
3690
4616
  defaultPageSize: initialSettings?.defaultPageSize ?? 25,
@@ -3692,6 +4618,32 @@ function Spreadsheet({
3692
4618
  autoSave: initialSettings?.autoSave ?? true,
3693
4619
  compactView: initialSettings?.compactView ?? false
3694
4620
  });
4621
+ const {
4622
+ isCellDuplicate,
4623
+ toggleDuplicateCheck,
4624
+ duplicateCheckColumns,
4625
+ duplicateRowIds,
4626
+ getDuplicateColumnCount
4627
+ } = useSpreadsheetDuplicates({
4628
+ data,
4629
+ columns,
4630
+ duplicateCheckColumns: propDuplicateCheckColumns ?? [],
4631
+ getRowId
4632
+ });
4633
+ const handleDuplicateCheckToggle = useCallback11(
4634
+ (columnId) => {
4635
+ setIsProcessing(true);
4636
+ startTransition(() => {
4637
+ toggleDuplicateCheck(columnId);
4638
+ const currentCols = Array.from(duplicateCheckColumns);
4639
+ const next = currentCols.includes(columnId) ? currentCols.filter((id) => id !== columnId) : [...currentCols, columnId];
4640
+ onDuplicateCheckChange?.(next);
4641
+ setIsProcessing(false);
4642
+ });
4643
+ },
4644
+ [toggleDuplicateCheck, duplicateCheckColumns, onDuplicateCheckChange]
4645
+ );
4646
+ const [isProcessing, setIsProcessing] = useState16(false);
3695
4647
  const {
3696
4648
  filters,
3697
4649
  sortConfig,
@@ -3711,7 +4663,9 @@ function Spreadsheet({
3711
4663
  serverSide,
3712
4664
  controlledFilters,
3713
4665
  controlledSortConfig,
3714
- defaultSortConfig: spreadsheetSettings.defaultSort
4666
+ defaultSortConfig: spreadsheetSettings.defaultSort,
4667
+ duplicateRowIds,
4668
+ getRowId
3715
4669
  });
3716
4670
  const {
3717
4671
  getCellHighlight,
@@ -3725,7 +4679,8 @@ function Spreadsheet({
3725
4679
  highlightPickerColumn,
3726
4680
  setHighlightPickerColumn,
3727
4681
  highlightPickerCell,
3728
- setHighlightPickerCell
4682
+ setHighlightPickerCell,
4683
+ recentColors
3729
4684
  } = useSpreadsheetHighlighting({
3730
4685
  externalRowHighlights,
3731
4686
  onRowHighlight,
@@ -3734,6 +4689,15 @@ function Spreadsheet({
3734
4689
  externalCellHighlights,
3735
4690
  onCellHighlight
3736
4691
  });
4692
+ const { getColumnWidth, getResizeHandleProps, isResizing, columnWidths } = useSpreadsheetColumnResize({
4693
+ initialColumnWidths: initialSettings?.columnWidths,
4694
+ onColumnResize: (columnId, width) => {
4695
+ setSpreadsheetSettings((prev) => ({
4696
+ ...prev,
4697
+ columnWidths: { ...prev.columnWidths, [columnId]: width }
4698
+ }));
4699
+ }
4700
+ });
3737
4701
  const {
3738
4702
  pinnedColumns,
3739
4703
  isRowIndexPinned,
@@ -3745,12 +4709,15 @@ function Spreadsheet({
3745
4709
  getColumnLeftOffset,
3746
4710
  getColumnRightOffset,
3747
4711
  isColumnPinned,
3748
- getColumnPinSide
4712
+ getColumnPinSide,
4713
+ getPinnedZIndex,
4714
+ measureRef
3749
4715
  } = useSpreadsheetPinning({
3750
4716
  columns,
3751
4717
  columnGroups,
3752
4718
  defaultPinnedColumns: initialSettings?.defaultPinnedColumns,
3753
- defaultPinnedRightColumns: initialSettings?.defaultPinnedRightColumns
4719
+ defaultPinnedRightColumns: initialSettings?.defaultPinnedRightColumns,
4720
+ getColumnWidth
3754
4721
  });
3755
4722
  const {
3756
4723
  getCellComments,
@@ -3767,8 +4734,8 @@ function Spreadsheet({
3767
4734
  onAddCellComment,
3768
4735
  onToggleCommentResolved
3769
4736
  });
3770
- const [showSettingsModal, setShowSettingsModal] = useState12(false);
3771
- const [showFiltersPanel, setShowFiltersPanel] = useState12(false);
4737
+ const [showSettingsModal, setShowSettingsModal] = useState16(false);
4738
+ const [showFiltersPanel, setShowFiltersPanel] = useState16(false);
3772
4739
  const {
3773
4740
  canUndo,
3774
4741
  canRedo,
@@ -3786,15 +4753,15 @@ function Spreadsheet({
3786
4753
  autoSave: spreadsheetSettings.autoSave,
3787
4754
  onSave
3788
4755
  });
3789
- const [selectedRows, setSelectedRows] = useState12(/* @__PURE__ */ new Set());
3790
- const [lastSelectedRow, setLastSelectedRow] = useState12(null);
3791
- const [hoveredRow, setHoveredRow] = useState12(null);
3792
- const [internalCurrentPage, setInternalCurrentPage] = useState12(1);
3793
- const [internalPageSize, setInternalPageSize] = useState12(spreadsheetSettings.defaultPageSize);
3794
- const [zoom, setZoom] = useState12(spreadsheetSettings.defaultZoom);
4756
+ const [selectedRows, setSelectedRows] = useState16(/* @__PURE__ */ new Set());
4757
+ const [lastSelectedRow, setLastSelectedRow] = useState16(null);
4758
+ const hoveredRowRef = useRef10(null);
4759
+ const [internalCurrentPage, setInternalCurrentPage] = useState16(1);
4760
+ const [internalPageSize, setInternalPageSize] = useState16(spreadsheetSettings.defaultPageSize);
4761
+ const [zoom, setZoom] = useState16(spreadsheetSettings.defaultZoom);
3795
4762
  const currentPage = controlledCurrentPage ?? internalCurrentPage;
3796
4763
  const pageSize = controlledPageSize ?? internalPageSize;
3797
- const handlePageChange = useCallback7(
4764
+ const handlePageChange = useCallback11(
3798
4765
  (newPage) => {
3799
4766
  if (controlledCurrentPage === void 0) {
3800
4767
  setInternalCurrentPage(newPage);
@@ -3803,7 +4770,7 @@ function Spreadsheet({
3803
4770
  },
3804
4771
  [controlledCurrentPage, onPageChange, pageSize]
3805
4772
  );
3806
- const handlePageSizeChange = useCallback7(
4773
+ const handlePageSizeChange = useCallback11(
3807
4774
  (newPageSize) => {
3808
4775
  if (controlledPageSize === void 0) {
3809
4776
  setInternalPageSize(newPageSize);
@@ -3815,30 +4782,43 @@ function Spreadsheet({
3815
4782
  },
3816
4783
  [controlledPageSize, controlledCurrentPage, onPageChange]
3817
4784
  );
3818
- const resetPaginationToFirstPage = useCallback7(() => {
4785
+ const resetPaginationToFirstPage = useCallback11(() => {
3819
4786
  if (controlledCurrentPage === void 0) {
3820
4787
  setInternalCurrentPage(1);
3821
4788
  }
3822
4789
  onPageChange?.(1, pageSize);
3823
4790
  }, [controlledCurrentPage, onPageChange, pageSize]);
3824
- const handleFilterChangeWithReset = useCallback7(
4791
+ const handleFilterChangeWithReset = useCallback11(
3825
4792
  (columnId, filter) => {
3826
4793
  handleFilterChange(columnId, filter);
3827
4794
  resetPaginationToFirstPage();
3828
4795
  },
3829
4796
  [handleFilterChange, resetPaginationToFirstPage]
3830
4797
  );
3831
- const clearAllFiltersWithReset = useCallback7(() => {
4798
+ const clearAllFiltersWithReset = useCallback11(() => {
3832
4799
  clearAllFilters();
3833
4800
  resetPaginationToFirstPage();
3834
4801
  }, [clearAllFilters, resetPaginationToFirstPage]);
3835
- useEffect6(() => {
4802
+ useEffect8(() => {
3836
4803
  setSpreadsheetSettings((prev) => ({
3837
4804
  ...prev,
3838
4805
  defaultSort: sortConfig
3839
4806
  }));
3840
4807
  }, [sortConfig]);
3841
- useEffect6(() => {
4808
+ const hasSyncedPinnedFromSettings = useRef10(false);
4809
+ useEffect8(() => {
4810
+ if (hasSyncedPinnedFromSettings.current) return;
4811
+ const settingsPinned = initialSettings?.defaultPinnedColumns;
4812
+ if (settingsPinned && settingsPinned.length > 0) {
4813
+ const currentIds = Array.from(pinnedColumns.keys());
4814
+ const hasAllSettingsPinned = settingsPinned.every((id) => pinnedColumns.has(id));
4815
+ if (!hasAllSettingsPinned) {
4816
+ setPinnedColumnsFromIds(settingsPinned);
4817
+ }
4818
+ hasSyncedPinnedFromSettings.current = true;
4819
+ }
4820
+ }, [initialSettings?.defaultPinnedColumns, pinnedColumns, setPinnedColumnsFromIds]);
4821
+ useEffect8(() => {
3842
4822
  const pinnedColumnIds = Array.from(pinnedColumns.keys());
3843
4823
  setSpreadsheetSettings((prev) => {
3844
4824
  const prevIds = prev.defaultPinnedColumns;
@@ -3851,15 +4831,15 @@ function Spreadsheet({
3851
4831
  };
3852
4832
  });
3853
4833
  }, [pinnedColumns]);
3854
- const isInitialMount = useRef5(true);
3855
- useEffect6(() => {
4834
+ const isInitialMount = useRef10(true);
4835
+ useEffect8(() => {
3856
4836
  if (isInitialMount.current) {
3857
4837
  isInitialMount.current = false;
3858
4838
  return;
3859
4839
  }
3860
4840
  onSettingsChange?.(spreadsheetSettings);
3861
4841
  }, [spreadsheetSettings, onSettingsChange]);
3862
- const applyUndo = useCallback7(() => {
4842
+ const applyUndo = useCallback11(() => {
3863
4843
  const entry = popUndoEntry();
3864
4844
  if (!entry || !onCellsEdit) return;
3865
4845
  if (entry.type === "cell-edit") {
@@ -3877,7 +4857,7 @@ function Spreadsheet({
3877
4857
  }
3878
4858
  markAsChanged();
3879
4859
  }, [popUndoEntry, onCellsEdit, markAsChanged]);
3880
- const applyRedo = useCallback7(() => {
4860
+ const applyRedo = useCallback11(() => {
3881
4861
  const entry = popRedoEntry();
3882
4862
  if (!entry || !onCellsEdit) return;
3883
4863
  if (entry.type === "cell-edit") {
@@ -3893,18 +4873,23 @@ function Spreadsheet({
3893
4873
  }
3894
4874
  markAsChanged();
3895
4875
  }, [popRedoEntry, onCellsEdit, markAsChanged]);
3896
- const paginatedData = useMemo5(() => {
4876
+ const paginatedData = useMemo7(() => {
3897
4877
  if (serverSide) {
3898
- return filteredData;
4878
+ return filteredData.toArray();
3899
4879
  }
3900
4880
  const startIndex = (currentPage - 1) * pageSize;
3901
4881
  return filteredData.slice(startIndex, startIndex + pageSize);
3902
4882
  }, [filteredData, currentPage, pageSize, serverSide]);
3903
4883
  const {
3904
4884
  focusedCell,
4885
+ setFocusedCell,
3905
4886
  editingCell,
3906
4887
  setEditingCell,
4888
+ selectedCellRange,
4889
+ setSelectedCellRange,
3907
4890
  handleCellMouseDown,
4891
+ handleCellMouseEnter,
4892
+ handleMouseUp,
3908
4893
  isCellInSelection,
3909
4894
  getCellSelectionEdge,
3910
4895
  clearSelection,
@@ -3912,14 +4897,20 @@ function Spreadsheet({
3912
4897
  handleTabNavigation,
3913
4898
  enterEditMode,
3914
4899
  copySelectedCells,
3915
- pasteFromSystemClipboard
4900
+ pasteFromSystemClipboard,
4901
+ getSelectedCellValues
3916
4902
  } = useSpreadsheetSelection({
3917
4903
  data: paginatedData,
3918
4904
  columns: visibleColumns,
3919
4905
  getRowId,
3920
4906
  enableCellEditing
3921
4907
  });
3922
- const handleEscapeCallback = useCallback7(() => {
4908
+ const selectedCellValues = useMemo7(() => getSelectedCellValues(), [getSelectedCellValues]);
4909
+ const { summary: selectionSummary, hasNumericValues } = useSpreadsheetSummary({
4910
+ selectedCellValues,
4911
+ columns: visibleColumns
4912
+ });
4913
+ const handleEscapeCallback = useCallback11(() => {
3923
4914
  if (commentModalCell !== null) {
3924
4915
  setCommentModalCell(null);
3925
4916
  } else if (viewCommentsCell !== null) {
@@ -3948,7 +4939,7 @@ function Spreadsheet({
3948
4939
  setHighlightPickerCell,
3949
4940
  clearSelection
3950
4941
  ]);
3951
- const handleNavigate = useCallback7(
4942
+ const handleNavigate = useCallback11(
3952
4943
  (direction, event) => {
3953
4944
  const extendSelection = event?.shiftKey ?? false;
3954
4945
  navigateCell(direction, extendSelection);
@@ -3969,10 +4960,10 @@ function Spreadsheet({
3969
4960
  },
3970
4961
  [navigateCell, focusedCell]
3971
4962
  );
3972
- const handleEnterEditMode = useCallback7(() => {
4963
+ const handleEnterEditMode = useCallback11(() => {
3973
4964
  enterEditMode();
3974
4965
  }, [enterEditMode]);
3975
- const handlePaste = useCallback7(async () => {
4966
+ const handlePaste = useCallback11(async () => {
3976
4967
  const edits = await pasteFromSystemClipboard();
3977
4968
  if (edits.length > 0 && onCellsEdit) {
3978
4969
  if (enableUndoRedo) {
@@ -4012,26 +5003,39 @@ function Spreadsheet({
4012
5003
  onTabNavigation: handleTabNavigation,
4013
5004
  onCopy: copySelectedCells,
4014
5005
  onPaste: handlePaste,
5006
+ onSelectAll: () => {
5007
+ if (paginatedData.length > 0 && visibleColumns.length > 0) {
5008
+ const firstRow = paginatedData[0];
5009
+ const lastRow = paginatedData[paginatedData.length - 1];
5010
+ const firstCol = visibleColumns[0];
5011
+ const lastCol = visibleColumns[visibleColumns.length - 1];
5012
+ setSelectedCellRange({
5013
+ start: { rowId: getRowId(firstRow), columnId: firstCol.id },
5014
+ end: { rowId: getRowId(lastRow), columnId: lastCol.id }
5015
+ });
5016
+ setFocusedCell({ rowId: getRowId(firstRow), columnId: firstCol.id });
5017
+ }
5018
+ },
4015
5019
  hasFocusedCell: focusedCell !== null,
4016
5020
  isEditing: editingCell !== null,
4017
5021
  enabled: true
4018
5022
  });
4019
5023
  const effectiveCompactMode = spreadsheetSettings.compactView ?? false;
4020
5024
  const rowIndexHighlightColor = getColumnHighlight(ROW_INDEX_COLUMN_ID);
4021
- const tableRef = useRef5(null);
5025
+ const tableRef = useRef10(null);
4022
5026
  const effectiveTotalItems = serverSide ? totalItems ?? data.length : filteredData.length;
4023
5027
  const totalPages = Math.max(1, Math.ceil(effectiveTotalItems / pageSize));
4024
- useEffect6(() => {
5028
+ useEffect8(() => {
4025
5029
  if (!serverSide && currentPage > totalPages) {
4026
5030
  setInternalCurrentPage(1);
4027
5031
  }
4028
5032
  }, [totalPages, currentPage, serverSide]);
4029
- const afterFilteredRef = useRef5(afterFiltered);
5033
+ const afterFilteredRef = useRef10(afterFiltered);
4030
5034
  afterFilteredRef.current = afterFiltered;
4031
- useEffect6(() => {
5035
+ useEffect8(() => {
4032
5036
  afterFilteredRef.current?.(filteredData.toArray());
4033
5037
  }, [filteredData]);
4034
- const handleRowSelect = useCallback7(
5038
+ const handleRowSelect = useCallback11(
4035
5039
  (rowId, event) => {
4036
5040
  if (!enableRowSelection) return;
4037
5041
  event.stopPropagation();
@@ -4080,14 +5084,23 @@ function Spreadsheet({
4080
5084
  onSelectionChange
4081
5085
  ]
4082
5086
  );
4083
- const handleCellClick = useCallback7(
5087
+ const handleCellClick = useCallback11(
4084
5088
  (rowId, columnId, event) => {
4085
5089
  event.stopPropagation();
4086
5090
  handleCellMouseDown(rowId, columnId, event);
4087
5091
  },
4088
5092
  [handleCellMouseDown]
4089
5093
  );
4090
- const handleCellChange = useCallback7(
5094
+ const handleCellDoubleClick = useCallback11(
5095
+ (rowId, columnId) => {
5096
+ const column = columns.find((c) => c.id === columnId);
5097
+ if (column?.editable && enableCellEditing) {
5098
+ setEditingCell({ rowId, columnId });
5099
+ }
5100
+ },
5101
+ [columns, enableCellEditing, setEditingCell]
5102
+ );
5103
+ const handleCellChange = useCallback11(
4091
5104
  (rowId, columnId, newValue) => {
4092
5105
  const row = data.find((r) => getRowId(r) === rowId);
4093
5106
  const previousValue = row ? row[columnId] : void 0;
@@ -4110,7 +5123,7 @@ function Spreadsheet({
4110
5123
  },
4111
5124
  [data, getRowId, enableUndoRedo, onCellsEdit, pushToUndoStack, markAsChanged]
4112
5125
  );
4113
- const handleConfirmEdit = useCallback7(
5126
+ const handleConfirmEdit = useCallback11(
4114
5127
  (finalValue) => {
4115
5128
  if (editingCell && finalValue !== void 0) {
4116
5129
  handleCellChange(editingCell.rowId, editingCell.columnId, finalValue);
@@ -4119,105 +5132,51 @@ function Spreadsheet({
4119
5132
  },
4120
5133
  [editingCell, handleCellChange, setEditingCell]
4121
5134
  );
4122
- const handleCancelEdit = useCallback7(() => {
5135
+ const handleCancelEdit = useCallback11(() => {
4123
5136
  setEditingCell(null);
4124
5137
  }, [setEditingCell]);
4125
- const handleRowIndexHighlightClick = useCallback7(() => {
5138
+ const handleRowIndexHighlightClick = useCallback11(() => {
4126
5139
  setHighlightPickerColumn(ROW_INDEX_COLUMN_ID);
4127
5140
  }, [setHighlightPickerColumn]);
4128
- const columnRenderItems = useMemo5(() => {
5141
+ const columnRenderItems = useMemo7(() => {
4129
5142
  if (!columnGroups || columnGroups.length === 0) {
4130
5143
  return visibleColumns.map((col) => ({
4131
5144
  type: "column",
4132
5145
  column: col
4133
5146
  }));
4134
5147
  }
4135
- const leftPinnedItems = [];
4136
- const middleItems = [];
4137
- const rightPinnedItems = [];
5148
+ const items = [];
5149
+ const allGroupedIds = new Set(columnGroups.flatMap((g) => g.columns));
5150
+ for (const col of visibleColumns) {
5151
+ if (!allGroupedIds.has(col.id)) {
5152
+ items.push({ type: "column", column: col });
5153
+ }
5154
+ }
4138
5155
  for (const group of columnGroups) {
4139
5156
  const isCollapsed = collapsedGroups.has(group.id);
5157
+ const groupVisibleCols = visibleColumns.filter((c) => group.columns.includes(c.id));
4140
5158
  if (isCollapsed) {
4141
- middleItems.push({
5159
+ items.push({
4142
5160
  type: "collapsed-placeholder",
4143
5161
  groupId: group.id,
4144
5162
  headerColor: group.headerColor
4145
5163
  });
4146
5164
  }
4147
- const groupVisibleCols = (columns || []).filter((c) => {
4148
- if (!group.columns.includes(c.id)) return false;
4149
- if (isCollapsed) return pinnedColumns.has(c.id);
4150
- return true;
4151
- });
4152
5165
  for (const col of groupVisibleCols) {
4153
- const pinSide = pinnedColumns.get(col.id);
4154
- if (pinSide === "left") {
4155
- leftPinnedItems.push({ type: "column", column: col });
4156
- } else if (pinSide === "right") {
4157
- rightPinnedItems.push({ type: "column", column: col });
4158
- } else {
4159
- middleItems.push({ type: "column", column: col });
4160
- }
4161
- }
4162
- }
4163
- const allGroupedIds = new Set(columnGroups.flatMap((g) => g.columns));
4164
- for (const col of visibleColumns) {
4165
- if (!allGroupedIds.has(col.id)) {
4166
- const pinSide = pinnedColumns.get(col.id);
4167
- if (pinSide === "left") {
4168
- leftPinnedItems.push({ type: "column", column: col });
4169
- } else if (pinSide === "right") {
4170
- rightPinnedItems.push({ type: "column", column: col });
4171
- } else {
4172
- middleItems.push({ type: "column", column: col });
4173
- }
5166
+ items.push({ type: "column", column: col });
4174
5167
  }
4175
5168
  }
4176
- const pinnedLeftOrder = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
4177
- const pinnedRightOrder = Array.from(pinnedColumns.entries()).filter(([, side]) => side === "right").map(([id]) => id);
4178
- leftPinnedItems.sort(
4179
- (a, b) => pinnedLeftOrder.indexOf(a.column.id) - pinnedLeftOrder.indexOf(b.column.id)
4180
- );
4181
- rightPinnedItems.sort(
4182
- (a, b) => pinnedRightOrder.indexOf(a.column.id) - pinnedRightOrder.indexOf(b.column.id)
4183
- );
4184
- return [...leftPinnedItems, ...middleItems, ...rightPinnedItems];
4185
- }, [columnGroups, collapsedGroups, columns, pinnedColumns, visibleColumns]);
4186
- const groupHeaderItems = useMemo5(() => {
5169
+ return items;
5170
+ }, [columnGroups, collapsedGroups, visibleColumns]);
5171
+ const groupHeaderItems = useMemo7(() => {
4187
5172
  if (!columnGroups || columnGroups.length === 0) return null;
4188
- const leftPinned = [];
4189
- const groups = [];
4190
- const rightPinned = [];
5173
+ const items = [];
4191
5174
  for (const group of columnGroups) {
4192
5175
  const isCollapsed = collapsedGroups.has(group.id);
4193
- const groupColumns = (columns || []).filter((c) => group.columns.includes(c.id));
4194
- const visibleGroupColumns = isCollapsed ? groupColumns.filter((c) => pinnedColumns.has(c.id)) : groupColumns;
4195
- let movedLeftCount = 0;
4196
- let movedRightCount = 0;
4197
- for (const col of visibleGroupColumns) {
4198
- const pinSide = pinnedColumns.get(col.id);
4199
- if (pinSide === "left") {
4200
- movedLeftCount++;
4201
- leftPinned.push({
4202
- type: "pinned-column",
4203
- columnId: col.id,
4204
- headerColor: group.headerColor,
4205
- pinSide: "left"
4206
- });
4207
- } else if (pinSide === "right") {
4208
- movedRightCount++;
4209
- rightPinned.push({
4210
- type: "pinned-column",
4211
- columnId: col.id,
4212
- headerColor: group.headerColor,
4213
- pinSide: "right"
4214
- });
4215
- }
4216
- }
4217
- const remainingCols = visibleGroupColumns.length - movedLeftCount - movedRightCount;
4218
- const colSpan = remainingCols + (isCollapsed ? 1 : 0);
5176
+ const visibleCount = visibleColumns.filter((c) => group.columns.includes(c.id)).length;
5177
+ const colSpan = visibleCount + (isCollapsed ? 1 : 0);
4219
5178
  if (colSpan > 0) {
4220
- groups.push({
5179
+ items.push({
4221
5180
  type: "group",
4222
5181
  group,
4223
5182
  colSpan,
@@ -4225,18 +5184,10 @@ function Spreadsheet({
4225
5184
  });
4226
5185
  }
4227
5186
  }
4228
- const pinnedLeftOrder = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
4229
- const pinnedRightOrder = Array.from(pinnedColumns.entries()).filter(([, side]) => side === "right").map(([id]) => id);
4230
- leftPinned.sort(
4231
- (a, b) => pinnedLeftOrder.indexOf(a.columnId) - pinnedLeftOrder.indexOf(b.columnId)
4232
- );
4233
- rightPinned.sort(
4234
- (a, b) => pinnedRightOrder.indexOf(a.columnId) - pinnedRightOrder.indexOf(b.columnId)
4235
- );
4236
- return [...leftPinned, ...groups, ...rightPinned];
4237
- }, [columnGroups, collapsedGroups, columns, pinnedColumns]);
4238
- return /* @__PURE__ */ jsxs13("div", { className: cn("flex flex-col h-full bg-white", className), children: [
4239
- showToolbar && /* @__PURE__ */ jsx13(
5187
+ return items;
5188
+ }, [columnGroups, collapsedGroups, visibleColumns]);
5189
+ return /* @__PURE__ */ jsxs14("div", { className: cn("flex flex-col h-full bg-white", className), children: [
5190
+ showToolbar && /* @__PURE__ */ jsx14(
4240
5191
  SpreadsheetToolbar,
4241
5192
  {
4242
5193
  zoom,
@@ -4271,386 +5222,405 @@ function Spreadsheet({
4271
5222
  menuItems: toolbarMenuItems
4272
5223
  }
4273
5224
  ),
4274
- /* @__PURE__ */ jsx13("div", { ref: tableRef, className: "flex-1 overflow-auto border border-gray-200 rounded", children: /* @__PURE__ */ jsx13(
4275
- "div",
4276
- {
4277
- style: {
4278
- zoom: zoom / 100
4279
- },
4280
- children: /* @__PURE__ */ jsxs13("table", { className: "border-separate border-spacing-0 text-xs select-none", children: [
4281
- /* @__PURE__ */ jsxs13("thead", { children: [
4282
- columnGroups && groupHeaderItems && /* @__PURE__ */ jsxs13("tr", { children: [
4283
- /* @__PURE__ */ jsx13(
4284
- RowIndexColumnHeader,
5225
+ /* @__PURE__ */ jsxs14("div", { ref: tableRef, className: cn("flex-1 overflow-auto border border-gray-200 rounded spreadsheet-scroll-container relative", isResizing && "select-none"), onMouseUp: handleMouseUp, children: [
5226
+ isProcessing && /* @__PURE__ */ jsx14("div", { className: "spreadsheet-processing-overlay", children: /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2 text-gray-500", children: [
5227
+ /* @__PURE__ */ jsx14("div", { className: "w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" }),
5228
+ "Processing..."
5229
+ ] }) }),
5230
+ /* @__PURE__ */ jsxs14("table", { ref: measureRef, className: "border-separate border-spacing-0 text-sm select-none", style: zoom !== 100 ? { zoom: zoom / 100 } : void 0, children: [
5231
+ /* @__PURE__ */ jsxs14("thead", { className: "sticky top-0", style: { zIndex: 50 }, children: [
5232
+ columnGroups && groupHeaderItems && /* @__PURE__ */ jsxs14("tr", { children: [
5233
+ /* @__PURE__ */ jsx14(
5234
+ RowIndexColumnHeader,
5235
+ {
5236
+ highlightColor: rowIndexHighlightColor,
5237
+ isPinned: isRowIndexPinned,
5238
+ isSecondRow: true,
5239
+ compactMode: effectiveCompactMode
5240
+ }
5241
+ ),
5242
+ groupHeaderItems.map((item) => {
5243
+ const { group, colSpan, isCollapsed } = item;
5244
+ return /* @__PURE__ */ jsx14(
5245
+ "th",
4285
5246
  {
4286
- highlightColor: rowIndexHighlightColor,
4287
- isPinned: isRowIndexPinned,
4288
- isSecondRow: true,
4289
- compactMode: effectiveCompactMode
4290
- }
4291
- ),
4292
- groupHeaderItems.map((item) => {
4293
- if (item.type === "pinned-column") {
4294
- const col = columns.find((c) => c.id === item.columnId);
4295
- const isPinnedLeft = item.pinSide === "left";
4296
- const pinnedWidth = Math.max(
4297
- col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH,
4298
- MIN_PINNED_COLUMN_WIDTH
4299
- );
4300
- return /* @__PURE__ */ jsx13(
4301
- "th",
4302
- {
4303
- className: cn(
4304
- "border border-gray-200 px-2 py-1.5 text-center font-bold text-gray-700",
4305
- "z-30"
4306
- ),
4307
- style: {
4308
- backgroundColor: item.headerColor || "rgb(243 244 246)",
4309
- position: "sticky",
4310
- left: isPinnedLeft ? `${getColumnLeftOffset(item.columnId)}px` : void 0,
4311
- right: !isPinnedLeft ? `${getColumnRightOffset(item.columnId)}px` : void 0,
4312
- minWidth: pinnedWidth
4313
- }
4314
- },
4315
- `pinned-group-${item.columnId}`
4316
- );
4317
- }
4318
- const { group, colSpan, isCollapsed } = item;
4319
- return /* @__PURE__ */ jsx13(
5247
+ colSpan,
5248
+ className: cn(
5249
+ "border border-gray-200 px-2 py-1.5 text-center font-bold text-gray-700",
5250
+ group.collapsible && "cursor-pointer hover:bg-gray-100"
5251
+ ),
5252
+ style: {
5253
+ backgroundColor: group.headerColor || "rgb(243 244 246)"
5254
+ },
5255
+ onClick: () => group.collapsible && handleToggleGroupCollapse(group.id),
5256
+ children: /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-center gap-1", children: [
5257
+ group.collapsible && (isCollapsed ? /* @__PURE__ */ jsx14(HiChevronRight, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx14(HiChevronDown, { className: "h-3 w-3" })),
5258
+ /* @__PURE__ */ jsx14("span", { children: group.label })
5259
+ ] })
5260
+ },
5261
+ group.id
5262
+ );
5263
+ })
5264
+ ] }),
5265
+ /* @__PURE__ */ jsxs14("tr", { children: [
5266
+ /* @__PURE__ */ jsx14(
5267
+ RowIndexColumnHeader,
5268
+ {
5269
+ enableHighlighting,
5270
+ highlightColor: rowIndexHighlightColor,
5271
+ isPinned: isRowIndexPinned,
5272
+ onHighlightClick: handleRowIndexHighlightClick,
5273
+ onPinClick: () => handleTogglePin(ROW_INDEX_COLUMN_ID),
5274
+ compactMode: effectiveCompactMode
5275
+ }
5276
+ ),
5277
+ columnRenderItems.map((item) => {
5278
+ if (item.type === "collapsed-placeholder") {
5279
+ return /* @__PURE__ */ jsx14(
4320
5280
  "th",
4321
5281
  {
4322
- colSpan,
4323
- className: cn(
4324
- "border border-gray-200 px-2 py-1.5 text-center font-bold text-gray-700",
4325
- group.collapsible && "cursor-pointer hover:bg-gray-100"
4326
- ),
5282
+ className: "border border-gray-200 px-2 py-1 text-center text-gray-400",
4327
5283
  style: {
4328
- backgroundColor: group.headerColor || "rgb(243 244 246)"
5284
+ backgroundColor: item.headerColor || "rgb(243 244 246)",
5285
+ minWidth: "30px"
4329
5286
  },
4330
- onClick: () => group.collapsible && handleToggleGroupCollapse(group.id),
4331
- children: /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-center gap-1", children: [
4332
- group.collapsible && (isCollapsed ? /* @__PURE__ */ jsx13(HiChevronRight, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx13(HiChevronDown, { className: "h-3 w-3" })),
4333
- /* @__PURE__ */ jsx13("span", { children: group.label })
4334
- ] })
5287
+ children: "..."
4335
5288
  },
4336
- group.id
5289
+ `${item.groupId}-placeholder`
4337
5290
  );
4338
- })
4339
- ] }),
4340
- /* @__PURE__ */ jsxs13("tr", { children: [
4341
- /* @__PURE__ */ jsx13(
4342
- RowIndexColumnHeader,
5291
+ }
5292
+ const column = item.column;
5293
+ const isPinnedLeft = isColumnPinned(column.id) && getColumnPinSide(column.id) === "left";
5294
+ const isPinnedRight = isColumnPinned(column.id) && getColumnPinSide(column.id) === "right";
5295
+ return /* @__PURE__ */ jsx14(
5296
+ MemoizedSpreadsheetHeader,
4343
5297
  {
4344
- enableHighlighting,
4345
- highlightColor: rowIndexHighlightColor,
4346
- isPinned: isRowIndexPinned,
4347
- onHighlightClick: handleRowIndexHighlightClick,
4348
- onPinClick: () => handleTogglePin(ROW_INDEX_COLUMN_ID),
4349
- compactMode: effectiveCompactMode
4350
- }
4351
- ),
4352
- columnRenderItems.map((item) => {
4353
- if (item.type === "collapsed-placeholder") {
4354
- return /* @__PURE__ */ jsx13(
4355
- "th",
4356
- {
4357
- className: "border border-gray-200 px-2 py-1 text-center text-gray-400 sticky z-20",
4358
- style: {
4359
- backgroundColor: item.headerColor || "rgb(243 244 246)",
4360
- minWidth: "30px",
4361
- top: 0
4362
- },
4363
- children: "..."
4364
- },
4365
- `${item.groupId}-placeholder`
4366
- );
4367
- }
4368
- const column = item.column;
4369
- const isPinnedLeft = isColumnPinned(column.id) && getColumnPinSide(column.id) === "left";
4370
- const isPinnedRight = isColumnPinned(column.id) && getColumnPinSide(column.id) === "right";
4371
- return /* @__PURE__ */ jsx13(
4372
- SpreadsheetHeader,
4373
- {
4374
- column,
4375
- sortConfig,
4376
- hasActiveFilter: !!filters[column.id],
4377
- isPinned: isColumnPinned(column.id),
4378
- pinSide: getColumnPinSide(column.id),
4379
- leftOffset: isPinnedLeft ? getColumnLeftOffset(column.id) : 0,
4380
- rightOffset: isPinnedRight ? getColumnRightOffset(column.id) : 0,
4381
- highlightColor: getColumnHighlight(column.id),
4382
- compactMode: effectiveCompactMode,
4383
- onClick: () => handleSort(column.id),
4384
- onFilterClick: () => setActiveFilterColumn(
4385
- activeFilterColumn === column.id ? null : column.id
4386
- ),
4387
- onPinClick: () => handleTogglePin(column.id),
4388
- onHighlightClick: enableHighlighting ? () => setHighlightPickerColumn(column.id) : void 0,
4389
- children: activeFilterColumn === column.id && /* @__PURE__ */ jsx13(
4390
- SpreadsheetFilterDropdown,
4391
- {
4392
- column,
4393
- filter: filters[column.id],
4394
- onFilterChange: (filter) => handleFilterChangeWithReset(
4395
- column.id,
4396
- filter
4397
- ),
4398
- onClose: () => setActiveFilterColumn(null)
5298
+ column,
5299
+ sortConfig,
5300
+ hasActiveFilter: !!filters[column.id],
5301
+ isPinned: isColumnPinned(column.id),
5302
+ pinSide: getColumnPinSide(column.id),
5303
+ pinnedZIndex: isColumnPinned(column.id) ? getPinnedZIndex(column.id) : void 0,
5304
+ leftOffset: isPinnedLeft ? getColumnLeftOffset(column.id) : 0,
5305
+ rightOffset: isPinnedRight ? getColumnRightOffset(column.id) : 0,
5306
+ highlightColor: getColumnHighlight(column.id),
5307
+ compactMode: effectiveCompactMode,
5308
+ onClick: () => {
5309
+ if (paginatedData.length > 0) {
5310
+ const firstRowId = getRowId(paginatedData[0]);
5311
+ const lastRowId = getRowId(paginatedData[paginatedData.length - 1]);
5312
+ const isAlreadySelected = selectedCellRange?.start.columnId === column.id && selectedCellRange?.end.columnId === column.id && selectedCellRange?.start.rowId === firstRowId && selectedCellRange?.end.rowId === lastRowId;
5313
+ if (isAlreadySelected) {
5314
+ setSelectedCellRange(null);
5315
+ setFocusedCell(null);
5316
+ } else {
5317
+ setSelectedCellRange({
5318
+ start: { rowId: firstRowId, columnId: column.id },
5319
+ end: { rowId: lastRowId, columnId: column.id }
5320
+ });
5321
+ setFocusedCell({ rowId: firstRowId, columnId: column.id });
4399
5322
  }
4400
- )
5323
+ }
4401
5324
  },
4402
- column.id
4403
- );
4404
- })
4405
- ] })
4406
- ] }),
4407
- /* @__PURE__ */ jsx13("tbody", { children: isLoading ? /* @__PURE__ */ jsx13("tr", { children: /* @__PURE__ */ jsx13(
4408
- "td",
4409
- {
4410
- colSpan: columnRenderItems.length + 1,
4411
- className: "text-center py-8 text-gray-500",
4412
- children: /* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-center gap-2", children: [
4413
- /* @__PURE__ */ jsx13("div", { className: "w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" }),
4414
- "Loading..."
4415
- ] })
4416
- }
4417
- ) }) : paginatedData.length === 0 ? /* @__PURE__ */ jsx13("tr", { children: /* @__PURE__ */ jsx13(
4418
- "td",
4419
- {
4420
- colSpan: columnRenderItems.length + 1,
4421
- className: "text-center py-8 text-gray-500",
4422
- children: emptyMessage
4423
- }
4424
- ) }) : paginatedData.map((row, rowIndex) => {
4425
- const rowId = getRowId(row);
4426
- const isRowSelected = selectedRows.has(rowId);
4427
- const isRowHovered = hoveredRow === rowId;
4428
- const rowHighlight = getRowHighlight(rowId);
4429
- const displayIndex = rowIndex + 1 + (currentPage - 1) * pageSize;
4430
- return /* @__PURE__ */ jsxs13(
4431
- "tr",
4432
- {
4433
- onMouseEnter: () => setHoveredRow(rowId),
4434
- onMouseLeave: () => setHoveredRow(null),
4435
- onClick: () => {
4436
- onRowClick?.(row, rowIndex);
4437
- },
4438
- onDoubleClick: () => onRowDoubleClick?.(row, rowIndex),
4439
- children: [
4440
- /* @__PURE__ */ jsx13(
4441
- "td",
5325
+ onSortClick: () => handleSort(column.id),
5326
+ onFilterClick: () => setActiveFilterColumn(
5327
+ activeFilterColumn === column.id ? null : column.id
5328
+ ),
5329
+ onPinClick: () => handleTogglePin(column.id),
5330
+ onHighlightClick: enableHighlighting ? () => setHighlightPickerColumn(column.id) : void 0,
5331
+ resizeHandleProps: getResizeHandleProps(
5332
+ column.id,
5333
+ column.width || column.minWidth || 100
5334
+ ),
5335
+ resolvedWidth: getColumnWidth(column.id),
5336
+ hasDuplicateCheck: duplicateCheckColumns.has(column.id),
5337
+ onDuplicateCheckClick: () => handleDuplicateCheckToggle(column.id),
5338
+ duplicateCount: getDuplicateColumnCount(column.id),
5339
+ children: activeFilterColumn === column.id && /* @__PURE__ */ jsx14(
5340
+ SpreadsheetFilterDropdown,
4442
5341
  {
4443
- onClick: (e) => handleRowSelect(rowId, e),
4444
- className: cn(
4445
- "border border-gray-200 text-center font-semibold cursor-pointer group",
4446
- effectiveCompactMode ? "text-[10px] px-1 py-px" : "text-xs px-2 py-1",
4447
- isRowIndexPinned ? "z-20" : "z-0",
4448
- isRowSelected && "bg-blue-100",
4449
- !isRowSelected && rowHighlight && "",
4450
- isRowHovered && !isRowSelected && !rowHighlight && "bg-gray-50"
5342
+ column,
5343
+ filter: filters[column.id],
5344
+ onFilterChange: (filter) => handleFilterChangeWithReset(
5345
+ column.id,
5346
+ filter
4451
5347
  ),
4452
- style: {
4453
- backgroundColor: rowHighlight?.color || (isRowSelected ? "#dbeafe" : isRowHovered ? "#f9fafb" : rowIndexHighlightColor || "white"),
4454
- minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
4455
- width: `${ROW_INDEX_COLUMN_WIDTH}px`,
4456
- ...isRowIndexPinned && {
4457
- position: "sticky",
4458
- left: 0
4459
- }
4460
- },
4461
- children: /* @__PURE__ */ jsxs13("div", { className: "relative flex items-center justify-center w-full h-full", children: [
4462
- /* @__PURE__ */ jsx13("span", { children: displayIndex }),
4463
- /* @__PURE__ */ jsxs13("div", { className: "absolute inset-0 flex items-center justify-between", children: [
4464
- rowContextMenuItems && rowContextMenuItems.length > 0 && /* @__PURE__ */ jsx13(
4465
- RowContextMenu,
4466
- {
4467
- row,
4468
- rowId,
4469
- items: rowContextMenuItems,
4470
- compactMode: effectiveCompactMode
4471
- }
4472
- ),
4473
- enableHighlighting && /* @__PURE__ */ jsx13(
4474
- "button",
4475
- {
4476
- type: "button",
4477
- onClick: (e) => {
4478
- e.stopPropagation();
4479
- setHighlightPickerRow(rowId);
4480
- },
4481
- className: "opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded",
4482
- title: "Highlight row",
4483
- children: /* @__PURE__ */ jsx13(
4484
- AiFillHighlight,
4485
- {
4486
- className: cn(
4487
- "h-2.5 w-2.5",
4488
- rowHighlight ? "text-amber-500" : "text-gray-500"
4489
- )
4490
- }
4491
- )
4492
- }
4493
- ),
4494
- enableComments && (cellHasComments(
5348
+ onClose: () => setActiveFilterColumn(null),
5349
+ hasDuplicateCheck: duplicateCheckColumns.has(column.id)
5350
+ }
5351
+ )
5352
+ },
5353
+ column.id
5354
+ );
5355
+ })
5356
+ ] })
5357
+ ] }),
5358
+ /* @__PURE__ */ jsx14("tbody", { children: isLoading ? /* @__PURE__ */ jsx14("tr", { children: /* @__PURE__ */ jsx14(
5359
+ "td",
5360
+ {
5361
+ colSpan: columnRenderItems.length + 1,
5362
+ className: "text-center py-8 text-gray-500",
5363
+ children: /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-center gap-2", children: [
5364
+ /* @__PURE__ */ jsx14("div", { className: "w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" }),
5365
+ "Loading..."
5366
+ ] })
5367
+ }
5368
+ ) }) : paginatedData.length === 0 ? /* @__PURE__ */ jsx14("tr", { children: /* @__PURE__ */ jsx14(
5369
+ "td",
5370
+ {
5371
+ colSpan: columnRenderItems.length + 1,
5372
+ className: "text-center py-8 text-gray-500",
5373
+ children: emptyMessage
5374
+ }
5375
+ ) }) : paginatedData.map((row, rowIndex) => {
5376
+ const rowId = getRowId(row);
5377
+ const isRowSelected = selectedRows.has(rowId);
5378
+ const rowHighlight = getRowHighlight(rowId);
5379
+ const displayIndex = rowIndex + 1 + (currentPage - 1) * pageSize;
5380
+ return /* @__PURE__ */ jsxs14(
5381
+ "tr",
5382
+ {
5383
+ onMouseEnter: () => {
5384
+ hoveredRowRef.current = rowId;
5385
+ },
5386
+ onMouseLeave: () => {
5387
+ hoveredRowRef.current = null;
5388
+ },
5389
+ onClick: () => {
5390
+ onRowClick?.(row, rowIndex);
5391
+ },
5392
+ onDoubleClick: () => onRowDoubleClick?.(row, rowIndex),
5393
+ children: [
5394
+ /* @__PURE__ */ jsx14(
5395
+ "td",
5396
+ {
5397
+ "data-column-id": "__row_index__",
5398
+ onClick: (e) => handleRowSelect(rowId, e),
5399
+ className: cn(
5400
+ "border border-gray-200 text-center font-semibold cursor-pointer group",
5401
+ effectiveCompactMode ? "text-xs px-1.5 py-0.5" : "text-sm px-2.5 py-1.5",
5402
+ "sticky",
5403
+ isRowSelected && "bg-blue-100",
5404
+ !isRowSelected && rowHighlight && ""
5405
+ ),
5406
+ style: {
5407
+ backgroundColor: rowHighlight?.color || (isRowSelected ? "#dbeafe" : rowIndexHighlightColor || (rowIndex % 2 !== 0 ? "#f9fafb" : "white")),
5408
+ minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
5409
+ width: `${ROW_INDEX_COLUMN_WIDTH}px`,
5410
+ position: "sticky",
5411
+ left: 0,
5412
+ zIndex: 40
5413
+ },
5414
+ children: /* @__PURE__ */ jsxs14("div", { className: "relative flex items-center w-full h-full", children: [
5415
+ /* @__PURE__ */ jsx14("span", { className: "pl-1", children: displayIndex }),
5416
+ /* @__PURE__ */ jsxs14("div", { className: "absolute inset-y-0 right-0 flex items-center gap-0.5 pr-0.5", children: [
5417
+ rowContextMenuItems && rowContextMenuItems.length > 0 && /* @__PURE__ */ jsx14(
5418
+ RowContextMenu,
5419
+ {
5420
+ row,
4495
5421
  rowId,
4496
- ROW_INDEX_COLUMN_ID
4497
- ) ? /* @__PURE__ */ jsxs13(
4498
- "button",
4499
- {
4500
- type: "button",
4501
- onClick: (e) => {
4502
- e.stopPropagation();
4503
- setViewCommentsCell({
4504
- rowId,
4505
- columnId: ROW_INDEX_COLUMN_ID
4506
- });
4507
- },
4508
- className: "p-0.5 bg-amber-100 hover:bg-amber-200 rounded transition-colors flex items-center gap-0.5",
4509
- title: `${getCellUnresolvedCommentCount(rowId, ROW_INDEX_COLUMN_ID)} comment(s) - click to view`,
4510
- children: [
4511
- /* @__PURE__ */ jsx13(FaComment, { className: "h-2.5 w-2.5 text-amber-500" }),
4512
- getCellUnresolvedCommentCount(
4513
- rowId,
4514
- ROW_INDEX_COLUMN_ID
4515
- ) > 0 && /* @__PURE__ */ jsx13("span", { className: "text-[9px] font-medium text-amber-600", children: getCellUnresolvedCommentCount(
4516
- rowId,
4517
- ROW_INDEX_COLUMN_ID
4518
- ) > 99 ? "99+" : getCellUnresolvedCommentCount(
4519
- rowId,
4520
- ROW_INDEX_COLUMN_ID
4521
- ) })
4522
- ]
4523
- }
4524
- ) : /* @__PURE__ */ jsx13(
5422
+ items: rowContextMenuItems,
5423
+ compactMode: effectiveCompactMode
5424
+ }
5425
+ ),
5426
+ enableHighlighting && /* @__PURE__ */ jsx14(
5427
+ "button",
5428
+ {
5429
+ type: "button",
5430
+ onClick: (e) => {
5431
+ e.stopPropagation();
5432
+ setHighlightPickerRow(rowId);
5433
+ },
5434
+ className: "opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded",
5435
+ title: "Highlight row",
5436
+ children: /* @__PURE__ */ jsx14(
5437
+ AiFillHighlight,
5438
+ {
5439
+ className: cn(
5440
+ "h-2.5 w-2.5",
5441
+ rowHighlight ? "text-amber-500" : "text-gray-500"
5442
+ )
5443
+ }
5444
+ )
5445
+ }
5446
+ ),
5447
+ enableComments && (cellHasComments(
5448
+ rowId,
5449
+ ROW_INDEX_COLUMN_ID
5450
+ ) ? /* @__PURE__ */ jsxs14(
5451
+ "button",
5452
+ {
5453
+ type: "button",
5454
+ onClick: (e) => {
5455
+ e.stopPropagation();
5456
+ setViewCommentsCell({
5457
+ rowId,
5458
+ columnId: ROW_INDEX_COLUMN_ID
5459
+ });
5460
+ },
5461
+ className: "p-0.5 bg-amber-100 hover:bg-amber-200 rounded transition-colors flex items-center gap-0.5",
5462
+ title: `${getCellUnresolvedCommentCount(rowId, ROW_INDEX_COLUMN_ID)} comment(s) - click to view`,
5463
+ children: [
5464
+ /* @__PURE__ */ jsx14(FaComment, { className: "h-2.5 w-2.5 text-amber-500" }),
5465
+ getCellUnresolvedCommentCount(
5466
+ rowId,
5467
+ ROW_INDEX_COLUMN_ID
5468
+ ) > 0 && /* @__PURE__ */ jsx14("span", { className: "text-[9px] font-medium text-amber-600", children: getCellUnresolvedCommentCount(
5469
+ rowId,
5470
+ ROW_INDEX_COLUMN_ID
5471
+ ) > 99 ? "99+" : getCellUnresolvedCommentCount(
5472
+ rowId,
5473
+ ROW_INDEX_COLUMN_ID
5474
+ ) })
5475
+ ]
5476
+ }
5477
+ ) : /* @__PURE__ */ jsx14(
5478
+ "button",
5479
+ {
5480
+ type: "button",
5481
+ onClick: (e) => {
5482
+ e.stopPropagation();
5483
+ setCommentModalCell({
5484
+ rowId,
5485
+ columnId: ROW_INDEX_COLUMN_ID
5486
+ });
5487
+ },
5488
+ className: "opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded",
5489
+ title: "Add comment",
5490
+ children: /* @__PURE__ */ jsx14(FaRegComment, { className: "h-2.5 w-2.5 text-gray-500" })
5491
+ }
5492
+ )),
5493
+ rowActions?.map((action) => {
5494
+ if (action.visible && !action.visible(row))
5495
+ return null;
5496
+ return /* @__PURE__ */ jsx14(
4525
5497
  "button",
4526
5498
  {
4527
5499
  type: "button",
4528
5500
  onClick: (e) => {
4529
5501
  e.stopPropagation();
4530
- setCommentModalCell({
4531
- rowId,
4532
- columnId: ROW_INDEX_COLUMN_ID
4533
- });
5502
+ action.onClick(row, rowId);
4534
5503
  },
4535
- className: "opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded",
4536
- title: "Add comment",
4537
- children: /* @__PURE__ */ jsx13(FaRegComment, { className: "h-2.5 w-2.5 text-gray-500" })
4538
- }
4539
- )),
4540
- rowActions?.map((action) => {
4541
- if (action.visible && !action.visible(row))
4542
- return null;
4543
- return /* @__PURE__ */ jsx13(
4544
- "button",
4545
- {
4546
- type: "button",
4547
- onClick: (e) => {
4548
- e.stopPropagation();
4549
- action.onClick(row, rowId);
4550
- },
4551
- className: cn(
4552
- "opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:bg-gray-200 rounded",
4553
- action.className
4554
- ),
4555
- title: action.tooltip,
4556
- children: action.icon
4557
- },
4558
- action.id
4559
- );
4560
- })
4561
- ] })
5504
+ className: cn(
5505
+ "opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:bg-gray-200 rounded",
5506
+ action.className
5507
+ ),
5508
+ title: action.tooltip,
5509
+ children: action.icon
5510
+ },
5511
+ action.id
5512
+ );
5513
+ })
4562
5514
  ] })
4563
- }
4564
- ),
4565
- columnRenderItems.map((item) => {
4566
- if (item.type === "collapsed-placeholder") {
4567
- return /* @__PURE__ */ jsx13(
4568
- "td",
4569
- {
4570
- className: "border border-gray-200 px-2 py-1 text-center text-gray-300",
4571
- style: {
4572
- backgroundColor: item.headerColor || "rgb(243 244 246)"
4573
- }
4574
- },
4575
- `${item.groupId}-placeholder`
4576
- );
4577
- }
4578
- const column = item.column;
4579
- const value = column.getValue ? column.getValue(row) : row[column.id];
4580
- const isEditing = editingCell?.rowId === rowId && editingCell?.columnId === column.id;
4581
- const isFocused = focusedCell?.rowId === rowId && focusedCell?.columnId === column.id;
4582
- const isInSelection = isCellInSelection(
4583
- rowId,
4584
- column.id
5515
+ ] })
5516
+ }
5517
+ ),
5518
+ columnRenderItems.map((item) => {
5519
+ if (item.type === "collapsed-placeholder") {
5520
+ return /* @__PURE__ */ jsx14(
5521
+ "td",
5522
+ {
5523
+ className: "border border-gray-200 px-2 py-1 text-center text-gray-300",
5524
+ style: {
5525
+ backgroundColor: item.headerColor || "rgb(243 244 246)"
5526
+ }
5527
+ },
5528
+ `${item.groupId}-placeholder`
4585
5529
  );
4586
- const selectionEdge = getCellSelectionEdge(
5530
+ }
5531
+ const column = item.column;
5532
+ const value = column.getValue ? column.getValue(row) : row[column.id];
5533
+ const isEditing = editingCell?.rowId === rowId && editingCell?.columnId === column.id;
5534
+ const isFocused = focusedCell?.rowId === rowId && focusedCell?.columnId === column.id;
5535
+ const isInSelection = isCellInSelection(
5536
+ rowId,
5537
+ column.id
5538
+ );
5539
+ const selectionEdge = getCellSelectionEdge(
5540
+ rowId,
5541
+ column.id
5542
+ );
5543
+ const cellOrRowOrColumnHighlight = getCellHighlight(rowId, column.id) || rowHighlight?.color || getColumnHighlight(column.id);
5544
+ const isColPinned = isColumnPinned(column.id);
5545
+ const colPinSide = getColumnPinSide(column.id);
5546
+ return /* @__PURE__ */ jsx14(
5547
+ MemoizedSpreadsheetCell,
5548
+ {
5549
+ value,
5550
+ column,
5551
+ row,
5552
+ rowIndex,
4587
5553
  rowId,
4588
- column.id
4589
- );
4590
- const cellOrRowOrColumnHighlight = getCellHighlight(rowId, column.id) || rowHighlight?.color || getColumnHighlight(column.id);
4591
- const isColPinned = isColumnPinned(column.id);
4592
- const colPinSide = getColumnPinSide(column.id);
4593
- return /* @__PURE__ */ jsx13(
4594
- MemoizedSpreadsheetCell,
4595
- {
4596
- value,
4597
- column,
4598
- row,
4599
- rowIndex,
4600
- rowId,
4601
- isEditable: column.editable && enableCellEditing,
4602
- isEditing,
4603
- isFocused,
4604
- isInSelection,
4605
- selectionEdge,
4606
- isRowSelected,
4607
- isRowHovered,
4608
- highlightColor: cellOrRowOrColumnHighlight,
4609
- compactMode: effectiveCompactMode,
4610
- isPinned: isColPinned,
4611
- pinSide: colPinSide,
4612
- leftOffset: getColumnLeftOffset(column.id),
4613
- rightOffset: getColumnRightOffset(
4614
- column.id
4615
- ),
4616
- onClick: (e) => handleCellClick(rowId, column.id, e),
4617
- onConfirm: handleConfirmEdit,
4618
- onCancel: handleCancelEdit,
4619
- onHighlight: enableHighlighting ? () => {
4620
- setHighlightPickerCell({
4621
- rowId,
4622
- columnId: column.id
4623
- });
4624
- } : void 0,
4625
- hasComments: cellHasComments(
4626
- rowId,
4627
- column.id
4628
- ),
4629
- unresolvedCommentCount: getCellUnresolvedCommentCount(
4630
- rowId,
4631
- column.id
4632
- ),
4633
- onAddComment: enableComments ? () => setCommentModalCell({
4634
- rowId,
4635
- columnId: column.id
4636
- }) : void 0,
4637
- onViewComments: enableComments && cellHasComments(rowId, column.id) ? () => setViewCommentsCell({
5554
+ isEditable: column.editable && enableCellEditing,
5555
+ isEditing,
5556
+ isFocused,
5557
+ isInSelection,
5558
+ selectionEdge,
5559
+ isRowSelected,
5560
+ isRowHovered: false,
5561
+ highlightColor: cellOrRowOrColumnHighlight,
5562
+ isDuplicate: isCellDuplicate(rowId, column.id),
5563
+ compactMode: effectiveCompactMode,
5564
+ isOddRow: rowIndex % 2 !== 0,
5565
+ resolvedWidth: getColumnWidth(column.id),
5566
+ isPinned: isColPinned,
5567
+ pinSide: colPinSide,
5568
+ pinnedZIndex: isColPinned ? getPinnedZIndex(column.id) : void 0,
5569
+ leftOffset: getColumnLeftOffset(column.id),
5570
+ rightOffset: getColumnRightOffset(
5571
+ column.id
5572
+ ),
5573
+ onClick: (e) => handleCellClick(rowId, column.id, e),
5574
+ onDoubleClick: () => handleCellDoubleClick(rowId, column.id),
5575
+ onMouseEnter: () => handleCellMouseEnter(rowId, column.id),
5576
+ onConfirm: handleConfirmEdit,
5577
+ onCancel: handleCancelEdit,
5578
+ onHighlight: enableHighlighting ? () => {
5579
+ setHighlightPickerCell({
4638
5580
  rowId,
4639
5581
  columnId: column.id
4640
- }) : void 0
4641
- },
4642
- column.id
4643
- );
4644
- })
4645
- ]
4646
- },
4647
- rowId
4648
- );
4649
- }) })
4650
- ] })
5582
+ });
5583
+ } : void 0,
5584
+ hasComments: cellHasComments(
5585
+ rowId,
5586
+ column.id
5587
+ ),
5588
+ unresolvedCommentCount: getCellUnresolvedCommentCount(
5589
+ rowId,
5590
+ column.id
5591
+ ),
5592
+ onAddComment: enableComments ? () => setCommentModalCell({
5593
+ rowId,
5594
+ columnId: column.id
5595
+ }) : void 0,
5596
+ onViewComments: enableComments && cellHasComments(rowId, column.id) ? () => setViewCommentsCell({
5597
+ rowId,
5598
+ columnId: column.id
5599
+ }) : void 0
5600
+ },
5601
+ column.id
5602
+ );
5603
+ })
5604
+ ]
5605
+ },
5606
+ rowId
5607
+ );
5608
+ }) })
5609
+ ] })
5610
+ ] }),
5611
+ /* @__PURE__ */ jsx14(
5612
+ SelectionSummaryBar,
5613
+ {
5614
+ summary: selectionSummary,
5615
+ focusedCell,
5616
+ columns: visibleColumns,
5617
+ data: paginatedData,
5618
+ getRowId,
5619
+ currentPage,
5620
+ pageSize
4651
5621
  }
4652
- ) }),
4653
- showPagination && effectiveTotalItems > 0 && /* @__PURE__ */ jsx13(
5622
+ ),
5623
+ showPagination && effectiveTotalItems > 0 && /* @__PURE__ */ jsx14(
4654
5624
  Pagination,
4655
5625
  {
4656
5626
  currentPage,
@@ -4665,7 +5635,7 @@ function Spreadsheet({
4665
5635
  onPageSizeChange: handlePageSizeChange
4666
5636
  }
4667
5637
  ),
4668
- /* @__PURE__ */ jsx13(
5638
+ /* @__PURE__ */ jsx14(
4669
5639
  AddCommentModal,
4670
5640
  {
4671
5641
  isOpen: commentModalCell !== null,
@@ -4676,7 +5646,7 @@ function Spreadsheet({
4676
5646
  }
4677
5647
  }
4678
5648
  ),
4679
- /* @__PURE__ */ jsx13(
5649
+ /* @__PURE__ */ jsx14(
4680
5650
  ViewCommentsModal,
4681
5651
  {
4682
5652
  isOpen: viewCommentsCell !== null,
@@ -4691,29 +5661,32 @@ function Spreadsheet({
4691
5661
  onClose: () => setViewCommentsCell(null)
4692
5662
  }
4693
5663
  ),
4694
- highlightPickerRow !== null && /* @__PURE__ */ jsx13(
5664
+ highlightPickerRow !== null && /* @__PURE__ */ jsx14(
4695
5665
  ColorPickerPopover,
4696
5666
  {
4697
5667
  title: "Highlight Row",
4698
5668
  paletteType: "row",
5669
+ recentColors,
4699
5670
  onSelectColor: (color) => handleRowHighlightToggle(highlightPickerRow, color),
4700
5671
  onClose: () => setHighlightPickerRow(null)
4701
5672
  }
4702
5673
  ),
4703
- highlightPickerColumn !== null && /* @__PURE__ */ jsx13(
5674
+ highlightPickerColumn !== null && /* @__PURE__ */ jsx14(
4704
5675
  ColorPickerPopover,
4705
5676
  {
4706
5677
  title: highlightPickerColumn === ROW_INDEX_COLUMN_ID ? "Highlight Row Index Column" : `Highlight Column: ${columns.find((c) => c.id === highlightPickerColumn)?.label || ""}`,
4707
5678
  paletteType: "column",
5679
+ recentColors,
4708
5680
  onSelectColor: (color) => handleColumnHighlightToggle(highlightPickerColumn, color),
4709
5681
  onClose: () => setHighlightPickerColumn(null)
4710
5682
  }
4711
5683
  ),
4712
- highlightPickerCell !== null && /* @__PURE__ */ jsx13(
5684
+ highlightPickerCell !== null && /* @__PURE__ */ jsx14(
4713
5685
  ColorPickerPopover,
4714
5686
  {
4715
5687
  title: "Highlight Cell",
4716
5688
  paletteType: "row",
5689
+ recentColors,
4717
5690
  onSelectColor: (color) => handleCellHighlightToggle(
4718
5691
  highlightPickerCell.rowId,
4719
5692
  highlightPickerCell.columnId,
@@ -4722,7 +5695,7 @@ function Spreadsheet({
4722
5695
  onClose: () => setHighlightPickerCell(null)
4723
5696
  }
4724
5697
  ),
4725
- /* @__PURE__ */ jsx13(
5698
+ /* @__PURE__ */ jsx14(
4726
5699
  KeyboardShortcutsModal,
4727
5700
  {
4728
5701
  isOpen: showKeyboardShortcuts,
@@ -4730,7 +5703,7 @@ function Spreadsheet({
4730
5703
  shortcuts
4731
5704
  }
4732
5705
  ),
4733
- /* @__PURE__ */ jsx13(
5706
+ /* @__PURE__ */ jsx14(
4734
5707
  SpreadsheetSettingsModal,
4735
5708
  {
4736
5709
  isOpen: showSettingsModal,
@@ -4758,12 +5731,14 @@ function Spreadsheet({
4758
5731
  Spreadsheet.displayName = "Spreadsheet";
4759
5732
  export {
4760
5733
  ActiveFiltersDisplay,
5734
+ MemoizedSpreadsheetHeader,
4761
5735
  RowContextMenu,
4762
5736
  Spreadsheet,
4763
5737
  MemoizedSpreadsheetCell as SpreadsheetCell,
4764
5738
  SpreadsheetFilterDropdown,
4765
5739
  SpreadsheetHeader,
4766
5740
  SpreadsheetSettingsModal,
4767
- SpreadsheetToolbar
5741
+ SpreadsheetToolbar,
5742
+ useSpreadsheetDuplicates
4768
5743
  };
4769
5744
  //# sourceMappingURL=index.mjs.map