@ai-matrx/records-ui 0.93.3 → 0.93.4

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
@@ -7978,6 +7978,10 @@ function useMergedGridControls(args) {
7978
7978
  (columnKey) => enabled && fieldOf2(columnKey) ? headerMenuItems(columnKey, ctx) : void 0,
7979
7979
  [enabled, ctx, fieldOf2]
7980
7980
  );
7981
+ const columnsControl = (0, import_react40.useMemo)(
7982
+ () => enabled ? { order, hidden, onChange: keepColumns } : void 0,
7983
+ [enabled, order, hidden, keepColumns]
7984
+ );
7981
7985
  const close = () => setDialog(null);
7982
7986
  const dialogs = !enabled ? null : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
7983
7987
  colours.dialogs,
@@ -8109,7 +8113,7 @@ function useMergedGridControls(args) {
8109
8113
  headerMenuFor,
8110
8114
  filterFor,
8111
8115
  contextMenu,
8112
- columns: enabled ? { order, hidden, onChange: keepColumns } : void 0,
8116
+ columns: columnsControl,
8113
8117
  dialogs,
8114
8118
  addColumn: () => setDialog({ kind: "insert", anchor: null, side: "right" }),
8115
8119
  sheetTools: enabled ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
@@ -9639,7 +9643,43 @@ function useGridUndo(args) {
9639
9643
  [push, undo, redo, held, busy, notice, dismiss]
9640
9644
  );
9641
9645
  }
