@ai-matrx/records-ui 0.62.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,8 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var src_exports = {};
33
33
  __export(src_exports, {
34
+ ABSENCE_WORD_LABEL: () => ABSENCE_WORD_LABEL,
35
+ ABSENCE_WORD_VALUES: () => ABSENCE_WORD_VALUES,
34
36
  ACTION_KINDS: () => ACTION_KINDS,
35
37
  ActionInbox: () => ActionInbox,
38
+ BOOKING_PAGE_STATE_LABEL: () => BOOKING_PAGE_STATE_LABEL,
39
+ BOOKING_PAGE_STATE_VALUES: () => BOOKING_PAGE_STATE_VALUES,
36
40
  BookingBuilder: () => BookingBuilder,
37
41
  BookingSlots: () => BookingSlots,
38
42
  BuildOrAsk: () => BuildOrAsk,
@@ -40,7 +44,10 @@ __export(src_exports, {
40
44
  CHART_KINDS: () => CHART_KINDS,
41
45
  CHART_KIND_LABEL: () => CHART_KIND_LABEL,
42
46
  CHART_NEEDS: () => CHART_NEEDS,
47
+ CHECKLIST_REQUIREMENT_KINDS: () => CHECKLIST_REQUIREMENT_KINDS,
48
+ CHECKLIST_REQUIREMENT_LABEL: () => CHECKLIST_REQUIREMENT_LABEL,
43
49
  CONDITION_OPS: () => CONDITION_OPS,
50
+ CONTEXT_POLICY_LABEL: () => CONTEXT_POLICY_LABEL,
44
51
  CaptureRun: () => CaptureRun,
45
52
  CaptureSheet: () => CaptureSheet,
46
53
  ChartBlock: () => ChartBlock,
@@ -56,6 +63,7 @@ __export(src_exports, {
56
63
  CustomFieldsSection: () => CustomFieldsSection,
57
64
  DEFAULT_FIELDS: () => DEFAULT_FIELDS,
58
65
  DEFAULT_VIEW_NAME: () => DEFAULT_VIEW_NAME,
66
+ DUE_STATE_LABEL: () => DUE_STATE_LABEL,
59
67
  DashboardCanvas: () => DashboardCanvas,
60
68
  DigestScheduler: () => DigestScheduler,
61
69
  DocRender: () => DocRender,
@@ -65,8 +73,14 @@ __export(src_exports, {
65
73
  EnrichBadge: () => EnrichBadge,
66
74
  EnrichPanel: () => EnrichPanel,
67
75
  ExportMenu: () => ExportMenu,
76
+ FIELD_RULE_KIND_LABEL: () => FIELD_RULE_KIND_LABEL,
77
+ FIELD_RULE_KIND_VALUES: () => FIELD_RULE_KIND_VALUES,
78
+ FIELD_SENSITIVITY_LABEL: () => FIELD_SENSITIVITY_LABEL,
68
79
  FIELD_TYPE_CHOICES: () => FIELD_TYPE_CHOICES,
69
80
  FIELD_TYPE_GROUPS: () => FIELD_TYPE_GROUPS,
81
+ FORMULA_OP_JOINS: () => FORMULA_OP_JOINS,
82
+ FORMULA_OP_LABEL: () => FORMULA_OP_LABEL,
83
+ FORMULA_OP_VALUES: () => FORMULA_OP_VALUES,
70
84
  FORM_FLOWS: () => FORM_FLOWS,
71
85
  FROZEN_COLUMN_WIDTH: () => FROZEN_COLUMN_WIDTH,
72
86
  FieldControl: () => FieldControl,
@@ -114,6 +128,8 @@ __export(src_exports, {
114
128
  PAGE_VIEW_LABEL: () => PAGE_VIEW_LABEL,
115
129
  PARITY_LABEL: () => PARITY_LABEL,
116
130
  PARITY_MADE_OF: () => PARITY_MADE_OF,
131
+ PROPOSED_CHANGE_ACT_LABEL: () => PROPOSED_CHANGE_ACT_LABEL,
132
+ PROPOSED_CHANGE_ACT_VALUES: () => PROPOSED_CHANGE_ACT_VALUES,
117
133
  Peek: () => Peek,
118
134
  PersonPicker: () => PersonPicker,
119
135
  PipelineBoard: () => PipelineBoard,
@@ -124,6 +140,8 @@ __export(src_exports, {
124
140
  ProposalRow: () => ProposalRow,
125
141
  ProvenanceBadge: () => ProvenanceBadge,
126
142
  PublicViewPage: () => PublicViewPage,
143
+ ROLLUP_AGG_LABEL: () => ROLLUP_AGG_LABEL,
144
+ ROLLUP_AGG_VALUES: () => ROLLUP_AGG_VALUES,
127
145
  RecordChat: () => RecordChat,
128
146
  RecordChip: () => RecordChip,
129
147
  RecordForm: () => RecordForm,
@@ -139,8 +157,13 @@ __export(src_exports, {
139
157
  SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS: () => SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS,
140
158
  SERIES_COLORS: () => SERIES_COLORS,
141
159
  SOMEBODY: () => SOMEBODY,
160
+ STAGE_RULE_ON_FAIL_LABEL: () => STAGE_RULE_ON_FAIL_LABEL,
142
161
  STORE_ANSWERS_THESE: () => STORE_ANSWERS_THESE,
143
162
  STORE_DECIDES_REASON: () => STORE_DECIDES_REASON,
163
+ SUBSCRIPTION_CADENCES: () => SUBSCRIPTION_CADENCES,
164
+ SUBSCRIPTION_CADENCE_LABEL: () => SUBSCRIPTION_CADENCE_LABEL,
165
+ SUBSCRIPTION_CHANNEL_LABEL: () => SUBSCRIPTION_CHANNEL_LABEL,
166
+ SUBSCRIPTION_CHANNEL_VALUES: () => SUBSCRIPTION_CHANNEL_VALUES,
144
167
  ShareControl: () => ShareControl,
145
168
  SignBlock: () => SignBlock,
146
169
  StageRulesSection: () => StageRulesSection,
@@ -158,6 +181,9 @@ __export(src_exports, {
158
181
  ViewBar: () => ViewBar,
159
182
  ViewSwitcher: () => ViewSwitcher,
160
183
  WITHHELD_RECORD_LABEL: () => WITHHELD_RECORD_LABEL,
184
+ WORK_DUE_STATES: () => WORK_DUE_STATES,
185
+ WORK_INBOX_KINDS: () => WORK_INBOX_KINDS,
186
+ WORK_INBOX_KIND_LABEL: () => WORK_INBOX_KIND_LABEL,
161
187
  WhoChangedSource: () => WhoChangedSource,
162
188
  actorBadge: () => actorBadge,
163
189
  actorWords: () => actorWords,
@@ -223,12 +249,14 @@ __export(src_exports, {
223
249
  previewWords: () => previewWords,
224
250
  publiclyAnswerable: () => publiclyAnswerable,
225
251
  recordName: () => recordName,
252
+ recordNameIn: () => recordNameIn,
226
253
  recordsDataSource: () => recordsDataSource,
227
254
  refusalForAPerson: () => refusalForAPerson,
228
255
  refusalLineForAPerson: () => refusalLineForAPerson,
229
256
  renderValue: () => renderValue,
230
257
  revokeConsequence: () => revokeConsequence,
231
258
  rowName: () => rowName,
259
+ rowNameIn: () => rowNameIn,
232
260
  scalarText: () => scalarText,
233
261
  shareUnavailableReason: () => shareUnavailableReason,
234
262
  specFromBlock: () => specFromBlock,
@@ -541,6 +569,12 @@ function looksLikeId(value) {
541
569
  function rowName(row, titleKey, fallback = "Untitled") {
542
570
  return recordName(row?.document, titleKey, fallback);
543
571
  }
572
+ function recordNameIn(table, document2, fallback = "Untitled") {
573
+ return recordName(document2, table?.title_field ?? null, fallback);
574
+ }
575
+ function rowNameIn(table, row, fallback = "Untitled") {
576
+ return recordName(row?.document, table?.title_field ?? null, fallback);
577
+ }
544
578
  var WITHHELD_RECORD_LABEL = "A record you have not been given access to";
545
579
 
546
580
  // src/ReferenceBuilder.tsx
@@ -818,50 +852,9 @@ var PARITY_LABEL = {
818
852
  var PARITY_MADE_OF = Object.fromEntries(
819
853
  import_records3.PARITY_FIELD_TYPES.map((p) => [p.parity_type, p.made_of])
820
854
  );
855
+ var KERNEL = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };
821
856
  function editorKindFor(field) {
822
- const format = (field.format ?? "").toLowerCase();
823
- const config = field.config ?? {};
824
- switch (field.type) {
825
- // LIMITS-FIX 2026-09-21: the tick box is a BEHAVIOUR of its own, so its editor is
826
- // decided by the behaviour alone — no format, no unit, no target to read. A record
827
- // that never answered holds no key at all, and `CheckboxControl` draws that third
828
- // state rather than pretending it is an unticked box.
829
- case "boolean":
830
- return "checkbox";
831
- case "text":
832
- if (format === "email") return "email";
833
- if (format === "phone") return "phone";
834
- if (format === "url") return "url";
835
- if (format === "json" || config["json"] === true) return "json";
836
- if (format === "long" || config["multiline"] === true) return "long_text";
837
- return "text";
838
- case "range": {
839
- const kind = String(config["kind"] ?? "number");
840
- if (kind === "date" || kind === "datetime") return "datetime";
841
- if (format === "currency") return "currency";
842
- if (format === "percent" || field.unit === "%") return "percent";
843
- return "number";
844
- }
845
- case "list":
846
- return field.multi ? "multi_select" : "select";
847
- case "relation":
848
- return relationEditor(field);
849
- case "formula": {
850
- const via = config["via"];
851
- if (via && config["agg"]) return "rollup";
852
- if (via) return "lookup";
853
- return "formula";
854
- }
855
- default:
856
- return "text";
857
- }
858
- }
859
- function relationEditor(field) {
860
- const parity = String(field.config?.["parity"] ?? "");
861
- if (parity === "attachment" || parity === "member") return parity;
862
- if (field.relation_target === KERNEL_FILE_TABLE) return "attachment";
863
- if (field.relation_target === KERNEL_PERSON_TABLE) return "member";
864
- return "relation";
857
+ return (0, import_records3.fieldKindFor)(field, KERNEL);
865
858
  }
866
859
  function fieldTypeLabel(field) {
867
860
  const kind = editorKindFor(field);
@@ -886,8 +879,7 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
886
879
  function RelationPicker({ field, value, onChange, disabled, id, className }) {
887
880
  const client = (0, import_react6.useRecordsClient)();
888
881
  const [rows, setRows] = (0, import_react5.useState)(null);
889
- const [titleKey, setTitleKey] = (0, import_react5.useState)(null);
890
- const [elsewhere, setElsewhere] = (0, import_react5.useState)({});
882
+ const [words, setWords] = (0, import_react5.useState)({});
891
883
  const [refusal, setRefusal] = (0, import_react5.useState)(null);
892
884
  const [search, setSearch] = (0, import_react5.useState)("");
893
885
  const [open, setOpen] = (0, import_react5.useState)(false);
@@ -904,47 +896,37 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
904
896
  if (result.ok) setRows(result.data.rows);
905
897
  else setRefusal(refusalLineForAPerson(result.error));
906
898
  });
907
- void client.tableList().then((result) => {
908
- if (cancelled || !result.ok) return;
909
- setTitleKey(result.data.find((table) => table.id === target)?.title_field ?? null);
910
- });
911
899
  return () => {
912
900
  cancelled = true;
913
901
  };
914
902
  }, [client, field.relation_target]);
915
903
  (0, import_react5.useEffect)(() => {
916
904
  if (!rows) return;
917
- const known = new Set(rows.map((row) => row.id));
918
- const missing = picked.filter((id2) => !known.has(id2) && elsewhere[id2] === void 0);
919
- if (missing.length === 0) return;
905
+ const wanted = [.../* @__PURE__ */ new Set([...rows.map((row) => row.id), ...picked])].filter(
906
+ (id2) => words[id2] === void 0
907
+ );
908
+ if (wanted.length === 0) return;
920
909
  let cancelled = false;
921
- void Promise.all(
922
- missing.map(async (id2) => {
923
- const result = await client.recordRead({ record_id: id2 });
924
- return [id2, result.ok ? rowName({ document: result.data.document }, titleKey) : WITHHELD_RECORD_LABEL];
925
- })
926
- ).then((pairs) => {
910
+ void client.relationWordsMany({ field_id: field.id, record_ids: wanted }).then((result) => {
927
911
  if (cancelled) return;
928
- setElsewhere((current) => ({ ...current, ...Object.fromEntries(pairs) }));
912
+ const learned = {};
913
+ for (const id2 of wanted) {
914
+ const said = result.ok ? result.data[id2] : void 0;
915
+ learned[id2] = typeof said === "string" && said !== "" ? said : WITHHELD_RECORD_LABEL;
916
+ }
917
+ setWords((current) => ({ ...current, ...learned }));
929
918
  });
930
919
  return () => {
931
920
  cancelled = true;
932
921
  };
933
- }, [client, rows, picked, elsewhere, titleKey]);
922
+ }, [client, field.id, rows, picked, words]);
934
923
  if (!field.relation_target) {
935
924
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
936
925
  fieldName(field),
937
926
  " points at records, but it does not say which Table yet, so there is nothing to pick from. A table admin sets its target in the field editor."
938
927
  ] });
939
928
  }
940
- const byId = new Map((rows ?? []).map((row) => [row.id, row]));
941
- const titleOf = (recordId) => {
942
- const row = byId.get(recordId);
943
- if (row) return rowName(row, titleKey, "Untitled");
944
- const away = elsewhere[recordId];
945
- if (away !== void 0) return away;
946
- return "Loading\u2026";
947
- };
929
+ const titleOf = (recordId) => words[recordId] ?? "Loading\u2026";
948
930
  const full = field.multi && field.relation_max !== null && picked.length >= field.relation_max;
949
931
  const toggle = (recordId) => {
950
932
  if (!field.multi) {
@@ -1688,28 +1670,12 @@ function RecordLabelProvider({ children }) {
1688
1670
  stopWaitClock(field.id);
1689
1671
  return;
1690
1672
  }
1691
- const target = field.relation_target;
1692
- if (!target) {
1673
+ if (!field.relation_target) {
1693
1674
  setSettled((current) => ({ ...current, [field.id]: true }));
1694
1675
  stopWaitClock(field.id);
1695
1676
  return;
1696
1677
  }
1697
- const [rows, tables] = await Promise.all([
1698
- client.list({ table_id: target, limit: 200 }),
1699
- client.tableList()
1700
- ]);
1701
- const titleKey = tables.ok ? tables.data.find((table) => table.id === target)?.title_field ?? null : null;
1702
- if (rows.ok) {
1703
- setNames((current) => {
1704
- const next = { ...current };
1705
- for (const row of rows.data.rows) {
1706
- next[row.id] = recordName(row.document, titleKey, "Untitled");
1707
- }
1708
- return next;
1709
- });
1710
- }
1711
1678
  setSettled((current) => ({ ...current, [field.id]: true }));
1712
- stopWaitClock(field.id);
1713
1679
  })();
1714
1680
  },
1715
1681
  [client, startWaitClock, stopWaitClock]
@@ -1718,27 +1684,26 @@ function RecordLabelProvider({ children }) {
1718
1684
  const flushing = (0, import_react13.useRef)(null);
1719
1685
  const flush = (0, import_react13.useCallback)(() => {
1720
1686
  flushing.current = null;
1721
- const batch = [...queued.current.entries()];
1687
+ const batch = [...queued.current.values()];
1722
1688
  queued.current.clear();
1723
1689
  if (batch.length === 0) return;
1724
1690
  void (async () => {
1725
1691
  const learned = {};
1726
- const LANES = 6;
1727
- let at = 0;
1728
1692
  await Promise.all(
1729
- Array.from({ length: Math.min(LANES, batch.length) }, async () => {
1730
- for (; ; ) {
1731
- const next = batch[at];
1732
- at += 1;
1733
- if (!next) return;
1734
- const [id] = next;
1735
- const result = await client.recordRead({ record_id: id });
1736
- learned[id] = result.ok ? recordName(result.data.document, null, "Untitled") : WITHHELD_RECORD_LABEL;
1693
+ batch.map(async ({ field, ids }) => {
1694
+ const wanted = [...ids];
1695
+ const result = await client.relationWordsMany({
1696
+ field_id: field.id,
1697
+ record_ids: wanted
1698
+ });
1699
+ for (const id of wanted) {
1700
+ const words = result.ok ? result.data[id] : void 0;
1701
+ learned[id] = typeof words === "string" && words !== "" ? words : WITHHELD_RECORD_LABEL;
1737
1702
  }
1738
1703
  })
1739
1704
  );
1740
1705
  setNames((current) => ({ ...current, ...learned }));
1741
- for (const [, field] of batch) stopWaitClock(field.id);
1706
+ for (const { field } of batch) stopWaitClock(field.id);
1742
1707
  })();
1743
1708
  }, [client, stopWaitClock]);
1744
1709
  const chase = (0, import_react13.useCallback)(
@@ -1746,7 +1711,9 @@ function RecordLabelProvider({ children }) {
1746
1711
  const key = `${field.id}:${id}`;
1747
1712
  if (chased.current.has(key)) return;
1748
1713
  chased.current.add(key);
1749
- queued.current.set(id, field);
1714
+ const pending = queued.current.get(field.id);
1715
+ if (pending) pending.ids.add(id);
1716
+ else queued.current.set(field.id, { field, ids: /* @__PURE__ */ new Set([id]) });
1750
1717
  startWaitClock(field, null);
1751
1718
  if (flushing.current === null) flushing.current = setTimeout(flush, 0);
1752
1719
  },
@@ -1783,18 +1750,137 @@ function useLabelWait(field) {
1783
1750
  return field ? waiting(field) : null;
1784
1751
  }
1785
1752
 
1786
- // src/values.tsx
1787
- var import_jsx_runtime9 = require("react/jsx-runtime");
1788
- function envelopeFor(document2, key) {
1789
- const block = document2?._values;
1790
- return block?.[key];
1753
+ // src/enumLabels.ts
1754
+ function exhaustiveValuesOf() {
1755
+ return function(values) {
1756
+ return values;
1757
+ };
1791
1758
  }
1792
- var ABSENCE_WORDS = {
1759
+ var WORK_DUE_STATES = exhaustiveValuesOf()([
1760
+ "overdue",
1761
+ "due_today",
1762
+ "scheduled",
1763
+ "undated",
1764
+ "finished"
1765
+ ]);
1766
+ var DUE_STATE_LABEL = {
1767
+ overdue: "Overdue",
1768
+ due_today: "Today",
1769
+ scheduled: "Scheduled",
1770
+ undated: "No date",
1771
+ finished: "Done"
1772
+ };
1773
+ var WORK_INBOX_KINDS = exhaustiveValuesOf()(["assignment", "approval", "proposal"]);
1774
+ var WORK_INBOX_KIND_LABEL = {
1775
+ assignment: "Yours",
1776
+ approval: "Approve",
1777
+ proposal: "Agent"
1778
+ };
1779
+ var CHECKLIST_REQUIREMENT_KINDS = exhaustiveValuesOf()([
1780
+ "none",
1781
+ "note",
1782
+ "answer",
1783
+ "record_field",
1784
+ "form",
1785
+ "document"
1786
+ ]);
1787
+ var CHECKLIST_REQUIREMENT_LABEL = {
1788
+ none: "",
1789
+ note: "Say what you did",
1790
+ answer: "Fill this in",
1791
+ record_field: "This has to be filled in on the record",
1792
+ form: "This step is a form",
1793
+ document: "This step is a document"
1794
+ };
1795
+ var FIELD_SENSITIVITY_LABEL = {
1796
+ public: "Anyone who can open the record",
1797
+ internal: "People in this organization",
1798
+ confidential: "People who have been given the record",
1799
+ restricted: "Owners and administrators only"
1800
+ };
1801
+ var CONTEXT_POLICY_LABEL = {
1802
+ include: "Yes, an assistant can read it",
1803
+ summarize: "Only a summary of it",
1804
+ on_request: "Only when somebody asks for it",
1805
+ exclude: "No, keep it away from assistants"
1806
+ };
1807
+ var STAGE_RULE_ON_FAIL_LABEL = {
1808
+ refuse: "Turn it away",
1809
+ require_approval: "Ask for approval"
1810
+ };
1811
+ var SUBSCRIPTION_CADENCES = exhaustiveValuesOf()(["instant", "hourly", "daily", "weekly"]);
1812
+ var SUBSCRIPTION_CADENCE_LABEL = {
1813
+ instant: "as it happens",
1814
+ hourly: "hourly summary",
1815
+ daily: "daily summary",
1816
+ weekly: "weekly summary"
1817
+ };
1818
+ var SUBSCRIPTION_CHANNEL_VALUES = ["in_app", "email", "sms"];
1819
+ var SUBSCRIPTION_CHANNEL_LABEL = {
1820
+ in_app: "in the app",
1821
+ email: "by email \u2014 only if an address is on the account",
1822
+ sms: "by text \u2014 only if a number is on the account"
1823
+ };
1824
+ var ROLLUP_AGG_VALUES = ["count", "sum", "avg", "min", "max"];
1825
+ var ROLLUP_AGG_LABEL = {
1826
+ count: "How many there are",
1827
+ sum: "The total of",
1828
+ avg: "The average of",
1829
+ min: "The smallest",
1830
+ max: "The largest"
1831
+ };
1832
+ var FORMULA_OP_VALUES = ["concat", "add", "sub", "mul", "div"];
1833
+ var FORMULA_OP_LABEL = {
1834
+ concat: "Join them together",
1835
+ add: "Add them up",
1836
+ sub: "Take the second from the first",
1837
+ mul: "Multiply them",
1838
+ div: "Divide the first by the second"
1839
+ };
1840
+ var FORMULA_OP_JOINS = {
1841
+ concat: true,
1842
+ add: true,
1843
+ sub: false,
1844
+ mul: true,
1845
+ div: false
1846
+ };
1847
+ var FIELD_RULE_KIND_VALUES = ["min", "max", "length", "pattern", "equals_field", "differs_from_field"];
1848
+ var FIELD_RULE_KIND_LABEL = {
1849
+ min: "At least",
1850
+ max: "At most",
1851
+ length: "No longer than",
1852
+ pattern: "Looks like",
1853
+ equals_field: "Same as the column",
1854
+ differs_from_field: "Different from the column"
1855
+ };
1856
+ var PROPOSED_CHANGE_ACT_VALUES = ["add", "update", "remove"];
1857
+ var PROPOSED_CHANGE_ACT_LABEL = {
1858
+ add: "Add",
1859
+ update: "Update",
1860
+ remove: "Remove"
1861
+ };
1862
+ var BOOKING_PAGE_STATE_VALUES = ["draft", "open", "closed", "full"];
1863
+ var BOOKING_PAGE_STATE_LABEL = {
1864
+ draft: "Not open yet \u2014 nobody can book until you open it.",
1865
+ open: "Open. Anyone with the link can book a time.",
1866
+ closed: "Closed. The link still works and says you are not taking bookings.",
1867
+ full: "Every time you offered is taken."
1868
+ };
1869
+ var ABSENCE_WORD_VALUES = ["never asked", "none", "refused", "conflicting"];
1870
+ var ABSENCE_WORD_LABEL = {
1793
1871
  "never asked": "never asked",
1794
1872
  none: "none",
1795
1873
  refused: "refused",
1796
1874
  conflicting: "conflicting"
1797
1875
  };
1876
+
1877
+ // src/values.tsx
1878
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1879
+ function envelopeFor(document2, key) {
1880
+ const block = document2?._values;
1881
+ return block?.[key];
1882
+ }
1883
+ var ABSENCE_WORDS = ABSENCE_WORD_LABEL;
1798
1884
  function renderValue(field, value, document2, labels, stillWaitingFor) {
1799
1885
  const envelope = envelopeFor(document2, field.key);
1800
1886
  if (envelope?.absent) {
@@ -3217,113 +3303,11 @@ function choiceOptionsOf(options) {
3217
3303
  }
3218
3304
  return { byWord, labels };
3219
3305
  }
3220
- var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
3221
- var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
3222
3306
  function valueFromPastedText(field, raw, options) {
3223
- const label = fieldName(field);
3224
- const text = raw.trim();
3225
- const kind = editorKindFor(field);
3226
- if (text === "" && kind !== "checkbox") return { value: null };
3227
- switch (kind) {
3228
- case "checkbox": {
3229
- const word = text.toLowerCase();
3230
- if (TRUE_WORDS.has(word)) return { value: true };
3231
- if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
3232
- return {
3233
- refusal: `${label} is a tick box, so it takes yes or no \u2014 \u201C${raw}\u201D is neither. Paste TRUE/FALSE, yes/no or 1/0.`
3234
- };
3235
- }
3236
- case "number":
3237
- case "currency":
3238
- case "percent": {
3239
- const negative = /^\(.*\)$/.test(text);
3240
- const bare = text.replace(/^\(|\)$/g, "").replace(/[\s,]/g, "").replace(/^[^\d.\-+]+/, "").replace(/[^\d.\-+eE]+$/, "");
3241
- const n = Number(bare);
3242
- if (bare === "" || Number.isNaN(n)) {
3243
- return {
3244
- refusal: `${label} holds a number, and \u201C${raw}\u201D is not one. Paste digits \u2014 a currency symbol, a percent sign, commas and brackets for a negative are all fine.`
3245
- };
3246
- }
3247
- return { value: negative ? -n : n };
3248
- }
3249
- case "datetime": {
3250
- const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
3251
- const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
3252
- if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
3253
- const at = Date.parse(text);
3254
- if (Number.isNaN(at)) {
3255
- return {
3256
- refusal: `${label} holds a date, and \u201C${raw}\u201D is not one this can read. Paste it as 2026-09-21, or as your spreadsheet's own date format.`
3257
- };
3258
- }
3259
- const parsed = new Date(at);
3260
- if (wantsTime) return { value: parsed.toISOString() };
3261
- const y = parsed.getFullYear();
3262
- const m = String(parsed.getMonth() + 1).padStart(2, "0");
3263
- const d = String(parsed.getDate()).padStart(2, "0");
3264
- return { value: `${y}-${m}-${d}` };
3265
- }
3266
- case "json": {
3267
- try {
3268
- return { value: JSON.parse(text) };
3269
- } catch {
3270
- return {
3271
- refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Paste the whole object, braces and all.`
3272
- };
3273
- }
3274
- }
3275
- // A LIST, A PERSON, A FILE OR ANOTHER RECORD IS AN IDENTITY, NOT A WORD.
3276
- // The store keeps the id of the option or the record, and a word off a
3277
- // clipboard is not that id — writing the word would put a string where an
3278
- // id belongs and every filter, colour and rollup built on that column would
3279
- // stop agreeing with it. So the cell is left alone and the person is told
3280
- // which control does hold it.
3281
- case "select":
3282
- case "multi_select": {
3283
- if (!options) {
3284
- return {
3285
- refusal: `${label} is a list and its options have not been read yet, so nothing can be matched to \u201C${raw}\u201D. Reopen the paste once the column's choices have loaded.`
3286
- };
3287
- }
3288
- const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
3289
- if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
3290
- const matched = [];
3291
- const unmatched = [];
3292
- for (const word of wanted) {
3293
- const hit = options.byWord.get(word.toLowerCase());
3294
- if (hit === void 0) unmatched.push(word);
3295
- else matched.push(hit);
3296
- }
3297
- if (unmatched.length > 0) {
3298
- const offered = options.labels.length > 0 ? options.labels.join(", ") : "it has none yet";
3299
- return {
3300
- refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
3301
- unmatched
3302
- };
3303
- }
3304
- return { value: kind === "multi_select" ? matched : matched[0] };
3305
- }
3306
- case "member":
3307
- return {
3308
- refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open the cell and pick them.`
3309
- };
3310
- case "attachment":
3311
- return {
3312
- refusal: `${label} holds a file, which cannot arrive on a clipboard. Open the cell and attach it.`
3313
- };
3314
- case "relation":
3315
- return {
3316
- refusal: `${label} points at another record, so it is chosen rather than typed. Open the cell and pick the record.`
3317
- };
3318
- case "formula":
3319
- case "lookup":
3320
- case "rollup":
3321
- return {
3322
- refusal: `${label} is worked out by the system, so nothing can be pasted into it.`
3323
- };
3324
- default:
3325
- return { value: raw };
3326
- }
3307
+ return (0, import_records5.coerceTypedAnswer)(field, raw, {
3308
+ label: fieldName(field),
3309
+ ...options ? { options } : {}
3310
+ });
3327
3311
  }
3328
3312
  function planPastedBlock(args) {
3329
3313
  const byKey = new Map(args.fields.map((f) => [f.key, f]));
@@ -3868,9 +3852,11 @@ function Grid({
3868
3852
  onClick: () => host.talkToRecord?.({
3869
3853
  tableId,
3870
3854
  recordId: row.id,
3871
- title: recordName(
3872
- row.document ?? {},
3873
- table.data?.title_field
3855
+ // ONE NAMING CALL (`names.ts`), not this file's own join
3856
+ // of a Table's title column to a document.
3857
+ title: recordNameIn(
3858
+ table.data,
3859
+ row.document ?? {}
3874
3860
  )
3875
3861
  }),
3876
3862
  children: "Talk"
@@ -4491,19 +4477,23 @@ var import_react27 = require("react");
4491
4477
  var import_react28 = require("@ai-matrx/records/react");
4492
4478
  var import_design_system13 = require("@ai-matrx/design-system");
4493
4479
  var import_jsx_runtime16 = require("react/jsx-runtime");
4494
- var ADDABLE_TYPES = [
4495
- { value: "text", label: "Text" },
4496
- { value: "long_text", label: "Long text" },
4497
- { value: "number", label: "Number" },
4498
- { value: "currency", label: "Money" },
4499
- { value: "percent", label: "Percentage" },
4500
- { value: "datetime", label: "Date" },
4501
- { value: "email", label: "Email" },
4502
- { value: "phone", label: "Phone" },
4503
- { value: "url", label: "Link" },
4504
- { value: "member", label: "Person" },
4505
- { value: "attachment", label: "File" }
4480
+ var ADDABLE_TYPE_IDS = [
4481
+ "text",
4482
+ "long_text",
4483
+ "number",
4484
+ "currency",
4485
+ "percent",
4486
+ "datetime",
4487
+ "email",
4488
+ "phone",
4489
+ "url",
4490
+ "member",
4491
+ "attachment"
4506
4492
  ];
4493
+ var ADDABLE_TYPES = ADDABLE_TYPE_IDS.map((value) => ({
4494
+ value,
4495
+ label: fieldTypeChoice(value)?.label ?? value
4496
+ }));
4507
4497
  function asField(f) {
4508
4498
  return {
4509
4499
  id: f.id,
@@ -4667,41 +4657,20 @@ function CustomFieldsSection({
4667
4657
  var import_react29 = require("react");
4668
4658
  var import_react30 = require("@ai-matrx/records/react");
4669
4659
  var import_design_system14 = require("@ai-matrx/design-system");
4660
+ var import_records6 = require("@ai-matrx/records");
4670
4661
  var import_jsx_runtime17 = require("react/jsx-runtime");
4671
- var RULE_KINDS = [
4672
- { kind: "min", label: "At least" },
4673
- { kind: "max", label: "At most" },
4674
- { kind: "length", label: "No longer than" },
4675
- { kind: "pattern", label: "Looks like" },
4676
- { kind: "equals_field", label: "Same as the column" },
4677
- { kind: "differs_from_field", label: "Different from the column" }
4678
- ];
4679
- var SENSITIVITIES = [
4680
- { value: "public", label: "Anyone who can open the record" },
4681
- { value: "internal", label: "People in this organization" },
4682
- { value: "confidential", label: "People who have been given the record" },
4683
- { value: "restricted", label: "Owners and administrators only" }
4684
- ];
4685
- var AGENT_VISIBILITY = [
4686
- { value: "include", label: "Yes, an assistant can read it" },
4687
- { value: "summarize", label: "Only a summary of it" },
4688
- { value: "on_request", label: "Only when somebody asks for it" },
4689
- { value: "exclude", label: "No, keep it away from assistants" }
4690
- ];
4691
- var ROLLUP_OPERATIONS = [
4692
- { value: "count", label: "How many there are" },
4693
- { value: "sum", label: "The total of" },
4694
- { value: "avg", label: "The average of" },
4695
- { value: "min", label: "The smallest" },
4696
- { value: "max", label: "The largest" }
4697
- ];
4698
- var FORMULA_OPERATIONS = [
4699
- { value: "concat", label: "Join them together", joins: true },
4700
- { value: "add", label: "Add them up", joins: true },
4701
- { value: "sub", label: "Take the second from the first", joins: false },
4702
- { value: "mul", label: "Multiply them", joins: true },
4703
- { value: "div", label: "Divide the first by the second", joins: false }
4704
- ];
4662
+ var RULE_KINDS = FIELD_RULE_KIND_VALUES.map((kind) => ({ kind, label: FIELD_RULE_KIND_LABEL[kind] }));
4663
+ var SENSITIVITIES = import_records6.FIELD_SENSITIVITIES.map((value) => ({ value, label: FIELD_SENSITIVITY_LABEL[value] }));
4664
+ var AGENT_VISIBILITY = import_records6.CONTEXT_POLICIES.map((value) => ({ value, label: CONTEXT_POLICY_LABEL[value] }));
4665
+ var ROLLUP_OPERATIONS = ROLLUP_AGG_VALUES.map((value) => ({
4666
+ value,
4667
+ label: ROLLUP_AGG_LABEL[value]
4668
+ }));
4669
+ var FORMULA_OPERATIONS = FORMULA_OP_VALUES.map((value) => ({
4670
+ value,
4671
+ label: FORMULA_OP_LABEL[value],
4672
+ joins: FORMULA_OP_JOINS[value]
4673
+ }));
4705
4674
  function keyFor(label) {
4706
4675
  const token = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
4707
4676
  if (token === "") return "";
@@ -5266,6 +5235,9 @@ var CONDITION_OPS = [
5266
5235
  { op: "gt", label: "is more than" },
5267
5236
  { op: "lt", label: "is less than" }
5268
5237
  ];
5238
+ function pointsAtARecord(f) {
5239
+ return !!f?.field && f.field.type === "relation" && !!f.field.relation_target;
5240
+ }
5269
5241
  function conditionIsSimple(expr) {
5270
5242
  if (expr === null || expr === void 0) return true;
5271
5243
  const node = expr;
@@ -5336,6 +5308,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5336
5308
  const op = node?.op ?? "";
5337
5309
  const onField = node?.args?.[0]?.field ?? "";
5338
5310
  const value = node?.args?.[1]?.const ?? "";
5311
+ const chosen = fields.find((f) => f.id === onField);
5339
5312
  function write(next) {
5340
5313
  const chosenOp = next.op ?? op;
5341
5314
  const chosenField = next.field ?? onField;
@@ -5374,7 +5347,24 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5374
5347
  children: CONDITION_OPS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: c.op, children: c.label }, c.op))
5375
5348
  }
5376
5349
  ),
5377
- op !== "present" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5350
+ op === "present" ? null : pointsAtARecord(chosen) ? (
5351
+ // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
5352
+ // cell uses, on the same `relation_words_many` the cell reads, so a filter and
5353
+ // the column it filters can never disagree about what a record is called. It
5354
+ // is asked for ONE record even when the column holds several: a clause compares
5355
+ // against one value, and a picker that let somebody choose three would be
5356
+ // writing an expression this row cannot draw back.
5357
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5358
+ RelationPicker,
5359
+ {
5360
+ field: { ...chosen.field, multi: false },
5361
+ value: value === "" ? null : value,
5362
+ onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
5363
+ id: "condition-value-record",
5364
+ className: "min-w-[8rem]"
5365
+ }
5366
+ )
5367
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5378
5368
  import_design_system15.BasicInput,
5379
5369
  {
5380
5370
  className: "h-8 w-28 text-xs",
@@ -5382,7 +5372,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5382
5372
  value: String(value ?? ""),
5383
5373
  onChange: (e) => write({ value: conditionValue(e.target.value) })
5384
5374
  }
5385
- ) : null
5375
+ )
5386
5376
  ] }) : null
5387
5377
  ] });
5388
5378
  }
@@ -5542,10 +5532,10 @@ var NO_RULES_YET = "Nothing has to be true to get here yet.";
5542
5532
  var NEEDS_A_SENTENCE = "Write the sentence somebody reads when this stops them, the way you would say it out loud.";
5543
5533
  var NOT_DRAWN_HERE = "This part of the rule is not something this builder can draw yet";
5544
5534
  var ON_FAIL = [
5545
- { value: "refuse", label: "Turn it away", note: "The card stays where it is and the person is told why." },
5535
+ { value: "refuse", label: STAGE_RULE_ON_FAIL_LABEL.refuse, note: "The card stays where it is and the person is told why." },
5546
5536
  {
5547
5537
  value: "require_approval",
5548
- label: "Ask for approval",
5538
+ label: STAGE_RULE_ON_FAIL_LABEL.require_approval,
5549
5539
  note: "The card stays where it is, and the people who can approve it are asked. Nobody is turned away."
5550
5540
  }
5551
5541
  ];
@@ -5596,7 +5586,7 @@ function StageRulesSection({ tableId, stage, className }) {
5596
5586
  const stageLabel = stages.find((s) => s.key === stageKey)?.label ?? stageKey ?? "";
5597
5587
  const gates = (0, import_react32.useMemo)(() => stageKey ? gatesOf(pipeline, stageKey) : [], [pipeline, stageKey]);
5598
5588
  const conditionFields = (0, import_react32.useMemo)(
5599
- () => (fields.data ?? []).map((f) => ({ id: String(f.id), key: f.key, label: fieldName(f) })),
5589
+ () => (fields.data ?? []).map((f) => ({ id: String(f.id), key: f.key, label: fieldName(f), field: f })),
5600
5590
  [fields.data]
5601
5591
  );
5602
5592
  (0, import_react32.useEffect)(() => {
@@ -6528,7 +6518,7 @@ function Peek({ tableId, recordId, onClose, className }) {
6528
6518
  if (record.error) return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RefusalNotice, { error: record.error, className });
6529
6519
  if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RefusalNotice, { error: fields.error, className });
6530
6520
  const document2 = record.data?.document;
6531
- const title = document2 ? recordName(document2, table.data?.title_field) : "";
6521
+ const title = document2 ? recordNameIn(table.data, document2) : "";
6532
6522
  const href = host.hrefForRecord && table.data ? host.hrefForRecord({ table: table.data, recordId }) : void 0;
6533
6523
  return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: (0, import_design_system21.cn)("flex min-w-0 flex-col gap-2 text-xs", className), children: [
6534
6524
  /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-center gap-2", children: [
@@ -7284,7 +7274,7 @@ function offerableFields(all, want) {
7284
7274
  return all.filter((field) => {
7285
7275
  const kind = editorKindFor(field);
7286
7276
  if (want === "choice") return kind === "select" || kind === "multi_select";
7287
- return kind === "datetime" || kind === "date";
7277
+ return kind === "datetime";
7288
7278
  });
7289
7279
  }
7290
7280
  function useStageField(tableId) {
@@ -7702,11 +7692,7 @@ var import_react54 = require("react");
7702
7692
  var import_react55 = require("@ai-matrx/records/react");
7703
7693
  var import_design_system25 = require("@ai-matrx/design-system");
7704
7694
  var import_jsx_runtime28 = require("react/jsx-runtime");
7705
- var ACT_WORD = {
7706
- add: "Add",
7707
- update: "Update",
7708
- remove: "Remove"
7709
- };
7695
+ var ACT_WORD = PROPOSED_CHANGE_ACT_LABEL;
7710
7696
  function ProposalRow({
7711
7697
  change,
7712
7698
  outcome,
@@ -7813,21 +7799,8 @@ var import_react56 = require("react");
7813
7799
  var import_react57 = require("@ai-matrx/records/react");
7814
7800
  var import_design_system26 = require("@ai-matrx/design-system");
7815
7801
  var import_jsx_runtime29 = require("react/jsx-runtime");
7816
- var DUE_WORD = {
7817
- overdue: "Overdue",
7818
- due_today: "Today",
7819
- scheduled: "Scheduled",
7820
- undated: "No date",
7821
- finished: "Done"
7822
- };
7823
- var REQUIRES_WORD = {
7824
- none: "",
7825
- note: "Say what you did",
7826
- answer: "Fill this in",
7827
- record_field: "This has to be filled in on the record",
7828
- form: "This step is a form",
7829
- document: "This step is a document"
7830
- };
7802
+ var DUE_WORD = DUE_STATE_LABEL;
7803
+ var REQUIRES_WORD = CHECKLIST_REQUIREMENT_LABEL;
7831
7804
  var NO_TEMPLATES = "No checklist has been written for this table yet. Ask an agent for one in a sentence \u2014 the steps, who each belongs to, and how many days in each is due \u2014 and it is stored whole, judged before a single step is written.";
7832
7805
  function ChecklistRunner({
7833
7806
  tableId,
@@ -8535,18 +8508,8 @@ function roleKey(said) {
8535
8508
  // src/ActionInbox.tsx
8536
8509
  var import_jsx_runtime30 = require("react/jsx-runtime");
8537
8510
  var ACTION_KINDS = ["approval", "assignment", "proposal"];
8538
- var KIND_WORD = {
8539
- assignment: "Yours",
8540
- approval: "Approve",
8541
- proposal: "Agent"
8542
- };
8543
- var DUE_WORD2 = {
8544
- overdue: "Overdue",
8545
- due_today: "Today",
8546
- scheduled: "Scheduled",
8547
- undated: "No date",
8548
- finished: "Finished"
8549
- };
8511
+ var KIND_WORD = WORK_INBOX_KIND_LABEL;
8512
+ var DUE_WORD2 = DUE_STATE_LABEL;
8550
8513
  function ActionInbox({ tableId, includeSettled = false, onOpenRecord, className }) {
8551
8514
  const client = (0, import_react59.useRecordsClient)();
8552
8515
  const [items, setItems] = (0, import_react58.useState)(null);
@@ -9767,7 +9730,7 @@ function PortalBuilder({ tableId, portalId, onSaved, onClose, className }) {
9767
9730
  // src/PortalsPanel.tsx
9768
9731
  var import_react68 = require("react");
9769
9732
  var import_react69 = require("@ai-matrx/records/react");
9770
- var import_records6 = require("@ai-matrx/records");
9733
+ var import_records7 = require("@ai-matrx/records");
9771
9734
  var import_design_system33 = require("@ai-matrx/design-system");
9772
9735
 
9773
9736
  // src/BuildOrAsk.tsx
@@ -9784,18 +9747,18 @@ function BuildOrAsk({
9784
9747
  buildLabel,
9785
9748
  className
9786
9749
  }) {
9750
+ const mayAsk = mayBuild && Boolean(onAsk);
9787
9751
  return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: (0, import_design_system32.cn)("rounded-md border border-dashed p-3", className), children: [
9788
9752
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-xs text-muted-foreground", children }),
9789
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "mt-2.5 flex flex-wrap items-center gap-2", children: [
9790
- onAsk ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_design_system32.Button, { size: "sm", variant: "outline", onClick: onAsk, children: "Ask an agent" }) : null,
9791
- mayBuild ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_design_system32.Button, { size: "sm", onClick: onBuild, children: buildLabel }) : null
9792
- ] }),
9793
- onAsk ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("p", { className: "mt-2 text-xs text-muted-foreground", children: [
9753
+ mayBuild ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "mt-2.5 flex flex-wrap items-center gap-2", children: [
9754
+ mayAsk ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_design_system32.Button, { size: "sm", variant: "outline", onClick: onAsk, children: "Ask an agent" }) : null,
9755
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_design_system32.Button, { size: "sm", onClick: onBuild, children: buildLabel })
9756
+ ] }) : null,
9757
+ !mayBuild ? whyNot ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-2 text-xs text-muted-foreground", children: whyNot }) : null : mayAsk ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("p", { className: "mt-2 text-xs text-muted-foreground", children: [
9794
9758
  "You would say something like: \u201C",
9795
9759
  suggestion,
9796
9760
  "\u201D"
9797
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-2 text-xs text-muted-foreground", children: NO_AGENT_PORT }),
9798
- !mayBuild && whyNot ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-2 text-xs text-muted-foreground", children: whyNot }) : null
9761
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-2 text-xs text-muted-foreground", children: NO_AGENT_PORT })
9799
9762
  ] });
9800
9763
  }
9801
9764
 
@@ -9834,30 +9797,41 @@ function PortalsPanel({ tableId, className }) {
9834
9797
  const client = (0, import_react69.useRecordsClient)();
9835
9798
  const host = useRecordsUi();
9836
9799
  const [portals, setPortals] = (0, import_react68.useState)(null);
9800
+ const [exposures, setExposures] = (0, import_react68.useState)([]);
9837
9801
  const [listError, setListError] = (0, import_react68.useState)(null);
9838
9802
  const [openId, setOpenId] = (0, import_react68.useState)(null);
9839
9803
  const [building, setBuilding] = (0, import_react68.useState)(false);
9804
+ const [adding, setAdding] = (0, import_react68.useState)(null);
9840
9805
  const load = (0, import_react68.useCallback)(async () => {
9841
- const answered = await client.portals();
9806
+ const [answered, mapped] = await Promise.all([client.portals(), client.portalTables()]);
9842
9807
  if (!answered.ok) {
9843
9808
  setListError(answered.error);
9844
9809
  setPortals([]);
9810
+ setExposures([]);
9845
9811
  return;
9846
9812
  }
9847
9813
  setListError(null);
9848
9814
  setPortals(answered.data);
9815
+ setExposures(mapped.ok ? mapped.data : []);
9849
9816
  }, [client]);
9850
9817
  (0, import_react68.useEffect)(() => {
9851
9818
  void load();
9852
9819
  }, [load]);
9853
9820
  if (portals === null) return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_design_system33.Skeleton, { className: (0, import_design_system33.cn)("h-32 w-full", className) });
9821
+ const onATable = Boolean(tableId);
9822
+ const holdingThis = new Set(
9823
+ exposures.filter((e) => e.table_id === tableId).map((e) => e.portal_id)
9824
+ );
9825
+ const shown = onATable ? portals.filter((p) => holdingThis.has(p.portal_id)) : portals;
9826
+ const couldTakeIt = onATable ? portals.filter((p) => !holdingThis.has(p.portal_id)) : [];
9827
+ const elsewhere = couldTakeIt.length;
9854
9828
  const origin = host.publicOrigin ?? (typeof window === "undefined" ? "" : window.location.origin);
9855
9829
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("section", { className: (0, import_design_system33.cn)("flex flex-col gap-3", className), children: [
9856
9830
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("header", { className: "flex items-center gap-2", children: [
9857
9831
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("h3", { className: "text-sm font-medium", children: "Portals" }),
9858
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "text-xs text-muted-foreground", children: portals.length === 0 ? "none yet" : `${portals.length}` }),
9832
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "text-xs text-muted-foreground", children: shown.length === 0 ? "none for this table" : `${shown.length}` }),
9859
9833
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex-1" }),
9860
- portals.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_design_system33.Button, { size: "sm", variant: building ? "secondary" : "ghost", onClick: () => setBuilding((b) => !b), children: building ? "Done building" : "Build a portal" }) : null
9834
+ shown.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_design_system33.Button, { size: "sm", variant: building ? "secondary" : "ghost", onClick: () => setBuilding((b) => !b), children: building ? "Done building" : "Build a portal" }) : null
9861
9835
  ] }),
9862
9836
  listError ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(RefusalNotice, { error: listError }) : null,
9863
9837
  building ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
@@ -9870,7 +9844,19 @@ function PortalsPanel({ tableId, className }) {
9870
9844
  onClose: () => setBuilding(false)
9871
9845
  }
9872
9846
  ) : null,
9873
- portals.length === 0 && !listError && !building ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9847
+ adding ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
9848
+ PortalBuilder,
9849
+ {
9850
+ ...tableId ? { tableId } : {},
9851
+ portalId: adding,
9852
+ onSaved: () => {
9853
+ setAdding(null);
9854
+ void load();
9855
+ },
9856
+ onClose: () => setAdding(null)
9857
+ }
9858
+ ) : null,
9859
+ shown.length === 0 && !listError && !building && !adding ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
9874
9860
  BuildOrAsk,
9875
9861
  {
9876
9862
  mayBuild: true,
@@ -9879,12 +9865,38 @@ function PortalsPanel({ tableId, className }) {
9879
9865
  onAsk: () => host.onAskForOne?.({ kind: "portal", tableId, suggestion: PORTAL_SUGGESTION })
9880
9866
  } : {},
9881
9867
  suggestion: PORTAL_SUGGESTION,
9882
- buildLabel: "Build the portal",
9883
- children: "A portal is how one of your clients signs in and sees the records that name them \u2014 their jobs, their invoices \u2014 and nothing else."
9868
+ buildLabel: elsewhere > 0 ? "Build a new portal" : "Build the portal",
9869
+ children: [
9870
+ "A portal is how one of your clients signs in and sees the records that name them \u2014 their jobs, their invoices \u2014 and nothing else.",
9871
+ elsewhere > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
9872
+ " ",
9873
+ "This table is not in",
9874
+ " ",
9875
+ elsewhere === 1 ? "the portal this organization already has" : `any of the ${elsewhere} portals this organization already has`,
9876
+ "."
9877
+ ] }) : null
9878
+ ]
9884
9879
  }
9885
9880
  ) : null,
9886
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "flex flex-col gap-2", children: portals.map((portal) => {
9887
- const url = `${origin}${(0, import_records6.portalPath)(portal.slug)}`;
9881
+ shown.length === 0 && !listError && !building && !adding && elsewhere > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-col gap-1.5 rounded-md border border-dashed border-border p-2.5", children: [
9882
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "text-xs text-muted-foreground", children: "Or add this table to a portal your clients already sign in to:" }),
9883
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex flex-wrap gap-1.5", children: couldTakeIt.map((portal) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
9884
+ import_design_system33.Button,
9885
+ {
9886
+ size: "sm",
9887
+ variant: "outline",
9888
+ onClick: () => setAdding(portal.portal_id),
9889
+ children: [
9890
+ "Add to \u201C",
9891
+ portal.title,
9892
+ "\u201D"
9893
+ ]
9894
+ },
9895
+ portal.portal_id
9896
+ )) })
9897
+ ] }) : null,
9898
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("ul", { className: "flex flex-col gap-2", children: shown.map((portal) => {
9899
+ const url = `${origin}${(0, import_records7.portalPath)(portal.slug)}`;
9888
9900
  const open = openId === portal.portal_id;
9889
9901
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("li", { className: "rounded-md border border-border bg-card p-2.5", children: [
9890
9902
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex items-center gap-2", children: [
@@ -10198,7 +10210,7 @@ function Preview({
10198
10210
  function Invite({ card, onInvited }) {
10199
10211
  const client = (0, import_react69.useRecordsClient)();
10200
10212
  const [rows, setRows] = (0, import_react68.useState)(null);
10201
- const [titleKey, setTitleKey] = (0, import_react68.useState)(null);
10213
+ const [clientTable, setClientTable] = (0, import_react68.useState)(null);
10202
10214
  const [search, setSearch] = (0, import_react68.useState)("");
10203
10215
  const [picked, setPicked] = (0, import_react68.useState)(null);
10204
10216
  const [email, setEmail] = (0, import_react68.useState)("");
@@ -10217,17 +10229,17 @@ function Invite({ card, onInvited }) {
10217
10229
  });
10218
10230
  void client.tableList().then((answered) => {
10219
10231
  if (cancelled || !answered.ok) return;
10220
- setTitleKey(answered.data.find((t) => t.id === card.client_table_id)?.title_field ?? null);
10232
+ setClientTable(answered.data.find((t) => t.id === card.client_table_id) ?? null);
10221
10233
  });
10222
10234
  return () => {
10223
10235
  cancelled = true;
10224
10236
  };
10225
10237
  }, [client, card.client_table_id]);
10226
10238
  const options = (0, import_react68.useMemo)(() => {
10227
- const all = (rows ?? []).map((row) => ({ id: row.id, name: rowName(row, titleKey) }));
10239
+ const all = (rows ?? []).map((row) => ({ id: row.id, name: rowNameIn(clientTable, row) }));
10228
10240
  const needle = search.trim().toLowerCase();
10229
10241
  return needle === "" ? all.slice(0, 25) : all.filter((o) => o.name.toLowerCase().includes(needle)).slice(0, 25);
10230
- }, [rows, titleKey, search]);
10242
+ }, [rows, clientTable, search]);
10231
10243
  const send = (0, import_react68.useCallback)(async () => {
10232
10244
  if (!picked) return;
10233
10245
  setBusy(true);
@@ -10610,11 +10622,7 @@ function whenItFires(subscription) {
10610
10622
  const every = subscription.cadence === "hourly" ? "an hourly summary" : subscription.cadence === "weekly" ? "a weekly summary" : "a daily summary";
10611
10623
  return subscription.schedule ? `${every}, ${subscription.schedule}` : every;
10612
10624
  }
10613
- var CHANNEL_WORDS2 = {
10614
- in_app: "in the app",
10615
- email: "by email \u2014 only if an address is on the account",
10616
- sms: "by text \u2014 only if a number is on the account"
10617
- };
10625
+ var CHANNEL_WORDS2 = SUBSCRIPTION_CHANNEL_LABEL;
10618
10626
  var DIGEST_SUGGESTION = "Email me a summary of this every Monday at 8 in the morning.";
10619
10627
  function SubscriptionsPanel({ tableId, className }) {
10620
10628
  const client = (0, import_react73.useRecordsClient)();
@@ -10788,7 +10796,7 @@ function SubscriptionsPanel({ tableId, className }) {
10788
10796
  // src/FormBuilder.tsx
10789
10797
  var import_react76 = require("react");
10790
10798
  var import_react77 = require("@ai-matrx/records/react");
10791
- var import_records7 = require("@ai-matrx/records");
10799
+ var import_records8 = require("@ai-matrx/records");
10792
10800
  var import_design_system37 = require("@ai-matrx/design-system");
10793
10801
 
10794
10802
  // src/FormRunner.tsx
@@ -11263,7 +11271,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
11263
11271
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
11264
11272
  PublishRow,
11265
11273
  {
11266
- url: `${publicOrigin}${(0, import_records7.publicFormPath)(draft.id)}`,
11274
+ url: `${publicOrigin}${(0, import_records8.publicFormPath)(draft.id)}`,
11267
11275
  state: draft.state,
11268
11276
  mayPublish: rights.structure,
11269
11277
  why: rights.why("structure"),
@@ -11382,7 +11390,12 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
11382
11390
  Condition,
11383
11391
  {
11384
11392
  question,
11385
- fieldKeys: fields.data ?? [],
11393
+ fieldKeys: (fields.data ?? []).map((f) => ({
11394
+ id: String(f.id),
11395
+ key: f.key,
11396
+ label: f.label || f.key,
11397
+ field: f
11398
+ })),
11386
11399
  onChange: (showIf) => patchQuestion(index, { showIf })
11387
11400
  }
11388
11401
  )
@@ -12115,17 +12128,8 @@ var import_react86 = require("react");
12115
12128
  var import_react87 = require("@ai-matrx/records/react");
12116
12129
  var import_design_system42 = require("@ai-matrx/design-system");
12117
12130
  var import_jsx_runtime45 = require("react/jsx-runtime");
12118
- var CADENCE_WORDS2 = {
12119
- instant: "as it happens",
12120
- hourly: "hourly summary",
12121
- daily: "daily summary",
12122
- weekly: "weekly summary"
12123
- };
12124
- var CHANNEL_WORDS3 = {
12125
- in_app: "in the app",
12126
- email: "by email",
12127
- sms: "by text"
12128
- };
12131
+ var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
12132
+ var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
12129
12133
  function NotifyRuleEditor({ tableId, seed, className }) {
12130
12134
  const client = (0, import_react87.useRecordsClient)();
12131
12135
  const host = useRecordsUi();
@@ -12329,7 +12333,7 @@ function NotifyRuleEditor({ tableId, seed, className }) {
12329
12333
  // src/ChartBlock.tsx
12330
12334
  var import_react88 = require("react");
12331
12335
  var import_recharts = require("recharts");
12332
- var import_records8 = require("@ai-matrx/records");
12336
+ var import_records9 = require("@ai-matrx/records");
12333
12337
  var import_core6 = require("@ai-matrx/records/core");
12334
12338
  var import_design_system43 = require("@ai-matrx/design-system");
12335
12339
  var import_jsx_runtime46 = require("react/jsx-runtime");
@@ -12350,7 +12354,7 @@ function ChartBlock({ block, subject, className }) {
12350
12354
  const kind = block.kind;
12351
12355
  const tableId = block.table_id ?? subject;
12352
12356
  const measures = block.measures && block.measures.length > 0 ? block.measures : [{ op: "count" }];
12353
- const primary = (0, import_records8.measureKey)(measures[0]);
12357
+ const primary = (0, import_records9.measureKey)(measures[0]);
12354
12358
  const series = [primary];
12355
12359
  const points = (0, import_react88.useMemo)(() => {
12356
12360
  if (kind === "stuck") return [];
@@ -12884,7 +12888,7 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
12884
12888
  // src/FormsPanel.tsx
12885
12889
  var import_react91 = require("react");
12886
12890
  var import_react92 = require("@ai-matrx/records/react");
12887
- var import_records9 = require("@ai-matrx/records");
12891
+ var import_records10 = require("@ai-matrx/records");
12888
12892
  var import_design_system45 = require("@ai-matrx/design-system");
12889
12893
  var import_jsx_runtime48 = require("react/jsx-runtime");
12890
12894
  var WHAT_A_FORM_IS = "A form asks for this table's own fields, and its answers land here as ordinary records stamped with the form they came through.";
@@ -12979,7 +12983,7 @@ function FormsPanel({ tableId, className }) {
12979
12983
  }
12980
12984
  ) : null,
12981
12985
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("ul", { className: "flex flex-col gap-2", children: forms.map((form) => {
12982
- const url = `${origin}${(0, import_records9.publicFormPath)(form.form_id)}`;
12986
+ const url = `${origin}${(0, import_records10.publicFormPath)(form.form_id)}`;
12983
12987
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("li", { className: "rounded-md border p-2.5", children: [
12984
12988
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
12985
12989
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", children: form.title ?? form.slug }),
@@ -13031,7 +13035,7 @@ function FormsPanel({ tableId, className }) {
13031
13035
  // src/BookingBuilder.tsx
13032
13036
  var import_react93 = require("react");
13033
13037
  var import_react94 = require("@ai-matrx/records/react");
13034
- var import_records10 = require("@ai-matrx/records");
13038
+ var import_records11 = require("@ai-matrx/records");
13035
13039
  var import_design_system46 = require("@ai-matrx/design-system");
13036
13040
  var import_jsx_runtime49 = require("react/jsx-runtime");
13037
13041
  var STORE_ANSWERS_THESE = ["slot", "status", "booked_with"];
@@ -13174,7 +13178,7 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
13174
13178
  if (!rights.structure) {
13175
13179
  return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: (0, import_design_system46.cn)("text-xs text-muted-foreground", className), children: rights.why("structure") ?? "Making a booking page needs the admin level on this table, because it lets people with no account take time in your calendar." });
13176
13180
  }
13177
- const url = formId ? `${publicOrigin}${(0, import_records10.bookingPath)(formId)}` : null;
13181
+ const url = formId ? `${publicOrigin}${(0, import_records11.bookingPath)(formId)}` : null;
13178
13182
  const shown = offer ?? null;
13179
13183
  return (
13180
13184
  // THE RAIL IS 384px WIDE AND THE VIEWPORT IS NOT (walk 1, 2026-09-21):
@@ -13376,7 +13380,7 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
13376
13380
  // src/BookingSlots.tsx
13377
13381
  var import_react95 = require("react");
13378
13382
  var import_react96 = require("@ai-matrx/records/react");
13379
- var import_records11 = require("@ai-matrx/records");
13383
+ var import_records12 = require("@ai-matrx/records");
13380
13384
  var import_design_system47 = require("@ai-matrx/design-system");
13381
13385
  var import_jsx_runtime50 = require("react/jsx-runtime");
13382
13386
  var WHAT_A_BOOKING_PAGE_IS = "A booking page offers times you are free and writes each appointment into this table as an ordinary record.";
@@ -13385,12 +13389,7 @@ function bookingSuggestion(tableName2) {
13385
13389
  return `Let clients book a 30-minute ${subject} slot on Tuesday and Thursday afternoons.`;
13386
13390
  }
13387
13391
  var NO_ADMIN2 = "Opening a booking page lets people with no account take time in your calendar, so it needs the admin level on the table the appointments land in.";
13388
- var STATE_WORDS = {
13389
- draft: "Not open yet \u2014 nobody can book until you open it.",
13390
- open: "Open. Anyone with the link can book a time.",
13391
- closed: "Closed. The link still works and says you are not taking bookings.",
13392
- full: "Every time you offered is taken."
13393
- };
13392
+ var STATE_WORDS = BOOKING_PAGE_STATE_LABEL;
13394
13393
  function BookingSlots({ tableId, className }) {
13395
13394
  const client = (0, import_react96.useRecordsClient)();
13396
13395
  const host = useRecordsUi();
@@ -13414,10 +13413,21 @@ function BookingSlots({ tableId, className }) {
13414
13413
  void load();
13415
13414
  }, [load]);
13416
13415
  const subjectIds = (0, import_react95.useMemo)(
13417
- () => Array.from(new Set((pages ?? []).map((p) => p.table_id))),
13418
- [pages]
13416
+ () => Array.from(
13417
+ /* @__PURE__ */ new Set([
13418
+ ...(pages ?? []).map((p) => p.table_id),
13419
+ // THE EMPTY STATE ASKS THE SAME QUESTION. There is no page to read a
13420
+ // table off yet, and the empty state's two buttons need the answer
13421
+ // about the table this rail is standing in — otherwise the rail
13422
+ // offers a viewer both ways into an act the door will refuse.
13423
+ ...tableId ? [tableId] : []
13424
+ ])
13425
+ ),
13426
+ [pages, tableId]
13419
13427
  );
13420
13428
  const levels = (0, import_react96.useMyLevels)(subjectIds);
13429
+ const levelsKnown = !levels.loading && Boolean(levels.data);
13430
+ const mayBuildHere = levelsKnown && Boolean(tableId) && levels.data?.[tableId] === "admin";
13421
13431
  const toggle = (0, import_react95.useCallback)(
13422
13432
  async (page) => {
13423
13433
  setBusy(page.form_id);
@@ -13466,7 +13476,8 @@ function BookingSlots({ tableId, className }) {
13466
13476
  pages.length === 0 && !building ? tableId ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13467
13477
  BuildOrAsk,
13468
13478
  {
13469
- mayBuild: true,
13479
+ mayBuild: mayBuildHere,
13480
+ whyNot: mayBuildHere ? null : levelsKnown ? NO_ADMIN2 : null,
13470
13481
  onBuild: () => setBuilding(true),
13471
13482
  ...host.onAskForOne ? {
13472
13483
  onAsk: () => host.onAskForOne?.({
@@ -13484,7 +13495,7 @@ function BookingSlots({ tableId, className }) {
13484
13495
  " Open the table the appointments should land in to make one."
13485
13496
  ] }) : null,
13486
13497
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("ul", { className: "flex flex-col gap-2", children: pages.map((page) => {
13487
- const url = `${origin}${(0, import_records11.bookingPath)(page.form_id)}`;
13498
+ const url = `${origin}${(0, import_records12.bookingPath)(page.form_id)}`;
13488
13499
  const mayOpen = levels.data?.[page.table_id] === "admin";
13489
13500
  const open = page.published_at !== null && page.closed_at === null;
13490
13501
  return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("li", { className: "rounded-md border p-2.5", "data-testid": "booking-page", children: [
@@ -13573,7 +13584,7 @@ var import_design_system49 = require("@ai-matrx/design-system");
13573
13584
 
13574
13585
  // src/CaptureRun.tsx
13575
13586
  var import_react97 = require("react");
13576
- var import_records12 = require("@ai-matrx/records");
13587
+ var import_records13 = require("@ai-matrx/records");
13577
13588
  var import_react98 = require("@ai-matrx/records/react");
13578
13589
  var import_design_system48 = require("@ai-matrx/design-system");
13579
13590
  var import_jsx_runtime51 = require("react/jsx-runtime");
@@ -13628,7 +13639,7 @@ function CaptureRun({ sheetId, face: given, className }) {
13628
13639
  const [sending, setSending] = (0, import_react97.useState)(false);
13629
13640
  const queueRef = (0, import_react97.useRef)(null);
13630
13641
  (0, import_react97.useEffect)(() => {
13631
- const q2 = (0, import_records12.openCaptureQueue)({
13642
+ const q2 = (0, import_records13.openCaptureQueue)({
13632
13643
  onChange: (c, all) => {
13633
13644
  setCounts(c);
13634
13645
  setItems(all);
@@ -13877,7 +13888,11 @@ function CaptureRun({ sheetId, face: given, className }) {
13877
13888
  value: String(answers[q.field] ?? ""),
13878
13889
  onChange: (e) => {
13879
13890
  const raw = e.target.value;
13880
- setAnswers((held) => ({ ...held, [q.field]: raw === "" ? "" : Number(raw) }));
13891
+ const answered2 = field ? (0, import_records13.coerceTypedAnswer)(field, raw, { label: q.ask ?? (0, import_records13.coercionLabel)(field) }) : { value: raw };
13892
+ setAnswers((held) => ({
13893
+ ...held,
13894
+ [q.field]: raw === "" ? "" : "refusal" in answered2 ? raw : answered2.value
13895
+ }));
13881
13896
  setMissing(null);
13882
13897
  }
13883
13898
  }