@ai-matrx/records-ui 0.63.0 → 0.66.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.js CHANGED
@@ -80,12 +80,12 @@ var NO_RIGHTS = tableRights(NOT_ANSWERED_YET);
80
80
  function tableRightsAt(level) {
81
81
  return tableRights(whatYouMayDo(level, true));
82
82
  }
83
- var NO_SAVED_VIEWS_REASON = "No saved-view port is bound, so there is nothing here to subscribe to. A subscription points at a `platform.saved_view`, which the record store has no door onto \u2014 bind `savedViews` on <RecordsUiProvider> with your host's own list and the picker appears.";
84
- var NO_MEMBERS_REASON = "No membership port is bound, so this question cannot offer anybody. A person field points at the kernel Person Table, and who is in this organization is the platform's answer rather than the record store's \u2014 bind `members` on <RecordsUiProvider> with your host's own roster and the picker appears.";
85
- var NO_SHARE_REASON = "No share dialog is bound, so this screen offers no Share button rather than one that opens nothing. Bind `share` on <RecordsUiProvider> with the app's own sharing dialog \u2014 AI Matrx has exactly one (features/sharing), and this package deliberately does not ship a second.";
86
- var NO_OPEN_RECORDS_REASON = "These numbers do not click through yet. Every one of them is a count of real records, and where this app puts its grid is the host's routing rather than this package's \u2014 bind `openRecords` on <RecordsUiProvider> and a bar, a slice or a row opens the records it counted.";
87
- var NO_REASK_REASON = "Asking for a change in words needs an agent, and this package deliberately reaches one through the host rather than calling a model itself. Bind `onReask` on <RecordsUiProvider> \u2014 it drives `dashboard_propose` on the server's `records` tool with this dashboard's id \u2014 and this box works.";
88
- var NO_UPLOAD_REASON = "No file store is bound, so this question cannot take a file. Bind `upload` on <RecordsUiProvider> with your host's file service and the chooser appears.";
83
+ var NO_SAVED_VIEWS_REASON = "There are no saved views here yet, so there is nothing to be told about. Save a view of this table first and it will be offered here.";
84
+ var NO_MEMBERS_REASON = "This app cannot offer the people in your organization on this screen yet, so there is nobody to pick. Type the person's name into a text field for now, and tell us \u2014 it is on our list, not yours.";
85
+ var NO_SHARE_REASON = "Sharing is not available on this screen, so there is no Share button here rather than one that opens nothing. Share this table from the tables list instead.";
86
+ var NO_OPEN_RECORDS_REASON = "These numbers do not click through yet. Every one of them is a count of real records \u2014 open the records view and group it by the same field to see them. It is on our list, not yours.";
87
+ var NO_REASK_REASON = "Changing this dashboard by describing it is not switched on here yet, so this box is off rather than one that goes nowhere. Add and remove the blocks by hand above. It is on our list, not yours.";
88
+ var NO_UPLOAD_REASON = "This screen cannot take a file yet, so there is no chooser here rather than one that loses what you pick. Put a link to the file in a text field for now. It is on our list, not yours.";
89
89
  var HostContext = createContext(null);
