@ai-matrx/records-ui 0.105.1 → 0.107.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
@@ -5637,7 +5637,8 @@ function formatControl(args) {
5637
5637
  {
5638
5638
  id,
5639
5639
  type: def.editor,
5640
- ...ownDetails && def.editor !== "url" ? { autoComplete: def.editor === "email" ? "email" : "tel" } : {},
5640
+ inputMode: def.editor,
5641
+ ...ownDetails ? { autoComplete: def.editor } : {},
5641
5642
  value: text2,
5642
5643
  disabled,
5643
5644
  onChange: (e) => commit(e.target.value)
@@ -6128,7 +6129,7 @@ function isPersonsName(field2) {
6128
6129
  const words2 = `${field2.key} ${field2.label ?? ""}`.toLowerCase();
6129
6130
  return /(^|[\s_])(full[\s_]?)?name(\s|$)/.test(words2) && !/(therapist|doctor|physician|company|business|pet|room)/.test(words2);
6130
6131
  }
6131
- function FieldControl({ field: field2, value, onChange, disabled, id, autoOpen, onClose, onCommit, onCancel, addsChoices, initialSearch, asForm }) {
6132
+ function FieldControl({ field: field2, value, onChange, disabled, id, autoOpen, onClose, onCommit, onCancel, addsChoices, initialSearch, asForm, onChosen }) {
6132
6133
  const host = useRecordsUi();
6133
6134
  const kind = editorKindFor(field2);
6134
6135
  const inputId = id ?? `field-${field2.key}`;
@@ -6192,7 +6193,8 @@ function FieldControl({ field: field2, value, onChange, disabled, id, autoOpen,
6192
6193
  {
6193
6194
  id: inputId,
6194
6195
  type: kind === "email" ? "email" : kind === "phone" ? "tel" : "url",
6195
- ...ownDetails && kind !== "url" ? { autoComplete: kind === "email" ? "email" : "tel" } : {},
6196
+ inputMode: kind === "email" ? "email" : kind === "phone" ? "tel" : "url",
6197
+ ...ownDetails ? { autoComplete: kind === "email" ? "email" : kind === "phone" ? "tel" : "url" } : {},
6196
6198
  value: value === null || value === void 0 ? "" : String(value),
6197
6199
  disabled,
6198
6200
  onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
@@ -6202,7 +6204,7 @@ function FieldControl({ field: field2, value, onChange, disabled, id, autoOpen,
6202
6204
  return /* @__PURE__ */ jsx23(CheckboxControl, { field: field2, value, onChange, disabled, id: inputId });
6203
6205
  case "select":
6204
6206
  case "multi_select":
6205
- return /* @__PURE__ */ jsx23(OptionControl, { field: field2, value, onChange, disabled, id: inputId, autoOpen, onClose, onCancel, addsChoices, initialSearch, asForm });
6207
+ return /* @__PURE__ */ jsx23(OptionControl, { field: field2, value, onChange, disabled, id: inputId, autoOpen, onClose, onCancel, addsChoices, initialSearch, asForm, onChosen });
6206
6208
  case "member":
6207
6209
  return /* @__PURE__ */ jsx23(PersonPicker, { field: field2, value, onChange, disabled, id: inputId, autoOpen, onClose, onCancel, initialSearch });
6208
6210
  case "entity_reference":
@@ -6384,7 +6386,7 @@ function inTheStoresOrder(options) {
6384
6386
  const made = (o) => String(o.created_at ?? "");
6385
6387
  return [...options].sort((a, b) => position(a) - position(b) || made(a).localeCompare(made(b)));
6386
6388
  }
6387
- function OptionControl({ field: field2, value, onChange, disabled, id, autoOpen, onClose, onCancel, addsChoices, initialSearch, asForm }) {
6389
+ function OptionControl({ field: field2, value, onChange, disabled, id, autoOpen, onClose, onCancel, addsChoices, initialSearch, asForm, onChosen }) {
6388
6390
  const client = useOptionalRecordsClient7();
6389
6391
  const [options, setOptions] = useState20(null);
6390
6392
  const [refusal4, setRefusal] = useState20(null);
@@ -6403,7 +6405,7 @@ function OptionControl({ field: field2, value, onChange, disabled, id, autoOpen,
6403
6405
  }, [client, field2.id, reread]);
6404
6406
  const handed = publicChoicesOf(field2);
6405
6407
  if (!client && handed.length > 0) {
6406
- return /* @__PURE__ */ jsx23(ChoiceAnswer, { field: field2, choices: handed, value, onChange, disabled, id });
6408
+ return /* @__PURE__ */ jsx23(ChoiceAnswer, { field: field2, choices: handed, value, onChange, disabled, id, onChosen });
6407
6409
  }
6408
6410
  if (!client) {
6409
6411
  return /* @__PURE__ */ jsxs21("div", { className: "flex flex-col gap-1", children: [
@@ -6433,6 +6435,7 @@ function OptionControl({ field: field2, value, onChange, disabled, id, autoOpen,
6433
6435
  onChange,
6434
6436
  disabled,
6435
6437
  id,
6438
+ onChosen,
6436
6439
  isChosen: (choice, v) => {
6437
6440
  const option = options.find((o) => optionKey2(o) === choice.key);
6438
6441
  return option ? isTheChosen(option, v) : false;
@@ -6703,20 +6706,24 @@ function ChoiceAnswer({
6703
6706
  onChange,
6704
6707
  disabled,
6705
6708
  id,
6706
- isChosen
6709
+ isChosen,
6710
+ onChosen
6707
6711
  }) {
6708
6712
  const [search, setSearch] = useState20("");
6709
6713
  const held = field2.multi ? choiceValuesOf(value) : value === null || value === void 0 || value === "" ? [] : [value];
6710
6714
  const on = (choice) => held.some((v) => isChosen ? isChosen(choice, v) : String(v) === choice.key || String(v) === choice.label);
6711
6715
  const press = (choice) => {
6712
6716
  if (!field2.multi) {
6713
- onChange(on(choice) ? null : choice.key);
6717
+ const picking = !on(choice);
6718
+ onChange(picking ? choice.key : null);
6719
+ if (picking) onChosen?.();
6714
6720
  return;
6715
6721
  }
6716
6722
  const keys = choices.filter((c) => c.key === choice.key ? !on(c) : on(c)).map((c) => c.key);
6717
6723
  onChange(keys.length === 0 ? null : keys);
6718
6724
  };
6719
6725
  const lettered = choices.length <= CHOICE_LETTERS.length;
6726
+ const tabStop = choices.find((c) => on(c))?.key ?? choices[0]?.key;
6720
6727
  const option = (choice) => {
6721
6728
  const chosen = on(choice);
6722
6729
  const letter = lettered ? CHOICE_LETTERS[choices.indexOf(choice)] : null;
@@ -6728,6 +6735,7 @@ function ChoiceAnswer({
6728
6735
  "aria-checked": chosen,
6729
6736
  "aria-label": choice.label,
6730
6737
  disabled,
6738
+ tabIndex: field2.multi || choice.key === tabStop ? 0 : -1,
6731
6739
  onClick: () => press(choice),
6732
6740
  "data-records-choice-answer": choice.key,
6733
6741
  className: cn16(
@@ -6754,8 +6762,25 @@ function ChoiceAnswer({
6754
6762
  );
6755
6763
  };
6756
6764
  const onKeyDown = (event) => {
6757
- if (!lettered || disabled || event.metaKey || event.ctrlKey || event.altKey) return;
6765
+ if (disabled || event.metaKey || event.ctrlKey || event.altKey) return;
6758
6766
  if (event.target.tagName === "INPUT") return;
6767
+ const arrow = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
6768
+ if (arrow) {
6769
+ const buttons = Array.from(
6770
+ event.currentTarget.querySelectorAll("[data-records-choice-answer]")
6771
+ );
6772
+ const here = buttons.indexOf(event.target);
6773
+ if (buttons.length === 0) return;
6774
+ event.preventDefault();
6775
+ const next = buttons[(Math.max(here, 0) + arrow + buttons.length) % buttons.length];
6776
+ next.focus();
6777
+ if (!field2.multi) {
6778
+ const choice = choices.find((c) => c.key === next.dataset["recordsChoiceAnswer"]);
6779
+ if (choice && !on(choice)) onChange(choice.key);
6780
+ }
6781
+ return;
6782
+ }
6783
+ if (!lettered) return;
6759
6784
  const at = CHOICE_LETTERS.indexOf(event.key.toUpperCase());
6760
6785
  if (event.key.length !== 1 || at < 0 || at >= choices.length) return;
6761
6786
  event.preventDefault();
@@ -9833,7 +9858,7 @@ function ChoiceValue({ field: field2, value, onChange }) {
9833
9858
  {
9834
9859
  triggerSize: "compact",
9835
9860
  triggerLabel: `Choice for ${field2.label || field2.key}`,
9836
- triggerText: value || "Choose",
9861
+ triggerText: choices.find((c) => c.key === value || c.label === value)?.label ?? (value || "Choose"),
9837
9862
  open,
9838
9863
  onOpenChange: (next) => {
9839
9864
  setOpen(next);
@@ -9845,7 +9870,7 @@ function ChoiceValue({ field: field2, value, onChange }) {
9845
9870
  id: c.key,
9846
9871
  label: c.label,
9847
9872
  chip: choiceChipLookOf(field2, c.key, c.label).className,
9848
- picked: c.label === value,
9873
+ picked: c.label === value || c.key === value,
9849
9874
  attrs: { "data-pick-value": c.label }
9850
9875
  })),
9851
9876
  onPick: (key) => {
@@ -9954,6 +9979,15 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
9954
9979
  attrs: { "data-condition": "field" }
9955
9980
  }
9956
9981
  ),
9982
+ onField && !fields.some((f) => f.id === onField) ? /* @__PURE__ */ jsx37(
9983
+ "span",
9984
+ {
9985
+ className: "rounded bg-destructive px-1.5 py-0.5 text-[11px] font-medium text-destructive-foreground",
9986
+ title: "This condition names a question that is no longer on the form.",
9987
+ "data-records-condition-missing": "",
9988
+ children: "Missing question"
9989
+ }
9990
+ ) : null,
9957
9991
  onField ? /* @__PURE__ */ jsxs32(Fragment11, { children: [
9958
9992
  /* @__PURE__ */ jsx37(
9959
9993
  PickOne,
@@ -14951,7 +14985,18 @@ function useMergedGridControls(args) {
14951
14985
  label: "Duplicate property",
14952
14986
  icon: "copy-plus",
14953
14987
  disabledReason: mayStructure ? void 0 : editable ? rights.why("structure") : "Editing is off here.",
14954
- onSelect: () => setDialog({ kind: "insert", anchor: columnId2, side: "right", copyOf: field2 })
14988
+ // DUPLICATE PROPERTY (Notion N5): one door declares the copy right after this column and copies
14989
+ // every record's value into it (`custom.field_duplicate`); nothing to fill in, as in Notion.
14990
+ onSelect: () => {
14991
+ void client.fieldDuplicate({ field_id: field2.id }).then((made) => {
14992
+ if (!made.ok) {
14993
+ notify?.error(`${fieldName(field2)} was not duplicated: ${refusalSentence(made.error)}`);
14994
+ return;
14995
+ }
14996
+ tableShapeChanged5(tableId);
14997
+ notify?.success(`${made.data.label} is added.`);
14998
+ });
14999
+ }
14955
15000
  });
14956
15001
  if (summaries) {
14957
15002
  out.push(summaryMenuItem(field2, columnId2 === firstKey, summaries.chosen[columnId2], summaries.operations, (op2) => summaries.choose(columnId2, op2)));
@@ -15298,7 +15343,7 @@ function useMergedGridControls(args) {
15298
15343
  },
15299
15344
  children: /* @__PURE__ */ jsxs49(DialogContent8, { className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl", children: [
15300
15345
  /* @__PURE__ */ jsxs49(DialogHeader8, { children: [
15301
- /* @__PURE__ */ jsx54(DialogTitle8, { children: columnDialog?.kind === "settings" ? `Column settings \xB7 ${fieldName(columnDialog.field)}` : columnDialog?.kind === "insert" && columnDialog.copyOf ? `Duplicate property \xB7 ${fieldName(columnDialog.copyOf)}` : columnDialog?.kind === "insert" && columnDialog.anchor ? `Add column ${columnDialog.side === "left" ? "left" : "right"} of ${fieldOf2(columnDialog.anchor) ? fieldName(fieldOf2(columnDialog.anchor)) : columnDialog.anchor}` : ADD_FIELD_OPEN_LABEL }),
15346
+ /* @__PURE__ */ jsx54(DialogTitle8, { children: columnDialog?.kind === "settings" ? `Column settings \xB7 ${fieldName(columnDialog.field)}` : columnDialog?.kind === "insert" && columnDialog.anchor ? `Add column ${columnDialog.side === "left" ? "left" : "right"} of ${fieldOf2(columnDialog.anchor) ? fieldName(fieldOf2(columnDialog.anchor)) : columnDialog.anchor}` : ADD_FIELD_OPEN_LABEL }),
15302
15347
  /* @__PURE__ */ jsx54(DialogDescription6, { className: "sr-only", children: "Name, type and look" })
15303
15348
  ] }),
15304
15349
  columnDialog?.kind === "settings" ? /* @__PURE__ */ jsx54(
@@ -15322,7 +15367,6 @@ function useMergedGridControls(args) {
15322
15367
  FieldEditor,
15323
15368
  {
15324
15369
  tableId,
15325
- ...columnDialog.copyOf ? { copyOf: columnDialog.copyOf } : {},
15326
15370
  onCancel: () => {
15327
15371
  pendingInsert.current = null;
15328
15372
  close();
@@ -37712,9 +37756,41 @@ function formPresentation(spec) {
37712
37756
  welcome: welcomeDocument(spec.welcome ?? null),
37713
37757
  endings: (spec.endings ?? []).map(endingDocument),
37714
37758
  hidden_fields: spec.hiddenFields ?? [],
37715
- theme: themeDocument(spec.theme ?? null)
37759
+ theme: themeDocument(spec.theme ?? null),
37760
+ ...typeof spec.showOwnerHeader === "boolean" ? { show_owner_header: spec.showOwnerHeader } : {},
37761
+ ...typeof spec.autoAdvance === "boolean" ? { auto_advance: spec.autoAdvance } : {},
37762
+ ...spec.notify && (spec.notify.webhookUrl || spec.notify.webhookAutomationId) ? { notify: strip({ webhook_url: text(spec.notify.webhookUrl), webhook_automation_id: text(spec.notify.webhookAutomationId) }) } : {}
37716
37763
  };
37717
37764
  }
37765
+ function notifyFromDocument(raw) {
37766
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
37767
+ const d = raw;
37768
+ const n = { webhookUrl: text(d["webhook_url"]), webhookAutomationId: text(d["webhook_automation_id"]) };
37769
+ return n.webhookUrl || n.webhookAutomationId ? n : null;
37770
+ }
37771
+ var bool = (v) => typeof v === "boolean" ? v : null;
37772
+ function answerRefusal(kind, required, value) {
37773
+ const empty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
37774
+ if (empty) return required ? "This field is required" : null;
37775
+ if (typeof value !== "string") return null;
37776
+ const v = value.trim();
37777
+ if (kind === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return "Enter a valid email";
37778
+ if (kind === "phone" && (v.replace(/\D/g, "").length < 7 || /[^\d\s()+.\-x]/i.test(v))) return "Enter a valid phone number";
37779
+ if (kind === "url" && !/^(https?:\/\/)?[^\s.\/]+(\.[^\s.\/]+)+(\/\S*)?$/i.test(v)) return "Enter a valid link";
37780
+ return null;
37781
+ }
37782
+ function pipeAnswers(text2, answers, questions, labelOf) {
37783
+ if (!text2.includes("{{")) return text2;
37784
+ return text2.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_all, name) => {
37785
+ const wanted = name.trim().toLowerCase();
37786
+ const q = questions.find((x) => x.key.toLowerCase() === wanted) ?? questions.find((x) => (x.ask ?? "").trim().toLowerCase() === wanted);
37787
+ if (!q) return "";
37788
+ const value = answers[q.key];
37789
+ if (value === void 0 || value === null) return "";
37790
+ const one = (v) => labelOf?.(q.key, v) ?? String(v);
37791
+ return Array.isArray(value) ? value.map(one).join(", ") : one(value);
37792
+ });
37793
+ }
37718
37794
  var text = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : null;
37719
37795
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
37720
37796
  var strip = (doc) => Object.fromEntries(Object.entries(doc).filter(([, v]) => v !== null && v !== void 0));
@@ -37889,6 +37965,9 @@ function formFromSummary(summary) {
37889
37965
  thankYou: thankYouFromDocument(presentation["thank_you"]),
37890
37966
  look: formLookFromDocument(presentation["look"]),
37891
37967
  submitLabel: presentation["submit_label"] ?? null,
37968
+ showOwnerHeader: bool(presentation["show_owner_header"]),
37969
+ autoAdvance: bool(presentation["auto_advance"]),
37970
+ notify: notifyFromDocument(presentation["notify"]),
37892
37971
  // A form is public when the store says it is OPEN. A screen never decides
37893
37972
  // this from a flag of its own: the link answers or it does not.
37894
37973
  isPublic: summary.state === "open",
@@ -38693,6 +38772,50 @@ function compactTick(value) {
38693
38772
  if (Math.abs(value) < 1e3 && !Number.isInteger(value)) return String(Math.round(value * 10) / 10);
38694
38773
  return formatCount5(value, { style: "compact" });
38695
38774
  }
38775
+ function wrapTickWords(label, perLine, maxLines = 3) {
38776
+ const words2 = label.split(/\s+/).filter(Boolean).map((w) => w.length > perLine ? `${w.slice(0, perLine - 1)}\u2026` : w);
38777
+ const lines = [];
38778
+ let line = "";
38779
+ for (const word of words2) {
38780
+ const next = line ? `${line} ${word}` : word;
38781
+ if (next.length <= perLine) line = next;
38782
+ else {
38783
+ lines.push(line);
38784
+ line = word;
38785
+ }
38786
+ }
38787
+ if (line) lines.push(line);
38788
+ if (lines.length <= maxLines) return lines;
38789
+ const kept = lines.slice(0, maxLines);
38790
+ const last = kept[maxLines - 1].replace(/…$/, "");
38791
+ kept[maxLines - 1] = `${last.length >= perLine ? last.slice(0, perLine - 1) : last}\u2026`;
38792
+ return kept;
38793
+ }
38794
+ function WrappedTick(props) {
38795
+ const { x = 0, y = 0, payload, across, perLine } = props;
38796
+ const lines = wrapTickWords(String(payload?.value ?? ""), perLine);
38797
+ const lineHeight = 11;
38798
+ return /* @__PURE__ */ jsx120(
38799
+ "text",
38800
+ {
38801
+ x,
38802
+ y,
38803
+ fontSize: 10,
38804
+ fill: "currentColor",
38805
+ textAnchor: across ? "end" : "middle",
38806
+ "data-records-chart-tick": "",
38807
+ children: lines.map((line, i) => /* @__PURE__ */ jsx120(
38808
+ "tspan",
38809
+ {
38810
+ x: across ? x - 4 : x,
38811
+ dy: i === 0 ? across ? -((lines.length - 1) * lineHeight) / 2 + 3 : 10 : lineHeight,
38812
+ children: line
38813
+ },
38814
+ i
38815
+ ))
38816
+ }
38817
+ );
38818
+ }
38696
38819
  function Drawing({
38697
38820
  kind,
38698
38821
  points,
@@ -38798,9 +38921,9 @@ function Drawing({
38798
38921
  /* @__PURE__ */ jsx120(CartesianGrid, { vertical: horizontal, horizontal: !horizontal }),
38799
38922
  horizontal ? /* @__PURE__ */ jsxs110(Fragment48, { children: [
38800
38923
  /* @__PURE__ */ jsx120(XAxis, { type: "number", ...axis }),
38801
- /* @__PURE__ */ jsx120(YAxis, { type: "category", dataKey: "label", width: 88, ...axis })
38924
+ /* @__PURE__ */ jsx120(YAxis, { type: "category", dataKey: "label", width: 96, ...axis, tick: /* @__PURE__ */ jsx120(WrappedTick, { across: true, perLine: 15 }) })
38802
38925
  ] }) : /* @__PURE__ */ jsxs110(Fragment48, { children: [
38803
- /* @__PURE__ */ jsx120(XAxis, { dataKey: "label", ...axis }),
38926
+ /* @__PURE__ */ jsx120(XAxis, { dataKey: "label", ...axis, interval: 0, height: 40, tick: /* @__PURE__ */ jsx120(WrappedTick, { perLine: Math.max(6, Math.floor(width / Math.max(points.length, 1) / 6)) }) }),
38804
38927
  /* @__PURE__ */ jsx120(YAxis, { width: 44, ...axis })
38805
38928
  ] }),
38806
38929
  tooltip,
@@ -40449,7 +40572,7 @@ function FormLookLogo({ view, size = "md" }) {
40449
40572
  }
40450
40573
  );
40451
40574
  }
40452
- function FormLookFrame({ look, title, children, contained = false, className }) {
40575
+ function FormLookFrame({ look, title, children, contained = false, showOwnerHeader = true, className }) {
40453
40576
  const view = formLookView(look, title);
40454
40577
  return /* @__PURE__ */ jsxs117(
40455
40578
  "div",
@@ -40466,10 +40589,10 @@ function FormLookFrame({ look, title, children, contained = false, className })
40466
40589
  }
40467
40590
  ) : null,
40468
40591
  view.coverUrl ? /* @__PURE__ */ jsx127("div", { "data-records-form-cover": "", className: "aspect-[3/1] w-full overflow-hidden rounded-xl border border-border bg-muted", children: /* @__PURE__ */ jsx127("img", { src: view.coverUrl, alt: "", className: "h-full w-full object-cover" }) }) : null,
40469
- /* @__PURE__ */ jsxs117("div", { className: "flex items-center gap-3", children: [
40592
+ showOwnerHeader ? /* @__PURE__ */ jsxs117("div", { className: "flex items-center gap-3", "data-records-form-owner": "", children: [
40470
40593
  /* @__PURE__ */ jsx127(FormLookLogo, { view }),
40471
40594
  /* @__PURE__ */ jsx127("p", { className: "min-w-0 truncate text-base font-semibold tracking-tight text-foreground", children: view.name })
40472
- ] }),
40595
+ ] }) : null,
40473
40596
  /* @__PURE__ */ jsx127("h1", { className: "text-2xl font-semibold tracking-tight text-foreground", children: title }),
40474
40597
  /* @__PURE__ */ jsx127("div", { className: "w-full", children }),
40475
40598
  view.footerLinks.length > 0 ? /* @__PURE__ */ jsx127("footer", { className: "mt-10 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 pb-6 text-sm text-muted-foreground", children: view.footerLinks.map((link) => /* @__PURE__ */ jsx127(
@@ -41292,17 +41415,20 @@ function SubscriptionsPanel({ tableId, activeRuleId, className }) {
41292
41415
  // src/FormBuilder.tsx
41293
41416
  import { copyText as copyText7 } from "@ai-matrx/kit/clipboard";
41294
41417
  import { ErrorBox as ErrorBox7 } from "@ai-matrx/design-system";
41295
- import { useCallback as useCallback55, useEffect as useEffect97, useMemo as useMemo68, useRef as useRef56, useState as useState111 } from "react";
41296
- import { useFields as useFields25, useRecordsClient as useRecordsClient80, useTable as useTable27 } from "@ai-matrx/records/react";
41418
+ import { useCallback as useCallback54, useEffect as useEffect96, useMemo as useMemo68, useRef as useRef56, useState as useState110 } from "react";
41419
+ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
41420
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
41421
+ import { CSS as CSS2 } from "@dnd-kit/utilities";
41422
+ import { useFields as useFields25, useRecordsClient as useRecordsClient79, useTable as useTable26 } from "@ai-matrx/records/react";
41297
41423
  import { publicFormPath } from "@ai-matrx/records";
41298
41424
  import {
41299
41425
  BasicInput as BasicInput28,
41300
41426
  BasicTextarea as BasicTextarea11,
41301
- Button as Button87,
41427
+ Button as Button86,
41302
41428
  Checkbox as Checkbox15,
41303
- Label as Label22,
41304
- Skeleton as Skeleton30,
41305
- cn as cn79
41429
+ Label as Label23,
41430
+ Skeleton as Skeleton29,
41431
+ cn as cn78
41306
41432
  } from "@ai-matrx/design-system";
41307
41433
 
41308
41434
  // src/FormFlowEditor.tsx
@@ -41560,7 +41686,7 @@ function EmbedSnippets({ url, label }) {
41560
41686
  // src/FormRunner.tsx
41561
41687
  import { useCallback as useCallback53, useEffect as useEffect95, useMemo as useMemo67, useRef as useRef55, useState as useState109 } from "react";
41562
41688
  import { useFields as useFields24, useOptionalRecordsClient as useOptionalRecordsClient14 } from "@ai-matrx/records/react";
41563
- import { Button as Button85, Progress, Skeleton as Skeleton28, cn as cn77, formThemeClasses } from "@ai-matrx/design-system";
41689
+ import { Button as Button85, Label as Label22, Progress, Skeleton as Skeleton28, cn as cn77, formThemeClasses } from "@ai-matrx/design-system";
41564
41690
  import { humanizeIdentifier as humanizeIdentifier18 } from "@ai-matrx/kit/text-case";
41565
41691
  import { Fragment as Fragment53, jsx as jsx132, jsxs as jsxs122 } from "react/jsx-runtime";
41566
41692
  function authored(host, text2, where) {
@@ -41659,7 +41785,8 @@ function FormStage({
41659
41785
  className,
41660
41786
  connected = false,
41661
41787
  onStart,
41662
- onQuestionShown
41788
+ onQuestionShown,
41789
+ autoAdvance
41663
41790
  }) {
41664
41791
  const host = useRecordsUi();
41665
41792
  const [answers, setAnswers] = useState109(() => ({ ...initialAnswers ?? {} }));
@@ -41672,6 +41799,11 @@ function FormStage({
41672
41799
  const [ending, setEnding] = useState109(null);
41673
41800
  const [started, setStarted] = useState109(() => !form.welcome || resumed);
41674
41801
  const routes = formRoutes(form);
41802
+ const [routeKnown, setRouteKnown] = useState109(() => !routes || !whichAsked);
41803
+ const [stopped, setStopped] = useState109(null);
41804
+ const answersRef = useRef55(answers);
41805
+ answersRef.current = answers;
41806
+ const choose = autoAdvance ?? form.autoAdvance ?? true;
41675
41807
  const decoy = useRef55("");
41676
41808
  const stage = useRef55(null);
41677
41809
  const questions = useMemo67(() => {
@@ -41707,6 +41839,7 @@ function FormStage({
41707
41839
  };
41708
41840
  }
41709
41841
  if (cancelled) return;
41842
+ setRouteKnown(true);
41710
41843
  if (!told2.ok) {
41711
41844
  setHidden({});
41712
41845
  setError(told2.error ?? refusal("not_supported", told2.message, "Answer every question shown; none were skipped."));
@@ -41728,7 +41861,7 @@ function FormStage({
41728
41861
  ) : null
41729
41862
  );
41730
41863
  })();
41731
- }, 250);
41864
+ }, routeKnown ? 250 : 0);
41732
41865
  return () => {
41733
41866
  cancelled = true;
41734
41867
  clearTimeout(timer);
@@ -41763,11 +41896,24 @@ function FormStage({
41763
41896
  const v = answers[q.key];
41764
41897
  return v !== void 0 && v !== null && v !== "";
41765
41898
  }).length;
41766
- const missing = live.filter((q) => {
41767
- const v = answers[q.key];
41768
- return q.required && (v === void 0 || v === null || v === "");
41769
- });
41899
+ const refusalFor = (q, values = answersRef.current) => answerRefusal(q.field ? String(editorKindFor(q.field)) : "text", q.required, values[q.key]);
41900
+ const refuse = (q, say2) => {
41901
+ setStopped({ key: q.key, say: say2 });
41902
+ const i = live.findIndex((x) => x.key === q.key);
41903
+ if (oneAtATime && i >= 0 && i !== index) setAt(i);
41904
+ window.setTimeout(() => {
41905
+ const box = stage.current?.querySelector(`[data-records-form-question="${CSS.escape(q.key)}"]`);
41906
+ box?.querySelector("[role='radio'][aria-checked='true'], input:not([tabindex='-1']), textarea, select, [role='combobox'], [role='radio'], [role='checkbox']")?.focus();
41907
+ }, 0);
41908
+ };
41770
41909
  const submit = useCallback53(async () => {
41910
+ for (const q of live) {
41911
+ const say2 = refusalFor(q);
41912
+ if (say2) {
41913
+ refuse(q, say2);
41914
+ return;
41915
+ }
41916
+ }
41771
41917
  if (preview) {
41772
41918
  setDone("preview");
41773
41919
  return;
@@ -41799,20 +41945,28 @@ function FormStage({
41799
41945
  }, [answers, form, honeypotKey, live, onSubmit, onSubmitted, preview, questions]);
41800
41946
  async function advance() {
41801
41947
  if (!oneAtATime) return;
41948
+ if (current) {
41949
+ const say2 = refusalFor(current);
41950
+ if (say2) {
41951
+ refuse(current, say2);
41952
+ return;
41953
+ }
41954
+ }
41955
+ setStopped(null);
41802
41956
  if (!routes || !whichAsked || !current) {
41803
41957
  if (index < live.length - 1) setAt(index + 1);
41804
- else if (missing.length === 0) void submit();
41958
+ else void submit();
41805
41959
  return;
41806
41960
  }
41807
41961
  let told2;
41808
41962
  try {
41809
- told2 = await whichAsked(answers);
41963
+ told2 = await whichAsked(answersRef.current);
41810
41964
  } catch {
41811
41965
  told2 = { ok: false, message: "This form could not work out which question comes next, so the next one is shown." };
41812
41966
  }
41813
41967
  if (!told2.ok) {
41814
41968
  if (index < live.length - 1) setAt(index + 1);
41815
- else if (missing.length === 0) void submit();
41969
+ else void submit();
41816
41970
  return;
41817
41971
  }
41818
41972
  const next = {};
@@ -41825,13 +41979,18 @@ function FormStage({
41825
41979
  setAt(here + 1);
41826
41980
  return;
41827
41981
  }
41828
- const stillMissing = onRoute.filter((q) => {
41829
- const v = answers[q.key];
41830
- return q.required && (v === void 0 || v === null || v === "");
41831
- });
41832
- if (stillMissing.length === 0) void submit();
41982
+ const stillMissing = onRoute.find((q) => refusalFor(q) !== null);
41983
+ if (stillMissing) refuse(stillMissing, refusalFor(stillMissing));
41984
+ else void submit();
41833
41985
  }
41986
+ const advanceRef = useRef55(advance);
41987
+ advanceRef.current = advance;
41988
+ const chosen = useCallback53(() => {
41989
+ if (!oneAtATime || !choose) return;
41990
+ window.setTimeout(() => void advanceRef.current(), 450);
41991
+ }, [oneAtATime, choose]);
41834
41992
  function retreat() {
41993
+ setStopped(null);
41835
41994
  if (oneAtATime && index > 0) setAt(index - 1);
41836
41995
  }
41837
41996
  function onKeyDown(event) {
@@ -41847,12 +42006,11 @@ function FormStage({
41847
42006
  else advance();
41848
42007
  }
41849
42008
  useEffect95(() => {
41850
- const input = stage.current?.querySelector(
41851
- // A choice question has no box: its first option takes the caret, so A / B / C answer it.
42009
+ const input = stage.current?.querySelector("[role='radio'][aria-checked='true']") ?? stage.current?.querySelector(
41852
42010
  "input:not([tabindex='-1']), textarea, select, [role='combobox'], [role='radio'], [role='checkbox']"
41853
42011
  );
41854
42012
  input?.focus();
41855
- }, [index, done]);
42013
+ }, [index, done, started]);
41856
42014
  const startTold = useRef55(false);
41857
42015
  useEffect95(() => {
41858
42016
  if (startTold.current || !onStart) return;
@@ -41871,6 +42029,10 @@ function FormStage({
41871
42029
  }
41872
42030
  }, [current, live, oneAtATime, onQuestionShown, started, done]);
41873
42031
  const theme = formThemeClasses(form.theme ?? null);
42032
+ const pipe = (text2) => pipeAnswers(text2, answers, questions, (key, v) => {
42033
+ const f = questions.find((q) => q.key === key)?.field;
42034
+ return f ? publicChoicesOf(f).find((c) => c.key === String(v))?.label ?? null : null;
42035
+ });
41874
42036
  if (done) {
41875
42037
  return /* @__PURE__ */ jsx132(
41876
42038
  ThankYou,
@@ -41879,6 +42041,7 @@ function FormStage({
41879
42041
  done,
41880
42042
  ending: (form.endings ?? []).find((e) => e.id === ending) ?? null,
41881
42043
  buttonClass: theme.buttonClass,
42044
+ pipe,
41882
42045
  className: cn77("mx-auto max-w-xl py-10", centred && "text-center", className)
41883
42046
  }
41884
42047
  );
@@ -41890,14 +42053,14 @@ function FormStage({
41890
42053
  return /* @__PURE__ */ jsxs122(
41891
42054
  "section",
41892
42055
  {
41893
- className: cn77("mx-auto flex w-full max-w-xl flex-col gap-3 py-8", theme.surface, theme.background !== "plain" && "rounded-lg px-5", centred && "items-center text-center", className),
42056
+ className: cn77("mx-auto flex min-h-[min(70dvh,640px)] w-full max-w-2xl flex-col justify-center gap-4 py-8", theme.surface, theme.background !== "plain" && "rounded-lg px-5", centred && "items-center text-center", className),
41894
42057
  style: theme.surfaceStyle,
41895
42058
  "data-records-form-welcome": "",
41896
42059
  children: [
41897
42060
  w.pictureUrl ? /* @__PURE__ */ jsx132("img", { src: w.pictureUrl, alt: "", className: "max-h-56 w-full rounded-md object-cover" }) : null,
41898
- w.title ? /* @__PURE__ */ jsx132("h2", { className: "text-xl font-semibold", children: w.title }) : null,
41899
- w.body ? /* @__PURE__ */ jsx132("p", { className: "text-sm text-muted-foreground", children: authored(host, w.body, "form-intro") }) : null,
41900
- /* @__PURE__ */ jsx132("div", { children: /* @__PURE__ */ jsx132(Button85, { size: "sm", className: theme.buttonClass, onClick: () => setStarted(true), "data-records-form-start": "", children: w.buttonLabel || "Start" }) })
42061
+ w.title ? /* @__PURE__ */ jsx132("h2", { className: "text-2xl font-semibold sm:text-3xl", children: w.title }) : null,
42062
+ w.body ? /* @__PURE__ */ jsx132("p", { className: "text-base text-muted-foreground", children: authored(host, w.body, "form-intro") }) : null,
42063
+ /* @__PURE__ */ jsx132("div", { children: /* @__PURE__ */ jsx132(Button85, { size: "lg", className: theme.buttonClass, onClick: () => setStarted(true), "data-records-form-start": "", children: w.buttonLabel || "Start" }) })
41901
42064
  ]
41902
42065
  }
41903
42066
  );
@@ -41906,7 +42069,15 @@ function FormStage({
41906
42069
  return /* @__PURE__ */ jsxs122(
41907
42070
  "section",
41908
42071
  {
41909
- className: cn77("mx-auto flex w-full max-w-xl flex-col gap-3 py-4", theme.surface, theme.background !== "plain" && "rounded-lg px-5", centred && "text-center", className),
42072
+ className: cn77(
42073
+ "mx-auto flex w-full max-w-2xl flex-col gap-4 py-4",
42074
+ // TYPEFORM'S SIZE (TYPEFORM-2): one question at a time fills the screen, centred in it.
42075
+ oneAtATime && "min-h-[min(70dvh,640px)]",
42076
+ theme.surface,
42077
+ theme.background !== "plain" && "rounded-lg px-5 sm:px-8",
42078
+ centred && "text-center",
42079
+ className
42080
+ ),
41910
42081
  style: theme.surfaceStyle,
41911
42082
  "data-records-form-font": theme.font,
41912
42083
  onKeyDown,
@@ -41916,20 +42087,41 @@ function FormStage({
41916
42087
  Progress,
41917
42088
  {
41918
42089
  value: live.length === 0 ? 0 : (oneAtATime ? index + 1 : answered) / live.length * 100,
41919
- className: "h-1 flex-1"
42090
+ className: "h-1 flex-1",
42091
+ "aria-label": "Form progress",
42092
+ ...routeKnown ? { "aria-valuetext": oneAtATime ? `${index + 1} of ${live.length}` : `${answered} of ${live.length}` } : {}
41920
42093
  }
41921
42094
  ),
41922
- /* @__PURE__ */ jsx132("span", { className: "tabular-nums", children: oneAtATime ? `${index + 1} of ${live.length}` : `${answered} of ${live.length}` })
42095
+ /* @__PURE__ */ jsx132("span", { className: cn77("min-w-[3.5rem] text-right tabular-nums", !routeKnown && "invisible"), "data-records-form-count": "", children: oneAtATime ? `${index + 1} of ${live.length}` : `${answered} of ${live.length}` })
41923
42096
  ] }),
41924
- signedInPublic ? /* @__PURE__ */ jsx132("p", { className: "rounded border border-dashed px-2 py-1 text-left text-xs text-muted-foreground", children: "You are answering this signed in, so the record will carry your name. A stranger answers the same form at its public link, where the answer arrives through the anonymous door instead." }) : null,
41925
- unanswerable ? /* @__PURE__ */ jsx132("p", { className: "rounded border border-dashed px-2 py-1 text-left text-xs text-muted-foreground", children: "This form has questions that only appear in certain cases. Nothing here can work out which ones, so all of them are being shown rather than any being skipped." }) : null,
41926
- unresolved.length > 0 ? /* @__PURE__ */ jsxs122("p", { className: "rounded border border-dashed px-2 py-1 text-left text-xs text-destructive", children: [
41927
- unresolved.map((q) => q.key).join(", "),
41928
- " ",
41929
- unresolved.length === 1 ? "is a question" : "are questions",
41930
- " this form asks for and this table has no such Field. Add the Field, or take the question out \u2014 it is shown here rather than quietly dropped, because a dropped question is an answer nobody gave."
41931
- ] }) : null,
41932
- form.intro && index === 0 ? /* @__PURE__ */ jsx132("p", { className: "text-sm text-muted-foreground", children: authored(host, form.intro, "form-intro") }) : null,
42097
+ signedInPublic ? /* @__PURE__ */ jsx132(
42098
+ "p",
42099
+ {
42100
+ className: "w-fit rounded border border-dashed px-2 py-0.5 text-left text-xs text-muted-foreground",
42101
+ title: "The record will carry your name. Strangers answer at the public link.",
42102
+ children: "Signed in \xB7 saved as you"
42103
+ }
42104
+ ) : null,
42105
+ unanswerable ? /* @__PURE__ */ jsx132(
42106
+ "p",
42107
+ {
42108
+ className: "w-fit rounded border border-dashed px-2 py-0.5 text-left text-xs text-muted-foreground",
42109
+ title: "Nothing here can work out which conditional questions apply, so all of them are shown.",
42110
+ children: "Showing every question"
42111
+ }
42112
+ ) : null,
42113
+ unresolved.length > 0 ? /* @__PURE__ */ jsxs122(
42114
+ "p",
42115
+ {
42116
+ className: "w-fit rounded border border-destructive/40 px-2 py-0.5 text-left text-xs text-destructive",
42117
+ title: "The table has no such field. Add the field, or take the question out.",
42118
+ children: [
42119
+ "Missing field: ",
42120
+ unresolved.map((q) => q.ask).join(", ")
42121
+ ]
42122
+ }
42123
+ ) : null,
42124
+ form.intro && index === 0 ? /* @__PURE__ */ jsx132("p", { className: "text-base text-muted-foreground", children: authored(host, form.intro, "form-intro") }) : null,
41933
42125
  error ? /* @__PURE__ */ jsx132(RefusalNotice, { error, className: "text-left" }) : null,
41934
42126
  refusal4 ? /* @__PURE__ */ jsx132(
41935
42127
  RefusalNotice,
@@ -41951,14 +42143,20 @@ function FormStage({
41951
42143
  (oneAtATime ? current ? [current] : [] : live).map((q) => /* @__PURE__ */ jsx132(
41952
42144
  Question,
41953
42145
  {
41954
- question: q,
42146
+ question: { ...q, ask: pipe(q.ask), help: q.help ? pipe(q.help) : null },
41955
42147
  value: answers[q.key],
41956
- onChange: (v) => setAnswers((prev) => ({ ...prev, [q.key]: v })),
42148
+ onChange: (v) => {
42149
+ if (stopped?.key === q.key) setStopped(null);
42150
+ setAnswers((prev) => ({ ...prev, [q.key]: v }));
42151
+ },
42152
+ onChosen: chosen,
42153
+ refused: stopped?.key === q.key ? stopped.say : null,
42154
+ large: oneAtATime,
41957
42155
  upload: host.upload
41958
42156
  },
41959
42157
  q.key
41960
42158
  )),
41961
- live.length === 0 ? /* @__PURE__ */ jsx132("p", { className: "text-xs text-muted-foreground", children: "Every question in this form is hidden by its own condition right now, so there is nothing to answer yet." }) : null
42159
+ live.length === 0 ? /* @__PURE__ */ jsx132("p", { className: "text-xs text-muted-foreground", title: "Every question is hidden by its own condition right now.", children: "Nothing to answer yet" }) : null
41962
42160
  ] }),
41963
42161
  honeypotKey ? /* @__PURE__ */ jsxs122("div", { "aria-hidden": true, className: "pointer-events-none absolute left-[-9999px] h-px w-px overflow-hidden", children: [
41964
42162
  /* @__PURE__ */ jsx132("label", { htmlFor: `hp-${honeypotKey}`, children: "Leave this field empty" }),
@@ -41977,16 +42175,14 @@ function FormStage({
41977
42175
  }
41978
42176
  )
41979
42177
  ] }) : null,
41980
- /* @__PURE__ */ jsxs122("footer", { className: cn77("flex flex-wrap items-center gap-2", centred && "justify-center"), children: [
41981
- oneAtATime && index > 0 ? /* @__PURE__ */ jsx132(Button85, { size: "sm", variant: "ghost", className: theme.buttonClass, onClick: retreat, children: "Back" }) : null,
41982
- oneAtATime && index < live.length - 1 ? /* @__PURE__ */ jsx132(Button85, { size: "sm", className: theme.buttonClass, onClick: () => void advance(), children: "Next" }) : /* @__PURE__ */ jsx132(Button85, { size: "sm", className: theme.buttonClass, disabled: writing || missing.length > 0, onClick: () => void submit(), children: writing ? "Sending\u2026" : form.submitLabel ?? "Submit" }),
41983
- missing.length > 0 && (!oneAtATime || index === live.length - 1) ? /* @__PURE__ */ jsxs122("span", { className: "text-xs text-muted-foreground", children: [
41984
- missing.map((q) => q.ask).join(", "),
41985
- " still ",
41986
- missing.length === 1 ? "needs" : "need",
41987
- " an answer."
42178
+ /* @__PURE__ */ jsxs122("footer", { className: cn77("flex flex-wrap items-center gap-3", centred && "justify-center"), children: [
42179
+ oneAtATime && index > 0 ? /* @__PURE__ */ jsx132(Button85, { size: "lg", variant: "ghost", className: theme.buttonClass, onClick: retreat, children: "Back" }) : null,
42180
+ oneAtATime && index < live.length - 1 ? /* @__PURE__ */ jsx132(Button85, { size: "lg", className: theme.buttonClass, onClick: () => void advance(), children: "Next" }) : /* @__PURE__ */ jsx132(Button85, { size: "lg", className: theme.buttonClass, disabled: writing, onClick: () => void (oneAtATime ? advance() : submit()), children: writing ? "Sending\u2026" : form.submitLabel ?? "Submit" }),
42181
+ oneAtATime ? /* @__PURE__ */ jsxs122("span", { className: "hidden text-xs text-muted-foreground sm:inline", "data-records-form-enter-hint": "", children: [
42182
+ "press ",
42183
+ /* @__PURE__ */ jsx132("strong", { className: "font-semibold text-foreground", children: "Enter \u21B5" })
41988
42184
  ] }) : null,
41989
- preview ? /* @__PURE__ */ jsx132("span", { className: "text-xs text-muted-foreground", children: "Preview \u2014 nothing is written." }) : null
42185
+ preview ? /* @__PURE__ */ jsx132("span", { className: "text-xs text-muted-foreground", title: "Nothing is written from a preview.", children: "Preview" }) : null
41990
42186
  ] })
41991
42187
  ]
41992
42188
  }
@@ -41998,12 +42194,15 @@ function ThankYou({
41998
42194
  done,
41999
42195
  className,
42000
42196
  ending,
42001
- buttonClass
42197
+ buttonClass,
42198
+ pipe = (t) => t
42002
42199
  }) {
42003
42200
  const host = useRecordsUi();
42004
42201
  const redirect = (ending ? ending.redirectUrl?.trim() : form.thankYou?.redirectUrl?.trim()) || null;
42005
- const title = ending ? ending.title : form.thankYou?.title;
42006
- const body = ending ? ending.body : form.thankYou?.body;
42202
+ const rawTitle = ending ? ending.title : form.thankYou?.title;
42203
+ const rawBody = ending ? ending.body : form.thankYou?.body;
42204
+ const title = rawTitle ? pipe(rawTitle) : rawTitle;
42205
+ const body = rawBody ? pipe(rawBody) : rawBody;
42007
42206
  const going = redirect !== null && done !== "preview";
42008
42207
  useEffect95(() => {
42009
42208
  if (!going || !redirect) return;
@@ -42020,8 +42219,8 @@ function ThankYou({
42020
42219
  }
42021
42220
  ),
42022
42221
  ending?.pictureUrl ? /* @__PURE__ */ jsx132("img", { src: ending.pictureUrl, alt: "", className: "mb-3 max-h-48 w-full rounded-md object-cover" }) : null,
42023
- /* @__PURE__ */ jsx132("h2", { className: "text-lg font-medium", "data-records-form-ending": ending?.id ?? "", children: title || "Thank you" }),
42024
- body ? /* @__PURE__ */ jsx132("p", { className: "mt-1 text-sm text-muted-foreground", children: authored(host, body, "form-thank-you") }) : null,
42222
+ /* @__PURE__ */ jsx132("h2", { className: "text-2xl font-semibold", "data-records-form-ending": ending?.id ?? "", children: title || "Thank you" }),
42223
+ body ? /* @__PURE__ */ jsx132("p", { className: "mt-2 text-base text-muted-foreground", children: authored(host, body, "form-thank-you") }) : null,
42025
42224
  going ? /* @__PURE__ */ jsxs122("p", { className: "mt-4 flex flex-wrap items-center gap-3 text-sm", children: [
42026
42225
  /* @__PURE__ */ jsx132(
42027
42226
  "a",
@@ -42034,11 +42233,11 @@ function ThankYou({
42034
42233
  ),
42035
42234
  /* @__PURE__ */ jsx132("span", { className: "text-muted-foreground", children: "Taking you there\u2026" })
42036
42235
  ] }) : null,
42037
- /* @__PURE__ */ jsx132("p", { className: "mt-3 text-xs text-muted-foreground", children: done === "preview" ? redirect ? `This was a preview, so nothing was written. The live form goes on to ${redirect}.` : "This was a preview, so nothing was written." : done === "sent" ? (
42236
+ /* @__PURE__ */ jsx132("p", { className: "mt-3 text-xs text-muted-foreground", children: done === "preview" ? redirect ? `Preview \xB7 then ${redirect}` : "Preview \xB7 nothing saved" : done === "sent" ? (
42038
42237
  // SAID TO THE PERSON WHO ANSWERED (lane HANDOVER, 2026-09-29): "Saved to Form 1, with
42039
42238
  // this form stamped on it" was the owner's bookkeeping read out to a front-desk member
42040
42239
  // and to strangers. Where it landed is the owner's to see on the table.
42041
- "Your answers were received."
42240
+ "Answers received"
42042
42241
  ) : done })
42043
42242
  ] });
42044
42243
  }
@@ -42046,6 +42245,9 @@ function Question({
42046
42245
  question,
42047
42246
  value,
42048
42247
  onChange,
42248
+ onChosen,
42249
+ refused,
42250
+ large,
42049
42251
  upload
42050
42252
  }) {
42051
42253
  const host = useRecordsUi();
@@ -42053,297 +42255,62 @@ function Question({
42053
42255
  const field2 = question.field;
42054
42256
  if (!field2) return null;
42055
42257
  const id = `form-${field2.key}`;
42258
+ const sayId = `${id}-refused`;
42056
42259
  const isAttachment = editorKindFor(field2) === "attachment";
42057
- return /* @__PURE__ */ jsxs122("div", { className: "flex flex-col gap-1.5", children: [
42058
- /* @__PURE__ */ jsx132(FieldLabel, { field: { ...field2, label: question.ask, required: question.required }, htmlFor: id }),
42059
- question.help ? /* @__PURE__ */ jsx132("p", { className: "text-xs text-muted-foreground", children: authored(host, question.help, "form-help") }) : null,
42060
- isAttachment && !upload ? /* @__PURE__ */ jsx132("p", { className: "rounded border border-dashed px-2 py-1 text-xs text-muted-foreground", children: NO_UPLOAD_REASON }) : isAttachment ? /* @__PURE__ */ jsxs122(Fragment53, { children: [
42061
- /* @__PURE__ */ jsx132(
42062
- "input",
42063
- {
42064
- id,
42065
- type: "file",
42066
- className: "text-xs",
42067
- onChange: (e) => {
42068
- const file = e.target.files?.[0];
42069
- if (!file) return;
42070
- setUploadError(null);
42071
- void upload?.(file).then((result) => {
42072
- if (!result) return;
42073
- if (result.ok) onChange(result.fileId);
42074
- else
42075
- setUploadError(
42076
- refusal("invalid_argument", result.reason, "Pick another file, or try this one again.")
42077
- );
42078
- });
42079
- }
42080
- }
42260
+ return /* @__PURE__ */ jsxs122(
42261
+ "div",
42262
+ {
42263
+ className: cn77(
42264
+ "flex flex-col",
42265
+ large ? "gap-3 [&_input]:text-base [&_textarea]:text-base [&_input]:min-h-12" : "gap-1.5 [&_input]:text-base [&_textarea]:text-base"
42081
42266
  ),
42082
- uploadError ? /* @__PURE__ */ jsx132(RefusalLine, { error: uploadError }) : null
42083
- ] }) : (
42084
- // A choice is laid out as pills or a searchable list in its own colours (`ChoiceAnswer`).
42085
- /* @__PURE__ */ jsx132(FieldControl, { field: field2, value, onChange, id, asForm: true })
42086
- )
42087
- ] });
42088
- }
42089
-
42090
- // src/NotifyRuleEditor.tsx
42091
- import { useCallback as useCallback54, useEffect as useEffect96, useState as useState110 } from "react";
42092
- import { useRecordsClient as useRecordsClient79, useTable as useTable26 } from "@ai-matrx/records/react";
42093
- import { hasQuietHours as hasQuietHours3 } from "@ai-matrx/records";
42094
- import { Button as Button86, Skeleton as Skeleton29, cn as cn78 } from "@ai-matrx/design-system";
42095
- import { jsx as jsx133, jsxs as jsxs123 } from "react/jsx-runtime";
42096
- var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
42097
- var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
42098
- function NotifyRuleEditor({ tableId, seed, chosenRuleId, onChoose, className }) {
42099
- const client = useRecordsClient79();
42100
- const host = useRecordsUi();
42101
- const table = useTable26(tableId);
42102
- const rights = useTableRights(table.data);
42103
- const [subscriptions, setSubscriptions] = useState110(null);
42104
- const [cadences, setCadences] = useState110([]);
42105
- const [views, setViews] = useState110(null);
42106
- const [error, setError] = useState110(null);
42107
- const [busy, setBusy] = useState110(false);
42108
- const [stopping, setStopping] = useState110(null);
42109
- const load = useCallback54(async () => {
42110
- const [held, offered] = await Promise.all([
42111
- // THE PERSON'S OWN DOOR, not the notifier's. It answers what is addressed
42112
- // to them plus — only where they hold admin on this Table — anyone's over
42113
- // it, and it is narrowed to Tables they can already open.
42114
- client.subscriptions({ table_id: tableId }),
42115
- // The cadences are ASKED of the store, never a list typed into a picker
42116
- // here that could drift from what the digest runner understands.
42117
- client.subscriptionCadences()
42118
- ]);
42119
- if (!held.ok) {
42120
- setError(held.error);
42121
- return;
42122
- }
42123
- setError(null);
42124
- setSubscriptions(held.data);
42125
- if (offered.ok) setCadences(offered.data);
42126
- if (host.savedViews) {
42127
- setViews(await host.savedViews());
42128
- return;
42129
- }
42130
- const listed = await client.views({ table_id: tableId });
42131
- if (!listed.ok) {
42132
- setError(listed.error);
42133
- return;
42134
- }
42135
- setViews(listed.data.map((v) => ({ id: v.view_id, name: v.name })));
42136
- }, [client, host, tableId]);
42137
- useEffect96(() => {
42138
- void load();
42139
- }, [load]);
42140
- const write = useCallback54(
42141
- async (spec) => {
42142
- const declared2 = await client.subscriptionDeclare({
42143
- table_id: tableId,
42144
- spec: {
42145
- name: spec.name,
42146
- saved_view_id: spec.savedViewId,
42147
- cadence: spec.cadence ?? "instant",
42148
- schedule: spec.schedule ?? null,
42149
- channel: spec.channel ?? "in_app",
42150
- quiet_hours: spec.quietHours ?? null,
42151
- // "Tell me" is the default, and the store refuses a subscription with
42152
- // nobody to tell rather than writing one that fires at nobody.
42153
- recipient_user_id: spec.recipientUserId ?? client.config.actor.user_id ?? null,
42154
- event_key: spec.eventKey ?? "records.changed"
42155
- }
42156
- });
42157
- if (!declared2.ok) {
42158
- setError(declared2.error);
42159
- return null;
42160
- }
42161
- return declared2.data;
42162
- },
42163
- [client, tableId]
42164
- );
42165
- useEffect96(() => {
42166
- if (!subscriptions || !seed || seed.length === 0) return;
42167
- const missing = seed.filter((s) => !subscriptions.some((held) => held.name === s.name));
42168
- if (missing.length === 0) return;
42169
- void (async () => {
42170
- for (const spec of missing) {
42171
- if (!await write(spec)) return;
42172
- }
42173
- await load();
42174
- })();
42175
- }, [subscriptions, seed, write, load]);
42176
- async function subscribe(viewId, viewName, cadence, channel, schedule, quiet2) {
42177
- setBusy(true);
42178
- const ok = await write({
42179
- name: `${viewName} \u2014 ${CADENCE_WORDS2[cadence] ?? cadence}`,
42180
- savedViewId: viewId,
42181
- cadence,
42182
- channel,
42183
- schedule: schedule.trim() === "" ? null : schedule.trim(),
42184
- quietHours: quiet2
42185
- });
42186
- setBusy(false);
42187
- if (!ok) return;
42188
- onChoose?.(ok);
42189
- await load();
42190
- }
42191
- async function makeEveryRecordView() {
42192
- setBusy(true);
42193
- const made = await client.viewDeclare({
42194
- table_id: tableId,
42195
- spec: { name: `Every ${table.data?.name ?? "record"}`, filters: null }
42196
- });
42197
- setBusy(false);
42198
- if (!made.ok) {
42199
- setError(made.error);
42200
- return;
42201
- }
42202
- await load();
42203
- }
42204
- async function unsubscribe(ruleId) {
42205
- setStopping(null);
42206
- setBusy(true);
42207
- const deleted = await client.recordDelete({ record_id: ruleId });
42208
- setBusy(false);
42209
- if (!deleted.ok) {
42210
- setError(deleted.error);
42211
- return;
42212
- }
42213
- await load();
42214
- }
42215
- if (error) {
42216
- return /* @__PURE__ */ jsx133(
42217
- RefusalNotice,
42218
- {
42219
- error,
42220
- className,
42221
- actions: /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
42222
- }
42223
- );
42224
- }
42225
- if (subscriptions === null) return /* @__PURE__ */ jsx133(Skeleton29, { className: cn78("h-40 w-full", className) });
42226
- return /* @__PURE__ */ jsxs123("div", { className: cn78("flex flex-col gap-2", className), children: [
42227
- /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
42228
- /* @__PURE__ */ jsx133("span", { className: "font-medium text-foreground", children: "Tell me when" }),
42229
- /* @__PURE__ */ jsxs123("span", { children: [
42230
- subscriptions.length,
42231
- " subscription",
42232
- subscriptions.length === 1 ? "" : "s"
42233
- ] })
42234
- ] }),
42235
- subscriptions.length === 0 ? /* @__PURE__ */ jsx133("p", { className: "text-xs text-muted-foreground", children: "Nothing is telling anyone about this yet. A subscription is a Rule over a saved view, so what you are told about is exactly what that view holds." }) : /* @__PURE__ */ jsx133("ul", { className: "flex flex-col gap-1", children: subscriptions.map((held) => /* @__PURE__ */ jsxs123("li", { className: "flex items-center gap-2 rounded border px-2 py-1.5 text-xs", children: [
42236
- /* @__PURE__ */ jsx133("span", { className: "truncate font-medium", children: held.name }),
42237
- /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: CADENCE_WORDS2[held.cadence] ?? held.cadence }),
42238
- /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: CHANNEL_WORDS3[held.channel] ?? held.channel }),
42239
- held.schedule ? /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: scheduleWords(held.schedule, held.quiet_hours?.tz) }) : null,
42240
- held.next_digest_at ? /* @__PURE__ */ jsxs123("span", { className: "text-muted-foreground", children: [
42241
- "next ",
42242
- new Date(held.next_digest_at).toLocaleString()
42243
- ] }) : null,
42244
- hasQuietHours3(held.quiet_hours) ? /* @__PURE__ */ jsxs123("span", { className: "text-muted-foreground", children: [
42245
- "quiet ",
42246
- held.quiet_hours.start,
42247
- "\u2013",
42248
- held.quiet_hours.end
42249
- ] }) : null,
42250
- held.recipient_user_id === null ? /* @__PURE__ */ jsx133("span", { className: "text-destructive", children: "This one has nobody to tell, so it fires at nobody." }) : null,
42251
- held.saved_view_id === null ? /* @__PURE__ */ jsx133("span", { className: "text-destructive", children: "This one names no view, so the store never admits a record to it." }) : null,
42252
- held.muted ? /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: "Switched off \u2014 it tells nobody until somebody switches it back on." }) : null,
42253
- onChoose ? held.rule_id === chosenRuleId ? /* @__PURE__ */ jsxs123("span", { className: "ml-auto flex items-center gap-1", children: [
42254
- /* @__PURE__ */ jsx133("span", { className: "font-medium text-foreground", children: "Used for this form" }),
42255
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", disabled: busy, onClick: () => onChoose(null), children: "Stop using" })
42256
- ] }) : /* @__PURE__ */ jsx133(
42257
- Button86,
42258
- {
42259
- size: "sm",
42260
- variant: "outline",
42261
- className: "ml-auto",
42262
- disabled: busy,
42263
- onClick: () => onChoose(held.rule_id),
42264
- children: "Use for this form"
42265
- }
42266
- ) : null,
42267
- rights.write && !(onChoose && held.rule_id === chosenRuleId) && stopping === held.rule_id ? /* @__PURE__ */ jsxs123("span", { className: cn78("flex flex-wrap items-center gap-2", onChoose ? void 0 : "ml-auto"), children: [
42268
- /* @__PURE__ */ jsx133("span", { className: "text-xs text-destructive", children: archiveConfirmSentence("this subscription") }),
42269
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "destructive", disabled: busy, onClick: () => void unsubscribe(held.rule_id), children: "Stop telling me" }),
42270
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: () => setStopping(null), children: "Keep it" })
42271
- ] }) : rights.write && !(onChoose && held.rule_id === chosenRuleId) ? /* @__PURE__ */ jsx133(
42272
- Button86,
42273
- {
42274
- size: "sm",
42275
- variant: "ghost",
42276
- className: onChoose ? void 0 : "ml-auto",
42277
- disabled: busy,
42278
- onClick: () => setStopping(held.rule_id),
42279
- children: "Stop telling me"
42280
- }
42281
- ) : null
42282
- ] }, held.rule_id)) }),
42283
- onChoose && chosenRuleId && !subscriptions.some((held) => held.rule_id === chosenRuleId) ? /* @__PURE__ */ jsx133("p", { className: "text-xs text-destructive", children: "This form points at a subscription that is not in this list any more, so nobody is told when it is answered. Choose one below, or make one." }) : null,
42284
- views === null ? /* @__PURE__ */ jsx133("p", { className: "rounded border border-dashed px-2 py-1 text-xs text-muted-foreground", children: NO_SAVED_VIEWS_REASON }) : views.length === 0 ? onChoose && rights.write ? /* @__PURE__ */ jsxs123("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
42285
- /* @__PURE__ */ jsx133("span", { children: "A subscription watches a saved view, and this table has none yet." }),
42286
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "outline", disabled: busy, onClick: () => void makeEveryRecordView(), children: "Watch every record" })
42287
- ] }) : /* @__PURE__ */ jsx133("p", { className: "text-xs text-muted-foreground", children: "This organization has no saved views yet, so there is nothing to subscribe to. Save a view first." }) : rights.write ? /* @__PURE__ */ jsxs123(
42288
- "form",
42289
- {
42290
- className: "flex flex-wrap items-center gap-1",
42291
- onSubmit: (event) => {
42292
- event.preventDefault();
42293
- const form = event.currentTarget;
42294
- const view = form.elements.namedItem("view").value;
42295
- const cadence = form.elements.namedItem("cadence").value;
42296
- const channel = form.elements.namedItem("channel").value;
42297
- const schedule = form.elements.namedItem("schedule").value;
42298
- const start = form.elements.namedItem("quiet_start").value;
42299
- const end = form.elements.namedItem("quiet_end").value;
42300
- const name = views.find((v) => v.id === view)?.name ?? "View";
42301
- const quiet2 = start && end ? { start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone } : null;
42302
- if (view) void subscribe(view, name, cadence, channel, schedule, quiet2);
42303
- },
42304
- children: [
42305
- /* @__PURE__ */ jsx133("select", { name: "view", "aria-label": "View to be told about", className: "h-7 rounded border bg-background px-1 text-xs", children: views.map((view) => /* @__PURE__ */ jsx133("option", { value: view.id, children: view.name }, view.id)) }),
42306
- /* @__PURE__ */ jsx133("select", { name: "cadence", "aria-label": "How often", className: "h-7 rounded border bg-background px-1 text-xs", children: (cadences.length > 0 ? cadences : ["instant"]).map((cadence) => /* @__PURE__ */ jsx133("option", { value: cadence, children: CADENCE_WORDS2[cadence] ?? cadence }, cadence)) }),
42307
- /* @__PURE__ */ jsx133("select", { name: "channel", "aria-label": "Where it arrives", className: "h-7 rounded border bg-background px-1 text-xs", children: ["in_app", "email", "sms"].map((channel) => /* @__PURE__ */ jsx133("option", { value: channel, children: CHANNEL_WORDS3[channel] }, channel)) }),
42308
- /* @__PURE__ */ jsx133(
42309
- "input",
42310
- {
42311
- name: "schedule",
42312
- "aria-label": "When the summary goes out",
42313
- placeholder: "monday 08:00",
42314
- className: "h-7 w-28 rounded border bg-background px-1 text-xs"
42315
- }
42316
- ),
42317
- /* @__PURE__ */ jsx133("span", { className: "text-xs text-muted-foreground", children: "not between" }),
42318
- /* @__PURE__ */ jsx133(
42319
- "input",
42320
- {
42321
- name: "quiet_start",
42322
- type: "time",
42323
- "aria-label": "Quiet hours start",
42324
- className: "h-7 rounded border bg-background px-1 text-xs"
42325
- }
42326
- ),
42327
- /* @__PURE__ */ jsx133("span", { className: "text-xs text-muted-foreground", children: "and" }),
42328
- /* @__PURE__ */ jsx133(
42267
+ "data-records-form-question": field2.key,
42268
+ "aria-describedby": refused ? sayId : void 0,
42269
+ children: [
42270
+ /* @__PURE__ */ jsxs122("div", { className: "flex flex-wrap items-baseline gap-x-2", children: [
42271
+ /* @__PURE__ */ jsx132(Label22, { htmlFor: id, className: cn77("font-medium leading-snug text-foreground", large ? "text-xl sm:text-2xl" : "text-base"), children: question.ask }),
42272
+ question.required ? /* @__PURE__ */ jsx132("span", { className: "text-sm text-foreground/80", children: "required" }) : null,
42273
+ field2.unit ? /* @__PURE__ */ jsx132("span", { className: "text-sm text-foreground/70", children: field2.unit }) : null
42274
+ ] }),
42275
+ question.help ? /* @__PURE__ */ jsx132("p", { className: cn77("text-muted-foreground", large ? "text-base" : "text-sm"), children: authored(host, question.help, "form-help") }) : null,
42276
+ isAttachment && !upload ? /* @__PURE__ */ jsx132("p", { className: "w-fit rounded border border-dashed px-2 py-0.5 text-xs text-muted-foreground", title: NO_UPLOAD_REASON, children: "File upload unavailable here" }) : isAttachment ? /* @__PURE__ */ jsxs122(Fragment53, { children: [
42277
+ /* @__PURE__ */ jsx132(
42329
42278
  "input",
42330
42279
  {
42331
- name: "quiet_end",
42332
- type: "time",
42333
- "aria-label": "Quiet hours end",
42334
- className: "h-7 rounded border bg-background px-1 text-xs"
42280
+ id,
42281
+ type: "file",
42282
+ className: "text-sm",
42283
+ onChange: (e) => {
42284
+ const file = e.target.files?.[0];
42285
+ if (!file) return;
42286
+ setUploadError(null);
42287
+ void upload?.(file).then((result) => {
42288
+ if (!result) return;
42289
+ if (result.ok) onChange(result.fileId);
42290
+ else
42291
+ setUploadError(
42292
+ refusal("invalid_argument", result.reason, "Pick another file, or try this one again.")
42293
+ );
42294
+ });
42295
+ }
42335
42296
  }
42336
42297
  ),
42337
- /* @__PURE__ */ jsx133(Button86, { size: "sm", type: "submit", disabled: busy, children: "Tell me" }),
42338
- /* @__PURE__ */ jsx133("p", { className: "w-full text-xs text-muted-foreground", children: "A send inside your quiet hours is held until they end \u2014 it is never dropped. Leave the schedule blank for the store's own default of 08:00." })
42339
- ]
42340
- }
42341
- ) : /* @__PURE__ */ jsx133("p", { className: "text-xs text-muted-foreground", children: rights.why("write") })
42342
- ] });
42298
+ uploadError ? /* @__PURE__ */ jsx132(RefusalLine, { error: uploadError }) : null
42299
+ ] }) : (
42300
+ // A choice is laid out as lettered rows in the form's colour (`ChoiceAnswer`).
42301
+ /* @__PURE__ */ jsx132(FieldControl, { field: field2, value, onChange, id, asForm: true, onChosen })
42302
+ ),
42303
+ /* @__PURE__ */ jsx132("p", { id: sayId, role: "alert", "aria-live": "assertive", className: "min-h-0 empty:hidden", "data-records-form-refused": "", children: refused ? /* @__PURE__ */ jsxs122("span", { className: "inline-flex items-center gap-1.5 rounded bg-destructive px-2 py-1 text-sm font-medium text-destructive-foreground", children: [
42304
+ /* @__PURE__ */ jsx132("span", { "aria-hidden": "true", children: "!" }),
42305
+ refused
42306
+ ] }) : null })
42307
+ ]
42308
+ }
42309
+ );
42343
42310
  }
42344
42311
 
42345
42312
  // src/FormBuilder.tsx
42346
- import { Fragment as Fragment54, jsx as jsx134, jsxs as jsxs124 } from "react/jsx-runtime";
42313
+ import { Fragment as Fragment54, jsx as jsx133, jsxs as jsxs123 } from "react/jsx-runtime";
42347
42314
  function freshFormName(base, taken) {
42348
42315
  const names = new Set(taken.map((n) => n.trim().toLowerCase()));
42349
42316
  if (!names.has(base.toLowerCase())) return base;
@@ -42360,7 +42327,21 @@ function isLookRefusal(error) {
42360
42327
  return Boolean(error && LOOK_REFUSAL.test(refusalSentence(error)));
42361
42328
  }
42362
42329
  var KEPT_DRAFT_PREFIX = "records-ui:form-draft:";
42363
- var SPEC_KEYS = ["name", "intro", "questions", "flow", "theme", "look", "thankYou", "submitLabel"];
42330
+ var SPEC_KEYS = [
42331
+ "name",
42332
+ "intro",
42333
+ "questions",
42334
+ "flow",
42335
+ "theme",
42336
+ "look",
42337
+ "thankYou",
42338
+ "submitLabel",
42339
+ "welcome",
42340
+ "endings",
42341
+ "hiddenFields",
42342
+ "showOwnerHeader",
42343
+ "autoAdvance"
42344
+ ];
42364
42345
  function readKeptDraft(formId) {
42365
42346
  try {
42366
42347
  const raw = window.localStorage.getItem(KEPT_DRAFT_PREFIX + formId);
@@ -42391,31 +42372,31 @@ function withKept(stored, spec) {
42391
42372
  return out;
42392
42373
  }
42393
42374
  function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne, createOnMount, className }) {
42394
- const client = useRecordsClient80();
42375
+ const client = useRecordsClient79();
42395
42376
  const host = useRecordsUi();
42396
42377
  const claimSeed = useSeedGuard();
42397
- const table = useTable27(tableId);
42378
+ const table = useTable26(tableId);
42398
42379
  const rights = useTableRights(table.data);
42399
42380
  const fields = useFields25(tableId);
42400
- const [forms, setForms] = useState111(null);
42401
- const [error, setError] = useState111(null);
42402
- const [refused, setRefused] = useState111(null);
42403
- const [activeId, setActiveId] = useState111(activeFormId ?? null);
42404
- const [draft, setDraft] = useState111(null);
42405
- const [saving, setSaving] = useState111(false);
42406
- const [saved, setSaved] = useState111(null);
42407
- const [publishing, setPublishing] = useState111(false);
42408
- const [copied, setCopied] = useState111(false);
42409
- const [shownUrl, setShownUrl] = useState111(null);
42410
- const [lookOptions, setLookOptions] = useState111(null);
42411
- const [lookUnread, setLookUnread] = useState111(null);
42412
- const [restoredAt, setRestoredAt] = useState111(null);
42413
- const [showResults, setShowResults] = useState111(false);
42414
- const [adding, setAdding] = useState111(false);
42381
+ const [forms, setForms] = useState110(null);
42382
+ const [error, setError] = useState110(null);
42383
+ const [refused, setRefused] = useState110(null);
42384
+ const [activeId, setActiveId] = useState110(activeFormId ?? null);
42385
+ const [draft, setDraft] = useState110(null);
42386
+ const [saving, setSaving] = useState110(false);
42387
+ const [saved, setSaved] = useState110(null);
42388
+ const [publishing, setPublishing] = useState110(false);
42389
+ const [copied, setCopied] = useState110(false);
42390
+ const [shownUrl, setShownUrl] = useState110(null);
42391
+ const [lookOptions, setLookOptions] = useState110(null);
42392
+ const [lookUnread, setLookUnread] = useState110(null);
42393
+ const [restoredAt, setRestoredAt] = useState110(null);
42394
+ const [showResults, setShowResults] = useState110(false);
42395
+ const [adding, setAdding] = useState110(false);
42415
42396
  const keysBeforeAdd = useRef56(null);
42416
- const [makeOnMount, setMakeOnMount] = useState111("waiting");
42397
+ const [makeOnMount, setMakeOnMount] = useState110("waiting");
42417
42398
  const publicOrigin = host.publicOrigin ?? (typeof window === "undefined" ? "" : window.location.origin);
42418
- const load = useCallback55(async () => {
42399
+ const load = useCallback54(async () => {
42419
42400
  const answered = await client.forms({ table_id: tableId });
42420
42401
  if (!answered.ok) {
42421
42402
  setError(answered.error);
@@ -42442,10 +42423,10 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42442
42423
  setError(null);
42443
42424
  setForms(mine);
42444
42425
  }, [client, tableId, seed, claimSeed]);
42445
- useEffect97(() => {
42426
+ useEffect96(() => {
42446
42427
  void load();
42447
42428
  }, [load]);
42448
- useEffect97(() => {
42429
+ useEffect96(() => {
42449
42430
  if (!forms || forms.length === 0) return;
42450
42431
  if (createOnMount && makeOnMount !== "made") return;
42451
42432
  const chosen = forms.find((f) => f.id === (activeFormId ?? activeId)) ?? forms[0];
@@ -42469,7 +42450,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42469
42450
  );
42470
42451
  const stored = useMemo68(() => draft ? (forms ?? []).find((f) => f.id === draft.id) ?? null : null, [draft, forms]);
42471
42452
  const dirty = Boolean(draft && stored && !sameForm(draft, stored));
42472
- useEffect97(() => {
42453
+ useEffect96(() => {
42473
42454
  if (!draft || !stored) return;
42474
42455
  const timer = window.setTimeout(() => {
42475
42456
  if (sameForm(draft, stored)) dropKeptDraft(draft.id);
@@ -42478,7 +42459,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42478
42459
  return () => window.clearTimeout(timer);
42479
42460
  }, [draft, stored]);
42480
42461
  const lookKey = draft ? JSON.stringify(formLookDocument(draft.look ?? null)) : null;
42481
- useEffect97(() => {
42462
+ useEffect96(() => {
42482
42463
  if (lookKey === null) return;
42483
42464
  let cancelled = false;
42484
42465
  const timer = window.setTimeout(() => {
@@ -42495,7 +42476,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42495
42476
  window.clearTimeout(timer);
42496
42477
  };
42497
42478
  }, [client, lookKey]);
42498
- useEffect97(() => {
42479
+ useEffect96(() => {
42499
42480
  const before = keysBeforeAdd.current;
42500
42481
  if (!before || !fields.data) return;
42501
42482
  const made = askableFields(fields.data).filter((f) => !before.has(f.key));
@@ -42534,7 +42515,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42534
42515
  }
42535
42516
  dropKeptDraft(draft.id);
42536
42517
  setRestoredAt(null);
42537
- setSaved("Saved.");
42518
+ setSaved("Saved");
42538
42519
  await load();
42539
42520
  return true;
42540
42521
  }
@@ -42573,7 +42554,22 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42573
42554
  setRefused(written.error);
42574
42555
  return;
42575
42556
  }
42576
- setSaved(ruleId ? "Saved \u2014 you will be told when somebody answers." : "Saved \u2014 nobody is told now.");
42557
+ setSaved("Saved");
42558
+ await load();
42559
+ }
42560
+ async function chooseWebhook(notify) {
42561
+ if (!draft) return;
42562
+ const next = { ...draft, notify };
42563
+ setDraft(next);
42564
+ setSaving(true);
42565
+ setRefused(null);
42566
+ const written = await client.formDeclare(formDeclareArgs(next));
42567
+ setSaving(false);
42568
+ if (!written.ok) {
42569
+ setRefused(written.error);
42570
+ return;
42571
+ }
42572
+ setSaved("Saved");
42577
42573
  await load();
42578
42574
  }
42579
42575
  async function create(name) {
@@ -42590,15 +42586,15 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42590
42586
  await load();
42591
42587
  return true;
42592
42588
  }
42593
- const [started, setStarted] = useState111(false);
42594
- useEffect97(() => {
42589
+ const [started, setStarted] = useState110(false);
42590
+ useEffect96(() => {
42595
42591
  if (!startWithOne || started || forms === null || forms.length > 0) return;
42596
42592
  if (askableFields(fields.data ?? []).length === 0) return;
42597
42593
  if (!table.data && !table.error) return;
42598
42594
  setStarted(true);
42599
42595
  void create(table.data?.name?.trim() ? `${table.data.name.trim()} form` : "Form 1");
42600
42596
  }, [startWithOne, started, forms, fields.data, create, table.data, table.error]);
42601
- useEffect97(() => {
42597
+ useEffect96(() => {
42602
42598
  if (!createOnMount || makeOnMount !== "waiting" || forms === null) return;
42603
42599
  if (!table.data && !table.error) return;
42604
42600
  if (askableFields(fields.data ?? []).length === 0) {
@@ -42611,396 +42607,320 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42611
42607
  (made) => setMakeOnMount(made ? "made" : "not-made")
42612
42608
  );
42613
42609
  }, [createOnMount, makeOnMount, forms, fields.data, create, table.data, table.error]);
42614
- if (fields.error) return /* @__PURE__ */ jsx134(RefusalNotice, { error: fields.error, className });
42610
+ if (fields.error) return /* @__PURE__ */ jsx133(RefusalNotice, { error: fields.error, className });
42615
42611
  if (error) {
42616
- return /* @__PURE__ */ jsx134(
42612
+ return /* @__PURE__ */ jsx133(
42617
42613
  RefusalNotice,
42618
42614
  {
42619
42615
  error,
42620
42616
  className,
42621
- actions: /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
42617
+ actions: /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
42622
42618
  }
42623
42619
  );
42624
42620
  }
42625
- if (fields.loading || forms === null) return /* @__PURE__ */ jsx134(Skeleton30, { className: cn79("h-64 w-full", className) });
42621
+ if (fields.loading || forms === null) return /* @__PURE__ */ jsx133(Skeleton29, { className: cn78("h-64 w-full", className) });
42626
42622
  if (createOnMount && (makeOnMount === "waiting" || makeOnMount === "making") && !refused) {
42627
- return /* @__PURE__ */ jsx134(Skeleton30, { className: cn79("h-64 w-full", className) });
42623
+ return /* @__PURE__ */ jsx133(Skeleton29, { className: cn78("h-64 w-full", className) });
42628
42624
  }
42629
42625
  if (!rights.admin) {
42630
- return /* @__PURE__ */ jsx134("p", { className: cn79("text-xs text-muted-foreground", className), children: rights.why("structure") });
42626
+ return /* @__PURE__ */ jsx133("p", { className: cn78("text-xs text-muted-foreground", className), children: rights.why("structure") });
42631
42627
  }
42632
- return /* @__PURE__ */ jsxs124("div", { className: cn79("@container flex min-h-0 flex-col gap-3 @3xl:flex-row", className), children: [
42633
- /* @__PURE__ */ jsxs124("div", { className: "flex min-w-0 flex-1 flex-col gap-2 overflow-auto", children: [
42634
- /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Forms", children: [
42635
- forms.map((form) => /* @__PURE__ */ jsx134(
42636
- Button87,
42637
- {
42638
- size: "sm",
42639
- variant: form.id === activeId ? "secondary" : "ghost",
42640
- "aria-pressed": form.id === activeId,
42641
- onClick: () => {
42642
- setActiveId(form.id);
42643
- setDraft(form);
42628
+ return (
42629
+ // THE CONTAINER IS THE PARENT (TYPEFORM-2): a container query asks an ANCESTOR, so `@3xl:` on the
42630
+ // element that is itself `@container` never fired and the preview sat 3,000 px below the editor.
42631
+ /* @__PURE__ */ jsx133("div", { className: cn78("@container min-h-0", className), children: /* @__PURE__ */ jsxs123("div", { className: "flex min-h-0 flex-col gap-3 @3xl:flex-row @3xl:items-start", children: [
42632
+ /* @__PURE__ */ jsxs123("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
42633
+ /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Forms", children: [
42634
+ forms.map((form) => /* @__PURE__ */ jsx133(
42635
+ Button86,
42636
+ {
42637
+ size: "sm",
42638
+ variant: form.id === activeId ? "secondary" : "ghost",
42639
+ "aria-pressed": form.id === activeId,
42640
+ onClick: () => {
42641
+ setActiveId(form.id);
42642
+ setDraft(form);
42643
+ },
42644
+ children: form.name
42644
42645
  },
42645
- children: form.name
42646
- },
42647
- form.id
42648
- )),
42649
- (fields.data ?? []).length > 0 ? /* @__PURE__ */ jsx134(
42650
- Button87,
42651
- {
42652
- size: "sm",
42653
- variant: "ghost",
42654
- onClick: () => void create(
42655
- freshFormName(
42656
- table.data?.name?.trim() ? `${table.data.name.trim()} form` : "Form",
42657
- forms.map((f) => f.name)
42658
- )
42659
- ),
42660
- children: "New form"
42661
- }
42662
- ) : null,
42663
- /* @__PURE__ */ jsxs124("span", { className: "ml-auto flex items-center gap-2", children: [
42664
- saved ? /* @__PURE__ */ jsx134("span", { className: "text-xs text-muted-foreground", children: saved }) : null,
42665
- stored ? /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: showResults ? "default" : "ghost", onClick: () => setShowResults((v) => !v), "data-records-form-results-toggle": "", children: "Results" }) : null,
42666
- /* @__PURE__ */ jsx134(Button87, { size: "sm", disabled: saving || !draft, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
42667
- ] })
42668
- ] }),
42669
- refused && !isLookRefusal(refused) ? /* @__PURE__ */ jsx134(
42670
- RefusalNotice,
42671
- {
42672
- error: refused,
42673
- onKeepEditing: () => setRefused(null),
42674
- onDiscard: () => {
42675
- const kept = forms.find((f) => f.id === activeId) ?? null;
42676
- if (kept) dropKeptDraft(kept.id);
42677
- setDraft(kept);
42678
- setRestoredAt(null);
42679
- setSaved(null);
42680
- setRefused(null);
42681
- }
42682
- }
42683
- ) : null,
42684
- restoredAt && dirty && stored ? /* @__PURE__ */ jsxs124("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-form-restored": "", children: [
42685
- /* @__PURE__ */ jsxs124("span", { children: [
42686
- "Unsaved changes from",
42687
- " ",
42688
- new Date(restoredAt).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" }),
42689
- " restored"
42646
+ form.id
42647
+ )),
42648
+ (fields.data ?? []).length > 0 ? /* @__PURE__ */ jsx133(
42649
+ Button86,
42650
+ {
42651
+ size: "sm",
42652
+ variant: "ghost",
42653
+ onClick: () => void create(
42654
+ freshFormName(
42655
+ table.data?.name?.trim() ? `${table.data.name.trim()} form` : "Form",
42656
+ forms.map((f) => f.name)
42657
+ )
42658
+ ),
42659
+ children: "New form"
42660
+ }
42661
+ ) : null,
42662
+ /* @__PURE__ */ jsxs123("span", { className: "ml-auto flex items-center gap-2", children: [
42663
+ saved ? /* @__PURE__ */ jsx133("span", { className: "text-xs text-muted-foreground", children: saved }) : null,
42664
+ stored ? /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: showResults ? "default" : "ghost", onClick: () => setShowResults((v) => !v), "data-records-form-results-toggle": "", children: "Results" }) : null,
42665
+ /* @__PURE__ */ jsx133(Button86, { size: "sm", disabled: saving || !draft, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
42666
+ ] })
42690
42667
  ] }),
42691
- /* @__PURE__ */ jsx134(
42692
- Button87,
42668
+ refused && !isLookRefusal(refused) ? /* @__PURE__ */ jsx133(
42669
+ RefusalNotice,
42693
42670
  {
42694
- size: "sm",
42695
- variant: "ghost",
42696
- onClick: () => {
42697
- dropKeptDraft(stored.id);
42698
- setDraft(stored);
42671
+ error: refused,
42672
+ onKeepEditing: () => setRefused(null),
42673
+ onDiscard: () => {
42674
+ const kept = forms.find((f) => f.id === activeId) ?? null;
42675
+ if (kept) dropKeptDraft(kept.id);
42676
+ setDraft(kept);
42699
42677
  setRestoredAt(null);
42700
- },
42701
- children: "Discard"
42702
- }
42703
- )
42704
- ] }) : null,
42705
- !draft ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: (fields.data ?? []).length === 0 ? "This table has no fields yet, so there is nothing a form could ask for. Add a field first \u2014 every question is one of this table's own fields." : 'No form collects into this table yet. "New form" makes one, or you ask an agent for the whole thing in a sentence.' }) : /* @__PURE__ */ jsxs124(Fragment54, { children: [
42706
- /* @__PURE__ */ jsx134(
42707
- PublishRow,
42708
- {
42709
- url: `${publicOrigin}${publicFormPath(draft.id)}`,
42710
- state: draft.state,
42711
- mayPublish: rights.structure,
42712
- why: rights.why("structure"),
42713
- busy: publishing,
42714
- copied,
42715
- shownUrl,
42716
- onPublish: (wanted) => void publish(wanted),
42717
- onCopy: (url) => void copyLink(url),
42718
- unsaved: dirty
42678
+ setSaved(null);
42679
+ setRefused(null);
42680
+ }
42719
42681
  }
42720
- ),
42721
- /* @__PURE__ */ jsx134(EmbedSnippets, { url: `${publicOrigin}${publicFormPath(draft.id)}`, label: draft.name }),
42722
- /* @__PURE__ */ jsxs124("section", { className: "flex flex-col gap-1 rounded border px-2.5 py-2", "aria-label": "When somebody answers", children: [
42723
- /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: draft.notifyRuleId ? "When somebody answers, you are told through the subscription marked below." : "Nobody is told when this form is answered yet. Pick a subscription below, or make one \u2014 email, text or in the app." }),
42724
- /* @__PURE__ */ jsx134(
42725
- NotifyRuleEditor,
42682
+ ) : null,
42683
+ restoredAt && dirty && stored ? /* @__PURE__ */ jsxs123("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-form-restored": "", children: [
42684
+ /* @__PURE__ */ jsxs123("span", { children: [
42685
+ "Unsaved changes from",
42686
+ " ",
42687
+ new Date(restoredAt).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" }),
42688
+ " restored"
42689
+ ] }),
42690
+ /* @__PURE__ */ jsx133(
42691
+ Button86,
42726
42692
  {
42727
- tableId,
42728
- chosenRuleId: draft.notifyRuleId,
42729
- onChoose: (ruleId) => void chooseNotify(ruleId)
42693
+ size: "sm",
42694
+ variant: "ghost",
42695
+ onClick: () => {
42696
+ dropKeptDraft(stored.id);
42697
+ setDraft(stored);
42698
+ setRestoredAt(null);
42699
+ },
42700
+ children: "Discard"
42730
42701
  }
42731
42702
  )
42732
- ] }),
42733
- /* @__PURE__ */ jsxs124("section", { className: "flex flex-col gap-2 rounded border px-2.5 py-2", "aria-label": "After somebody sends it", children: [
42734
- /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: draft.thankYou?.redirectUrl ? "After sending, people see this message for a moment and then go on to the page below." : "After sending, people see this message. Add a page to send them on to, if there is a next step somewhere else." }),
42735
- /* @__PURE__ */ jsxs124("div", { className: "grid grid-cols-2 gap-2", children: [
42736
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42737
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Thank-you title" }),
42738
- /* @__PURE__ */ jsx134(
42739
- BasicInput28,
42740
- {
42741
- value: draft.thankYou?.title ?? "",
42742
- placeholder: "Thank you",
42743
- onChange: (e) => patch({ thankYou: { ...draft.thankYou, title: e.target.value || null } })
42744
- }
42745
- )
42746
- ] }),
42747
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42748
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Then go to (optional)" }),
42749
- /* @__PURE__ */ jsx134(
42750
- BasicInput28,
42703
+ ] }) : null,
42704
+ !draft ? /* @__PURE__ */ jsx133("p", { className: "text-xs text-muted-foreground", children: (fields.data ?? []).length === 0 ? "No fields yet" : "No form yet" }) : /* @__PURE__ */ jsxs123(Fragment54, { children: [
42705
+ /* @__PURE__ */ jsx133(
42706
+ PublishRow,
42707
+ {
42708
+ url: `${publicOrigin}${publicFormPath(draft.id)}`,
42709
+ state: draft.state,
42710
+ mayPublish: rights.structure,
42711
+ why: rights.why("structure"),
42712
+ busy: publishing,
42713
+ copied,
42714
+ shownUrl,
42715
+ onPublish: (wanted) => void publish(wanted),
42716
+ onCopy: (url) => void copyLink(url),
42717
+ unsaved: dirty
42718
+ }
42719
+ ),
42720
+ /* @__PURE__ */ jsx133(EmbedSnippets, { url: `${publicOrigin}${publicFormPath(draft.id)}`, label: draft.name }),
42721
+ /* @__PURE__ */ jsx133(
42722
+ NotifyOnAnswer,
42723
+ {
42724
+ tableId,
42725
+ tableName: table.data?.name ?? "record",
42726
+ formName: draft.name,
42727
+ ruleId: draft.notifyRuleId,
42728
+ notify: draft.notify ?? null,
42729
+ onRule: (ruleId) => void chooseNotify(ruleId),
42730
+ onNotify: (notify) => void chooseWebhook(notify)
42731
+ }
42732
+ ),
42733
+ /* @__PURE__ */ jsxs123("section", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 rounded border px-2.5 py-2 text-xs", "aria-label": "Options", children: [
42734
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", title: "Off hides your logo and name \u2014 for a client's form.", children: [
42735
+ /* @__PURE__ */ jsx133(
42736
+ Checkbox15,
42751
42737
  {
42752
- type: "url",
42753
- inputMode: "url",
42754
- value: draft.thankYou?.redirectUrl ?? "",
42755
- placeholder: "https://",
42756
- onChange: (e) => patch({ thankYou: { ...draft.thankYou, redirectUrl: e.target.value || null } })
42738
+ checked: draft.showOwnerHeader ?? true,
42739
+ onCheckedChange: (checked) => patch({ showOwnerHeader: checked === true }),
42740
+ "aria-label": "Show our logo and name"
42757
42741
  }
42758
- )
42742
+ ),
42743
+ "Show our logo and name"
42759
42744
  ] }),
42760
- /* @__PURE__ */ jsxs124("label", { className: "col-span-2 flex flex-col gap-1", children: [
42761
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Thank-you message" }),
42762
- /* @__PURE__ */ jsx134(
42763
- BasicTextarea11,
42745
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", title: "A single choice goes on to the next question when picked.", children: [
42746
+ /* @__PURE__ */ jsx133(
42747
+ Checkbox15,
42764
42748
  {
42765
- rows: 2,
42766
- value: draft.thankYou?.body ?? "",
42767
- placeholder: "What happens next, in your words.",
42768
- onChange: (e) => patch({ thankYou: { ...draft.thankYou, body: e.target.value || null } })
42749
+ checked: draft.autoAdvance ?? true,
42750
+ onCheckedChange: (checked) => patch({ autoAdvance: checked === true }),
42751
+ "aria-label": "Next after a choice"
42769
42752
  }
42770
- )
42753
+ ),
42754
+ "Next after a choice"
42771
42755
  ] })
42772
- ] })
42773
- ] }),
42774
- /* @__PURE__ */ jsx134(WelcomeEditor, { value: draft.welcome, pictures: lookOptions?.pictures ?? null, onChange: (welcome) => patch({ welcome }) }),
42775
- /* @__PURE__ */ jsx134(
42776
- EndingsEditor,
42777
- {
42778
- endings: draft.endings ?? [],
42779
- pictures: lookOptions?.pictures ?? null,
42780
- fieldKeys: conditionFields,
42781
- scored: draft.questions.some((q) => q.points),
42782
- onChange: (endings) => patch({ endings })
42783
- }
42784
- ),
42785
- /* @__PURE__ */ jsxs124("div", { className: "grid grid-cols-2 gap-2", children: [
42786
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42787
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Name" }),
42788
- /* @__PURE__ */ jsx134(BasicInput28, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) })
42789
- ] }),
42790
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42791
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Flow" }),
42792
- /* @__PURE__ */ jsx134(
42793
- "select",
42794
- {
42795
- className: "h-9 rounded border bg-background px-2 text-sm",
42796
- value: draft.flow ?? "one-at-a-time",
42797
- onChange: (e) => patch({ flow: e.target.value }),
42798
- children: FORM_FLOWS.map((flow) => /* @__PURE__ */ jsx134("option", { value: flow, children: flow === "one-at-a-time" ? "One question at a time" : "All on one page" }, flow))
42799
- }
42800
- )
42801
42756
  ] }),
42802
- /* @__PURE__ */ jsxs124("label", { className: "col-span-2 flex flex-col gap-1", children: [
42803
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Intro" }),
42804
- /* @__PURE__ */ jsx134(
42805
- BasicTextarea11,
42806
- {
42807
- rows: 2,
42808
- value: draft.intro ?? "",
42809
- onChange: (e) => patch({ intro: e.target.value || null })
42810
- }
42811
- )
42812
- ] }),
42813
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42814
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Submit button" }),
42815
- /* @__PURE__ */ jsx134(
42816
- BasicInput28,
42817
- {
42818
- value: draft.submitLabel ?? "",
42819
- placeholder: "Submit",
42820
- onChange: (e) => patch({ submitLabel: e.target.value || null })
42821
- }
42822
- )
42757
+ /* @__PURE__ */ jsxs123("section", { className: "flex flex-col gap-2 rounded border px-2.5 py-2", "aria-label": "After somebody sends it", children: [
42758
+ /* @__PURE__ */ jsx133("span", { className: "text-xs font-medium", title: "What people see after sending, and an optional page they go on to.", children: "After sending" }),
42759
+ /* @__PURE__ */ jsxs123("div", { className: "grid grid-cols-2 gap-2", children: [
42760
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42761
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Thank-you title" }),
42762
+ /* @__PURE__ */ jsx133(
42763
+ BasicInput28,
42764
+ {
42765
+ value: draft.thankYou?.title ?? "",
42766
+ placeholder: "Thank you",
42767
+ onChange: (e) => patch({ thankYou: { ...draft.thankYou, title: e.target.value || null } })
42768
+ }
42769
+ )
42770
+ ] }),
42771
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42772
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Then go to (optional)" }),
42773
+ /* @__PURE__ */ jsx133(
42774
+ BasicInput28,
42775
+ {
42776
+ type: "url",
42777
+ inputMode: "url",
42778
+ value: draft.thankYou?.redirectUrl ?? "",
42779
+ placeholder: "https://",
42780
+ onChange: (e) => patch({ thankYou: { ...draft.thankYou, redirectUrl: e.target.value || null } })
42781
+ }
42782
+ )
42783
+ ] }),
42784
+ /* @__PURE__ */ jsxs123("label", { className: "col-span-2 flex flex-col gap-1", children: [
42785
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Thank-you message" }),
42786
+ /* @__PURE__ */ jsx133(
42787
+ BasicTextarea11,
42788
+ {
42789
+ rows: 2,
42790
+ value: draft.thankYou?.body ?? "",
42791
+ placeholder: "What happens next, in your words.",
42792
+ onChange: (e) => patch({ thankYou: { ...draft.thankYou, body: e.target.value || null } })
42793
+ }
42794
+ )
42795
+ ] })
42796
+ ] })
42823
42797
  ] }),
42824
- /* @__PURE__ */ jsx134(HiddenFieldsEditor, { value: draft.hiddenFields ?? [], onChange: (hiddenFields) => patch({ hiddenFields }) })
42825
- ] }),
42826
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1.5", "data-records-form-look-editor": "", children: [
42827
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Look" }),
42828
- /* @__PURE__ */ jsx134(
42829
- AccentSwatches,
42798
+ /* @__PURE__ */ jsx133(WelcomeEditor, { value: draft.welcome, pictures: lookOptions?.pictures ?? null, onChange: (welcome) => patch({ welcome }) }),
42799
+ /* @__PURE__ */ jsx133(
42800
+ EndingsEditor,
42830
42801
  {
42831
- value: draft.look?.accent ?? "",
42832
- noneLabel: "Organization colour",
42833
- onChange: (accent) => patchLook({ accent: accent || null })
42802
+ endings: draft.endings ?? [],
42803
+ pictures: lookOptions?.pictures ?? null,
42804
+ fieldKeys: conditionFields,
42805
+ scored: draft.questions.some((q) => q.points),
42806
+ onChange: (endings) => patch({ endings })
42834
42807
  }
42835
42808
  ),
42836
- /* @__PURE__ */ jsx134(ThemeEditor, { theme: draft.theme, pictures: lookOptions?.pictures ?? null, onChange: (theme) => patch({ theme }) }),
42837
- lookOptions ? /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-2", children: [
42838
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1", children: [
42839
- /* @__PURE__ */ jsx134("span", { className: "text-xs", children: "Logo" }),
42840
- /* @__PURE__ */ jsx134(
42841
- PublicPicturePicker,
42809
+ /* @__PURE__ */ jsxs123("div", { className: "grid grid-cols-2 gap-2", children: [
42810
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42811
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Name" }),
42812
+ /* @__PURE__ */ jsx133(BasicInput28, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) })
42813
+ ] }),
42814
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42815
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Flow" }),
42816
+ /* @__PURE__ */ jsx133(
42817
+ "select",
42842
42818
  {
42843
- label: "Logo",
42844
- pictures: lookOptions.pictures,
42845
- value: draft.look?.logoFileId ?? "",
42846
- noneLabel: "Organization logo",
42847
- onChange: (id) => patchLook({ logoFileId: id || null })
42819
+ className: "h-9 rounded border bg-background px-2 text-sm",
42820
+ value: draft.flow ?? "one-at-a-time",
42821
+ onChange: (e) => patch({ flow: e.target.value }),
42822
+ children: FORM_FLOWS.map((flow) => /* @__PURE__ */ jsx133("option", { value: flow, children: flow === "one-at-a-time" ? "One question at a time" : "All on one page" }, flow))
42848
42823
  }
42849
42824
  )
42850
42825
  ] }),
42851
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1", children: [
42852
- /* @__PURE__ */ jsx134("span", { className: "text-xs", children: "Cover" }),
42853
- /* @__PURE__ */ jsx134(
42854
- PublicPicturePicker,
42826
+ /* @__PURE__ */ jsxs123("label", { className: "col-span-2 flex flex-col gap-1", children: [
42827
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Intro" }),
42828
+ /* @__PURE__ */ jsx133(
42829
+ BasicTextarea11,
42855
42830
  {
42856
- label: "Cover",
42857
- pictures: lookOptions.pictures,
42858
- value: draft.look?.coverFileId ?? "",
42859
- noneLabel: "No cover",
42860
- onChange: (id) => patchLook({ coverFileId: id || null })
42831
+ rows: 2,
42832
+ value: draft.intro ?? "",
42833
+ onChange: (e) => patch({ intro: e.target.value || null })
42861
42834
  }
42862
42835
  )
42863
- ] })
42864
- ] }) : lookUnread ? null : /* @__PURE__ */ jsx134(Skeleton30, { className: "h-12 w-full" }),
42865
- lookUnread ? /* @__PURE__ */ jsx134(RefusalLine, { error: lookUnread }) : null,
42866
- refused && isLookRefusal(refused) ? /* @__PURE__ */ jsx134(RefusalLine, { error: refused }) : null
42867
- ] }),
42868
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1", children: [
42869
- /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
42870
- /* @__PURE__ */ jsx134("span", { className: "font-medium text-foreground", children: "Questions" }),
42871
- /* @__PURE__ */ jsxs124("span", { children: [
42872
- draft.questions.length,
42873
- " of this table's ",
42874
- fields.data?.length ?? 0,
42875
- " fields"
42876
- ] })
42836
+ ] }),
42837
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42838
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Submit button" }),
42839
+ /* @__PURE__ */ jsx133(
42840
+ BasicInput28,
42841
+ {
42842
+ value: draft.submitLabel ?? "",
42843
+ placeholder: "Submit",
42844
+ onChange: (e) => patch({ submitLabel: e.target.value || null })
42845
+ }
42846
+ )
42847
+ ] }),
42848
+ /* @__PURE__ */ jsx133(HiddenFieldsEditor, { value: draft.hiddenFields ?? [], onChange: (hiddenFields) => patch({ hiddenFields }) })
42877
42849
  ] }),
42878
- askableFields(fields.data ?? []).map((field2) => {
42879
- const index = draft.questions.findIndex((q) => q.field === field2.key);
42880
- const asked3 = index >= 0;
42881
- const question = asked3 ? draft.questions[index] : null;
42882
- return /* @__PURE__ */ jsxs124("div", { className: "rounded border px-2 py-1.5", children: [
42883
- /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-2", children: [
42884
- /* @__PURE__ */ jsx134(
42885
- Checkbox15,
42850
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1.5", "data-records-form-look-editor": "", children: [
42851
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Look" }),
42852
+ /* @__PURE__ */ jsx133(
42853
+ AccentSwatches,
42854
+ {
42855
+ value: draft.look?.accent ?? "",
42856
+ noneLabel: "Organization colour",
42857
+ onChange: (accent) => patchLook({ accent: accent || null })
42858
+ }
42859
+ ),
42860
+ /* @__PURE__ */ jsx133(ThemeEditor, { theme: draft.theme, pictures: lookOptions?.pictures ?? null, onChange: (theme) => patch({ theme }) }),
42861
+ lookOptions ? /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-2", children: [
42862
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1", children: [
42863
+ /* @__PURE__ */ jsx133("span", { className: "text-xs", children: "Logo" }),
42864
+ /* @__PURE__ */ jsx133(
42865
+ PublicPicturePicker,
42886
42866
  {
42887
- checked: asked3,
42888
- "aria-label": `Ask for ${fieldName(field2)}`,
42889
- onCheckedChange: (checked) => patch({
42890
- questions: checked ? [...draft.questions, { field: field2.key }] : draft.questions.filter((q) => q.field !== field2.key)
42891
- })
42867
+ label: "Logo",
42868
+ pictures: lookOptions.pictures,
42869
+ value: draft.look?.logoFileId ?? "",
42870
+ noneLabel: "Organization logo",
42871
+ onChange: (id) => patchLook({ logoFileId: id || null })
42892
42872
  }
42893
- ),
42894
- /* @__PURE__ */ jsx134("span", { className: "text-xs font-medium", children: fieldName(field2) }),
42895
- field2.required ? /* @__PURE__ */ jsx134("span", { className: "text-xs text-destructive", children: "required" }) : null
42873
+ )
42896
42874
  ] }),
42897
- asked3 && question ? /* @__PURE__ */ jsxs124("div", { className: "mt-1.5 grid grid-cols-2 gap-2 pl-6", children: [
42898
- /* @__PURE__ */ jsx134(
42899
- BasicInput28,
42900
- {
42901
- className: "h-8 text-xs",
42902
- placeholder: fieldName(field2),
42903
- "aria-label": `How to ask for ${fieldName(field2)}`,
42904
- value: question.ask ?? "",
42905
- onChange: (e) => patchQuestion(index, { ask: e.target.value || null })
42906
- }
42907
- ),
42908
- /* @__PURE__ */ jsx134(
42909
- BasicInput28,
42910
- {
42911
- className: "h-8 text-xs",
42912
- placeholder: "Help text",
42913
- "aria-label": `Help text for ${fieldName(field2)}`,
42914
- value: question.help ?? "",
42915
- onChange: (e) => patchQuestion(index, { help: e.target.value || null })
42916
- }
42917
- ),
42918
- /* @__PURE__ */ jsxs124("label", { className: "col-span-2 flex items-center gap-2 text-xs", children: [
42919
- /* @__PURE__ */ jsx134(
42920
- Checkbox15,
42921
- {
42922
- checked: field2.required ? true : question.required === true,
42923
- disabled: field2.required === true,
42924
- "aria-label": `${fieldName(field2)} is required`,
42925
- onCheckedChange: (checked) => patchQuestion(index, { required: checked === true ? true : null })
42926
- }
42927
- ),
42928
- "Required"
42929
- ] }),
42930
- /* @__PURE__ */ jsx134(
42931
- Condition,
42932
- {
42933
- question,
42934
- fieldKeys: (fields.data ?? []).map((f) => ({
42935
- id: String(f.id),
42936
- key: f.key,
42937
- label: f.label || f.key,
42938
- field: f
42939
- })),
42940
- onChange: (showIf) => patchQuestion(index, { showIf })
42941
- }
42942
- ),
42943
- /* @__PURE__ */ jsx134(
42944
- QuestionLogic,
42875
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1", children: [
42876
+ /* @__PURE__ */ jsx133("span", { className: "text-xs", children: "Cover" }),
42877
+ /* @__PURE__ */ jsx133(
42878
+ PublicPicturePicker,
42945
42879
  {
42946
- question,
42947
- index,
42948
- questions: draft.questions,
42949
- endings: draft.endings ?? [],
42950
- field: field2,
42951
- fieldKeys: conditionFields,
42952
- label: (key) => {
42953
- const f = byKey.get(key);
42954
- return draft.questions.find((q) => q.field === key)?.ask || (f ? fieldName(f) : key);
42955
- },
42956
- onChange: (part) => patchQuestion(index, part)
42880
+ label: "Cover",
42881
+ pictures: lookOptions.pictures,
42882
+ value: draft.look?.coverFileId ?? "",
42883
+ noneLabel: "No cover",
42884
+ onChange: (id) => patchLook({ coverFileId: id || null })
42957
42885
  }
42958
42886
  )
42959
- ] }) : null
42960
- ] }, field2.id);
42961
- }),
42962
- whyNotAskable(fields.data ?? []) ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: whyNotAskable(fields.data ?? []) }) : null,
42963
- requiredLeftUnasked(fields.data ?? [], draft.questions.map((q) => q.field)) ? /* @__PURE__ */ jsx134(
42964
- ErrorBox7,
42965
- {
42966
- className: "rounded border border-destructive/40 bg-destructive/5 px-2 py-1.5 text-xs text-destructive",
42967
- message: requiredLeftUnasked(fields.data ?? [], draft.questions.map((q) => q.field)) ?? "",
42968
- operation: "Build a form",
42969
- origin: "records-ui/FormBuilder"
42970
- }
42971
- ) : null,
42972
- draft.questions.filter((q) => !byKey.has(q.field)).map((q) => /* @__PURE__ */ jsxs124("p", { className: "text-xs text-destructive", children: [
42973
- 'This form asks for "',
42974
- q.field,
42975
- '" and this table has no such Field. It is shown here rather than quietly dropped.'
42976
- ] }, q.field)),
42977
- adding ? /* @__PURE__ */ jsx134("div", { className: "rounded border p-2", "data-records-form-add-question": "", children: /* @__PURE__ */ jsx134(
42978
- FieldEditor,
42887
+ ] })
42888
+ ] }) : lookUnread ? null : /* @__PURE__ */ jsx133(Skeleton29, { className: "h-12 w-full" }),
42889
+ lookUnread ? /* @__PURE__ */ jsx133(RefusalLine, { error: lookUnread }) : null,
42890
+ refused && isLookRefusal(refused) ? /* @__PURE__ */ jsx133(RefusalLine, { error: refused }) : null
42891
+ ] }),
42892
+ /* @__PURE__ */ jsx133(
42893
+ QuestionList,
42979
42894
  {
42980
42895
  tableId,
42981
- onCancel: () => {
42982
- keysBeforeAdd.current = null;
42983
- setAdding(false);
42896
+ draft,
42897
+ fields: fields.data ?? [],
42898
+ byKey,
42899
+ adding,
42900
+ onAdding: (on) => {
42901
+ keysBeforeAdd.current = on ? new Set((fields.data ?? []).map((f) => f.key)) : null;
42902
+ setAdding(on);
42984
42903
  },
42985
- onSaved: () => setAdding(false)
42904
+ onAdded: () => setAdding(false),
42905
+ onQuestions: (questions) => patch({ questions }),
42906
+ onQuestion: patchQuestion
42986
42907
  }
42987
- ) }) : /* @__PURE__ */ jsx134("div", { children: /* @__PURE__ */ jsx134(
42988
- Button87,
42989
- {
42990
- size: "sm",
42991
- variant: "outline",
42992
- onClick: () => {
42993
- keysBeforeAdd.current = new Set((fields.data ?? []).map((f) => f.key));
42994
- setAdding(true);
42995
- },
42996
- children: "Add a question"
42997
- }
42998
- ) })
42908
+ )
42999
42909
  ] })
43000
- ] })
43001
- ] }),
43002
- draft ? /* @__PURE__ */ jsx134("aside", { className: "w-full shrink-0 overflow-auto rounded border p-2 @3xl:w-96", children: showResults && stored ? /* @__PURE__ */ jsx134(FormResultsPanel, { form: stored }) : /* @__PURE__ */ jsx134(FormLookFrame, { look: lookOptions?.look ?? null, title: draft.name, contained: true, children: /* @__PURE__ */ jsx134(FormRunner, { form: draft, preview: true }) }) }) : null
43003
- ] });
42910
+ ] }),
42911
+ draft ? (
42912
+ // BESIDE THE EDITOR, ALWAYS IN VIEW (TYPEFORM-2): it follows the scroll and updates as you type.
42913
+ /* @__PURE__ */ jsx133(
42914
+ "aside",
42915
+ {
42916
+ className: "w-full shrink-0 overflow-auto rounded border p-2 @3xl:sticky @3xl:top-2 @3xl:max-h-[calc(100dvh-6rem)] @3xl:w-[min(45%,520px)]",
42917
+ "data-records-form-preview": "",
42918
+ children: showResults && stored ? /* @__PURE__ */ jsx133(FormResultsPanel, { form: stored }) : /* @__PURE__ */ jsx133(FormLookFrame, { look: lookOptions?.look ?? null, title: draft.name, contained: true, showOwnerHeader: draft.showOwnerHeader ?? true, children: /* @__PURE__ */ jsx133(FormRunner, { form: draft, preview: true }) })
42919
+ }
42920
+ )
42921
+ ) : null
42922
+ ] }) })
42923
+ );
43004
42924
  }
43005
42925
  function PublishRow({
43006
42926
  url,
@@ -43015,10 +42935,10 @@ function PublishRow({
43015
42935
  unsaved = false
43016
42936
  }) {
43017
42937
  const open = state === "open";
43018
- return /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1.5 rounded border px-2.5 py-2", children: [
43019
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-wrap items-center gap-2", children: [
43020
- mayPublish ? /* @__PURE__ */ jsx134(
43021
- Button87,
42938
+ return /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1.5 rounded border px-2.5 py-2", children: [
42939
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-wrap items-center gap-2", children: [
42940
+ mayPublish ? /* @__PURE__ */ jsx133(
42941
+ Button86,
43022
42942
  {
43023
42943
  size: "sm",
43024
42944
  variant: open ? "ghost" : "default",
@@ -43027,8 +42947,8 @@ function PublishRow({
43027
42947
  children: busy ? "\u2026" : open ? "Unpublish" : unsaved ? "Save and publish" : "Publish"
43028
42948
  }
43029
42949
  ) : null,
43030
- state !== "draft" ? /* @__PURE__ */ jsxs124(Fragment54, { children: [
43031
- /* @__PURE__ */ jsx134(
42950
+ state !== "draft" ? /* @__PURE__ */ jsxs123(Fragment54, { children: [
42951
+ /* @__PURE__ */ jsx133(
43032
42952
  "a",
43033
42953
  {
43034
42954
  href: url,
@@ -43038,14 +42958,14 @@ function PublishRow({
43038
42958
  children: url
43039
42959
  }
43040
42960
  ),
43041
- /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: "outline", onClick: () => onCopy(url), children: copied ? "Copied" : "Copy link" })
42961
+ /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "outline", onClick: () => onCopy(url), children: copied ? "Copied" : "Copy link" })
43042
42962
  ] }) : null
43043
42963
  ] }),
43044
- /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: FORM_STATE_WORDS[state] }),
43045
- open && unsaved ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", "data-records-form-unsaved": "", children: "Unsaved changes \u2014 the live form shows your last save" }) : null,
43046
- !mayPublish ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: why }) : null,
43047
- shownUrl ? /* @__PURE__ */ jsxs124("p", { className: "break-all rounded border border-dashed px-2 py-1 text-xs", children: [
43048
- "This browser would not let the page copy for you, so here it is to copy by hand: ",
42964
+ /* @__PURE__ */ jsx133("p", { className: "w-fit text-xs text-muted-foreground", title: FORM_STATE_WORDS[state], "data-records-form-state": state, children: FORM_STATE_LABELS[state] }),
42965
+ open && unsaved ? /* @__PURE__ */ jsx133("p", { className: "text-xs text-muted-foreground", "data-records-form-unsaved": "", children: "Unsaved changes \u2014 the live form shows your last save" }) : null,
42966
+ !mayPublish ? /* @__PURE__ */ jsx133("p", { className: "w-fit text-xs text-muted-foreground", title: why, children: "Publishing needs table admin" }) : null,
42967
+ shownUrl ? /* @__PURE__ */ jsxs123("p", { className: "break-all rounded border border-dashed px-2 py-1 text-xs", title: "This browser would not copy for you.", children: [
42968
+ "Copy by hand: ",
43049
42969
  shownUrl
43050
42970
  ] }) : null
43051
42971
  ] });
@@ -43055,7 +42975,7 @@ function Condition({
43055
42975
  fieldKeys,
43056
42976
  onChange
43057
42977
  }) {
43058
- return /* @__PURE__ */ jsx134(
42978
+ return /* @__PURE__ */ jsx133(
43059
42979
  ConditionGroup,
43060
42980
  {
43061
42981
  lead: "Ask only when",
@@ -43066,26 +42986,326 @@ function Condition({
43066
42986
  }
43067
42987
  );
43068
42988
  }
42989
+ var FORM_STATE_LABELS = {
42990
+ draft: "Not published",
42991
+ open: "Open",
42992
+ closed: "Closed",
42993
+ full: "Full"
42994
+ };
42995
+ function QuestionList({
42996
+ tableId,
42997
+ draft,
42998
+ fields,
42999
+ byKey,
43000
+ adding,
43001
+ onAdding,
43002
+ onAdded,
43003
+ onQuestions,
43004
+ onQuestion
43005
+ }) {
43006
+ const sensors = useSensors(
43007
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
43008
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
43009
+ );
43010
+ const askable = askableFields([...fields]);
43011
+ const unasked = askable.filter((f) => !draft.questions.some((q) => q.field === f.key));
43012
+ const asked3 = draft.questions.map((q) => byKey.get(q.field)).filter((f) => Boolean(f)).map((f) => ({ id: String(f.id), key: f.key, label: draft.questions.find((q) => q.field === f.key)?.ask || fieldName(f), field: f }));
43013
+ const label = (key) => {
43014
+ const f = byKey.get(key);
43015
+ return draft.questions.find((q) => q.field === key)?.ask || (f ? fieldName(f) : key);
43016
+ };
43017
+ function dragEnd(event) {
43018
+ const from = draft.questions.findIndex((q) => q.field === event.active.id);
43019
+ const to = event.over ? draft.questions.findIndex((q) => q.field === event.over.id) : -1;
43020
+ if (from < 0 || to < 0 || from === to) return;
43021
+ onQuestions(arrayMove(draft.questions, from, to));
43022
+ }
43023
+ const notAskable = whyNotAskable([...fields]);
43024
+ const leftUnasked = requiredLeftUnasked([...fields], draft.questions.map((q) => q.field));
43025
+ return /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1.5", "data-records-form-questions": "", children: [
43026
+ /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43027
+ /* @__PURE__ */ jsx133("span", { className: "font-medium text-foreground", children: "Questions" }),
43028
+ /* @__PURE__ */ jsx133("span", { className: "tabular-nums", children: draft.questions.length })
43029
+ ] }),
43030
+ /* @__PURE__ */ jsx133(DndContext, { sensors, collisionDetection: closestCenter, onDragEnd: dragEnd, children: /* @__PURE__ */ jsx133(SortableContext, { items: draft.questions.map((q) => q.field), strategy: verticalListSortingStrategy, children: draft.questions.map((question, index) => /* @__PURE__ */ jsx133(
43031
+ QuestionRow,
43032
+ {
43033
+ index,
43034
+ question,
43035
+ field: byKey.get(question.field) ?? null,
43036
+ draft,
43037
+ conditionFields: asked3.filter((f) => f.key !== question.field),
43038
+ label,
43039
+ onChange: (change) => onQuestion(index, change),
43040
+ onRemove: () => onQuestions(draft.questions.filter((q) => q.field !== question.field))
43041
+ },
43042
+ question.field
43043
+ )) }) }),
43044
+ unasked.length > 0 ? /* @__PURE__ */ jsx133("div", { className: "flex flex-wrap items-center gap-1", "data-records-form-unasked": "", children: unasked.map((f) => /* @__PURE__ */ jsxs123(
43045
+ Button86,
43046
+ {
43047
+ size: "sm",
43048
+ variant: "outline",
43049
+ "aria-label": `Ask for ${fieldName(f)}`,
43050
+ onClick: () => onQuestions([...draft.questions, { field: f.key }]),
43051
+ children: [
43052
+ "+ ",
43053
+ fieldName(f)
43054
+ ]
43055
+ },
43056
+ f.id
43057
+ )) }) : null,
43058
+ notAskable ? /* @__PURE__ */ jsx133("p", { className: "w-fit text-xs text-muted-foreground", title: notAskable, children: "Some fields cannot be asked publicly" }) : null,
43059
+ leftUnasked ? /* @__PURE__ */ jsx133(
43060
+ ErrorBox7,
43061
+ {
43062
+ className: "rounded border border-destructive/40 bg-destructive/5 px-2 py-1.5 text-xs text-destructive",
43063
+ message: leftUnasked,
43064
+ operation: "Build a form",
43065
+ origin: "records-ui/FormBuilder"
43066
+ }
43067
+ ) : null,
43068
+ adding ? /* @__PURE__ */ jsx133("div", { className: "rounded border p-2", "data-records-form-add-question": "", children: /* @__PURE__ */ jsx133(FieldEditor, { tableId, onCancel: () => onAdding(false), onSaved: onAdded }) }) : /* @__PURE__ */ jsx133("div", { children: /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "outline", onClick: () => onAdding(true), children: "Add a question" }) })
43069
+ ] });
43070
+ }
43071
+ function QuestionRow({
43072
+ index,
43073
+ question,
43074
+ field: field2,
43075
+ draft,
43076
+ conditionFields,
43077
+ label,
43078
+ onChange,
43079
+ onRemove
43080
+ }) {
43081
+ const sortable = useSortable({ id: question.field });
43082
+ const [open, setOpen] = useState110(true);
43083
+ const name = field2 ? fieldName(field2) : question.field;
43084
+ return /* @__PURE__ */ jsxs123(
43085
+ "div",
43086
+ {
43087
+ ref: sortable.setNodeRef,
43088
+ style: { transform: CSS2.Transform.toString(sortable.transform), transition: sortable.transition },
43089
+ className: cn78("rounded border bg-background px-2 py-1.5", sortable.isDragging && "z-10 shadow-md"),
43090
+ "data-records-form-question-row": question.field,
43091
+ children: [
43092
+ /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-2", children: [
43093
+ /* @__PURE__ */ jsx133(
43094
+ "button",
43095
+ {
43096
+ type: "button",
43097
+ className: "cursor-grab touch-none rounded px-1 text-muted-foreground hover:bg-muted active:cursor-grabbing",
43098
+ "aria-label": `Move ${name}`,
43099
+ title: "Drag to reorder",
43100
+ "data-records-form-drag": "",
43101
+ ...sortable.attributes,
43102
+ ...sortable.listeners,
43103
+ children: "\u22EE\u22EE"
43104
+ }
43105
+ ),
43106
+ /* @__PURE__ */ jsx133("span", { className: "w-5 text-right text-xs tabular-nums text-muted-foreground", children: index + 1 }),
43107
+ /* @__PURE__ */ jsx133("button", { type: "button", className: "min-w-0 flex-1 truncate text-left text-xs font-medium", onClick: () => setOpen((v) => !v), "aria-expanded": open, children: question.ask?.trim() || name }),
43108
+ field2?.required || question.required ? /* @__PURE__ */ jsx133("span", { className: "text-xs text-foreground/80", children: "required" }) : null,
43109
+ !field2 ? /* @__PURE__ */ jsx133("span", { className: "rounded bg-destructive px-1.5 text-[11px] text-destructive-foreground", title: "The table has no such field.", children: "Missing field" }) : null,
43110
+ /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: onRemove, "aria-label": `Stop asking ${name}`, children: "Remove" })
43111
+ ] }),
43112
+ open && field2 ? /* @__PURE__ */ jsxs123("div", { className: "mt-1.5 grid grid-cols-2 gap-2 pl-7", children: [
43113
+ /* @__PURE__ */ jsx133(
43114
+ BasicInput28,
43115
+ {
43116
+ className: "h-8 text-xs",
43117
+ placeholder: name,
43118
+ "aria-label": `How to ask for ${name}`,
43119
+ title: "Use {{question}} to recall an earlier answer",
43120
+ value: question.ask ?? "",
43121
+ onChange: (e) => onChange({ ask: e.target.value || null })
43122
+ }
43123
+ ),
43124
+ /* @__PURE__ */ jsx133(
43125
+ BasicInput28,
43126
+ {
43127
+ className: "h-8 text-xs",
43128
+ placeholder: "Help text",
43129
+ "aria-label": `Help text for ${name}`,
43130
+ value: question.help ?? "",
43131
+ onChange: (e) => onChange({ help: e.target.value || null })
43132
+ }
43133
+ ),
43134
+ /* @__PURE__ */ jsxs123("label", { className: "col-span-2 flex items-center gap-2 text-xs", children: [
43135
+ /* @__PURE__ */ jsx133(
43136
+ Checkbox15,
43137
+ {
43138
+ checked: field2.required ? true : question.required === true,
43139
+ disabled: field2.required === true,
43140
+ "aria-label": `${name} is required`,
43141
+ onCheckedChange: (checked) => onChange({ required: checked === true ? true : null })
43142
+ }
43143
+ ),
43144
+ "Required"
43145
+ ] }),
43146
+ /* @__PURE__ */ jsx133(Condition, { question, fieldKeys: conditionFields, onChange: (showIf) => onChange({ showIf }) }),
43147
+ /* @__PURE__ */ jsx133(
43148
+ QuestionLogic,
43149
+ {
43150
+ question,
43151
+ index,
43152
+ questions: draft.questions,
43153
+ endings: draft.endings ?? [],
43154
+ field: field2,
43155
+ fieldKeys: conditionFields,
43156
+ label,
43157
+ onChange
43158
+ }
43159
+ )
43160
+ ] }) : null
43161
+ ]
43162
+ }
43163
+ );
43164
+ }
43165
+ function NotifyOnAnswer({
43166
+ tableId,
43167
+ tableName: tableName2,
43168
+ formName,
43169
+ ruleId,
43170
+ notify,
43171
+ onRule,
43172
+ onNotify
43173
+ }) {
43174
+ const client = useRecordsClient79();
43175
+ const [subs, setSubs] = useState110(null);
43176
+ const [busy, setBusy] = useState110(false);
43177
+ const [error, setError] = useState110(null);
43178
+ const [said2, setSaid] = useState110(null);
43179
+ const [url, setUrl] = useState110(notify?.webhookUrl ?? "");
43180
+ useEffect96(() => setUrl(notify?.webhookUrl ?? ""), [notify?.webhookUrl]);
43181
+ useEffect96(() => {
43182
+ let cancelled = false;
43183
+ void client.subscriptions({ table_id: tableId }).then((r) => {
43184
+ if (!cancelled && r.ok) setSubs(r.data);
43185
+ });
43186
+ return () => {
43187
+ cancelled = true;
43188
+ };
43189
+ }, [client, tableId, ruleId]);
43190
+ const chosen = subs?.find((s) => s.rule_id === ruleId) ?? null;
43191
+ async function emailMe(on) {
43192
+ setBusy(true);
43193
+ setError(null);
43194
+ try {
43195
+ if (!on) {
43196
+ onRule(null);
43197
+ if (ruleId) await client.recordDelete({ record_id: ruleId });
43198
+ return;
43199
+ }
43200
+ const listed = await client.views({ table_id: tableId });
43201
+ if (!listed.ok) return setError(listed.error);
43202
+ let viewId = listed.data.find((v) => !v.filters || Object.keys(v.filters).length === 0)?.view_id ?? null;
43203
+ if (!viewId) {
43204
+ const made = await client.viewDeclare({ table_id: tableId, spec: { name: `Every ${tableName2}`, filters: null } });
43205
+ if (!made.ok) return setError(made.error);
43206
+ viewId = made.data;
43207
+ }
43208
+ const declared2 = await client.subscriptionDeclare({
43209
+ table_id: tableId,
43210
+ spec: {
43211
+ name: `${formName} \u2014 new answers`,
43212
+ saved_view_id: viewId,
43213
+ cadence: "instant",
43214
+ schedule: null,
43215
+ channel: "email",
43216
+ quiet_hours: null,
43217
+ recipient_user_id: client.config.actor.user_id ?? null,
43218
+ event_key: "records.changed"
43219
+ }
43220
+ });
43221
+ if (!declared2.ok) return setError(declared2.error);
43222
+ onRule(declared2.data);
43223
+ } finally {
43224
+ setBusy(false);
43225
+ }
43226
+ }
43227
+ async function saveWebhook() {
43228
+ const next = url.trim();
43229
+ if (next === (notify?.webhookUrl ?? "")) return;
43230
+ setBusy(true);
43231
+ setError(null);
43232
+ setSaid(null);
43233
+ try {
43234
+ if (next === "") {
43235
+ if (notify?.webhookAutomationId) await client.automationArchive({ automation_id: notify.webhookAutomationId });
43236
+ onNotify(null);
43237
+ return;
43238
+ }
43239
+ const declared2 = await client.automationDeclare({
43240
+ table_id: tableId,
43241
+ automation_id: notify?.webhookAutomationId ?? null,
43242
+ spec: { name: `${formName} \u2014 webhook`, trigger: { on: "form_answered" }, actions: [{ do: "webhook", url: next }] }
43243
+ });
43244
+ if (!declared2.ok) return setError(declared2.error);
43245
+ if (!declared2.data.ok) {
43246
+ setSaid(Object.values(declared2.data._errors)[0] ?? "Not saved");
43247
+ return;
43248
+ }
43249
+ onNotify({ webhookUrl: next, webhookAutomationId: declared2.data.automation_id });
43250
+ } finally {
43251
+ setBusy(false);
43252
+ }
43253
+ }
43254
+ return /* @__PURE__ */ jsxs123("section", { className: "flex flex-col gap-2 rounded border px-2.5 py-2 text-xs", "aria-label": "Notify on answer", "data-records-form-notify": "", children: [
43255
+ /* @__PURE__ */ jsx133("span", { className: "font-medium", children: "Notify on answer" }),
43256
+ /* @__PURE__ */ jsx133("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1", children: /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", title: "An email to you each time somebody answers.", children: [
43257
+ /* @__PURE__ */ jsx133(Checkbox15, { checked: Boolean(ruleId), disabled: busy, onCheckedChange: (c) => void emailMe(c === true), "aria-label": "Email me" }),
43258
+ "Email me",
43259
+ chosen && chosen.channel !== "email" ? /* @__PURE__ */ jsxs123("span", { className: "text-muted-foreground", children: [
43260
+ "(",
43261
+ chosen.channel === "sms" ? "text" : "in app",
43262
+ ")"
43263
+ ] }) : null
43264
+ ] }) }),
43265
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", children: [
43266
+ /* @__PURE__ */ jsx133("span", { className: "shrink-0", children: "Webhook" }),
43267
+ /* @__PURE__ */ jsx133(
43268
+ BasicInput28,
43269
+ {
43270
+ type: "url",
43271
+ inputMode: "url",
43272
+ className: "h-8 text-xs",
43273
+ placeholder: "https://",
43274
+ "aria-label": "Webhook URL",
43275
+ value: url,
43276
+ disabled: busy,
43277
+ onChange: (e) => setUrl(e.target.value),
43278
+ onBlur: () => void saveWebhook(),
43279
+ onKeyDown: (e) => {
43280
+ if (e.key === "Enter") void saveWebhook();
43281
+ }
43282
+ }
43283
+ )
43284
+ ] }),
43285
+ said2 ? /* @__PURE__ */ jsx133("p", { className: "text-destructive", children: said2 }) : null,
43286
+ error ? /* @__PURE__ */ jsx133(RefusalLine, { error }) : null
43287
+ ] });
43288
+ }
43069
43289
 
