@ai-matrx/records-ui 0.57.0 → 0.58.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/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.58.0
4
+
5
+ **A word pasted into a choice column becomes that option — and a paste is no
6
+ longer capped by the page it is looking at.**
7
+
8
+ **Pasting "Gold Annual" into a choice column.** Until now every word pasted into
9
+ a `select` or `multi_select` was refused outright: correct about a raw string,
10
+ and useless to Rincon Plumbing's office manager whose Service plan column says
11
+ "Gold Annual" forty times. Airtable, the champion here, matches the word to the
12
+ option labelled that word, and so does this.
13
+
14
+ * The grid now READS each choice column's options before the preview draws, so
15
+ the preview is an answer rather than a "loading" a person would have to
16
+ confirm blind.
17
+ * Matching ignores case and also accepts the option's key or id, so a round trip
18
+ through a spreadsheet comes back in. A multi-choice cell splits on commas and
19
+ semicolons and is written WHOLE or not at all — half a multi-choice answer is
20
+ a wrong answer, not a partial one.
21
+ * What is stored is the option RECORD'S ID (FLD-5/FLD-6), which is what the
22
+ store's own validator accepts — writing the slug would have made the preview
23
+ promise a write the door then refused.
24
+ * An unmatched word is REFUSED, naming the line, the column and the options that
25
+ ARE on offer, and **never silently created**: a typo promoted to a lifecycle
26
+ stage is a column nobody chose. The preview offers them back as ONE explicit
27
+ "Add these N options" action, pressed by a person; it sends the existing labels
28
+ back with the new ones (the door REPLACES the list) and re-plans, so the rows
29
+ land without pasting again.
30
+
31
+ **A 120-line paste against a 50-row page.** The page ceiling was never a paste
32
+ ceiling, and nothing exercised past it.
33
+
34
+ * The creates now chunk through `record_write_many` at the grid's own page size —
35
+ the number a person is looking at is the number that travels. A refused chunk is
36
+ refused whole, so every line in it is NAMED ("Rows 51-100: …"); updates stay one
37
+ at a time because they carry a per-row optimistic version and no bulk door can
38
+ honour it.
39
+ * The read-back PAGES. A single `limit: 500` read silently stopped counting at row
40
+ 500, so a table that had grown past it reported values "missing" that were
41
+ sitting in it — a read-back that lies is worse than none.
42
+
43
+ **Consumer action:** none. `PastePreview` gains an optional `onAddOptions`; a host
44
+ that does not pass it simply does not offer the action.
45
+
3
46
  ## 0.57.0
4
47
 
5
48
  **The builders are usable where a person actually reaches them: a 384px rail.**
package/dist/index.cjs CHANGED
@@ -2731,9 +2731,18 @@ function EnrichPanel({ tableId, fieldId, className }) {
2731
2731
  var import_react17 = require("react");
2732
2732
  var import_design_system8 = require("@ai-matrx/design-system");
2733
2733
  var import_jsx_runtime11 = require("react/jsx-runtime");
2734
- function PastePreview({ plan, noun, nounPlural, onConfirm, onCancel }) {
2734
+ function PastePreview({
2735
+ plan,
2736
+ noun,
2737
+ nounPlural,
2738
+ onConfirm,
2739
+ onCancel,
2740
+ onAddOptions
2741
+ }) {
2735
2742
  const [busy, setBusy] = (0, import_react17.useState)(false);
2736
2743
  const [outcome, setOutcome] = (0, import_react17.useState)(null);
2744
+ const [adding, setAdding] = (0, import_react17.useState)(null);
2745
+ const [addFailed, setAddFailed] = (0, import_react17.useState)(null);
2737
2746
  const nothingToDo = plan.cells === 0;
2738
2747
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2739
2748
  "div",
@@ -2772,6 +2781,37 @@ function PastePreview({ plan, noun, nounPlural, onConfirm, onCancel }) {
2772
2781
  " more of the same kinds."
2773
2782
  ] }) : null
2774
2783
  ] }) : null,
