@ai-matrx/records-ui 0.93.3 → 0.93.6

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
@@ -54,6 +54,7 @@ __export(src_exports, {
54
54
  CHECKLIST_REQUIREMENT_LABEL: () => CHECKLIST_REQUIREMENT_LABEL,
55
55
  CONDITION_OPS: () => CONDITION_OPS,
56
56
  CONTEXT_POLICY_LABEL: () => CONTEXT_POLICY_LABEL,
57
+ CUSTOM_FIELD_ADDABLE_TYPES: () => ADDABLE_TYPES,
57
58
  CaptureRun: () => CaptureRun,
58
59
  CaptureSheet: () => CaptureSheet,
59
60
  ChartBlock: () => ChartBlock,
@@ -246,6 +247,7 @@ __export(src_exports, {
246
247
  conditionValue: () => conditionValue,
247
248
  controlFor: () => controlFor,
248
249
  currencyCodeFor: () => currencyCodeFor,
250
+ customFieldRequestProblems: () => customFieldRequestProblems,
249
251
  dashboardDeclareArgs: () => dashboardDeclareArgs,
250
252
  dashboardFromSummary: () => dashboardFromSummary,
251
253
  dateFormatFor: () => dateFormatFor,
@@ -7978,6 +7980,10 @@ function useMergedGridControls(args) {
7978
7980
  (columnKey) => enabled && fieldOf2(columnKey) ? headerMenuItems(columnKey, ctx) : void 0,
7979
7981
  [enabled, ctx, fieldOf2]
7980
7982
  );
7983
+ const columnsControl = (0, import_react40.useMemo)(
7984
+ () => enabled ? { order, hidden, onChange: keepColumns } : void 0,
7985
+ [enabled, order, hidden, keepColumns]
7986
+ );
7981
7987
  const close = () => setDialog(null);
7982
7988
  const dialogs = !enabled ? null : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
7983
7989
  colours.dialogs,
@@ -8109,7 +8115,7 @@ function useMergedGridControls(args) {
8109
8115
  headerMenuFor,
8110
8116
  filterFor,
8111
8117
  contextMenu,
8112
- columns: enabled ? { order, hidden, onChange: keepColumns } : void 0,
8118
+ columns: columnsControl,
8113
8119
  dialogs,
8114
8120
  addColumn: () => setDialog({ kind: "insert", anchor: null, side: "right" }),
8115
8121
  sheetTools: enabled ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
@@ -9639,7 +9645,43 @@ function useGridUndo(args) {
9639
9645
  [push, undo, redo, held, busy, notice, dismiss]
9640
9646
  );
9641
9647
  }
9642
- function UndoRedoButtons({ undo }) {
9648
+ function useUndoSource(undo) {
9649
+ const box = (0, import_react52.useRef)({
9650
+ undo,
9651
+ face: { back: undo.depth.back, forward: undo.depth.forward, busy: undo.busy },
9652
+ listeners: /* @__PURE__ */ new Set()
9653
+ });
9654
+ box.current.undo = undo;
9655
+ const source = (0, import_react52.useMemo)(
9656
+ () => ({
9657
+ face: () => box.current.face,
9658
+ subscribe: (listener) => {
9659
+ box.current.listeners.add(listener);
9660
+ return () => {
9661
+ box.current.listeners.delete(listener);
9662
+ };
9663
+ },
9664
+ undo: () => void box.current.undo.undo(),
9665
+ redo: () => void box.current.undo.redo()
9666
+ }),
9667
+ []
9668
+ );
9669
+ const { back, forward } = undo.depth;
9670
+ const busy = undo.busy;
9671
+ (0, import_react52.useLayoutEffect)(() => {
9672
+ const held = box.current.face;
9673
+ if (held.back === back && held.forward === forward && held.busy === busy) return;
9674
+ box.current.face = { back, forward, busy };
9675
+ for (const listener of box.current.listeners) listener();
9676
+ }, [back, forward, busy]);
9677
+ return source;
9678
+ }
9679
+ function UndoRedoLive({ source }) {
9680
+ const face = (0, import_react52.useSyncExternalStore)(source.subscribe, source.face, source.face);
9681
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(UndoRedoPair, { face, onUndo: source.undo, onRedo: source.redo });
9682
+ }
9683
+ function UndoRedoPair({ face, onUndo, onRedo }) {
9684
+ const undo = { depth: { back: face.back, forward: face.forward }, busy: face.busy };
9643
9685
  if (undo.depth.back === 0 && undo.depth.forward === 0) return null;
9644
9686
  return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
9645
9687
  /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
@@ -9651,7 +9693,7 @@ function UndoRedoButtons({ undo }) {
9651
9693
  title: undo.depth.back > 0 ? `Undo (Cmd-Z) \xB7 ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"} back` : "Nothing to undo",
9652
9694
  "aria-label": `Undo, ${undo.depth.back} ${undo.depth.back === 1 ? "step" : "steps"}`,
9653
9695
  disabled: undo.busy || undo.depth.back === 0,
9654
- onClick: () => void undo.undo(),
9696
+ onClick: onUndo,
9655
9697
  children: [
9656
9698
  "Undo ",
9657
9699
  /* @__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 +9709,7 @@ function UndoRedoButtons({ undo }) {
9667
9709
  title: undo.depth.forward > 0 ? `Redo (Shift-Cmd-Z) \xB7 ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"} forward` : "Nothing to redo",
9668
9710
  "aria-label": `Redo, ${undo.depth.forward} ${undo.depth.forward === 1 ? "step" : "steps"}`,
9669
9711
  disabled: undo.busy || undo.depth.forward === 0,
9670
- onClick: () => void undo.redo(),
9712
+ onClick: onRedo,
9671
9713
  children: [
9672
9714
  "Redo ",
9673
9715
  /* @__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 +11055,7 @@ function Grid({
11013
11055
  );
11014
11056
  const rowVersion = (0, import_react60.useCallback)((row) => editingNow.current.rowSignature(row.id), []);
11015
11057
  const undo = useGridUndo({ tableId, reload: records.reload, forget: editing.forget });
11058
+ const undoSource = useUndoSource(undo);
11016
11059
  pushWritten.current = (write) => {
11017
11060
  const field2 = (fields.data ?? []).find((f) => f.key === write.key);
11018
11061
  const name = changedRecordName(table.data, rows.find((r) => r.id === write.rowId)?.document ?? {}, write.key, write.after);
@@ -11564,9 +11607,14 @@ function Grid({
11564
11607
  );
11565
11608
  const footerNow = (0, import_react60.useRef)({
11566
11609
  summaryCell: () => null,
11567
- addLine: () => null
11610
+ addLine: () => null,
11611
+ choose: () => void 0,
11612
+ add: () => void 0
11568
11613
  });
11569
11614
  footerNow.current = {
11615
+ // The footer is memoised on what it shows (`footerRows` below), so its buttons act through here.
11616
+ choose: chooseSummary,
11617
+ add: () => void addRowHere(),
11570
11618
  summaryCell: (columnId) => {
11571
11619
  const field2 = ordered.find((f) => f.key === columnId);
11572
11620
  if (!field2) return null;
@@ -11579,11 +11627,11 @@ function Grid({
11579
11627
  value: summaries.values[columnId],
11580
11628
  rowCount: summaries.rowCount,
11581
11629
  summaries,
11582
- onChoose: (op2) => chooseSummary(columnId, op2)
11630
+ onChoose: (op2) => footerNow.current.choose(columnId, op2)
11583
11631
  }
11584
11632
  );
11585
11633
  },
11586
- addLine: () => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddRowLine, { onAdd: () => void addRowHere(), busy: added.busy })
11634
+ addLine: () => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddRowLine, { onAdd: () => footerNow.current.add(), busy: added.busy })
11587
11635
  };
11588
11636
  const addColumnNow = (0, import_react60.useRef)(controls.addColumn);
11589
11637
  addColumnNow.current = controls.addColumn;
@@ -11698,13 +11746,24 @@ function Grid({
11698
11746
  // eslint-disable-next-line react-hooks/exhaustive-deps -- `hasTalk`/`hasAgent` stand for the host ports the JSX reads; handlers run through `rowActionsNow`.
11699
11747
  [canWrite, mayWriteRow, tableActions.runnable, tableActions.agent, runner.busy, hasTalk, hasAgent, editable, rights.write]
11700
11748
  );
11701
- const columns = (0, import_react60.useMemo)(
11749
+ const headerHeld = (0, import_react60.useRef)(/* @__PURE__ */ new Map());
11750
+ const menuLatest = (0, import_react60.useRef)(/* @__PURE__ */ new Map());
11751
+ const addColumnDef = (0, import_react60.useMemo)(() => addColumnColumn(() => addColumnNow.current()), []);
11752
+ const drawnColumns = (0, import_react60.useMemo)(
11702
11753
  () => {
11703
- const drawn = withHeaderMenus(bodyColumns, controls.headerMenuFor, controls.filterFor);
11704
- return merged && rights.structure && editable ? [...drawn, addColumnColumn(() => addColumnNow.current())] : drawn;
11754
+ const filterFor = controls.filterFor;
11755
+ const drawn = withHeaderMenus(
11756
+ bodyColumns,
11757
+ (key) => holdHeaderMenu(headerHeld.current, menuLatest.current, key, controls.headerMenuFor(key)),
11758
+ (key) => holdByContent(headerHeld.current, `filter:${key}`, filterFor(key))
11759
+ );
11760
+ return merged && rights.structure && editable ? [...drawn, addColumnDef] : drawn;
11705
11761
  },
11706
- [bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable]
11762
+ [bodyColumns, controls.headerMenuFor, controls.filterFor, merged, rights.structure, editable, addColumnDef]
11707
11763
  );
11764
+ const columnsHeld = (0, import_react60.useRef)(null);
11765
+ const columns = columnsHeld.current !== null && sameColumns(columnsHeld.current, drawnColumns) ? columnsHeld.current : drawnColumns;
11766
+ columnsHeld.current = columns;
11708
11767
  const [localColumns, setLocalColumns] = (0, import_react60.useState)(
11709
11768
  null
11710
11769
  );
@@ -11777,6 +11836,274 @@ function Grid({
11777
11836
  } : {}
11778
11837
  };
11779
11838
  }, [presentation, onPresentationChange, writeLook, merged, phone]);
11839
+ const queryTotal = records.data?.total ?? rows.length;
11840
+ const tableQuery = (0, import_react60.useMemo)(
11841
+ () => merged ? {
11842
+ // THE STORE ANSWERS THE QUERY (tranche 6k): `data` is already the page.
11843
+ mode: "controlled",
11844
+ state: query,
11845
+ totalItems: queryTotal,
11846
+ onStateChange: setQuery,
11847
+ sourceProcessing: { search: "source", columnFilters: "source", sort: "source" }
11848
+ } : { mode: "controlled-local", state: query, onStateChange: setQuery },
11849
+ [merged, query, queryTotal]
11850
+ );
11851
+ const showAddLine = mayAddRow && rows.length > 0;
11852
+ const showSummaryLine = rows.length > 0 && (!merged || Object.keys(chosenSummaries).length > 0);
11853
+ const footerFace = JSON.stringify([
11854
+ showAddLine ? added.busy : null,
11855
+ showSummaryLine ? [chosenSummaries, summaries.values, summaries.rowCount, summaries.loading] : null
11856
+ ]);
11857
+ const footerRows = (0, import_react60.useMemo)(
11858
+ () => [...showAddLine ? [ADD_ROW] : [], ...showSummaryLine ? [SUMMARY_ROW] : []],
11859
+ // `footerFace`, `ordered` and the summaries' operations are what the footer's cells draw.
11860
+ // eslint-disable-next-line react-hooks/exhaustive-deps
11861
+ [showAddLine, showSummaryLine, footerFace, ordered, summaries.operations, summaries.askOperations]
11862
+ );
11863
+ const coverageTotal = typeof records.data?.total === "number" ? records.data.total : null;
11864
+ const tableCoverage = (0, import_react60.useMemo)(
11865
+ () => ({
11866
+ answeredBy: merged ? "source" : "client",
11867
+ ...merged && coverageTotal !== null ? { matched: coverageTotal } : {},
11868
+ noun: "record",
11869
+ ...coverageTotal !== null ? { total: coverageTotal } : {}
11870
+ }),
11871
+ [merged, coverageTotal]
11872
+ );
11873
+ const sheetNow = (0, import_react60.useRef)({ merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify });
11874
+ sheetNow.current = { merged, writeRange, editing, fields: fields.data ?? [], mayWriteRow, notify: host.notify };
11875
+ const hasNotify = Boolean(host.notify);
11876
+ const readOnlyReason = !editable || !rights.known || rights.write ? "Editing is off here, so this table can be copied but not changed." : rights.why("write");
11877
+ const spreadsheetConfig = (0, import_react60.useMemo)(
11878
+ () => canWrite && editable ? {
11879
+ enabled: true,
11880
+ writableColumnIds: pasteableKeys,
11881
+ onPatch: (patches, intent) => {
11882
+ const now = sheetNow.current;
11883
+ if (now.merged) {
11884
+ void now.writeRange(patches, intent);
11885
+ return;
11886
+ }
11887
+ for (const patch of patches) {
11888
+ now.editing.setCell(patch.rowId, patch.columnId, patch.value);
11889
+ }
11890
+ },
11891
+ ...merged ? { undoDepth: 0, onSelectionChange: followSelection, controllerRef: sheetController } : {},
11892
+ columnRefusal: (columnId) => {
11893
+ const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
11894
+ if (!field2) return `This table has no column called ${columnId}.`;
11895
+ return `${fieldName(field2)} cannot take a pasted value \u2014 ${fieldTypeLabel(field2)} is worked out or chosen, not typed.`;
11896
+ },
11897
+ validateCell: ({ columnId, value }) => {
11898
+ const field2 = sheetNow.current.fields.find((f) => f.key === columnId);
11899
+ if (!field2) return `This table has no column called ${columnId}.`;
11900
+ const judged = valueFromPastedText(field2, value === null ? "" : String(value));
11901
+ return "refusal" in judged ? judged.refusal : void 0;
11902
+ },
11903
+ onPastePlan: receivePastePlan,
11904
+ /*
11905
+ * TYPE-TO-EDIT (design-system 0.43 `onEditRequest`): the cell a person is on
11906
+ * opens ITS OWN editor — the Field's — holding what they typed (a letter, an
11907
+ * IME composition, an Option/AltGr character, dictation). A worked-out column
11908
+ * or a row this person may not write answers false, so nothing is swallowed.
11909
+ */
11910
+ onEditRequest: (address, seed) => {
11911
+ const now = sheetNow.current;
11912
+ const field2 = now.fields.find((f) => f.key === address.columnId);
11913
+ if (!field2 || !fieldIsEditable(field2) || !now.mayWriteRow(address.rowId)) return false;
11914
+ if (seed !== null && seed !== "" && typingOpens(field2) === "none") return false;
11915
+ now.editing.begin({ rowId: address.rowId, key: field2.key }, seed);
11916
+ return true;
11917
+ },
11918
+ ...hasNotify ? { notify: (message) => sheetNow.current.notify?.success(message) } : {}
11919
+ } : {
11920
+ /*
11921
+ * RANGE SELECTION AND COPY FOR A READER (merge step 3, 0.42 `readOnly`). A person
11922
+ * who may not change this table may still drag out a range, select all and press
11923
+ * Cmd-C for a real Excel/Sheets block — the UX walk graded range selection BROKEN
11924
+ * on a table whose writes were refused. Every write gesture (cut, paste, clear,
11925
+ * fill, undo) says why it cannot, in the store's own words.
11926
+ */
11927
+ enabled: true,
11928
+ readOnly: true,
11929
+ ...merged ? { onSelectionChange: followSelection, controllerRef: sheetController } : {},
11930
+ readOnlyReason
11931
+ },
11932
+ [canWrite, editable, pasteableKeys, merged, followSelection, receivePastePlan, hasNotify, readOnlyReason]
11933
+ );
11934
+ const toolbarFields = fields.data;
11935
+ const hasSeveralRows = rows.length > 1;
11936
+ const editingBusy = editing.busy;
11937
+ const addRecord = editing.addRecord;
11938
+ const phoneSheetTools = merged && phone ? controls.sheetTools : null;
11939
+ const tableToolbar = (0, import_react60.useMemo)(
11940
+ () => ({
11941
+ search: true,
11942
+ searchPlaceholder: "Search records",
11943
+ /*
11944
+ * A PHONE (tranche 6o): search and the one Tools button sit side by side at the START of
11945
+ * the row, so Tools is never scrolled out of reach; the search is the same query.
11946
+ */
11947
+ ...merged && phone ? {
11948
+ 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)(
11949
+ "input",
11950
+ {
11951
+ type: "search",
11952
+ value: query.search,
11953
+ placeholder: "Search",
11954
+ "aria-label": "Search records",
11955
+ onChange: (event) => {
11956
+ const search = event.target.value;
11957
+ setQuery((held) => ({ ...held, search, page: 1 }));
11958
+ },
11959
+ 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",
11960
+ style: { fontSize: "16px" }
11961
+ }
11962
+ ) })
11963
+ } : {},
11964
+ /* ONE ROW (lane TABLE-PAGE-CHROME): on a table page the grid's search, columns and
11965
+ sort are drawn INTO the page's toolbar row, never a second bar under it. */
11966
+ ...toolbarSlot !== void 0 ? { portalInto: toolbarSlot, singleRow: true } : {},
11967
+ // SCR-2 / SCR-4: PRESENT for an admin, ABSENT for everyone else.
11968
+ // There is no third state: this package ships no disabled variant of
11969
+ // these controls, so a member cannot be shown a dead one by mistake.
11970
+ /*
11971
+ * WHERE THE GRID'S CONTROLS GO (lane C, review 2026-09-26 D6). Inline when the row has
11972
+ * room; behind the row's "…" once the page row is folded that far (Undo / Redo and New
11973
+ * record stay beside it — the primary action is never folded); on a phone EVERY control
11974
+ * — the table's own (clear, copy/export, group, columns), these, the search, the sort and
11975
+ * the page's filter, layout and share — in ONE "Tools" sheet. Never dropped.
11976
+ */
11977
+ actions: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
11978
+ manual ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
11979
+ SortStateControl,
11980
+ {
11981
+ ...handOrderSortState({
11982
+ sortName: columnSort ? columnLabel(toolbarFields ?? [], columnSort.id) : null,
11983
+ direction: columnSort?.direction ?? null,
11984
+ onBack: () => setQuery((held) => ({ ...held, sort: null, page: 1 })),
11985
+ ...viewOrder?.onUseSort && columnSort ? {
11986
+ onUseSort: () => {
11987
+ const sort = columnSort;
11988
+ const chosen = { field: sort.id, direction: sort.direction };
11989
+ setHeldOrder({ order: "sorted", sorts: [chosen] });
11990
+ viewOrder.onUseSort?.(chosen);
11991
+ host.notify?.success(
11992
+ `Sorted by ${columnLabel(toolbarFields ?? [], sort.id)} ${sortArrow(sort.direction)}. ${MANUAL_SORT.replacedToast}`
11993
+ );
11994
+ }
11995
+ } : {}
11996
+ })
11997
+ }
11998
+ ) : null,
11999
+ 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,
12000
+ !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,
12001
+ rights.structure && onAddField ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFieldButton, { onClick: onAddField }) : null,
12002
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12003
+ GridLayoutMenu,
12004
+ {
12005
+ look,
12006
+ firstColumn: sortedKeys[0] ? {
12007
+ key: sortedKeys[0],
12008
+ name: (() => {
12009
+ const first = (toolbarFields ?? []).find((f) => f.key === sortedKeys[0]);
12010
+ return first ? fieldName(first) : humanize(sortedKeys[0]);
12011
+ })()
12012
+ } : null,
12013
+ onChange: changeLayout,
12014
+ kept: Boolean(onPresentationChange)
12015
+ }
12016
+ )
12017
+ ] }),
12018
+ ...merged ? {
12019
+ overflow: {
12020
+ mode: phone ? "sheet" : toolbarFold2 >= 3 ? "menu" : "inline",
12021
+ pinned: /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
12022
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(UndoRedoLive, { source: undoSource }),
12023
+ 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
12024
+ ] }),
12025
+ ...phone ? {
12026
+ 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: [
12027
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
12028
+ "input",
12029
+ {
12030
+ type: "search",
12031
+ value: query.search,
12032
+ placeholder: "Search",
12033
+ "aria-label": "Search records",
12034
+ onChange: (event) => {
12035
+ const search = event.target.value;
12036
+ setQuery((held) => ({ ...held, search, page: 1 }));
12037
+ },
12038
+ className: "h-10 w-full min-w-0 rounded-md border border-input bg-background px-2 text-foreground placeholder:text-muted-foreground",
12039
+ style: { fontSize: "16px" }
12040
+ }
12041
+ ),
12042
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-grid-sheet-sort": "", children: [
12043
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "shrink-0", children: "Sort by" }),
12044
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
12045
+ "select",
12046
+ {
12047
+ "aria-label": "Sort by",
12048
+ value: query.sort ? `${query.sort.id}:${query.sort.direction}` : "",
12049
+ onChange: (event) => {
12050
+ const [id, direction] = event.target.value.split(":");
12051
+ setQuery((held) => ({
12052
+ ...held,
12053
+ page: 1,
12054
+ sort: id ? { id, direction: direction === "desc" ? "desc" : "asc" } : null
12055
+ }));
12056
+ },
12057
+ className: "h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-foreground",
12058
+ style: { fontSize: "16px" },
12059
+ children: [
12060
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: "", children: "No sort" }),
12061
+ ordered.flatMap((field2) => [
12062
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:asc`, children: `${fieldName(field2)} \u2191` }, `${field2.key}:asc`),
12063
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("option", { value: `${field2.key}:desc`, children: `${fieldName(field2)} \u2193` }, `${field2.key}:desc`)
12064
+ ])
12065
+ ]
12066
+ }
12067
+ )
12068
+ ] }),
12069
+ phoneSheetTools
12070
+ ] }),
12071
+ ...sheetExtras ? { sheetExtras } : {}
12072
+ } : {}
12073
+ }
12074
+ } : {}
12075
+ }),
12076
+ [
12077
+ merged,
12078
+ phone,
12079
+ query.search,
12080
+ query.sort,
12081
+ toolbarSlot,
12082
+ manual,
12083
+ columnSort,
12084
+ toolbarFields,
12085
+ viewOrder,
12086
+ host,
12087
+ viewId,
12088
+ hasSeveralRows,
12089
+ editable,
12090
+ rights.write,
12091
+ rights.structure,
12092
+ editingBusy,
12093
+ addRecord,
12094
+ onAddField,
12095
+ look,
12096
+ sortedKeys,
12097
+ changeLayout,
12098
+ onPresentationChange,
12099
+ toolbarFold2,
12100
+ undoSource,
12101
+ added.busy,
12102
+ ordered,
12103
+ phoneSheetTools,
12104
+ sheetExtras
12105
+ ]
12106
+ );
11780
12107
  const labelOfField = (0, import_react60.useCallback)(
11781
12108
  (key) => {
11782
12109
  const held = (fields.data ?? []).find((f) => f.key === key);
@@ -11945,25 +12272,12 @@ function Grid({
11945
12272
  viewTabs: false,
11946
12273
  stickyHeader: true,
11947
12274
  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 },
12275
+ query: tableQuery,
11956
12276
  tableClassName: (0, import_design_system35.cn)(
11957
12277
  !look.wrap && "[&_tbody_td]:max-w-[28rem] [&_tbody_td]:overflow-hidden [&_tbody_td]:text-ellipsis [&_tbody_td]:whitespace-nowrap! [&_tbody_td_*]:whitespace-nowrap!",
11958
12278
  look.fit === "scroll" && "sm:[&>table]:w-max! sm:[&>table]:max-w-none! sm:[&>table]:min-w-full!"
11959
12279
  ),
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
- })(),
12280
+ footerRows,
11967
12281
  ...copyConfig ? { copy: copyConfig } : {},
11968
12282
  ...offerSelection ? {
11969
12283
  selection: {
@@ -11997,75 +12311,15 @@ function Grid({
11997
12311
  ] })
11998
12312
  }
11999
12313
  } : {},
12000
- detail: { enabled: false },
12001
- window: { enabled: false },
12314
+ detail: DETAIL_OFF,
12315
+ window: WINDOW_OFF,
12002
12316
  ...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
- },
12317
+ spreadsheet: spreadsheetConfig,
12059
12318
  ...groupingConfig ? { grouping: groupingConfig } : {},
12060
12319
  columnState,
12061
12320
  ...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
- },
12321
+ virtualize: fills ? VIRTUALIZE_FILLED : true,
12322
+ coverage: tableCoverage,
12069
12323
  ...tableStyleProp ? { tableStyle: tableStyleProp } : {},
12070
12324
  ...onOpenRecord ? { onRowOpen: (row) => onOpenRecord(row.id) } : {},
12071
12325
  ...offersRowActions ? { rowActions } : {},
@@ -12077,142 +12331,7 @@ function Grid({
12077
12331
  ...mayAddRow ? { addFirst: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(AddFirstRowButton, { onAdd: () => void addRowHere(), busy: added.busy }) } : {},
12078
12332
  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
12333
  }),
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
- }
12334
+ toolbar: tableToolbar
12216
12335
  },
12217
12336
  `records-grid-${tableId}:${openingKey}`
12218
12337
  )
@@ -12314,6 +12433,58 @@ function Grid({
12314
12433
  }
12315
12434
  ) });
12316
12435
  }
12436
+ var DETAIL_OFF = { enabled: false };
12437
+ var WINDOW_OFF = { enabled: false };
12438
+ var VIRTUALIZE_FILLED = { threshold: 40 };
12439
+ function menuFace(items) {
12440
+ return JSON.stringify(items, (_key, value) => typeof value === "function" ? "\u0192" : value);
12441
+ }
12442
+ function menuThroughLatest(items, at, latest) {
12443
+ return items.map((item) => {
12444
+ const path = `${at}/${item.id}`;
12445
+ return {
12446
+ ...item,
12447
+ ...item.onSelect ? { onSelect: () => latest.get(path)?.() } : {},
12448
+ ...item.submenu ? { submenu: menuThroughLatest(item.submenu, path, latest) } : {}
12449
+ };
12450
+ });
12451
+ }
12452
+ function bindLatest(items, at, latest) {
12453
+ for (const item of items) {
12454
+ const path = `${at}/${item.id}`;
12455
+ if (item.onSelect) latest.set(path, item.onSelect);
12456
+ if (item.submenu) bindLatest(item.submenu, path, latest);
12457
+ }
12458
+ }
12459
+ function holdHeaderMenu(held, latest, key, items) {
12460
+ if (!items) return items;
12461
+ const at = `menu:${key}`;
12462
+ bindLatest(items, at, latest);
12463
+ const sig = menuFace(items);
12464
+ const was = held.get(at);
12465
+ if (was && was.sig === sig) return was.value;
12466
+ const value = menuThroughLatest(items, at, latest);
12467
+ held.set(at, { sig, value });
12468
+ return value;
12469
+ }
12470
+ function holdByContent(held, at, value) {
12471
+ if (value === void 0) return value;
12472
+ const sig = JSON.stringify(value);
12473
+ const was = held.get(at);
12474
+ if (was && was.sig === sig) return was.value;
12475
+ held.set(at, { sig, value });
12476
+ return value;
12477
+ }
12478
+ function sameColumns(a, b) {
12479
+ if (a === b) return true;
12480
+ if (a.length !== b.length) return false;
12481
+ return a.every((column, i) => {
12482
+ const other = b[i];
12483
+ if (column === other) return true;
12484
+ const keys = Object.keys(column);
12485
+ return keys.length === Object.keys(other).length && keys.every((key) => Object.is(column[key], other[key]));
12486
+ });
12487
+ }
12317
12488
  function withHeaderMenus(columns, menuFor, filterFor) {
12318
12489
  return columns.map((column) => {
12319
12490
  const items = menuFor(String(column.id));
@@ -13178,6 +13349,32 @@ var ADDABLE_TYPES = ADDABLE_TYPE_IDS.map((value) => ({
13178
13349
  value,
13179
13350
  label: fieldTypeChoice(value)?.label ?? value
13180
13351
  }));
13352
+ var ADDABLE_TYPE_SET = new Set(ADDABLE_TYPE_IDS);
13353
+ function customFieldRequestProblems(requests, existingLabels) {
13354
+ const problems = [];
13355
+ const taken = new Set(existingLabels.map((l) => l.trim().toLowerCase()));
13356
+ const asked = /* @__PURE__ */ new Set();
13357
+ if (requests.length === 0) problems.push("Name at least one field to add.");
13358
+ requests.forEach((request, index) => {
13359
+ const at = requests.length > 1 ? `Field ${index + 1}: ` : "";
13360
+ const label = typeof request?.label === "string" ? request.label.trim() : "";
13361
+ if (!label) {
13362
+ problems.push(`${at}a field needs a name.`);
13363
+ return;
13364
+ }
13365
+ const type = request.type ?? "text";
13366
+ if (!ADDABLE_TYPE_SET.has(type)) {
13367
+ problems.push(
13368
+ `${at}"${label}" cannot hold "${String(type)}". Choose one of: ${ADDABLE_TYPE_IDS.join(", ")}.`
13369
+ );
13370
+ }
13371
+ const key = label.toLowerCase();
13372
+ if (taken.has(key)) problems.push(`${at}there is already a field named "${label}".`);
13373
+ else if (asked.has(key)) problems.push(`${at}"${label}" is asked for twice.`);
13374
+ asked.add(key);
13375
+ });
13376
+ return problems;
13377
+ }
13181
13378
  function asField(f) {
13182
13379
  return {
13183
13380
  id: f.id,
@@ -13198,8 +13395,10 @@ function CustomFieldsSection({
13198
13395
  entityToken,
13199
13396
  recordId,
13200
13397
  title = "Custom fields",
13201
- className
13398
+ className,
13399
+ agentDoor
13202
13400
  }) {
13401
+ const client = (0, import_react68.useRecordsClient)();
13203
13402
  const read = (0, import_react68.useEntityRecord)(entityToken, recordId);
13204
13403
  const rights = (0, import_react68.useEntityFieldRights)(entityToken);
13205
13404
  const mutation = (0, import_react68.useEntityFieldMutation)();
@@ -13210,14 +13409,70 @@ function CustomFieldsSection({
13210
13409
  const [draft, setDraft] = (0, import_react67.useState)({});
13211
13410
  const fields = (0, import_react67.useMemo)(() => read.data?.fields ?? [], [read.data]);
13212
13411
  const mayAdd = rights.data?.may_declare === true;
13412
+ const live = (0, import_react67.useRef)({ fields, rights: rights.data, loading: rights.loading, reload: read.reload });
13413
+ (0, import_react67.useEffect)(() => {
13414
+ live.current = { fields, rights: rights.data, loading: rights.loading, reload: read.reload };
13415
+ });
13416
+ (0, import_react67.useEffect)(() => {
13417
+ if (!agentDoor) return;
13418
+ const state = () => {
13419
+ const now = live.current;
13420
+ return {
13421
+ entityToken,
13422
+ recordId,
13423
+ entityLabel: now.rights?.label ?? null,
13424
+ mayAdd: now.rights ? now.rights.may_declare === true : now.loading ? null : false,
13425
+ refusal: now.rights && !now.rights.may_declare ? now.rights.reason ?? null : null,
13426
+ fields: now.fields.map((f) => ({ key: f.key, label: f.label, type: f.type })),
13427
+ types: ADDABLE_TYPES
13428
+ };
13429
+ };
13430
+ const check = (requests) => {
13431
+ const now = state();
13432
+ if (now.mayAdd === false) {
13433
+ return [now.refusal ?? "This person may not add a field here."];
13434
+ }
13435
+ if (now.mayAdd === null) return ["The section is still reading who may add a field. Try again in a moment."];
13436
+ return customFieldRequestProblems(requests, now.fields.map((f) => f.label));
13437
+ };
13438
+ const addField = async (request) => {
13439
+ const problems = check([request]);
13440
+ if (problems.length > 0) return { ok: false, message: problems.join(" ") };
13441
+ const label = request.label.trim();
13442
+ const type = request.type ?? "text";
13443
+ const declared2 = await client.entityFieldDeclare({ token: entityToken, spec: { label, type } });
13444
+ if (!declared2.ok) {
13445
+ const hint = hintForAPerson(declared2.error.hint);
13446
+ return { ok: false, message: hint ? `${declared2.error.message} ${hint}` : declared2.error.message };
13447
+ }
13448
+ const back = await client.entityRecordRead({ token: entityToken, record_id: recordId });
13449
+ live.current.reload();
13450
+ if (!back.ok || !(back.data?.fields ?? []).some((f) => f.id === declared2.data)) {
13451
+ return {
13452
+ ok: false,
13453
+ message: `The store took "${label}" but this record does not show it yet. Nothing was lost \u2014 read the page again before adding it a second time.`
13454
+ };
13455
+ }
13456
+ return { ok: true, field_id: declared2.data, label, type };
13457
+ };
13458
+ return agentDoor({ entityToken, recordId, state, check, addField });
13459
+ }, [agentDoor, client, entityToken, recordId]);
13213
13460
  if (!read.loading && !read.error && fields.length === 0 && !mayAdd) return null;
13214
13461
  if (read.error) return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(RefusalLine, { error: read.error, className });
13215
13462
  const values = read.data?.custom ?? {};
13216
13463
  const written = read.data?.custom_written ?? {};
13464
+ const empty = fields.length === 0;
13465
+ if (empty && !adding) {
13466
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("section", { className: (0, import_design_system38.cn)("flex min-w-0 items-center gap-2 text-xs", className), children: [
13467
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h3", { className: "text-sm font-medium", children: title }),
13468
+ !read.loading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "None yet" }) : null,
13469
+ mayAdd ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.Button, { size: "sm", variant: "ghost", className: "ml-auto", onClick: () => setAdding(true), children: "Add field" }) : null
13470
+ ] });
13471
+ }
13217
13472
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("section", { className: (0, import_design_system38.cn)("flex min-w-0 flex-col gap-2 text-xs", className), children: [
13218
13473
  /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
13219
13474
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h3", { className: "text-sm font-medium", children: title }),
13220
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: fields.length }),
13475
+ !empty ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: fields.length }) : null,
13221
13476
  fields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13222
13477
  import_design_system38.Button,
13223
13478
  {
@@ -13286,9 +13541,9 @@ function CustomFieldsSection({
13286
13541
  /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("label", { className: "flex flex-col gap-1", children: [
13287
13542
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "Name" }),
13288
13543
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13289
- "input",
13544
+ import_design_system38.BasicInput,
13290
13545
  {
13291
- className: "h-8 rounded-md border bg-background px-2",
13546
+ className: "h-8",
13292
13547
  value: newLabel,
13293
13548
  onChange: (e) => setNewLabel(e.target.value),
13294
13549
  placeholder: "Account tier",
@@ -13296,17 +13551,12 @@ function CustomFieldsSection({
13296
13551
  }
13297
13552
  )
13298
13553
  ] }),
13299
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("label", { className: "flex flex-col gap-1", children: [
13554
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col gap-1", children: [
13300
13555
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "Holds" }),
13301
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13302
- "select",
13303
- {
13304
- className: "h-8 rounded-md border bg-background px-2",
13305
- value: newType,
13306
- onChange: (e) => setNewType(e.target.value),
13307
- children: ADDABLE_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("option", { value: t.value, children: t.label }, t.value))
13308
- }
13309
- )
13556
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_design_system38.Select, { value: newType, onValueChange: setNewType, children: [
13557
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectTrigger, { size: "sm", "aria-label": "What this field holds", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectValue, {}) }),
13558
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectContent, { children: ADDABLE_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectItem, { value: t.value, children: t.label }, t.value)) })
13559
+ ] })
13310
13560
  ] }),
13311
13561
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.Button, { size: "sm", type: "submit", disabled: mutation.saving || newLabel.trim() === "", children: mutation.saving ? "Adding\u2026" : "Add" })
13312
13562
  ]