43070
43290
  // src/DocTemplate.tsx
43071
- import { useCallback as useCallback56, useEffect as useEffect98, useState as useState112 } from "react";
43072
- import { useFields as useFields26, useRecordsClient as useRecordsClient81, useTable as useTable28 } from "@ai-matrx/records/react";
43073
- import { BasicInput as BasicInput29, BasicTextarea as BasicTextarea12, Button as Button88, Label as Label23, Skeleton as Skeleton31, cn as cn80 } from "@ai-matrx/design-system";
43074
- import { jsx as jsx135, jsxs as jsxs125 } from "react/jsx-runtime";
43291
+ import { useCallback as useCallback55, useEffect as useEffect97, useState as useState111 } from "react";
43292
+ import { useFields as useFields26, useRecordsClient as useRecordsClient80, useTable as useTable27 } from "@ai-matrx/records/react";
43293
+ import { BasicInput as BasicInput29, BasicTextarea as BasicTextarea12, Button as Button87, Label as Label24, Skeleton as Skeleton30, cn as cn79 } from "@ai-matrx/design-system";
43294
+ import { jsx as jsx134, jsxs as jsxs124 } from "react/jsx-runtime";
43075
43295
  function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, className }) {
43076
- const client = useRecordsClient81();
43296
+ const client = useRecordsClient80();
43077
43297
  const claimSeed = useSeedGuard();
43078
- const table = useTable28(tableId);
43298
+ const table = useTable27(tableId);
43079
43299
  const rights = useTableRights(table.data);
43080
43300
  const fields = useFields26(tableId);
43081
- const [templates, setTemplates] = useState112(null);
43082
- const [error, setError] = useState112(null);
43083
- const [activeId, setActiveId] = useState112(activeTemplateId ?? null);
43084
- const [draftName, setDraftName] = useState112("");
43085
- const [draftBody, setDraftBody] = useState112("");
43086
- const [unresolved, setUnresolved] = useState112([]);
43087
- const [saving, setSaving] = useState112(false);
43088
- const load = useCallback56(async () => {
43301
+ const [templates, setTemplates] = useState111(null);
43302
+ const [error, setError] = useState111(null);
43303
+ const [activeId, setActiveId] = useState111(activeTemplateId ?? null);
43304
+ const [draftName, setDraftName] = useState111("");
43305
+ const [draftBody, setDraftBody] = useState111("");
43306
+ const [unresolved, setUnresolved] = useState111([]);
43307
+ const [saving, setSaving] = useState111(false);
43308
+ const load = useCallback55(async () => {
43089
43309
  const held = await client.docTemplates({ table_id: tableId });
43090
43310
  if (!held.ok) {
43091
43311
  setError(held.error);
@@ -43116,10 +43336,10 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43116
43336
  setError(null);
43117
43337
  setTemplates(rows);
43118
43338
  }, [client, tableId, seed, fields.data]);
43119
- useEffect98(() => {
43339
+ useEffect97(() => {
43120
43340
  void load();
43121
43341
  }, [load]);
43122
- useEffect98(() => {
43342
+ useEffect97(() => {
43123
43343
  if (!templates || templates.length === 0) return;
43124
43344
  const chosen = templates.find((t) => t.id === (activeTemplateId ?? activeId)) ?? templates[0];
43125
43345
  if (chosen.id !== activeId) setActiveId(chosen.id);
@@ -43127,7 +43347,7 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43127
43347
  setDraftBody(chosen.body);
43128
43348
  onActiveTemplate?.(chosen);
43129
43349
  }, [templates, activeTemplateId]);
43130
- useEffect98(() => {
43350
+ useEffect97(() => {
43131
43351
  let cancelled = false;
43132
43352
  if (draftBody.trim() === "") {
43133
43353
  setUnresolved([]);
@@ -43157,25 +43377,25 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43157
43377
  setActiveId(saved.data);
43158
43378
  await load();
43159
43379
  }
43160
- if (fields.error) return /* @__PURE__ */ jsx135(RefusalNotice, { error: fields.error, className });
43380
+ if (fields.error) return /* @__PURE__ */ jsx134(RefusalNotice, { error: fields.error, className });
43161
43381
  if (error) {
43162
- return /* @__PURE__ */ jsx135(
43382
+ return /* @__PURE__ */ jsx134(
43163
43383
  RefusalNotice,
43164
43384
  {
43165
43385
  error,
43166
43386
  className,
43167
- actions: /* @__PURE__ */ jsx135(Button88, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43387
+ actions: /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43168
43388
  }
43169
43389
  );
43170
43390
  }
43171
- if (templates === null || fields.loading && !fields.data) return /* @__PURE__ */ jsx135(Skeleton31, { className: cn80("h-64 w-full", className) });
43391
+ if (templates === null || fields.loading && !fields.data) return /* @__PURE__ */ jsx134(Skeleton30, { className: cn79("h-64 w-full", className) });
43172
43392
  if (!rights.admin) {
43173
- return /* @__PURE__ */ jsx135("p", { className: cn80("text-xs text-muted-foreground", className), children: rights.why("structure") });
43393
+ return /* @__PURE__ */ jsx134("p", { className: cn79("text-xs text-muted-foreground", className), children: rights.why("structure") });
43174
43394
  }
43175
- return /* @__PURE__ */ jsxs125("div", { className: cn80("flex flex-col gap-2", className), children: [
43176
- /* @__PURE__ */ jsxs125("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Templates", children: [
43177
- templates.map((t) => /* @__PURE__ */ jsxs125(
43178
- Button88,
43395
+ return /* @__PURE__ */ jsxs124("div", { className: cn79("flex flex-col gap-2", className), children: [
43396
+ /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Templates", children: [
43397
+ templates.map((t) => /* @__PURE__ */ jsxs124(
43398
+ Button87,
43179
43399
  {
43180
43400
  size: "sm",
43181
43401
  variant: t.id === activeId ? "secondary" : "ghost",
@@ -43187,7 +43407,7 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43187
43407
  },
43188
43408
  children: [
43189
43409
  t.name,
43190
- /* @__PURE__ */ jsxs125("span", { className: "ml-1 text-[10px] opacity-70", children: [
43410
+ /* @__PURE__ */ jsxs124("span", { className: "ml-1 text-[10px] opacity-70", children: [
43191
43411
  "v",
43192
43412
  t.template_version
43193
43413
  ] })
@@ -43195,8 +43415,8 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43195
43415
  },
43196
43416
  t.id
43197
43417
  )),
43198
- /* @__PURE__ */ jsx135(
43199
- Button88,
43418
+ /* @__PURE__ */ jsx134(
43419
+ Button87,
43200
43420
  {
43201
43421
  size: "sm",
43202
43422
  variant: "ghost",
@@ -43208,16 +43428,16 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43208
43428
  children: "New template"
43209
43429
  }
43210
43430
  ),
43211
- /* @__PURE__ */ jsx135(Button88, { size: "sm", className: "ml-auto", disabled: saving, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
43431
+ /* @__PURE__ */ jsx134(Button87, { size: "sm", className: "ml-auto", disabled: saving, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
43212
43432
  ] }),
43213
- /* @__PURE__ */ jsxs125("label", { className: "flex flex-col gap-1", children: [
43214
- /* @__PURE__ */ jsx135(Label23, { className: "text-xs font-medium", children: "Name" }),
43215
- /* @__PURE__ */ jsx135(BasicInput29, { value: draftName, onChange: (e) => setDraftName(e.target.value), placeholder: "Agreement" })
43433
+ /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
43434
+ /* @__PURE__ */ jsx134(Label24, { className: "text-xs font-medium", children: "Name" }),
43435
+ /* @__PURE__ */ jsx134(BasicInput29, { value: draftName, onChange: (e) => setDraftName(e.target.value), placeholder: "Agreement" })
43216
43436
  ] }),
43217
- /* @__PURE__ */ jsxs125("div", { className: "flex flex-wrap items-center gap-1", children: [
43218
- /* @__PURE__ */ jsx135("span", { className: "text-xs text-muted-foreground", children: "Insert" }),
43219
- (fields.data ?? []).map((field2) => /* @__PURE__ */ jsx135(
43220
- Button88,
43437
+ /* @__PURE__ */ jsxs124("div", { className: "flex flex-wrap items-center gap-1", children: [
43438
+ /* @__PURE__ */ jsx134("span", { className: "text-xs text-muted-foreground", children: "Insert" }),
43439
+ (fields.data ?? []).map((field2) => /* @__PURE__ */ jsx134(
43440
+ Button87,
43221
43441
  {
43222
43442
  size: "sm",
43223
43443
  variant: "ghost",
@@ -43228,7 +43448,7 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43228
43448
  field2.id
43229
43449
  ))
43230
43450
  ] }),
43231
- /* @__PURE__ */ jsx135(
43451
+ /* @__PURE__ */ jsx134(
43232
43452
  BasicTextarea12,
43233
43453
  {
43234
43454
  rows: 10,
@@ -43238,33 +43458,33 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43238
43458
  onChange: (e) => setDraftBody(e.target.value)
43239
43459
  }
43240
43460
  ),
43241
- unresolved.length > 0 ? /* @__PURE__ */ jsx135("ul", { className: "flex flex-col gap-0.5 rounded border border-dashed px-2 py-1 text-xs", children: unresolved.map((token) => /* @__PURE__ */ jsxs125("li", { className: "flex items-baseline gap-1", children: [
43242
- /* @__PURE__ */ jsx135("code", { className: "text-destructive", children: token.raw }),
43243
- /* @__PURE__ */ jsx135("span", { "aria-hidden": "true", children: "\u2014" }),
43244
- /* @__PURE__ */ jsx135(
43461
+ unresolved.length > 0 ? /* @__PURE__ */ jsx134("ul", { className: "flex flex-col gap-0.5 rounded border border-dashed px-2 py-1 text-xs", children: unresolved.map((token) => /* @__PURE__ */ jsxs124("li", { className: "flex items-baseline gap-1", children: [
43462
+ /* @__PURE__ */ jsx134("code", { className: "text-destructive", children: token.raw }),
43463
+ /* @__PURE__ */ jsx134("span", { "aria-hidden": "true", children: "\u2014" }),
43464
+ /* @__PURE__ */ jsx134(
43245
43465
  RefusalLine,
43246
43466
  {
43247
43467
  error: refusal("invalid_argument", token.why, "Change it to a Field this table has, or take it out.")
43248
43468
  }
43249
43469
  )
43250
- ] }, token.raw)) }) : draftBody.trim() !== "" ? /* @__PURE__ */ jsx135("p", { className: "text-xs text-muted-foreground", children: "Every token in this body points at a Field this table can answer." }) : null
43470
+ ] }, token.raw)) }) : draftBody.trim() !== "" ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: "Every token in this body points at a Field this table can answer." }) : null
43251
43471
  ] });
43252
43472
  }
43253
43473
 
43254
43474
  // src/DocRender.tsx
43255
- import { useCallback as useCallback57, useEffect as useEffect99, useRef as useRef57, useState as useState113 } from "react";
43256
- import { useRecordsClient as useRecordsClient82 } from "@ai-matrx/records/react";
43257
- import { Button as Button89, Skeleton as Skeleton32, cn as cn81 } from "@ai-matrx/design-system";
43258
- import { jsx as jsx136, jsxs as jsxs126 } from "react/jsx-runtime";
43475
+ import { useCallback as useCallback56, useEffect as useEffect98, useRef as useRef57, useState as useState112 } from "react";
43476
+ import { useRecordsClient as useRecordsClient81 } from "@ai-matrx/records/react";
43477
+ import { Button as Button88, Skeleton as Skeleton31, cn as cn80 } from "@ai-matrx/design-system";
43478
+ import { jsx as jsx135, jsxs as jsxs125 } from "react/jsx-runtime";
43259
43479
  function DocRender({ templateId, recordId, filename, onRendered, className }) {
43260
- const client = useRecordsClient82();
43261
- const [preview, setPreview] = useState113(null);
43262
- const [renders, setRenders] = useState113(null);
43263
- const [showing, setShowing] = useState113(null);
43264
- const [error, setError] = useState113(null);
43265
- const [busy, setBusy] = useState113(null);
43480
+ const client = useRecordsClient81();
43481
+ const [preview, setPreview] = useState112(null);
43482
+ const [renders, setRenders] = useState112(null);
43483
+ const [showing, setShowing] = useState112(null);
43484
+ const [error, setError] = useState112(null);
43485
+ const [busy, setBusy] = useState112(null);
43266
43486
  const paper = useRef57(null);
43267
- const load = useCallback57(async () => {
43487
+ const load = useCallback56(async () => {
43268
43488
  const [body, held] = await Promise.all([
43269
43489
  client.docRenderBody({ template_id: templateId, record_id: recordId }),
43270
43490
  client.docRenders({ record_id: recordId })
@@ -43281,7 +43501,7 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43281
43501
  setPreview(body.data);
43282
43502
  setRenders(held.data.filter((r) => r.template_id === templateId));
43283
43503
  }, [client, templateId, recordId]);
43284
- useEffect99(() => {
43504
+ useEffect98(() => {
43285
43505
  void load();
43286
43506
  }, [load]);
43287
43507
  async function freeze() {
@@ -43321,22 +43541,22 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43321
43541
  }
43322
43542
  }
43323
43543
  if (error) {
43324
- return /* @__PURE__ */ jsx136(
43544
+ return /* @__PURE__ */ jsx135(
43325
43545
  RefusalNotice,
43326
43546
  {
43327
43547
  error,
43328
43548
  className,
43329
- actions: /* @__PURE__ */ jsx136(Button89, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43549
+ actions: /* @__PURE__ */ jsx135(Button88, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43330
43550
  }
43331
43551
  );
43332
43552
  }
43333
- if (preview === null || renders === null) return /* @__PURE__ */ jsx136(Skeleton32, { className: cn81("h-64 w-full", className) });
43553
+ if (preview === null || renders === null) return /* @__PURE__ */ jsx135(Skeleton31, { className: cn80("h-64 w-full", className) });
43334
43554
  const frozen = showing ? renders.find((r) => r.id === showing) ?? null : null;
43335
43555
  const text2 = frozen ? frozen.body : preview;
43336
- return /* @__PURE__ */ jsxs126("div", { className: cn81("flex flex-col gap-2", className), children: [
43337
- /* @__PURE__ */ jsxs126("div", { className: "flex items-center gap-1 text-xs", children: [
43338
- /* @__PURE__ */ jsx136(
43339
- Button89,
43556
+ return /* @__PURE__ */ jsxs125("div", { className: cn80("flex flex-col gap-2", className), children: [
43557
+ /* @__PURE__ */ jsxs125("div", { className: "flex items-center gap-1 text-xs", children: [
43558
+ /* @__PURE__ */ jsx135(
43559
+ Button88,
43340
43560
  {
43341
43561
  size: "sm",
43342
43562
  variant: frozen ? "ghost" : "secondary",
@@ -43345,8 +43565,8 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43345
43565
  children: "As it reads now"
43346
43566
  }
43347
43567
  ),
43348
- renders.map((render) => /* @__PURE__ */ jsx136(
43349
- Button89,
43568
+ renders.map((render) => /* @__PURE__ */ jsx135(
43569
+ Button88,
43350
43570
  {
43351
43571
  size: "sm",
43352
43572
  variant: render.id === showing ? "secondary" : "ghost",
@@ -43357,33 +43577,33 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43357
43577
  },
43358
43578
  render.id
43359
43579
  )),
43360
- /* @__PURE__ */ jsxs126("span", { className: "ml-auto flex items-center gap-1", children: [
43361
- /* @__PURE__ */ jsx136(Button89, { size: "sm", disabled: busy !== null, onClick: () => void freeze(), children: busy === "freeze" ? "Freezing\u2026" : "Freeze this version" }),
43362
- /* @__PURE__ */ jsx136(Button89, { size: "sm", variant: "ghost", disabled: busy !== null, onClick: () => void toPdf(), children: busy === "pdf" ? "Making the PDF\u2026" : "PDF" })
43580
+ /* @__PURE__ */ jsxs125("span", { className: "ml-auto flex items-center gap-1", children: [
43581
+ /* @__PURE__ */ jsx135(Button88, { size: "sm", disabled: busy !== null, onClick: () => void freeze(), children: busy === "freeze" ? "Freezing\u2026" : "Freeze this version" }),
43582
+ /* @__PURE__ */ jsx135(Button88, { size: "sm", variant: "ghost", disabled: busy !== null, onClick: () => void toPdf(), children: busy === "pdf" ? "Making the PDF\u2026" : "PDF" })
43363
43583
  ] })
43364
43584
  ] }),
43365
- /* @__PURE__ */ jsx136("div", { ref: paper, className: "whitespace-pre-wrap rounded border bg-background p-4 text-sm text-foreground", children: text2 }),
43366
- /* @__PURE__ */ jsx136("p", { className: "text-xs text-muted-foreground", children: frozen ? `Frozen ${new Date(frozen.rendered_at).toLocaleString()} from template v${frozen.template_version}. Its text and its SHA-256 (${frozen.content_hash.slice(0, 12)}\u2026) are what any signature seals.` : "This is how the template reads against this record right now. Nothing has been frozen, so there is no version for anyone to sign yet." })
43585
+ /* @__PURE__ */ jsx135("div", { ref: paper, className: "whitespace-pre-wrap rounded border bg-background p-4 text-sm text-foreground", children: text2 }),
43586
+ /* @__PURE__ */ jsx135("p", { className: "text-xs text-muted-foreground", children: frozen ? `Frozen ${new Date(frozen.rendered_at).toLocaleString()} from template v${frozen.template_version}. Its text and its SHA-256 (${frozen.content_hash.slice(0, 12)}\u2026) are what any signature seals.` : "This is how the template reads against this record right now. Nothing has been frozen, so there is no version for anyone to sign yet." })
43367
43587
  ] });
43368
43588
  }
43369
43589
 
43370
43590
  // src/SignBlock.tsx
43371
- import { useCallback as useCallback58, useEffect as useEffect100, useState as useState114 } from "react";
43372
- import { useFields as useFields27, useRecordsClient as useRecordsClient83 } from "@ai-matrx/records/react";
43373
- import { BasicInput as BasicInput30, Button as Button90, Skeleton as Skeleton33, cn as cn82 } from "@ai-matrx/design-system";
43374
- import { useTable as useTable29 } from "@ai-matrx/records/react";
43375
- import { jsx as jsx137, jsxs as jsxs127 } from "react/jsx-runtime";
43591
+ import { useCallback as useCallback57, useEffect as useEffect99, useState as useState113 } from "react";
43592
+ import { useFields as useFields27, useRecordsClient as useRecordsClient82 } from "@ai-matrx/records/react";
43593
+ import { BasicInput as BasicInput30, Button as Button89, Skeleton as Skeleton32, cn as cn81 } from "@ai-matrx/design-system";
43594
+ import { useTable as useTable28 } from "@ai-matrx/records/react";
43595
+ import { jsx as jsx136, jsxs as jsxs126 } from "react/jsx-runtime";
43376
43596
  function SignBlock({ tableId, recordId, render, className }) {
43377
- const client = useRecordsClient83();
43378
- const table = useTable29(tableId);
43597
+ const client = useRecordsClient82();
43598
+ const table = useTable28(tableId);
43379
43599
  const rights = useTableRights(table.data);
43380
43600
  const fields = useFields27(tableId);
43381
- const [signatures, setSignatures] = useState114(null);
43382
- const [verdicts, setVerdicts] = useState114({});
43383
- const [error, setError] = useState114(null);
43384
- const [name, setName] = useState114("");
43385
- const [busy, setBusy] = useState114(false);
43386
- const load = useCallback58(async () => {
43601
+ const [signatures, setSignatures] = useState113(null);
43602
+ const [verdicts, setVerdicts] = useState113({});
43603
+ const [error, setError] = useState113(null);
43604
+ const [name, setName] = useState113("");
43605
+ const [busy, setBusy] = useState113(false);
43606
+ const load = useCallback57(async () => {
43387
43607
  const held = await client.docSignatures({ record_id: recordId });
43388
43608
  if (!held.ok) {
43389
43609
  setError(held.error);
@@ -43398,7 +43618,7 @@ function SignBlock({ tableId, recordId, render, className }) {
43398
43618
  }
43399
43619
  setVerdicts(answers);
43400
43620
  }, [client, recordId]);
43401
- useEffect100(() => {
43621
+ useEffect99(() => {
43402
43622
  void load();
43403
43623
  }, [load]);
43404
43624
  async function sign(field2) {
@@ -43418,36 +43638,36 @@ function SignBlock({ tableId, recordId, render, className }) {
43418
43638
  setName("");
43419
43639
  await load();
43420
43640
  }
43421
- if (fields.error) return /* @__PURE__ */ jsx137(RefusalNotice, { error: fields.error, className });
43641
+ if (fields.error) return /* @__PURE__ */ jsx136(RefusalNotice, { error: fields.error, className });
43422
43642
  if (error) {
43423
- return /* @__PURE__ */ jsx137(
43643
+ return /* @__PURE__ */ jsx136(
43424
43644
  RefusalNotice,
43425
43645
  {
43426
43646
  error,
43427
43647
  className,
43428
- actions: /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43648
+ actions: /* @__PURE__ */ jsx136(Button89, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43429
43649
  }
43430
43650
  );
43431
43651
  }
43432
- if (fields.loading || signatures === null) return /* @__PURE__ */ jsx137(Skeleton33, { className: cn82("h-32 w-full", className) });
43652
+ if (fields.loading || signatures === null) return /* @__PURE__ */ jsx136(Skeleton32, { className: cn81("h-32 w-full", className) });
43433
43653
  const signable = (fields.data ?? []).filter(isSignatureField);
43434
- return /* @__PURE__ */ jsxs127("div", { className: cn82("flex flex-col gap-2", className), children: [
43435
- /* @__PURE__ */ jsxs127("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43436
- /* @__PURE__ */ jsx137("span", { className: "font-medium text-foreground", children: "Signatures" }),
43437
- /* @__PURE__ */ jsxs127("span", { children: [
43654
+ return /* @__PURE__ */ jsxs126("div", { className: cn81("flex flex-col gap-2", className), children: [
43655
+ /* @__PURE__ */ jsxs126("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43656
+ /* @__PURE__ */ jsx136("span", { className: "font-medium text-foreground", children: "Signatures" }),
43657
+ /* @__PURE__ */ jsxs126("span", { children: [
43438
43658
  signatures.length,
43439
43659
  " on this record"
43440
43660
  ] })
43441
43661
  ] }),
43442
- signatures.length > 0 ? /* @__PURE__ */ jsx137("ul", { className: "flex flex-col gap-1", children: signatures.map((signature) => {
43662
+ signatures.length > 0 ? /* @__PURE__ */ jsx136("ul", { className: "flex flex-col gap-1", children: signatures.map((signature) => {
43443
43663
  const verdict = verdicts[signature.id];
43444
43664
  const intact = typeof verdict === "object" && verdict !== null ? verdict.intact : void 0;
43445
- return /* @__PURE__ */ jsxs127("li", { className: "rounded border px-2 py-1.5 text-xs", children: [
43446
- /* @__PURE__ */ jsxs127("div", { className: "flex items-center gap-2", children: [
43447
- /* @__PURE__ */ jsx137("span", { className: "font-medium", children: signature.signer_name }),
43448
- /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: signature.field_key }),
43449
- /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: new Date(signature.signed_at).toLocaleString() }),
43450
- /* @__PURE__ */ jsxs127("span", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: [
43665
+ return /* @__PURE__ */ jsxs126("li", { className: "rounded border px-2 py-1.5 text-xs", children: [
43666
+ /* @__PURE__ */ jsxs126("div", { className: "flex items-center gap-2", children: [
43667
+ /* @__PURE__ */ jsx136("span", { className: "font-medium", children: signature.signer_name }),
43668
+ /* @__PURE__ */ jsx136("span", { className: "text-muted-foreground", children: signature.field_key }),
43669
+ /* @__PURE__ */ jsx136("span", { className: "text-muted-foreground", children: new Date(signature.signed_at).toLocaleString() }),
43670
+ /* @__PURE__ */ jsxs126("span", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: [
43451
43671
  "v",
43452
43672
  signature.document_version,
43453
43673
  " \xB7 ",
@@ -43457,7 +43677,7 @@ function SignBlock({ tableId, recordId, render, className }) {
43457
43677
  intact === false ? (
43458
43678
  // THE STORE REFUSED TO CALL IT INTACT: its reason goes through the one
43459
43679
  // formatter, with what to do next, never raw in red (lane REFUSAL-SWEEP).
43460
- /* @__PURE__ */ jsx137(
43680
+ /* @__PURE__ */ jsx136(
43461
43681
  RefusalLine,
43462
43682
  {
43463
43683
  className: "mt-0.5",
@@ -43468,22 +43688,22 @@ function SignBlock({ tableId, recordId, render, className }) {
43468
43688
  )
43469
43689
  }
43470
43690
  )
43471
- ) : /* @__PURE__ */ jsx137("p", { className: "mt-0.5 text-muted-foreground", children: intact === true ? "The store says what was signed is still exactly what is there." : String(verdict ?? "") })
43691
+ ) : /* @__PURE__ */ jsx136("p", { className: "mt-0.5 text-muted-foreground", children: intact === true ? "The store says what was signed is still exactly what is there." : String(verdict ?? "") })
43472
43692
  ] }, signature.id);
43473
43693
  }) }) : null,
43474
- !render ? /* @__PURE__ */ jsx137("p", { className: "text-xs text-muted-foreground", children: 'Nothing has been frozen yet, so there is no version to sign. A signature seals one exact document version and its hash \u2014 "the document as it reads today" is not something anyone can agree to.' }) : signable.length === 0 ? /* @__PURE__ */ jsxs127("p", { className: "text-xs text-muted-foreground", children: [
43694
+ !render ? /* @__PURE__ */ jsx136("p", { className: "text-xs text-muted-foreground", children: 'Nothing has been frozen yet, so there is no version to sign. A signature seals one exact document version and its hash \u2014 "the document as it reads today" is not something anyone can agree to.' }) : signable.length === 0 ? /* @__PURE__ */ jsxs126("p", { className: "text-xs text-muted-foreground", children: [
43475
43695
  "This table has no signature Field, so there is nowhere to write a signature. A signature is a Value on a text Field whose format is ",
43476
- /* @__PURE__ */ jsx137("code", { children: "signature" }),
43696
+ /* @__PURE__ */ jsx136("code", { children: "signature" }),
43477
43697
  " \u2014 add one and the signing appears."
43478
- ] }) : !rights.write ? /* @__PURE__ */ jsx137("p", { className: "text-xs text-muted-foreground", children: rights.why("write") }) : /* @__PURE__ */ jsx137("div", { className: "flex flex-col gap-1", children: signable.map((field2) => {
43698
+ ] }) : !rights.write ? /* @__PURE__ */ jsx136("p", { className: "text-xs text-muted-foreground", children: rights.why("write") }) : /* @__PURE__ */ jsx136("div", { className: "flex flex-col gap-1", children: signable.map((field2) => {
43479
43699
  const already = signatures.find((s) => s.field_key === field2.key);
43480
43700
  if (already) {
43481
- return /* @__PURE__ */ jsxs127("p", { className: "text-xs text-muted-foreground", children: [
43701
+ return /* @__PURE__ */ jsxs126("p", { className: "text-xs text-muted-foreground", children: [
43482
43702
  fieldName(field2),
43483
43703
  " is signed, and a signature is immutable once made. A further agreement is a further Field with its own signature, or a further version with its own seal."
43484
43704
  ] }, field2.id);
43485
43705
  }
43486
- return /* @__PURE__ */ jsxs127(
43706
+ return /* @__PURE__ */ jsxs126(
43487
43707
  "form",
43488
43708
  {
43489
43709
  className: "flex items-center gap-1",
@@ -43492,7 +43712,7 @@ function SignBlock({ tableId, recordId, render, className }) {
43492
43712
  if (name.trim() !== "") void sign(field2);
43493
43713
  },
43494
43714
  children: [
43495
- /* @__PURE__ */ jsx137(
43715
+ /* @__PURE__ */ jsx136(
43496
43716
  BasicInput30,
43497
43717
  {
43498
43718
  className: "h-8 w-56 text-xs",
@@ -43502,8 +43722,8 @@ function SignBlock({ tableId, recordId, render, className }) {
43502
43722
  onChange: (e) => setName(e.target.value)
43503
43723
  }
43504
43724
  ),
43505
- /* @__PURE__ */ jsx137(Button90, { size: "sm", type: "submit", disabled: busy || name.trim() === "", children: "Sign" }),
43506
- /* @__PURE__ */ jsxs127("span", { className: "text-xs text-muted-foreground", children: [
43725
+ /* @__PURE__ */ jsx136(Button89, { size: "sm", type: "submit", disabled: busy || name.trim() === "", children: "Sign" }),
43726
+ /* @__PURE__ */ jsxs126("span", { className: "text-xs text-muted-foreground", children: [
43507
43727
  "seals version ",
43508
43728
  render.template_version,
43509
43729
  " \xB7 ",
@@ -43518,6 +43738,261 @@ function SignBlock({ tableId, recordId, render, className }) {
43518
43738
  ] });
43519
43739
  }
43520
43740
 
43741
+ // src/NotifyRuleEditor.tsx
43742
+ import { useCallback as useCallback58, useEffect as useEffect100, useState as useState114 } from "react";
43743
+ import { useRecordsClient as useRecordsClient83, useTable as useTable29 } from "@ai-matrx/records/react";
43744
+ import { hasQuietHours as hasQuietHours3 } from "@ai-matrx/records";
43745
+ import { Button as Button90, Skeleton as Skeleton33, cn as cn82 } from "@ai-matrx/design-system";
43746
+ import { jsx as jsx137, jsxs as jsxs127 } from "react/jsx-runtime";
43747
+ var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
43748
+ var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
43749
+ function NotifyRuleEditor({ tableId, seed, chosenRuleId, onChoose, className }) {
43750
+ const client = useRecordsClient83();
43751
+ const host = useRecordsUi();
43752
+ const table = useTable29(tableId);
43753
+ const rights = useTableRights(table.data);
43754
+ const [subscriptions, setSubscriptions] = useState114(null);
43755
+ const [cadences, setCadences] = useState114([]);
43756
+ const [views, setViews] = useState114(null);
43757
+ const [error, setError] = useState114(null);
43758
+ const [busy, setBusy] = useState114(false);
43759
+ const [stopping, setStopping] = useState114(null);
43760
+ const load = useCallback58(async () => {
43761
+ const [held, offered] = await Promise.all([
43762
+ // THE PERSON'S OWN DOOR, not the notifier's. It answers what is addressed
43763
+ // to them plus — only where they hold admin on this Table — anyone's over
43764
+ // it, and it is narrowed to Tables they can already open.
43765
+ client.subscriptions({ table_id: tableId }),
43766
+ // The cadences are ASKED of the store, never a list typed into a picker
43767
+ // here that could drift from what the digest runner understands.
43768
+ client.subscriptionCadences()
43769
+ ]);
43770
+ if (!held.ok) {
43771
+ setError(held.error);
43772
+ return;
43773
+ }
43774
+ setError(null);
43775
+ setSubscriptions(held.data);
43776
+ if (offered.ok) setCadences(offered.data);
43777
+ if (host.savedViews) {
43778
+ setViews(await host.savedViews());
43779
+ return;
43780
+ }
43781
+ const listed = await client.views({ table_id: tableId });
43782
+ if (!listed.ok) {
43783
+ setError(listed.error);
43784
+ return;
43785
+ }
43786
+ setViews(listed.data.map((v) => ({ id: v.view_id, name: v.name })));
43787
+ }, [client, host, tableId]);
43788
+ useEffect100(() => {
43789
+ void load();
43790
+ }, [load]);
43791
+ const write = useCallback58(
43792
+ async (spec) => {
43793
+ const declared2 = await client.subscriptionDeclare({
43794
+ table_id: tableId,
43795
+ spec: {
43796
+ name: spec.name,
43797
+ saved_view_id: spec.savedViewId,
43798
+ cadence: spec.cadence ?? "instant",
43799
+ schedule: spec.schedule ?? null,
43800
+ channel: spec.channel ?? "in_app",
43801
+ quiet_hours: spec.quietHours ?? null,
43802
+ // "Tell me" is the default, and the store refuses a subscription with
43803
+ // nobody to tell rather than writing one that fires at nobody.
43804
+ recipient_user_id: spec.recipientUserId ?? client.config.actor.user_id ?? null,
43805
+ event_key: spec.eventKey ?? "records.changed"
43806
+ }
43807
+ });
43808
+ if (!declared2.ok) {
43809
+ setError(declared2.error);
43810
+ return null;
43811
+ }
43812
+ return declared2.data;
43813
+ },
43814
+ [client, tableId]
43815
+ );
43816
+ useEffect100(() => {
43817
+ if (!subscriptions || !seed || seed.length === 0) return;
43818
+ const missing = seed.filter((s) => !subscriptions.some((held) => held.name === s.name));
43819
+ if (missing.length === 0) return;
43820
+ void (async () => {
43821
+ for (const spec of missing) {
43822
+ if (!await write(spec)) return;
43823
+ }
43824
+ await load();
43825
+ })();
43826
+ }, [subscriptions, seed, write, load]);
43827
+ async function subscribe(viewId, viewName, cadence, channel, schedule, quiet2) {
43828
+ setBusy(true);
43829
+ const ok = await write({
43830
+ name: `${viewName} \u2014 ${CADENCE_WORDS2[cadence] ?? cadence}`,
43831
+ savedViewId: viewId,
43832
+ cadence,
43833
+ channel,
43834
+ schedule: schedule.trim() === "" ? null : schedule.trim(),
43835
+ quietHours: quiet2
43836
+ });
43837
+ setBusy(false);
43838
+ if (!ok) return;
43839
+ onChoose?.(ok);
43840
+ await load();
43841
+ }
43842
+ async function makeEveryRecordView() {
43843
+ setBusy(true);
43844
+ const made = await client.viewDeclare({
43845
+ table_id: tableId,
43846
+ spec: { name: `Every ${table.data?.name ?? "record"}`, filters: null }
43847
+ });
43848
+ setBusy(false);
43849
+ if (!made.ok) {
43850
+ setError(made.error);
43851
+ return;
43852
+ }
43853
+ await load();
43854
+ }
43855
+ async function unsubscribe(ruleId) {
43856
+ setStopping(null);
43857
+ setBusy(true);
43858
+ const deleted = await client.recordDelete({ record_id: ruleId });
43859
+ setBusy(false);
43860
+ if (!deleted.ok) {
43861
+ setError(deleted.error);
43862
+ return;
43863
+ }
43864
+ await load();
43865
+ }
43866
+ if (error) {
43867
+ return /* @__PURE__ */ jsx137(
43868
+ RefusalNotice,
43869
+ {
43870
+ error,
43871
+ className,
43872
+ actions: /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43873
+ }
43874
+ );
43875
+ }
43876
+ if (subscriptions === null) return /* @__PURE__ */ jsx137(Skeleton33, { className: cn82("h-40 w-full", className) });
43877
+ return /* @__PURE__ */ jsxs127("div", { className: cn82("flex flex-col gap-2", className), children: [
43878
+ /* @__PURE__ */ jsxs127("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43879
+ /* @__PURE__ */ jsx137("span", { className: "font-medium text-foreground", children: "Tell me when" }),
43880
+ /* @__PURE__ */ jsxs127("span", { children: [
43881
+ subscriptions.length,
43882
+ " subscription",
43883
+ subscriptions.length === 1 ? "" : "s"
43884
+ ] })
43885
+ ] }),
43886
+ subscriptions.length === 0 ? /* @__PURE__ */ jsx137("p", { className: "text-xs text-muted-foreground", children: "Nothing is telling anyone about this yet. A subscription is a Rule over a saved view, so what you are told about is exactly what that view holds." }) : /* @__PURE__ */ jsx137("ul", { className: "flex flex-col gap-1", children: subscriptions.map((held) => /* @__PURE__ */ jsxs127("li", { className: "flex items-center gap-2 rounded border px-2 py-1.5 text-xs", children: [
43887
+ /* @__PURE__ */ jsx137("span", { className: "truncate font-medium", children: held.name }),
43888
+ /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: CADENCE_WORDS2[held.cadence] ?? held.cadence }),
43889
+ /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: CHANNEL_WORDS3[held.channel] ?? held.channel }),
43890
+ held.schedule ? /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: scheduleWords(held.schedule, held.quiet_hours?.tz) }) : null,
43891
+ held.next_digest_at ? /* @__PURE__ */ jsxs127("span", { className: "text-muted-foreground", children: [
43892
+ "next ",
43893
+ new Date(held.next_digest_at).toLocaleString()
43894
+ ] }) : null,
43895
+ hasQuietHours3(held.quiet_hours) ? /* @__PURE__ */ jsxs127("span", { className: "text-muted-foreground", children: [
43896
+ "quiet ",
43897
+ held.quiet_hours.start,
43898
+ "\u2013",
43899
+ held.quiet_hours.end
43900
+ ] }) : null,
43901
+ held.recipient_user_id === null ? /* @__PURE__ */ jsx137("span", { className: "text-destructive", children: "This one has nobody to tell, so it fires at nobody." }) : null,
43902
+ held.saved_view_id === null ? /* @__PURE__ */ jsx137("span", { className: "text-destructive", children: "This one names no view, so the store never admits a record to it." }) : null,
43903
+ held.muted ? /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: "Switched off \u2014 it tells nobody until somebody switches it back on." }) : null,
43904
+ onChoose ? held.rule_id === chosenRuleId ? /* @__PURE__ */ jsxs127("span", { className: "ml-auto flex items-center gap-1", children: [
43905
+ /* @__PURE__ */ jsx137("span", { className: "font-medium text-foreground", children: "Used for this form" }),
43906
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", disabled: busy, onClick: () => onChoose(null), children: "Stop using" })
43907
+ ] }) : /* @__PURE__ */ jsx137(
43908
+ Button90,
43909
+ {
43910
+ size: "sm",
43911
+ variant: "outline",
43912
+ className: "ml-auto",
43913
+ disabled: busy,
43914
+ onClick: () => onChoose(held.rule_id),
43915
+ children: "Use for this form"
43916
+ }
43917
+ ) : null,
43918
+ rights.write && !(onChoose && held.rule_id === chosenRuleId) && stopping === held.rule_id ? /* @__PURE__ */ jsxs127("span", { className: cn82("flex flex-wrap items-center gap-2", onChoose ? void 0 : "ml-auto"), children: [
43919
+ /* @__PURE__ */ jsx137("span", { className: "text-xs text-destructive", children: archiveConfirmSentence("this subscription") }),
43920
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "destructive", disabled: busy, onClick: () => void unsubscribe(held.rule_id), children: "Stop telling me" }),
43921
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", onClick: () => setStopping(null), children: "Keep it" })
43922
+ ] }) : rights.write && !(onChoose && held.rule_id === chosenRuleId) ? /* @__PURE__ */ jsx137(
43923
+ Button90,
43924
+ {
43925
+ size: "sm",
43926
+ variant: "ghost",
43927
+ className: onChoose ? void 0 : "ml-auto",
43928
+ disabled: busy,
43929
+ onClick: () => setStopping(held.rule_id),
43930
+ children: "Stop telling me"
43931
+ }
43932
+ ) : null
43933
+ ] }, held.rule_id)) }),
43934
+ onChoose && chosenRuleId && !subscriptions.some((held) => held.rule_id === chosenRuleId) ? /* @__PURE__ */ jsx137("p", { className: "text-xs text-destructive", children: "This form points at a subscription that is not in this list any more, so nobody is told when it is answered. Choose one below, or make one." }) : null,
43935
+ views === null ? /* @__PURE__ */ jsx137("p", { className: "rounded border border-dashed px-2 py-1 text-xs text-muted-foreground", children: NO_SAVED_VIEWS_REASON }) : views.length === 0 ? onChoose && rights.write ? /* @__PURE__ */ jsxs127("div", { className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [
43936
+ /* @__PURE__ */ jsx137("span", { children: "A subscription watches a saved view, and this table has none yet." }),
43937
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "outline", disabled: busy, onClick: () => void makeEveryRecordView(), children: "Watch every record" })
43938
+ ] }) : /* @__PURE__ */ jsx137("p", { className: "text-xs text-muted-foreground", children: "This organization has no saved views yet, so there is nothing to subscribe to. Save a view first." }) : rights.write ? /* @__PURE__ */ jsxs127(
43939
+ "form",
43940
+ {
43941
+ className: "flex flex-wrap items-center gap-1",
43942
+ onSubmit: (event) => {
43943
+ event.preventDefault();
43944
+ const form = event.currentTarget;
43945
+ const view = form.elements.namedItem("view").value;
43946
+ const cadence = form.elements.namedItem("cadence").value;
43947
+ const channel = form.elements.namedItem("channel").value;
43948
+ const schedule = form.elements.namedItem("schedule").value;
43949
+ const start = form.elements.namedItem("quiet_start").value;
43950
+ const end = form.elements.namedItem("quiet_end").value;
43951
+ const name = views.find((v) => v.id === view)?.name ?? "View";
43952
+ const quiet2 = start && end ? { start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone } : null;
43953
+ if (view) void subscribe(view, name, cadence, channel, schedule, quiet2);
43954
+ },
43955
+ children: [
43956
+ /* @__PURE__ */ jsx137("select", { name: "view", "aria-label": "View to be told about", className: "h-7 rounded border bg-background px-1 text-xs", children: views.map((view) => /* @__PURE__ */ jsx137("option", { value: view.id, children: view.name }, view.id)) }),
43957
+ /* @__PURE__ */ jsx137("select", { name: "cadence", "aria-label": "How often", className: "h-7 rounded border bg-background px-1 text-xs", children: (cadences.length > 0 ? cadences : ["instant"]).map((cadence) => /* @__PURE__ */ jsx137("option", { value: cadence, children: CADENCE_WORDS2[cadence] ?? cadence }, cadence)) }),
43958
+ /* @__PURE__ */ jsx137("select", { name: "channel", "aria-label": "Where it arrives", className: "h-7 rounded border bg-background px-1 text-xs", children: ["in_app", "email", "sms"].map((channel) => /* @__PURE__ */ jsx137("option", { value: channel, children: CHANNEL_WORDS3[channel] }, channel)) }),
43959
+ /* @__PURE__ */ jsx137(
43960
+ "input",
43961
+ {
43962
+ name: "schedule",
43963
+ "aria-label": "When the summary goes out",
43964
+ placeholder: "monday 08:00",
43965
+ className: "h-7 w-28 rounded border bg-background px-1 text-xs"
43966
+ }
43967
+ ),
43968
+ /* @__PURE__ */ jsx137("span", { className: "text-xs text-muted-foreground", children: "not between" }),
43969
+ /* @__PURE__ */ jsx137(
43970
+ "input",
43971
+ {
43972
+ name: "quiet_start",
43973
+ type: "time",
43974
+ "aria-label": "Quiet hours start",
43975
+ className: "h-7 rounded border bg-background px-1 text-xs"
43976
+ }
43977
+ ),
43978
+ /* @__PURE__ */ jsx137("span", { className: "text-xs text-muted-foreground", children: "and" }),
43979
+ /* @__PURE__ */ jsx137(
43980
+ "input",
43981
+ {
43982
+ name: "quiet_end",
43983
+ type: "time",
43984
+ "aria-label": "Quiet hours end",
43985
+ className: "h-7 rounded border bg-background px-1 text-xs"
43986
+ }
43987
+ ),
43988
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", type: "submit", disabled: busy, children: "Tell me" }),
43989
+ /* @__PURE__ */ jsx137("p", { className: "w-full text-xs text-muted-foreground", children: "A send inside your quiet hours is held until they end \u2014 it is never dropped. Leave the schedule blank for the store's own default of 08:00." })
43990
+ ]
43991
+ }
43992
+ ) : /* @__PURE__ */ jsx137("p", { className: "text-xs text-muted-foreground", children: rights.why("write") })
43993
+ ] });
43994
+ }
43995
+
43521
43996
  // src/DashboardCanvas.tsx
43522
43997
  import { useCallback as useCallback59, useEffect as useEffect102, useMemo as useMemo71, useState as useState117 } from "react";
43523
43998
  import { useFields as useFields30, useRecordsClient as useRecordsClient85, useTable as useTable31 } from "@ai-matrx/records/react";
@@ -43538,20 +44013,20 @@ import {
43538
44013
 
43539
44014
  // src/DashboardGrid.tsx
43540
44015
  import {
43541
- DndContext,
43542
- KeyboardSensor,
43543
- PointerSensor,
44016
+ DndContext as DndContext2,
44017
+ KeyboardSensor as KeyboardSensor2,
44018
+ PointerSensor as PointerSensor2,
43544
44019
  pointerWithin,
43545
- useSensor,
43546
- useSensors
44020
+ useSensor as useSensor2,
44021
+ useSensors as useSensors2
43547
44022
  } from "@dnd-kit/core";
43548
44023
  import {
43549
- SortableContext,
44024
+ SortableContext as SortableContext2,
43550
44025
  rectSortingStrategy,
43551
- sortableKeyboardCoordinates,
43552
- useSortable
44026
+ sortableKeyboardCoordinates as sortableKeyboardCoordinates2,
44027
+ useSortable as useSortable2
43553
44028
  } from "@dnd-kit/sortable";
43554
- import { CSS } from "@dnd-kit/utilities";
44029
+ import { CSS as CSS3 } from "@dnd-kit/utilities";
43555
44030
  import { cn as cn83 } from "@ai-matrx/design-system";
43556
44031
 
43557
44032
  // src/dashboardLayout.ts
@@ -43615,9 +44090,9 @@ var dashboardCollision = (args) => {
43615
44090
  }).sort((a, b) => a.data.value - b.data.value);
43616
44091
  };
43617
44092
  function DashboardGrid({ items, arranging, onMove, onSpan }) {
43618
- const sensors = useSensors(
43619
- useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
43620
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
44093
+ const sensors = useSensors2(
44094
+ useSensor2(PointerSensor2, { activationConstraint: { distance: 4 } }),
44095
+ useSensor2(KeyboardSensor2, { coordinateGetter: sortableKeyboardCoordinates2 })
43621
44096
  );
43622
44097
  const ids = items.map((item) => item.id);
43623
44098
  function dragEnd(event) {
@@ -43627,7 +44102,7 @@ function DashboardGrid({ items, arranging, onMove, onSpan }) {
43627
44102
  }
43628
44103
  const grid = /* @__PURE__ */ jsx138("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-12", children: items.map((item, index) => /* @__PURE__ */ jsx138(GridCell2, { item, index, arranging, onSpan }, item.id)) });
43629
44104
  if (!arranging) return grid;
43630
- return /* @__PURE__ */ jsx138(DndContext, { sensors, collisionDetection: dashboardCollision, onDragEnd: dragEnd, children: /* @__PURE__ */ jsx138(SortableContext, { items: ids, strategy: rectSortingStrategy, children: grid }) });
44105
+ return /* @__PURE__ */ jsx138(DndContext2, { sensors, collisionDetection: dashboardCollision, onDragEnd: dragEnd, children: /* @__PURE__ */ jsx138(SortableContext2, { items: ids, strategy: rectSortingStrategy, children: grid }) });
43631
44106
  }
43632
44107
  function GridCell2({
43633
44108
  item,
@@ -43635,8 +44110,8 @@ function GridCell2({
43635
44110
  arranging,
43636
44111
  onSpan
43637
44112
  }) {
43638
- const sortable = useSortable({ id: item.id, disabled: !arranging });
43639
- const style = arranging ? { transform: CSS.Transform.toString(sortable.transform), transition: sortable.transition } : void 0;
44113
+ const sortable = useSortable2({ id: item.id, disabled: !arranging });
44114
+ const style = arranging ? { transform: CSS3.Transform.toString(sortable.transform), transition: sortable.transition } : void 0;
43640
44115
  return /* @__PURE__ */ jsxs128(
43641
44116
  "div",
43642
44117
  {
@@ -44842,7 +45317,7 @@ function TargetControl({
44842
45317
  // src/QuestionPanel.tsx
44843
45318
  import { copyText as copyText8 } from "@ai-matrx/kit/clipboard";
44844
45319
  import { useEffect as useEffect103, useMemo as useMemo72, useRef as useRef59, useState as useState118 } from "react";
44845
- import { Badge as Badge23, BasicInput as BasicInput32, Button as Button94, Checkbox as Checkbox16, Label as Label24, Skeleton as Skeleton35, cn as cn85 } from "@ai-matrx/design-system";
45320
+ import { Badge as Badge23, BasicInput as BasicInput32, Button as Button94, Checkbox as Checkbox16, Label as Label25, Skeleton as Skeleton35, cn as cn85 } from "@ai-matrx/design-system";
44846
45321
  import { useFields as useFields31, useRecordsClient as useRecordsClient86 } from "@ai-matrx/records/react";
44847
45322
 
44848
45323
  // src/question.ts
@@ -45424,9 +45899,9 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45424
45899
  linkMissed ? /* @__PURE__ */ jsx143("p", { className: "text-muted-foreground", children: "That question is not here or not shared with you." }) : null,
45425
45900
  saveError ? /* @__PURE__ */ jsx143(RefusalNotice, { error: saveError }) : null,
45426
45901
  /* @__PURE__ */ jsxs132("div", { className: "grid grid-cols-[6.5rem_minmax(0,1fr)] items-start gap-x-3 gap-y-2", "data-testid": "question-builder", children: [
45427
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Measure" }),
45902
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Measure" }),
45428
45903
  /* @__PURE__ */ jsx143(Measures, { measures: spec.measures, fields: offered, onChange: (measures) => patch({ measures }) }),
45429
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Filter" }),
45904
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Filter" }),
45430
45905
  /* @__PURE__ */ jsxs132("div", { className: "flex flex-col gap-1.5", children: [
45431
45906
  /* @__PURE__ */ jsx143(
45432
45907
  ConditionGroup,
@@ -45440,9 +45915,9 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45440
45915
  ),
45441
45916
  /* @__PURE__ */ jsx143(Through, { conditions: spec.through, related, onChange: (through) => patch({ through }) })
45442
45917
  ] }),
45443
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Summarize by" }),
45918
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Summarize by" }),
45444
45919
  /* @__PURE__ */ jsx143(GroupPicker, { spec, fields: offered, related, onChange: (group) => patch({ group }) }),
45445
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "As of" }),
45920
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "As of" }),
45446
45921
  /* @__PURE__ */ jsxs132("div", { className: "flex items-center gap-1.5", children: [
45447
45922
  /* @__PURE__ */ jsx143(
45448
45923
  Checkbox16,
@@ -45466,7 +45941,7 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45466
45941
  ) : /* @__PURE__ */ jsx143("span", { className: "text-muted-foreground", children: "Now" })
45467
45942
  ] }),
45468
45943
  spec.group ? /* @__PURE__ */ jsxs132(Fragment57, { children: [
45469
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Sort" }),
45944
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Sort" }),
45470
45945
  /* @__PURE__ */ jsxs132("div", { className: "flex flex-wrap items-center gap-1.5", children: [
45471
45946
  /* @__PURE__ */ jsx143(
45472
45947
  PickOne,
@@ -45497,7 +45972,7 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45497
45972
  }
45498
45973
  )
45499
45974
  ] }),
45500
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Show as" }),
45975
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Show as" }),
45501
45976
  /* @__PURE__ */ jsx143("div", { className: "flex flex-wrap gap-1", role: "radiogroup", "aria-label": "Show as", children: SHOWS.map((s) => /* @__PURE__ */ jsx143(
45502
45977
  Button94,
45503
45978
  {