@geomak/ui 7.10.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6503,6 +6503,7 @@ function DataGrid({
6503
6503
  virtualize = true,
6504
6504
  overscan = 4,
6505
6505
  rowNumbers = true,
6506
+ trailingRows = 0,
6506
6507
  onCellEdit,
6507
6508
  className = "",
6508
6509
  style,
@@ -6539,7 +6540,9 @@ function DataGrid({
6539
6540
  if (sortProp === void 0) setInternalSort(next);
6540
6541
  onSortChange?.(next);
6541
6542
  };
6542
- const totalHeight = rows.length * rowHeight;
6543
+ const displayRowCount = rows.length + Math.max(0, trailingRows);
6544
+ const totalHeight = displayRowCount * rowHeight;
6545
+ const rowIndexForDisp = (disp) => disp < rows.length ? order[disp] : disp;
6543
6546
  React36.useEffect(() => {
6544
6547
  const el = scrollRef.current;
6545
6548
  if (!el || typeof ResizeObserver === "undefined") return;
@@ -6551,7 +6554,7 @@ function DataGrid({
6551
6554
  }, []);
6552
6555
  const bodyH = (viewport.h || (typeof height === "number" ? height : 480)) - headerHeight;
6553
6556
  const rowStart = virtualize ? Math.max(0, Math.floor(scroll.top / rowHeight) - overscan) : 0;
6554
- const rowEnd = virtualize ? Math.min(rows.length, Math.ceil((scroll.top + bodyH) / rowHeight) + overscan) : rows.length;
6557
+ const rowEnd = virtualize ? Math.min(displayRowCount, Math.ceil((scroll.top + bodyH) / rowHeight) + overscan) : displayRowCount;
6555
6558
  let colStart = 0;
6556
6559
  let colEnd = columns.length;
6557
6560
  if (virtualize && viewport.w) {
@@ -6569,13 +6572,14 @@ function DataGrid({
6569
6572
  const commit = React36.useCallback(() => {
6570
6573
  if (!editing) return;
6571
6574
  const col = columns[editing.col];
6572
- onCellEdit?.({ row: order[editing.disp], column: col.key, value: draft });
6575
+ const ri = editing.disp < rows.length ? order[editing.disp] : editing.disp;
6576
+ onCellEdit?.({ row: ri, column: col.key, value: draft });
6573
6577
  setEditing(null);
6574
- }, [editing, columns, draft, onCellEdit, order]);
6578
+ }, [editing, columns, draft, onCellEdit, order, rows.length]);
6575
6579
  const startEdit = (disp, col) => {
6576
6580
  const c = columns[col];
6577
6581
  if (!(c.editable ?? editable)) return;
6578
- setDraft(displayValue(rows[order[disp]]?.[c.key] ?? ""));
6582
+ setDraft(displayValue(rows[rowIndexForDisp(disp)]?.[c.key] ?? ""));
6579
6583
  setEditing({ disp, col });
6580
6584
  };
6581
6585
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -6638,7 +6642,7 @@ function DataGrid({
6638
6642
  `g-${disp}`
6639
6643
  )),
6640
6644
  visibleRows.map((disp) => {
6641
- const ri = order[disp];
6645
+ const ri = rowIndexForDisp(disp);
6642
6646
  return visibleCols.map((ci) => {
6643
6647
  const c = columns[ci];
6644
6648
  const isEditing = editing?.disp === disp && editing?.col === ci;
@@ -6681,7 +6685,7 @@ function DataGrid({
6681
6685
  });
6682
6686
  })
6683
6687
  ] }),
6684
- rows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center text-foreground-muted", style: { top: headerHeight }, children: emptyState })
6688
+ displayRowCount === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center text-foreground-muted", style: { top: headerHeight }, children: emptyState })
6685
6689
  ]
6686
6690
  }
6687
6691
  );
@@ -6699,6 +6703,18 @@ function cellValue(v) {
6699
6703
  if (v != null && typeof v === "object" && "value" in v) return v.value;
6700
6704
  return v;
6701
6705
  }
6706
+ function columnLetter(i) {
6707
+ let s = "";
6708
+ let n = i;
6709
+ do {
6710
+ s = String.fromCharCode(65 + n % 26) + s;
6711
+ n = Math.floor(n / 26) - 1;
6712
+ } while (n >= 0);
6713
+ return s;
6714
+ }
6715
+ function blankSheet(name, cols = 8) {
6716
+ return { name, columns: Array.from({ length: cols }, (_, i) => ({ key: columnLetter(i), label: columnLetter(i) })), rows: [] };
6717
+ }
6702
6718
  function toPlainRows(sheet, columns) {
6703
6719
  return sheet.rows.map((row) => {
6704
6720
  const out = {};
@@ -6731,11 +6747,14 @@ function Spreadsheet({
6731
6747
  fileName,
6732
6748
  virtualize = true,
6733
6749
  sortable = true,
6750
+ emptyRows = 50,
6751
+ allowAddSheet,
6734
6752
  height = 480,
6735
6753
  width,
6736
6754
  className = "",
6737
6755
  style
6738
6756
  }) {
6757
+ const canAddSheet = allowAddSheet ?? editable;
6739
6758
  const [sheets, setSheets] = React36.useState(Array.isArray(source) ? source : null);
6740
6759
  const [active, setActive] = React36.useState(0);
6741
6760
  const [status, setStatus] = React36.useState(Array.isArray(source) ? "ready" : "loading");
@@ -6795,6 +6814,7 @@ function Spreadsheet({
6795
6814
  if (!prev) return prev;
6796
6815
  const next = prev.map((s, i) => i === active ? { ...s, rows: s.rows.map((r) => ({ ...r })) } : s);
6797
6816
  const target = next[active];
6817
+ while (target.rows.length <= row) target.rows.push({});
6798
6818
  const existing = target.rows[row]?.[column];
6799
6819
  const prevValue = cellValue(existing);
6800
6820
  coerced = coerceToCellType(prevValue, value);
@@ -6808,6 +6828,25 @@ function Spreadsheet({
6808
6828
  });
6809
6829
  onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value: coerced });
6810
6830
  }, [active, onCellEdit, onChange, sheets]);
6831
+ const addSheet = React36.useCallback(() => {
6832
+ setSheets((prev) => {
6833
+ const list = prev ?? [];
6834
+ const used = new Set(list.map((s) => s.name));
6835
+ let n = list.length + 1;
6836
+ while (used.has(`Sheet ${n}`)) n++;
6837
+ const next = [...list, blankSheet(`Sheet ${n}`)];
6838
+ setActive(next.length - 1);
6839
+ onChange?.(next);
6840
+ return next;
6841
+ });
6842
+ }, [onChange]);
6843
+ const deleteSheet = React36.useCallback((index) => {
6844
+ if (!sheets || sheets.length <= 1) return;
6845
+ const next = sheets.filter((_, i) => i !== index);
6846
+ setSheets(next);
6847
+ setActive((a) => Math.max(0, Math.min(index < a ? a - 1 : a, next.length - 1)));
6848
+ onChange?.(next);
6849
+ }, [sheets, onChange]);
6811
6850
  const baseName = React36.useMemo(
6812
6851
  () => (fileName || (typeof source === "object" && "name" in source ? sourceName(source) : null) || "spreadsheet").replace(/\.[^.]+$/, ""),
6813
6852
  [fileName, source]
@@ -6894,39 +6933,21 @@ function Spreadsheet({
6894
6933
  }
6895
6934
  const formats = exportFormats || [];
6896
6935
  const exportLabels = {
6897
- xlsx: "Excel workbook (.xlsx)",
6898
- csv: "CSV \u2014 this sheet (.csv)",
6899
- pdf: "PDF table (.pdf)"
6936
+ xlsx: "Export to Excel (.xlsx)",
6937
+ csv: "Export to CSV (.csv)",
6938
+ pdf: "Export to PDF (.pdf)"
6900
6939
  };
6940
+ const fileItems = formats.map((fmt) => ({ key: fmt, label: exportLabels[fmt], onSelect: () => runExport(fmt) }));
6941
+ const sheetItems = [
6942
+ { key: "insert", label: "Insert sheet", onSelect: addSheet },
6943
+ { key: "delete", label: "Delete sheet", danger: true, disabled: sheets.length <= 1, separatorBefore: true, onSelect: () => deleteSheet(active) }
6944
+ ];
6945
+ const showMenuBar = fileItems.length > 0 || editable;
6946
+ const canDeleteSheet = editable && sheets.length > 1;
6901
6947
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("flex flex-col overflow-hidden rounded-lg border border-border bg-surface-raised", className), style: { height, width, ...style }, children: [
6902
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 items-center gap-2 border-b border-border bg-surface px-3 py-1.5", children: [
6903
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground", children: sheet?.name || "Sheet 1" }),
6904
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden flex-shrink-0 text-xs tabular-nums text-foreground-muted sm:inline", children: [
6905
- plainRows.length.toLocaleString(),
6906
- " ",
6907
- plainRows.length === 1 ? "row" : "rows",
6908
- " \xB7 ",
6909
- columns.length,
6910
- " cols"
6911
- ] }),
6912
- formats.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6913
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-5 w-px flex-shrink-0 bg-border", "aria-hidden": "true" }),
6914
- /* @__PURE__ */ jsxRuntime.jsx(
6915
- MenuButton,
6916
- {
6917
- label: "Export",
6918
- size: "sm",
6919
- variant: "outline",
6920
- align: "end",
6921
- icon: /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon2, {}),
6922
- items: formats.map((fmt) => ({
6923
- key: fmt,
6924
- label: exportLabels[fmt],
6925
- onSelect: () => runExport(fmt)
6926
- }))
6927
- }
6928
- )
6929
- ] })
6948
+ showMenuBar && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 items-center gap-0.5 border-b border-border bg-surface px-1.5 py-1", children: [
6949
+ fileItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(MenuButton, { label: "File", variant: "ghost", size: "sm", hideChevron: true, items: fileItems }),
6950
+ editable && /* @__PURE__ */ jsxRuntime.jsx(MenuButton, { label: "Sheet", variant: "ghost", size: "sm", hideChevron: true, items: sheetItems })
6930
6951
  ] }),
