@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.js CHANGED
@@ -276,6 +276,12 @@ function looksLikeId(value) {
276
276
  function rowName(row, titleKey, fallback = "Untitled") {
277
277
  return recordName(row?.document, titleKey, fallback);
278
278
  }
279
+ function recordNameIn(table, document2, fallback = "Untitled") {
280
+ return recordName(document2, table?.title_field ?? null, fallback);
281
+ }
282
+ function rowNameIn(table, row, fallback = "Untitled") {
283
+ return recordName(row?.document, table?.title_field ?? null, fallback);
284
+ }
279
285
  var WITHHELD_RECORD_LABEL = "A record you have not been given access to";
280
286
 
281
287
  // src/ReferenceBuilder.tsx
@@ -518,7 +524,11 @@ import {
518
524
  } from "@ai-matrx/design-system";
519
525
 
520
526
  // src/parity.ts
521
- import { KERNEL_TABLES, PARITY_FIELD_TYPES as PARITY_FIELD_TYPES2 } from "@ai-matrx/records";
527
+ import {
528
+ KERNEL_TABLES,
529
+ PARITY_FIELD_TYPES as PARITY_FIELD_TYPES2,
530
+ fieldKindFor
531
+ } from "@ai-matrx/records";
522
532
 
523
533
  // src/fieldTypes.ts
524
534
  import { PARITY_FIELD_TYPES } from "@ai-matrx/records";
@@ -585,50 +595,9 @@ var PARITY_LABEL = {
585
595
  var PARITY_MADE_OF = Object.fromEntries(
586
596
  PARITY_FIELD_TYPES2.map((p) => [p.parity_type, p.made_of])
587
597
  );
598
+ var KERNEL = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };
588
599
  function editorKindFor(field) {
589
- const format = (field.format ?? "").toLowerCase();
590
- const config = field.config ?? {};
591
- switch (field.type) {
592
- // LIMITS-FIX 2026-09-21: the tick box is a BEHAVIOUR of its own, so its editor is
593
- // decided by the behaviour alone — no format, no unit, no target to read. A record
594
- // that never answered holds no key at all, and `CheckboxControl` draws that third
595
- // state rather than pretending it is an unticked box.
596
- case "boolean":
597
- return "checkbox";
598
- case "text":
599
- if (format === "email") return "email";
600
- if (format === "phone") return "phone";
601
- if (format === "url") return "url";
602
- if (format === "json" || config["json"] === true) return "json";
603
- if (format === "long" || config["multiline"] === true) return "long_text";
604
- return "text";
605
- case "range": {
606
- const kind = String(config["kind"] ?? "number");
607
- if (kind === "date" || kind === "datetime") return "datetime";
608
- if (format === "currency") return "currency";
609
- if (format === "percent" || field.unit === "%") return "percent";
610
- return "number";
611
- }
612
- case "list":
613
- return field.multi ? "multi_select" : "select";
614
- case "relation":
615
- return relationEditor(field);
616
- case "formula": {
617
- const via = config["via"];
618
- if (via && config["agg"]) return "rollup";
619
- if (via) return "lookup";
620
- return "formula";
621
- }
622
- default:
623
- return "text";
624
- }
625
- }
626
- function relationEditor(field) {
627
- const parity = String(field.config?.["parity"] ?? "");
628
- if (parity === "attachment" || parity === "member") return parity;
629
- if (field.relation_target === KERNEL_FILE_TABLE) return "attachment";
630
- if (field.relation_target === KERNEL_PERSON_TABLE) return "member";
631
- return "relation";
600
+ return fieldKindFor(field, KERNEL);
632
601
  }
633
602
  function fieldTypeLabel(field) {
634
603
  const kind = editorKindFor(field);
@@ -661,8 +630,7 @@ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
661
630
  function RelationPicker({ field, value, onChange, disabled, id, className }) {
662
631
  const client = useRecordsClient();
663
632
  const [rows, setRows] = useState2(null);
664
- const [titleKey, setTitleKey] = useState2(null);
665
- const [elsewhere, setElsewhere] = useState2({});
633
+ const [words, setWords] = useState2({});
666
634
  const [refusal, setRefusal] = useState2(null);
667
635
  const [search, setSearch] = useState2("");
668
636
  const [open, setOpen] = useState2(false);
@@ -679,47 +647,37 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
679
647
  if (result.ok) setRows(result.data.rows);
680
648
  else setRefusal(refusalLineForAPerson(result.error));
681
649
  });
682
- void client.tableList().then((result) => {
683
- if (cancelled || !result.ok) return;
684
- setTitleKey(result.data.find((table) => table.id === target)?.title_field ?? null);
685
- });
686
650
  return () => {
687
651
  cancelled = true;
688
652
  };
689
653
  }, [client, field.relation_target]);
690
654
  useEffect(() => {
691
655
  if (!rows) return;
692
- const known = new Set(rows.map((row) => row.id));
693
- const missing = picked.filter((id2) => !known.has(id2) && elsewhere[id2] === void 0);
694
- if (missing.length === 0) return;
656
+ const wanted = [.../* @__PURE__ */ new Set([...rows.map((row) => row.id), ...picked])].filter(
657
+ (id2) => words[id2] === void 0
658
+ );
659
+ if (wanted.length === 0) return;
695
660
  let cancelled = false;
696
- void Promise.all(
697
- missing.map(async (id2) => {
698
- const result = await client.recordRead({ record_id: id2 });
699
- return [id2, result.ok ? rowName({ document: result.data.document }, titleKey) : WITHHELD_RECORD_LABEL];
700
- })
701
- ).then((pairs) => {
661
+ void client.relationWordsMany({ field_id: field.id, record_ids: wanted }).then((result) => {
702
662
  if (cancelled) return;
703
- setElsewhere((current) => ({ ...current, ...Object.fromEntries(pairs) }));
663
+ const learned = {};
664
+ for (const id2 of wanted) {
665
+ const said = result.ok ? result.data[id2] : void 0;
666
+ learned[id2] = typeof said === "string" && said !== "" ? said : WITHHELD_RECORD_LABEL;
667
+ }
668
+ setWords((current) => ({ ...current, ...learned }));
704
669
  });
705
670
  return () => {
706
671
  cancelled = true;
707
672
  };
708
- }, [client, rows, picked, elsewhere, titleKey]);
673
+ }, [client, field.id, rows, picked, words]);
709
674
  if (!field.relation_target) {
710
675
  return /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground", children: [
711
676
  fieldName(field),
712
677
  " 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."
713
678
  ] });
714
679
  }
715
- const byId = new Map((rows ?? []).map((row) => [row.id, row]));
716
- const titleOf = (recordId) => {
717
- const row = byId.get(recordId);
718
- if (row) return rowName(row, titleKey, "Untitled");
719
- const away = elsewhere[recordId];
720
- if (away !== void 0) return away;
721
- return "Loading\u2026";
722
- };
680
+ const titleOf = (recordId) => words[recordId] ?? "Loading\u2026";
723
681
  const full = field.multi && field.relation_max !== null && picked.length >= field.relation_max;