90
90
  function RecordsUiProvider({ value, children }) {
91
91
  const bound = useMemo(() => value, [value]);
@@ -160,6 +160,10 @@ var TITLE = {
160
160
  not_supported: "That cannot be done here",
161
161
  store_limit: "This has got as big as it can",
162
162
  store_not_served: "This organization does not keep its data here",
163
+ unreachable: "We could not reach your data",
164
+ needs_approval: "This is waiting on someone's approval",
165
+ stale_read: "Your view of this is a moment behind",
166
+ timed_out: "That took too long to answer",
163
167
  internal: "Something went wrong at our end"
164
168
  };
165
169
  var REMEDY = {
@@ -174,8 +178,35 @@ var REMEDY = {
174
178
  not_supported: "There is another way to do this \u2014 ask, and we will point you at it.",
175
179
  store_limit: "Split it up, or ask us to raise the ceiling for your organization.",
176
180
  store_not_served: "An administrator of this organization turns the record store on from the data settings screen.",
181
+ // 🚨 NOTHING WAS WRITTEN AND NOTHING WAS REFUSED (lane TAILS-6). The call
182
+ // never arrived, so "try again" is the honest instruction rather than a
183
+ // consolation — and saying nothing changed is the half a person actually
184
+ // worries about.
185
+ unreachable: "Nothing was changed. Check your connection and open this again.",
186
+ needs_approval: "Nothing was changed yet. The people who can approve it have been asked.",
187
+ stale_read: "Open this again in a moment and it will be up to date.",
188
+ timed_out: "Nothing was changed. Try a smaller page, or try again in a moment.",
177
189
  internal: "Nothing you did caused this. Try again, and tell us if it keeps happening."
178
190
  };
191
+ var ENGINEER_TEXT = [
192
+ /@ai-matrx\//,
193
+ /\bSQLSTATE\b/i,
194
+ /\bPostgREST\b/i,
195
+ /\bIndexedDB\b/i,
196
+ // `<RecordsUiProvider>` — a component, on a screen.
197
+ /<\/?[A-Z][A-Za-z0-9]*\s*\/?>/,
198
+ // `savedViews`, `openRecords`, `onReask` — an identifier, however it is
199
+ // quoted. English does not write camelCase; code does.
200
+ /\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b/,
201
+ // A backticked token is a thing you type into an editor, not a thing you do.
202
+ /`[A-Za-z_][A-Za-z0-9_.]*`/,
203
+ // "bind X on Y", "pass X", "import X" — an instruction to whoever maintains
204
+ // the app, arriving on the screen of somebody who cannot act on it.
205
+ /\bbind\s+[`'"A-Za-z]/i
206
+ ];
207
+ function isEngineerText(text) {
208
+ return ENGINEER_TEXT.some((shape) => shape.test(text));
209
+ }
179
210
  function withoutMachineClauses(sentence2) {
180
211
  const dropped = [];
181
212
  const footnotes = [];
@@ -213,11 +244,17 @@ function refusalForAPerson(error) {
213
244
  const message = plainSentence(String(error.message ?? ""));
214
245
  const hint = plainSentence(String(error.hint ?? ""));
215
246
  const title = TITLE[code] ?? "The store refused this";
216
- const remedy = hint.kept ?? REMEDY[code] ?? REMEDY["internal"];
217
- const said = (message.kept ?? "").trim();
247
+ const hintIsForEngineers = isEngineerText(String(error.hint ?? ""));
248
+ const remedy = (hintIsForEngineers ? null : hint.kept) ?? REMEDY[code] ?? REMEDY["internal"];
249
+ const messageIsForEngineers = isEngineerText(String(error.message ?? ""));
250
+ const said = (messageIsForEngineers ? "" : message.kept ?? "").trim();
218
251
  const forEngineers = [
219
252
  error.sqlstate ? `SQLSTATE ${error.sqlstate}.` : null,
220
- error.hint && hint.kept !== error.hint ? `Hint: ${error.hint}` : null,
253
+ // The client's own note about a class it did not recognise, or a call that
254
+ // never reached the store. It is rendered out of sight, never as a sentence.
255
+ error.diagnostic ? error.diagnostic : null,
256
+ messageIsForEngineers ? `Message: ${error.message}` : null,
257
+ error.hint && (hintIsForEngineers || hint.kept !== error.hint) ? `Hint: ${error.hint}` : null,
221
258
  message.dropped.length > 0 || hint.dropped.length > 0 ? `Dropped from the person's view: ${[...message.dropped, ...hint.dropped].join(" | ")}` : null
222
259
  ].filter(Boolean).join(" ");
223
260
  return {
@@ -524,7 +561,11 @@ import {
524
561
  } from "@ai-matrx/design-system";
525
562
 
526
563
  // src/parity.ts
527
- import { KERNEL_TABLES, PARITY_FIELD_TYPES as PARITY_FIELD_TYPES2 } from "@ai-matrx/records";
564
+ import {
565
+ KERNEL_TABLES,
566
+ PARITY_FIELD_TYPES as PARITY_FIELD_TYPES2,
567
+ fieldKindFor
568
+ } from "@ai-matrx/records";
528
569
 
529
570
  // src/fieldTypes.ts
530
571
  import { PARITY_FIELD_TYPES } from "@ai-matrx/records";
@@ -591,50 +632,9 @@ var PARITY_LABEL = {
591
632
  var PARITY_MADE_OF = Object.fromEntries(
592
633
  PARITY_FIELD_TYPES2.map((p) => [p.parity_type, p.made_of])
593
634
  );
635
+ var KERNEL = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };
594
636
  function editorKindFor(field) {
595
- const format = (field.format ?? "").toLowerCase();
596
- const config = field.config ?? {};
597
- switch (field.type) {
598
- // LIMITS-FIX 2026-09-21: the tick box is a BEHAVIOUR of its own, so its editor is
599
- // decided by the behaviour alone — no format, no unit, no target to read. A record
600
- // that never answered holds no key at all, and `CheckboxControl` draws that third
601
- // state rather than pretending it is an unticked box.
602
- case "boolean":
603
- return "checkbox";
604
- case "text":
605
- if (format === "email") return "email";
606
- if (format === "phone") return "phone";
607
- if (format === "url") return "url";
608
- if (format === "json" || config["json"] === true) return "json";
609
- if (format === "long" || config["multiline"] === true) return "long_text";
610
- return "text";
611
- case "range": {
612
- const kind = String(config["kind"] ?? "number");
613
- if (kind === "date" || kind === "datetime") return "datetime";
614
- if (format === "currency") return "currency";
615
- if (format === "percent" || field.unit === "%") return "percent";
616
- return "number";
617
- }
618
- case "list":
619
- return field.multi ? "multi_select" : "select";
620
- case "relation":
621
- return relationEditor(field);
622
- case "formula": {
623
- const via = config["via"];
624
- if (via && config["agg"]) return "rollup";
625
- if (via) return "lookup";
626
- return "formula";
627
- }
628
- default:
629
- return "text";
630
- }
631
- }
632
- function relationEditor(field) {
633
- const parity = String(field.config?.["parity"] ?? "");
634
- if (parity === "attachment" || parity === "member") return parity;
635
- if (field.relation_target === KERNEL_FILE_TABLE) return "attachment";
636
- if (field.relation_target === KERNEL_PERSON_TABLE) return "member";
637
- return "relation";
637
+ return fieldKindFor(field, KERNEL);
638
638
  }
639
639
  function fieldTypeLabel(field) {
640
640
  const kind = editorKindFor(field);
@@ -1541,18 +1541,137 @@ function useLabelWait(field) {
1541
1541
  return field ? waiting(field) : null;
1542
1542
  }
1543
1543
 
1544
- // src/values.tsx
1545
- import { Fragment as Fragment3, jsx as jsx9 } from "react/jsx-runtime";
1546
- function envelopeFor(document2, key) {
1547
- const block = document2?._values;
1548
- return block?.[key];
1544
+ // src/enumLabels.ts
1545
+ function exhaustiveValuesOf() {
1546
+ return function(values) {
1547
+ return values;
1548
+ };
1549
1549
  }
1550
- var ABSENCE_WORDS = {
1550
+ var WORK_DUE_STATES = exhaustiveValuesOf()([
1551
+ "overdue",
1552
+ "due_today",
1553
+ "scheduled",
1554
+ "undated",
1555
+ "finished"
1556
+ ]);
1557
+ var DUE_STATE_LABEL = {
1558
+ overdue: "Overdue",
1559
+ due_today: "Today",
1560
+ scheduled: "Scheduled",
1561
+ undated: "No date",
1562
+ finished: "Done"
1563
+ };
1564
+ var WORK_INBOX_KINDS = exhaustiveValuesOf()(["assignment", "approval", "proposal"]);
1565
+ var WORK_INBOX_KIND_LABEL = {
1566
+ assignment: "Yours",
1567
+ approval: "Approve",
1568
+ proposal: "Agent"
1569
+ };
1570
+ var CHECKLIST_REQUIREMENT_KINDS = exhaustiveValuesOf()([
1571
+ "none",
1572
+ "note",
1573
+ "answer",
1574
+ "record_field",
1575
+ "form",
1576
+ "document"
1577
+ ]);
1578
+ var CHECKLIST_REQUIREMENT_LABEL = {
1579
+ none: "",
1580
+ note: "Say what you did",
1581
+ answer: "Fill this in",
1582
+ record_field: "This has to be filled in on the record",
1583
+ form: "This step is a form",
1584
+ document: "This step is a document"
1585
+ };
1586
+ var FIELD_SENSITIVITY_LABEL = {
1587
+ public: "Anyone who can open the record",
1588
+ internal: "People in this organization",
1589
+ confidential: "People who have been given the record",
1590
+ restricted: "Owners and administrators only"
1591
+ };
1592
+ var CONTEXT_POLICY_LABEL = {
1593
+ include: "Yes, an assistant can read it",
1594
+ summarize: "Only a summary of it",
1595
+ on_request: "Only when somebody asks for it",
1596
+ exclude: "No, keep it away from assistants"
1597
+ };
1598
+ var STAGE_RULE_ON_FAIL_LABEL = {
1599
+ refuse: "Turn it away",
1600
+ require_approval: "Ask for approval"
1601
+ };
1602
+ var SUBSCRIPTION_CADENCES = exhaustiveValuesOf()(["instant", "hourly", "daily", "weekly"]);
1603
+ var SUBSCRIPTION_CADENCE_LABEL = {
1604
+ instant: "as it happens",
1605
+ hourly: "hourly summary",
1606
+ daily: "daily summary",
1607
+ weekly: "weekly summary"
1608
+ };
1609
+ var SUBSCRIPTION_CHANNEL_VALUES = ["in_app", "email", "sms"];
1610
+ var SUBSCRIPTION_CHANNEL_LABEL = {
1611
+ in_app: "in the app",
1612
+ email: "by email \u2014 only if an address is on the account",
1613
+ sms: "by text \u2014 only if a number is on the account"
1614
+ };
1615
+ var ROLLUP_AGG_VALUES = ["count", "sum", "avg", "min", "max"];
1616
+ var ROLLUP_AGG_LABEL = {
1617
+ count: "How many there are",
1618
+ sum: "The total of",
1619
+ avg: "The average of",
1620
+ min: "The smallest",
1621
+ max: "The largest"
1622
+ };
1623
+ var FORMULA_OP_VALUES = ["concat", "add", "sub", "mul", "div"];
1624
+ var FORMULA_OP_LABEL = {
1625
+ concat: "Join them together",
1626
+ add: "Add them up",
1627
+ sub: "Take the second from the first",
1628
+ mul: "Multiply them",
1629
+ div: "Divide the first by the second"
1630
+ };
1631
+ var FORMULA_OP_JOINS = {
1632
+ concat: true,
1633
+ add: true,
1634
+ sub: false,
1635
+ mul: true,
1636
+ div: false
1637
+ };
1638
+ var FIELD_RULE_KIND_VALUES = ["min", "max", "length", "pattern", "equals_field", "differs_from_field"];
1639
+ var FIELD_RULE_KIND_LABEL = {
1640
+ min: "At least",
1641
+ max: "At most",
1642
+ length: "No longer than",
1643
+ pattern: "Looks like",
1644
+ equals_field: "Same as the column",
1645
+ differs_from_field: "Different from the column"
1646
+ };
1647
+ var PROPOSED_CHANGE_ACT_VALUES = ["add", "update", "remove"];
1648
+ var PROPOSED_CHANGE_ACT_LABEL = {
1649
+ add: "Add",
1650
+ update: "Update",
1651
+ remove: "Remove"
1652
+ };
1653
+ var BOOKING_PAGE_STATE_VALUES = ["draft", "open", "closed", "full"];
1654
+ var BOOKING_PAGE_STATE_LABEL = {
1655
+ draft: "Not open yet \u2014 nobody can book until you open it.",
1656
+ open: "Open. Anyone with the link can book a time.",
1657
+ closed: "Closed. The link still works and says you are not taking bookings.",
1658
+ full: "Every time you offered is taken."
1659
+ };
1660
+ var ABSENCE_WORD_VALUES = ["never asked", "none", "refused", "conflicting"];
1661
+ var ABSENCE_WORD_LABEL = {
1551
1662
  "never asked": "never asked",
1552
1663
  none: "none",
1553
1664
  refused: "refused",
1554
1665
  conflicting: "conflicting"
1555
1666
  };
1667
+
1668
+ // src/values.tsx
1669
+ import { Fragment as Fragment3, jsx as jsx9 } from "react/jsx-runtime";
1670
+ function envelopeFor(document2, key) {
1671
+ const block = document2?._values;
1672
+ return block?.[key];
1673
+ }
1674
+ var ABSENCE_WORDS = ABSENCE_WORD_LABEL;
1556
1675
  function renderValue(field, value, document2, labels, stillWaitingFor) {
1557
1676
  const envelope = envelopeFor(document2, field.key);
1558
1677
  if (envelope?.absent) {
@@ -2400,7 +2519,7 @@ import {
2400
2519
  cn as cn8
2401
2520
  } from "@ai-matrx/design-system";
2402
2521
  import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2403
- var NO_ENRICH_REASON = "No enrich port is bound, so this panel does not offer to fill a column it cannot fill. The value has to be written by the SERVER with actor `agent` on behalf of you \u2014 a browser that stamped itself `agent` would be a browser claiming to be one \u2014 so bind `enrich` on <RecordsUiProvider> with a call to the server's own `records` tool (action `enrich_propose`). Everything else on this panel works without it.";
2522
+ var NO_ENRICH_REASON = "Having an agent fill this column is not switched on here yet, so this panel does not offer it rather than offering something that cannot happen. Everything else on this panel works. It is on our list, not yours.";
2404
2523
  function cellState(cell) {
2405
2524
  if (!cell || !cell.agent_owned) return "none";
2406
2525
  if (cell.pinned) return "yours";
@@ -2982,7 +3101,7 @@ function confirmLabel(plan, noun, nounPlural) {
2982
3101
 
2983
3102
  // src/pasteValues.ts
2984
3103
  import { predictValueRefusals } from "@ai-matrx/records/core";
2985
- import { optionKey as optionKey2 } from "@ai-matrx/records";
3104
+ import { coerceTypedAnswer, optionKey as optionKey2 } from "@ai-matrx/records";
2986
3105
  function choiceOptionsOf(options) {
2987
3106
  const byWord = /* @__PURE__ */ new Map();
2988
3107
  const labels = [];
@@ -2999,113 +3118,11 @@ function choiceOptionsOf(options) {
2999
3118
  }
3000
3119
  return { byWord, labels };
3001
3120
  }
3002
- var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
3003
- var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
3004
3121
  function valueFromPastedText(field, raw, options) {
3005
- const label = fieldName(field);
3006
- const text = raw.trim();
3007
- const kind = editorKindFor(field);
3008
- if (text === "" && kind !== "checkbox") return { value: null };
3009
- switch (kind) {
3010
- case "checkbox": {
3011
- const word = text.toLowerCase();
3012
- if (TRUE_WORDS.has(word)) return { value: true };
3013
- if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
3014
- return {
3015
- 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.`
3016
- };
3017
- }
3018
- case "number":
3019
- case "currency":
3020
- case "percent": {
3021
- const negative = /^\(.*\)$/.test(text);
3022
- const bare = text.replace(/^\(|\)$/g, "").replace(/[\s,]/g, "").replace(/^[^\d.\-+]+/, "").replace(/[^\d.\-+eE]+$/, "");
3023
- const n = Number(bare);
3024
- if (bare === "" || Number.isNaN(n)) {
3025
- return {
3026
- 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.`
3027
- };
3028
- }
3029
- return { value: negative ? -n : n };
3030
- }
3031
- case "datetime": {
3032
- const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
3033
- const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
3034
- if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
3035
- const at = Date.parse(text);
3036
- if (Number.isNaN(at)) {
3037
- return {
3038
- 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.`
3039
- };
3040
- }
3041
- const parsed = new Date(at);
3042
- if (wantsTime) return { value: parsed.toISOString() };
3043
- const y = parsed.getFullYear();
3044
- const m = String(parsed.getMonth() + 1).padStart(2, "0");
3045
- const d = String(parsed.getDate()).padStart(2, "0");
3046
- return { value: `${y}-${m}-${d}` };
3047
- }
3048
- case "json": {
3049
- try {
3050
- return { value: JSON.parse(text) };
3051
- } catch {
3052
- return {
3053
- refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Paste the whole object, braces and all.`
3054
- };
3055
- }
3056
- }
3057
- // A LIST, A PERSON, A FILE OR ANOTHER RECORD IS AN IDENTITY, NOT A WORD.
3058
- // The store keeps the id of the option or the record, and a word off a
3059
- // clipboard is not that id — writing the word would put a string where an
3060
- // id belongs and every filter, colour and rollup built on that column would
3061
- // stop agreeing with it. So the cell is left alone and the person is told
3062
- // which control does hold it.
3063
- case "select":
3064
- case "multi_select": {
3065
- if (!options) {
3066
- return {
3067
- 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.`
3068
- };
3069
- }
3070
- const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
3071
- if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
3072
- const matched = [];
3073
- const unmatched = [];
3074
- for (const word of wanted) {
3075
- const hit = options.byWord.get(word.toLowerCase());
3076
- if (hit === void 0) unmatched.push(word);
3077
- else matched.push(hit);
3078
- }
3079
- if (unmatched.length > 0) {
3080
- const offered = options.labels.length > 0 ? options.labels.join(", ") : "it has none yet";
3081
- return {
3082
- refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
3083
- unmatched
3084
- };
3085
- }
3086
- return { value: kind === "multi_select" ? matched : matched[0] };
3087
- }
3088
- case "member":
3089
- return {
3090
- refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open the cell and pick them.`
3091
- };
3092
- case "attachment":
3093
- return {
3094
- refusal: `${label} holds a file, which cannot arrive on a clipboard. Open the cell and attach it.`
3095
- };
3096
- case "relation":
3097
- return {
3098
- refusal: `${label} points at another record, so it is chosen rather than typed. Open the cell and pick the record.`
3099
- };
3100
- case "formula":
3101
- case "lookup":
3102
- case "rollup":
3103
- return {
3104
- refusal: `${label} is worked out by the system, so nothing can be pasted into it.`
3105
- };
3106
- default:
3107
- return { value: raw };
3108
- }
3122
+ return coerceTypedAnswer(field, raw, {
3123
+ label: fieldName(field),
3124
+ ...options ? { options } : {}
3125
+ });
3109
3126
  }
3110
3127
  function planPastedBlock(args) {
3111
3128
  const byKey = new Map(args.fields.map((f) => [f.key, f]));
@@ -4282,19 +4299,23 @@ import {
4282
4299
  } from "@ai-matrx/records/react";
4283
4300
  import { Button as Button11, Separator as Separator3, cn as cn12 } from "@ai-matrx/design-system";
4284
4301
  import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4285
- var ADDABLE_TYPES = [
4286
- { value: "text", label: "Text" },
4287
- { value: "long_text", label: "Long text" },
4288
- { value: "number", label: "Number" },
4289
- { value: "currency", label: "Money" },
4290
- { value: "percent", label: "Percentage" },
4291
- { value: "datetime", label: "Date" },
4292
- { value: "email", label: "Email" },
4293
- { value: "phone", label: "Phone" },
4294
- { value: "url", label: "Link" },
4295
- { value: "member", label: "Person" },
4296
- { value: "attachment", label: "File" }
4302
+ var ADDABLE_TYPE_IDS = [
4303
+ "text",
4304
+ "long_text",
4305
+ "number",
4306
+ "currency",
4307
+ "percent",
4308
+ "datetime",
4309
+ "email",
4310
+ "phone",
4311
+ "url",
4312
+ "member",
4313
+ "attachment"
4297
4314
  ];
4315
+ var ADDABLE_TYPES = ADDABLE_TYPE_IDS.map((value) => ({
4316
+ value,
4317
+ label: fieldTypeChoice(value)?.label ?? value
4318
+ }));
4298
4319
  function asField(f) {
4299
4320
  return {
4300
4321
  id: f.id,
@@ -4473,41 +4494,20 @@ import {
4473
4494
  Separator as Separator4,
4474
4495
  cn as cn13
4475
4496
  } from "@ai-matrx/design-system";
4497
+ import { FIELD_SENSITIVITIES, CONTEXT_POLICIES } from "@ai-matrx/records";
4476
4498
  import { Fragment as Fragment8, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
4477
- var RULE_KINDS = [
4478
- { kind: "min", label: "At least" },
4479
- { kind: "max", label: "At most" },
4480
- { kind: "length", label: "No longer than" },
4481
- { kind: "pattern", label: "Looks like" },
4482
- { kind: "equals_field", label: "Same as the column" },
4483
- { kind: "differs_from_field", label: "Different from the column" }
4484
- ];
4485
- var SENSITIVITIES = [
4486
- { value: "public", label: "Anyone who can open the record" },
4487
- { value: "internal", label: "People in this organization" },
4488
- { value: "confidential", label: "People who have been given the record" },
4489
- { value: "restricted", label: "Owners and administrators only" }
4490
- ];
4491
- var AGENT_VISIBILITY = [
4492
- { value: "include", label: "Yes, an assistant can read it" },
4493
- { value: "summarize", label: "Only a summary of it" },
4494
- { value: "on_request", label: "Only when somebody asks for it" },
4495
- { value: "exclude", label: "No, keep it away from assistants" }
4496
- ];
4497
- var ROLLUP_OPERATIONS = [
4498
- { value: "count", label: "How many there are" },
4499
- { value: "sum", label: "The total of" },
4500
- { value: "avg", label: "The average of" },
4501
- { value: "min", label: "The smallest" },
4502
- { value: "max", label: "The largest" }
4503
- ];
4504
- var FORMULA_OPERATIONS = [
4505
- { value: "concat", label: "Join them together", joins: true },
4506
- { value: "add", label: "Add them up", joins: true },
4507
- { value: "sub", label: "Take the second from the first", joins: false },
4508
- { value: "mul", label: "Multiply them", joins: true },
4509
- { value: "div", label: "Divide the first by the second", joins: false }
4510
- ];
4499
+ var RULE_KINDS = FIELD_RULE_KIND_VALUES.map((kind) => ({ kind, label: FIELD_RULE_KIND_LABEL[kind] }));
4500
+ var SENSITIVITIES = FIELD_SENSITIVITIES.map((value) => ({ value, label: FIELD_SENSITIVITY_LABEL[value] }));
4501
+ var AGENT_VISIBILITY = CONTEXT_POLICIES.map((value) => ({ value, label: CONTEXT_POLICY_LABEL[value] }));
4502
+ var ROLLUP_OPERATIONS = ROLLUP_AGG_VALUES.map((value) => ({
4503
+ value,
4504
+ label: ROLLUP_AGG_LABEL[value]
4505
+ }));
4506
+ var FORMULA_OPERATIONS = FORMULA_OP_VALUES.map((value) => ({
4507
+ value,
4508
+ label: FORMULA_OP_LABEL[value],
4509
+ joins: FORMULA_OP_JOINS[value]
4510
+ }));
4511
4511
  function keyFor(label) {
4512
4512
  const token = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
4513
4513
  if (token === "") return "";
@@ -5072,6 +5072,9 @@ var CONDITION_OPS = [
5072
5072
  { op: "gt", label: "is more than" },
5073
5073
  { op: "lt", label: "is less than" }
5074
5074
  ];
5075
+ function pointsAtARecord(f) {
5076
+ return !!f?.field && f.field.type === "relation" && !!f.field.relation_target;
5077
+ }
5075
5078
  function conditionIsSimple(expr) {
5076
5079
  if (expr === null || expr === void 0) return true;
5077
5080
  const node = expr;
@@ -5142,6 +5145,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5142
5145
  const op = node?.op ?? "";
5143
5146
  const onField = node?.args?.[0]?.field ?? "";
5144
5147
  const value = node?.args?.[1]?.const ?? "";
5148
+ const chosen = fields.find((f) => f.id === onField);
5145
5149
  function write(next) {
5146
5150
  const chosenOp = next.op ?? op;
5147
5151
  const chosenField = next.field ?? onField;
@@ -5180,7 +5184,24 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5180
5184
  children: CONDITION_OPS.map((c) => /* @__PURE__ */ jsx18("option", { value: c.op, children: c.label }, c.op))
5181
5185
  }
5182
5186
  ),
5183
- op !== "present" ? /* @__PURE__ */ jsx18(
5187
+ op === "present" ? null : pointsAtARecord(chosen) ? (
5188
+ // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
5189
+ // cell uses, on the same `relation_words_many` the cell reads, so a filter and
5190
+ // the column it filters can never disagree about what a record is called. It
5191
+ // is asked for ONE record even when the column holds several: a clause compares
5192
+ // against one value, and a picker that let somebody choose three would be
5193
+ // writing an expression this row cannot draw back.
5194
+ /* @__PURE__ */ jsx18(
5195
+ RelationPicker,
5196
+ {
5197
+ field: { ...chosen.field, multi: false },
5198
+ value: value === "" ? null : value,
5199
+ onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
5200
+ id: "condition-value-record",
5201
+ className: "min-w-[8rem]"
5202
+ }
5203
+ )
5204
+ ) : /* @__PURE__ */ jsx18(
5184
5205
  BasicInput6,
5185
5206
  {
5186
5207
  className: "h-8 w-28 text-xs",
@@ -5188,7 +5209,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5188
5209
  value: String(value ?? ""),
5189
5210
  onChange: (e) => write({ value: conditionValue(e.target.value) })
5190
5211
  }
5191
- ) : null
5212
+ )
5192
5213
  ] }) : null
5193
5214
  ] });
5194
5215
  }
@@ -5348,10 +5369,10 @@ var NO_RULES_YET = "Nothing has to be true to get here yet.";
5348
5369
  var NEEDS_A_SENTENCE = "Write the sentence somebody reads when this stops them, the way you would say it out loud.";
5349
5370
  var NOT_DRAWN_HERE = "This part of the rule is not something this builder can draw yet";
5350
5371
  var ON_FAIL = [
5351
- { value: "refuse", label: "Turn it away", note: "The card stays where it is and the person is told why." },
5372
+ { value: "refuse", label: STAGE_RULE_ON_FAIL_LABEL.refuse, note: "The card stays where it is and the person is told why." },
5352
5373
  {
5353
5374
  value: "require_approval",
5354
- label: "Ask for approval",
5375
+ label: STAGE_RULE_ON_FAIL_LABEL.require_approval,
5355
5376
  note: "The card stays where it is, and the people who can approve it are asked. Nobody is turned away."
5356
5377
  }
5357
5378
  ];
@@ -5402,7 +5423,7 @@ function StageRulesSection({ tableId, stage, className }) {
5402
5423
  const stageLabel = stages.find((s) => s.key === stageKey)?.label ?? stageKey ?? "";
5403
5424
  const gates = useMemo13(() => stageKey ? gatesOf(pipeline, stageKey) : [], [pipeline, stageKey]);
5404
5425
  const conditionFields = useMemo13(
5405
- () => (fields.data ?? []).map((f) => ({ id: String(f.id), key: f.key, label: fieldName(f) })),
5426
+ () => (fields.data ?? []).map((f) => ({ id: String(f.id), key: f.key, label: fieldName(f), field: f })),
5406
5427
  [fields.data]
5407
5428
  );
5408
5429
  useEffect8(() => {
@@ -5832,7 +5853,7 @@ import { useEffect as useEffect9, useMemo as useMemo14, useState as useState18 }
5832
5853
  import { useRecordsClient as useRecordsClient12 } from "@ai-matrx/records/react";
5833
5854
  import { Skeleton as Skeleton4, cn as cn16 } from "@ai-matrx/design-system";
5834
5855
  import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
5835
- var NO_CHAT_REASON = "No chat port is bound, so this panel is absent rather than showing a composer that goes nowhere. Bind `chat` on <RecordsUiProvider> with your host's own chat surface \u2014 in AI Matrx that is `AgentConversationColumn`, launched through a Mandate \u2014 and this panel becomes that surface with this record as its context. This package deliberately ships no second chat UI.";
5856
+ var NO_CHAT_REASON = "Chatting about this record is not switched on here yet, so this panel is empty rather than showing a box that goes nowhere. It is on our list, not yours.";
5836
5857
  function RecordChat({ tableId, recordId, className }) {
5837
5858
  const client = useRecordsClient12();
5838
5859
  const host = useRecordsUi();
@@ -7103,7 +7124,7 @@ function offerableFields(all, want) {
7103
7124
  return all.filter((field) => {
7104
7125
  const kind = editorKindFor(field);
7105
7126
  if (want === "choice") return kind === "select" || kind === "multi_select";
7106
- return kind === "datetime" || kind === "date";
7127
+ return kind === "datetime";
7107
7128
  });
7108
7129
  }
7109
7130
  function useStageField(tableId) {
@@ -7521,11 +7542,7 @@ import { useState as useState26 } from "react";
7521
7542
  import { useRecordsClient as useRecordsClient18 } from "@ai-matrx/records/react";
7522
7543
  import { Badge as Badge7, Button as Button22, cn as cn22 } from "@ai-matrx/design-system";
7523
7544
  import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
7524
- var ACT_WORD = {
7525
- add: "Add",
7526
- update: "Update",
7527
- remove: "Remove"
7528
- };
7545
+ var ACT_WORD = PROPOSED_CHANGE_ACT_LABEL;
7529
7546
  function ProposalRow({
7530
7547
  change,
7531
7548
  outcome,
@@ -7632,21 +7649,8 @@ import { useCallback as useCallback12, useEffect as useEffect15, useMemo as useM
7632
7649
  import { useRecordsClient as useRecordsClient19, useTable as useTable9 } from "@ai-matrx/records/react";
7633
7650
  import { Badge as Badge8, BasicInput as BasicInput8, BasicTextarea as BasicTextarea4, Button as Button23, Skeleton as Skeleton9, cn as cn23 } from "@ai-matrx/design-system";
7634
7651
  import { Fragment as Fragment13, jsx as jsx29, jsxs as jsxs25 } from "react/jsx-runtime";
7635
- var DUE_WORD = {
7636
- overdue: "Overdue",
7637
- due_today: "Today",
7638
- scheduled: "Scheduled",
7639
- undated: "No date",
7640
- finished: "Done"
7641
- };
7642
- var REQUIRES_WORD = {
7643
- none: "",
7644
- note: "Say what you did",
7645
- answer: "Fill this in",
7646
- record_field: "This has to be filled in on the record",
7647
- form: "This step is a form",
7648
- document: "This step is a document"
7649
- };
7652
+ var DUE_WORD = DUE_STATE_LABEL;
7653
+ var REQUIRES_WORD = CHECKLIST_REQUIREMENT_LABEL;
7650
7654
  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.";
7651
7655
  function ChecklistRunner({
7652
7656
  tableId,
@@ -8354,18 +8358,8 @@ function roleKey(said) {
8354
8358
  // src/ActionInbox.tsx
8355
8359
  import { jsx as jsx30, jsxs as jsxs26 } from "react/jsx-runtime";
8356
8360
  var ACTION_KINDS = ["approval", "assignment", "proposal"];
8357
- var KIND_WORD = {
8358
- assignment: "Yours",
8359
- approval: "Approve",
8360
- proposal: "Agent"
8361
- };
8362
- var DUE_WORD2 = {
8363
- overdue: "Overdue",
8364
- due_today: "Today",
8365
- scheduled: "Scheduled",
8366
- undated: "No date",
8367
- finished: "Finished"
8368
- };
8361
+ var KIND_WORD = WORK_INBOX_KIND_LABEL;
8362
+ var DUE_WORD2 = DUE_STATE_LABEL;
8369
8363
  function ActionInbox({ tableId, includeSettled = false, onOpenRecord, className }) {
8370
8364
  const client = useRecordsClient20();
8371
8365
  const [items, setItems] = useState28(null);
@@ -9617,18 +9611,18 @@ function BuildOrAsk({
9617
9611
  buildLabel,
9618
9612
  className
9619
9613
  }) {
9614
+ const mayAsk = mayBuild && Boolean(onAsk);
9620
9615
  return /* @__PURE__ */ jsxs31("div", { className: cn29("rounded-md border border-dashed p-3", className), children: [
9621
9616
  /* @__PURE__ */ jsx35("p", { className: "text-xs text-muted-foreground", children }),
9622
- /* @__PURE__ */ jsxs31("div", { className: "mt-2.5 flex flex-wrap items-center gap-2", children: [
9623
- onAsk ? /* @__PURE__ */ jsx35(Button29, { size: "sm", variant: "outline", onClick: onAsk, children: "Ask an agent" }) : null,
9624
- mayBuild ? /* @__PURE__ */ jsx35(Button29, { size: "sm", onClick: onBuild, children: buildLabel }) : null
9625
- ] }),
9626
- onAsk ? /* @__PURE__ */ jsxs31("p", { className: "mt-2 text-xs text-muted-foreground", children: [
9617
+ mayBuild ? /* @__PURE__ */ jsxs31("div", { className: "mt-2.5 flex flex-wrap items-center gap-2", children: [
9618
+ mayAsk ? /* @__PURE__ */ jsx35(Button29, { size: "sm", variant: "outline", onClick: onAsk, children: "Ask an agent" }) : null,
9619
+ /* @__PURE__ */ jsx35(Button29, { size: "sm", onClick: onBuild, children: buildLabel })
9620
+ ] }) : null,
9621
+ !mayBuild ? whyNot ? /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: whyNot }) : null : mayAsk ? /* @__PURE__ */ jsxs31("p", { className: "mt-2 text-xs text-muted-foreground", children: [
9627
9622
  "You would say something like: \u201C",
9628
9623
  suggestion,
9629
9624
  "\u201D"
9630
- ] }) : /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: NO_AGENT_PORT }),
9631
- !mayBuild && whyNot ? /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: whyNot }) : null
9625
+ ] }) : /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: NO_AGENT_PORT })
9632
9626
  ] });
9633
9627
  }
9634
9628
 
@@ -9667,30 +9661,41 @@ function PortalsPanel({ tableId, className }) {
9667
9661
  const client = useRecordsClient25();
9668
9662
  const host = useRecordsUi();
9669
9663
  const [portals, setPortals] = useState33(null);
9664
+ const [exposures, setExposures] = useState33([]);
9670
9665
  const [listError, setListError] = useState33(null);
9671
9666
  const [openId, setOpenId] = useState33(null);
9672
9667
  const [building, setBuilding] = useState33(false);
9668
+ const [adding, setAdding] = useState33(null);
9673
9669
  const load = useCallback18(async () => {
9674
- const answered = await client.portals();
9670
+ const [answered, mapped] = await Promise.all([client.portals(), client.portalTables()]);
9675
9671
  if (!answered.ok) {
9676
9672
  setListError(answered.error);
9677
9673
  setPortals([]);
9674
+ setExposures([]);
9678
9675
  return;
9679
9676
  }
9680
9677
  setListError(null);
9681
9678
  setPortals(answered.data);
9679
+ setExposures(mapped.ok ? mapped.data : []);
9682
9680
  }, [client]);
9683
9681
  useEffect21(() => {
9684
9682
  void load();
9685
9683
  }, [load]);
9686
9684
  if (portals === null) return /* @__PURE__ */ jsx36(Skeleton15, { className: cn30("h-32 w-full", className) });
9685
+ const onATable = Boolean(tableId);
9686
+ const holdingThis = new Set(
9687
+ exposures.filter((e) => e.table_id === tableId).map((e) => e.portal_id)
9688
+ );
9689
+ const shown = onATable ? portals.filter((p) => holdingThis.has(p.portal_id)) : portals;
9690
+ const couldTakeIt = onATable ? portals.filter((p) => !holdingThis.has(p.portal_id)) : [];
9691
+ const elsewhere = couldTakeIt.length;
9687
9692
  const origin = host.publicOrigin ?? (typeof window === "undefined" ? "" : window.location.origin);
9688
9693
  return /* @__PURE__ */ jsxs32("section", { className: cn30("flex flex-col gap-3", className), children: [
9689
9694
  /* @__PURE__ */ jsxs32("header", { className: "flex items-center gap-2", children: [
9690
9695
  /* @__PURE__ */ jsx36("h3", { className: "text-sm font-medium", children: "Portals" }),
9691
- /* @__PURE__ */ jsx36("span", { className: "text-xs text-muted-foreground", children: portals.length === 0 ? "none yet" : `${portals.length}` }),
9696
+ /* @__PURE__ */ jsx36("span", { className: "text-xs text-muted-foreground", children: shown.length === 0 ? "none for this table" : `${shown.length}` }),
9692
9697
  /* @__PURE__ */ jsx36("div", { className: "flex-1" }),
9693
- portals.length > 0 ? /* @__PURE__ */ jsx36(Button30, { size: "sm", variant: building ? "secondary" : "ghost", onClick: () => setBuilding((b) => !b), children: building ? "Done building" : "Build a portal" }) : null
9698
+ shown.length > 0 ? /* @__PURE__ */ jsx36(Button30, { size: "sm", variant: building ? "secondary" : "ghost", onClick: () => setBuilding((b) => !b), children: building ? "Done building" : "Build a portal" }) : null
9694
9699
  ] }),
9695
9700
  listError ? /* @__PURE__ */ jsx36(RefusalNotice, { error: listError }) : null,
9696
9701
  building ? /* @__PURE__ */ jsx36(
@@ -9703,7 +9708,19 @@ function PortalsPanel({ tableId, className }) {
9703
9708
  onClose: () => setBuilding(false)
9704
9709
  }
9705
9710
  ) : null,
9706
- portals.length === 0 && !listError && !building ? /* @__PURE__ */ jsx36(
9711
+ adding ? /* @__PURE__ */ jsx36(
9712
+ PortalBuilder,
9713
+ {
9714
+ ...tableId ? { tableId } : {},
9715
+ portalId: adding,
9716
+ onSaved: () => {
9717
+ setAdding(null);
9718
+ void load();
9719
+ },
9720
+ onClose: () => setAdding(null)
9721
+ }
9722
+ ) : null,
9723
+ shown.length === 0 && !listError && !building && !adding ? /* @__PURE__ */ jsxs32(
9707
9724
  BuildOrAsk,
9708
9725
  {
9709
9726
  mayBuild: true,
@@ -9712,11 +9729,37 @@ function PortalsPanel({ tableId, className }) {
9712
9729
  onAsk: () => host.onAskForOne?.({ kind: "portal", tableId, suggestion: PORTAL_SUGGESTION })
9713
9730
  } : {},
9714
9731
  suggestion: PORTAL_SUGGESTION,
9715
- buildLabel: "Build the portal",
9716
- 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."
9732
+ buildLabel: elsewhere > 0 ? "Build a new portal" : "Build the portal",
9733
+ children: [
9734
+ "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.",
9735
+ elsewhere > 0 ? /* @__PURE__ */ jsxs32(Fragment16, { children: [
9736
+ " ",
9737
+ "This table is not in",
9738
+ " ",
9739
+ elsewhere === 1 ? "the portal this organization already has" : `any of the ${elsewhere} portals this organization already has`,
9740
+ "."
9741
+ ] }) : null
9742
+ ]
9717
9743
  }
9718
9744
  ) : null,
9719
- /* @__PURE__ */ jsx36("ul", { className: "flex flex-col gap-2", children: portals.map((portal) => {
9745
+ shown.length === 0 && !listError && !building && !adding && elsewhere > 0 ? /* @__PURE__ */ jsxs32("div", { className: "flex flex-col gap-1.5 rounded-md border border-dashed border-border p-2.5", children: [
9746
+ /* @__PURE__ */ jsx36("p", { className: "text-xs text-muted-foreground", children: "Or add this table to a portal your clients already sign in to:" }),
9747
+ /* @__PURE__ */ jsx36("div", { className: "flex flex-wrap gap-1.5", children: couldTakeIt.map((portal) => /* @__PURE__ */ jsxs32(
9748
+ Button30,
9749
+ {
9750
+ size: "sm",
9751
+ variant: "outline",
9752
+ onClick: () => setAdding(portal.portal_id),
9753
+ children: [
9754
+ "Add to \u201C",
9755
+ portal.title,
9756
+ "\u201D"
9757
+ ]
9758
+ },
9759
+ portal.portal_id
9760
+ )) })
9761
+ ] }) : null,
9762
+ /* @__PURE__ */ jsx36("ul", { className: "flex flex-col gap-2", children: shown.map((portal) => {
9720
9763
  const url = `${origin}${portalPath(portal.slug)}`;
9721
9764
  const open = openId === portal.portal_id;
9722
9765
  return /* @__PURE__ */ jsxs32("li", { className: "rounded-md border border-border bg-card p-2.5", children: [
@@ -10443,11 +10486,7 @@ function whenItFires(subscription) {
10443
10486
  const every = subscription.cadence === "hourly" ? "an hourly summary" : subscription.cadence === "weekly" ? "a weekly summary" : "a daily summary";
10444
10487
  return subscription.schedule ? `${every}, ${subscription.schedule}` : every;
10445
10488
  }
10446
- var CHANNEL_WORDS2 = {
10447
- in_app: "in the app",
10448
- email: "by email \u2014 only if an address is on the account",
10449
- sms: "by text \u2014 only if a number is on the account"
10450
- };
10489
+ var CHANNEL_WORDS2 = SUBSCRIPTION_CHANNEL_LABEL;
10451
10490
  var DIGEST_SUGGESTION = "Email me a summary of this every Monday at 8 in the morning.";
10452
10491
  function SubscriptionsPanel({ tableId, className }) {
10453
10492
  const client = useRecordsClient27();
@@ -11223,7 +11262,12 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
11223
11262
  Condition,
11224
11263
  {
11225
11264
  question,
11226
- fieldKeys: fields.data ?? [],
11265
+ fieldKeys: (fields.data ?? []).map((f) => ({
11266
+ id: String(f.id),
11267
+ key: f.key,
11268
+ label: f.label || f.key,
11269
+ field: f
11270
+ })),
11227
11271
  onChange: (showIf) => patchQuestion(index, { showIf })
11228
11272
  }
11229
11273
  )
@@ -11961,17 +12005,8 @@ import { useCallback as useCallback26, useEffect as useEffect30, useState as use
11961
12005
  import { useRecordsClient as useRecordsClient32, useTable as useTable16 } from "@ai-matrx/records/react";
11962
12006
  import { Button as Button38, Skeleton as Skeleton23, cn as cn39 } from "@ai-matrx/design-system";
11963
12007
  import { jsx as jsx45, jsxs as jsxs41 } from "react/jsx-runtime";
11964
- var CADENCE_WORDS2 = {
11965
- instant: "as it happens",
11966
- hourly: "hourly summary",
11967
- daily: "daily summary",
11968
- weekly: "weekly summary"
11969
- };
11970
- var CHANNEL_WORDS3 = {
11971
- in_app: "in the app",
11972
- email: "by email",
11973
- sms: "by text"
11974
- };
12008
+ var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
12009
+ var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
11975
12010
  function NotifyRuleEditor({ tableId, seed, className }) {
11976
12011
  const client = useRecordsClient32();
11977
12012
  const host = useRecordsUi();
@@ -13248,12 +13283,7 @@ function bookingSuggestion(tableName2) {
13248
13283
  return `Let clients book a 30-minute ${subject} slot on Tuesday and Thursday afternoons.`;
13249
13284
  }
13250
13285
  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.";
13251
- var STATE_WORDS = {
13252
- draft: "Not open yet \u2014 nobody can book until you open it.",
13253
- open: "Open. Anyone with the link can book a time.",
13254
- closed: "Closed. The link still works and says you are not taking bookings.",
13255
- full: "Every time you offered is taken."
13256
- };
13286
+ var STATE_WORDS = BOOKING_PAGE_STATE_LABEL;
13257
13287
  function BookingSlots({ tableId, className }) {
13258
13288
  const client = useRecordsClient36();
13259
13289
  const host = useRecordsUi();
@@ -13277,10 +13307,21 @@ function BookingSlots({ tableId, className }) {
13277
13307
  void load();
13278
13308
  }, [load]);
13279
13309
  const subjectIds = useMemo30(
13280
- () => Array.from(new Set((pages ?? []).map((p) => p.table_id))),
13281
- [pages]
13310
+ () => Array.from(
13311
+ /* @__PURE__ */ new Set([
13312
+ ...(pages ?? []).map((p) => p.table_id),
13313
+ // THE EMPTY STATE ASKS THE SAME QUESTION. There is no page to read a
13314
+ // table off yet, and the empty state's two buttons need the answer
13315
+ // about the table this rail is standing in — otherwise the rail
13316
+ // offers a viewer both ways into an act the door will refuse.
13317
+ ...tableId ? [tableId] : []
13318
+ ])
13319
+ ),
13320
+ [pages, tableId]
13282
13321
  );
13283
13322
  const levels = useMyLevels2(subjectIds);
13323
+ const levelsKnown = !levels.loading && Boolean(levels.data);
13324
+ const mayBuildHere = levelsKnown && Boolean(tableId) && levels.data?.[tableId] === "admin";
13284
13325
  const toggle = useCallback30(
13285
13326
  async (page) => {
13286
13327
  setBusy(page.form_id);
@@ -13329,7 +13370,8 @@ function BookingSlots({ tableId, className }) {
13329
13370
  pages.length === 0 && !building ? tableId ? /* @__PURE__ */ jsx50(
13330
13371
  BuildOrAsk,
13331
13372
  {
13332
- mayBuild: true,
13373
+ mayBuild: mayBuildHere,
13374
+ whyNot: mayBuildHere ? null : levelsKnown ? NO_ADMIN2 : null,
13333
13375
  onBuild: () => setBuilding(true),
13334
13376
  ...host.onAskForOne ? {
13335
13377
  onAsk: () => host.onAskForOne?.({
@@ -13437,6 +13479,8 @@ import { Button as Button44, Input as Input4, Skeleton as Skeleton29, Textarea a
13437
13479
  // src/CaptureRun.tsx
13438
13480
  import { useCallback as useCallback31, useEffect as useEffect35, useMemo as useMemo31, useRef as useRef13, useState as useState47 } from "react";
13439
13481
  import {
13482
+ coerceTypedAnswer as coerceTypedAnswer2,
13483
+ coercionLabel,
13440
13484
  openCaptureQueue
13441
13485
  } from "@ai-matrx/records";
13442
13486
  import { useRecordsClient as useRecordsClient37 } from "@ai-matrx/records/react";
@@ -13742,7 +13786,11 @@ function CaptureRun({ sheetId, face: given, className }) {
13742
13786
  value: String(answers[q.field] ?? ""),
13743
13787
  onChange: (e) => {
13744
13788
  const raw = e.target.value;
13745
- setAnswers((held) => ({ ...held, [q.field]: raw === "" ? "" : Number(raw) }));
13789
+ const answered2 = field ? coerceTypedAnswer2(field, raw, { label: q.ask ?? coercionLabel(field) }) : { value: raw };
13790
+ setAnswers((held) => ({
13791
+ ...held,
13792
+ [q.field]: raw === "" ? "" : "refusal" in answered2 ? raw : answered2.value
13793
+ }));
13746
13794
  setMissing(null);
13747
13795
  }
13748
13796
  }
@@ -13799,7 +13847,7 @@ function CaptureRun({ sheetId, face: given, className }) {
13799
13847
  // src/CaptureSheet.tsx
13800
13848
  import { Fragment as Fragment25, jsx as jsx52, jsxs as jsxs48 } from "react/jsx-runtime";
13801
13849
  var CAPTURE_MODES = ["reading", "photo", "voice"];
13802
- var IN_MEMORY_QUEUE_REASON = "No capture queue is bound, so anything that cannot be sent right now is held in this page's memory and is lost if the page closes. Bind `captureQueue` on <RecordsUiProvider> with your host's own durable store (IndexedDB in a browser) and a queued capture survives the tab, the reload and the flight. It is said here rather than discovered later.";
13850
+ var IN_MEMORY_QUEUE_REASON = "Anything you capture while offline is held only in this page until it can be sent, and it is lost if you close the tab. Keep this page open until you are back on a connection. It is said here rather than discovered later.";
13803
13851
  function mintClientKey() {
13804
13852
  const c = globalThis.crypto;
13805
13853
  if (typeof c?.randomUUID === "function") return `capture:${c.randomUUID()}`;
@@ -14675,6 +14723,10 @@ import { useCallback as useCallback37, useEffect as useEffect41, useState as use
14675
14723
  import { useRecordsClient as useRecordsClient43, useTable as useTable22 } from "@ai-matrx/records/react";
14676
14724
  import { Button as Button48, Separator as Separator12, Skeleton as Skeleton34, cn as cn51 } from "@ai-matrx/design-system";
14677
14725
  import { Fragment as Fragment28, jsx as jsx58, jsxs as jsxs53 } from "react/jsx-runtime";
14726
+ function cameFromLine(label, groupField) {
14727
+ const grouped = groupField ? `grouped into the same columns the chart used` : `for this table`;
14728
+ return `You came here from \u201C${label}\u201D. This is every record ${grouped} \u2014 not only the ones that number counted \u2014 so the column you clicked is here with the rest beside it.`;
14729
+ }
14678
14730
  var TABLE_NOT_REACHABLE = "This table is not in the organization you are working in, so there is nothing here to show. Switch to the organization that owns it and open it again \u2014 or it may have been deleted.";
14679
14731
  var DEFAULT_VIEW_NAME = "All records";
14680
14732
  var VIEW_NOT_SAVED_YET = "This table has no saved view yet, so the layout you pick here is not being kept. Save one from the bar above and it is remembered.";
@@ -14718,6 +14770,8 @@ function TablePage({
14718
14770
  activeRecordId,
14719
14771
  activeView,
14720
14772
  onViewChanged,
14773
+ activeGroupField,
14774
+ cameFrom,
14721
14775
  className
14722
14776
  }) {
14723
14777
  const client = useRecordsClient43();
@@ -14911,10 +14965,24 @@ function TablePage({
14911
14965
  ] }),
14912
14966
  main === "dashboards" ? /* @__PURE__ */ jsx58(DashboardCanvas, { tableId, activeDashboardId: activeDashboardId ?? null }) : /* @__PURE__ */ jsxs53(Fragment28, { children: [
14913
14967
  opened.unknown ? /* @__PURE__ */ jsx58("p", { className: "rounded-md border border-amber-600/40 bg-amber-500/5 px-3 py-2 text-xs leading-relaxed text-amber-700 dark:border-amber-400/40 dark:text-amber-300", children: unknownViewLine(opened.unknown) }) : null,
14968
+ cameFrom ? /* @__PURE__ */ jsx58(
14969
+ "p",
14970
+ {
14971
+ "data-testid": "came-from-a-number",
14972
+ className: "rounded-md border px-3 py-2 text-xs leading-relaxed text-muted-foreground",
14973
+ children: cameFromLine(cameFrom, activeGroupField ?? null)
14974
+ }
14975
+ ) : null,
14914
14976
  /* @__PURE__ */ jsx58(
14915
14977
  ViewSwitcher,
14916
14978
  {
14917
- view: { ...view ?? defaultView(tableId), layout: shownLayout },
14979
+ view: {
14980
+ ...view ?? defaultView(tableId),
14981
+ layout: shownLayout,
14982
+ // The address's field wins for this visit; silence keeps the
14983
+ // saved view's own choice, exactly as it always did.
14984
+ ...activeGroupField ? { groupField: activeGroupField } : {}
14985
+ },
14918
14986
  pageSize,
14919
14987
  onLayoutChange: (layout) => {
14920
14988
  setLayoutFromLink(layout);
@@ -15016,8 +15084,12 @@ function TablePage({
15016
15084
  ] });
15017
15085
  }
15018
15086
  export {
15087
+ ABSENCE_WORD_LABEL,
15088
+ ABSENCE_WORD_VALUES,
15019
15089
  ACTION_KINDS,
15020
15090
  ActionInbox,
15091
+ BOOKING_PAGE_STATE_LABEL,
15092
+ BOOKING_PAGE_STATE_VALUES,
15021
15093
  BookingBuilder,
15022
15094
  BookingSlots,
15023
15095
  BuildOrAsk,
@@ -15025,7 +15097,10 @@ export {
15025
15097
  CHART_KINDS,
15026
15098
  CHART_KIND_LABEL,
15027
15099
  CHART_NEEDS,
15100
+ CHECKLIST_REQUIREMENT_KINDS,
15101
+ CHECKLIST_REQUIREMENT_LABEL,
15028
15102
  CONDITION_OPS,
15103
+ CONTEXT_POLICY_LABEL,
15029
15104
  CaptureRun,
15030
15105
  CaptureSheet,
15031
15106
  ChartBlock,
@@ -15041,17 +15116,25 @@ export {
15041
15116
  CustomFieldsSection,
15042
15117
  DEFAULT_FIELDS,
15043
15118
  DEFAULT_VIEW_NAME,
15119
+ DUE_STATE_LABEL,
15044
15120
  DashboardCanvas,
15045
15121
  DigestScheduler,
15046
15122
  DocRender,
15047
15123
  DocTemplate,
15048
15124
  EMPTY_PRESENTATION,
15125
+ ENGINEER_TEXT,
15049
15126
  EmbedFrame,
15050
15127
  EnrichBadge,
15051
15128
  EnrichPanel,
15052
15129
  ExportMenu,
15130
+ FIELD_RULE_KIND_LABEL,
15131
+ FIELD_RULE_KIND_VALUES,
15132
+ FIELD_SENSITIVITY_LABEL,
15053
15133
  FIELD_TYPE_CHOICES,
15054
15134
  FIELD_TYPE_GROUPS,
15135
+ FORMULA_OP_JOINS,
15136
+ FORMULA_OP_LABEL,
15137
+ FORMULA_OP_VALUES,
15055
15138
  FORM_FLOWS,
15056
15139
  FROZEN_COLUMN_WIDTH,
15057
15140
  FieldControl,
@@ -15099,6 +15182,8 @@ export {
15099
15182
  PAGE_VIEW_LABEL,
15100
15183
  PARITY_LABEL,
15101
15184
  PARITY_MADE_OF,
15185
+ PROPOSED_CHANGE_ACT_LABEL,
15186
+ PROPOSED_CHANGE_ACT_VALUES,
15102
15187
  Peek,
15103
15188
  PersonPicker,
15104
15189
  PipelineBoard,
@@ -15109,6 +15194,8 @@ export {
15109
15194
  ProposalRow,
15110
15195
  ProvenanceBadge,
15111
15196
  PublicViewPage,
15197
+ ROLLUP_AGG_LABEL,
15198
+ ROLLUP_AGG_VALUES,
15112
15199
  RecordChat,
15113
15200
  RecordChip,
15114
15201
  RecordForm,
@@ -15124,8 +15211,13 @@ export {
15124
15211
  SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS,
15125
15212
  SERIES_COLORS,
15126
15213
  SOMEBODY,
15214
+ STAGE_RULE_ON_FAIL_LABEL,
15127
15215
  STORE_ANSWERS_THESE,
15128
15216
  STORE_DECIDES_REASON,
15217
+ SUBSCRIPTION_CADENCES,
15218
+ SUBSCRIPTION_CADENCE_LABEL,
15219
+ SUBSCRIPTION_CHANNEL_LABEL,
15220
+ SUBSCRIPTION_CHANNEL_VALUES,
15129
15221
  ShareControl,
15130
15222
  SignBlock,
15131
15223
  StageRulesSection,
@@ -15143,6 +15235,9 @@ export {
15143
15235
  ViewBar,
15144
15236
  ViewSwitcher,
15145
15237
  WITHHELD_RECORD_LABEL,
15238
+ WORK_DUE_STATES,
15239
+ WORK_INBOX_KINDS,
15240
+ WORK_INBOX_KIND_LABEL,
15146
15241
  WhoChangedSource,
15147
15242
  actorBadge,
15148
15243
  actorWords,
@@ -15151,6 +15246,7 @@ export {
15151
15246
  blockFromSpec,
15152
15247
  bodyForReading,
15153
15248
  bodyFromKeys,
15249
+ cameFromLine,
15154
15250
  cellState,
15155
15251
  chooseSurface,
15156
15252
  colorFromTheValue,
@@ -15182,6 +15278,7 @@ export {
15182
15278
  hintIsMachineIdentity,
15183
15279
  humanize,
15184
15280
  idsOf,
15281
+ isEngineerText,
15185
15282
  isId,
15186
15283
  isMachineIdentity,
15187
15284
  isPlainFieldType,