@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.d.cts CHANGED
@@ -2745,7 +2745,13 @@ interface DataGridProps {
2745
2745
  overscan?: number;
2746
2746
  /** Show the left row-number gutter. Default true. */
2747
2747
  rowNumbers?: boolean;
2748
- /** `row` is the index into `rows` (stable across sorting). */
2748
+ /**
2749
+ * Blank rows rendered below the data, like a real spreadsheet's empty grid.
2750
+ * Default 0. When `editable`, typing into one emits `onCellEdit` with a
2751
+ * `row` index at/after `rows.length` so the consumer can append the row.
2752
+ */
2753
+ trailingRows?: number;
2754
+ /** `row` is the index into `rows` (stable across sorting; may equal `rows.length`+ for a blank trailing row). */
2749
2755
  onCellEdit?: (e: {
2750
2756
  row: number;
2751
2757
  column: string;
@@ -2767,7 +2773,7 @@ interface DataGridProps {
2767
2773
  * `onCellEdit` on commit. Wrap it with {@link Spreadsheet} for multi-sheet
2768
2774
  * switching, file parsing and export.
2769
2775
  */
2770
- declare function DataGrid({ columns, rows, rowHeight, headerHeight, height, width, editable, sortable, sort: sortProp, onSortChange, virtualize, overscan, rowNumbers, onCellEdit, className, style, emptyState, }: DataGridProps): react_jsx_runtime.JSX.Element;
2776
+ declare function DataGrid({ columns, rows, rowHeight, headerHeight, height, width, editable, sortable, sort: sortProp, onSortChange, virtualize, overscan, rowNumbers, trailingRows, onCellEdit, className, style, emptyState, }: DataGridProps): react_jsx_runtime.JSX.Element;
2771
2777
 
2772
2778
  interface Cell {
2773
2779
  value: CellValue;
@@ -2799,6 +2805,10 @@ interface SpreadsheetProps {
2799
2805
  virtualize?: boolean;
2800
2806
  /** Click column headers to sort. Default true. */
2801
2807
  sortable?: boolean;
2808
+ /** Blank rows kept below the data (the spreadsheet "slack"). Default 50. */
2809
+ emptyRows?: number;
2810
+ /** Show a “+” to add a blank sheet. Defaults to `editable`. */
2811
+ allowAddSheet?: boolean;
2802
2812
  /** Overall height. Default 480. */
2803
2813
  height?: number | string;
2804
2814
  /** Overall width. Defaults to filling the container. */
@@ -2815,7 +2825,7 @@ interface SpreadsheetProps {
2815
2825
  * Exports to multi-sheet `.xlsx`, `.csv` (active sheet, BOM-prefixed) or a
2816
2826
  * paginated table `.pdf` (jsPDF, lazy).
2817
2827
  */
2818
- declare function Spreadsheet({ source, remote, editable, onCellEdit, onChange, export: exportFormats, fileName, virtualize, sortable, height, width, className, style, }: SpreadsheetProps): react_jsx_runtime.JSX.Element;
2828
+ declare function Spreadsheet({ source, remote, editable, onCellEdit, onChange, export: exportFormats, fileName, virtualize, sortable, emptyRows, allowAddSheet, height, width, className, style, }: SpreadsheetProps): react_jsx_runtime.JSX.Element;
2819
2829
 
2820
2830
  interface ThemeSwitchProps {
2821
2831
  checked: boolean;
package/dist/index.d.ts CHANGED
@@ -2745,7 +2745,13 @@ interface DataGridProps {
2745
2745
  overscan?: number;
2746
2746
  /** Show the left row-number gutter. Default true. */
2747
2747
  rowNumbers?: boolean;
2748
- /** `row` is the index into `rows` (stable across sorting). */
2748
+ /**
2749
+ * Blank rows rendered below the data, like a real spreadsheet's empty grid.
2750
+ * Default 0. When `editable`, typing into one emits `onCellEdit` with a
2751
+ * `row` index at/after `rows.length` so the consumer can append the row.
2752
+ */
2753
+ trailingRows?: number;
2754
+ /** `row` is the index into `rows` (stable across sorting; may equal `rows.length`+ for a blank trailing row). */
2749
2755
  onCellEdit?: (e: {
2750
2756
  row: number;
2751
2757
  column: string;
@@ -2767,7 +2773,7 @@ interface DataGridProps {
2767
2773
  * `onCellEdit` on commit. Wrap it with {@link Spreadsheet} for multi-sheet
2768
2774
  * switching, file parsing and export.
2769
2775
  */
2770
- declare function DataGrid({ columns, rows, rowHeight, headerHeight, height, width, editable, sortable, sort: sortProp, onSortChange, virtualize, overscan, rowNumbers, onCellEdit, className, style, emptyState, }: DataGridProps): react_jsx_runtime.JSX.Element;
2776
+ declare function DataGrid({ columns, rows, rowHeight, headerHeight, height, width, editable, sortable, sort: sortProp, onSortChange, virtualize, overscan, rowNumbers, trailingRows, onCellEdit, className, style, emptyState, }: DataGridProps): react_jsx_runtime.JSX.Element;
2771
2777
 
2772
2778
  interface Cell {
2773
2779
  value: CellValue;
@@ -2799,6 +2805,10 @@ interface SpreadsheetProps {
2799
2805
  virtualize?: boolean;
2800
2806
  /** Click column headers to sort. Default true. */
2801
2807
  sortable?: boolean;
2808
+ /** Blank rows kept below the data (the spreadsheet "slack"). Default 50. */
2809
+ emptyRows?: number;
2810
+ /** Show a “+” to add a blank sheet. Defaults to `editable`. */
2811
+ allowAddSheet?: boolean;
2802
2812
  /** Overall height. Default 480. */
2803
2813
  height?: number | string;
2804
2814
  /** Overall width. Defaults to filling the container. */
@@ -2815,7 +2825,7 @@ interface SpreadsheetProps {
2815
2825
  * Exports to multi-sheet `.xlsx`, `.csv` (active sheet, BOM-prefixed) or a
2816
2826
  * paginated table `.pdf` (jsPDF, lazy).
2817
2827
  */
2818
- declare function Spreadsheet({ source, remote, editable, onCellEdit, onChange, export: exportFormats, fileName, virtualize, sortable, height, width, className, style, }: SpreadsheetProps): react_jsx_runtime.JSX.Element;
2828
+ declare function Spreadsheet({ source, remote, editable, onCellEdit, onChange, export: exportFormats, fileName, virtualize, sortable, emptyRows, allowAddSheet, height, width, className, style, }: SpreadsheetProps): react_jsx_runtime.JSX.Element;
2819
2829
 
2820
2830
  interface ThemeSwitchProps {
2821
2831
  checked: boolean;
package/dist/index.js CHANGED
@@ -6467,6 +6467,7 @@ function DataGrid({
6467
6467
  virtualize = true,
6468
6468
  overscan = 4,
6469
6469
  rowNumbers = true,
6470
+ trailingRows = 0,
6470
6471
  onCellEdit,
6471
6472
  className = "",
6472
6473
  style,
@@ -6503,7 +6504,9 @@ function DataGrid({
6503
6504
  if (sortProp === void 0) setInternalSort(next);
6504
6505
  onSortChange?.(next);
6505
6506
  };
6506
- const totalHeight = rows.length * rowHeight;
6507
+ const displayRowCount = rows.length + Math.max(0, trailingRows);
6508
+ const totalHeight = displayRowCount * rowHeight;
6509
+ const rowIndexForDisp = (disp) => disp < rows.length ? order[disp] : disp;
6507
6510
  useEffect(() => {
6508
6511
  const el = scrollRef.current;
6509
6512
  if (!el || typeof ResizeObserver === "undefined") return;
@@ -6515,7 +6518,7 @@ function DataGrid({
6515
6518
  }, []);
6516
6519
  const bodyH = (viewport.h || (typeof height === "number" ? height : 480)) - headerHeight;
6517
6520
  const rowStart = virtualize ? Math.max(0, Math.floor(scroll.top / rowHeight) - overscan) : 0;
6518
- const rowEnd = virtualize ? Math.min(rows.length, Math.ceil((scroll.top + bodyH) / rowHeight) + overscan) : rows.length;
6521
+ const rowEnd = virtualize ? Math.min(displayRowCount, Math.ceil((scroll.top + bodyH) / rowHeight) + overscan) : displayRowCount;
6519
6522
  let colStart = 0;
6520
6523
  let colEnd = columns.length;
6521
6524
  if (virtualize && viewport.w) {
@@ -6533,13 +6536,14 @@ function DataGrid({
6533
6536
  const commit = useCallback(() => {
6534
6537
  if (!editing) return;
6535
6538
  const col = columns[editing.col];
6536
- onCellEdit?.({ row: order[editing.disp], column: col.key, value: draft });
6539
+ const ri = editing.disp < rows.length ? order[editing.disp] : editing.disp;
6540
+ onCellEdit?.({ row: ri, column: col.key, value: draft });
6537
6541
  setEditing(null);
6538
- }, [editing, columns, draft, onCellEdit, order]);
6542
+ }, [editing, columns, draft, onCellEdit, order, rows.length]);
6539
6543
  const startEdit = (disp, col) => {
6540
6544
  const c = columns[col];
6541
6545
  if (!(c.editable ?? editable)) return;
6542
- setDraft(displayValue(rows[order[disp]]?.[c.key] ?? ""));
6546
+ setDraft(displayValue(rows[rowIndexForDisp(disp)]?.[c.key] ?? ""));
6543
6547
  setEditing({ disp, col });
6544
6548
  };
6545
6549
  return /* @__PURE__ */ jsxs(
@@ -6602,7 +6606,7 @@ function DataGrid({
6602
6606
  `g-${disp}`
6603
6607
  )),
6604
6608
  visibleRows.map((disp) => {
6605
- const ri = order[disp];
6609
+ const ri = rowIndexForDisp(disp);
6606
6610
  return visibleCols.map((ci) => {
6607
6611
  const c = columns[ci];
6608
6612
  const isEditing = editing?.disp === disp && editing?.col === ci;
@@ -6645,7 +6649,7 @@ function DataGrid({
6645
6649
  });
6646
6650
  })
6647
6651
  ] }),
6648
- rows.length === 0 && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-foreground-muted", style: { top: headerHeight }, children: emptyState })
6652
+ displayRowCount === 0 && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-foreground-muted", style: { top: headerHeight }, children: emptyState })
6649
6653
  ]
6650
6654
  }
6651
6655
  );
@@ -6663,6 +6667,18 @@ function cellValue(v) {
6663
6667
  if (v != null && typeof v === "object" && "value" in v) return v.value;
6664
6668
  return v;
6665
6669
  }
6670
+ function columnLetter(i) {
6671
+ let s = "";
6672
+ let n = i;
6673
+ do {
6674
+ s = String.fromCharCode(65 + n % 26) + s;
6675
+ n = Math.floor(n / 26) - 1;
6676
+ } while (n >= 0);
6677
+ return s;
6678
+ }
6679
+ function blankSheet(name, cols = 8) {
6680
+ return { name, columns: Array.from({ length: cols }, (_, i) => ({ key: columnLetter(i), label: columnLetter(i) })), rows: [] };
6681
+ }
6666
6682
  function toPlainRows(sheet, columns) {
6667
6683
  return sheet.rows.map((row) => {
6668
6684
  const out = {};
@@ -6695,11 +6711,14 @@ function Spreadsheet({
6695
6711
  fileName,
6696
6712
  virtualize = true,
6697
6713
  sortable = true,
6714
+ emptyRows = 50,
6715
+ allowAddSheet,
6698
6716
  height = 480,
6699
6717
  width,
6700
6718
  className = "",
6701
6719
  style
6702
6720
  }) {
6721
+ const canAddSheet = allowAddSheet ?? editable;
6703
6722
  const [sheets, setSheets] = useState(Array.isArray(source) ? source : null);
6704
6723
  const [active, setActive] = useState(0);
6705
6724
  const [status, setStatus] = useState(Array.isArray(source) ? "ready" : "loading");
@@ -6759,6 +6778,7 @@ function Spreadsheet({
6759
6778
  if (!prev) return prev;
6760
6779
  const next = prev.map((s, i) => i === active ? { ...s, rows: s.rows.map((r) => ({ ...r })) } : s);
6761
6780
  const target = next[active];
6781
+ while (target.rows.length <= row) target.rows.push({});
6762
6782
  const existing = target.rows[row]?.[column];
6763
6783
  const prevValue = cellValue(existing);
6764
6784
  coerced = coerceToCellType(prevValue, value);
@@ -6772,6 +6792,25 @@ function Spreadsheet({
6772
6792
  });
6773
6793
  onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value: coerced });
6774
6794
  }, [active, onCellEdit, onChange, sheets]);
6795
+ const addSheet = useCallback(() => {
6796
+ setSheets((prev) => {
6797
+ const list = prev ?? [];
6798
+ const used = new Set(list.map((s) => s.name));
6799
+ let n = list.length + 1;
6800
+ while (used.has(`Sheet ${n}`)) n++;
6801
+ const next = [...list, blankSheet(`Sheet ${n}`)];
6802
+ setActive(next.length - 1);
6803
+ onChange?.(next);
6804
+ return next;
6805
+ });
6806
+ }, [onChange]);
6807
+ const deleteSheet = useCallback((index) => {
6808
+ if (!sheets || sheets.length <= 1) return;
6809
+ const next = sheets.filter((_, i) => i !== index);
6810
+ setSheets(next);
6811
+ setActive((a) => Math.max(0, Math.min(index < a ? a - 1 : a, next.length - 1)));
6812
+ onChange?.(next);
6813
+ }, [sheets, onChange]);
6775
6814
  const baseName = useMemo(
6776
6815
  () => (fileName || (typeof source === "object" && "name" in source ? sourceName(source) : null) || "spreadsheet").replace(/\.[^.]+$/, ""),
6777
6816
  [fileName, source]
@@ -6858,39 +6897,21 @@ function Spreadsheet({
6858
6897
  }
6859
6898
  const formats = exportFormats || [];
6860
6899
  const exportLabels = {
6861
- xlsx: "Excel workbook (.xlsx)",
6862
- csv: "CSV \u2014 this sheet (.csv)",
6863
- pdf: "PDF table (.pdf)"
6900
+ xlsx: "Export to Excel (.xlsx)",
6901
+ csv: "Export to CSV (.csv)",
6902
+ pdf: "Export to PDF (.pdf)"
6864
6903
  };
6904
+ const fileItems = formats.map((fmt) => ({ key: fmt, label: exportLabels[fmt], onSelect: () => runExport(fmt) }));
6905
+ const sheetItems = [
6906
+ { key: "insert", label: "Insert sheet", onSelect: addSheet },
6907
+ { key: "delete", label: "Delete sheet", danger: true, disabled: sheets.length <= 1, separatorBefore: true, onSelect: () => deleteSheet(active) }
6908
+ ];
6909
+ const showMenuBar = fileItems.length > 0 || editable;
6910
+ const canDeleteSheet = editable && sheets.length > 1;
6865
6911
  return /* @__PURE__ */ jsxs("div", { className: cx("flex flex-col overflow-hidden rounded-lg border border-border bg-surface-raised", className), style: { height, width, ...style }, children: [
6866
- /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-2 border-b border-border bg-surface px-3 py-1.5", children: [
6867
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground", children: sheet?.name || "Sheet 1" }),
6868
- /* @__PURE__ */ jsxs("span", { className: "hidden flex-shrink-0 text-xs tabular-nums text-foreground-muted sm:inline", children: [
6869
- plainRows.length.toLocaleString(),
6870
- " ",
6871
- plainRows.length === 1 ? "row" : "rows",
6872
- " \xB7 ",
6873
- columns.length,
6874
- " cols"
6875
- ] }),
6876
- formats.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
6877
- /* @__PURE__ */ jsx("span", { className: "h-5 w-px flex-shrink-0 bg-border", "aria-hidden": "true" }),
6878
- /* @__PURE__ */ jsx(
6879
- MenuButton,
6880
- {
6881
- label: "Export",
6882
- size: "sm",
6883
- variant: "outline",
6884
- align: "end",
6885
- icon: /* @__PURE__ */ jsx(DownloadIcon2, {}),
6886
- items: formats.map((fmt) => ({
6887
- key: fmt,
6888
- label: exportLabels[fmt],
6889
- onSelect: () => runExport(fmt)
6890
- }))
6891
- }
6892
- )
6893
- ] })
6912
+ showMenuBar && /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-0.5 border-b border-border bg-surface px-1.5 py-1", children: [
6913
+ fileItems.length > 0 && /* @__PURE__ */ jsx(MenuButton, { label: "File", variant: "ghost", size: "sm", hideChevron: true, items: fileItems }),
6914
+ editable && /* @__PURE__ */ jsx(MenuButton, { label: "Sheet", variant: "ghost", size: "sm", hideChevron: true, items: sheetItems })
6894
6915
  ] }),
6895
6916
  /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
6896
6917
  DataGrid,
@@ -6900,31 +6921,83 @@ function Spreadsheet({
6900
6921
  editable,
6901
6922
  sortable,
6902
6923
  virtualize,
6924
+ trailingRows: emptyRows,
6903
6925
  onCellEdit: handleCellEdit,
6904
6926
  height: "100%",
6905
6927
  className: "!rounded-none !border-0"
6906
6928
  },
6907
6929
  active
6908
6930
  ) }),
6909
- sheets.length > 1 && /* @__PURE__ */ 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__ */ jsx(
6910
- "button",
6911
- {
6912
- role: "tab",
6913
- type: "button",
6914
- "aria-selected": i === active,
6915
- onClick: () => setActive(i),
6916
- className: cx(
6917
- "flex-shrink-0 rounded-md px-3 py-1 text-xs font-medium transition-colors",
6918
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
6919
- i === active ? "bg-surface-raised text-foreground shadow-sm" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"
6920
- ),
6921
- children: s.name || `Sheet ${i + 1}`
6922
- },
6923
- `${s.name}-${i}`
6924
- )) })
6931
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-2 border-t border-border bg-surface px-2 py-1", children: [
6932
+ /* @__PURE__ */ jsxs("div", { role: "tablist", "aria-label": "Sheets", className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto", children: [
6933
+ sheets.map((s, i) => {
6934
+ const activeTab = i === active;
6935
+ const name = s.name || `Sheet ${i + 1}`;
6936
+ return /* @__PURE__ */ jsxs(
6937
+ "div",
6938
+ {
6939
+ role: "tab",
6940
+ tabIndex: 0,
6941
+ "aria-selected": activeTab,
6942
+ onClick: () => setActive(i),
6943
+ onKeyDown: (e) => {
6944
+ if (e.key === "Enter" || e.key === " ") {
6945
+ e.preventDefault();
6946
+ setActive(i);
6947
+ }
6948
+ },
6949
+ className: cx(
6950
+ "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",
6951
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
6952
+ 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"
6953
+ ),
6954
+ children: [
6955
+ /* @__PURE__ */ jsx("span", { className: "max-w-[160px] truncate", children: name }),
6956
+ canDeleteSheet && /* @__PURE__ */ jsx(
6957
+ "button",
6958
+ {
6959
+ type: "button",
6960
+ title: "Delete sheet",
6961
+ "aria-label": `Delete ${name}`,
6962
+ onClick: (e) => {
6963
+ e.stopPropagation();
6964
+ deleteSheet(i);
6965
+ },
6966
+ className: cx(
6967
+ "flex h-4 w-4 items-center justify-center rounded text-foreground-muted transition-opacity hover:text-status-error",
6968
+ activeTab ? "opacity-100" : "opacity-0 group-hover:opacity-100"
6969
+ ),
6970
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M6 6l12 12M18 6 6 18" }) })
6971
+ }
6972
+ )
6973
+ ]
6974
+ },
6975
+ `${s.name}-${i}`
6976
+ );
6977
+ }),
6978
+ canAddSheet && /* @__PURE__ */ jsx(
6979
+ "button",
6980
+ {
6981
+ type: "button",
6982
+ onClick: addSheet,
6983
+ title: "Add sheet",
6984
+ "aria-label": "Add sheet",
6985
+ 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",
6986
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M12 5v14M5 12h14" }) })
6987
+ }
6988
+ )
6989
+ ] }),
6990
+ /* @__PURE__ */ jsxs("span", { className: "hidden flex-shrink-0 text-xs tabular-nums text-foreground-muted sm:inline", children: [
6991
+ plainRows.length.toLocaleString(),
6992
+ " ",
6993
+ plainRows.length === 1 ? "row" : "rows",
6994
+ " \xB7 ",
6995
+ columns.length,
6996
+ " cols"
6997
+ ] })
6998
+ ] })
6925
6999
  ] });
6926
7000
  }
6927
- var DownloadIcon2 = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) });
6928
7001
  function ThemeSwitch({ checked, onChange, label = "Toggle dark mode", className = "" }) {
6929
7002
  const id = useId();
6930
7003
  return /* @__PURE__ */ jsx("label", { htmlFor: id, className: `flex items-center gap-2 cursor-pointer select-none ${className}`.trim(), children: /* @__PURE__ */ jsx(