6931
6952
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
6932
6953
  DataGrid,
@@ -6936,31 +6957,83 @@ function Spreadsheet({
6936
6957
  editable,
6937
6958
  sortable,
6938
6959
  virtualize,
6960
+ trailingRows: emptyRows,
6939
6961
  onCellEdit: handleCellEdit,
6940
6962
  height: "100%",
6941
6963
  className: "!rounded-none !border-0"
6942
6964
  },
6943
6965
  active
6944
6966
  ) }),
6945
- sheets.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", "aria-label": "Sheets", className: "flex flex-shrink-0 items-center gap-1 overflow-x-auto border-t border-border bg-surface px-2 py-1", children: sheets.map((s, i) => /* @__PURE__ */ jsxRuntime.jsx(
6946
- "button",
6947
- {
6948
- role: "tab",
6949
- type: "button",
6950
- "aria-selected": i === active,
6951
- onClick: () => setActive(i),
6952
- className: cx(
6953
- "flex-shrink-0 rounded-md px-3 py-1 text-xs font-medium transition-colors",
6954
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
6955
- i === active ? "bg-surface-raised text-foreground shadow-sm" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6956
- ),
6957
- children: s.name || `Sheet ${i + 1}`
6958
- },
6959
- `${s.name}-${i}`
6960
- )) })
6967
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-shrink-0 items-center gap-2 border-t border-border bg-surface px-2 py-1", children: [
6968
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "tablist", "aria-label": "Sheets", className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto", children: [
6969
+ sheets.map((s, i) => {
6970
+ const activeTab = i === active;
6971
+ const name = s.name || `Sheet ${i + 1}`;
6972
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6973
+ "div",
6974
+ {
6975
+ role: "tab",
6976
+ tabIndex: 0,
6977
+ "aria-selected": activeTab,
6978
+ onClick: () => setActive(i),
6979
+ onKeyDown: (e) => {
6980
+ if (e.key === "Enter" || e.key === " ") {
6981
+ e.preventDefault();
6982
+ setActive(i);
6983
+ }
6984
+ },
6985
+ className: cx(
6986
+ "group flex flex-shrink-0 cursor-pointer items-center gap-1 rounded-t-md border-t-2 px-3 py-1 text-xs transition-colors",
6987
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
6988
+ activeTab ? "border-t-accent bg-surface-raised font-semibold text-foreground shadow-sm" : "border-t-transparent font-medium text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6989
+ ),
6990
+ children: [
6991
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "max-w-[160px] truncate", children: name }),
6992
+ canDeleteSheet && /* @__PURE__ */ jsxRuntime.jsx(
6993
+ "button",
6994
+ {
6995
+ type: "button",
6996
+ title: "Delete sheet",
6997
+ "aria-label": `Delete ${name}`,
6998
+ onClick: (e) => {
6999
+ e.stopPropagation();
7000
+ deleteSheet(i);
7001
+ },
7002
+ className: cx(
7003
+ "flex h-4 w-4 items-center justify-center rounded text-foreground-muted transition-opacity hover:text-status-error",
7004
+ activeTab ? "opacity-100" : "opacity-0 group-hover:opacity-100"
7005
+ ),
7006
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", d: "M6 6l12 12M18 6 6 18" }) })
7007
+ }
7008
+ )
7009
+ ]
7010
+ },
7011
+ `${s.name}-${i}`
7012
+ );
7013
+ }),
7014
+ canAddSheet && /* @__PURE__ */ jsxRuntime.jsx(
7015
+ "button",
7016
+ {
7017
+ type: "button",
7018
+ onClick: addSheet,
7019
+ title: "Add sheet",
7020
+ "aria-label": "Add sheet",
7021
+ className: "flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md text-foreground-muted transition-colors hover:bg-surface-raised hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
7022
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", d: "M12 5v14M5 12h14" }) })
7023
+ }
7024
+ )
7025
+ ] }),
7026
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden flex-shrink-0 text-xs tabular-nums text-foreground-muted sm:inline", children: [
7027
+ plainRows.length.toLocaleString(),
7028
+ " ",
7029
+ plainRows.length === 1 ? "row" : "rows",
7030
+ " \xB7 ",
7031
+ columns.length,
7032
+ " cols"
7033
+ ] })
7034
+ ] })
6961
7035
  ] });
6962
7036
  }
6963
- var DownloadIcon2 = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) });
6964
7037
  function ThemeSwitch({ checked, onChange, label = "Toggle dark mode", className = "" }) {
6965
7038
  const id = React36.useId();
6966
7039
  return /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: `flex items-center gap-2 cursor-pointer select-none ${className}`.trim(), children: /* @__PURE__ */ jsxRuntime.jsx(