@ai-matrx/records-ui 0.93.2 → 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.js CHANGED
@@ -4012,7 +4012,20 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow,
4012
4012
  }
4013
4013
  return null;
4014
4014
  }, [client, tableId, reload, order]);
4015
+ const rowSignature = useCallback5(
4016
+ (rowId) => {
4017
+ const out = [];
4018
+ if (editing?.rowId === rowId) out.push("open", editing.key, draft);
4019
+ const prefix = `${rowId}::`;
4020
+ for (const id in optimistic) if (id.startsWith(prefix)) out.push("held", id, optimistic[id]);
4021
+ for (const id in saving) if (id.startsWith(prefix)) out.push("saving", id);
4022
+ for (const id in refusals) if (id.startsWith(prefix)) out.push("refused", id, refusals[id]);
4023
+ return out;
4024
+ },
4025
+ [editing, draft, optimistic, saving, refusals]
4026
+ );
4015
4027
  return {
4028
+ rowSignature,
4016
4029
  valueFor,
4017
4030
  editing,
4018
4031
  draft,
@@ -7699,6 +7712,10 @@ function useMergedGridControls(args) {
7699
7712
  (columnKey) => enabled && fieldOf2(columnKey) ? headerMenuItems(columnKey, ctx) : void 0,
7700
7713
  [enabled, ctx, fieldOf2]
7701
7714
  );
7715
+ const columnsControl = useMemo14(
7716
+ () => enabled ? { order, hidden, onChange: keepColumns } : void 0,
7717
+ [enabled, order, hidden, keepColumns]
7718
+ );
7702
7719
  const close = () => setDialog(null);
7703
7720
  const dialogs = !enabled ? null : /* @__PURE__ */ jsxs24(Fragment8, { children: [
7704
7721
  colours.dialogs,
@@ -7830,7 +7847,7 @@ function useMergedGridControls(args) {
7830
7847
  headerMenuFor,
7831
7848
  filterFor,
7832
7849
  contextMenu,
7833
- columns: enabled ? { order, hidden, onChange: keepColumns } : void 0,
7850
+ columns: columnsControl,
7834
7851
  dialogs,
7835
7852
  addColumn: () => setDialog({ kind: "insert", anchor: null, side: "right" }),
7836
7853
  sheetTools: enabled ? /* @__PURE__ */ jsxs24(Fragment8, { children: [
@@ -7936,11 +7953,12 @@ function placePinnedRows(pageRows, pinned, anchors) {
7936
7953
  }
7937
7954
  function withAddRowCells(columns, line) {
7938
7955
  if (line === null) return columns;
7956
+ const draw = () => typeof line === "function" ? line() : line;
7939
7957
  return columns.map((column, index) => {
7940
7958
  const own2 = column.cell;
7941
7959
  return {
7942
7960
  ...column,
7943
- cell: (row, rowIndex) => row.id === ADD_ROW_ID ? index === 0 ? line : null : own2 ? own2(row, rowIndex) : null
7961
+ cell: (row, rowIndex) => row.id === ADD_ROW_ID ? index === 0 ? draw() : null : own2 ? own2(row, rowIndex) : null
7944
7962
  };
7945
7963
  });
7946
7964
  }
@@ -7963,6 +7981,7 @@ function AddRowLine({ onAdd, busy }) {
7963
7981
  }
7964
7982
  );
7965
7983
  }
7984
+ var drawNothing = () => null;
7966
7985
  function addColumnColumn(onAdd) {
7967
7986
  return {
7968
7987
  id: ADD_COLUMN_ID,
@@ -7982,7 +8001,8 @@ function addColumnColumn(onAdd) {
7982
8001
  children: "+"
7983
8002
  }
7984
8003
  ),
7985
- cell: () => null,
8004
+ // ONE function for the life of the module: a body cell that draws nothing never changes.
8005
+ cell: drawNothing,
7986
8006
  sortable: false,
7987
8007
  filter: false,
7988
8008
  hideable: false,
@@ -9277,7 +9297,7 @@ function GridLayoutMenu({
9277
9297
  }
9278
9298
 
9279
9299
  // src/gridUndo.tsx
9280
- import { useCallback as useCallback13, useEffect as useEffect21, useMemo as useMemo18, useRef as useRef11, useState as useState26 } from "react";
9300
+ import { useCallback as useCallback13, useEffect as useEffect21, useLayoutEffect, useMemo as useMemo18, useRef as useRef11, useState as useState26, useSyncExternalStore } from "react";
9281
9301
  import { useRecordsClient as useRecordsClient16 } from "@ai-matrx/records/react";
9282
9302
  import { announceRecordChanged } from "@ai-matrx/records/core";
9283
9303
  import { Button as Button23 } from "@ai-matrx/design-system";
@@ -9383,7 +9403,43 @@ function useGridUndo(args) {
9383
9403
  [push, undo, redo, held, busy, notice, dismiss]
9384
9404
  );
9385
9405
  }
9386
- function UndoRedoButtons({ undo }) {
9406
+ function useUndoSource(undo) {
9407
+ const box = useRef11({
9408
+ undo,
9409
+ face: { back: undo.depth.back, forward: undo.depth.forward, busy: undo.busy },
9410
+ listeners: /* @__PURE__ */ new Set()
9411
+ });
9412
+ box.current.undo = undo;
9413
+ const source = useMemo18(
9414
+ () => ({
9415
+ face: () => box.current.face,
9416
+ subscribe: (listener) => {
9417
+ box.current.listeners.add(listener);
9418
+ return () => {
9419
+ box.current.listeners.delete(listener);
9420
+ };
9421
+ },
9422
+ undo: () => void box.current.undo.undo(),
9423
+ redo: () => void box.current.undo.redo()
9424
+ }),
9425
+ []
9426
+ );
9427
+ const { back, forward } = undo.depth;
9428
+ const busy = undo.busy;
9429
+ useLayoutEffect(() => {
9430
+ const held = box.current.face;
9431
+ if (held.back === back && held.forward === forward && held.busy === busy) return;
9432
+ box.current.face = { back, forward, busy };
9433
+ for (const listener of box.current.listeners) listener();
9434
+ }, [back, forward, busy]);
9435
+ return source;
9436
+ }
9437
+ function UndoRedoLive({ source }) {
9438
+ const face = useSyncExternalStore(source.subscribe, source.face, source.face);
9439
+ return /* @__PURE__ */ jsx37(UndoRedoPair, { face, onUndo: source.undo, onRedo: source.redo });
9440
+ }
9441
+ function UndoRedoPair({ face, onUndo, onRedo }) {
9442
+ const undo = { depth: { back: face.back, forward: face.forward }, busy: face.busy };
9387
9443
  if (undo.depth.back === 0 && undo.depth.forward === 0) return null;
9388
9444
  return /* @__PURE__ */ jsxs32(Fragment13, { children: [
9389
9445
  /* @__PURE__ */ jsxs32(
@@ -9395,7 +9451,7 @@ function UndoRedoButtons({ undo }) {
9395
9451
  title: undo.depth.back > 0 ? `Undo (Cmd-Z) \xB7 ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"} back` : "Nothing to undo",
9396
9452
  "aria-label": `Undo, ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"}`,
9397
9453
  disabled: undo.busy || undo.depth.back === 0,
9398
- onClick: () => void undo.undo(),
9454
+ onClick: onUndo,
9399
9455
  children: [
9400
9456
  "Undo ",
9401
9457
  /* @__PURE__ */ jsx37("span", { className: "ml-1 tabular-nums text-muted-foreground", "data-records-undo-count": "", children: undo.depth.back })
@@ -9411,7 +9467,7 @@ function UndoRedoButtons({ undo }) {
9411
9467
  title: undo.depth.forward > 0 ? `Redo (Shift-Cmd-Z) \xB7 ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"} forward` : "Nothing to redo",
9412
9468
  "aria-label": `Redo, ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"}`,
9413
9469
  disabled: undo.busy || undo.depth.forward === 0,
9414
- onClick: () => void undo.redo(),
9470
+ onClick: onRedo,
9415
9471
  children: [
9416
9472
  "Redo ",
9417
9473
  /* @__PURE__ */ jsx37("span", { className: "ml-1 tabular-nums text-muted-foreground", "data-records-redo-count": "", children: undo.depth.forward })
@@ -10615,6 +10671,8 @@ function HandOrderDialog({
10615
10671
  import { Fragment as Fragment15, jsx as jsx42, jsxs as jsxs36 } from "react/jsx-runtime";
10616
10672
  var NO_COLUMNS_YET = "This table has no columns yet";
10617
10673
  var NO_COLUMNS_WHY = "A column is what a record holds \u2014 a name, a date, an amount, a person. Add the first one and the grid appears with it; everything else on this page works the moment it exists.";
10674
+ var NO_SUMMARIES = Object.freeze({});
10675
+ var rowIdOf = (row) => row.id;
10618
10676
  function Grid({
10619
10677
  tableId,
10620
10678
  pageSize = 50,
@@ -10764,7 +10822,17 @@ function Grid({
10764
10822
  onEnded: (exit, from, how) => sheetController.current?.endEdit(exit, { rowId: from.rowId, columnId: from.key }, how)
10765
10823
  } : {}
10766
10824
  });
10825
+ const editingNow = useRef14(editing);
10826
+ editingNow.current = editing;
10827
+ const editingHandle = useMemo22(
10828
+ () => new Proxy({}, {
10829
+ get: (_target, key) => editingNow.current[key]
10830
+ }),
10831
+ []
10832
+ );
10833
+ const rowVersion = useCallback16((row) => editingNow.current.rowSignature(row.id), []);
10767
10834
  const undo = useGridUndo({ tableId, reload: records.reload, forget: editing.forget });
10835
+ const undoSource = useUndoSource(undo);
10768
10836
  pushWritten.current = (write) => {
10769
10837
  const field2 = (fields.data ?? []).find((f) => f.key === write.key);
10770
10838
  const name = changedRecordName(table.data, rows.find((r) => r.id === write.rowId)?.document ?? {}, write.key, write.after);
@@ -10834,7 +10902,7 @@ function Grid({
10834
10902
  const [selectedIds, setSelectedIds] = useState31([]);
10835
10903
  const offerSelection = canWrite && (merged || tableActions.runnable.length > 0);
10836
10904
  const [reordering, setReordering] = useState31(false);
10837
- const chosenSummaries = localSummaries ?? presentation.summaries ?? {};
10905
+ const chosenSummaries = localSummaries ?? presentation.summaries ?? NO_SUMMARIES;
10838
10906
  const summaries = useColumnSummaries({ tableId, chosen: chosenSummaries, filter: question.filter, version: records.data });
10839
10907
  const [duplicated, setDuplicated] = useState31(null);
10840
10908
  const duplicate = useCallback16(
@@ -11283,7 +11351,7 @@ function Grid({
11283
11351
  () => ordered.map(
11284
11352
  (field2) => columnForField(
11285
11353
  field2,
11286
- canWrite ? editing : null,
11354
+ canWrite ? editingHandle : null,
11287
11355
  mayWriteRow,
11288
11356
  whyNotRow,
11289
11357
  {
@@ -11302,7 +11370,7 @@ function Grid({
11302
11370
  [
11303
11371
  ordered,
11304
11372
  canWrite,
11305
- editing,
11373
+ editingHandle,
11306
11374
  mayWriteRow,
11307
11375
  whyNotRow,
11308
11376
  presentation.formats,
@@ -11314,9 +11382,39 @@ function Grid({
11314
11382
  pinCell
11315
11383
  ]
11316
11384
  );
11317
- const columns = useMemo22(
11318
- () => {
11319
- const drawn = withHeaderMenus(withAddRowCells(withSummaryCells(
11385
+ const footerNow = useRef14({
11386
+ summaryCell: () => null,
11387
+ addLine: () => null,
11388
+ choose: () => void 0,
11389
+ add: () => void 0
11390
+ });
11391
+ footerNow.current = {
11392
+ // The footer is memoised on what it shows (`footerRows` below), so its buttons act through here.
11393
+ choose: chooseSummary,
11394
+ add: () => void addRowHere(),
11395
+ summaryCell: (columnId) => {
11396
+ const field2 = ordered.find((f) => f.key === columnId);
11397
+ if (!field2) return null;
11398
+ return /* @__PURE__ */ jsx42(
11399
+ SummaryCell,
11400
+ {
11401
+ field: field2,
11402
+ firstColumn: ordered[0]?.key === columnId,
11403
+ chosen: chosenSummaries[columnId],
11404
+ value: summaries.values[columnId],
11405
+ rowCount: summaries.rowCount,
11406
+ summaries,
11407
+ onChoose: (op2) => footerNow.current.choose(columnId, op2)
11408
+ }
11409
+ );
11410
+ },
11411
+ addLine: () => /* @__PURE__ */ jsx42(AddRowLine, { onAdd: () => footerNow.current.add(), busy: added.busy })
11412
+ };
11413
+ const addColumnNow = useRef14(controls.addColumn);
11414
+ addColumnNow.current = controls.addColumn;
11415
+ const bodyColumns = useMemo22(
11416
+ () => withAddRowCells(
11417
+ withSummaryCells(
11320
11418
  // THE ONE WAY A ROW OPENS ITS RECORD (VERIFIER-17): an Open control on the first cell,
11321
11419
  // and Space on any cell. Absent when the host cannot open a record.
11322
11420
  withOpenRecord(
@@ -11326,27 +11424,123 @@ function Grid({
11326
11424
  // The merged grid's handle sits at the START of the row, where Airtable's expand sits.
11327
11425
  merged ? "start" : "end"
11328
11426
  ),
11329
- (columnId) => {
11330
- const field2 = ordered.find((f) => f.key === columnId);
11331
- if (!field2) return null;
11332
- return /* @__PURE__ */ jsx42(
11333
- SummaryCell,
11334
- {
11335
- field: field2,
11336
- firstColumn: ordered[0]?.key === columnId,
11337
- chosen: chosenSummaries[columnId],
11338
- value: summaries.values[columnId],
11339
- rowCount: summaries.rowCount,
11340
- summaries,
11341
- onChoose: (op2) => chooseSummary(columnId, op2)
11342
- }
11343
- );
11427
+ (columnId) => footerNow.current.summaryCell(columnId)
11428
+ ),
11429
+ mayAddRow ? () => footerNow.current.addLine() : null
11430
+ ),
11431
+ [fieldColumns, onOpenRecord, table.data, merged, mayAddRow]
11432
+ );
11433
+ const isRowSelectable = useCallback16((row) => row.id !== SUMMARY_ROW.id && mayWriteRow(row.id), [mayWriteRow]);
11434
+ const copyConfig = useMemo22(
11435
+ () => table.data ? gridCopyConfig({
11436
+ tableId,
11437
+ tableName: tableName(table.data),
11438
+ fields: ordered,
11439
+ rowLabel: (row) => recordNameIn(table.data, row.document ?? {})
11440
+ }) : null,
11441
+ [tableId, table.data, ordered]
11442
+ );
11443
+ const tableStyleProp = useMemo22(
11444
+ () => tableStyleIsEmpty2(paint.style) ? null : {
11445
+ style: paint.style,
11446
+ // A choice is read as its OPTION (`paintByOption`): the rule, the value and the
11447
+ // colour a column keeps for it all name the same option, however it is stored.
11448
+ documentOf: (row) => paint.documentOf(row.document ?? {}),
11449
+ // A value its column keeps a colour for wears THAT colour (`choiceLooks.ts`);
11450
+ // any other value still paints from its word.
11451
+ choiceColorFor: paint.choiceColorFor
11452
+ },
11453
+ [paint]
11454
+ );
11455
+ const rowActionsNow = useRef14({ runner, handToAgent, host, table: table.data, duplicate });
11456
+ rowActionsNow.current = { runner, handToAgent, host, table: table.data, duplicate };
11457
+ const offersRowActions = canWrite || Boolean(host.talkToRecord) || Boolean(host.runAgentAction && tableActions.agent.length > 0);
11458
+ const hasTalk = Boolean(host.talkToRecord);
11459
+ const hasAgent = Boolean(host.runAgentAction);
11460
+ const rowActions = useCallback16(
11461
+ (row) => /* @__PURE__ */ jsxs36(Fragment15, { children: [
11462
+ canWrite && mayWriteRow(row.id) ? tableActions.runnable.map((action) => /* @__PURE__ */ jsx42(
11463
+ ActionButton,
11464
+ {
11465
+ action,
11466
+ busy: runner.busy === action.id,
11467
+ onPress: () => rowActionsNow.current.runner.press(action, [row.id])
11468
+ },
11469
+ action.id
11470
+ )) : null,
11471
+ host.runAgentAction ? tableActions.agent.map((action) => /* @__PURE__ */ jsx42(
11472
+ ActionButton,
11473
+ {
11474
+ action,
11475
+ busy: false,
11476
+ onPress: () => rowActionsNow.current.handToAgent(action, row)
11477
+ },
11478
+ action.id
11479
+ )) : null,
11480
+ host.talkToRecord ? /* @__PURE__ */ jsx42(
11481
+ Button27,
11482
+ {
11483
+ size: "sm",
11484
+ variant: "ghost",
11485
+ onClick: () => rowActionsNow.current.host.talkToRecord?.({
11486
+ tableId,
11487
+ recordId: row.id,
11488
+ // ONE NAMING CALL (`names.ts`), not this file's own join
11489
+ // of a Table's title column to a document.
11490
+ title: recordNameIn(
11491
+ rowActionsNow.current.table,
11492
+ row.document ?? {}
11493
+ )
11494
+ }),
11495
+ children: "Talk"
11496
+ }
11497
+ ) : null,
11498
+ Boolean(editable) && rights.write ? /* @__PURE__ */ jsx42(
11499
+ Button27,
11500
+ {
11501
+ size: "sm",
11502
+ variant: "ghost",
11503
+ "data-records-duplicate": row.id,
11504
+ title: "Duplicate this record",
11505
+ onClick: (event) => {
11506
+ event.stopPropagation();
11507
+ void rowActionsNow.current.duplicate(row);
11508
+ },
11509
+ children: "Duplicate"
11344
11510
  }
11345
- ), mayAddRow ? /* @__PURE__ */ jsx42(AddRowLine, { onAdd: () => void addRowHere(), busy: added.busy }) : null), controls.headerMenuFor, controls.filterFor);
11346
- return merged && rights.structure && editable ? [...drawn, addColumnColumn(controls.addColumn)] : drawn;
11511
+ ) : null,
11512
+ canWrite && mayWriteRow(row.id) ? /* @__PURE__ */ jsx42(
11513
+ Button27,
11514
+ {
11515
+ size: "sm",
11516
+ variant: "ghost",
11517
+ title: "Delete this record",
11518
+ onClick: () => setDeleting(row),
11519
+ children: "Delete"
11520
+ }
11521
+ ) : null
11522
+ ] }),
11523
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `hasTalk`/`hasAgent` stand for the host ports the JSX reads; handlers run through `rowActionsNow`.
11524
+ [canWrite, mayWriteRow, tableActions.runnable, tableActions.agent, runner.busy, hasTalk, hasAgent, editable, rights.write]
11525
+ );
11526
+ const headerHeld = useRef14(/* @__PURE__ */ new Map());
11527
+ const menuLatest = useRef14(/* @__PURE__ */ new Map());
11528
+ const addColumnDef = useMemo22(() => addColumnColumn(() => addColumnNow.current()), []);
11529
+ const drawnColumns = useMemo22(
11530
+ () => {
11531
+ const filterFor = controls.filterFor;
11532
+ const drawn = withHeaderMenus(
11533
+ bodyColumns,
11534
+ (key) => holdHeaderMenu(headerHeld.current, menuLatest.current, key, controls.headerMenuFor(key)),
11535
+ (key) => holdByContent(headerHeld.current, `filter:${key}`, filterFor(key))
11536
+ );
11537
+ return merged && rights.structure && editable ? [...drawn, addColumnDef] : drawn;
11347
11538
  },
11348
- [fieldColumns, onOpenRecord, table.data, ordered, chosenSummaries, summaries, chooseSummary, controls.headerMenuFor, controls.filterFor, mayAddRow, addRowHere, added.busy, merged, rights.structure, editable, controls.addColumn]
11539
+ [bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable, addColumnDef]
11349
11540
  );
11541
+ const columnsHeld = useRef14(null);
11542
+ const columns = columnsHeld.current !== null && sameColumns(columnsHeld.current, drawnColumns) ? columnsHeld.current : drawnColumns;
11543
+ columnsHeld.current = columns;
11350
11544
  const [localColumns, setLocalColumns] = useState31(
11351
11545
  null
11352
11546
  );
@@ -11419,6 +11613,274 @@ function Grid({
11419
11613
  } : {}
11420
11614
  };
11421
11615
  }, [presentation, onPresentationChange, writeLook, merged, phone]);
11616
+ const queryTotal = records.data?.total ?? rows.length;
11617
+ const tableQuery = useMemo22(
11618
+ () => merged ? {
11619
+ // THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
11620
+ mode: "controlled",
11621
+ state: query,
11622
+ totalItems: queryTotal,
11623
+ onStateChange: setQuery,
11624
+ sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
11625
+ } : { mode: "controlled-local", state: query, onStateChange: setQuery },
11626
+ [merged, query, queryTotal]
11627
+ );
11628
+ const showAddLine = mayAddRow && rows.length > 0;
11629
+ const showSummaryLine = rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0);
11630
+ const footerFace = JSON.stringify([
11631
+ showAddLine ? added.busy : null,
11632
+ showSummaryLine ? [chosenSummaries, summaries.values, summaries.rowCount, summaries.loading] : null
11633
+ ]);
11634
+ const footerRows = useMemo22(
11635
+ () => [...showAddLine ? [ADD_ROW] : [], ...showSummaryLine ? [SUMMARY_ROW] : []],
11636
+ // `footerFace`, `ordered` and the summaries' operations are what the footer's cells draw.
11637
+ // eslint-disable-next-line react-hooks/exhaustive-deps
11638
+ [showAddLine, showSummaryLine, footerFace, ordered, summaries.operations, summaries.askOperations]
11639
+ );
11640
+ const coverageTotal = typeof records.data?.total === "number" ? records.data.total : null;
11641
+ const tableCoverage = useMemo22(
11642
+ () => ({
11643
+ answeredBy: merged ? "source" : "client",
11644
+ ...merged && coverageTotal !== null ? { matched: coverageTotal } : {},
11645
+ noun: "record",
11646
+ ...coverageTotal !== null ? { total: coverageTotal } : {}
11647
+ }),
11648
+ [merged, coverageTotal]
11649
+ );
11650
+ const sheetNow = useRef14({ merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify });
11651
+ sheetNow.current = { merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify };
11652
+ const hasNotify = Boolean(host.notify);
11653
+ const readOnlyReason = !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write");
11654
+ const spreadsheetConfig = useMemo22(
11655
+ () => canWrite && editable ? {
11656
+ enabled: true,
11657
+ writableColumnIds: pasteableKeys,
11658
+ onPatch: (patches, intent) => {
11659
+ const now = sheetNow.current;
11660
+ if (now.merged) {
11661
+ void now.writeRange(patches, intent);
11662
+ return;
11663
+ }
11664
+ for (const patch of patches) {
11665
+ now.editing.setCell(patch.rowId, patch.columnId, patch.value);
11666
+ }
11667
+ },
11668
+ ...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
11669
+ columnRefusal: (columnId) => {
11670
+ const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
11671
+ if (!field2) return `This table has no column called ${columnId}.`;
11672
+ return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
11673
+ },
11674
+ validateCell: ({ columnId, value }) => {
11675
+ const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
11676
+ if (!field2) return `This table has no column called ${columnId}.`;
11677
+ const judged = valueFromPastedText(field2, value === null ? "" : String(value));
11678
+ return "refusal" in judged ? judged.refusal : void 0;
11679
+ },
11680
+ onPastePlan: receivePastePlan,
11681
+ /*
11682
+ * TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
11683
+ * opens ITS OWN editor — the Field's — holding what they typed (a letter, an
11684
+ * IME composition, an Option/AltGr character, dictation). A worked-out column
11685
+ * or a row this person may not write answers false, so nothing is swallowed.
11686
+ */
11687
+ onEditRequest: (address, seed) => {
11688
+ const now = sheetNow.current;
11689
+ const field2 = now.fields.find((f) => f.key === address.columnId);
11690
+ if (!field2 || !fieldIsEditable(field2) || !now.mayWriteRow(address.rowId)) return false;
11691
+ if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
11692
+ now.editing.begin({ rowId: address.rowId, key: field2.key }, seed);
11693
+ return true;
11694
+ },
11695
+ ...hasNotify ? { notify: (message) => sheetNow.current.notify?.success(message) } : {}
11696
+ } : {
11697
+ /*
11698
+ * RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
11699
+ * who may not change this table may still drag out a range, select all and press
11700
+ * Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
11701
+ * on a table whose writes were refused. Every write gesture (cut, paste, clear,
11702
+ * fill, undo) says why it cannot, in the store's own words.
11703
+ */
11704
+ enabled: true,
11705
+ readOnly: true,
11706
+ ...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
11707
+ readOnlyReason
11708
+ },
11709
+ [canWrite, editable, pasteableKeys, merged, followSelection, receivePastePlan, hasNotify, readOnlyReason]
11710
+ );
11711
+ const toolbarFields = fields.data;
11712
+ const hasSeveralRows = rows.length > 1;
11713
+ const editingBusy = editing.busy;
11714
+ const addRecord = editing.addRecord;
11715
+ const phoneSheetTools = merged && phone ? controls.sheetTools : null;
11716
+ const tableToolbar = useMemo22(
11717
+ () => ({
11718
+ search: true,
11719
+ searchPlaceholder: "Search records",
11720
+ /*
11721
+ * A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
11722
+ * the row, so Tools is never scrolled out of reach; the search is the same query.
11723
+ */
11724
+ ...merged && phone ? {
11725
+ customSearch: /* @__PURE__ */ jsx42("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", "data-records-grid-phone-search": "", children: /* @__PURE__ */ jsx42(
11726
+ "input",
11727
+ {
11728
+ type: "search",
11729
+ value: query.search,
11730
+ placeholder: "Search",
11731
+ "aria-label": "Search records",
11732
+ onChange: (event) => {
11733
+ const search = event.target.value;
11734
+ setQuery((held) => ({ ...held, search, page: 1 }));
11735
+ },
11736
+ 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",
11737
+ style: { fontSize: "16px" }
11738
+ }
11739
+ ) })
11740
+ } : {},
11741
+ /* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
11742
+ sort are drawn INTO the page's toolbar row, never a second bar under it. */
11743
+ ...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
11744
+ // SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
11745
+ // There is no third state: this package ships no disabled variant of
11746
+ // these controls, so a member cannot be shown a dead one by mistake.
11747
+ /*
11748
+ * WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
11749
+ * room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
11750
+ * record stay beside it — the primary action is never folded); on a phone EVERY control
11751
+ * — the table's own (clear, copy/export, group, columns), these, the search, the sort and
11752
+ * the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
11753
+ */
11754
+ actions: /* @__PURE__ */ jsxs36(Fragment15, { children: [
11755
+ manual ? /* @__PURE__ */ jsx42(
11756
+ SortStateControl,
11757
+ {
11758
+ ...handOrderSortState({
11759
+ sortName: columnSort ? columnLabel(toolbarFields ?? [], columnSort.id) : null,
11760
+ direction: columnSort?.direction ?? null,
11761
+ onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
11762
+ ...viewOrder?.onUseSort && columnSort ? {
11763
+ onUseSort: () => {
11764
+ const sort = columnSort;
11765
+ const chosen = { field: sort.id, direction: sort.direction };
11766
+ setHeldOrder({ order: "sorted", sorts: [chosen] });
11767
+ viewOrder.onUseSort?.(chosen);
11768
+ host.notify?.success(
11769
+ `Sorted by ${columnLabel(toolbarFields ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
11770
+ );
11771
+ }
11772
+ } : {}
11773
+ })
11774
+ }
11775
+ ) : null,
11776
+ viewId && viewOrder?.onPlaced && hasSeveralRows ? /* @__PURE__ */ jsx42(Button27, { size: "sm", variant: "ghost", onClick: () => setReordering(true), "data-records-reorder-open": "", children: "Reorder" }) : null,
11777
+ !merged && Boolean(editable) && rights.write ? /* @__PURE__ */ jsx42(Button27, { size: "sm", disabled: editingBusy, onClick: () => void addRecord(), children: editingBusy ? "Adding\u2026" : "New record" }) : null,
11778
+ rights.structure && onAddField ? /* @__PURE__ */ jsx42(AddFieldButton, { onClick: onAddField }) : null,
11779
+ /* @__PURE__ */ jsx42(
11780
+ GridLayoutMenu,
11781
+ {
11782
+ look,
11783
+ firstColumn: sortedKeys[0] ? {
11784
+ key: sortedKeys[0],
11785
+ name: (() => {
11786
+ const first = (toolbarFields ?? []).find((f) => f.key === sortedKeys[0]);
11787
+ return first ? fieldName(first) : humanize(sortedKeys[0]);
11788
+ })()
11789
+ } : null,
11790
+ onChange: changeLayout,
11791
+ kept: Boolean(onPresentationChange)
11792
+ }
11793
+ )
11794
+ ] }),
11795
+ ...merged ? {
11796
+ overflow: {
11797
+ mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
11798
+ pinned: /* @__PURE__ */ jsxs36(Fragment15, { children: [
11799
+ /* @__PURE__ */ jsx42(UndoRedoLive, { source: undoSource }),
11800
+ Boolean(editable) && rights.write ? /* @__PURE__ */ jsx42(Button27, { size: "sm", className: "shrink-0", disabled: added.busy, onClick: () => addRowRef.current(), children: added.busy ? "Adding\u2026" : "New record" }) : null
11801
+ ] }),
11802
+ ...phone ? {
11803
+ sheetTop: /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-1.5 pb-1", "data-records-grid-sheet-top": "", "data-matrx-table-tools-keep-open": "", children: [
11804
+ /* @__PURE__ */ jsx42(
11805
+ "input",
11806
+ {
11807
+ type: "search",
11808
+ value: query.search,
11809
+ placeholder: "Search",
11810
+ "aria-label": "Search records",
11811
+ onChange: (event) => {
11812
+ const search = event.target.value;
11813
+ setQuery((held) => ({ ...held, search, page: 1 }));
11814
+ },
11815
+ className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
11816
+ style: { fontSize: "16px" }
11817
+ }
11818
+ ),
11819
+ /* @__PURE__ */ jsxs36("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
11820
+ /* @__PURE__ */ jsx42("span", { className: "shrink-0", children: "Sort by" }),
11821
+ /* @__PURE__ */ jsxs36(
11822
+ "select",
11823
+ {
11824
+ "aria-label": "Sort by",
11825
+ value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
11826
+ onChange: (event) => {
11827
+ const [id, direction] = event.target.value.split(":");
11828
+ setQuery((held) => ({
11829
+ ...held,
11830
+ page: 1,
11831
+ sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
11832
+ }));
11833
+ },
11834
+ className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
11835
+ style: { fontSize: "16px" },
11836
+ children: [
11837
+ /* @__PURE__ */ jsx42("option", { value: "", children: "No sort" }),
11838
+ ordered.flatMap((field2) => [
11839
+ /* @__PURE__ */ jsx42("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
11840
+ /* @__PURE__ */ jsx42("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
11841
+ ])
11842
+ ]
11843
+ }
11844
+ )
11845
+ ] }),
11846
+ phoneSheetTools
11847
+ ] }),
11848
+ ...sheetExtras ? { sheetExtras } : {}
11849
+ } : {}
11850
+ }
11851
+ } : {}
11852
+ }),
11853
+ [
11854
+ merged,
11855
+ phone,
11856
+ query.search,
11857
+ query.sort,
11858
+ toolbarSlot,
11859
+ manual,
11860
+ columnSort,
11861
+ toolbarFields,
11862
+ viewOrder,
11863
+ host,
11864
+ viewId,
11865
+ hasSeveralRows,
11866
+ editable,
11867
+ rights.write,
11868
+ rights.structure,
11869
+ editingBusy,
11870
+ addRecord,
11871
+ onAddField,
11872
+ look,
11873
+ sortedKeys,
11874
+ changeLayout,
11875
+ onPresentationChange,
11876
+ toolbarFold2,
11877
+ undoSource,
11878
+ added.busy,
11879
+ ordered,
11880
+ phoneSheetTools,
11881
+ sheetExtras
11882
+ ]
11883
+ );
11422
11884
  const labelOfField = useCallback16(
11423
11885
  (key) => {
11424
11886
  const held = (fields.data ?? []).find((f) => f.key === key);
@@ -11579,46 +12041,27 @@ function Grid({
11579
12041
  ...fills ? { className: "min-h-0 flex-1" } : {},
11580
12042
  data: rows,
11581
12043
  columns,
11582
- getRowId: (row) => row.id,
12044
+ getRowId: rowIdOf,
12045
+ rowVersion,
11583
12046
  isLoading: nothingToShowYet,
11584
12047
  isFetching: refreshing,
11585
12048
  density: host.density ?? "condensed",
11586
12049
  viewTabs: false,
11587
12050
  stickyHeader: true,
11588
12051
  pageSize,
11589
- query: merged ? {
11590
- // THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
11591
- mode: "controlled",
11592
- state: query,
11593
- totalItems: records.data?.total ?? rows.length,
11594
- onStateChange: setQuery,
11595
- sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
11596
- } : { mode: "controlled-local", state: query, onStateChange: setQuery },
12052
+ query: tableQuery,
11597
12053
  tableClassName: cn20(
11598
12054
  !look.wrap && "[&_tbody_td]:max-w-[28rem] [&_tbody_td]:overflow-hidden [&_tbody_td]:text-ellipsis [&_tbody_td]:whitespace-nowrap! [&_tbody_td_*]:whitespace-nowrap!",
11599
12055
  look.fit === "scroll" && "sm:[&>table]:w-max! sm:[&>table]:max-w-none! sm:[&>table]:min-w-full!"
11600
12056
  ),
11601
- ...(() => {
11602
- const footerRows = [
11603
- ...mayAddRow && rows.length > 0 ? [ADD_ROW] : [],
11604
- ...rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0) ? [SUMMARY_ROW] : []
11605
- ];
11606
- return footerRows.length > 0 ? { footerRows } : {};
11607
- })(),
11608
- ...table.data ? {
11609
- copy: gridCopyConfig({
11610
- tableId,
11611
- tableName: tableName(table.data),
11612
- fields: ordered,
11613
- rowLabel: (row) => recordNameIn(table.data, row.document ?? {})
11614
- })
11615
- } : {},
12057
+ footerRows,
12058
+ ...copyConfig ? { copy: copyConfig } : {},
11616
12059
  ...offerSelection ? {
11617
12060
  selection: {
11618
12061
  selectedIds,
11619
12062
  onSelectedIdsChange: setSelectedIds,
11620
12063
  noun,
11621
- isRowSelectable: (row) => row.id !== SUMMARY_ROW.id && mayWriteRow(row.id),
12064
+ isRowSelectable,
11622
12065
  actions: (_selected, ids) => /* @__PURE__ */ jsxs36(Fragment15, { children: [
11623
12066
  merged ? /* @__PURE__ */ jsx42(
11624
12067
  BulkActions,
@@ -11645,154 +12088,18 @@ function Grid({
11645
12088
  ] })
11646
12089
  }
11647
12090
  } : {},
11648
- detail: { enabled: false },
11649
- window: { enabled: false },
12091
+ detail: DETAIL_OFF,
12092
+ window: WINDOW_OFF,
11650
12093
  ...controls.contextMenu ? { contextMenu: controls.contextMenu } : {},
11651
- ...canWrite && editable ? {
11652
- spreadsheet: {
11653
- enabled: true,
11654
- writableColumnIds: pasteableKeys,
11655
- onPatch: (patches, intent) => {
11656
- if (merged) {
11657
- void writeRange(patches, intent);
11658
- return;
11659
- }
11660
- for (const patch of patches) {
11661
- editing.setCell(patch.rowId, patch.columnId, patch.value);
11662
- }
11663
- },
11664
- ...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
11665
- columnRefusal: (columnId) => {
11666
- const field2 = (fields.data ?? []).find((f) => f.key === columnId);
11667
- if (!field2) return `This table has no column called ${columnId}.`;
11668
- return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
11669
- },
11670
- validateCell: ({ columnId, value }) => {
11671
- const field2 = (fields.data ?? []).find((f) => f.key === columnId);
11672
- if (!field2) return `This table has no column called ${columnId}.`;
11673
- const judged = valueFromPastedText(field2, value === null ? "" : String(value));
11674
- return "refusal" in judged ? judged.refusal : void 0;
11675
- },
11676
- onPastePlan: receivePastePlan,
11677
- /*
11678
- * TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
11679
- * opens ITS OWN editor — the Field's — holding what they typed (a letter, an
11680
- * IME composition, an Option/AltGr character, dictation). A worked-out column
11681
- * or a row this person may not write answers false, so nothing is swallowed.
11682
- */
11683
- onEditRequest: (address, seed) => {
11684
- const field2 = (fields.data ?? []).find((f) => f.key === address.columnId);
11685
- if (!field2 || !fieldIsEditable(field2) || !mayWriteRow(address.rowId)) return false;
11686
- if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
11687
- editing.begin({ rowId: address.rowId, key: field2.key }, seed);
11688
- return true;
11689
- },
11690
- ...host.notify ? { notify: (message) => host.notify?.success(message) } : {}
11691
- }
11692
- } : {
11693
- /*
11694
- * RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
11695
- * who may not change this table may still drag out a range, select all and press
11696
- * Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
11697
- * on a table whose writes were refused. Every write gesture (cut, paste, clear,
11698
- * fill, undo) says why it cannot, in the store's own words.
11699
- */
11700
- spreadsheet: {
11701
- enabled: true,
11702
- readOnly: true,
11703
- ...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
11704
- readOnlyReason: !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write")
11705
- }
11706
- },
12094
+ spreadsheet: spreadsheetConfig,
11707
12095
  ...groupingConfig ? { grouping: groupingConfig } : {},
11708
12096
  columnState,
11709
12097
  ...look.rowHeight ? { rowHeight: look.rowHeight } : {},
11710
- virtualize: fills ? { threshold: 40 } : true,
11711
- coverage: {
11712
- answeredBy: merged ? "source" : "client",
11713
- ...merged && typeof records.data?.total === "number" ? { matched: records.data.total } : {},
11714
- noun: "record",
11715
- ...typeof records.data?.total === "number" ? { total: records.data.total } : {}
11716
- },
11717
- ...!tableStyleIsEmpty2(paint.style) ? {
11718
- tableStyle: {
11719
- style: paint.style,
11720
- // A choice is read as its OPTION (`paintByOption`): the rule, the value and the
11721
- // colour a column keeps for it all name the same option, however it is stored.
11722
- documentOf: (row) => paint.documentOf(row.document ?? {}),
11723
- // A value its column keeps a colour for wears THAT colour (`choiceLooks.ts`);
11724
- // any other value still paints from its word.
11725
- choiceColorFor: paint.choiceColorFor
11726
- }
11727
- } : {},
12098
+ virtualize: fills ? VIRTUALIZE_FILLED : true,
12099
+ coverage: tableCoverage,
12100
+ ...tableStyleProp ? { tableStyle: tableStyleProp } : {},
11728
12101
  ...onOpenRecord ? { onRowOpen: (row) => onOpenRecord(row.id) } : {},
11729
- ...canWrite || host.talkToRecord || host.runAgentAction && tableActions.agent.length > 0 ? {
11730
- // ABSENT, NEVER DEAD: a row this person may not delete carries no
11731
- // Delete at all, rather than one that refuses when pressed — and a
11732
- // deployment with no chat surface bound carries no Talk.
11733
- rowActions: (row) => /* @__PURE__ */ jsxs36(Fragment15, { children: [
11734
- canWrite && mayWriteRow(row.id) ? tableActions.runnable.map((action) => /* @__PURE__ */ jsx42(
11735
- ActionButton,
11736
- {
11737
- action,
11738
- busy: runner.busy === action.id,
11739
- onPress: () => runner.press(action, [row.id])
11740
- },
11741
- action.id
11742
- )) : null,
11743
- host.runAgentAction ? tableActions.agent.map((action) => /* @__PURE__ */ jsx42(
11744
- ActionButton,
11745
- {
11746
- action,
11747
- busy: false,
11748
- onPress: () => handToAgent(action, row)
11749
- },
11750
- action.id
11751
- )) : null,
11752
- host.talkToRecord ? /* @__PURE__ */ jsx42(
11753
- Button27,
11754
- {
11755
- size: "sm",
11756
- variant: "ghost",
11757
- onClick: () => host.talkToRecord?.({
11758
- tableId,
11759
- recordId: row.id,
11760
- // ONE NAMING CALL (`names.ts`), not this file's own join
11761
- // of a Table's title column to a document.
11762
- title: recordNameIn(
11763
- table.data,
11764
- row.document ?? {}
11765
- )
11766
- }),
11767
- children: "Talk"
11768
- }
11769
- ) : null,
11770
- Boolean(editable) && rights.write ? /* @__PURE__ */ jsx42(
11771
- Button27,
11772
- {
11773
- size: "sm",
11774
- variant: "ghost",
11775
- "data-records-duplicate": row.id,
11776
- title: "Duplicate this record",
11777
- onClick: (event) => {
11778
- event.stopPropagation();
11779
- void duplicate(row);
11780
- },
11781
- children: "Duplicate"
11782
- }
11783
- ) : null,
11784
- canWrite && mayWriteRow(row.id) ? /* @__PURE__ */ jsx42(
11785
- Button27,
11786
- {
11787
- size: "sm",
11788
- variant: "ghost",
11789
- title: "Delete this record",
11790
- onClick: () => setDeleting(row),
11791
- children: "Delete"
11792
- }
11793
- ) : null
11794
- ] })
11795
- } : {},
12102
+ ...offersRowActions ? { rowActions } : {},
11796
12103
  emptyState: emptyWords({
11797
12104
  error: records.error,
11798
12105
  asked: questionIsAsked({ search: query.search, columnFilters: query.columnFilters, filter }),
@@ -11801,142 +12108,7 @@ function Grid({
11801
12108
  ...mayAddRow ? { addFirst: /* @__PURE__ */ jsx42(AddFirstRowButton, { onAdd: () => void addRowHere(), busy: added.busy }) } : {},
11802
12109
  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."
11803
12110
  }),
11804
- toolbar: {
11805
- search: true,
11806
- searchPlaceholder: "Search records",
11807
- /*
11808
- * A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
11809
- * the row, so Tools is never scrolled out of reach; the search is the same query.
11810
- */
11811
- ...merged && phone ? {
11812
- customSearch: /* @__PURE__ */ jsx42("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", "data-records-grid-phone-search": "", children: /* @__PURE__ */ jsx42(
11813
- "input",
11814
- {
11815
- type: "search",
11816
- value: query.search,
11817
- placeholder: "Search",
11818
- "aria-label": "Search records",
11819
- onChange: (event) => {
11820
- const search = event.target.value;
11821
- setQuery((held) => ({ ...held, search, page: 1 }));
11822
- },
11823
- 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",
11824
- style: { fontSize: "16px" }
11825
- }
11826
- ) })
11827
- } : {},
11828
- /* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
11829
- sort are drawn INTO the page's toolbar row, never a second bar under it. */
11830
- ...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
11831
- // SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
11832
- // There is no third state: this package ships no disabled variant of
11833
- // these controls, so a member cannot be shown a dead one by mistake.
11834
- /*
11835
- * WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
11836
- * room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
11837
- * record stay beside it — the primary action is never folded); on a phone EVERY control
11838
- * — the table's own (clear, copy/export, group, columns), these, the search, the sort and
11839
- * the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
11840
- */
11841
- actions: /* @__PURE__ */ jsxs36(Fragment15, { children: [
11842
- manual ? /* @__PURE__ */ jsx42(
11843
- SortStateControl,
11844
- {
11845
- ...handOrderSortState({
11846
- sortName: columnSort ? columnLabel(fields.data ?? [], columnSort.id) : null,
11847
- direction: columnSort?.direction ?? null,
11848
- onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
11849
- ...viewOrder?.onUseSort && columnSort ? {
11850
- onUseSort: () => {
11851
- const sort = columnSort;
11852
- const chosen = { field: sort.id, direction: sort.direction };
11853
- setHeldOrder({ order: "sorted", sorts: [chosen] });
11854
- viewOrder.onUseSort?.(chosen);
11855
- host.notify?.success(
11856
- `Sorted by ${columnLabel(fields.data ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
11857
- );
11858
- }
11859
- } : {}
11860
- })
11861
- }
11862
- ) : null,
11863
- viewId && viewOrder?.onPlaced && rows.length > 1 ? /* @__PURE__ */ jsx42(Button27, { size: "sm", variant: "ghost", onClick: () => setReordering(true), "data-records-reorder-open": "", children: "Reorder" }) : null,
11864
- !merged && Boolean(editable) && rights.write ? /* @__PURE__ */ jsx42(Button27, { size: "sm", disabled: editing.busy, onClick: () => void editing.addRecord(), children: editing.busy ? "Adding\u2026" : "New record" }) : null,
11865
- rights.structure && onAddField ? /* @__PURE__ */ jsx42(AddFieldButton, { onClick: onAddField }) : null,
11866
- /* @__PURE__ */ jsx42(
11867
- GridLayoutMenu,
11868
- {
11869
- look,
11870
- firstColumn: sortedKeys[0] ? {
11871
- key: sortedKeys[0],
11872
- name: (() => {
11873
- const first = (fields.data ?? []).find((f) => f.key === sortedKeys[0]);
11874
- return first ? fieldName(first) : humanize(sortedKeys[0]);
11875
- })()
11876
- } : null,
11877
- onChange: changeLayout,
11878
- kept: Boolean(onPresentationChange)
11879
- }
11880
- )
11881
- ] }),
11882
- ...merged ? {
11883
- overflow: {
11884
- mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
11885
- pinned: /* @__PURE__ */ jsxs36(Fragment15, { children: [
11886
- /* @__PURE__ */ jsx42(UndoRedoButtons, { undo }),
11887
- Boolean(editable) && rights.write ? /* @__PURE__ */ jsx42(Button27, { size: "sm", className: "shrink-0", disabled: added.busy, onClick: () => void addRowHere(), children: added.busy ? "Adding\u2026" : "New record" }) : null
11888
- ] }),
11889
- ...phone ? {
11890
- sheetTop: /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-1.5 pb-1", "data-records-grid-sheet-top": "", "data-matrx-table-tools-keep-open": "", children: [
11891
- /* @__PURE__ */ jsx42(
11892
- "input",
11893
- {
11894
- type: "search",
11895
- value: query.search,
11896
- placeholder: "Search",
11897
- "aria-label": "Search records",
11898
- onChange: (event) => {
11899
- const search = event.target.value;
11900
- setQuery((held) => ({ ...held, search, page: 1 }));
11901
- },
11902
- className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
11903
- style: { fontSize: "16px" }
11904
- }
11905
- ),
11906
- /* @__PURE__ */ jsxs36("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
11907
- /* @__PURE__ */ jsx42("span", { className: "shrink-0", children: "Sort by" }),
11908
- /* @__PURE__ */ jsxs36(
11909
- "select",
11910
- {
11911
- "aria-label": "Sort by",
11912
- value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
11913
- onChange: (event) => {
11914
- const [id, direction] = event.target.value.split(":");
11915
- setQuery((held) => ({
11916
- ...held,
11917
- page: 1,
11918
- sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
11919
- }));
11920
- },
11921
- className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
11922
- style: { fontSize: "16px" },
11923
- children: [
11924
- /* @__PURE__ */ jsx42("option", { value: "", children: "No sort" }),
11925
- ordered.flatMap((field2) => [
11926
- /* @__PURE__ */ jsx42("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
11927
- /* @__PURE__ */ jsx42("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
11928
- ])
11929
- ]
11930
- }
11931
- )
11932
- ] }),
11933
- controls.sheetTools
11934
- ] }),
11935
- ...sheetExtras ? { sheetExtras } : {}
11936
- } : {}
11937
- }
11938
- } : {}
11939
- }
12111
+ toolbar: tableToolbar
11940
12112
  },
11941
12113
  `records-grid-${tableId}:${openingKey}`
11942
12114
  )
@@ -12038,6 +12210,58 @@ function Grid({
12038
12210
  }
12039
12211
  ) });
12040
12212
  }
12213
+ var DETAIL_OFF = { enabled: false };
12214
+ var WINDOW_OFF = { enabled: false };
12215
+ var VIRTUALIZE_FILLED = { threshold: 40 };
12216
+ function menuFace(items) {
12217
+ return JSON.stringify(items, (_key, value) => typeof value === "function" ? "\u0192" : value);
12218
+ }
12219
+ function menuThroughLatest(items, at, latest) {
12220
+ return items.map((item) => {
12221
+ const path = `${at}/${item.id}`;
12222
+ return {
12223
+ ...item,
12224
+ ...item.onSelect ? { onSelect: () => latest.get(path)?.() } : {},
12225
+ ...item.submenu ? { submenu: menuThroughLatest(item.submenu, path, latest) } : {}
12226
+ };
12227
+ });
12228
+ }
12229
+ function bindLatest(items, at, latest) {
12230
+ for (const item of items) {
12231
+ const path = `${at}/${item.id}`;
12232
+ if (item.onSelect) latest.set(path, item.onSelect);
12233
+ if (item.submenu) bindLatest(item.submenu, path, latest);
12234
+ }
12235
+ }
12236
+ function holdHeaderMenu(held, latest, key, items) {
12237
+ if (!items) return items;
12238
+ const at = `menu:${key}`;
12239
+ bindLatest(items, at, latest);
12240
+ const sig = menuFace(items);
12241
+ const was = held.get(at);
12242
+ if (was && was.sig === sig) return was.value;
12243
+ const value = menuThroughLatest(items, at, latest);
12244
+ held.set(at, { sig, value });
12245
+ return value;
12246
+ }
12247
+ function holdByContent(held, at, value) {
12248
+ if (value === void 0) return value;
12249
+ const sig = JSON.stringify(value);
12250
+ const was = held.get(at);
12251
+ if (was && was.sig === sig) return was.value;
12252
+ held.set(at, { sig, value });
12253
+ return value;
12254
+ }
12255
+ function sameColumns(a, b) {
12256
+ if (a === b) return true;
12257
+ if (a.length !== b.length) return false;
12258
+ return a.every((column, i) => {
12259
+ const other = b[i];
12260
+ if (column === other) return true;
12261
+ const keys = Object.keys(column);
12262
+ return keys.length === Object.keys(other).length && keys.every((key) => Object.is(column[key], other[key]));
12263
+ });
12264
+ }
12041
12265
  function withHeaderMenus(columns, menuFor, filterFor) {
12042
12266
  return columns.map((column) => {
12043
12267
  const items = menuFor(String(column.id));
@@ -26424,6 +26648,7 @@ function useEmbedHandshake(args) {
26424
26648
  }
26425
26649
 
26426
26650
  // src/RecordsMount.tsx
26651
+ import { useRef as useRef26 } from "react";
26427
26652
  import { RecordsProvider } from "@ai-matrx/records/react";
26428
26653
  import { jsx as jsx91 } from "react/jsx-runtime";
26429
26654
  var STORE_DECIDES_REASON = "This host offers exactly what the record store says this person may do: the level comes back with the table on the read door, so a control they cannot use is never drawn in the first place.";
@@ -26436,10 +26661,26 @@ function RecordsMount({
26436
26661
  letTheStoreDecideRights,
26437
26662
  children
26438
26663
  }) {
26439
- const bound = { ...host ?? {} };
26664
+ const bound = useShallowStableHost(host);
26440
26665
  void letTheStoreDecideRights;
26441
26666
  return /* @__PURE__ */ jsx91(RecordsProvider, { config, children: /* @__PURE__ */ jsx91(RecordsUiProvider, { value: bound, children: /* @__PURE__ */ jsx91(RecordLabelProvider, { children }) }) });
26442
26667
  }
26668
+ function useShallowStableHost(host) {
26669
+ const held = useRef26(null);
26670
+ const next = host ?? {};
26671
+ const previous = held.current;
26672
+ if (previous !== null && sameParts(previous, next)) return previous;
26673
+ const bound = { ...next };
26674
+ held.current = bound;
26675
+ return bound;
26676
+ }
26677
+ function sameParts(a, b) {
26678
+ const aKeys = Object.keys(a);
26679
+ const bKeys = Object.keys(b);
26680
+ if (aKeys.length !== bKeys.length) return false;
26681
+ for (const key of aKeys) if (a[key] !== b[key]) return false;
26682
+ return true;
26683
+ }
26443
26684
  function personActor(userId) {
26444
26685
  return userId ? { actor: "user", user_id: userId } : { actor: "user" };
26445
26686
  }
@@ -27042,7 +27283,7 @@ function TablesHome({ onOpenTable, organizationName, makingOnly = false, classNa
27042
27283
  }
27043
27284
 
27044
27285
  // src/TablePage.tsx
27045
- import { useCallback as useCallback51, useEffect as useEffect72, useRef as useRef26, useState as useState84 } from "react";
27286
+ import { useCallback as useCallback51, useEffect as useEffect72, useRef as useRef27, useState as useState84 } from "react";
27046
27287
  import { archivedByLine as archivedByLine2 } from "@ai-matrx/records";
27047
27288
  import { useFields as useFields26, useRecordsClient as useRecordsClient64, useTable as useTable24 } from "@ai-matrx/records/react";
27048
27289
  import {
@@ -27684,7 +27925,7 @@ function TablePage({
27684
27925
  lookDiffers: view ? lookDiffers(view, look) : false
27685
27926
  };
27686
27927
  const shownSignature = JSON.stringify(shownReport);
27687
- const reportRef = useRef26(onShownViewChange);
27928
+ const reportRef = useRef27(onShownViewChange);
27688
27929
  reportRef.current = onShownViewChange;
27689
27930
  useEffect72(() => {
27690
27931
  reportRef.current?.(JSON.parse(shownSignature));