724
682
  const toggle = (recordId) => {
725
683
  if (!field.multi) {
@@ -1466,28 +1424,12 @@ function RecordLabelProvider({ children }) {
1466
1424
  stopWaitClock(field.id);
1467
1425
  return;
1468
1426
  }
1469
- const target = field.relation_target;
1470
- if (!target) {
1427
+ if (!field.relation_target) {
1471
1428
  setSettled((current) => ({ ...current, [field.id]: true }));
1472
1429
  stopWaitClock(field.id);
1473
1430
  return;
1474
1431
  }
1475
- const [rows, tables] = await Promise.all([
1476
- client.list({ table_id: target, limit: 200 }),
1477
- client.tableList()
1478
- ]);
1479
- const titleKey = tables.ok ? tables.data.find((table) => table.id === target)?.title_field ?? null : null;
1480
- if (rows.ok) {
1481
- setNames((current) => {
1482
- const next = { ...current };
1483
- for (const row of rows.data.rows) {
1484
- next[row.id] = recordName(row.document, titleKey, "Untitled");
1485
- }
1486
- return next;
1487
- });
1488
- }
1489
1432
  setSettled((current) => ({ ...current, [field.id]: true }));
1490
- stopWaitClock(field.id);
1491
1433
  })();
1492
1434
  },
1493
1435
  [client, startWaitClock, stopWaitClock]