2784
+ onAddOptions && plan.unmatchedOptions.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "data-matrx-paste-add-options": true, className: "flex flex-col gap-2", children: [
2785
+ plan.unmatchedOptions.map((group) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
2786
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "text-xs", children: [
2787
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "font-medium", children: group.column }),
2788
+ " does not offer",
2789
+ " ",
2790
+ group.words.map((w) => `\u201C${w}\u201D`).join(", "),
2791
+ "."
2792
+ ] }),
2793
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2794
+ import_design_system8.Button,
2795
+ {
2796
+ type: "button",
2797
+ size: "sm",
2798
+ variant: "outline",
2799
+ disabled: adding !== null || busy,
2800
+ onClick: () => {
2801
+ setAddFailed(null);
2802
+ setAdding(group.fieldKey);
2803
+ void onAddOptions(group.fieldKey, group.words).catch(
2804
+ (err) => setAddFailed(
2805
+ `Those options were not added: ${err instanceof Error ? err.message : String(err)}`
2806
+ )
2807
+ ).finally(() => setAdding(null));
2808
+ },
2809
+ children: adding === group.fieldKey ? "Adding\u2026" : group.words.length === 1 ? `Add \u201C${group.words[0]}\u201D as an option` : `Add these ${group.words.length} options`
2810
+ }
2811
+ )
2812
+ ] }, group.fieldKey)),
2813
+ addFailed ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-destructive", children: addFailed }) : null
2814
+ ] }) : null,
2775
2815
  plan.pastRightEdge > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
2776
2816
  plan.pastRightEdge,
2777
2817
  " ",
