@pixerate/editor-react 0.17.0 → 0.18.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.
@@ -238,7 +238,7 @@ function useSpreadsheetEditor(options = {}) {
238
238
  }
239
239
 
240
240
  // src/spreadsheet/SpreadsheetEditor.tsx
241
- import { useRef as useRef7, useState as useState7, useMemo as useMemo4, useEffect as useEffect5, useCallback as useCallback3 } from "react";
241
+ import { useRef as useRef8, useState as useState8, useMemo as useMemo5, useEffect as useEffect6, useCallback as useCallback4 } from "react";
242
242
  import {
243
243
  indexToColName,
244
244
  getSpreadsheetMenuItems,
@@ -328,6 +328,7 @@ var SpreadsheetContextMenu = ({ items, x, y, onClose, onOpenPanel, ariaLabel = "
328
328
  };
329
329
 
330
330
  // src/spreadsheet/SpreadsheetGroupRow.tsx
331
+ import React2 from "react";
331
332
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
332
333
  var formatSummary = (value) => value === null ? "\u2013" : String(value);
333
334
  var SpreadsheetGroupRow = ({
@@ -335,9 +336,14 @@ var SpreadsheetGroupRow = ({
335
336
  columns,
336
337
  groupColId,
337
338
  onToggle,
338
- trailingCell
339
+ trailingCell,
340
+ labelColumns = columns,
341
+ leadingColumns,
342
+ leftSpace = 0,
343
+ rightSpace = 0
339
344
  }) => {
340
- const labelColId = columns.some((c) => c.column.id === groupColId) ? groupColId : columns[0]?.column.id;
345
+ const labelColId = labelColumns.some((c) => c.column.id === groupColId) ? groupColId : labelColumns[0]?.column.id;
346
+ const spacer = (key) => /* @__PURE__ */ jsx2("td", { "aria-hidden": "true", "data-spacer": "", style: { padding: 0 } }, key);
341
347
  return /* @__PURE__ */ jsxs2("tr", { className: "border-b border-border bg-muted/40 font-semibold", "data-group-key": group.key, style: { height: 28 }, children: [
342
348
  /* @__PURE__ */ jsx2("td", { className: "w-12 min-w-[3rem] sticky left-0 z-10 bg-muted border-r border-border text-center", children: /* @__PURE__ */ jsx2(
343
349
  "button",
@@ -353,18 +359,24 @@ var SpreadsheetGroupRow = ({
353
359
  children: group.collapsed ? "\u25B8" : "\u25BE"
354
360
  }
355
361
  ) }),
356
- columns.map(({ column }) => {
362
+ columns.map(({ column }, i) => {
357
363
  const summary = group.summaries[column.id];
358
- return /* @__PURE__ */ jsx2("td", { className: "border-r border-border px-2 py-1 truncate max-w-0 text-foreground", style: { maxWidth: 0, overflow: "hidden" }, children: column.id === labelColId ? /* @__PURE__ */ jsxs2("span", { className: "truncate", children: [
359
- group.label,
360
- " ",
361
- /* @__PURE__ */ jsxs2("span", { className: "font-normal text-muted-foreground", children: [
362
- "(",
363
- group.rowIndices.length,
364
- ")"
365
- ] })
366
- ] }) : summary !== void 0 ? /* @__PURE__ */ jsx2("span", { className: "truncate", "data-summary": column.id, children: formatSummary(summary) }) : null }, column.id);
364
+ const before = leadingColumns !== void 0 && i === leadingColumns && leftSpace > 0 ? spacer("space:left") : null;
365
+ return /* @__PURE__ */ jsxs2(React2.Fragment, { children: [
366
+ before,
367
+ /* @__PURE__ */ jsx2("td", { className: "border-r border-border px-2 py-1 truncate max-w-0 text-foreground", style: { maxWidth: 0, overflow: "hidden" }, children: column.id === labelColId ? /* @__PURE__ */ jsxs2("span", { className: "truncate", children: [
368
+ group.label,
369
+ " ",
370
+ /* @__PURE__ */ jsxs2("span", { className: "font-normal text-muted-foreground", children: [
371
+ "(",
372
+ group.rowIndices.length,
373
+ ")"
374
+ ] })
375
+ ] }) : summary !== void 0 ? /* @__PURE__ */ jsx2("span", { className: "truncate", "data-summary": column.id, children: formatSummary(summary) }) : null })
376
+ ] }, column.id);
367
377
  }),
378
+ leadingColumns !== void 0 && leadingColumns >= columns.length && leftSpace > 0 && spacer("space:left"),
379
+ rightSpace > 0 && spacer("space:right"),
368
380
  trailingCell && /* @__PURE__ */ jsx2("td", { className: "w-10 min-w-[2.5rem] bg-muted/10 border-r border-border" })
369
381
  ] });
370
382
  };
@@ -768,25 +780,96 @@ var SpreadsheetDataValidationPanel = ({ state, onClose }) => {
768
780
  ] });
769
781
  };
770
782
 
783
+ // src/spreadsheet/useSpreadsheetViewport.ts
784
+ import { useCallback as useCallback3, useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
785
+ import {
786
+ fullSpreadsheetViewWindow,
787
+ getSpreadsheetScrollToCell,
788
+ getSpreadsheetViewWindow,
789
+ measureSpreadsheetView,
790
+ sameSpreadsheetViewWindow,
791
+ shouldVirtualizeSpreadsheet
792
+ } from "@pixerate/editor/spreadsheet";
793
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect2 : useEffect4;
794
+ function useSpreadsheetViewport(scrollRef, view, options = {}) {
795
+ const metrics = useMemo3(() => measureSpreadsheetView(view), [view]);
796
+ const virtual = shouldVirtualizeSpreadsheet(view, options.virtualize ?? "auto");
797
+ const [viewport, setViewport] = useState5({ scrollTop: 0, scrollLeft: 0, width: 0, height: 0 });
798
+ const windowOptions = useMemo3(
799
+ () => ({ merges: options.merges, overscanRows: options.overscanRows, overscanColumns: options.overscanColumns }),
800
+ [options.merges, options.overscanRows, options.overscanColumns]
801
+ );
802
+ const win = useMemo3(
803
+ () => virtual ? getSpreadsheetViewWindow(metrics, viewport, windowOptions) : fullSpreadsheetViewWindow(metrics),
804
+ [virtual, metrics, viewport, windowOptions]
805
+ );
806
+ const latest = useRef5({ metrics, viewport, virtual, windowOptions });
807
+ latest.current = { metrics, viewport, virtual, windowOptions };
808
+ const readViewport = useCallback3(() => {
809
+ const el = scrollRef.current;
810
+ if (!el) return null;
811
+ return { scrollTop: el.scrollTop, scrollLeft: el.scrollLeft, width: el.clientWidth, height: el.clientHeight };
812
+ }, [scrollRef]);
813
+ const updateViewport = useCallback3((next) => {
814
+ setViewport((prev) => {
815
+ const { metrics: m, virtual: v, windowOptions: o } = latest.current;
816
+ if (!v) return prev;
817
+ const same = prev.width === next.width && prev.height === next.height && sameSpreadsheetViewWindow(getSpreadsheetViewWindow(m, prev, o), getSpreadsheetViewWindow(m, next, o));
818
+ return same ? prev : next;
819
+ });
820
+ }, []);
821
+ const onScroll = useCallback3(() => {
822
+ const next = readViewport();
823
+ if (next) updateViewport(next);
824
+ }, [readViewport, updateViewport]);
825
+ useIsomorphicLayoutEffect(() => {
826
+ if (!virtual) return;
827
+ onScroll();
828
+ const el = scrollRef.current;
829
+ if (!el || typeof ResizeObserver === "undefined") return;
830
+ const observer = new ResizeObserver(() => onScroll());
831
+ observer.observe(el);
832
+ return () => observer.disconnect();
833
+ }, [virtual, onScroll, scrollRef]);
834
+ const activeRow = options.activeCell?.row;
835
+ const activeCol = options.activeCell?.col;
836
+ useIsomorphicLayoutEffect(() => {
837
+ if (!virtual || activeRow === void 0 || activeCol === void 0) return;
838
+ const el = scrollRef.current;
839
+ const current = latest.current.viewport;
840
+ const measured = readViewport();
841
+ const base = measured ? { ...measured, scrollTop: Math.max(measured.scrollTop, 0), scrollLeft: Math.max(measured.scrollLeft, 0) } : current;
842
+ const target = getSpreadsheetScrollToCell(latest.current.metrics, base, { row: activeRow, col: activeCol });
843
+ if (!target) return;
844
+ const next = { ...base, ...target };
845
+ if (el) {
846
+ if (target.scrollTop !== void 0) el.scrollTop = target.scrollTop;
847
+ if (target.scrollLeft !== void 0) el.scrollLeft = target.scrollLeft;
848
+ }
849
+ updateViewport(next);
850
+ }, [virtual, activeRow, activeCol]);
851
+ return { metrics, window: win, onScroll };
852
+ }
853
+
771
854
  // src/spreadsheet/FormulaHints.tsx
772
- import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
855
+ import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useMemo as useMemo4, useRef as useRef6, useState as useState6 } from "react";
773
856
  import { applyFormulaSuggestion, getFormulaHints } from "@pixerate/editor/spreadsheet";
774
857
  import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
775
858
  var FormulaHints = ({ inputRef, value, onAccept, limit = 8, names }) => {
776
- const [caret, setCaret] = useState5(null);
777
- const [focused, setFocused] = useState5(false);
778
- const [selected, setSelected] = useState5(0);
779
- const [dismissed, setDismissed] = useState5(false);
780
- const [position, setPosition] = useState5(null);
781
- const pendingCaret = useRef5(null);
782
- const hints = useMemo3(
859
+ const [caret, setCaret] = useState6(null);
860
+ const [focused, setFocused] = useState6(false);
861
+ const [selected, setSelected] = useState6(0);
862
+ const [dismissed, setDismissed] = useState6(false);
863
+ const [position, setPosition] = useState6(null);
864
+ const pendingCaret = useRef6(null);
865
+ const hints = useMemo4(
783
866
  () => getFormulaHints(value, caret ?? value.length, { limit, names }),
784
867
  [value, caret, limit, names]
785
868
  );
786
869
  const suggestions = dismissed ? [] : hints.suggestions;
787
870
  const selectedIndex = Math.max(0, Math.min(selected, suggestions.length - 1));
788
871
  const visible = focused && value.trimStart().startsWith("=") && (suggestions.length > 0 || !!hints.activeCall);
789
- const latest = useRef5({ suggestions, selected: selectedIndex, value, caret, onAccept });
872
+ const latest = useRef6({ suggestions, selected: selectedIndex, value, caret, onAccept });
790
873
  latest.current = { suggestions, selected: selectedIndex, value, caret, onAccept };
791
874
  const accept = (index) => {
792
875
  const { suggestions: list, value: current, caret: at, onAccept: commit } = latest.current;
@@ -796,7 +879,7 @@ var FormulaHints = ({ inputRef, value, onAccept, limit = 8, names }) => {
796
879
  pendingCaret.current = next.caret;
797
880
  commit(next.value);
798
881
  };
799
- useEffect4(() => {
882
+ useEffect5(() => {
800
883
  const input = inputRef.current;
801
884
  if (!input) return;
802
885
  const syncCaret = () => setCaret(input.selectionStart ?? input.value.length);
@@ -846,7 +929,7 @@ var FormulaHints = ({ inputRef, value, onAccept, limit = 8, names }) => {
846
929
  input.removeEventListener("keydown", onKeyDown);
847
930
  };
848
931
  }, [inputRef]);
849
- useLayoutEffect2(() => {
932
+ useLayoutEffect3(() => {
850
933
  const input = inputRef.current;
851
934
  const at = pendingCaret.current;
852
935
  if (!input || at === null || input.value !== value) return;
@@ -855,7 +938,7 @@ var FormulaHints = ({ inputRef, value, onAccept, limit = 8, names }) => {
855
938
  setCaret(at);
856
939
  setSelected(0);
857
940
  }, [value, inputRef]);
858
- useLayoutEffect2(() => {
941
+ useLayoutEffect3(() => {
859
942
  const input = inputRef.current;
860
943
  if (!visible || !input) return;
861
944
  const rect = input.getBoundingClientRect();
@@ -919,16 +1002,16 @@ var FormulaHints = ({ inputRef, value, onAccept, limit = 8, names }) => {
919
1002
  };
920
1003
 
921
1004
  // src/spreadsheet/SheetTabs.tsx
922
- import { useRef as useRef6, useState as useState6 } from "react";
1005
+ import { useRef as useRef7, useState as useState7 } from "react";
923
1006
  import { getSheetTabMenuItems, sheetDropIndex } from "@pixerate/editor/spreadsheet";
924
1007
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
925
1008
  var SheetTabs = ({ state, readOnly = false, className = "" }) => {
926
1009
  const { controller, sheets } = state;
927
- const [renaming, setRenaming] = useState6(null);
928
- const [error, setError] = useState6(null);
929
- const [menu, setMenu] = useState6(null);
930
- const dragFrom = useRef6(null);
931
- const [dropTarget, setDropTarget] = useState6(null);
1010
+ const [renaming, setRenaming] = useState7(null);
1011
+ const [error, setError] = useState7(null);
1012
+ const [menu, setMenu] = useState7(null);
1013
+ const dragFrom = useRef7(null);
1014
+ const [dropTarget, setDropTarget] = useState7(null);
932
1015
  const commitRename = (cancelOnError) => {
933
1016
  if (!renaming) return;
934
1017
  const sheet = sheets.find((s) => s.id === renaming.id);
@@ -1125,30 +1208,59 @@ var SpreadsheetEditor = ({
1125
1208
  onAddColumnClick,
1126
1209
  onColumnHeaderClick,
1127
1210
  onColumnResize,
1128
- showSheetTabs = false
1211
+ showSheetTabs = false,
1212
+ virtualize = "auto"
1129
1213
  }) => {
1130
1214
  const defaultState = useSpreadsheetEditor();
1131
1215
  const state = externalState || defaultState;
1132
- const gridRef = useRef7(null);
1133
- const formulaBarRef = useRef7(null);
1134
- const cellInputRef = useRef7(null);
1135
- const [resizingColId, setResizingColId] = useState7(null);
1136
- const [hoveredColId, setHoveredColId] = useState7(null);
1137
- const [resizeStartX, setResizeStartX] = useState7(0);
1138
- const [resizeStartWidth, setResizeStartWidth] = useState7(0);
1216
+ const gridRef = useRef8(null);
1217
+ const scrollRef = useRef8(null);
1218
+ const formulaBarRef = useRef8(null);
1219
+ const cellInputRef = useRef8(null);
1220
+ const [resizingColId, setResizingColId] = useState8(null);
1221
+ const [hoveredColId, setHoveredColId] = useState8(null);
1222
+ const [resizeStartX, setResizeStartX] = useState8(0);
1223
+ const [resizeStartWidth, setResizeStartWidth] = useState8(0);
1139
1224
  const view = state.viewModel;
1225
+ const { window: viewWindow, onScroll: handleGridScroll } = useSpreadsheetViewport(scrollRef, view, {
1226
+ virtualize,
1227
+ merges: state.document.table?.merges,
1228
+ activeCell: state.activeCell
1229
+ });
1230
+ const columnSlots = useMemo5(() => {
1231
+ const col = (viewCol) => ({ kind: "col", viewCol });
1232
+ if (!viewWindow.virtual) return view.columns.map(col);
1233
+ const slots = view.columns.slice(0, viewWindow.frozenColumns).map(col);
1234
+ if (viewWindow.leftSpace > 0) slots.push({ kind: "space", key: "space:left", width: viewWindow.leftSpace });
1235
+ for (let i = viewWindow.colStart; i < viewWindow.colEnd; i++) slots.push(col(view.columns[i]));
1236
+ if (viewWindow.rightSpace > 0) slots.push({ kind: "space", key: "space:right", width: viewWindow.rightSpace });
1237
+ return slots;
1238
+ }, [view, viewWindow]);
1239
+ const renderedColumns = useMemo5(
1240
+ () => columnSlots.flatMap((slot) => slot.kind === "col" ? [slot.viewCol] : []),
1241
+ [columnSlots]
1242
+ );
1243
+ const rowSlots = useMemo5(() => {
1244
+ const row = (viewRow) => ({ kind: "row", viewRow });
1245
+ if (!viewWindow.virtual) return view.rows.map(row);
1246
+ const slots = view.rows.slice(0, viewWindow.frozenRows).map(row);
1247
+ if (viewWindow.topSpace > 0) slots.push({ kind: "space", key: "space:top", height: viewWindow.topSpace });
1248
+ for (let i = viewWindow.rowStart; i < viewWindow.rowEnd; i++) slots.push(row(view.rows[i]));
1249
+ if (viewWindow.bottomSpace > 0) slots.push({ kind: "space", key: "space:bottom", height: viewWindow.bottomSpace });
1250
+ return slots;
1251
+ }, [view, viewWindow]);
1140
1252
  const { copyShortcutFallback } = useSpreadsheetClipboard(gridRef, state, readOnly);
1141
1253
  const { fillTarget, startFill, extendFill } = useFillHandle(state);
1142
- const [menu, setMenu] = useState7(null);
1143
- const [panel, setPanel] = useState7(null);
1144
- const [listMenu, setListMenu] = useState7(null);
1254
+ const [menu, setMenu] = useState8(null);
1255
+ const [panel, setPanel] = useState8(null);
1256
+ const [listMenu, setListMenu] = useState8(null);
1145
1257
  const overlay = state.conditionalFormatOverlay;
1146
1258
  const filter = state.document.table?.filter;
1147
- const filteredColIds = useMemo4(
1259
+ const filteredColIds = useMemo5(
1148
1260
  () => new Set(filter?.enabled ? filter.rules.map((r) => r.colId) : []),
1149
1261
  [filter]
1150
1262
  );
1151
- const handleCellContextMenu = useCallback3((rIdx, cIdx, e) => {
1263
+ const handleCellContextMenu = useCallback4((rIdx, cIdx, e) => {
1152
1264
  if (readOnly) return;
1153
1265
  e.preventDefault();
1154
1266
  if (state.editingCell) state.commitEditing();
@@ -1157,33 +1269,33 @@ var SpreadsheetEditor = ({
1157
1269
  }
1158
1270
  setMenu({ x: e.clientX, y: e.clientY, items: getSpreadsheetMenuItems(state.controller, { row: rIdx, col: cIdx }) });
1159
1271
  }, [readOnly, state]);
1160
- const nameSuggestions = useMemo4(
1272
+ const nameSuggestions = useMemo5(
1161
1273
  () => state.controller.getNameSuggestions(),
1162
1274
  // eslint-disable-next-line react-hooks/exhaustive-deps
1163
1275
  [state.controller, state.document]
1164
1276
  );
1165
- const activeRawValue = useMemo4(() => {
1277
+ const activeRawValue = useMemo5(() => {
1166
1278
  if (!state.activeCell) return "";
1167
1279
  const row = state.document.rows[state.activeCell.row];
1168
1280
  const col = state.document.columns[state.activeCell.col];
1169
1281
  if (!row || !col) return "";
1170
1282
  return state.getCell(row.id, col.id).raw;
1171
1283
  }, [state.activeCell, state.document]);
1172
- const activeSpill = useMemo4(
1284
+ const activeSpill = useMemo5(
1173
1285
  () => state.activeCell ? state.controller.getSpillAt(state.activeCell.row, state.activeCell.col) : null,
1174
1286
  // eslint-disable-next-line react-hooks/exhaustive-deps
1175
1287
  [state.controller, state.activeCell, state.document]
1176
1288
  );
1177
1289
  const spillFormula = activeSpill && !activeRawValue && activeSpill.key !== coordKey(state.activeCell) ? activeSpill.formula : "";
1178
- const coordLabel = useMemo4(() => {
1290
+ const coordLabel = useMemo5(() => {
1179
1291
  if (!state.activeCell) return "--";
1180
1292
  const colName = indexToColName(state.activeCell.col);
1181
1293
  return `${colName}${state.activeCell.row + 1}`;
1182
1294
  }, [state.activeCell]);
1183
- const [formulaDraft, setFormulaDraft] = useState7(null);
1295
+ const [formulaDraft, setFormulaDraft] = useState8(null);
1184
1296
  const activeColumn = state.activeCell ? state.document.columns[state.activeCell.col] : void 0;
1185
1297
  const formulaBarReadOnly = readOnly || !state.activeCell || !!activeColumn?.readOnly;
1186
- useEffect5(() => {
1298
+ useEffect6(() => {
1187
1299
  setFormulaDraft(null);
1188
1300
  }, [state.activeCell?.row, state.activeCell?.col]);
1189
1301
  const commitFormulaBar = () => {
@@ -1199,19 +1311,19 @@ var SpreadsheetEditor = ({
1199
1311
  }
1200
1312
  setFormulaDraft(null);
1201
1313
  };
1202
- const handleCellClick = useCallback3((rIdx, cIdx, e) => {
1314
+ const handleCellClick = useCallback4((rIdx, cIdx, e) => {
1203
1315
  if (state.editingCell && (state.editingCell.row !== rIdx || state.editingCell.col !== cIdx)) {
1204
1316
  state.commitEditing();
1205
1317
  }
1206
1318
  state.selectCell(rIdx, cIdx, e.shiftKey);
1207
1319
  }, [state]);
1208
- const handleCellDblClick = useCallback3((rIdx, cIdx) => {
1320
+ const handleCellDblClick = useCallback4((rIdx, cIdx) => {
1209
1321
  if (readOnly) return;
1210
1322
  const col = state.document.columns[cIdx];
1211
1323
  if (col?.readOnly) return;
1212
1324
  state.startEditing(rIdx, cIdx);
1213
1325
  }, [readOnly, state]);
1214
- const handleKeyDown = useCallback3((e) => {
1326
+ const handleKeyDown = useCallback4((e) => {
1215
1327
  if (state.editingCell) {
1216
1328
  if (e.key === "Enter") {
1217
1329
  e.preventDefault();
@@ -1283,12 +1395,12 @@ var SpreadsheetEditor = ({
1283
1395
  }
1284
1396
  }
1285
1397
  }, [readOnly, state, copyShortcutFallback]);
1286
- const activeListValues = useMemo4(
1398
+ const activeListValues = useMemo5(
1287
1399
  () => state.activeCell && !readOnly ? state.controller.getValidationListValues(state.activeCell.row, state.activeCell.col) : null,
1288
1400
  // eslint-disable-next-line react-hooks/exhaustive-deps
1289
1401
  [state.controller, state.activeCell, state.document, readOnly]
1290
1402
  );
1291
- const activeRule = useMemo4(
1403
+ const activeRule = useMemo5(
1292
1404
  () => state.activeCell ? state.controller.getDataValidationAt(state.activeCell.row, state.activeCell.col) : void 0,
1293
1405
  // eslint-disable-next-line react-hooks/exhaustive-deps
1294
1406
  [state.controller, state.activeCell, state.document]
@@ -1302,7 +1414,7 @@ var SpreadsheetEditor = ({
1302
1414
  const rect = td?.getBoundingClientRect();
1303
1415
  setListMenu({ x: rect ? rect.left : 0, y: rect ? rect.bottom : 0, row, col, values });
1304
1416
  }
1305
- useEffect5(() => {
1417
+ useEffect6(() => {
1306
1418
  if (!listMenu) return;
1307
1419
  const close = (e) => {
1308
1420
  if (!e.target?.closest?.('[aria-label="Allowed values"], .spreadsheet-list-arrow')) setListMenu(null);
@@ -1325,11 +1437,11 @@ var SpreadsheetEditor = ({
1325
1437
  else gridRef.current?.focus();
1326
1438
  });
1327
1439
  };
1328
- const stateRef = useRef7(state);
1440
+ const stateRef = useRef8(state);
1329
1441
  stateRef.current = state;
1330
- const onColumnResizeRef = useRef7(onColumnResize);
1442
+ const onColumnResizeRef = useRef8(onColumnResize);
1331
1443
  onColumnResizeRef.current = onColumnResize;
1332
- useEffect5(() => {
1444
+ useEffect6(() => {
1333
1445
  if (!resizingColId) return;
1334
1446
  if (typeof document !== "undefined") {
1335
1447
  document.body.style.cursor = "col-resize";
@@ -1380,7 +1492,7 @@ var SpreadsheetEditor = ({
1380
1492
  window.removeEventListener("mouseup", handleMouseUp);
1381
1493
  };
1382
1494
  }, [resizingColId, resizeStartX, resizeStartWidth]);
1383
- const measureElementContentWidth = useCallback3((el, container) => {
1495
+ const measureElementContentWidth = useCallback4((el, container) => {
1384
1496
  if (typeof document === "undefined") return 0;
1385
1497
  let maxW = el.scrollWidth || 0;
1386
1498
  const firstChild = el.firstElementChild;
@@ -1428,7 +1540,7 @@ var SpreadsheetEditor = ({
1428
1540
  }
1429
1541
  return maxW;
1430
1542
  }, []);
1431
- const autoFitColumn = useCallback3((colId, colIndex) => {
1543
+ const autoFitColumn = useCallback4((colId, colIndex) => {
1432
1544
  const col = state.document.columns.find((c) => c.id === colId);
1433
1545
  if (!col) return;
1434
1546
  let calculatedWidth = 0;
@@ -1464,12 +1576,12 @@ var SpreadsheetEditor = ({
1464
1576
  onColumnResize(updatedCol.id, updatedCol.width || 130);
1465
1577
  }
1466
1578
  }, [state, measureElementContentWidth, onColumnResize]);
1467
- const handleResizeDblClick = useCallback3((colId, colIndex, e) => {
1579
+ const handleResizeDblClick = useCallback4((colId, colIndex, e) => {
1468
1580
  e.preventDefault();
1469
1581
  e.stopPropagation();
1470
1582
  autoFitColumn(colId, colIndex);
1471
1583
  }, [autoFitColumn]);
1472
- const handleHeaderDblClick = useCallback3((colId, colIndex, e) => {
1584
+ const handleHeaderDblClick = useCallback4((colId, colIndex, e) => {
1473
1585
  const target = e.currentTarget;
1474
1586
  if (target) {
1475
1587
  const rect = target.getBoundingClientRect();
@@ -1554,10 +1666,19 @@ var SpreadsheetEditor = ({
1554
1666
  inputMessage.inputTitle && /* @__PURE__ */ jsx7("span", { className: "mr-1 font-semibold text-foreground", children: inputMessage.inputTitle }),
1555
1667
  inputMessage.inputMessage
1556
1668
  ] }),
1557
- /* @__PURE__ */ jsx7("div", { className: "relative flex-1 overflow-auto", children: /* @__PURE__ */ jsxs7("table", { className: "border-collapse table-fixed w-max min-w-full", children: [
1669
+ /* @__PURE__ */ jsx7("div", { ref: scrollRef, className: "relative flex-1 overflow-auto", onScroll: handleGridScroll, children: /* @__PURE__ */ jsxs7("table", { className: "border-separate border-spacing-0 table-fixed w-max min-w-full [&>*>tr:not([data-spacer])>*]:border-b [&>*>tr>*]:border-border", children: [
1558
1670
  /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { className: "h-7 bg-muted sticky top-0 z-20 border-b border-border shadow-xs", children: [
1559
1671
  /* @__PURE__ */ jsx7("th", { className: "w-12 min-w-[3rem] sticky left-0 z-30 bg-muted border-r border-border text-center text-[10px] text-muted-foreground font-medium", children: "#" }),
1560
- view.columns.map(({ column: col, index: cIdx, frozen, stickyLeft }) => /* @__PURE__ */ jsxs7(
1672
+ columnSlots.map((slot) => slot.kind === "space" ? /* @__PURE__ */ jsx7(
1673
+ "th",
1674
+ {
1675
+ "aria-hidden": "true",
1676
+ "data-spacer": "",
1677
+ className: "p-0",
1678
+ style: { width: slot.width, minWidth: slot.width, maxWidth: slot.width, padding: 0 }
1679
+ },
1680
+ slot.key
1681
+ ) : (({ column: col, index: cIdx, frozen, stickyLeft }) => /* @__PURE__ */ jsxs7(
1561
1682
  "th",
1562
1683
  {
1563
1684
  "data-col": cIdx,
@@ -1616,7 +1737,7 @@ var SpreadsheetEditor = ({
1616
1737
  ]
1617
1738
  },
1618
1739
  col.id
1619
- )),
1740
+ ))(slot.viewCol)),
1620
1741
  onAddColumnClick && /* @__PURE__ */ jsx7(
1621
1742
  "th",
1622
1743
  {
@@ -1627,13 +1748,21 @@ var SpreadsheetEditor = ({
1627
1748
  }
1628
1749
  )
1629
1750
  ] }) }),
1630
- /* @__PURE__ */ jsx7("tbody", { children: view.rows.map((viewRow) => {
1751
+ /* @__PURE__ */ jsx7("tbody", { children: rowSlots.map((rowSlot) => {
1752
+ if (rowSlot.kind === "space") {
1753
+ return /* @__PURE__ */ jsx7("tr", { "aria-hidden": "true", "data-spacer": "", style: { height: rowSlot.height }, children: /* @__PURE__ */ jsx7("td", { colSpan: columnSlots.length + 2, style: { padding: 0, border: 0 } }) }, rowSlot.key);
1754
+ }
1755
+ const viewRow = rowSlot.viewRow;
1631
1756
  if (viewRow.kind === "group") {
1632
1757
  return /* @__PURE__ */ jsx7(
1633
1758
  SpreadsheetGroupRow,
1634
1759
  {
1635
1760
  group: viewRow.group,
1636
- columns: view.columns,
1761
+ columns: renderedColumns,
1762
+ labelColumns: view.columns,
1763
+ leadingColumns: viewWindow.virtual ? viewWindow.frozenColumns : void 0,
1764
+ leftSpace: viewWindow.leftSpace,
1765
+ rightSpace: viewWindow.rightSpace,
1637
1766
  groupColId: state.document.table?.grouping?.colId ?? "",
1638
1767
  onToggle: state.toggleGroupCollapsed,
1639
1768
  trailingCell: !!onAddColumnClick
@@ -1658,7 +1787,9 @@ var SpreadsheetEditor = ({
1658
1787
  children: displayRowIndex + 1
1659
1788
  }
1660
1789
  ),
1661
- view.columns.map((viewCol) => {
1790
+ columnSlots.map((slot) => {
1791
+ if (slot.kind === "space") return /* @__PURE__ */ jsx7("td", { "aria-hidden": "true", "data-spacer": "", style: { padding: 0 } }, slot.key);
1792
+ const viewCol = slot.viewCol;
1662
1793
  const span = view.getCellSpan(displayRowIndex, viewCol.index);
1663
1794
  if (!span) return null;
1664
1795
  const rIdx = span.source.row;
@@ -1841,19 +1972,19 @@ var SpreadsheetEditor = ({
1841
1972
  };
1842
1973
 
1843
1974
  // src/spreadsheet/NameManager.tsx
1844
- import { useMemo as useMemo5, useState as useState8 } from "react";
1975
+ import { useMemo as useMemo6, useState as useState9 } from "react";
1845
1976
  import { getNameManagerRows, parseFormula } from "@pixerate/editor/spreadsheet";
1846
1977
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1847
1978
  var EMPTY_DRAFT = { original: null, name: "", refersTo: "=", scope: "", comment: "" };
1848
1979
  var NameManager = ({ state, readOnly = false, onClose, className = "" }) => {
1849
1980
  const { controller, sheets } = state;
1850
- const rows = useMemo5(
1981
+ const rows = useMemo6(
1851
1982
  () => getNameManagerRows(controller),
1852
1983
  // eslint-disable-next-line react-hooks/exhaustive-deps
1853
1984
  [controller, state.document, state.names]
1854
1985
  );
1855
- const [draft, setDraft] = useState8(EMPTY_DRAFT);
1856
- const [error, setError] = useState8(null);
1986
+ const [draft, setDraft] = useState9(EMPTY_DRAFT);
1987
+ const [error, setError] = useState9(null);
1857
1988
  const edit = (row) => {
1858
1989
  setError(null);
1859
1990
  setDraft({
@@ -2004,9 +2135,10 @@ export {
2004
2135
  SpreadsheetValidationDialog,
2005
2136
  SpreadsheetConditionalFormatPanel,
2006
2137
  SpreadsheetDataValidationPanel,
2138
+ useSpreadsheetViewport,
2007
2139
  FormulaHints,
2008
2140
  SheetTabs,
2009
2141
  SpreadsheetEditor,
2010
2142
  NameManager
2011
2143
  };
2012
- //# sourceMappingURL=chunk-JR5ZBDR7.js.map
2144
+ //# sourceMappingURL=chunk-34HEZX7J.js.map