@geomak/ui 7.13.0 → 7.15.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
@@ -6507,6 +6507,15 @@ function displayValue(v) {
6507
6507
  if (v == null) return "";
6508
6508
  return String(v);
6509
6509
  }
6510
+ function colLetter(i) {
6511
+ let s = "";
6512
+ let n = i;
6513
+ do {
6514
+ s = String.fromCharCode(65 + n % 26) + s;
6515
+ n = Math.floor(n / 26) - 1;
6516
+ } while (n >= 0);
6517
+ return s;
6518
+ }
6510
6519
  function DataGrid({
6511
6520
  columns,
6512
6521
  rows,
@@ -6522,10 +6531,13 @@ function DataGrid({
6522
6531
  overscan = 4,
6523
6532
  rowNumbers = true,
6524
6533
  trailingRows = 0,
6534
+ trailingCols = 0,
6525
6535
  onCellEdit,
6526
6536
  contextMenu = false,
6527
6537
  onInsertRow,
6528
6538
  onDeleteRow,
6539
+ onInsertColumn,
6540
+ onHeaderEdit,
6529
6541
  className = "",
6530
6542
  style,
6531
6543
  emptyState = "No data"
@@ -6538,12 +6550,22 @@ function DataGrid({
6538
6550
  const [ctxTarget, setCtxTarget] = React36.useState(null);
6539
6551
  const [editing, setEditing] = React36.useState(null);
6540
6552
  const [draft, setDraft] = React36.useState("");
6553
+ const [editingHeader, setEditingHeader] = React36.useState(null);
6554
+ const [headerDraft, setHeaderDraft] = React36.useState("");
6541
6555
  const [internalSort, setInternalSort] = React36.useState(null);
6542
6556
  const sort = sortProp !== void 0 ? sortProp : internalSort;
6543
6557
  const gutter = rowNumbers ? GUTTER : 0;
6544
6558
  const colSortable = (c) => c.sortable ?? sortable;
6559
+ const cols = React36.useMemo(() => {
6560
+ if (trailingCols <= 0) return columns;
6561
+ const extra = Array.from({ length: trailingCols }, (_, k) => {
6562
+ const idx = columns.length + k;
6563
+ return { key: `__c${idx}`, label: colLetter(idx), editable: false, sortable: false };
6564
+ });
6565
+ return [...columns, ...extra];
6566
+ }, [columns, trailingCols]);
6545
6567
  const { widths, offsets, totalWidth } = React36.useMemo(() => {
6546
- const widths2 = columns.map((c) => resolveWidth(c.width));
6568
+ const widths2 = cols.map((c) => resolveWidth(c.width));
6547
6569
  const offsets2 = [];
6548
6570
  let acc = 0;
6549
6571
  for (const w of widths2) {
@@ -6551,7 +6573,7 @@ function DataGrid({
6551
6573
  acc += w;
6552
6574
  }
6553
6575
  return { widths: widths2, offsets: offsets2, totalWidth: acc };
6554
- }, [columns]);
6576
+ }, [cols]);
6555
6577
  const order = React36.useMemo(() => {
6556
6578
  const idx = rows.map((_, i) => i);
6557
6579
  if (!sort) return idx;
@@ -6580,45 +6602,56 @@ function DataGrid({
6580
6602
  const rowStart = virtualize ? Math.max(0, Math.floor(scroll.top / rowHeight) - overscan) : 0;
6581
6603
  const rowEnd = virtualize ? Math.min(displayRowCount, Math.ceil((scroll.top + bodyH) / rowHeight) + overscan) : displayRowCount;
6582
6604
  let colStart = 0;
6583
- let colEnd = columns.length;
6605
+ let colEnd = cols.length;
6584
6606
  if (virtualize && viewport.w) {
6585
6607
  const viewLeft = scroll.left;
6586
6608
  const viewRight = scroll.left + (viewport.w - gutter);
6587
6609
  colStart = Math.max(0, offsets.findIndex((o, i) => o + widths[i] > viewLeft));
6588
6610
  if (colStart < 0) colStart = 0;
6589
6611
  colEnd = offsets.findIndex((o) => o > viewRight);
6590
- colEnd = colEnd === -1 ? columns.length : Math.min(columns.length, colEnd + 1);
6612
+ colEnd = colEnd === -1 ? cols.length : Math.min(cols.length, colEnd + 1);
6591
6613
  colStart = Math.max(0, colStart - overscan);
6592
- colEnd = Math.min(columns.length, colEnd + overscan);
6614
+ colEnd = Math.min(cols.length, colEnd + overscan);
6593
6615
  }
6594
6616
  const visibleRows = Array.from({ length: Math.max(0, rowEnd - rowStart) }, (_, i) => rowStart + i);
6595
6617
  const visibleCols = Array.from({ length: Math.max(0, colEnd - colStart) }, (_, i) => colStart + i);
6596
6618
  const commit = React36.useCallback(() => {
6597
6619
  if (!editing) return;
6598
- const col = columns[editing.col];
6620
+ const col = cols[editing.col];
6599
6621
  const ri = editing.disp < rows.length ? order[editing.disp] : editing.disp;
6600
6622
  onCellEdit?.({ row: ri, column: col.key, value: draft });
6601
6623
  setEditing(null);
6602
- }, [editing, columns, draft, onCellEdit, order, rows.length]);
6624
+ }, [editing, cols, draft, onCellEdit, order, rows.length]);
6603
6625
  const startEdit = (disp, col) => {
6604
- const c = columns[col];
6626
+ const c = cols[col];
6605
6627
  if (!(c.editable ?? editable)) return;
6606
6628
  setDraft(displayValue(rows[rowIndexForDisp(disp)]?.[c.key] ?? ""));
6607
6629
  setEditing({ disp, col });
6608
6630
  };
6609
- const cellText = (sel) => displayValue(rows[rowIndexForDisp(sel.disp)]?.[columns[sel.col].key] ?? "");
6631
+ const startHeaderEdit = (col) => {
6632
+ if (!onHeaderEdit) return;
6633
+ const c = cols[col];
6634
+ setHeaderDraft(typeof c.label === "string" ? c.label : String(c.key));
6635
+ setEditingHeader(col);
6636
+ };
6637
+ const commitHeader = () => {
6638
+ if (editingHeader == null) return;
6639
+ onHeaderEdit?.(editingHeader, headerDraft);
6640
+ setEditingHeader(null);
6641
+ };
6642
+ const cellText = (sel) => displayValue(rows[rowIndexForDisp(sel.disp)]?.[cols[sel.col].key] ?? "");
6610
6643
  const copyCell = React36.useCallback(async () => {
6611
6644
  if (!selected) return;
6612
6645
  try {
6613
6646
  await navigator.clipboard?.writeText(cellText(selected));
6614
6647
  } catch {
6615
6648
  }
6616
- }, [selected, rows, columns, order]);
6649
+ }, [selected, rows, cols, order]);
6617
6650
  const cutCell = React36.useCallback(async () => {
6618
6651
  if (!selected) return;
6619
6652
  await copyCell();
6620
- onCellEdit?.({ row: rowIndexForDisp(selected.disp), column: columns[selected.col].key, value: "" });
6621
- }, [selected, copyCell, onCellEdit, columns, order, rows.length]);
6653
+ onCellEdit?.({ row: rowIndexForDisp(selected.disp), column: cols[selected.col].key, value: "" });
6654
+ }, [selected, copyCell, onCellEdit, cols, order, rows.length]);
6622
6655
  const pasteCell = React36.useCallback(async () => {
6623
6656
  if (!selected) return;
6624
6657
  let text = "";
@@ -6627,9 +6660,17 @@ function DataGrid({
6627
6660
  } catch {
6628
6661
  return;
6629
6662
  }
6630
- onCellEdit?.({ row: rowIndexForDisp(selected.disp), column: columns[selected.col].key, value: text });
6631
- }, [selected, onCellEdit, columns, order, rows.length]);
6663
+ onCellEdit?.({ row: rowIndexForDisp(selected.disp), column: cols[selected.col].key, value: text });
6664
+ }, [selected, onCellEdit, cols, order, rows.length]);
6632
6665
  const ctxItems = React36.useMemo(() => {
6666
+ if (ctxTarget?.kind === "header") {
6667
+ const ci = ctxTarget.col;
6668
+ return [
6669
+ { key: "rename", value: "Rename column", disabled: !onHeaderEdit, onClick: () => startHeaderEdit(ci) },
6670
+ { key: "left", value: "Add column to the left", disabled: !onInsertColumn, separatorBefore: true, onClick: () => onInsertColumn?.(ci) },
6671
+ { key: "right", value: "Add column to the right", disabled: !onInsertColumn, onClick: () => onInsertColumn?.(ci + 1) }
6672
+ ];
6673
+ }
6633
6674
  if (ctxTarget?.kind === "row") {
6634
6675
  const ri = rowIndexForDisp(ctxTarget.disp);
6635
6676
  const isData = ctxTarget.disp < rows.length;
@@ -6644,7 +6685,7 @@ function DataGrid({
6644
6685
  { key: "cut", value: "Cut", disabled: !editable, onClick: () => void cutCell() },
6645
6686
  { key: "paste", value: "Paste", disabled: !editable, onClick: () => void pasteCell() }
6646
6687
  ];
6647
- }, [ctxTarget, rows.length, editable, onInsertRow, onDeleteRow, copyCell, cutCell, pasteCell, order]);
6688
+ }, [ctxTarget, rows.length, editable, onInsertRow, onDeleteRow, onInsertColumn, onHeaderEdit, copyCell, cutCell, pasteCell, order]);
6648
6689
  const rowHighlighted = (disp) => hoveredRow === disp || selected?.disp === disp;
6649
6690
  const gridInner = /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", width: gutter + totalWidth + END_PAD, height: headerHeight + totalHeight + END_PAD }, children: [
6650
6691
  rowNumbers && /* @__PURE__ */ jsxRuntime.jsx(
@@ -6655,18 +6696,23 @@ function DataGrid({
6655
6696
  }
6656
6697
  ),
6657
6698
  visibleCols.map((ci) => {
6658
- const c = columns[ci];
6699
+ const c = cols[ci];
6659
6700
  const sortDir = sort?.key === c.key ? sort.dir : null;
6660
6701
  const canSort = colSortable(c);
6661
- return /* @__PURE__ */ jsxRuntime.jsxs(
6702
+ const renamable = !!onHeaderEdit;
6703
+ const isEditingHeader = editingHeader === ci;
6704
+ return /* @__PURE__ */ jsxRuntime.jsx(
6662
6705
  "div",
6663
6706
  {
6664
6707
  role: "columnheader",
6665
6708
  "aria-sort": sortDir ? sortDir === "asc" ? "ascending" : "descending" : void 0,
6666
- onClick: canSort ? () => toggleSort(c.key) : void 0,
6709
+ onClick: canSort && !renamable ? () => toggleSort(c.key) : void 0,
6710
+ onDoubleClick: renamable ? () => startHeaderEdit(ci) : void 0,
6711
+ onContextMenu: contextMenu ? () => setCtxTarget({ kind: "header", col: ci }) : void 0,
6667
6712
  className: cx(
6668
6713
  "flex items-center gap-1 border-b border-r border-border bg-surface px-3 font-medium text-foreground-secondary",
6669
- canSort && "cursor-pointer select-none hover:text-foreground"
6714
+ canSort && !renamable && "cursor-pointer select-none hover:text-foreground",
6715
+ renamable && "cursor-default select-none"
6670
6716
  ),
6671
6717
  style: {
6672
6718
  position: "absolute",
@@ -6677,10 +6723,27 @@ function DataGrid({
6677
6723
  zIndex: 2,
6678
6724
  justifyContent: c.align === "right" ? "flex-end" : c.align === "center" ? "center" : "flex-start"
6679
6725
  },
6680
- children: [
6726
+ children: isEditingHeader ? /* @__PURE__ */ jsxRuntime.jsx(
6727
+ "input",
6728
+ {
6729
+ autoFocus: true,
6730
+ value: headerDraft,
6731
+ onChange: (e) => setHeaderDraft(e.target.value),
6732
+ onBlur: commitHeader,
6733
+ onClick: (e) => e.stopPropagation(),
6734
+ onKeyDown: (e) => {
6735
+ if (e.key === "Enter") commitHeader();
6736
+ else if (e.key === "Escape") setEditingHeader(null);
6737
+ },
6738
+ className: "h-full w-full bg-transparent font-medium text-foreground outline-none"
6739
+ }
6740
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6681
6741
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: c.label ?? c.key }),
6682
- canSort && /* @__PURE__ */ jsxRuntime.jsx(SortCaret, { dir: sortDir })
6683
- ]
6742
+ canSort && (renamable ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", title: "Sort", onClick: (e) => {
6743
+ e.stopPropagation();
6744
+ toggleSort(c.key);
6745
+ }, className: "flex-shrink-0 rounded hover:text-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(SortCaret, { dir: sortDir }) }) : /* @__PURE__ */ jsxRuntime.jsx(SortCaret, { dir: sortDir }))
6746
+ ] })
6684
6747
  },
6685
6748
  `h-${c.key}`
6686
6749
  );
@@ -6706,7 +6769,7 @@ function DataGrid({
6706
6769
  const ri = rowIndexForDisp(disp);
6707
6770
  const hi = rowHighlighted(disp);
6708
6771
  return visibleCols.map((ci) => {
6709
- const c = columns[ci];
6772
+ const c = cols[ci];
6710
6773
  const isEditing = editing?.disp === disp && editing?.col === ci;
6711
6774
  const isSelected = selected?.disp === disp && selected?.col === ci;
6712
6775
  const canEdit = c.editable ?? editable;
@@ -6801,8 +6864,24 @@ function columnLetter(i) {
6801
6864
  } while (n >= 0);
6802
6865
  return s;
6803
6866
  }
6867
+ function makeColumns(existing, count) {
6868
+ const used = new Set(existing.map((c) => c.key));
6869
+ const out = [];
6870
+ let n = existing.length;
6871
+ for (let k = 0; k < count; k++) {
6872
+ let key = columnLetter(n);
6873
+ while (used.has(key)) {
6874
+ n++;
6875
+ key = columnLetter(n);
6876
+ }
6877
+ used.add(key);
6878
+ out.push({ key, label: key });
6879
+ n++;
6880
+ }
6881
+ return out;
6882
+ }
6804
6883
  function blankSheet(name, cols = 8) {
6805
- return { name, columns: Array.from({ length: cols }, (_, i) => ({ key: columnLetter(i), label: columnLetter(i) })), rows: [] };
6884
+ return { name, columns: makeColumns([], cols), rows: [] };
6806
6885
  }
6807
6886
  function parseWorkbook(XLSX, bytes) {
6808
6887
  const wb = XLSX.read(bytes, { type: "array" });
@@ -6857,6 +6936,7 @@ function Spreadsheet({
6857
6936
  virtualize = true,
6858
6937
  sortable = true,
6859
6938
  emptyRows = 50,
6939
+ emptyCols = 6,
6860
6940
  allowAddSheet,
6861
6941
  height = 480,
6862
6942
  width,
@@ -6900,12 +6980,21 @@ function Spreadsheet({
6900
6980
  const sheet = sheets?.[active];
6901
6981
  const columns = React36.useMemo(() => sheet ? toColumns(sheet.columns) : [], [sheet]);
6902
6982
  const plainRows = React36.useMemo(() => sheet ? toPlainRows(sheet, columns) : [], [sheet, columns]);
6983
+ const displayColumns = React36.useMemo(
6984
+ () => [...columns, ...makeColumns(columns, Math.max(0, emptyCols)).map((c) => ({ ...c, sortable: false }))],
6985
+ [columns, emptyCols]
6986
+ );
6987
+ const isSlackKey = (key) => !columns.some((c) => c.key === key);
6903
6988
  const handleCellEdit = React36.useCallback(({ row, column, value }) => {
6904
6989
  let coerced = value;
6905
6990
  setSheets((prev) => {
6906
6991
  if (!prev) return prev;
6907
- const next = prev.map((s, i) => i === active ? { ...s, rows: s.rows.map((r) => ({ ...r })) } : s);
6992
+ const next = prev.map((s, i) => i === active ? { ...s, columns: toColumns(s.columns), rows: s.rows.map((r) => ({ ...r })) } : s);
6908
6993
  const target = next[active];
6994
+ if (isSlackKey(column)) {
6995
+ const j = displayColumns.findIndex((c) => c.key === column);
6996
+ if (j >= 0) target.columns = displayColumns.slice(0, j + 1).map((c) => ({ key: c.key, label: c.label }));
6997
+ }
6909
6998
  while (target.rows.length <= row) target.rows.push({});
6910
6999
  const existing = target.rows[row]?.[column];
6911
7000
  const prevValue = cellValue(existing);
@@ -6919,7 +7008,22 @@ function Spreadsheet({
6919
7008
  return next;
6920
7009
  });
6921
7010
  onCellEdit?.({ sheet: sheets?.[active]?.name ?? "", row, column, value: coerced });
6922
- }, [active, onCellEdit, onChange, sheets]);
7011
+ }, [active, onCellEdit, onChange, sheets, displayColumns, columns]);
7012
+ const renameColumn = React36.useCallback((index, label) => {
7013
+ setSheets((prev) => {
7014
+ if (!prev) return prev;
7015
+ const next = prev.map((s, i) => i === active ? { ...s, columns: toColumns(s.columns) } : s);
7016
+ const target = next[active];
7017
+ let tcols = target.columns;
7018
+ if (index >= tcols.length) {
7019
+ tcols = displayColumns.slice(0, index + 1).map((c) => ({ key: c.key, label: c.label }));
7020
+ target.columns = tcols;
7021
+ }
7022
+ if (tcols[index]) tcols[index] = { ...tcols[index], label };
7023
+ onChange?.(next);
7024
+ return next;
7025
+ });
7026
+ }, [active, onChange, displayColumns]);
6923
7027
  const addSheet = React36.useCallback(() => {
6924
7028
  setSheets((prev) => {
6925
7029
  const list = prev ?? [];
@@ -6947,6 +7051,33 @@ function Spreadsheet({
6947
7051
  return next;
6948
7052
  });
6949
7053
  }, [active, onChange]);
7054
+ const add10Columns = React36.useCallback(() => {
7055
+ setSheets((prev) => {
7056
+ if (!prev) return prev;
7057
+ const next = prev.map((s, i) => {
7058
+ if (i !== active) return s;
7059
+ const cols = toColumns(s.columns);
7060
+ return { ...s, columns: [...cols, ...makeColumns(cols, 10)] };
7061
+ });
7062
+ onChange?.(next);
7063
+ return next;
7064
+ });
7065
+ }, [active, onChange]);
7066
+ const insertColumn = React36.useCallback((index) => {
7067
+ setSheets((prev) => {
7068
+ if (!prev) return prev;
7069
+ const next = prev.map((s, i) => {
7070
+ if (i !== active) return s;
7071
+ const cols = toColumns(s.columns);
7072
+ const at = Math.max(0, Math.min(index, cols.length));
7073
+ const out = [...cols];
7074
+ out.splice(at, 0, makeColumns(cols, 1)[0]);
7075
+ return { ...s, columns: out };
7076
+ });
7077
+ onChange?.(next);
7078
+ return next;
7079
+ });
7080
+ }, [active, onChange]);
6950
7081
  const insertRow = React36.useCallback((index) => {
6951
7082
  setSheets((prev) => {
6952
7083
  if (!prev) return prev;
@@ -7086,7 +7217,8 @@ function Spreadsheet({
7086
7217
  const sheetItems = [
7087
7218
  { key: "add", label: "Add Sheet", onSelect: addSheet },
7088
7219
  { key: "delete", label: "Delete Sheet", danger: true, disabled: sheets.length <= 1, onSelect: () => deleteSheet(active) },
7089
- { key: "add100", label: "Add 100 rows", separatorBefore: true, onSelect: add100Rows }
7220
+ { key: "add100", label: "Add 100 rows", separatorBefore: true, onSelect: add100Rows },
7221
+ { key: "add10cols", label: "Add 10 columns", onSelect: add10Columns }
7090
7222
  ];
7091
7223
  const canDeleteSheet = editable && sheets.length > 1;
7092
7224
  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: [
@@ -7109,7 +7241,7 @@ function Spreadsheet({
7109
7241
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
7110
7242
  DataGrid,
7111
7243
  {
7112
- columns,
7244
+ columns: displayColumns,
7113
7245
  rows: plainRows,
7114
7246
  editable,
7115
7247
  sortable,
@@ -7118,6 +7250,8 @@ function Spreadsheet({
7118
7250
  contextMenu: true,
7119
7251
  onInsertRow: editable ? insertRow : void 0,
7120
7252
  onDeleteRow: editable ? deleteRow : void 0,
7253
+ onInsertColumn: editable ? insertColumn : void 0,
7254
+ onHeaderEdit: editable ? renameColumn : void 0,
7121
7255
  onCellEdit: handleCellEdit,
7122
7256
  height: "100%",
7123
7257
  className: "!rounded-none !border-0"