9642
- function UndoRedoButtons({ undo }) {
9646
+ function useUndoSource(undo) {
9647
+ const box = (0, import_react52.useRef)({
9648
+ undo,
9649
+ face: { back: undo.depth.back, forward: undo.depth.forward, busy: undo.busy },
9650
+ listeners: /* @__PURE__ */ new Set()
9651
+ });
9652
+ box.current.undo = undo;
9653
+ const source = (0, import_react52.useMemo)(
9654
+ () => ({
9655
+ face: () => box.current.face,
9656
+ subscribe: (listener) => {
9657
+ box.current.listeners.add(listener);
9658
+ return () => {
9659
+ box.current.listeners.delete(listener);
9660
+ };
9661
+ },
9662
+ undo: () => void box.current.undo.undo(),
9663
+ redo: () => void box.current.undo.redo()
9664
+ }),
9665
+ []
9666
+ );
9667
+ const { back, forward } = undo.depth;
9668
+ const busy = undo.busy;
9669
+ (0, import_react52.useLayoutEffect)(() => {
9670
+ const held = box.current.face;
9671
+ if (held.back === back && held.forward === forward && held.busy === busy) return;
9672
+ box.current.face = { back, forward, busy };
9673
+ for (const listener of box.current.listeners) listener();
9674
+ }, [back, forward, busy]);
9675
+ return source;
9676
+ }
9677
+ function UndoRedoLive({ source }) {
9678
+ const face = (0, import_react52.useSyncExternalStore)(source.subscribe, source.face, source.face);
9679
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(UndoRedoPair, { face, onUndo: source.undo, onRedo: source.redo });
9680
+ }
9681
+ function UndoRedoPair({ face, onUndo, onRedo }) {
9682
+ const undo = { depth: { back: face.back, forward: face.forward }, busy: face.busy };
9643
9683
  if (undo.depth.back === 0 && undo.depth.forward === 0) return null;
9644
9684
  return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
9645
9685
  /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
@@ -9651,7 +9691,7 @@ function UndoRedoButtons({ undo }) {
9651
9691
  title: undo.depth.back > 0 ? `Undo (Cmd-Z) \xB7 ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"} back` : "Nothing to undo",
9652
9692
  "aria-label": `Undo, ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"}`,
9653
9693
  disabled: undo.busy || undo.depth.back === 0,
9654
- onClick: () => void undo.undo(),
9694
+ onClick: onUndo,
9655
9695
  children: [
9656
9696
  "Undo ",
9657
9697
  /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "ml-1 tabular-nums text-muted-foreground", "data-records-undo-count": "", children: undo.depth.back })
@@ -9667,7 +9707,7 @@ function UndoRedoButtons({ undo }) {
9667
9707
  title: undo.depth.forward > 0 ? `Redo (Shift-Cmd-Z) \xB7 ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"} forward` : "Nothing to redo",
9668
9708
  "aria-label": `Redo, ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"}`,
9669
9709
  disabled: undo.busy || undo.depth.forward === 0,
9670
- onClick: () => void undo.redo(),
9710
+ onClick: onRedo,
9671
9711
  children: [
9672
9712
  "Redo ",
9673
9713
  /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "ml-1 tabular-nums text-muted-foreground", "data-records-redo-count": "", children: undo.depth.forward })
@@ -11013,6 +11053,7 @@ function Grid({
11013
11053
  );
11014
11054
  const rowVersion = (0, import_react60.useCallback)((row) => editingNow.current.rowSignature(row.id), []);
11015
11055
  const undo = useGridUndo({ tableId, reload: records.reload, forget: editing.forget });
11056
+ const undoSource = useUndoSource(undo);
11016
11057
  pushWritten.current = (write) => {
11017
11058
  const field2 = (fields.data ?? []).find((f) => f.key === write.key);
11018
11059
  const name = changedRecordName(table.data, rows.find((r) => r.id === write.rowId)?.document ?? {}, write.key, write.after);
@@ -11564,9 +11605,14 @@ function Grid({
11564
11605
  );
11565
11606
  const footerNow = (0, import_react60.useRef)({
11566
11607
  summaryCell: () => null,
11567
- addLine: () => null
11608
+ addLine: () => null,
11609
+ choose: () => void 0,
11610
+ add: () => void 0
11568
11611
  });
11569
11612
  footerNow.current = {
11613
+ // The footer is memoised on what it shows (`footerRows` below), so its buttons act through here.
11614
+ choose: chooseSummary,
11615
+ add: () => void addRowHere(),
11570
11616
  summaryCell: (columnId) => {
11571
11617
  const field2 = ordered.find((f) => f.key === columnId);
11572
11618
  if (!field2) return null;
@@ -11579,11 +11625,11 @@ function Grid({
11579
11625
  value: summaries.values[columnId],
11580
11626
  rowCount: summaries.rowCount,
11581
11627
  summaries,
11582
- onChoose: (op2) => chooseSummary(columnId, op2)
11628
+ onChoose: (op2) => footerNow.current.choose(columnId, op2)
11583
11629
  }
11584
11630
  );
11585
11631
  },
11586
- addLine: () => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddRowLine, { onAdd: () => void addRowHere(), busy: added.busy })
11632
+ addLine: () => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddRowLine, { onAdd: () => footerNow.current.add(), busy: added.busy })
11587
11633
  };
11588
11634
  const addColumnNow = (0, import_react60.useRef)(controls.addColumn);
11589
11635
  addColumnNow.current = controls.addColumn;
@@ -11698,13 +11744,24 @@ function Grid({
11698
11744
  // eslint-disable-next-line react-hooks/exhaustive-deps -- `hasTalk`/`hasAgent` stand for the host ports the JSX reads; handlers run through `rowActionsNow`.
11699
11745
  [canWrite, mayWriteRow, tableActions.runnable, tableActions.agent, runner.busy, hasTalk, hasAgent, editable, rights.write]
11700
11746
  );
11701
- const columns = (0, import_react60.useMemo)(
11747
+ const headerHeld = (0, import_react60.useRef)(/* @__PURE__ */ new Map());
11748
+ const menuLatest = (0, import_react60.useRef)(/* @__PURE__ */ new Map());
11749
+ const addColumnDef = (0, import_react60.useMemo)(() => addColumnColumn(() => addColumnNow.current()), []);
11750
+ const drawnColumns = (0, import_react60.useMemo)(
11702
11751
  () => {
11703
- const drawn = withHeaderMenus(bodyColumns, controls.headerMenuFor, controls.filterFor);
11704
- return merged && rights.structure && editable ? [...drawn, addColumnColumn(() => addColumnNow.current())] : drawn;
11752
+ const filterFor = controls.filterFor;
11753
+ const drawn = withHeaderMenus(
11754
+ bodyColumns,
11755
+ (key) => holdHeaderMenu(headerHeld.current, menuLatest.current, key, controls.headerMenuFor(key)),
11756
+ (key) => holdByContent(headerHeld.current, `filter:${key}`, filterFor(key))
11757
+ );
11758
+ return merged && rights.structure && editable ? [...drawn, addColumnDef] : drawn;
11705
11759
  },
11706
- [bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable]
11760
+ [bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable, addColumnDef]
11707
11761
  );
11762
+ const columnsHeld = (0, import_react60.useRef)(null);
11763
+ const columns = columnsHeld.current !== null && sameColumns(columnsHeld.current, drawnColumns) ? columnsHeld.current : drawnColumns;
11764
+ columnsHeld.current = columns;
11708
11765
  const [localColumns, setLocalColumns] = (0, import_react60.useState)(
11709
11766
  null
11710
11767
  );
@@ -11777,6 +11834,274 @@ function Grid({
11777
11834
  } : {}
11778
11835
  };
11779
11836
  }, [presentation, onPresentationChange, writeLook, merged, phone]);
11837
+ const queryTotal = records.data?.total ?? rows.length;
11838
+ const tableQuery = (0, import_react60.useMemo)(
11839
+ () => merged ? {
11840
+ // THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
11841
+ mode: "controlled",
11842
+ state: query,
11843
+ totalItems: queryTotal,
11844
+ onStateChange: setQuery,
11845
+ sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
11846
+ } : { mode: "controlled-local", state: query, onStateChange: setQuery },
11847
+ [merged, query, queryTotal]
11848
+ );
11849
+ const showAddLine = mayAddRow && rows.length > 0;
11850
+ const showSummaryLine = rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0);
11851
+ const footerFace = JSON.stringify([
11852
+ showAddLine ? added.busy : null,
11853
+ showSummaryLine ? [chosenSummaries, summaries.values, summaries.rowCount, summaries.loading] : null
11854
+ ]);
11855
+ const footerRows = (0, import_react60.useMemo)(
11856
+ () => [...showAddLine ? [ADD_ROW] : [], ...showSummaryLine ? [SUMMARY_ROW] : []],
11857
+ // `footerFace`, `ordered` and the summaries' operations are what the footer's cells draw.
11858
+ // eslint-disable-next-line react-hooks/exhaustive-deps
11859
+ [showAddLine, showSummaryLine, footerFace, ordered, summaries.operations, summaries.askOperations]
11860
+ );
11861
+ const coverageTotal = typeof records.data?.total === "number" ? records.data.total : null;
11862
+ const tableCoverage = (0, import_react60.useMemo)(
11863
+ () => ({
11864
+ answeredBy: merged ? "source" : "client",
11865
+ ...merged && coverageTotal !== null ? { matched: coverageTotal } : {},
11866
+ noun: "record",
11867
+ ...coverageTotal !== null ? { total: coverageTotal } : {}
11868
+ }),
11869
+ [merged, coverageTotal]
11870
+ );
11871
+ const sheetNow = (0, import_react60.useRef)({ merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify });
11872
+ sheetNow.current = { merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify };
11873
+ const hasNotify = Boolean(host.notify);
11874
+ const readOnlyReason = !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write");
11875
+ const spreadsheetConfig = (0, import_react60.useMemo)(
11876
+ () => canWrite && editable ? {
11877
+ enabled: true,
11878
+ writableColumnIds: pasteableKeys,
11879
+ onPatch: (patches, intent) => {
11880
+ const now = sheetNow.current;
11881
+ if (now.merged) {
11882
+ void now.writeRange(patches, intent);
11883
+ return;
11884
+ }
11885
+ for (const patch of patches) {
11886
+ now.editing.setCell(patch.rowId, patch.columnId, patch.value);
11887
+ }
11888
+ },
11889
+ ...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
11890
+ columnRefusal: (columnId) => {
11891
+ const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
11892
+ if (!field2) return `This table has no column called ${columnId}.`;
11893
+ return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
11894
+ },
11895
+ validateCell: ({ columnId, value }) => {
11896
+ const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
11897
+ if (!field2) return `This table has no column called ${columnId}.`;
11898
+ const judged = valueFromPastedText(field2, value === null ? "" : String(value));
11899
+ return "refusal" in judged ? judged.refusal : void 0;
11900
+ },
11901
+ onPastePlan: receivePastePlan,
11902
+ /*
11903
+ * TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
11904
+ * opens ITS OWN editor — the Field's — holding what they typed (a letter, an
11905
+ * IME composition, an Option/AltGr character, dictation). A worked-out column
11906
+ * or a row this person may not write answers false, so nothing is swallowed.
11907
+ */
11908
+ onEditRequest: (address, seed) => {
11909
+ const now = sheetNow.current;
11910
+ const field2 = now.fields.find((f) => f.key === address.columnId);
11911
+ if (!field2 || !fieldIsEditable(field2) || !now.mayWriteRow(address.rowId)) return false;
11912
+ if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
11913
+ now.editing.begin({ rowId: address.rowId, key: field2.key }, seed);
11914
+ return true;
11915
+ },
11916
+ ...hasNotify ? { notify: (message) => sheetNow.current.notify?.success(message) } : {}
11917
+ } : {
11918
+ /*
11919
+ * RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
11920
+ * who may not change this table may still drag out a range, select all and press
11921
+ * Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
11922
+ * on a table whose writes were refused. Every write gesture (cut, paste, clear,
11923
+ * fill, undo) says why it cannot, in the store's own words.
11924
+ */
11925
+ enabled: true,
11926
+ readOnly: true,
11927
+ ...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
11928
+ readOnlyReason
11929
+ },
11930
+ [canWrite, editable, pasteableKeys, merged, followSelection, receivePastePlan, hasNotify, readOnlyReason]
11931
+ );
11932
+ const toolbarFields = fields.data;
11933
+ const hasSeveralRows = rows.length > 1;
11934
+ const editingBusy = editing.busy;
11935
+ const addRecord = editing.addRecord;
11936
+ const phoneSheetTools = merged && phone ? controls.sheetTools : null;
11937
+ const tableToolbar = (0, import_react60.useMemo)(
11938
+ () => ({
11939
+ search: true,
11940
+ searchPlaceholder: "Search records",
11941
+ /*
11942
+ * A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
11943
+ * the row, so Tools is never scrolled out of reach; the search is the same query.
11944
+ */
11945
+ ...merged && phone ? {
11946
+ customSearch: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", "data-records-grid-phone-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
11947
+ "input",
11948
+ {
11949
+ type: "search",
11950
+ value: query.search,
11951
+ placeholder: "Search",
11952
+ "aria-label": "Search records",
11953
+ onChange: (event) => {
11954
+ const search = event.target.value;
11955
+ setQuery((held) => ({ ...held, search, page: 1 }));
11956
+ },
11957
+ className: "h-8 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
11958
+ style: { fontSize: "16px" }
11959
+ }
11960
+ ) })
11961
+ } : {},
11962
+ /* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
11963
+ sort are drawn INTO the page's toolbar row, never a second bar under it. */
11964
+ ...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
11965
+ // SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
11966
+ // There is no third state: this package ships no disabled variant of
11967
+ // these controls, so a member cannot be shown a dead one by mistake.
11968
+ /*
11969
+ * WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
11970
+ * room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
11971
+ * record stay beside it — the primary action is never folded); on a phone EVERY control
11972
+ * — the table's own (clear, copy/export, group, columns), these, the search, the sort and
11973
+ * the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
11974
+ */
11975
+ actions: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
11976
+ manual ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
11977
+ SortStateControl,
11978
+ {
11979
+ ...handOrderSortState({
11980
+ sortName: columnSort ? columnLabel(toolbarFields ?? [], columnSort.id) : null,
11981
+ direction: columnSort?.direction ?? null,
11982
+ onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
11983
+ ...viewOrder?.onUseSort && columnSort ? {
11984
+ onUseSort: () => {
11985
+ const sort = columnSort;
11986
+ const chosen = { field: sort.id, direction: sort.direction };
11987
+ setHeldOrder({ order: "sorted", sorts: [chosen] });
11988
+ viewOrder.onUseSort?.(chosen);
11989
+ host.notify?.success(
11990
+ `Sorted by ${columnLabel(toolbarFields ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
11991
+ );
11992
+ }
11993
+ } : {}
11994
+ })
11995
+ }
11996
+ ) : null,
11997
+ viewId && viewOrder?.onPlaced && hasSeveralRows ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", variant: "ghost", onClick: () => setReordering(true), "data-records-reorder-open": "", children: "Reorder" }) : null,
11998
+ !merged && Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", disabled: editingBusy, onClick: () => void addRecord(), children: editingBusy ? "Adding\u2026" : "New record" }) : null,
11999
+ rights.structure && onAddField ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFieldButton, { onClick: onAddField }) : null,
12000
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12001
+ GridLayoutMenu,
12002
+ {
12003
+ look,
12004
+ firstColumn: sortedKeys[0] ? {
12005
+ key: sortedKeys[0],
12006
+ name: (() => {
12007
+ const first = (toolbarFields ?? []).find((f) => f.key === sortedKeys[0]);
12008
+ return first ? fieldName(first) : humanize(sortedKeys[0]);
12009
+ })()
12010
+ } : null,
12011
+ onChange: changeLayout,
12012
+ kept: Boolean(onPresentationChange)
12013
+ }
12014
+ )
12015
+ ] }),
12016
+ ...merged ? {
12017
+ overflow: {
12018
+ mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
12019
+ pinned: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
12020
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(UndoRedoLive, { source: undoSource }),
12021
+ Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", className: "shrink-0", disabled: added.busy, onClick: () => addRowRef.current(), children: added.busy ? "Adding\u2026" : "New record" }) : null
12022
+ ] }),
12023
+ ...phone ? {
12024
+ sheetTop: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-1.5 pb-1", "data-records-grid-sheet-top": "", "data-matrx-table-tools-keep-open": "", children: [
12025
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12026
+ "input",
12027
+ {
12028
+ type: "search",
12029
+ value: query.search,
12030
+ placeholder: "Search",
12031
+ "aria-label": "Search records",
12032
+ onChange: (event) => {
12033
+ const search = event.target.value;
12034
+ setQuery((held) => ({ ...held, search, page: 1 }));
12035
+ },
12036
+ className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
12037
+ style: { fontSize: "16px" }
12038
+ }
12039
+ ),
12040
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
12041
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0", children: "Sort by" }),
12042
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
12043
+ "select",
12044
+ {
12045
+ "aria-label": "Sort by",
12046
+ value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
12047
+ onChange: (event) => {
12048
+ const [id, direction] = event.target.value.split(":");
12049
+ setQuery((held) => ({
12050
+ ...held,
12051
+ page: 1,
12052
+ sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
12053
+ }));
12054
+ },
12055
+ className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
12056
+ style: { fontSize: "16px" },
12057
+ children: [
12058
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: "", children: "No sort" }),
12059
+ ordered.flatMap((field2) => [
12060
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
12061
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
12062
+ ])
12063
+ ]
12064
+ }
12065
+ )
12066
+ ] }),
12067
+ phoneSheetTools
12068
+ ] }),
12069
+ ...sheetExtras ? { sheetExtras } : {}
12070
+ } : {}
12071
+ }
12072
+ } : {}
12073
+ }),
12074
+ [
12075
+ merged,
12076
+ phone,
12077
+ query.search,
12078
+ query.sort,
12079
+ toolbarSlot,
12080
+ manual,
12081
+ columnSort,
12082
+ toolbarFields,
12083
+ viewOrder,
12084
+ host,
12085
+ viewId,
12086
+ hasSeveralRows,
12087
+ editable,
12088
+ rights.write,
12089
+ rights.structure,
12090
+ editingBusy,
12091
+ addRecord,
12092
+ onAddField,
12093
+ look,
12094
+ sortedKeys,
12095
+ changeLayout,
12096
+ onPresentationChange,
12097
+ toolbarFold2,
12098
+ undoSource,
12099
+ added.busy,
12100
+ ordered,
12101
+ phoneSheetTools,
12102
+ sheetExtras
12103
+ ]
12104
+ );
11780
12105
  const labelOfField = (0, import_react60.useCallback)(
11781
12106
  (key) => {
11782
12107
  const held = (fields.data ?? []).find((f) => f.key === key);
@@ -11945,25 +12270,12 @@ function Grid({
11945
12270
  viewTabs: false,
11946
12271
  stickyHeader: true,
11947
12272
  pageSize,
11948
- query: merged ? {
11949
- // THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
11950
- mode: "controlled",
11951
- state: query,
11952
- totalItems: records.data?.total ?? rows.length,
11953
- onStateChange: setQuery,
11954
- sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
11955
- } : { mode: "controlled-local", state: query, onStateChange: setQuery },
12273
+ query: tableQuery,
11956
12274
  tableClassName: (0, import_design_system35.cn)(
11957
12275
  !look.wrap && "[&_tbody_td]:max-w-[28rem] [&_tbody_td]:overflow-hidden [&_tbody_td]:text-ellipsis [&_tbody_td]:whitespace-nowrap! [&_tbody_td_*]:whitespace-nowrap!",
11958
12276
  look.fit === "scroll" && "sm:[&>table]:w-max! sm:[&>table]:max-w-none! sm:[&>table]:min-w-full!"
11959
12277
  ),
11960
- ...(() => {
11961
- const footerRows = [
11962
- ...mayAddRow && rows.length > 0 ? [ADD_ROW] : [],
11963
- ...rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0) ? [SUMMARY_ROW] : []
11964
- ];
11965
- return footerRows.length > 0 ? { footerRows } : {};
11966
- })(),
12278
+ footerRows,
11967
12279
  ...copyConfig ? { copy: copyConfig } : {},
11968
12280
  ...offerSelection ? {
11969
12281
  selection: {
@@ -11997,75 +12309,15 @@ function Grid({
11997
12309
  ] })
11998
12310
  }
11999
12311
  } : {},
12000
- detail: { enabled: false },
12001
- window: { enabled: false },
12312
+ detail: DETAIL_OFF,
12313
+ window: WINDOW_OFF,
12002
12314
  ...controls.contextMenu ? { contextMenu: controls.contextMenu } : {},
12003
- ...canWrite && editable ? {
12004
- spreadsheet: {
12005
- enabled: true,
12006
- writableColumnIds: pasteableKeys,
12007
- onPatch: (patches, intent) => {
12008
- if (merged) {
12009
- void writeRange(patches, intent);
12010
- return;
12011
- }
12012
- for (const patch of patches) {
12013
- editing.setCell(patch.rowId, patch.columnId, patch.value);
12014
- }
12015
- },
12016
- ...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
12017
- columnRefusal: (columnId) => {
12018
- const field2 = (fields.data ?? []).find((f) => f.key === columnId);
12019
- if (!field2) return `This table has no column called ${columnId}.`;
12020
- return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
12021
- },
12022
- validateCell: ({ columnId, value }) => {
12023
- const field2 = (fields.data ?? []).find((f) => f.key === columnId);
12024
- if (!field2) return `This table has no column called ${columnId}.`;
12025
- const judged = valueFromPastedText(field2, value === null ? "" : String(value));
12026
- return "refusal" in judged ? judged.refusal : void 0;
12027
- },
12028
- onPastePlan: receivePastePlan,
12029
- /*
12030
- * TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
12031
- * opens ITS OWN editor — the Field's — holding what they typed (a letter, an
12032
- * IME composition, an Option/AltGr character, dictation). A worked-out column
12033
- * or a row this person may not write answers false, so nothing is swallowed.
12034
- */
12035
- onEditRequest: (address, seed) => {
12036
- const field2 = (fields.data ?? []).find((f) => f.key === address.columnId);
12037
- if (!field2 || !fieldIsEditable(field2) || !mayWriteRow(address.rowId)) return false;
12038
- if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
12039
- editing.begin({ rowId: address.rowId, key: field2.key }, seed);
12040
- return true;
12041
- },
12042
- ...host.notify ? { notify: (message) => host.notify?.success(message) } : {}
12043
- }
12044
- } : {
12045
- /*
12046
- * RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
12047
- * who may not change this table may still drag out a range, select all and press
12048
- * Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
12049
- * on a table whose writes were refused. Every write gesture (cut, paste, clear,
12050
- * fill, undo) says why it cannot, in the store's own words.
12051
- */
12052
- spreadsheet: {
12053
- enabled: true,
12054
- readOnly: true,
12055
- ...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
12056
- readOnlyReason: !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write")
12057
- }
12058
- },
12315
+ spreadsheet: spreadsheetConfig,
12059
12316
  ...groupingConfig ? { grouping: groupingConfig } : {},
12060
12317
  columnState,
12061
12318
  ...look.rowHeight ? { rowHeight: look.rowHeight } : {},
12062
- virtualize: fills ? { threshold: 40 } : true,
12063
- coverage: {
12064
- answeredBy: merged ? "source" : "client",
12065
- ...merged && typeof records.data?.total === "number" ? { matched: records.data.total } : {},
12066
- noun: "record",
12067
- ...typeof records.data?.total === "number" ? { total: records.data.total } : {}
12068
- },
12319
+ virtualize: fills ? VIRTUALIZE_FILLED : true,
12320
+ coverage: tableCoverage,
12069
12321
  ...tableStyleProp ? { tableStyle: tableStyleProp } : {},
12070
12322
  ...onOpenRecord ? { onRowOpen: (row) => onOpenRecord(row.id) } : {},
12071
12323
  ...offersRowActions ? { rowActions } : {},
@@ -12077,142 +12329,7 @@ function Grid({
12077
12329
  ...mayAddRow ? { addFirst: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFirstRowButton, { onAdd: () => void addRowHere(), busy: added.busy }) } : {},
12078
12330
  emptyDescription: rights.write ? "Add the first one and it appears here." : rights.known ? `Nothing has been written to this table. ${rights.why("write")}` : "Nothing has been written to this table."
12079
12331
  }),
12080
- toolbar: {
12081
- search: true,
12082
- searchPlaceholder: "Search records",
12083
- /*
12084
- * A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
12085
- * the row, so Tools is never scrolled out of reach; the search is the same query.
12086
- */
12087
- ...merged && phone ? {
12088
- customSearch: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", "data-records-grid-phone-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12089
- "input",
12090
- {
12091
- type: "search",
12092
- value: query.search,
12093
- placeholder: "Search",
12094
- "aria-label": "Search records",
12095
- onChange: (event) => {
12096
- const search = event.target.value;
12097
- setQuery((held) => ({ ...held, search, page: 1 }));
12098
- },
12099
- className: "h-8 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
12100
- style: { fontSize: "16px" }
12101
- }
12102
- ) })
12103
- } : {},
12104
- /* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
12105
- sort are drawn INTO the page's toolbar row, never a second bar under it. */
12106
- ...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
12107
- // SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
12108
- // There is no third state: this package ships no disabled variant of
12109
- // these controls, so a member cannot be shown a dead one by mistake.
12110
- /*
12111
- * WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
12112
- * room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
12113
- * record stay beside it — the primary action is never folded); on a phone EVERY control
12114
- * — the table's own (clear, copy/export, group, columns), these, the search, the sort and
12115
- * the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
12116
- */
12117
- actions: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
12118
- manual ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12119
- SortStateControl,
12120
- {
12121
- ...handOrderSortState({
12122
- sortName: columnSort ? columnLabel(fields.data ?? [], columnSort.id) : null,
12123
- direction: columnSort?.direction ?? null,
12124
- onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
12125
- ...viewOrder?.onUseSort && columnSort ? {
12126
- onUseSort: () => {
12127
- const sort = columnSort;
12128
- const chosen = { field: sort.id, direction: sort.direction };
12129
- setHeldOrder({ order: "sorted", sorts: [chosen] });
12130
- viewOrder.onUseSort?.(chosen);
12131
- host.notify?.success(
12132
- `Sorted by ${columnLabel(fields.data ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
12133
- );
12134
- }
12135
- } : {}
12136
- })
12137
- }
12138
- ) : null,
12139
- viewId && viewOrder?.onPlaced && rows.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", variant: "ghost", onClick: () => setReordering(true), "data-records-reorder-open": "", children: "Reorder" }) : null,
12140
- !merged && Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", disabled: editing.busy, onClick: () => void editing.addRecord(), children: editing.busy ? "Adding\u2026" : "New record" }) : null,
12141
- rights.structure && onAddField ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFieldButton, { onClick: onAddField }) : null,
12142
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12143
- GridLayoutMenu,
12144
- {
12145
- look,
12146
- firstColumn: sortedKeys[0] ? {
12147
- key: sortedKeys[0],
12148
- name: (() => {
12149
- const first = (fields.data ?? []).find((f) => f.key === sortedKeys[0]);
12150
- return first ? fieldName(first) : humanize(sortedKeys[0]);
12151
- })()
12152
- } : null,
12153
- onChange: changeLayout,
12154
- kept: Boolean(onPresentationChange)
12155
- }
12156
- )
12157
- ] }),
12158
- ...merged ? {
12159
- overflow: {
12160
- mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
12161
- pinned: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
12162
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(UndoRedoButtons, { undo }),
12163
- Boolean(editable) && rights.write ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_design_system35.Button, { size: "sm", className: "shrink-0", disabled: added.busy, onClick: () => void addRowHere(), children: added.busy ? "Adding\u2026" : "New record" }) : null
12164
- ] }),
12165
- ...phone ? {
12166
- sheetTop: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-1.5 pb-1", "data-records-grid-sheet-top": "", "data-matrx-table-tools-keep-open": "", children: [
12167
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12168
- "input",
12169
- {
12170
- type: "search",
12171
- value: query.search,
12172
- placeholder: "Search",
12173
- "aria-label": "Search records",
12174
- onChange: (event) => {
12175
- const search = event.target.value;
12176
- setQuery((held) => ({ ...held, search, page: 1 }));
12177
- },
12178
- className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
12179
- style: { fontSize: "16px" }
12180
- }
12181
- ),
12182
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
12183
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0", children: "Sort by" }),
12184
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
12185
- "select",
12186
- {
12187
- "aria-label": "Sort by",
12188
- value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
12189
- onChange: (event) => {
12190
- const [id, direction] = event.target.value.split(":");
12191
- setQuery((held) => ({
12192
- ...held,
12193
- page: 1,
12194
- sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
12195
- }));
12196
- },
12197
- className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
12198
- style: { fontSize: "16px" },
12199
- children: [
12200
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: "", children: "No sort" }),
12201
- ordered.flatMap((field2) => [
12202
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
12203
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
12204
- ])
12205
- ]
12206
- }
12207
- )
12208
- ] }),
12209
- controls.sheetTools
12210
- ] }),
12211
- ...sheetExtras ? { sheetExtras } : {}
12212
- } : {}
12213
- }
12214
- } : {}
12215
- }
12332
+ toolbar: tableToolbar
12216
12333
  },