@@ -2863,9 +2903,26 @@ function confirmLabel(plan, noun, nounPlural) {
2863
2903
 
2864
2904
  // src/pasteValues.ts
2865
2905
  var import_core2 = require("@ai-matrx/records/core");
2906
+ var import_records5 = require("@ai-matrx/records");
2907
+ function choiceOptionsOf(options) {
2908
+ const byWord = /* @__PURE__ */ new Map();
2909
+ const labels = [];
2910
+ for (const option of options) {
2911
+ const id = option.id;
2912
+ const key = (0, import_records5.optionKey)(option);
2913
+ const label = recordName(option.data, null, "");
2914
+ if (label) {
2915
+ labels.push(label);
2916
+ if (!byWord.has(label.toLowerCase())) byWord.set(label.toLowerCase(), id);
2917
+ }
2918
+ if (key && !byWord.has(String(key).toLowerCase())) byWord.set(String(key).toLowerCase(), id);
2919
+ if (id && !byWord.has(id.toLowerCase())) byWord.set(id.toLowerCase(), id);
2920
+ }
2921
+ return { byWord, labels };
2922
+ }
2866
2923
  var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
2867
2924
  var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
2868
- function valueFromPastedText(field, raw) {
2925
+ function valueFromPastedText(field, raw, options) {
2869
2926
  const label = fieldName(field);
2870
2927
  const text = raw.trim();
2871
2928
  const kind = editorKindFor(field);
@@ -2925,10 +2982,30 @@ function valueFromPastedText(field, raw) {
2925
2982
  // stop agreeing with it. So the cell is left alone and the person is told
2926
2983
  // which control does hold it.
2927
2984
  case "select":
2928
- case "multi_select":
2929
- return {
2930
- refusal: `${label} is a list, and a pasted word is not one of its options. Open the cell and pick the option \u2014 or add \u201C${raw}\u201D as an option first, and paste again.`
2931
- };
2985
+ case "multi_select": {
2986
+ if (!options) {
2987
+ return {
2988
+ 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.`
2989
+ };
2990
+ }
2991
+ const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
2992
+ if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
2993
+ const matched = [];
2994
+ const unmatched = [];
2995
+ for (const word of wanted) {
2996
+ const hit = options.byWord.get(word.toLowerCase());
2997
+ if (hit === void 0) unmatched.push(word);
2998
+ else matched.push(hit);
2999
+ }
3000
+ if (unmatched.length > 0) {
3001
+ const offered = options.labels.length > 0 ? options.labels.join(", ") : "it has none yet";
3002
+ return {
3003
+ refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
3004
+ unmatched
3005
+ };
3006
+ }
3007
+ return { value: kind === "multi_select" ? matched : matched[0] };
3008
+ }
2932
3009
  case "member":
2933
3010
  return {
2934
3011
  refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open the cell and pick them.`
@@ -2955,6 +3032,7 @@ function planPastedBlock(args) {
2955
3032
  const byKey = new Map(args.fields.map((f) => [f.key, f]));
2956
3033
  const rows = [];
2957
3034
  const refusals = [];
3035
+ const unmatched = /* @__PURE__ */ new Map();
2958
3036
  let cells = 0;
2959
3037
  const lines = [
2960
3038
  ...args.onExisting.map((r) => ({ recordId: r.recordId, raw: r.raw })),
@@ -2970,8 +3048,15 @@ function planPastedBlock(args) {
2970
3048
  const field = byKey.get(key);
2971
3049
  const raw = line.raw[c];
2972
3050
  if (field === void 0 || raw === void 0) continue;
2973
- const judged = valueFromPastedText(field, raw);
3051
+ const judged = valueFromPastedText(field, raw, args.options?.get(key));
2974
3052
  if ("refusal" in judged) {
3053
+ if ("unmatched" in judged) {
3054
+ const seen = unmatched.get(key) ?? { column: fieldName(field), words: [] };
3055
+ for (const word of judged.unmatched) {
3056
+ if (!seen.words.some((w) => w.toLowerCase() === word.toLowerCase())) seen.words.push(word);
3057
+ }
3058
+ unmatched.set(key, seen);
3059
+ }
2975
3060
  built.push({ key, raw, value: null, refusal: judged.refusal });
2976
3061
  refusals.push({ fromLine, column: fieldName(field), raw, why: judged.refusal });
2977
3062
  continue;
@@ -3007,7 +3092,12 @@ function planPastedBlock(args) {
3007
3092
  creates: keepers.filter((r) => r.recordId === null).length,
3008
3093
  cells,
3009
3094
  refusals,
3010
- pastRightEdge: args.pastRightEdge ?? 0
3095
+ pastRightEdge: args.pastRightEdge ?? 0,
3096
+ unmatchedOptions: [...unmatched.entries()].map(([fieldKey, seen]) => ({
3097
+ fieldKey,
3098
+ column: seen.column,
3099
+ words: seen.words
3100
+ }))
3011
3101
  };
3012
3102
  }
3013
3103
  function documentOf(row) {
@@ -3089,13 +3179,29 @@ function Grid({
3089
3179
  [client, host, refreshEnrich]
3090
3180
  );
3091
3181
  const [pastePlan, setPastePlan] = (0, import_react18.useState)(null);
3182
+ const choiceOptions = (0, import_react18.useRef)(/* @__PURE__ */ new Map());
3183
+ const loadChoiceOptions = (0, import_react18.useCallback)(
3184
+ async (fieldList) => {
3185
+ const wanted = fieldList.filter(
3186
+ (f) => (editorKindFor(f) === "select" || editorKindFor(f) === "multi_select") && !choiceOptions.current.has(f.key)
3187
+ );
3188
+ await Promise.all(
3189
+ wanted.map(async (field) => {
3190
+ const got = await client.fieldOptions({ field_id: field.id });
3191
+ if (got.ok) choiceOptions.current.set(field.key, choiceOptionsOf(got.data ?? []));
3192
+ })
3193
+ );
3194
+ },
3195
+ [client]
3196
+ );
3092
3197
  const pasteableKeys = (0, import_react18.useMemo)(
3093
3198
  () => ordered.filter((f) => fieldIsEditable(f)).map((f) => f.key),
3094
3199
  [ordered]
3095
3200
  );
3096
3201
  const receivePastePlan = (0, import_react18.useCallback)(
3097
- (sheetPlan) => {
3202
+ async (sheetPlan) => {
3098
3203
  const fieldList = fields.data ?? [];
3204
+ await loadChoiceOptions(fieldList);
3099
3205
  const byRecord = /* @__PURE__ */ new Map();
3100
3206
  const order = [];
3101
3207
  const take = (rowId, columnId, value) => {
@@ -3123,6 +3229,7 @@ function Grid({
3123
3229
  );
3124
3230
  const built = planPastedBlock({
3125
3231
  fields: fieldList,
3232
+ options: choiceOptions.current,
3126
3233
  columnKeys,
3127
3234
  onExisting: touched.map((recordId) => ({
3128
3235
  recordId,
@@ -3146,7 +3253,37 @@ function Grid({
3146
3253
  }
3147
3254
  setPastePlan({ plan: built });
3148
3255
  },
3149
- [fields.data, rows]
3256
+ [fields.data, loadChoiceOptions, rows]
3257
+ );
3258
+ const addPastedOptions = (0, import_react18.useCallback)(
3259
+ async (fieldKey, words) => {
3260
+ const field = (fields.data ?? []).find((f) => f.key === fieldKey);
3261
+ if (!field) throw new Error(`This table has no column called ${fieldKey}.`);
3262
+ const existing = choiceOptions.current.get(fieldKey)?.labels ?? [];
3263
+ const written = await client.fieldUpdate({
3264
+ field_id: field.id,
3265
+ patch: { options: [...existing, ...words] }
3266
+ });
3267
+ if (!written.ok) throw new Error(written.error.message);
3268
+ choiceOptions.current.delete(fieldKey);
3269
+ await loadChoiceOptions(fields.data ?? []);
3270
+ setPastePlan(
3271
+ (held) => held ? {
3272
+ plan: planPastedBlock({
3273
+ fields: fields.data ?? [],
3274
+ options: choiceOptions.current,
3275
+ columnKeys: held.plan.rows[0]?.cells.map((c) => c.key) ?? [],
3276
+ onExisting: held.plan.rows.filter((r) => r.recordId !== null).map((r) => ({
3277
+ recordId: r.recordId,
3278
+ raw: r.cells.map((c) => c.raw)
3279
+ })),
3280
+ onNew: held.plan.rows.filter((r) => r.recordId === null).map((r) => r.cells.map((c) => c.raw)),
3281
+ pastRightEdge: held.plan.pastRightEdge
3282
+ })
3283
+ } : held
3284
+ );
3285
+ },
3286
+ [client, fields.data, loadChoiceOptions]
3150
3287
  );
3151
3288
  const applyPaste = (0, import_react18.useCallback)(async () => {
3152
3289
  const held = pastePlan;
@@ -3154,39 +3291,56 @@ function Grid({
3154
3291
  const refusals = [];
3155
3292
  let written = 0;
3156
3293
  const expected = [];
3157
- for (const row of held.plan.rows) {
3158
- const document2 = documentOf(row);
3159
- if (Object.keys(document2).length === 0) continue;
3160
- if (row.recordId === null) {
3161
- const made = await client.recordWrite({ table_id: tableId, data: document2 });
3162
- if (!made.ok) {
3163
- refusals.push(`Row ${row.fromLine}: ${made.error.message}`);
3164
- continue;
3165
- }
3166
- written += 1;
3167
- expected.push({ recordId: made.data, document: document2 });
3168
- } else {
3169
- const done = await client.recordUpdate({ record_id: row.recordId, patch: document2 });
3170
- if (!done.ok) {
3171
- refusals.push(`Row ${row.fromLine}: ${done.error.message}`);
3172
- continue;
3294
+ const creates = held.plan.rows.filter((row) => row.recordId === null).map((row) => ({ row, document: documentOf(row) })).filter((c) => Object.keys(c.document).length > 0);
3295
+ const updates = held.plan.rows.filter((row) => row.recordId !== null).map((row) => ({ row, document: documentOf(row) })).filter((c) => Object.keys(c.document).length > 0);
3296
+ for (let at = 0; at < creates.length; at += pageSize) {
3297
+ const chunk = creates.slice(at, at + pageSize);
3298
+ const made = await client.recordWriteMany({
3299
+ table_id: tableId,
3300
+ rows: chunk.map((c) => c.document)
3301
+ });
3302
+ if (!made.ok) {
3303
+ refusals.push(
3304
+ `Rows ${chunk[0].row.fromLine}-${chunk[chunk.length - 1].row.fromLine}: ${made.error.message}`
3305
+ );
3306
+ continue;
3307
+ }
3308
+ chunk.forEach((c, index) => {
3309
+ const id = made.data[index];
3310
+ if (id === void 0) {
3311
+ refusals.push(`Row ${c.row.fromLine}: the store wrote the batch and named no id for this line.`);
3312
+ return;
3173
3313
  }
3174
3314
  written += 1;
3175
- expected.push({ recordId: row.recordId, document: document2 });
3315
+ expected.push({ recordId: id, document: c.document });
3316
+ });
3317
+ }
3318
+ for (const { row, document: document2 } of updates) {
3319
+ const done = await client.recordUpdate({ record_id: row.recordId, patch: document2 });
3320
+ if (!done.ok) {
3321
+ refusals.push(`Row ${row.fromLine}: ${done.error.message}`);
3322
+ continue;
3176
3323
  }
3324
+ written += 1;
3325
+ expected.push({ recordId: row.recordId, document: document2 });
3177
3326
  }
3178
3327
  let readBack = 0;
3179
3328
  let missing = 0;
3180
3329
  const seen = /* @__PURE__ */ new Map();
3181
- const reread = await client.list({ table_id: tableId, limit: 500, offset: 0 });
3182
- if (reread.ok) {
3330
+ const READ_BACK_PAGE = 500;
3331
+ for (let offset = 0; ; offset += READ_BACK_PAGE) {
3332
+ const reread = await client.list({ table_id: tableId, limit: READ_BACK_PAGE, offset });
3333
+ if (!reread.ok) {
3334
+ refusals.push(
3335
+ `The rows were written and this could not read them back to check: ${reread.error.message}`
3336
+ );
3337
+ break;
3338
+ }
3183
3339
  for (const row of reread.data.rows) {
3184
3340
  seen.set(row.id, row.document ?? {});
3185
3341
  }
3186
- } else {
3187
- refusals.push(
3188
- `The rows were written and this could not read them back to check: ${reread.error.message}`
3189
- );
3342
+ if (reread.data.rows.length < READ_BACK_PAGE) break;
3343
+ if (expected.every((e) => seen.has(e.recordId))) break;
3190
3344
  }
3191
3345
  for (const entry of expected) {
3192
3346
  const stored = seen.get(entry.recordId);
@@ -3459,6 +3613,7 @@ function Grid({
3459
3613
  noun: table.data?.label_singular ?? "record",
3460
3614
  nounPlural: table.data?.label_plural ?? `${table.data?.label_singular ?? "record"}s`,
3461
3615
  onConfirm: applyPaste,
3616
+ onAddOptions: addPastedOptions,
3462
3617
  onCancel: () => setPastePlan(null)
3463
3618
  }
3464
3619
  ) : null,
@@ -9133,7 +9288,7 @@ function PortalBuilder({ tableId, portalId, onSaved, onClose, className }) {
9133
9288
  // src/PortalsPanel.tsx
9134
9289
  var import_react65 = require("react");
9135
9290
  var import_react66 = require("@ai-matrx/records/react");
9136
- var import_records5 = require("@ai-matrx/records");
9291
+ var import_records6 = require("@ai-matrx/records");
9137
9292
  var import_design_system32 = require("@ai-matrx/design-system");
9138
9293
 
9139
9294
  // src/BuildOrAsk.tsx
@@ -9250,7 +9405,7 @@ function PortalsPanel({ tableId, className }) {
9250
9405
  }
9251
9406
  ) : null,
9252
9407
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "flex flex-col gap-2", children: portals.map((portal) => {
9253
- const url = `${origin}${(0, import_records5.portalPath)(portal.slug)}`;
9408
+ const url = `${origin}${(0, import_records6.portalPath)(portal.slug)}`;
9254
9409
  const open = openId === portal.portal_id;
9255
9410
  return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "rounded-md border border-border bg-card p-2.5", children: [
9256
9411
  /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-center gap-2", children: [
@@ -10154,7 +10309,7 @@ function SubscriptionsPanel({ tableId, className }) {
10154
10309
  // src/FormBuilder.tsx
10155
10310
  var import_react73 = require("react");
10156
10311
  var import_react74 = require("@ai-matrx/records/react");
10157
- var import_records6 = require("@ai-matrx/records");
10312
+ var import_records7 = require("@ai-matrx/records");
10158
10313
  var import_design_system36 = require("@ai-matrx/design-system");
10159
10314
 
10160
10315
  // src/FormRunner.tsx
@@ -10607,7 +10762,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
10607
10762
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10608
10763
  PublishRow,
10609
10764
  {
10610
- url: `${publicOrigin}${(0, import_records6.publicFormPath)(draft.id)}`,
10765
+ url: `${publicOrigin}${(0, import_records7.publicFormPath)(draft.id)}`,
10611
10766
  state: draft.state,
10612
10767
  mayPublish: rights.structure,
10613
10768
  why: rights.why("structure"),
@@ -11672,7 +11827,7 @@ function NotifyRuleEditor({ tableId, seed, className }) {
11672
11827
  // src/ChartBlock.tsx
11673
11828
  var import_react85 = require("react");
11674
11829
  var import_recharts = require("recharts");
11675
- var import_records7 = require("@ai-matrx/records");
11830
+ var import_records8 = require("@ai-matrx/records");
11676
11831
  var import_core6 = require("@ai-matrx/records/core");
11677
11832
  var import_design_system42 = require("@ai-matrx/design-system");
11678
11833
  var import_jsx_runtime45 = require("react/jsx-runtime");
@@ -11693,7 +11848,7 @@ function ChartBlock({ block, subject, className }) {
11693
11848
  const kind = block.kind;
11694
11849
  const tableId = block.table_id ?? subject;
11695
11850
  const measures = block.measures && block.measures.length > 0 ? block.measures : [{ op: "count" }];
11696
- const primary = (0, import_records7.measureKey)(measures[0]);
11851
+ const primary = (0, import_records8.measureKey)(measures[0]);
11697
11852
  const series = [primary];
11698
11853
  const points = (0, import_react85.useMemo)(() => {
11699
11854
  if (kind === "stuck") return [];
@@ -12227,7 +12382,7 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
12227
12382
  // src/FormsPanel.tsx
12228
12383
  var import_react88 = require("react");
12229
12384
  var import_react89 = require("@ai-matrx/records/react");
12230
- var import_records8 = require("@ai-matrx/records");
12385
+ var import_records9 = require("@ai-matrx/records");
12231
12386
  var import_design_system44 = require("@ai-matrx/design-system");
12232
12387
  var import_jsx_runtime47 = require("react/jsx-runtime");
12233
12388
  var WHAT_A_FORM_IS = "A form asks for this table's own fields, and its answers land here as ordinary records stamped with the form they came through.";
@@ -12322,7 +12477,7 @@ function FormsPanel({ tableId, className }) {
12322
12477
  }
12323
12478
  ) : null,
12324
12479
  /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ul", { className: "flex flex-col gap-2", children: forms.map((form) => {
12325
- const url = `${origin}${(0, import_records8.publicFormPath)(form.form_id)}`;
12480
+ const url = `${origin}${(0, import_records9.publicFormPath)(form.form_id)}`;
12326
12481
  return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("li", { className: "rounded-md border p-2.5", children: [
12327
12482
  /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex items-center gap-2", children: [
12328
12483
  /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", children: form.title ?? form.slug }),
@@ -12374,7 +12529,7 @@ function FormsPanel({ tableId, className }) {
12374
12529
  // src/BookingBuilder.tsx
12375
12530
  var import_react90 = require("react");
12376
12531
  var import_react91 = require("@ai-matrx/records/react");
12377
- var import_records9 = require("@ai-matrx/records");
12532
+ var import_records10 = require("@ai-matrx/records");
12378
12533
  var import_design_system45 = require("@ai-matrx/design-system");
12379
12534
  var import_jsx_runtime48 = require("react/jsx-runtime");
12380
12535
  var STORE_ANSWERS_THESE = ["slot", "status", "booked_with"];
@@ -12505,7 +12660,7 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
12505
12660
  if (!rights.structure) {
12506
12661
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: (0, import_design_system45.cn)("text-xs text-muted-foreground", className), children: rights.why("structure") ?? "Making a booking page needs the admin level on this table, because it lets people with no account take time in your calendar." });
12507
12662
  }
12508
- const url = formId ? `${publicOrigin}${(0, import_records9.bookingPath)(formId)}` : null;
12663
+ const url = formId ? `${publicOrigin}${(0, import_records10.bookingPath)(formId)}` : null;
12509
12664
  const shown = offer ?? null;
12510
12665
  return (
12511
12666
  // THE RAIL IS 384px WIDE AND THE VIEWPORT IS NOT (walk 1, 2026-09-21):
@@ -12706,7 +12861,7 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
12706
12861
  // src/BookingSlots.tsx
12707
12862
  var import_react92 = require("react");
12708
12863
  var import_react93 = require("@ai-matrx/records/react");
12709
- var import_records10 = require("@ai-matrx/records");
12864
+ var import_records11 = require("@ai-matrx/records");
12710
12865
  var import_design_system46 = require("@ai-matrx/design-system");
12711
12866
  var import_jsx_runtime49 = require("react/jsx-runtime");
12712
12867
  var WHAT_A_BOOKING_PAGE_IS = "A booking page offers times you are free and writes each appointment into this table as an ordinary record.";
@@ -12814,7 +12969,7 @@ function BookingSlots({ tableId, className }) {
12814
12969
  " Open the table the appointments should land in to make one."
12815
12970
  ] }) : null,
12816
12971
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("ul", { className: "flex flex-col gap-2", children: pages.map((page) => {
12817
- const url = `${origin}${(0, import_records10.bookingPath)(page.form_id)}`;
12972
+ const url = `${origin}${(0, import_records11.bookingPath)(page.form_id)}`;
12818
12973
  const mayOpen = levels.data?.[page.table_id] === "admin";
12819
12974
  const open = page.published_at !== null && page.closed_at === null;
12820
12975
  return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { className: "rounded-md border p-2.5", "data-testid": "booking-page", children: [
@@ -12903,7 +13058,7 @@ var import_design_system48 = require("@ai-matrx/design-system");
12903
13058
 
12904
13059
  // src/CaptureRun.tsx
12905
13060
  var import_react94 = require("react");
12906
- var import_records11 = require("@ai-matrx/records");
13061
+ var import_records12 = require("@ai-matrx/records");
12907
13062
  var import_react95 = require("@ai-matrx/records/react");
12908
13063
  var import_design_system47 = require("@ai-matrx/design-system");
12909
13064
  var import_jsx_runtime50 = require("react/jsx-runtime");
@@ -12958,7 +13113,7 @@ function CaptureRun({ sheetId, face: given, className }) {
12958
13113
  const [sending, setSending] = (0, import_react94.useState)(false);
12959
13114
  const queueRef = (0, import_react94.useRef)(null);
12960
13115
  (0, import_react94.useEffect)(() => {
12961
- const q2 = (0, import_records11.openCaptureQueue)({
13116
+ const q2 = (0, import_records12.openCaptureQueue)({
12962
13117
  onChange: (c, all) => {
12963
13118
  setCounts(c);
12964
13119
  setItems(all);