@@ -1496,27 +1438,26 @@ function RecordLabelProvider({ children }) {
1496
1438
  const flushing = useRef2(null);
1497
1439
  const flush = useCallback3(() => {
1498
1440
  flushing.current = null;
1499
- const batch = [...queued.current.entries()];
1441
+ const batch = [...queued.current.values()];
1500
1442
  queued.current.clear();
1501
1443
  if (batch.length === 0) return;
1502
1444
  void (async () => {
1503
1445
  const learned = {};
1504
- const LANES = 6;
1505
- let at = 0;
1506
1446
  await Promise.all(
1507
- Array.from({ length: Math.min(LANES, batch.length) }, async () => {
1508
- for (; ; ) {
1509
- const next = batch[at];
1510
- at += 1;
1511
- if (!next) return;
1512
- const [id] = next;
1513
- const result = await client.recordRead({ record_id: id });
1514
- learned[id] = result.ok ? recordName(result.data.document, null, "Untitled") : WITHHELD_RECORD_LABEL;
1447
+ batch.map(async ({ field, ids }) => {
1448
+ const wanted = [...ids];
1449
+ const result = await client.relationWordsMany({
1450
+ field_id: field.id,
1451
+ record_ids: wanted
1452
+ });
1453
+ for (const id of wanted) {
1454
+ const words = result.ok ? result.data[id] : void 0;
1455
+ learned[id] = typeof words === "string" && words !== "" ? words : WITHHELD_RECORD_LABEL;
1515
1456
  }
1516
1457
  })
1517
1458
  );
1518
1459
  setNames((current) => ({ ...current, ...learned }));
1519
- for (const [, field] of batch) stopWaitClock(field.id);
1460
+ for (const { field } of batch) stopWaitClock(field.id);
1520
1461
  })();
1521
1462
  }, [client, stopWaitClock]);
1522
1463
  const chase = useCallback3(
@@ -1524,7 +1465,9 @@ function RecordLabelProvider({ children }) {
1524
1465
  const key = `${field.id}:${id}`;
1525
1466
  if (chased.current.has(key)) return;
1526
1467
  chased.current.add(key);
1527
- queued.current.set(id, field);
1468
+ const pending = queued.current.get(field.id);
1469
+ if (pending) pending.ids.add(id);
1470
+ else queued.current.set(field.id, { field, ids: /* @__PURE__ */ new Set([id]) });
1528
1471
  startWaitClock(field, null);
1529
1472
  if (flushing.current === null) flushing.current = setTimeout(flush, 0);
1530
1473
  },
@@ -1561,18 +1504,137 @@ function useLabelWait(field) {
1561
1504
  return field ? waiting(field) : null;
1562
1505
  }
1563
1506
 
1564
- // src/values.tsx
1565
- import { Fragment as Fragment3, jsx as jsx9 } from "react/jsx-runtime";
1566
- function envelopeFor(document2, key) {
1567
- const block = document2?._values;
1568
- return block?.[key];
1507
+ // src/enumLabels.ts
1508
+ function exhaustiveValuesOf() {
1509
+ return function(values) {
1510
+ return values;
1511
+ };
1569
1512
  }
1570
- var ABSENCE_WORDS = {
1513
+ var WORK_DUE_STATES = exhaustiveValuesOf()([
1514
+ "overdue",
1515
+ "due_today",
1516
+ "scheduled",
1517
+ "undated",
1518
+ "finished"
1519
+ ]);
1520
+ var DUE_STATE_LABEL = {
1521
+ overdue: "Overdue",
1522
+ due_today: "Today",
1523
+ scheduled: "Scheduled",
1524
+ undated: "No date",
1525
+ finished: "Done"
1526
+ };
1527
+ var WORK_INBOX_KINDS = exhaustiveValuesOf()(["assignment", "approval", "proposal"]);
1528
+ var WORK_INBOX_KIND_LABEL = {
1529
+ assignment: "Yours",
1530
+ approval: "Approve",
1531
+ proposal: "Agent"
1532
+ };
1533
+ var CHECKLIST_REQUIREMENT_KINDS = exhaustiveValuesOf()([
1534
+ "none",
1535
+ "note",
1536
+ "answer",
1537
+ "record_field",
1538
+ "form",
1539
+ "document"
1540
+ ]);
1541
+ var CHECKLIST_REQUIREMENT_LABEL = {
1542
+ none: "",
1543
+ note: "Say what you did",
1544
+ answer: "Fill this in",
1545
+ record_field: "This has to be filled in on the record",
1546
+ form: "This step is a form",
1547
+ document: "This step is a document"
1548
+ };
1549
+ var FIELD_SENSITIVITY_LABEL = {
1550
+ public: "Anyone who can open the record",
1551
+ internal: "People in this organization",
1552
+ confidential: "People who have been given the record",
1553
+ restricted: "Owners and administrators only"
1554
+ };
1555
+ var CONTEXT_POLICY_LABEL = {
1556
+ include: "Yes, an assistant can read it",
1557
+ summarize: "Only a summary of it",
1558
+ on_request: "Only when somebody asks for it",
1559
+ exclude: "No, keep it away from assistants"
1560
+ };
1561
+ var STAGE_RULE_ON_FAIL_LABEL = {
1562
+ refuse: "Turn it away",
1563
+ require_approval: "Ask for approval"
1564
+ };
1565
+ var SUBSCRIPTION_CADENCES = exhaustiveValuesOf()(["instant", "hourly", "daily", "weekly"]);
1566
+ var SUBSCRIPTION_CADENCE_LABEL = {
1567
+ instant: "as it happens",
1568
+ hourly: "hourly summary",
1569
+ daily: "daily summary",
1570
+ weekly: "weekly summary"
1571
+ };
1572
+ var SUBSCRIPTION_CHANNEL_VALUES = ["in_app", "email", "sms"];
1573
+ var SUBSCRIPTION_CHANNEL_LABEL = {
1574
+ in_app: "in the app",
1575
+ email: "by email \u2014 only if an address is on the account",
1576
+ sms: "by text \u2014 only if a number is on the account"
1577
+ };
1578
+ var ROLLUP_AGG_VALUES = ["count", "sum", "avg", "min", "max"];
1579
+ var ROLLUP_AGG_LABEL = {
1580
+ count: "How many there are",
1581
+ sum: "The total of",
1582
+ avg: "The average of",
1583
+ min: "The smallest",
1584
+ max: "The largest"
1585
+ };
1586
+ var FORMULA_OP_VALUES = ["concat", "add", "sub", "mul", "div"];
1587
+ var FORMULA_OP_LABEL = {
1588
+ concat: "Join them together",
1589
+ add: "Add them up",
1590
+ sub: "Take the second from the first",
1591
+ mul: "Multiply them",
1592
+ div: "Divide the first by the second"
1593
+ };
1594
+ var FORMULA_OP_JOINS = {
1595
+ concat: true,
1596
+ add: true,
1597
+ sub: false,
1598
+ mul: true,
1599
+ div: false
1600
+ };
1601
+ var FIELD_RULE_KIND_VALUES = ["min", "max", "length", "pattern", "equals_field", "differs_from_field"];
1602
+ var FIELD_RULE_KIND_LABEL = {
1603
+ min: "At least",
1604
+ max: "At most",
1605
+ length: "No longer than",
1606
+ pattern: "Looks like",
1607
+ equals_field: "Same as the column",
1608
+ differs_from_field: "Different from the column"
1609
+ };
1610
+ var PROPOSED_CHANGE_ACT_VALUES = ["add", "update", "remove"];
1611
+ var PROPOSED_CHANGE_ACT_LABEL = {
1612
+ add: "Add",
1613
+ update: "Update",
1614
+ remove: "Remove"
1615
+ };
1616
+ var BOOKING_PAGE_STATE_VALUES = ["draft", "open", "closed", "full"];
1617
+ var BOOKING_PAGE_STATE_LABEL = {
1618
+ draft: "Not open yet \u2014 nobody can book until you open it.",
1619
+ open: "Open. Anyone with the link can book a time.",
1620
+ closed: "Closed. The link still works and says you are not taking bookings.",
1621
+ full: "Every time you offered is taken."
1622
+ };
1623
+ var ABSENCE_WORD_VALUES = ["never asked", "none", "refused", "conflicting"];
1624
+ var ABSENCE_WORD_LABEL = {
1571
1625
  "never asked": "never asked",
1572
1626
  none: "none",
1573
1627
  refused: "refused",
1574
1628
  conflicting: "conflicting"
1575
1629
  };
1630
+
1631
+ // src/values.tsx
1632
+ import { Fragment as Fragment3, jsx as jsx9 } from "react/jsx-runtime";
1633
+ function envelopeFor(document2, key) {
1634
+ const block = document2?._values;
1635
+ return block?.[key];
1636
+ }
1637
+ var ABSENCE_WORDS = ABSENCE_WORD_LABEL;
1576
1638
  function renderValue(field, value, document2, labels, stillWaitingFor) {
1577
1639
  const envelope = envelopeFor(document2, field.key);
1578
1640
  if (envelope?.absent) {
@@ -3002,7 +3064,7 @@ function confirmLabel(plan, noun, nounPlural) {
3002
3064
 
3003
3065
  // src/pasteValues.ts
3004
3066
  import { predictValueRefusals } from "@ai-matrx/records/core";
3005
- import { optionKey as optionKey2 } from "@ai-matrx/records";
3067
+ import { coerceTypedAnswer, optionKey as optionKey2 } from "@ai-matrx/records";
3006
3068
  function choiceOptionsOf(options) {
3007
3069
  const byWord = /* @__PURE__ */ new Map();
3008
3070
  const labels = [];
@@ -3019,113 +3081,11 @@ function choiceOptionsOf(options) {
3019
3081
  }
3020
3082
  return { byWord, labels };
3021
3083
  }
3022
- var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
3023
- var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
3024
3084
  function valueFromPastedText(field, raw, options) {
3025
- const label = fieldName(field);
3026
- const text = raw.trim();
3027
- const kind = editorKindFor(field);
3028
- if (text === "" && kind !== "checkbox") return { value: null };
3029
- switch (kind) {
3030
- case "checkbox": {
3031
- const word = text.toLowerCase();
3032
- if (TRUE_WORDS.has(word)) return { value: true };
3033
- if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
3034
- return {
3035
- 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.`
3036
- };
3037
- }
3038
- case "number":
3039
- case "currency":
3040
- case "percent": {
3041
- const negative = /^\(.*\)$/.test(text);
3042
- const bare = text.replace(/^\(|\)$/g, "").replace(/[\s,]/g, "").replace(/^[^\d.\-+]+/, "").replace(/[^\d.\-+eE]+$/, "");
3043
- const n = Number(bare);
3044
- if (bare === "" || Number.isNaN(n)) {
3045
- return {
3046
- 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.`
3047
- };
3048
- }
3049
- return { value: negative ? -n : n };
3050
- }
3051
- case "datetime": {
3052
- const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
3053
- const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
3054
- if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
3055
- const at = Date.parse(text);
3056
- if (Number.isNaN(at)) {
3057
- return {
3058
- 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.`
3059
- };
3060
- }
3061
- const parsed = new Date(at);
3062
- if (wantsTime) return { value: parsed.toISOString() };
3063
- const y = parsed.getFullYear();
3064
- const m = String(parsed.getMonth() + 1).padStart(2, "0");
3065
- const d = String(parsed.getDate()).padStart(2, "0");
3066
- return { value: `${y}-${m}-${d}` };
3067
- }
3068
- case "json": {
3069
- try {
3070
- return { value: JSON.parse(text) };
3071
- } catch {
3072
- return {
3073
- refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Paste the whole object, braces and all.`
3074
- };
3075
- }
3076
- }
3077
- // A LIST, A PERSON, A FILE OR ANOTHER RECORD IS AN IDENTITY, NOT A WORD.
3078
- // The store keeps the id of the option or the record, and a word off a
3079
- // clipboard is not that id — writing the word would put a string where an
3080
- // id belongs and every filter, colour and rollup built on that column would
3081
- // stop agreeing with it. So the cell is left alone and the person is told
3082
- // which control does hold it.
3083
- case "select":
3084
- case "multi_select": {
3085
- if (!options) {
3086
- return {
3087
- 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.`
3088
- };
3089
- }
3090
- const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
3091
- if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
3092
- const matched = [];
3093
- const unmatched = [];
3094
- for (const word of wanted) {
3095
- const hit = options.byWord.get(word.toLowerCase());
3096
- if (hit === void 0) unmatched.push(word);
3097
- else matched.push(hit);
3098
- }
3099
- if (unmatched.length > 0) {
3100
- const offered = options.labels.length > 0 ? options.labels.join(", ") : "it has none yet";
3101
- return {
3102
- refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
3103
- unmatched
3104
- };
3105
- }
3106
- return { value: kind === "multi_select" ? matched : matched[0] };
3107
- }
3108
- case "member":
3109
- return {
3110
- refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open the cell and pick them.`
3111
- };
3112
- case "attachment":
3113
- return {
3114
- refusal: `${label} holds a file, which cannot arrive on a clipboard. Open the cell and attach it.`
3115
- };
3116
- case "relation":
3117
- return {
3118
- refusal: `${label} points at another record, so it is chosen rather than typed. Open the cell and pick the record.`
3119
- };
3120
- case "formula":
3121
- case "lookup":
3122
- case "rollup":
3123
- return {
3124
- refusal: `${label} is worked out by the system, so nothing can be pasted into it.`
3125
- };
3126
- default:
3127
- return { value: raw };
3128
- }
3085
+ return coerceTypedAnswer(field, raw, {
3086
+ label: fieldName(field),
3087
+ ...options ? { options } : {}
3088
+ });
3129
3089
  }
3130
3090
  function planPastedBlock(args) {
3131
3091
  const byKey = new Map(args.fields.map((f) => [f.key, f]));
@@ -3670,9 +3630,11 @@ function Grid({
3670
3630
  onClick: () => host.talkToRecord?.({
3671
3631
  tableId,
3672
3632
  recordId: row.id,
3673
- title: recordName(
3674
- row.document ?? {},
3675
- table.data?.title_field
3633
+ // ONE NAMING CALL (`names.ts`), not this file's own join
3634
+ // of a Table's title column to a document.
3635
+ title: recordNameIn(
3636
+ table.data,
3637
+ row.document ?? {}
3676
3638
  )
3677
3639
  }),
3678
3640
  children: "Talk"
@@ -4300,19 +4262,23 @@ import {
4300
4262
  } from "@ai-matrx/records/react";
4301
4263
  import { Button as Button11, Separator as Separator3, cn as cn12 } from "@ai-matrx/design-system";
4302
4264
  import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4303
- var ADDABLE_TYPES = [
4304
- { value: "text", label: "Text" },
4305
- { value: "long_text", label: "Long text" },
4306
- { value: "number", label: "Number" },
4307
- { value: "currency", label: "Money" },
4308
- { value: "percent", label: "Percentage" },
4309
- { value: "datetime", label: "Date" },
4310
- { value: "email", label: "Email" },
4311
- { value: "phone", label: "Phone" },
4312
- { value: "url", label: "Link" },
4313
- { value: "member", label: "Person" },
4314
- { value: "attachment", label: "File" }
4265
+ var ADDABLE_TYPE_IDS = [
4266
+ "text",
4267
+ "long_text",
4268
+ "number",
4269
+ "currency",
4270
+ "percent",
4271
+ "datetime",
4272
+ "email",
4273
+ "phone",
4274
+ "url",
4275
+ "member",
4276
+ "attachment"
4315
4277
  ];
4278
+ var ADDABLE_TYPES = ADDABLE_TYPE_IDS.map((value) => ({
4279
+ value,
4280
+ label: fieldTypeChoice(value)?.label ?? value
4281
+ }));
4316
4282
  function asField(f) {
4317
4283
  return {
4318
4284
  id: f.id,
@@ -4491,41 +4457,20 @@ import {
4491
4457
  Separator as Separator4,
4492
4458
  cn as cn13
4493
4459
  } from "@ai-matrx/design-system";
4460
+ import { FIELD_SENSITIVITIES, CONTEXT_POLICIES } from "@ai-matrx/records";
4494
4461
  import { Fragment as Fragment8, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
4495
- var RULE_KINDS = [
4496
- { kind: "min", label: "At least" },
4497
- { kind: "max", label: "At most" },
4498
- { kind: "length", label: "No longer than" },
4499
- { kind: "pattern", label: "Looks like" },
4500
- { kind: "equals_field", label: "Same as the column" },
4501
- { kind: "differs_from_field", label: "Different from the column" }
4502
- ];
4503
- var SENSITIVITIES = [
4504
- { value: "public", label: "Anyone who can open the record" },
4505
- { value: "internal", label: "People in this organization" },
4506
- { value: "confidential", label: "People who have been given the record" },
4507
- { value: "restricted", label: "Owners and administrators only" }
4508
- ];
4509
- var AGENT_VISIBILITY = [
4510
- { value: "include", label: "Yes, an assistant can read it" },
4511
- { value: "summarize", label: "Only a summary of it" },
4512
- { value: "on_request", label: "Only when somebody asks for it" },
4513
- { value: "exclude", label: "No, keep it away from assistants" }
4514
- ];
4515
- var ROLLUP_OPERATIONS = [
4516
- { value: "count", label: "How many there are" },
4517
- { value: "sum", label: "The total of" },
4518
- { value: "avg", label: "The average of" },
4519
- { value: "min", label: "The smallest" },
4520
- { value: "max", label: "The largest" }
4521
- ];
4522
- var FORMULA_OPERATIONS = [
4523
- { value: "concat", label: "Join them together", joins: true },
4524
- { value: "add", label: "Add them up", joins: true },
4525
- { value: "sub", label: "Take the second from the first", joins: false },
4526
- { value: "mul", label: "Multiply them", joins: true },
4527
- { value: "div", label: "Divide the first by the second", joins: false }
4528
- ];
4462
+ var RULE_KINDS = FIELD_RULE_KIND_VALUES.map((kind) => ({ kind, label: FIELD_RULE_KIND_LABEL[kind] }));
4463
+ var SENSITIVITIES = FIELD_SENSITIVITIES.map((value) => ({ value, label: FIELD_SENSITIVITY_LABEL[value] }));
4464
+ var AGENT_VISIBILITY = CONTEXT_POLICIES.map((value) => ({ value, label: CONTEXT_POLICY_LABEL[value] }));
4465
+ var ROLLUP_OPERATIONS = ROLLUP_AGG_VALUES.map((value) => ({
4466
+ value,
4467
+ label: ROLLUP_AGG_LABEL[value]
4468
+ }));
4469
+ var FORMULA_OPERATIONS = FORMULA_OP_VALUES.map((value) => ({
4470
+ value,
4471
+ label: FORMULA_OP_LABEL[value],
4472
+ joins: FORMULA_OP_JOINS[value]
4473
+ }));
4529
4474
  function keyFor(label) {
4530
4475
  const token = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
4531
4476
  if (token === "") return "";
@@ -5090,6 +5035,9 @@ var CONDITION_OPS = [
5090
5035
  { op: "gt", label: "is more than" },
5091
5036
  { op: "lt", label: "is less than" }
5092
5037
  ];
5038
+ function pointsAtARecord(f) {
5039
+ return !!f?.field && f.field.type === "relation" && !!f.field.relation_target;
5040
+ }
5093
5041
  function conditionIsSimple(expr) {
5094
5042
  if (expr === null || expr === void 0) return true;
5095
5043
  const node = expr;
@@ -5160,6 +5108,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5160
5108
  const op = node?.op ?? "";
5161
5109
  const onField = node?.args?.[0]?.field ?? "";
5162
5110
  const value = node?.args?.[1]?.const ?? "";
5111
+ const chosen = fields.find((f) => f.id === onField);
5163
5112
  function write(next) {
5164
5113
  const chosenOp = next.op ?? op;
5165
5114
  const chosenField = next.field ?? onField;
@@ -5198,7 +5147,24 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5198
5147
  children: CONDITION_OPS.map((c) => /* @__PURE__ */ jsx18("option", { value: c.op, children: c.label }, c.op))
5199
5148
  }
5200
5149
  ),
5201
- op !== "present" ? /* @__PURE__ */ jsx18(
5150
+ op === "present" ? null : pointsAtARecord(chosen) ? (
5151
+ // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
5152
+ // cell uses, on the same `relation_words_many` the cell reads, so a filter and
5153
+ // the column it filters can never disagree about what a record is called. It
5154
+ // is asked for ONE record even when the column holds several: a clause compares
5155
+ // against one value, and a picker that let somebody choose three would be
5156
+ // writing an expression this row cannot draw back.
5157
+ /* @__PURE__ */ jsx18(
5158
+ RelationPicker,
5159
+ {
5160
+ field: { ...chosen.field, multi: false },
5161
+ value: value === "" ? null : value,
5162
+ onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
5163
+ id: "condition-value-record",
5164
+ className: "min-w-[8rem]"
5165
+ }
5166
+ )
5167
+ ) : /* @__PURE__ */ jsx18(
5202
5168
  BasicInput6,
5203
5169
  {
5204
5170
  className: "h-8 w-28 text-xs",
@@ -5206,7 +5172,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5206
5172
  value: String(value ?? ""),
5207
5173
  onChange: (e) => write({ value: conditionValue(e.target.value) })
5208
5174
  }
5209
- ) : null
5175
+ )
5210
5176
  ] }) : null
5211
5177
  ] });
5212
5178
  }
@@ -5366,10 +5332,10 @@ var NO_RULES_YET = "Nothing has to be true to get here yet.";
5366
5332
  var NEEDS_A_SENTENCE = "Write the sentence somebody reads when this stops them, the way you would say it out loud.";
5367
5333
  var NOT_DRAWN_HERE = "This part of the rule is not something this builder can draw yet";
5368
5334
  var ON_FAIL = [
5369
- { value: "refuse", label: "Turn it away", note: "The card stays where it is and the person is told why." },
5335
+ { value: "refuse", label: STAGE_RULE_ON_FAIL_LABEL.refuse, note: "The card stays where it is and the person is told why." },
5370
5336
  {
5371
5337
  value: "require_approval",
5372
- label: "Ask for approval",
5338
+ label: STAGE_RULE_ON_FAIL_LABEL.require_approval,
5373
5339
  note: "The card stays where it is, and the people who can approve it are asked. Nobody is turned away."
5374
5340
  }
5375
5341
  ];
@@ -5420,7 +5386,7 @@ function StageRulesSection({ tableId, stage, className }) {
5420
5386
  const stageLabel = stages.find((s) => s.key === stageKey)?.label ?? stageKey ?? "";
5421
5387
  const gates = useMemo13(() => stageKey ? gatesOf(pipeline, stageKey) : [], [pipeline, stageKey]);
5422
5388
  const conditionFields = useMemo13(
5423
- () => (fields.data ?? []).map((f) => ({ id: String(f.id), key: f.key, label: fieldName(f) })),
5389
+ () => (fields.data ?? []).map((f) => ({ id: String(f.id), key: f.key, label: fieldName(f), field: f })),
5424
5390
  [fields.data]
5425
5391
  );
5426
5392
  useEffect8(() => {
@@ -6361,7 +6327,7 @@ function Peek({ tableId, recordId, onClose, className }) {
6361
6327
  if (record.error) return /* @__PURE__ */ jsx24(RefusalNotice, { error: record.error, className });
6362
6328
  if (fields.error) return /* @__PURE__ */ jsx24(RefusalNotice, { error: fields.error, className });
6363
6329
  const document2 = record.data?.document;
6364
- const title = document2 ? recordName(document2, table.data?.title_field) : "";
6330
+ const title = document2 ? recordNameIn(table.data, document2) : "";
6365
6331
  const href = host.hrefForRecord && table.data ? host.hrefForRecord({ table: table.data, recordId }) : void 0;
6366
6332
  return /* @__PURE__ */ jsxs20("div", { className: cn18("flex min-w-0 flex-col gap-2 text-xs", className), children: [
6367
6333
  /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
@@ -7121,7 +7087,7 @@ function offerableFields(all, want) {
7121
7087
  return all.filter((field) => {
7122
7088
  const kind = editorKindFor(field);
7123
7089
  if (want === "choice") return kind === "select" || kind === "multi_select";
7124
- return kind === "datetime" || kind === "date";
7090
+ return kind === "datetime";
7125
7091
  });
7126
7092
  }
7127
7093
  function useStageField(tableId) {
@@ -7539,11 +7505,7 @@ import { useState as useState26 } from "react";
7539
7505
  import { useRecordsClient as useRecordsClient18 } from "@ai-matrx/records/react";
7540
7506
  import { Badge as Badge7, Button as Button22, cn as cn22 } from "@ai-matrx/design-system";
7541
7507
  import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
7542
- var ACT_WORD = {
7543
- add: "Add",
7544
- update: "Update",
7545
- remove: "Remove"
7546
- };
7508
+ var ACT_WORD = PROPOSED_CHANGE_ACT_LABEL;
7547
7509
  function ProposalRow({
7548
7510
  change,
7549
7511
  outcome,
@@ -7650,21 +7612,8 @@ import { useCallback as useCallback12, useEffect as useEffect15, useMemo as useM
7650
7612
  import { useRecordsClient as useRecordsClient19, useTable as useTable9 } from "@ai-matrx/records/react";
7651
7613
  import { Badge as Badge8, BasicInput as BasicInput8, BasicTextarea as BasicTextarea4, Button as Button23, Skeleton as Skeleton9, cn as cn23 } from "@ai-matrx/design-system";
7652
7614
  import { Fragment as Fragment13, jsx as jsx29, jsxs as jsxs25 } from "react/jsx-runtime";
7653
- var DUE_WORD = {
7654
- overdue: "Overdue",
7655
- due_today: "Today",
7656
- scheduled: "Scheduled",
7657
- undated: "No date",
7658
- finished: "Done"
7659
- };
7660
- var REQUIRES_WORD = {
7661
- none: "",
7662
- note: "Say what you did",
7663
- answer: "Fill this in",
7664
- record_field: "This has to be filled in on the record",
7665
- form: "This step is a form",
7666
- document: "This step is a document"
7667
- };
7615
+ var DUE_WORD = DUE_STATE_LABEL;
7616
+ var REQUIRES_WORD = CHECKLIST_REQUIREMENT_LABEL;
7668
7617
  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.";
7669
7618
  function ChecklistRunner({
7670
7619
  tableId,
@@ -8372,18 +8321,8 @@ function roleKey(said) {
8372
8321
  // src/ActionInbox.tsx
8373
8322
  import { jsx as jsx30, jsxs as jsxs26 } from "react/jsx-runtime";
8374
8323
  var ACTION_KINDS = ["approval", "assignment", "proposal"];
8375
- var KIND_WORD = {
8376
- assignment: "Yours",
8377
- approval: "Approve",
8378
- proposal: "Agent"
8379
- };
8380
- var DUE_WORD2 = {
8381
- overdue: "Overdue",
8382
- due_today: "Today",
8383
- scheduled: "Scheduled",
8384
- undated: "No date",
8385
- finished: "Finished"
8386
- };
8324
+ var KIND_WORD = WORK_INBOX_KIND_LABEL;
8325
+ var DUE_WORD2 = DUE_STATE_LABEL;
8387
8326
  function ActionInbox({ tableId, includeSettled = false, onOpenRecord, className }) {
8388
8327
  const client = useRecordsClient20();
8389
8328
  const [items, setItems] = useState28(null);
@@ -9635,18 +9574,18 @@ function BuildOrAsk({
9635
9574
  buildLabel,
9636
9575
  className
9637
9576
  }) {
9577
+ const mayAsk = mayBuild && Boolean(onAsk);
9638
9578
  return /* @__PURE__ */ jsxs31("div", { className: cn29("rounded-md border border-dashed p-3", className), children: [
9639
9579
  /* @__PURE__ */ jsx35("p", { className: "text-xs text-muted-foreground", children }),
9640
- /* @__PURE__ */ jsxs31("div", { className: "mt-2.5 flex flex-wrap items-center gap-2", children: [
9641
- onAsk ? /* @__PURE__ */ jsx35(Button29, { size: "sm", variant: "outline", onClick: onAsk, children: "Ask an agent" }) : null,
9642
- mayBuild ? /* @__PURE__ */ jsx35(Button29, { size: "sm", onClick: onBuild, children: buildLabel }) : null
9643
- ] }),
9644
- onAsk ? /* @__PURE__ */ jsxs31("p", { className: "mt-2 text-xs text-muted-foreground", children: [
9580
+ mayBuild ? /* @__PURE__ */ jsxs31("div", { className: "mt-2.5 flex flex-wrap items-center gap-2", children: [
9581
+ mayAsk ? /* @__PURE__ */ jsx35(Button29, { size: "sm", variant: "outline", onClick: onAsk, children: "Ask an agent" }) : null,
9582
+ /* @__PURE__ */ jsx35(Button29, { size: "sm", onClick: onBuild, children: buildLabel })
9583
+ ] }) : null,
9584
+ !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: [
9645
9585
  "You would say something like: \u201C",
9646
9586
  suggestion,
9647
9587
  "\u201D"
9648
- ] }) : /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: NO_AGENT_PORT }),
9649
- !mayBuild && whyNot ? /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: whyNot }) : null
9588
+ ] }) : /* @__PURE__ */ jsx35("p", { className: "mt-2 text-xs text-muted-foreground", children: NO_AGENT_PORT })
9650
9589
  ] });
9651
9590
  }
9652
9591
 
@@ -9685,30 +9624,41 @@ function PortalsPanel({ tableId, className }) {
9685
9624
  const client = useRecordsClient25();
9686
9625
  const host = useRecordsUi();
9687
9626
  const [portals, setPortals] = useState33(null);
9627
+ const [exposures, setExposures] = useState33([]);
9688
9628
  const [listError, setListError] = useState33(null);
9689
9629
  const [openId, setOpenId] = useState33(null);
9690
9630
  const [building, setBuilding] = useState33(false);
9631
+ const [adding, setAdding] = useState33(null);
9691
9632
  const load = useCallback18(async () => {
9692
- const answered = await client.portals();
9633
+ const [answered, mapped] = await Promise.all([client.portals(), client.portalTables()]);
9693
9634
  if (!answered.ok) {
9694
9635
  setListError(answered.error);
9695
9636
  setPortals([]);
9637
+ setExposures([]);
9696
9638
  return;
9697
9639
  }
9698
9640
  setListError(null);
9699
9641
  setPortals(answered.data);
9642
+ setExposures(mapped.ok ? mapped.data : []);
9700
9643
  }, [client]);
9701
9644
  useEffect21(() => {
9702
9645
  void load();
9703
9646
  }, [load]);
9704
9647
  if (portals === null) return /* @__PURE__ */ jsx36(Skeleton15, { className: cn30("h-32 w-full", className) });
9648
+ const onATable = Boolean(tableId);
9649
+ const holdingThis = new Set(
9650
+ exposures.filter((e) => e.table_id === tableId).map((e) => e.portal_id)
9651
+ );
9652
+ const shown = onATable ? portals.filter((p) => holdingThis.has(p.portal_id)) : portals;
9653
+ const couldTakeIt = onATable ? portals.filter((p) => !holdingThis.has(p.portal_id)) : [];
9654
+ const elsewhere = couldTakeIt.length;
9705
9655
  const origin = host.publicOrigin ?? (typeof window === "undefined" ? "" : window.location.origin);
9706
9656
  return /* @__PURE__ */ jsxs32("section", { className: cn30("flex flex-col gap-3", className), children: [
9707
9657
  /* @__PURE__ */ jsxs32("header", { className: "flex items-center gap-2", children: [
9708
9658
  /* @__PURE__ */ jsx36("h3", { className: "text-sm font-medium", children: "Portals" }),
9709
- /* @__PURE__ */ jsx36("span", { className: "text-xs text-muted-foreground", children: portals.length === 0 ? "none yet" : `${portals.length}` }),
9659
+ /* @__PURE__ */ jsx36("span", { className: "text-xs text-muted-foreground", children: shown.length === 0 ? "none for this table" : `${shown.length}` }),
9710
9660
  /* @__PURE__ */ jsx36("div", { className: "flex-1" }),
9711
- portals.length > 0 ? /* @__PURE__ */ jsx36(Button30, { size: "sm", variant: building ? "secondary" : "ghost", onClick: () => setBuilding((b) => !b), children: building ? "Done building" : "Build a portal" }) : null
9661
+ shown.length > 0 ? /* @__PURE__ */ jsx36(Button30, { size: "sm", variant: building ? "secondary" : "ghost", onClick: () => setBuilding((b) => !b), children: building ? "Done building" : "Build a portal" }) : null
9712
9662
  ] }),
9713
9663
  listError ? /* @__PURE__ */ jsx36(RefusalNotice, { error: listError }) : null,
9714
9664
  building ? /* @__PURE__ */ jsx36(
@@ -9721,7 +9671,19 @@ function PortalsPanel({ tableId, className }) {
9721
9671
  onClose: () => setBuilding(false)
9722
9672
  }
9723
9673
  ) : null,
9724
- portals.length === 0 && !listError && !building ? /* @__PURE__ */ jsx36(
9674
+ adding ? /* @__PURE__ */ jsx36(
9675
+ PortalBuilder,
9676
+ {
9677
+ ...tableId ? { tableId } : {},
9678
+ portalId: adding,
9679
+ onSaved: () => {
9680
+ setAdding(null);
9681
+ void load();
9682
+ },
9683
+ onClose: () => setAdding(null)
9684
+ }
9685
+ ) : null,
9686
+ shown.length === 0 && !listError && !building && !adding ? /* @__PURE__ */ jsxs32(
9725
9687
  BuildOrAsk,
9726
9688
  {
9727
9689
  mayBuild: true,
@@ -9730,11 +9692,37 @@ function PortalsPanel({ tableId, className }) {
9730
9692
  onAsk: () => host.onAskForOne?.({ kind: "portal", tableId, suggestion: PORTAL_SUGGESTION })
9731
9693
  } : {},
9732
9694
  suggestion: PORTAL_SUGGESTION,
9733
- buildLabel: "Build the portal",
9734
- 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."
9695
+ buildLabel: elsewhere > 0 ? "Build a new portal" : "Build the portal",
9696
+ children: [
9697
+ "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.",
9698
+ elsewhere > 0 ? /* @__PURE__ */ jsxs32(Fragment16, { children: [
9699
+ " ",
9700
+ "This table is not in",
9701
+ " ",
9702
+ elsewhere === 1 ? "the portal this organization already has" : `any of the ${elsewhere} portals this organization already has`,
9703
+ "."
9704
+ ] }) : null
9705
+ ]
9735
9706
  }
9736
9707
  ) : null,
9737
- /* @__PURE__ */ jsx36("ul", { className: "flex flex-col gap-2", children: portals.map((portal) => {
9708
+ 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: [
9709
+ /* @__PURE__ */ jsx36("p", { className: "text-xs text-muted-foreground", children: "Or add this table to a portal your clients already sign in to:" }),
9710
+ /* @__PURE__ */ jsx36("div", { className: "flex flex-wrap gap-1.5", children: couldTakeIt.map((portal) => /* @__PURE__ */ jsxs32(
9711
+ Button30,
9712
+ {
9713
+ size: "sm",
9714
+ variant: "outline",
9715
+ onClick: () => setAdding(portal.portal_id),
9716
+ children: [
9717
+ "Add to \u201C",
9718
+ portal.title,
9719
+ "\u201D"
9720
+ ]
9721
+ },
9722
+ portal.portal_id
9723
+ )) })
9724
+ ] }) : null,
9725
+ /* @__PURE__ */ jsx36("ul", { className: "flex flex-col gap-2", children: shown.map((portal) => {
9738
9726
  const url = `${origin}${portalPath(portal.slug)}`;
9739
9727
  const open = openId === portal.portal_id;
9740
9728
  return /* @__PURE__ */ jsxs32("li", { className: "rounded-md border border-border bg-card p-2.5", children: [
@@ -10049,7 +10037,7 @@ function Preview({
10049
10037
  function Invite({ card, onInvited }) {
10050
10038
  const client = useRecordsClient25();
10051
10039
  const [rows, setRows] = useState33(null);
10052
- const [titleKey, setTitleKey] = useState33(null);
10040
+ const [clientTable, setClientTable] = useState33(null);
10053
10041
  const [search, setSearch] = useState33("");
10054
10042
  const [picked, setPicked] = useState33(null);
10055
10043
  const [email, setEmail] = useState33("");
@@ -10068,17 +10056,17 @@ function Invite({ card, onInvited }) {
10068
10056
  });
10069
10057
  void client.tableList().then((answered) => {
10070
10058
  if (cancelled || !answered.ok) return;
10071
- setTitleKey(answered.data.find((t) => t.id === card.client_table_id)?.title_field ?? null);
10059
+ setClientTable(answered.data.find((t) => t.id === card.client_table_id) ?? null);
10072
10060
  });
10073
10061
  return () => {
10074
10062
  cancelled = true;
10075
10063
  };
10076
10064
  }, [client, card.client_table_id]);
10077
10065
  const options = useMemo23(() => {
10078
- const all = (rows ?? []).map((row) => ({ id: row.id, name: rowName(row, titleKey) }));
10066
+ const all = (rows ?? []).map((row) => ({ id: row.id, name: rowNameIn(clientTable, row) }));
10079
10067
  const needle = search.trim().toLowerCase();
10080
10068
  return needle === "" ? all.slice(0, 25) : all.filter((o) => o.name.toLowerCase().includes(needle)).slice(0, 25);
10081
- }, [rows, titleKey, search]);
10069
+ }, [rows, clientTable, search]);
10082
10070
  const send = useCallback18(async () => {
10083
10071
  if (!picked) return;
10084
10072
  setBusy(true);
@@ -10461,11 +10449,7 @@ function whenItFires(subscription) {
10461
10449
  const every = subscription.cadence === "hourly" ? "an hourly summary" : subscription.cadence === "weekly" ? "a weekly summary" : "a daily summary";
10462
10450
  return subscription.schedule ? `${every}, ${subscription.schedule}` : every;
10463
10451
  }
10464
- var CHANNEL_WORDS2 = {
10465
- in_app: "in the app",
10466
- email: "by email \u2014 only if an address is on the account",
10467
- sms: "by text \u2014 only if a number is on the account"
10468
- };
10452
+ var CHANNEL_WORDS2 = SUBSCRIPTION_CHANNEL_LABEL;
10469
10453
  var DIGEST_SUGGESTION = "Email me a summary of this every Monday at 8 in the morning.";
10470
10454
  function SubscriptionsPanel({ tableId, className }) {
10471
10455
  const client = useRecordsClient27();
@@ -11241,7 +11225,12 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
11241
11225
  Condition,
11242
11226
  {
11243
11227
  question,
11244
- fieldKeys: fields.data ?? [],
11228
+ fieldKeys: (fields.data ?? []).map((f) => ({
11229
+ id: String(f.id),
11230
+ key: f.key,
11231
+ label: f.label || f.key,
11232
+ field: f
11233
+ })),
11245
11234
  onChange: (showIf) => patchQuestion(index, { showIf })
11246
11235
  }
11247
11236
  )
@@ -11979,17 +11968,8 @@ import { useCallback as useCallback26, useEffect as useEffect30, useState as use
11979
11968
  import { useRecordsClient as useRecordsClient32, useTable as useTable16 } from "@ai-matrx/records/react";
11980
11969
  import { Button as Button38, Skeleton as Skeleton23, cn as cn39 } from "@ai-matrx/design-system";
11981
11970
  import { jsx as jsx45, jsxs as jsxs41 } from "react/jsx-runtime";
11982
- var CADENCE_WORDS2 = {
11983
- instant: "as it happens",
11984
- hourly: "hourly summary",
11985
- daily: "daily summary",
11986
- weekly: "weekly summary"
11987
- };
11988
- var CHANNEL_WORDS3 = {
11989
- in_app: "in the app",
11990
- email: "by email",
11991
- sms: "by text"
11992
- };
11971
+ var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
11972
+ var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
11993
11973
  function NotifyRuleEditor({ tableId, seed, className }) {
11994
11974
  const client = useRecordsClient32();
11995
11975
  const host = useRecordsUi();
@@ -13266,12 +13246,7 @@ function bookingSuggestion(tableName2) {
13266
13246
  return `Let clients book a 30-minute ${subject} slot on Tuesday and Thursday afternoons.`;
13267
13247
  }
13268
13248
  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.";
13269
- var STATE_WORDS = {
13270
- draft: "Not open yet \u2014 nobody can book until you open it.",
13271
- open: "Open. Anyone with the link can book a time.",
13272
- closed: "Closed. The link still works and says you are not taking bookings.",
13273
- full: "Every time you offered is taken."
13274
- };
13249
+ var STATE_WORDS = BOOKING_PAGE_STATE_LABEL;
13275
13250
  function BookingSlots({ tableId, className }) {
13276
13251
  const client = useRecordsClient36();
13277
13252
  const host = useRecordsUi();
@@ -13295,10 +13270,21 @@ function BookingSlots({ tableId, className }) {
13295
13270
  void load();
13296
13271
  }, [load]);
13297
13272
  const subjectIds = useMemo30(
13298
- () => Array.from(new Set((pages ?? []).map((p) => p.table_id))),
13299
- [pages]
13273
+ () => Array.from(
13274
+ /* @__PURE__ */ new Set([
13275
+ ...(pages ?? []).map((p) => p.table_id),
13276
+ // THE EMPTY STATE ASKS THE SAME QUESTION. There is no page to read a
13277
+ // table off yet, and the empty state's two buttons need the answer
13278
+ // about the table this rail is standing in — otherwise the rail
13279
+ // offers a viewer both ways into an act the door will refuse.
13280
+ ...tableId ? [tableId] : []
13281
+ ])
13282
+ ),
13283
+ [pages, tableId]
13300
13284
  );
13301
13285
  const levels = useMyLevels2(subjectIds);
13286
+ const levelsKnown = !levels.loading && Boolean(levels.data);
13287
+ const mayBuildHere = levelsKnown && Boolean(tableId) && levels.data?.[tableId] === "admin";
13302
13288
  const toggle = useCallback30(
13303
13289
  async (page) => {
13304
13290
  setBusy(page.form_id);
@@ -13347,7 +13333,8 @@ function BookingSlots({ tableId, className }) {
13347
13333
  pages.length === 0 && !building ? tableId ? /* @__PURE__ */ jsx50(
13348
13334
  BuildOrAsk,
13349
13335
  {
13350
- mayBuild: true,
13336
+ mayBuild: mayBuildHere,
13337
+ whyNot: mayBuildHere ? null : levelsKnown ? NO_ADMIN2 : null,
13351
13338
  onBuild: () => setBuilding(true),
13352
13339
  ...host.onAskForOne ? {
13353
13340
  onAsk: () => host.onAskForOne?.({
@@ -13455,6 +13442,8 @@ import { Button as Button44, Input as Input4, Skeleton as Skeleton29, Textarea a
13455
13442
  // src/CaptureRun.tsx
13456
13443
  import { useCallback as useCallback31, useEffect as useEffect35, useMemo as useMemo31, useRef as useRef13, useState as useState47 } from "react";
13457
13444
  import {
13445
+ coerceTypedAnswer as coerceTypedAnswer2,
13446
+ coercionLabel,
13458
13447
  openCaptureQueue
13459
13448
  } from "@ai-matrx/records";
13460
13449
  import { useRecordsClient as useRecordsClient37 } from "@ai-matrx/records/react";
@@ -13760,7 +13749,11 @@ function CaptureRun({ sheetId, face: given, className }) {
13760
13749
  value: String(answers[q.field] ?? ""),
13761
13750
  onChange: (e) => {
13762
13751
  const raw = e.target.value;
13763
- setAnswers((held) => ({ ...held, [q.field]: raw === "" ? "" : Number(raw) }));
13752
+ const answered2 = field ? coerceTypedAnswer2(field, raw, { label: q.ask ?? coercionLabel(field) }) : { value: raw };
13753
+ setAnswers((held) => ({
13754
+ ...held,
13755
+ [q.field]: raw === "" ? "" : "refusal" in answered2 ? raw : answered2.value
13756
+ }));
13764
13757
  setMissing(null);
13765
13758
  }
13766
13759
  }
@@ -15034,8 +15027,12 @@ function TablePage({
15034
15027
  ] });
15035
15028
  }
15036
15029
  export {
15030
+ ABSENCE_WORD_LABEL,
15031
+ ABSENCE_WORD_VALUES,
15037
15032
  ACTION_KINDS,
15038
15033
  ActionInbox,
15034
+ BOOKING_PAGE_STATE_LABEL,
15035
+ BOOKING_PAGE_STATE_VALUES,
15039
15036
  BookingBuilder,
15040
15037
  BookingSlots,
15041
15038
  BuildOrAsk,
@@ -15043,7 +15040,10 @@ export {
15043
15040
  CHART_KINDS,
15044
15041
  CHART_KIND_LABEL,
15045
15042
  CHART_NEEDS,
15043
+ CHECKLIST_REQUIREMENT_KINDS,
15044
+ CHECKLIST_REQUIREMENT_LABEL,
15046
15045
  CONDITION_OPS,
15046
+ CONTEXT_POLICY_LABEL,
15047
15047
  CaptureRun,
15048
15048
  CaptureSheet,
15049
15049
  ChartBlock,
@@ -15059,6 +15059,7 @@ export {
15059
15059
  CustomFieldsSection,
15060
15060
  DEFAULT_FIELDS,
15061
15061
  DEFAULT_VIEW_NAME,
15062
+ DUE_STATE_LABEL,
15062
15063
  DashboardCanvas,
15063
15064
  DigestScheduler,
15064
15065
  DocRender,
@@ -15068,8 +15069,14 @@ export {
15068
15069
  EnrichBadge,
15069
15070
  EnrichPanel,
15070
15071
  ExportMenu,
15072
+ FIELD_RULE_KIND_LABEL,
15073
+ FIELD_RULE_KIND_VALUES,
15074
+ FIELD_SENSITIVITY_LABEL,
15071
15075
  FIELD_TYPE_CHOICES,
15072
15076
  FIELD_TYPE_GROUPS,
15077
+ FORMULA_OP_JOINS,
15078
+ FORMULA_OP_LABEL,
15079
+ FORMULA_OP_VALUES,
15073
15080
  FORM_FLOWS,
15074
15081
  FROZEN_COLUMN_WIDTH,
15075
15082
  FieldControl,
@@ -15117,6 +15124,8 @@ export {
15117
15124
  PAGE_VIEW_LABEL,
15118
15125
  PARITY_LABEL,
15119
15126
  PARITY_MADE_OF,
15127
+ PROPOSED_CHANGE_ACT_LABEL,
15128
+ PROPOSED_CHANGE_ACT_VALUES,
15120
15129
  Peek,
15121
15130
  PersonPicker,
15122
15131
  PipelineBoard,
@@ -15127,6 +15136,8 @@ export {
15127
15136
  ProposalRow,
15128
15137
  ProvenanceBadge,
15129
15138
  PublicViewPage,
15139
+ ROLLUP_AGG_LABEL,
15140
+ ROLLUP_AGG_VALUES,
15130
15141
  RecordChat,
15131
15142
  RecordChip,
15132
15143
  RecordForm,
@@ -15142,8 +15153,13 @@ export {
15142
15153
  SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS,
15143
15154
  SERIES_COLORS,
15144
15155
  SOMEBODY,
15156
+ STAGE_RULE_ON_FAIL_LABEL,
15145
15157
  STORE_ANSWERS_THESE,
15146
15158
  STORE_DECIDES_REASON,
15159
+ SUBSCRIPTION_CADENCES,
15160
+ SUBSCRIPTION_CADENCE_LABEL,
15161
+ SUBSCRIPTION_CHANNEL_LABEL,
15162
+ SUBSCRIPTION_CHANNEL_VALUES,
15147
15163
  ShareControl,
15148
15164
  SignBlock,
15149
15165
  StageRulesSection,
@@ -15161,6 +15177,9 @@ export {
15161
15177
  ViewBar,
15162
15178
  ViewSwitcher,
15163
15179
  WITHHELD_RECORD_LABEL,
15180
+ WORK_DUE_STATES,
15181
+ WORK_INBOX_KINDS,
15182
+ WORK_INBOX_KIND_LABEL,
15164
15183
  WhoChangedSource,
15165
15184
  actorBadge,
15166
15185
  actorWords,
@@ -15226,12 +15245,14 @@ export {
15226
15245
  previewWords,
15227
15246
  publiclyAnswerable,
15228
15247
  recordName,
15248
+ recordNameIn,
15229
15249
  recordsDataSource,
15230
15250
  refusalForAPerson,
15231
15251
  refusalLineForAPerson,
15232
15252
  renderValue,
15233
15253
  revokeConsequence,
15234
15254
  rowName,
15255
+ rowNameIn,
15235
15256
  scalarText,
15236
15257
  shareUnavailableReason,
15237
15258
  specFromBlock,