12217
12334
  `records-grid-${tableId}:${openingKey}`
12218
12335
  )
@@ -12314,6 +12431,58 @@ function Grid({
12314
12431
  }
12315
12432
  ) });
12316
12433
  }
12434
+ var DETAIL_OFF = { enabled: false };
12435
+ var WINDOW_OFF = { enabled: false };
12436
+ var VIRTUALIZE_FILLED = { threshold: 40 };
12437
+ function menuFace(items) {
12438
+ return JSON.stringify(items, (_key, value) => typeof value === "function" ? "\u0192" : value);
12439
+ }
12440
+ function menuThroughLatest(items, at, latest) {
12441
+ return items.map((item) => {
12442
+ const path = `${at}/${item.id}`;
12443
+ return {
12444
+ ...item,
12445
+ ...item.onSelect ? { onSelect: () => latest.get(path)?.() } : {},
12446
+ ...item.submenu ? { submenu: menuThroughLatest(item.submenu, path, latest) } : {}
12447
+ };
12448
+ });
12449
+ }
12450
+ function bindLatest(items, at, latest) {
12451
+ for (const item of items) {
12452
+ const path = `${at}/${item.id}`;
12453
+ if (item.onSelect) latest.set(path, item.onSelect);
12454
+ if (item.submenu) bindLatest(item.submenu, path, latest);
12455
+ }
12456
+ }
12457
+ function holdHeaderMenu(held, latest, key, items) {
12458
+ if (!items) return items;
12459
+ const at = `menu:${key}`;
12460
+ bindLatest(items, at, latest);
12461
+ const sig = menuFace(items);
12462
+ const was = held.get(at);
12463
+ if (was && was.sig === sig) return was.value;
12464
+ const value = menuThroughLatest(items, at, latest);
12465
+ held.set(at, { sig, value });
12466
+ return value;
12467
+ }
12468
+ function holdByContent(held, at, value) {
12469
+ if (value === void 0) return value;
12470
+ const sig = JSON.stringify(value);
12471
+ const was = held.get(at);
12472
+ if (was && was.sig === sig) return was.value;
12473
+ held.set(at, { sig, value });
12474
+ return value;
12475
+ }
12476
+ function sameColumns(a, b) {
12477
+ if (a === b) return true;
12478
+ if (a.length !== b.length) return false;
12479
+ return a.every((column, i) => {
12480
+ const other = b[i];
12481
+ if (column === other) return true;
12482
+ const keys = Object.keys(column);
12483
+ return keys.length === Object.keys(other).length && keys.every((key) => Object.is(column[key], other[key]));
12484
+ });
12485
+ }
12317
12486
  function withHeaderMenus(columns, menuFor, filterFor) {
12318
12487
  return columns.map((column) => {
12319
12488
  const items = menuFor(String(column.id));