@ai-matrx/records-ui 0.106.0 → 0.107.1

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,
@@ -37722,9 +37756,53 @@ function formPresentation(spec) {
37722
37756
  welcome: welcomeDocument(spec.welcome ?? null),
37723
37757
  endings: (spec.endings ?? []).map(endingDocument),
37724
37758
  hidden_fields: spec.hiddenFields ?? [],
37725
- 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 || spec.notify.emailRespondent) ? {
37763
+ notify: strip({
37764
+ webhook_url: text(spec.notify.webhookUrl),
37765
+ webhook_automation_id: text(spec.notify.webhookAutomationId),
37766
+ email_respondent: spec.notify.emailRespondent ? true : null,
37767
+ respondent_field: spec.notify.emailRespondent ? text(spec.notify.respondentField) : null
37768
+ })
37769
+ } : {}
37726
37770
  };
37727
37771
  }
37772
+ function notifyFromDocument(raw) {
37773
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
37774
+ const d = raw;
37775
+ const n = {
37776
+ webhookUrl: text(d["webhook_url"]),
37777
+ webhookAutomationId: text(d["webhook_automation_id"]),
37778
+ emailRespondent: d["email_respondent"] === true ? true : null,
37779
+ respondentField: text(d["respondent_field"])
37780
+ };
37781
+ return n.webhookUrl || n.webhookAutomationId || n.emailRespondent ? n : null;
37782
+ }
37783
+ var bool = (v) => typeof v === "boolean" ? v : null;
37784
+ function answerRefusal(kind, required, value) {
37785
+ const empty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
37786
+ if (empty) return required ? "This field is required" : null;
37787
+ if (typeof value !== "string") return null;
37788
+ const v = value.trim();
37789
+ if (kind === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return "Enter a valid email";
37790
+ if (kind === "phone" && (v.replace(/\D/g, "").length < 7 || /[^\d\s()+.\-x]/i.test(v))) return "Enter a valid phone number";
37791
+ if (kind === "url" && !/^(https?:\/\/)?[^\s.\/]+(\.[^\s.\/]+)+(\/\S*)?$/i.test(v)) return "Enter a valid link";
37792
+ return null;
37793
+ }
37794
+ function pipeAnswers(text2, answers, questions, labelOf) {
37795
+ if (!text2.includes("{{")) return text2;
37796
+ return text2.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_all, name) => {
37797
+ const wanted = name.trim().toLowerCase();
37798
+ const q = questions.find((x) => x.key.toLowerCase() === wanted) ?? questions.find((x) => (x.ask ?? "").trim().toLowerCase() === wanted);
37799
+ if (!q) return "";
37800
+ const value = answers[q.key];
37801
+ if (value === void 0 || value === null) return "";
37802
+ const one = (v) => labelOf?.(q.key, v) ?? String(v);
37803
+ return Array.isArray(value) ? value.map(one).join(", ") : one(value);
37804
+ });
37805
+ }
37728
37806
  var text = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : null;
37729
37807
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
37730
37808
  var strip = (doc) => Object.fromEntries(Object.entries(doc).filter(([, v]) => v !== null && v !== void 0));
@@ -37899,6 +37977,9 @@ function formFromSummary(summary) {
37899
37977
  thankYou: thankYouFromDocument(presentation["thank_you"]),
37900
37978
  look: formLookFromDocument(presentation["look"]),
37901
37979
  submitLabel: presentation["submit_label"] ?? null,
37980
+ showOwnerHeader: bool(presentation["show_owner_header"]),
37981
+ autoAdvance: bool(presentation["auto_advance"]),
37982
+ notify: notifyFromDocument(presentation["notify"]),
37902
37983
  // A form is public when the store says it is OPEN. A screen never decides
37903
37984
  // this from a flag of its own: the link answers or it does not.
37904
37985
  isPublic: summary.state === "open",
@@ -38703,6 +38784,50 @@ function compactTick(value) {
38703
38784
  if (Math.abs(value) < 1e3 && !Number.isInteger(value)) return String(Math.round(value * 10) / 10);
38704
38785
  return formatCount5(value, { style: "compact" });
38705
38786
  }
38787
+ function wrapTickWords(label, perLine, maxLines = 3) {
38788
+ const words2 = label.split(/\s+/).filter(Boolean).map((w) => w.length > perLine ? `${w.slice(0, perLine - 1)}\u2026` : w);
38789
+ const lines = [];
38790
+ let line = "";
38791
+ for (const word of words2) {
38792
+ const next = line ? `${line} ${word}` : word;
38793
+ if (next.length <= perLine) line = next;
38794
+ else {
38795
+ lines.push(line);
38796
+ line = word;
38797
+ }
38798
+ }
38799
+ if (line) lines.push(line);
38800
+ if (lines.length <= maxLines) return lines;
38801
+ const kept = lines.slice(0, maxLines);
38802
+ const last = kept[maxLines - 1].replace(/…$/, "");
38803
+ kept[maxLines - 1] = `${last.length >= perLine ? last.slice(0, perLine - 1) : last}\u2026`;
38804
+ return kept;
38805
+ }
38806
+ function WrappedTick(props) {
38807
+ const { x = 0, y = 0, payload, across, perLine } = props;
38808
+ const lines = wrapTickWords(String(payload?.value ?? ""), perLine);
38809
+ const lineHeight = 11;
38810
+ return /* @__PURE__ */ jsx120(
38811
+ "text",
38812
+ {
38813
+ x,
38814
+ y,
38815
+ fontSize: 10,
38816
+ fill: "currentColor",
38817
+ textAnchor: across ? "end" : "middle",
38818
+ "data-records-chart-tick": "",
38819
+ children: lines.map((line, i) => /* @__PURE__ */ jsx120(
38820
+ "tspan",
38821
+ {
38822
+ x: across ? x - 4 : x,
38823
+ dy: i === 0 ? across ? -((lines.length - 1) * lineHeight) / 2 + 3 : 10 : lineHeight,
38824
+ children: line
38825
+ },
38826
+ i
38827
+ ))
38828
+ }
38829
+ );
38830
+ }
38706
38831
  function Drawing({
38707
38832
  kind,
38708
38833
  points,
@@ -38808,9 +38933,9 @@ function Drawing({
38808
38933
  /* @__PURE__ */ jsx120(CartesianGrid, { vertical: horizontal, horizontal: !horizontal }),
38809
38934
  horizontal ? /* @__PURE__ */ jsxs110(Fragment48, { children: [
38810
38935
  /* @__PURE__ */ jsx120(XAxis, { type: "number", ...axis }),
38811
- /* @__PURE__ */ jsx120(YAxis, { type: "category", dataKey: "label", width: 88, ...axis })
38936
+ /* @__PURE__ */ jsx120(YAxis, { type: "category", dataKey: "label", width: 96, ...axis, tick: /* @__PURE__ */ jsx120(WrappedTick, { across: true, perLine: 15 }) })
38812
38937
  ] }) : /* @__PURE__ */ jsxs110(Fragment48, { children: [
38813
- /* @__PURE__ */ jsx120(XAxis, { dataKey: "label", ...axis }),
38938
+ /* @__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)) }) }),
38814
38939
  /* @__PURE__ */ jsx120(YAxis, { width: 44, ...axis })
38815
38940
  ] }),
38816
38941
  tooltip,
@@ -40459,7 +40584,7 @@ function FormLookLogo({ view, size = "md" }) {
40459
40584
  }
40460
40585
  );
40461
40586
  }
40462
- function FormLookFrame({ look, title, children, contained = false, className }) {
40587
+ function FormLookFrame({ look, title, children, contained = false, showOwnerHeader = true, className }) {
40463
40588
  const view = formLookView(look, title);
40464
40589
  return /* @__PURE__ */ jsxs117(
40465
40590
  "div",
@@ -40476,10 +40601,10 @@ function FormLookFrame({ look, title, children, contained = false, className })
40476
40601
  }
40477
40602
  ) : null,
40478
40603
  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,
40479
- /* @__PURE__ */ jsxs117("div", { className: "flex items-center gap-3", children: [
40604
+ showOwnerHeader ? /* @__PURE__ */ jsxs117("div", { className: "flex items-center gap-3", "data-records-form-owner": "", children: [
40480
40605
  /* @__PURE__ */ jsx127(FormLookLogo, { view }),
40481
40606
  /* @__PURE__ */ jsx127("p", { className: "min-w-0 truncate text-base font-semibold tracking-tight text-foreground", children: view.name })
40482
- ] }),
40607
+ ] }) : null,
40483
40608
  /* @__PURE__ */ jsx127("h1", { className: "text-2xl font-semibold tracking-tight text-foreground", children: title }),
40484
40609
  /* @__PURE__ */ jsx127("div", { className: "w-full", children }),
40485
40610
  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(
@@ -41302,17 +41427,20 @@ function SubscriptionsPanel({ tableId, activeRuleId, className }) {
41302
41427
  // src/FormBuilder.tsx
41303
41428
  import { copyText as copyText7 } from "@ai-matrx/kit/clipboard";
41304
41429
  import { ErrorBox as ErrorBox7 } from "@ai-matrx/design-system";
41305
- import { useCallback as useCallback55, useEffect as useEffect97, useMemo as useMemo68, useRef as useRef56, useState as useState111 } from "react";
41306
- import { useFields as useFields25, useRecordsClient as useRecordsClient80, useTable as useTable27 } from "@ai-matrx/records/react";
41430
+ import { useCallback as useCallback54, useEffect as useEffect96, useMemo as useMemo68, useRef as useRef56, useState as useState110 } from "react";
41431
+ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
41432
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
41433
+ import { CSS as CSS2 } from "@dnd-kit/utilities";
41434
+ import { useFields as useFields25, useRecordsClient as useRecordsClient79, useTable as useTable26 } from "@ai-matrx/records/react";
41307
41435
  import { publicFormPath } from "@ai-matrx/records";
41308
41436
  import {
41309
41437
  BasicInput as BasicInput28,
41310
41438
  BasicTextarea as BasicTextarea11,
41311
- Button as Button87,
41439
+ Button as Button86,
41312
41440
  Checkbox as Checkbox15,
41313
- Label as Label22,
41314
- Skeleton as Skeleton30,
41315
- cn as cn79
41441
+ Label as Label23,
41442
+ Skeleton as Skeleton29,
41443
+ cn as cn78
41316
41444
  } from "@ai-matrx/design-system";
41317
41445
 
41318
41446
  // src/FormFlowEditor.tsx
@@ -41570,7 +41698,7 @@ function EmbedSnippets({ url, label }) {
41570
41698
  // src/FormRunner.tsx
41571
41699
  import { useCallback as useCallback53, useEffect as useEffect95, useMemo as useMemo67, useRef as useRef55, useState as useState109 } from "react";
41572
41700
  import { useFields as useFields24, useOptionalRecordsClient as useOptionalRecordsClient14 } from "@ai-matrx/records/react";
41573
- import { Button as Button85, Progress, Skeleton as Skeleton28, cn as cn77, formThemeClasses } from "@ai-matrx/design-system";
41701
+ import { Button as Button85, Label as Label22, Progress, Skeleton as Skeleton28, cn as cn77, formThemeClasses } from "@ai-matrx/design-system";
41574
41702
  import { humanizeIdentifier as humanizeIdentifier18 } from "@ai-matrx/kit/text-case";
41575
41703
  import { Fragment as Fragment53, jsx as jsx132, jsxs as jsxs122 } from "react/jsx-runtime";
41576
41704
  function authored(host, text2, where) {
@@ -41669,7 +41797,8 @@ function FormStage({
41669
41797
  className,
41670
41798
  connected = false,
41671
41799
  onStart,
41672
- onQuestionShown
41800
+ onQuestionShown,
41801
+ autoAdvance
41673
41802
  }) {
41674
41803
  const host = useRecordsUi();
41675
41804
  const [answers, setAnswers] = useState109(() => ({ ...initialAnswers ?? {} }));
@@ -41682,6 +41811,11 @@ function FormStage({
41682
41811
  const [ending, setEnding] = useState109(null);
41683
41812
  const [started, setStarted] = useState109(() => !form.welcome || resumed);
41684
41813
  const routes = formRoutes(form);
41814
+ const [routeKnown, setRouteKnown] = useState109(() => !routes || !whichAsked);
41815
+ const [stopped, setStopped] = useState109(null);
41816
+ const answersRef = useRef55(answers);
41817
+ answersRef.current = answers;
41818
+ const choose = autoAdvance ?? form.autoAdvance ?? true;
41685
41819
  const decoy = useRef55("");
41686
41820
  const stage = useRef55(null);
41687
41821
  const questions = useMemo67(() => {
@@ -41717,6 +41851,7 @@ function FormStage({
41717
41851
  };
41718
41852
  }
41719
41853
  if (cancelled) return;
41854
+ setRouteKnown(true);
41720
41855
  if (!told2.ok) {
41721
41856
  setHidden({});
41722
41857
  setError(told2.error ?? refusal("not_supported", told2.message, "Answer every question shown; none were skipped."));
@@ -41738,7 +41873,7 @@ function FormStage({
41738
41873
  ) : null
41739
41874
  );
41740
41875
  })();
41741
- }, 250);
41876
+ }, routeKnown ? 250 : 0);
41742
41877
  return () => {
41743
41878
  cancelled = true;
41744
41879
  clearTimeout(timer);
@@ -41773,11 +41908,24 @@ function FormStage({
41773
41908
  const v = answers[q.key];
41774
41909
  return v !== void 0 && v !== null && v !== "";
41775
41910
  }).length;
41776
- const missing = live.filter((q) => {
41777
- const v = answers[q.key];
41778
- return q.required && (v === void 0 || v === null || v === "");
41779
- });
41911
+ const refusalFor = (q, values = answersRef.current) => answerRefusal(q.field ? String(editorKindFor(q.field)) : "text", q.required, values[q.key]);
41912
+ const refuse = (q, say2) => {
41913
+ setStopped({ key: q.key, say: say2 });
41914
+ const i = live.findIndex((x) => x.key === q.key);
41915
+ if (oneAtATime && i >= 0 && i !== index) setAt(i);
41916
+ window.setTimeout(() => {
41917
+ const box = stage.current?.querySelector(`[data-records-form-question="${CSS.escape(q.key)}"]`);
41918
+ box?.querySelector("[role='radio'][aria-checked='true'], input:not([tabindex='-1']), textarea, select, [role='combobox'], [role='radio'], [role='checkbox']")?.focus();
41919
+ }, 0);
41920
+ };
41780
41921
  const submit = useCallback53(async () => {
41922
+ for (const q of live) {
41923
+ const say2 = refusalFor(q);
41924
+ if (say2) {
41925
+ refuse(q, say2);
41926
+ return;
41927
+ }
41928
+ }
41781
41929
  if (preview) {
41782
41930
  setDone("preview");
41783
41931
  return;
@@ -41809,20 +41957,28 @@ function FormStage({
41809
41957
  }, [answers, form, honeypotKey, live, onSubmit, onSubmitted, preview, questions]);
41810
41958
  async function advance() {
41811
41959
  if (!oneAtATime) return;
41960
+ if (current) {
41961
+ const say2 = refusalFor(current);
41962
+ if (say2) {
41963
+ refuse(current, say2);
41964
+ return;
41965
+ }
41966
+ }
41967
+ setStopped(null);
41812
41968
  if (!routes || !whichAsked || !current) {
41813
41969
  if (index < live.length - 1) setAt(index + 1);
41814
- else if (missing.length === 0) void submit();
41970
+ else void submit();
41815
41971
  return;
41816
41972
  }
41817
41973
  let told2;
41818
41974
  try {
41819
- told2 = await whichAsked(answers);
41975
+ told2 = await whichAsked(answersRef.current);
41820
41976
  } catch {
41821
41977
  told2 = { ok: false, message: "This form could not work out which question comes next, so the next one is shown." };
41822
41978
  }
41823
41979
  if (!told2.ok) {
41824
41980
  if (index < live.length - 1) setAt(index + 1);
41825
- else if (missing.length === 0) void submit();
41981
+ else void submit();
41826
41982
  return;
41827
41983
  }
41828
41984
  const next = {};
@@ -41835,13 +41991,18 @@ function FormStage({
41835
41991
  setAt(here + 1);
41836
41992
  return;
41837
41993
  }
41838
- const stillMissing = onRoute.filter((q) => {
41839
- const v = answers[q.key];
41840
- return q.required && (v === void 0 || v === null || v === "");
41841
- });
41842
- if (stillMissing.length === 0) void submit();
41994
+ const stillMissing = onRoute.find((q) => refusalFor(q) !== null);
41995
+ if (stillMissing) refuse(stillMissing, refusalFor(stillMissing));
41996
+ else void submit();
41843
41997
  }
41998
+ const advanceRef = useRef55(advance);
41999
+ advanceRef.current = advance;
42000
+ const chosen = useCallback53(() => {
42001
+ if (!oneAtATime || !choose) return;
42002
+ window.setTimeout(() => void advanceRef.current(), 450);
42003
+ }, [oneAtATime, choose]);
41844
42004
  function retreat() {
42005
+ setStopped(null);
41845
42006
  if (oneAtATime && index > 0) setAt(index - 1);
41846
42007
  }
41847
42008
  function onKeyDown(event) {
@@ -41857,12 +42018,11 @@ function FormStage({
41857
42018
  else advance();
41858
42019
  }
41859
42020
  useEffect95(() => {
41860
- const input = stage.current?.querySelector(
41861
- // A choice question has no box: its first option takes the caret, so A / B / C answer it.
42021
+ const input = stage.current?.querySelector("[role='radio'][aria-checked='true']") ?? stage.current?.querySelector(
41862
42022
  "input:not([tabindex='-1']), textarea, select, [role='combobox'], [role='radio'], [role='checkbox']"
41863
42023
  );
41864
42024
  input?.focus();
41865
- }, [index, done]);
42025
+ }, [index, done, started]);
41866
42026
  const startTold = useRef55(false);
41867
42027
  useEffect95(() => {
41868
42028
  if (startTold.current || !onStart) return;
@@ -41881,6 +42041,10 @@ function FormStage({
41881
42041
  }
41882
42042
  }, [current, live, oneAtATime, onQuestionShown, started, done]);
41883
42043
  const theme = formThemeClasses(form.theme ?? null);
42044
+ const pipe = (text2) => pipeAnswers(text2, answers, questions, (key, v) => {
42045
+ const f = questions.find((q) => q.key === key)?.field;
42046
+ return f ? publicChoicesOf(f).find((c) => c.key === String(v))?.label ?? null : null;
42047
+ });
41884
42048
  if (done) {
41885
42049
  return /* @__PURE__ */ jsx132(
41886
42050
  ThankYou,
@@ -41889,6 +42053,7 @@ function FormStage({
41889
42053
  done,
41890
42054
  ending: (form.endings ?? []).find((e) => e.id === ending) ?? null,
41891
42055
  buttonClass: theme.buttonClass,
42056
+ pipe,
41892
42057
  className: cn77("mx-auto max-w-xl py-10", centred && "text-center", className)
41893
42058
  }
41894
42059
  );
@@ -41900,14 +42065,14 @@ function FormStage({
41900
42065
  return /* @__PURE__ */ jsxs122(
41901
42066
  "section",
41902
42067
  {
41903
- 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),
42068
+ 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),
41904
42069
  style: theme.surfaceStyle,
41905
42070
  "data-records-form-welcome": "",
41906
42071
  children: [
41907
42072
  w.pictureUrl ? /* @__PURE__ */ jsx132("img", { src: w.pictureUrl, alt: "", className: "max-h-56 w-full rounded-md object-cover" }) : null,
41908
- w.title ? /* @__PURE__ */ jsx132("h2", { className: "text-xl font-semibold", children: w.title }) : null,
41909
- w.body ? /* @__PURE__ */ jsx132("p", { className: "text-sm text-muted-foreground", children: authored(host, w.body, "form-intro") }) : null,
41910
- /* @__PURE__ */ jsx132("div", { children: /* @__PURE__ */ jsx132(Button85, { size: "sm", className: theme.buttonClass, onClick: () => setStarted(true), "data-records-form-start": "", children: w.buttonLabel || "Start" }) })
42073
+ w.title ? /* @__PURE__ */ jsx132("h2", { className: "text-2xl font-semibold sm:text-3xl", children: w.title }) : null,
42074
+ w.body ? /* @__PURE__ */ jsx132("p", { className: "text-base text-muted-foreground", children: authored(host, w.body, "form-intro") }) : null,
42075
+ /* @__PURE__ */ jsx132("div", { children: /* @__PURE__ */ jsx132(Button85, { size: "lg", className: theme.buttonClass, onClick: () => setStarted(true), "data-records-form-start": "", children: w.buttonLabel || "Start" }) })
41911
42076
  ]
41912
42077
  }
41913
42078
  );
@@ -41916,7 +42081,15 @@ function FormStage({
41916
42081
  return /* @__PURE__ */ jsxs122(
41917
42082
  "section",
41918
42083
  {
41919
- 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),
42084
+ className: cn77(
42085
+ "mx-auto flex w-full max-w-2xl flex-col gap-4 py-4",
42086
+ // TYPEFORM'S SIZE (TYPEFORM-2): one question at a time fills the screen, centred in it.
42087
+ oneAtATime && "min-h-[min(70dvh,640px)]",
42088
+ theme.surface,
42089
+ theme.background !== "plain" && "rounded-lg px-5 sm:px-8",
42090
+ centred && "text-center",
42091
+ className
42092
+ ),
41920
42093
  style: theme.surfaceStyle,
41921
42094
  "data-records-form-font": theme.font,
41922
42095
  onKeyDown,
@@ -41926,20 +42099,41 @@ function FormStage({
41926
42099
  Progress,
41927
42100
  {
41928
42101
  value: live.length === 0 ? 0 : (oneAtATime ? index + 1 : answered) / live.length * 100,
41929
- className: "h-1 flex-1"
42102
+ className: "h-1 flex-1",
42103
+ "aria-label": "Form progress",
42104
+ ...routeKnown ? { "aria-valuetext": oneAtATime ? `${index + 1} of ${live.length}` : `${answered} of ${live.length}` } : {}
41930
42105
  }
41931
42106
  ),
41932
- /* @__PURE__ */ jsx132("span", { className: "tabular-nums", children: oneAtATime ? `${index + 1} of ${live.length}` : `${answered} of ${live.length}` })
42107
+ /* @__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}` })
41933
42108
  ] }),
41934
- 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,
41935
- 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,
41936
- unresolved.length > 0 ? /* @__PURE__ */ jsxs122("p", { className: "rounded border border-dashed px-2 py-1 text-left text-xs text-destructive", children: [
41937
- unresolved.map((q) => q.key).join(", "),
41938
- " ",
41939
- unresolved.length === 1 ? "is a question" : "are questions",
41940
- " 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."
41941
- ] }) : null,
41942
- form.intro && index === 0 ? /* @__PURE__ */ jsx132("p", { className: "text-sm text-muted-foreground", children: authored(host, form.intro, "form-intro") }) : null,
42109
+ signedInPublic ? /* @__PURE__ */ jsx132(
42110
+ "p",
42111
+ {
42112
+ className: "w-fit rounded border border-dashed px-2 py-0.5 text-left text-xs text-muted-foreground",
42113
+ title: "The record will carry your name. Strangers answer at the public link.",
42114
+ children: "Signed in \xB7 saved as you"
42115
+ }
42116
+ ) : null,
42117
+ unanswerable ? /* @__PURE__ */ jsx132(
42118
+ "p",
42119
+ {
42120
+ className: "w-fit rounded border border-dashed px-2 py-0.5 text-left text-xs text-muted-foreground",
42121
+ title: "Nothing here can work out which conditional questions apply, so all of them are shown.",
42122
+ children: "Showing every question"
42123
+ }
42124
+ ) : null,
42125
+ unresolved.length > 0 ? /* @__PURE__ */ jsxs122(
42126
+ "p",
42127
+ {
42128
+ className: "w-fit rounded border border-destructive/40 px-2 py-0.5 text-left text-xs text-destructive",
42129
+ title: "The table has no such field. Add the field, or take the question out.",
42130
+ children: [
42131
+ "Missing field: ",
42132
+ unresolved.map((q) => q.ask).join(", ")
42133
+ ]
42134
+ }
42135
+ ) : null,
42136
+ form.intro && index === 0 ? /* @__PURE__ */ jsx132("p", { className: "text-base text-muted-foreground", children: authored(host, form.intro, "form-intro") }) : null,
41943
42137
  error ? /* @__PURE__ */ jsx132(RefusalNotice, { error, className: "text-left" }) : null,
41944
42138
  refusal4 ? /* @__PURE__ */ jsx132(
41945
42139
  RefusalNotice,
@@ -41961,14 +42155,20 @@ function FormStage({
41961
42155
  (oneAtATime ? current ? [current] : [] : live).map((q) => /* @__PURE__ */ jsx132(
41962
42156
  Question,
41963
42157
  {
41964
- question: q,
42158
+ question: { ...q, ask: pipe(q.ask), help: q.help ? pipe(q.help) : null },
41965
42159
  value: answers[q.key],
41966
- onChange: (v) => setAnswers((prev) => ({ ...prev, [q.key]: v })),
42160
+ onChange: (v) => {
42161
+ if (stopped?.key === q.key) setStopped(null);
42162
+ setAnswers((prev) => ({ ...prev, [q.key]: v }));
42163
+ },
42164
+ onChosen: chosen,
42165
+ refused: stopped?.key === q.key ? stopped.say : null,
42166
+ large: oneAtATime,
41967
42167
  upload: host.upload
41968
42168
  },
41969
42169
  q.key
41970
42170
  )),
41971
- 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
42171
+ 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
41972
42172
  ] }),
41973
42173
  honeypotKey ? /* @__PURE__ */ jsxs122("div", { "aria-hidden": true, className: "pointer-events-none absolute left-[-9999px] h-px w-px overflow-hidden", children: [
41974
42174
  /* @__PURE__ */ jsx132("label", { htmlFor: `hp-${honeypotKey}`, children: "Leave this field empty" }),
@@ -41987,16 +42187,14 @@ function FormStage({
41987
42187
  }
41988
42188
  )
41989
42189
  ] }) : null,
41990
- /* @__PURE__ */ jsxs122("footer", { className: cn77("flex flex-wrap items-center gap-2", centred && "justify-center"), children: [
41991
- oneAtATime && index > 0 ? /* @__PURE__ */ jsx132(Button85, { size: "sm", variant: "ghost", className: theme.buttonClass, onClick: retreat, children: "Back" }) : null,
41992
- 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" }),
41993
- missing.length > 0 && (!oneAtATime || index === live.length - 1) ? /* @__PURE__ */ jsxs122("span", { className: "text-xs text-muted-foreground", children: [
41994
- missing.map((q) => q.ask).join(", "),
41995
- " still ",
41996
- missing.length === 1 ? "needs" : "need",
41997
- " an answer."
42190
+ /* @__PURE__ */ jsxs122("footer", { className: cn77("flex flex-wrap items-center gap-3", centred && "justify-center"), children: [
42191
+ oneAtATime && index > 0 ? /* @__PURE__ */ jsx132(Button85, { size: "lg", variant: "ghost", className: theme.buttonClass, onClick: retreat, children: "Back" }) : null,
42192
+ 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" }),
42193
+ oneAtATime ? /* @__PURE__ */ jsxs122("span", { className: "hidden text-xs text-muted-foreground sm:inline", "data-records-form-enter-hint": "", children: [
42194
+ "press ",
42195
+ /* @__PURE__ */ jsx132("strong", { className: "font-semibold text-foreground", children: "Enter \u21B5" })
41998
42196
  ] }) : null,
41999
- preview ? /* @__PURE__ */ jsx132("span", { className: "text-xs text-muted-foreground", children: "Preview \u2014 nothing is written." }) : null
42197
+ preview ? /* @__PURE__ */ jsx132("span", { className: "text-xs text-muted-foreground", title: "Nothing is written from a preview.", children: "Preview" }) : null
42000
42198
  ] })
42001
42199
  ]
42002
42200
  }
@@ -42008,12 +42206,15 @@ function ThankYou({
42008
42206
  done,
42009
42207
  className,
42010
42208
  ending,
42011
- buttonClass
42209
+ buttonClass,
42210
+ pipe = (t) => t
42012
42211
  }) {
42013
42212
  const host = useRecordsUi();
42014
42213
  const redirect = (ending ? ending.redirectUrl?.trim() : form.thankYou?.redirectUrl?.trim()) || null;
42015
- const title = ending ? ending.title : form.thankYou?.title;
42016
- const body = ending ? ending.body : form.thankYou?.body;
42214
+ const rawTitle = ending ? ending.title : form.thankYou?.title;
42215
+ const rawBody = ending ? ending.body : form.thankYou?.body;
42216
+ const title = rawTitle ? pipe(rawTitle) : rawTitle;
42217
+ const body = rawBody ? pipe(rawBody) : rawBody;
42017
42218
  const going = redirect !== null && done !== "preview";
42018
42219
  useEffect95(() => {
42019
42220
  if (!going || !redirect) return;
@@ -42030,8 +42231,8 @@ function ThankYou({
42030
42231
  }
42031
42232
  ),
42032
42233
  ending?.pictureUrl ? /* @__PURE__ */ jsx132("img", { src: ending.pictureUrl, alt: "", className: "mb-3 max-h-48 w-full rounded-md object-cover" }) : null,
42033
- /* @__PURE__ */ jsx132("h2", { className: "text-lg font-medium", "data-records-form-ending": ending?.id ?? "", children: title || "Thank you" }),
42034
- body ? /* @__PURE__ */ jsx132("p", { className: "mt-1 text-sm text-muted-foreground", children: authored(host, body, "form-thank-you") }) : null,
42234
+ /* @__PURE__ */ jsx132("h2", { className: "text-2xl font-semibold", "data-records-form-ending": ending?.id ?? "", children: title || "Thank you" }),
42235
+ body ? /* @__PURE__ */ jsx132("p", { className: "mt-2 text-base text-muted-foreground", children: authored(host, body, "form-thank-you") }) : null,
42035
42236
  going ? /* @__PURE__ */ jsxs122("p", { className: "mt-4 flex flex-wrap items-center gap-3 text-sm", children: [
42036
42237
  /* @__PURE__ */ jsx132(
42037
42238
  "a",
@@ -42044,11 +42245,11 @@ function ThankYou({
42044
42245
  ),
42045
42246
  /* @__PURE__ */ jsx132("span", { className: "text-muted-foreground", children: "Taking you there\u2026" })
42046
42247
  ] }) : null,
42047
- /* @__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" ? (
42248
+ /* @__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" ? (
42048
42249
  // SAID TO THE PERSON WHO ANSWERED (lane HANDOVER, 2026-09-29): "Saved to Form 1, with
42049
42250
  // this form stamped on it" was the owner's bookkeeping read out to a front-desk member
42050
42251
  // and to strangers. Where it landed is the owner's to see on the table.
42051
- "Your answers were received."
42252
+ "Answers received"
42052
42253
  ) : done })
42053
42254
  ] });
42054
42255
  }
@@ -42056,6 +42257,9 @@ function Question({
42056
42257
  question,
42057
42258
  value,
42058
42259
  onChange,
42260
+ onChosen,
42261
+ refused,
42262
+ large,
42059
42263
  upload
42060
42264
  }) {
42061
42265
  const host = useRecordsUi();
@@ -42063,297 +42267,62 @@ function Question({
42063
42267
  const field2 = question.field;
42064
42268
  if (!field2) return null;
42065
42269
  const id = `form-${field2.key}`;
42270
+ const sayId = `${id}-refused`;
42066
42271
  const isAttachment = editorKindFor(field2) === "attachment";
42067
- return /* @__PURE__ */ jsxs122("div", { className: "flex flex-col gap-1.5", children: [
42068
- /* @__PURE__ */ jsx132(FieldLabel, { field: { ...field2, label: question.ask, required: question.required }, htmlFor: id }),
42069
- question.help ? /* @__PURE__ */ jsx132("p", { className: "text-xs text-muted-foreground", children: authored(host, question.help, "form-help") }) : null,
42070
- 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: [
42071
- /* @__PURE__ */ jsx132(
42072
- "input",
42073
- {
42074
- id,
42075
- type: "file",
42076
- className: "text-xs",
42077
- onChange: (e) => {
42078
- const file = e.target.files?.[0];
42079
- if (!file) return;
42080
- setUploadError(null);
42081
- void upload?.(file).then((result) => {
42082
- if (!result) return;
42083
- if (result.ok) onChange(result.fileId);
42084
- else
42085
- setUploadError(
42086
- refusal("invalid_argument", result.reason, "Pick another file, or try this one again.")
42087
- );
42088
- });
42089
- }
42090
- }
42272
+ return /* @__PURE__ */ jsxs122(
42273
+ "div",
42274
+ {
42275
+ className: cn77(
42276
+ "flex flex-col",
42277
+ large ? "gap-3 [&_input]:text-base [&_textarea]:text-base [&_input]:min-h-12" : "gap-1.5 [&_input]:text-base [&_textarea]:text-base"
42091
42278
  ),
42092
- uploadError ? /* @__PURE__ */ jsx132(RefusalLine, { error: uploadError }) : null
42093
- ] }) : (
42094
- // A choice is laid out as pills or a searchable list in its own colours (`ChoiceAnswer`).
42095
- /* @__PURE__ */ jsx132(FieldControl, { field: field2, value, onChange, id, asForm: true })
42096
- )
42097
- ] });
42098
- }
42099
-
42100
- // src/NotifyRuleEditor.tsx
42101
- import { useCallback as useCallback54, useEffect as useEffect96, useState as useState110 } from "react";
42102
- import { useRecordsClient as useRecordsClient79, useTable as useTable26 } from "@ai-matrx/records/react";
42103
- import { hasQuietHours as hasQuietHours3 } from "@ai-matrx/records";
42104
- import { Button as Button86, Skeleton as Skeleton29, cn as cn78 } from "@ai-matrx/design-system";
42105
- import { jsx as jsx133, jsxs as jsxs123 } from "react/jsx-runtime";
42106
- var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
42107
- var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
42108
- function NotifyRuleEditor({ tableId, seed, chosenRuleId, onChoose, className }) {
42109
- const client = useRecordsClient79();
42110
- const host = useRecordsUi();
42111
- const table = useTable26(tableId);
42112
- const rights = useTableRights(table.data);
42113
- const [subscriptions, setSubscriptions] = useState110(null);
42114
- const [cadences, setCadences] = useState110([]);
42115
- const [views, setViews] = useState110(null);
42116
- const [error, setError] = useState110(null);
42117
- const [busy, setBusy] = useState110(false);
42118
- const [stopping, setStopping] = useState110(null);
42119
- const load = useCallback54(async () => {
42120
- const [held, offered] = await Promise.all([
42121
- // THE PERSON'S OWN DOOR, not the notifier's. It answers what is addressed
42122
- // to them plus — only where they hold admin on this Table — anyone's over
42123
- // it, and it is narrowed to Tables they can already open.
42124
- client.subscriptions({ table_id: tableId }),
42125
- // The cadences are ASKED of the store, never a list typed into a picker
42126
- // here that could drift from what the digest runner understands.
42127
- client.subscriptionCadences()
42128
- ]);
42129
- if (!held.ok) {
42130
- setError(held.error);
42131
- return;
42132
- }
42133
- setError(null);
42134
- setSubscriptions(held.data);
42135
- if (offered.ok) setCadences(offered.data);
42136
- if (host.savedViews) {
42137
- setViews(await host.savedViews());
42138
- return;
42139
- }
42140
- const listed = await client.views({ table_id: tableId });
42141
- if (!listed.ok) {
42142
- setError(listed.error);
42143
- return;
42144
- }
42145
- setViews(listed.data.map((v) => ({ id: v.view_id, name: v.name })));
42146
- }, [client, host, tableId]);
42147
- useEffect96(() => {
42148
- void load();
42149
- }, [load]);
42150
- const write = useCallback54(
42151
- async (spec) => {
42152
- const declared2 = await client.subscriptionDeclare({
42153
- table_id: tableId,
42154
- spec: {
42155
- name: spec.name,
42156
- saved_view_id: spec.savedViewId,
42157
- cadence: spec.cadence ?? "instant",
42158
- schedule: spec.schedule ?? null,
42159
- channel: spec.channel ?? "in_app",
42160
- quiet_hours: spec.quietHours ?? null,
42161
- // "Tell me" is the default, and the store refuses a subscription with
42162
- // nobody to tell rather than writing one that fires at nobody.
42163
- recipient_user_id: spec.recipientUserId ?? client.config.actor.user_id ?? null,
42164
- event_key: spec.eventKey ?? "records.changed"
42165
- }
42166
- });
42167
- if (!declared2.ok) {
42168
- setError(declared2.error);
42169
- return null;
42170
- }
42171
- return declared2.data;
42172
- },
42173
- [client, tableId]
42174
- );
42175
- useEffect96(() => {
42176
- if (!subscriptions || !seed || seed.length === 0) return;
42177
- const missing = seed.filter((s) => !subscriptions.some((held) => held.name === s.name));
42178
- if (missing.length === 0) return;
42179
- void (async () => {
42180
- for (const spec of missing) {
42181
- if (!await write(spec)) return;
42182
- }
42183
- await load();
42184
- })();
42185
- }, [subscriptions, seed, write, load]);
42186
- async function subscribe(viewId, viewName, cadence, channel, schedule, quiet2) {
42187
- setBusy(true);
42188
- const ok = await write({
42189
- name: `${viewName} \u2014 ${CADENCE_WORDS2[cadence] ?? cadence}`,
42190
- savedViewId: viewId,
42191
- cadence,
42192
- channel,
42193
- schedule: schedule.trim() === "" ? null : schedule.trim(),
42194
- quietHours: quiet2
42195
- });
42196
- setBusy(false);
42197
- if (!ok) return;
42198
- onChoose?.(ok);
42199
- await load();
42200
- }
42201
- async function makeEveryRecordView() {
42202
- setBusy(true);
42203
- const made = await client.viewDeclare({
42204
- table_id: tableId,
42205
- spec: { name: `Every ${table.data?.name ?? "record"}`, filters: null }
42206
- });
42207
- setBusy(false);
42208
- if (!made.ok) {
42209
- setError(made.error);
42210
- return;
42211
- }
42212
- await load();
42213
- }
42214
- async function unsubscribe(ruleId) {
42215
- setStopping(null);
42216
- setBusy(true);
42217
- const deleted = await client.recordDelete({ record_id: ruleId });
42218
- setBusy(false);
42219
- if (!deleted.ok) {
42220
- setError(deleted.error);
42221
- return;
42222
- }
42223
- await load();
42224
- }
42225
- if (error) {
42226
- return /* @__PURE__ */ jsx133(
42227
- RefusalNotice,
42228
- {
42229
- error,
42230
- className,
42231
- actions: /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
42232
- }
42233
- );
42234
- }
42235
- if (subscriptions === null) return /* @__PURE__ */ jsx133(Skeleton29, { className: cn78("h-40 w-full", className) });
42236
- return /* @__PURE__ */ jsxs123("div", { className: cn78("flex flex-col gap-2", className), children: [
42237
- /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
42238
- /* @__PURE__ */ jsx133("span", { className: "font-medium text-foreground", children: "Tell me when" }),
42239
- /* @__PURE__ */ jsxs123("span", { children: [
42240
- subscriptions.length,
42241
- " subscription",
42242
- subscriptions.length === 1 ? "" : "s"
42243
- ] })
42244
- ] }),
42245
- 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: [
42246
- /* @__PURE__ */ jsx133("span", { className: "truncate font-medium", children: held.name }),
42247
- /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: CADENCE_WORDS2[held.cadence] ?? held.cadence }),
42248
- /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: CHANNEL_WORDS3[held.channel] ?? held.channel }),
42249
- held.schedule ? /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: scheduleWords(held.schedule, held.quiet_hours?.tz) }) : null,
42250
- held.next_digest_at ? /* @__PURE__ */ jsxs123("span", { className: "text-muted-foreground", children: [
42251
- "next ",
42252
- new Date(held.next_digest_at).toLocaleString()
42253
- ] }) : null,
42254
- hasQuietHours3(held.quiet_hours) ? /* @__PURE__ */ jsxs123("span", { className: "text-muted-foreground", children: [
42255
- "quiet ",
42256
- held.quiet_hours.start,
42257
- "\u2013",
42258
- held.quiet_hours.end
42259
- ] }) : null,
42260
- held.recipient_user_id === null ? /* @__PURE__ */ jsx133("span", { className: "text-destructive", children: "This one has nobody to tell, so it fires at nobody." }) : null,
42261
- 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,
42262
- held.muted ? /* @__PURE__ */ jsx133("span", { className: "text-muted-foreground", children: "Switched off \u2014 it tells nobody until somebody switches it back on." }) : null,
42263
- onChoose ? held.rule_id === chosenRuleId ? /* @__PURE__ */ jsxs123("span", { className: "ml-auto flex items-center gap-1", children: [
42264
- /* @__PURE__ */ jsx133("span", { className: "font-medium text-foreground", children: "Used for this form" }),
42265
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", disabled: busy, onClick: () => onChoose(null), children: "Stop using" })
42266
- ] }) : /* @__PURE__ */ jsx133(
42267
- Button86,
42268
- {
42269
- size: "sm",
42270
- variant: "outline",
42271
- className: "ml-auto",
42272
- disabled: busy,
42273
- onClick: () => onChoose(held.rule_id),
42274
- children: "Use for this form"
42275
- }
42276
- ) : null,
42277
- 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: [
42278
- /* @__PURE__ */ jsx133("span", { className: "text-xs text-destructive", children: archiveConfirmSentence("this subscription") }),
42279
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "destructive", disabled: busy, onClick: () => void unsubscribe(held.rule_id), children: "Stop telling me" }),
42280
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: () => setStopping(null), children: "Keep it" })
42281
- ] }) : rights.write && !(onChoose && held.rule_id === chosenRuleId) ? /* @__PURE__ */ jsx133(
42282
- Button86,
42283
- {
42284
- size: "sm",
42285
- variant: "ghost",
42286
- className: onChoose ? void 0 : "ml-auto",
42287
- disabled: busy,
42288
- onClick: () => setStopping(held.rule_id),
42289
- children: "Stop telling me"
42290
- }
42291
- ) : null
42292
- ] }, held.rule_id)) }),
42293
- 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,
42294
- 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: [
42295
- /* @__PURE__ */ jsx133("span", { children: "A subscription watches a saved view, and this table has none yet." }),
42296
- /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "outline", disabled: busy, onClick: () => void makeEveryRecordView(), children: "Watch every record" })
42297
- ] }) : /* @__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(
42298
- "form",
42299
- {
42300
- className: "flex flex-wrap items-center gap-1",
42301
- onSubmit: (event) => {
42302
- event.preventDefault();
42303
- const form = event.currentTarget;
42304
- const view = form.elements.namedItem("view").value;
42305
- const cadence = form.elements.namedItem("cadence").value;
42306
- const channel = form.elements.namedItem("channel").value;
42307
- const schedule = form.elements.namedItem("schedule").value;
42308
- const start = form.elements.namedItem("quiet_start").value;
42309
- const end = form.elements.namedItem("quiet_end").value;
42310
- const name = views.find((v) => v.id === view)?.name ?? "View";
42311
- const quiet2 = start && end ? { start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone } : null;
42312
- if (view) void subscribe(view, name, cadence, channel, schedule, quiet2);
42313
- },
42314
- children: [
42315
- /* @__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)) }),
42316
- /* @__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)) }),
42317
- /* @__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)) }),
42318
- /* @__PURE__ */ jsx133(
42319
- "input",
42320
- {
42321
- name: "schedule",
42322
- "aria-label": "When the summary goes out",
42323
- placeholder: "monday 08:00",
42324
- className: "h-7 w-28 rounded border bg-background px-1 text-xs"
42325
- }
42326
- ),
42327
- /* @__PURE__ */ jsx133("span", { className: "text-xs text-muted-foreground", children: "not between" }),
42328
- /* @__PURE__ */ jsx133(
42329
- "input",
42330
- {
42331
- name: "quiet_start",
42332
- type: "time",
42333
- "aria-label": "Quiet hours start",
42334
- className: "h-7 rounded border bg-background px-1 text-xs"
42335
- }
42336
- ),
42337
- /* @__PURE__ */ jsx133("span", { className: "text-xs text-muted-foreground", children: "and" }),
42338
- /* @__PURE__ */ jsx133(
42279
+ "data-records-form-question": field2.key,
42280
+ "aria-describedby": refused ? sayId : void 0,
42281
+ children: [
42282
+ /* @__PURE__ */ jsxs122("div", { className: "flex flex-wrap items-baseline gap-x-2", children: [
42283
+ /* @__PURE__ */ jsx132(Label22, { htmlFor: id, className: cn77("font-medium leading-snug text-foreground", large ? "text-xl sm:text-2xl" : "text-base"), children: question.ask }),
42284
+ question.required ? /* @__PURE__ */ jsx132("span", { className: "text-sm text-foreground/80", children: "required" }) : null,
42285
+ field2.unit ? /* @__PURE__ */ jsx132("span", { className: "text-sm text-foreground/70", children: field2.unit }) : null
42286
+ ] }),
42287
+ question.help ? /* @__PURE__ */ jsx132("p", { className: cn77("text-muted-foreground", large ? "text-base" : "text-sm"), children: authored(host, question.help, "form-help") }) : null,
42288
+ 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: [
42289
+ /* @__PURE__ */ jsx132(
42339
42290
  "input",
42340
42291
  {
42341
- name: "quiet_end",
42342
- type: "time",
42343
- "aria-label": "Quiet hours end",
42344
- className: "h-7 rounded border bg-background px-1 text-xs"
42292
+ id,
42293
+ type: "file",
42294
+ className: "text-sm",
42295
+ onChange: (e) => {
42296
+ const file = e.target.files?.[0];
42297
+ if (!file) return;
42298
+ setUploadError(null);
42299
+ void upload?.(file).then((result) => {
42300
+ if (!result) return;
42301
+ if (result.ok) onChange(result.fileId);
42302
+ else
42303
+ setUploadError(
42304
+ refusal("invalid_argument", result.reason, "Pick another file, or try this one again.")
42305
+ );
42306
+ });
42307
+ }
42345
42308
  }
42346
42309
  ),
42347
- /* @__PURE__ */ jsx133(Button86, { size: "sm", type: "submit", disabled: busy, children: "Tell me" }),
42348
- /* @__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." })
42349
- ]
42350
- }
42351
- ) : /* @__PURE__ */ jsx133("p", { className: "text-xs text-muted-foreground", children: rights.why("write") })
42352
- ] });
42310
+ uploadError ? /* @__PURE__ */ jsx132(RefusalLine, { error: uploadError }) : null
42311
+ ] }) : (
42312
+ // A choice is laid out as lettered rows in the form's colour (`ChoiceAnswer`).
42313
+ /* @__PURE__ */ jsx132(FieldControl, { field: field2, value, onChange, id, asForm: true, onChosen })
42314
+ ),
42315
+ /* @__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: [
42316
+ /* @__PURE__ */ jsx132("span", { "aria-hidden": "true", children: "!" }),
42317
+ refused
42318
+ ] }) : null })
42319
+ ]
42320
+ }
42321
+ );
42353
42322
  }
42354
42323
 
42355
42324
  // src/FormBuilder.tsx
42356
- import { Fragment as Fragment54, jsx as jsx134, jsxs as jsxs124 } from "react/jsx-runtime";
42325
+ import { Fragment as Fragment54, jsx as jsx133, jsxs as jsxs123 } from "react/jsx-runtime";
42357
42326
  function freshFormName(base, taken) {
42358
42327
  const names = new Set(taken.map((n) => n.trim().toLowerCase()));
42359
42328
  if (!names.has(base.toLowerCase())) return base;
@@ -42370,7 +42339,21 @@ function isLookRefusal(error) {
42370
42339
  return Boolean(error && LOOK_REFUSAL.test(refusalSentence(error)));
42371
42340
  }
42372
42341
  var KEPT_DRAFT_PREFIX = "records-ui:form-draft:";
42373
- var SPEC_KEYS = ["name", "intro", "questions", "flow", "theme", "look", "thankYou", "submitLabel"];
42342
+ var SPEC_KEYS = [
42343
+ "name",
42344
+ "intro",
42345
+ "questions",
42346
+ "flow",
42347
+ "theme",
42348
+ "look",
42349
+ "thankYou",
42350
+ "submitLabel",
42351
+ "welcome",
42352
+ "endings",
42353
+ "hiddenFields",
42354
+ "showOwnerHeader",
42355
+ "autoAdvance"
42356
+ ];
42374
42357
  function readKeptDraft(formId) {
42375
42358
  try {
42376
42359
  const raw = window.localStorage.getItem(KEPT_DRAFT_PREFIX + formId);
@@ -42401,31 +42384,32 @@ function withKept(stored, spec) {
42401
42384
  return out;
42402
42385
  }
42403
42386
  function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne, createOnMount, className }) {
42404
- const client = useRecordsClient80();
42387
+ const client = useRecordsClient79();
42405
42388
  const host = useRecordsUi();
42406
42389
  const claimSeed = useSeedGuard();
42407
- const table = useTable27(tableId);
42390
+ const table = useTable26(tableId);
42408
42391
  const rights = useTableRights(table.data);
42409
42392
  const fields = useFields25(tableId);
42410
- const [forms, setForms] = useState111(null);
42411
- const [error, setError] = useState111(null);
42412
- const [refused, setRefused] = useState111(null);
42413
- const [activeId, setActiveId] = useState111(activeFormId ?? null);
42414
- const [draft, setDraft] = useState111(null);
42415
- const [saving, setSaving] = useState111(false);
42416
- const [saved, setSaved] = useState111(null);
42417
- const [publishing, setPublishing] = useState111(false);
42418
- const [copied, setCopied] = useState111(false);
42419
- const [shownUrl, setShownUrl] = useState111(null);
42420
- const [lookOptions, setLookOptions] = useState111(null);
42421
- const [lookUnread, setLookUnread] = useState111(null);
42422
- const [restoredAt, setRestoredAt] = useState111(null);
42423
- const [showResults, setShowResults] = useState111(false);
42424
- const [adding, setAdding] = useState111(false);
42393
+ const [forms, setForms] = useState110(null);
42394
+ const [error, setError] = useState110(null);
42395
+ const [refused, setRefused] = useState110(null);
42396
+ const [activeId, setActiveId] = useState110(activeFormId ?? null);
42397
+ const [draft, setDraft] = useState110(null);
42398
+ const [saving, setSaving] = useState110(false);
42399
+ const [saved, setSaved] = useState110(null);
42400
+ const [publishing, setPublishing] = useState110(false);
42401
+ const [copied, setCopied] = useState110(false);
42402
+ const [shownUrl, setShownUrl] = useState110(null);
42403
+ const [lookOptions, setLookOptions] = useState110(null);
42404
+ const [lookUnread, setLookUnread] = useState110(null);
42405
+ const [orgKnobs, setOrgKnobs] = useState110(null);
42406
+ const [restoredAt, setRestoredAt] = useState110(null);
42407
+ const [showResults, setShowResults] = useState110(false);
42408
+ const [adding, setAdding] = useState110(false);
42425
42409
  const keysBeforeAdd = useRef56(null);
42426
- const [makeOnMount, setMakeOnMount] = useState111("waiting");
42410
+ const [makeOnMount, setMakeOnMount] = useState110("waiting");
42427
42411
  const publicOrigin = host.publicOrigin ?? (typeof window === "undefined" ? "" : window.location.origin);
42428
- const load = useCallback55(async () => {
42412
+ const load = useCallback54(async () => {
42429
42413
  const answered = await client.forms({ table_id: tableId });
42430
42414
  if (!answered.ok) {
42431
42415
  setError(answered.error);
@@ -42452,10 +42436,19 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42452
42436
  setError(null);
42453
42437
  setForms(mine);
42454
42438
  }, [client, tableId, seed, claimSeed]);
42455
- useEffect97(() => {
42439
+ useEffect96(() => {
42456
42440
  void load();
42457
42441
  }, [load]);
42458
- useEffect97(() => {
42442
+ useEffect96(() => {
42443
+ let cancelled = false;
42444
+ Promise.resolve().then(() => client.formKnobs()).then((r) => {
42445
+ if (!cancelled && r.ok) setOrgKnobs(r.data);
42446
+ }).catch(() => void 0);
42447
+ return () => {
42448
+ cancelled = true;
42449
+ };
42450
+ }, [client]);
42451
+ useEffect96(() => {
42459
42452
  if (!forms || forms.length === 0) return;
42460
42453
  if (createOnMount && makeOnMount !== "made") return;
42461
42454
  const chosen = forms.find((f) => f.id === (activeFormId ?? activeId)) ?? forms[0];
@@ -42479,7 +42472,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42479
42472
  );
42480
42473
  const stored = useMemo68(() => draft ? (forms ?? []).find((f) => f.id === draft.id) ?? null : null, [draft, forms]);
42481
42474
  const dirty = Boolean(draft && stored && !sameForm(draft, stored));
42482
- useEffect97(() => {
42475
+ useEffect96(() => {
42483
42476
  if (!draft || !stored) return;
42484
42477
  const timer = window.setTimeout(() => {
42485
42478
  if (sameForm(draft, stored)) dropKeptDraft(draft.id);
@@ -42488,7 +42481,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42488
42481
  return () => window.clearTimeout(timer);
42489
42482
  }, [draft, stored]);
42490
42483
  const lookKey = draft ? JSON.stringify(formLookDocument(draft.look ?? null)) : null;
42491
- useEffect97(() => {
42484
+ useEffect96(() => {
42492
42485
  if (lookKey === null) return;
42493
42486
  let cancelled = false;
42494
42487
  const timer = window.setTimeout(() => {
@@ -42505,7 +42498,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42505
42498
  window.clearTimeout(timer);
42506
42499
  };
42507
42500
  }, [client, lookKey]);
42508
- useEffect97(() => {
42501
+ useEffect96(() => {
42509
42502
  const before = keysBeforeAdd.current;
42510
42503
  if (!before || !fields.data) return;
42511
42504
  const made = askableFields(fields.data).filter((f) => !before.has(f.key));
@@ -42544,7 +42537,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42544
42537
  }
42545
42538
  dropKeptDraft(draft.id);
42546
42539
  setRestoredAt(null);
42547
- setSaved("Saved.");
42540
+ setSaved("Saved");
42548
42541
  await load();
42549
42542
  return true;
42550
42543
  }
@@ -42583,7 +42576,22 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42583
42576
  setRefused(written.error);
42584
42577
  return;
42585
42578
  }
42586
- setSaved(ruleId ? "Saved \u2014 you will be told when somebody answers." : "Saved \u2014 nobody is told now.");
42579
+ setSaved("Saved");
42580
+ await load();
42581
+ }
42582
+ async function chooseWebhook(notify) {
42583
+ if (!draft) return;
42584
+ const next = { ...draft, notify };
42585
+ setDraft(next);
42586
+ setSaving(true);
42587
+ setRefused(null);
42588
+ const written = await client.formDeclare(formDeclareArgs(next));
42589
+ setSaving(false);
42590
+ if (!written.ok) {
42591
+ setRefused(written.error);
42592
+ return;
42593
+ }
42594
+ setSaved("Saved");
42587
42595
  await load();
42588
42596
  }
42589
42597
  async function create(name) {
@@ -42600,15 +42608,15 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42600
42608
  await load();
42601
42609
  return true;
42602
42610
  }
42603
- const [started, setStarted] = useState111(false);
42604
- useEffect97(() => {
42611
+ const [started, setStarted] = useState110(false);
42612
+ useEffect96(() => {
42605
42613
  if (!startWithOne || started || forms === null || forms.length > 0) return;
42606
42614
  if (askableFields(fields.data ?? []).length === 0) return;
42607
42615
  if (!table.data && !table.error) return;
42608
42616
  setStarted(true);
42609
42617
  void create(table.data?.name?.trim() ? `${table.data.name.trim()} form` : "Form 1");
42610
42618
  }, [startWithOne, started, forms, fields.data, create, table.data, table.error]);
42611
- useEffect97(() => {
42619
+ useEffect96(() => {
42612
42620
  if (!createOnMount || makeOnMount !== "waiting" || forms === null) return;
42613
42621
  if (!table.data && !table.error) return;
42614
42622
  if (askableFields(fields.data ?? []).length === 0) {
@@ -42621,396 +42629,325 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, startWithOne,
42621
42629
  (made) => setMakeOnMount(made ? "made" : "not-made")
42622
42630
  );
42623
42631
  }, [createOnMount, makeOnMount, forms, fields.data, create, table.data, table.error]);
42624
- if (fields.error) return /* @__PURE__ */ jsx134(RefusalNotice, { error: fields.error, className });
42632
+ if (fields.error) return /* @__PURE__ */ jsx133(RefusalNotice, { error: fields.error, className });
42625
42633
  if (error) {
42626
- return /* @__PURE__ */ jsx134(
42634
+ return /* @__PURE__ */ jsx133(
42627
42635
  RefusalNotice,
42628
42636
  {
42629
42637
  error,
42630
42638
  className,
42631
- actions: /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
42639
+ actions: /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
42632
42640
  }
42633
42641
  );
42634
42642
  }
42635
- if (fields.loading || forms === null) return /* @__PURE__ */ jsx134(Skeleton30, { className: cn79("h-64 w-full", className) });
42643
+ if (fields.loading || forms === null) return /* @__PURE__ */ jsx133(Skeleton29, { className: cn78("h-64 w-full", className) });
42636
42644
  if (createOnMount && (makeOnMount === "waiting" || makeOnMount === "making") && !refused) {
42637
- return /* @__PURE__ */ jsx134(Skeleton30, { className: cn79("h-64 w-full", className) });
42645
+ return /* @__PURE__ */ jsx133(Skeleton29, { className: cn78("h-64 w-full", className) });
42638
42646
  }
42639
42647
  if (!rights.admin) {
42640
- return /* @__PURE__ */ jsx134("p", { className: cn79("text-xs text-muted-foreground", className), children: rights.why("structure") });
42648
+ return /* @__PURE__ */ jsx133("p", { className: cn78("text-xs text-muted-foreground", className), children: rights.why("structure") });
42641
42649
  }
42642
- return /* @__PURE__ */ jsxs124("div", { className: cn79("@container flex min-h-0 flex-col gap-3 @3xl:flex-row", className), children: [
42643
- /* @__PURE__ */ jsxs124("div", { className: "flex min-w-0 flex-1 flex-col gap-2 overflow-auto", children: [
42644
- /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Forms", children: [
42645
- forms.map((form) => /* @__PURE__ */ jsx134(
42646
- Button87,
42647
- {
42648
- size: "sm",
42649
- variant: form.id === activeId ? "secondary" : "ghost",
42650
- "aria-pressed": form.id === activeId,
42651
- onClick: () => {
42652
- setActiveId(form.id);
42653
- setDraft(form);
42650
+ return (
42651
+ // THE CONTAINER IS THE PARENT (TYPEFORM-2): a container query asks an ANCESTOR, so `@3xl:` on the
42652
+ // element that is itself `@container` never fired and the preview sat 3,000 px below the editor.
42653
+ /* @__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: [
42654
+ /* @__PURE__ */ jsxs123("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
42655
+ /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Forms", children: [
42656
+ forms.map((form) => /* @__PURE__ */ jsx133(
42657
+ Button86,
42658
+ {
42659
+ size: "sm",
42660
+ variant: form.id === activeId ? "secondary" : "ghost",
42661
+ "aria-pressed": form.id === activeId,
42662
+ onClick: () => {
42663
+ setActiveId(form.id);
42664
+ setDraft(form);
42665
+ },
42666
+ children: form.name
42654
42667
  },
42655
- children: form.name
42656
- },
42657
- form.id
42658
- )),
42659
- (fields.data ?? []).length > 0 ? /* @__PURE__ */ jsx134(
42660
- Button87,
42661
- {
42662
- size: "sm",
42663
- variant: "ghost",
42664
- onClick: () => void create(
42665
- freshFormName(
42666
- table.data?.name?.trim() ? `${table.data.name.trim()} form` : "Form",
42667
- forms.map((f) => f.name)
42668
- )
42669
- ),
42670
- children: "New form"
42671
- }
42672
- ) : null,
42673
- /* @__PURE__ */ jsxs124("span", { className: "ml-auto flex items-center gap-2", children: [
42674
- saved ? /* @__PURE__ */ jsx134("span", { className: "text-xs text-muted-foreground", children: saved }) : null,
42675
- stored ? /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: showResults ? "default" : "ghost", onClick: () => setShowResults((v) => !v), "data-records-form-results-toggle": "", children: "Results" }) : null,
42676
- /* @__PURE__ */ jsx134(Button87, { size: "sm", disabled: saving || !draft, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
42677
- ] })
42678
- ] }),
42679
- refused && !isLookRefusal(refused) ? /* @__PURE__ */ jsx134(
42680
- RefusalNotice,
42681
- {
42682
- error: refused,
42683
- onKeepEditing: () => setRefused(null),
42684
- onDiscard: () => {
42685
- const kept = forms.find((f) => f.id === activeId) ?? null;
42686
- if (kept) dropKeptDraft(kept.id);
42687
- setDraft(kept);
42688
- setRestoredAt(null);
42689
- setSaved(null);
42690
- setRefused(null);
42691
- }
42692
- }
42693
- ) : null,
42694
- restoredAt && dirty && stored ? /* @__PURE__ */ jsxs124("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-form-restored": "", children: [
42695
- /* @__PURE__ */ jsxs124("span", { children: [
42696
- "Unsaved changes from",
42697
- " ",
42698
- new Date(restoredAt).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" }),
42699
- " restored"
42668
+ form.id
42669
+ )),
42670
+ (fields.data ?? []).length > 0 ? /* @__PURE__ */ jsx133(
42671
+ Button86,
42672
+ {
42673
+ size: "sm",
42674
+ variant: "ghost",
42675
+ onClick: () => void create(
42676
+ freshFormName(
42677
+ table.data?.name?.trim() ? `${table.data.name.trim()} form` : "Form",
42678
+ forms.map((f) => f.name)
42679
+ )
42680
+ ),
42681
+ children: "New form"
42682
+ }
42683
+ ) : null,
42684
+ /* @__PURE__ */ jsxs123("span", { className: "ml-auto flex items-center gap-2", children: [
42685
+ saved ? /* @__PURE__ */ jsx133("span", { className: "text-xs text-muted-foreground", children: saved }) : null,
42686
+ stored ? /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: showResults ? "default" : "ghost", onClick: () => setShowResults((v) => !v), "data-records-form-results-toggle": "", children: "Results" }) : null,
42687
+ /* @__PURE__ */ jsx133(Button86, { size: "sm", disabled: saving || !draft, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
42688
+ ] })
42700
42689
  ] }),
42701
- /* @__PURE__ */ jsx134(
42702
- Button87,
42690
+ refused && !isLookRefusal(refused) ? /* @__PURE__ */ jsx133(
42691
+ RefusalNotice,
42703
42692
  {
42704
- size: "sm",
42705
- variant: "ghost",
42706
- onClick: () => {
42707
- dropKeptDraft(stored.id);
42708
- setDraft(stored);
42693
+ error: refused,
42694
+ onKeepEditing: () => setRefused(null),
42695
+ onDiscard: () => {
42696
+ const kept = forms.find((f) => f.id === activeId) ?? null;
42697
+ if (kept) dropKeptDraft(kept.id);
42698
+ setDraft(kept);
42709
42699
  setRestoredAt(null);
42710
- },
42711
- children: "Discard"
42712
- }
42713
- )
42714
- ] }) : null,
42715
- !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: [
42716
- /* @__PURE__ */ jsx134(
42717
- PublishRow,
42718
- {
42719
- url: `${publicOrigin}${publicFormPath(draft.id)}`,
42720
- state: draft.state,
42721
- mayPublish: rights.structure,
42722
- why: rights.why("structure"),
42723
- busy: publishing,
42724
- copied,
42725
- shownUrl,
42726
- onPublish: (wanted) => void publish(wanted),
42727
- onCopy: (url) => void copyLink(url),
42728
- unsaved: dirty
42700
+ setSaved(null);
42701
+ setRefused(null);
42702
+ }
42729
42703
  }
42730
- ),
42731
- /* @__PURE__ */ jsx134(EmbedSnippets, { url: `${publicOrigin}${publicFormPath(draft.id)}`, label: draft.name }),
42732
- /* @__PURE__ */ jsxs124("section", { className: "flex flex-col gap-1 rounded border px-2.5 py-2", "aria-label": "When somebody answers", children: [
42733
- /* @__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." }),
42734
- /* @__PURE__ */ jsx134(
42735
- NotifyRuleEditor,
42704
+ ) : null,
42705
+ restoredAt && dirty && stored ? /* @__PURE__ */ jsxs123("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", "data-records-form-restored": "", children: [
42706
+ /* @__PURE__ */ jsxs123("span", { children: [
42707
+ "Unsaved changes from",
42708
+ " ",
42709
+ new Date(restoredAt).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" }),
42710
+ " restored"
42711
+ ] }),
42712
+ /* @__PURE__ */ jsx133(
42713
+ Button86,
42736
42714
  {
42737
- tableId,
42738
- chosenRuleId: draft.notifyRuleId,
42739
- onChoose: (ruleId) => void chooseNotify(ruleId)
42715
+ size: "sm",
42716
+ variant: "ghost",
42717
+ onClick: () => {
42718
+ dropKeptDraft(stored.id);
42719
+ setDraft(stored);
42720
+ setRestoredAt(null);
42721
+ },
42722
+ children: "Discard"
42740
42723
  }
42741
42724
  )
42742
- ] }),
42743
- /* @__PURE__ */ jsxs124("section", { className: "flex flex-col gap-2 rounded border px-2.5 py-2", "aria-label": "After somebody sends it", children: [
42744
- /* @__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." }),
42745
- /* @__PURE__ */ jsxs124("div", { className: "grid grid-cols-2 gap-2", children: [
42746
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42747
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Thank-you title" }),
42748
- /* @__PURE__ */ jsx134(
42749
- BasicInput28,
42750
- {
42751
- value: draft.thankYou?.title ?? "",
42752
- placeholder: "Thank you",
42753
- onChange: (e) => patch({ thankYou: { ...draft.thankYou, title: e.target.value || null } })
42754
- }
42755
- )
42756
- ] }),
42757
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42758
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Then go to (optional)" }),
42759
- /* @__PURE__ */ jsx134(
42760
- BasicInput28,
42725
+ ] }) : null,
42726
+ !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: [
42727
+ /* @__PURE__ */ jsx133(
42728
+ PublishRow,
42729
+ {
42730
+ url: `${publicOrigin}${publicFormPath(draft.id)}`,
42731
+ state: draft.state,
42732
+ mayPublish: rights.structure,
42733
+ why: rights.why("structure"),
42734
+ busy: publishing,
42735
+ copied,
42736
+ shownUrl,
42737
+ onPublish: (wanted) => void publish(wanted),
42738
+ onCopy: (url) => void copyLink(url),
42739
+ unsaved: dirty
42740
+ }
42741
+ ),
42742
+ /* @__PURE__ */ jsx133(EmbedSnippets, { url: `${publicOrigin}${publicFormPath(draft.id)}`, label: draft.name }),
42743
+ /* @__PURE__ */ jsx133(
42744
+ NotifyOnAnswer,
42745
+ {
42746
+ tableId,
42747
+ tableName: table.data?.name ?? "record",
42748
+ formName: draft.name,
42749
+ ruleId: draft.notifyRuleId,
42750
+ notify: draft.notify ?? null,
42751
+ emailQuestion: (
42752
+ // An email field first; else a question whose field is named for an email (an imported
42753
+ // "Contact email" kept as text). The store's resolver refuses an answer that is not one.
42754
+ (draft.questions.map((q) => byKey.get(q.field)).find((f) => f && String(editorKindFor(f)) === "email") ?? draft.questions.map((q) => byKey.get(q.field)).find((f) => f && /e-?mail/i.test(`${f.key} ${f.label ?? ""}`)))?.key ?? null
42755
+ ),
42756
+ onRule: (ruleId) => void chooseNotify(ruleId),
42757
+ onNotify: (notify) => void chooseWebhook(notify)
42758
+ }
42759
+ ),
42760
+ /* @__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: [
42761
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", title: "Off hides your logo and name \u2014 for a client's form.", children: [
42762
+ /* @__PURE__ */ jsx133(
42763
+ Checkbox15,
42761
42764
  {
42762
- type: "url",
42763
- inputMode: "url",
42764
- value: draft.thankYou?.redirectUrl ?? "",
42765
- placeholder: "https://",
42766
- onChange: (e) => patch({ thankYou: { ...draft.thankYou, redirectUrl: e.target.value || null } })
42765
+ checked: draft.showOwnerHeader ?? orgKnobs?.show_owner_header ?? true,
42766
+ onCheckedChange: (checked) => patch({ showOwnerHeader: checked === true }),
42767
+ "aria-label": "Show our logo and name"
42767
42768
  }
42768
- )
42769
+ ),
42770
+ "Show our logo and name"
42769
42771
  ] }),
42770
- /* @__PURE__ */ jsxs124("label", { className: "col-span-2 flex flex-col gap-1", children: [
42771
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Thank-you message" }),
42772
- /* @__PURE__ */ jsx134(
42773
- BasicTextarea11,
42772
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", title: "A single choice goes on to the next question when picked.", children: [
42773
+ /* @__PURE__ */ jsx133(
42774
+ Checkbox15,
42774
42775
  {
42775
- rows: 2,
42776
- value: draft.thankYou?.body ?? "",
42777
- placeholder: "What happens next, in your words.",
42778
- onChange: (e) => patch({ thankYou: { ...draft.thankYou, body: e.target.value || null } })
42776
+ checked: draft.autoAdvance ?? orgKnobs?.choice_auto_advance ?? true,
42777
+ onCheckedChange: (checked) => patch({ autoAdvance: checked === true }),
42778
+ "aria-label": "Next after a choice"
42779
42779
  }
42780
- )
42780
+ ),
42781
+ "Next after a choice"
42781
42782
  ] })
42782
- ] })
42783
- ] }),
42784
- /* @__PURE__ */ jsx134(WelcomeEditor, { value: draft.welcome, pictures: lookOptions?.pictures ?? null, onChange: (welcome) => patch({ welcome }) }),
42785
- /* @__PURE__ */ jsx134(
42786
- EndingsEditor,
42787
- {
42788
- endings: draft.endings ?? [],
42789
- pictures: lookOptions?.pictures ?? null,
42790
- fieldKeys: conditionFields,
42791
- scored: draft.questions.some((q) => q.points),
42792
- onChange: (endings) => patch({ endings })
42793
- }
42794
- ),
42795
- /* @__PURE__ */ jsxs124("div", { className: "grid grid-cols-2 gap-2", children: [
42796
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42797
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Name" }),
42798
- /* @__PURE__ */ jsx134(BasicInput28, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) })
42799
- ] }),
42800
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42801
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Flow" }),
42802
- /* @__PURE__ */ jsx134(
42803
- "select",
42804
- {
42805
- className: "h-9 rounded border bg-background px-2 text-sm",
42806
- value: draft.flow ?? "one-at-a-time",
42807
- onChange: (e) => patch({ flow: e.target.value }),
42808
- 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))
42809
- }
42810
- )
42811
42783
  ] }),
42812
- /* @__PURE__ */ jsxs124("label", { className: "col-span-2 flex flex-col gap-1", children: [
42813
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Intro" }),
42814
- /* @__PURE__ */ jsx134(
42815
- BasicTextarea11,
42816
- {
42817
- rows: 2,
42818
- value: draft.intro ?? "",
42819
- onChange: (e) => patch({ intro: e.target.value || null })
42820
- }
42821
- )
42822
- ] }),
42823
- /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
42824
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Submit button" }),
42825
- /* @__PURE__ */ jsx134(
42826
- BasicInput28,
42827
- {
42828
- value: draft.submitLabel ?? "",
42829
- placeholder: "Submit",
42830
- onChange: (e) => patch({ submitLabel: e.target.value || null })
42831
- }
42832
- )
42784
+ /* @__PURE__ */ jsxs123("section", { className: "flex flex-col gap-2 rounded border px-2.5 py-2", "aria-label": "After somebody sends it", children: [
42785
+ /* @__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" }),
42786
+ /* @__PURE__ */ jsxs123("div", { className: "grid grid-cols-2 gap-2", children: [
42787
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42788
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Thank-you title" }),
42789
+ /* @__PURE__ */ jsx133(
42790
+ BasicInput28,
42791
+ {
42792
+ value: draft.thankYou?.title ?? "",
42793
+ placeholder: "Thank you",
42794
+ onChange: (e) => patch({ thankYou: { ...draft.thankYou, title: e.target.value || null } })
42795
+ }
42796
+ )
42797
+ ] }),
42798
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42799
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Then go to (optional)" }),
42800
+ /* @__PURE__ */ jsx133(
42801
+ BasicInput28,
42802
+ {
42803
+ type: "url",
42804
+ inputMode: "url",
42805
+ value: draft.thankYou?.redirectUrl ?? "",
42806
+ placeholder: "https://",
42807
+ onChange: (e) => patch({ thankYou: { ...draft.thankYou, redirectUrl: e.target.value || null } })
42808
+ }
42809
+ )
42810
+ ] }),
42811
+ /* @__PURE__ */ jsxs123("label", { className: "col-span-2 flex flex-col gap-1", children: [
42812
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Thank-you message" }),
42813
+ /* @__PURE__ */ jsx133(
42814
+ BasicTextarea11,
42815
+ {
42816
+ rows: 2,
42817
+ value: draft.thankYou?.body ?? "",
42818
+ placeholder: "What happens next, in your words.",
42819
+ onChange: (e) => patch({ thankYou: { ...draft.thankYou, body: e.target.value || null } })
42820
+ }
42821
+ )
42822
+ ] })
42823
+ ] })
42833
42824
  ] }),
42834
- /* @__PURE__ */ jsx134(HiddenFieldsEditor, { value: draft.hiddenFields ?? [], onChange: (hiddenFields) => patch({ hiddenFields }) })
42835
- ] }),
42836
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1.5", "data-records-form-look-editor": "", children: [
42837
- /* @__PURE__ */ jsx134(Label22, { className: "text-xs font-medium", children: "Look" }),
42838
- /* @__PURE__ */ jsx134(
42839
- AccentSwatches,
42825
+ /* @__PURE__ */ jsx133(WelcomeEditor, { value: draft.welcome, pictures: lookOptions?.pictures ?? null, onChange: (welcome) => patch({ welcome }) }),
42826
+ /* @__PURE__ */ jsx133(
42827
+ EndingsEditor,
42840
42828
  {
42841
- value: draft.look?.accent ?? "",
42842
- noneLabel: "Organization colour",
42843
- onChange: (accent) => patchLook({ accent: accent || null })
42829
+ endings: draft.endings ?? [],
42830
+ pictures: lookOptions?.pictures ?? null,
42831
+ fieldKeys: conditionFields,
42832
+ scored: draft.questions.some((q) => q.points),
42833
+ onChange: (endings) => patch({ endings })
42844
42834
  }
42845
42835
  ),
42846
- /* @__PURE__ */ jsx134(ThemeEditor, { theme: draft.theme, pictures: lookOptions?.pictures ?? null, onChange: (theme) => patch({ theme }) }),
42847
- lookOptions ? /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-2", children: [
42848
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1", children: [
42849
- /* @__PURE__ */ jsx134("span", { className: "text-xs", children: "Logo" }),
42850
- /* @__PURE__ */ jsx134(
42851
- PublicPicturePicker,
42836
+ /* @__PURE__ */ jsxs123("div", { className: "grid grid-cols-2 gap-2", children: [
42837
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42838
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Name" }),
42839
+ /* @__PURE__ */ jsx133(BasicInput28, { value: draft.name, onChange: (e) => patch({ name: e.target.value }) })
42840
+ ] }),
42841
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42842
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Flow" }),
42843
+ /* @__PURE__ */ jsx133(
42844
+ "select",
42852
42845
  {
42853
- label: "Logo",
42854
- pictures: lookOptions.pictures,
42855
- value: draft.look?.logoFileId ?? "",
42856
- noneLabel: "Organization logo",
42857
- onChange: (id) => patchLook({ logoFileId: id || null })
42846
+ className: "h-9 rounded border bg-background px-2 text-sm",
42847
+ value: draft.flow ?? "one-at-a-time",
42848
+ onChange: (e) => patch({ flow: e.target.value }),
42849
+ 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))
42858
42850
  }
42859
42851
  )
42860
42852
  ] }),
42861
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1", children: [
42862
- /* @__PURE__ */ jsx134("span", { className: "text-xs", children: "Cover" }),
42863
- /* @__PURE__ */ jsx134(
42864
- PublicPicturePicker,
42853
+ /* @__PURE__ */ jsxs123("label", { className: "col-span-2 flex flex-col gap-1", children: [
42854
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Intro" }),
42855
+ /* @__PURE__ */ jsx133(
42856
+ BasicTextarea11,
42865
42857
  {
42866
- label: "Cover",
42867
- pictures: lookOptions.pictures,
42868
- value: draft.look?.coverFileId ?? "",
42869
- noneLabel: "No cover",
42870
- onChange: (id) => patchLook({ coverFileId: id || null })
42858
+ rows: 2,
42859
+ value: draft.intro ?? "",
42860
+ onChange: (e) => patch({ intro: e.target.value || null })
42871
42861
  }
42872
42862
  )
42873
- ] })
42874
- ] }) : lookUnread ? null : /* @__PURE__ */ jsx134(Skeleton30, { className: "h-12 w-full" }),
42875
- lookUnread ? /* @__PURE__ */ jsx134(RefusalLine, { error: lookUnread }) : null,
42876
- refused && isLookRefusal(refused) ? /* @__PURE__ */ jsx134(RefusalLine, { error: refused }) : null
42877
- ] }),
42878
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1", children: [
42879
- /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
42880
- /* @__PURE__ */ jsx134("span", { className: "font-medium text-foreground", children: "Questions" }),
42881
- /* @__PURE__ */ jsxs124("span", { children: [
42882
- draft.questions.length,
42883
- " of this table's ",
42884
- fields.data?.length ?? 0,
42885
- " fields"
42886
- ] })
42863
+ ] }),
42864
+ /* @__PURE__ */ jsxs123("label", { className: "flex flex-col gap-1", children: [
42865
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Submit button" }),
42866
+ /* @__PURE__ */ jsx133(
42867
+ BasicInput28,
42868
+ {
42869
+ value: draft.submitLabel ?? "",
42870
+ placeholder: "Submit",
42871
+ onChange: (e) => patch({ submitLabel: e.target.value || null })
42872
+ }
42873
+ )
42874
+ ] }),
42875
+ /* @__PURE__ */ jsx133(HiddenFieldsEditor, { value: draft.hiddenFields ?? [], onChange: (hiddenFields) => patch({ hiddenFields }) })
42887
42876
  ] }),
42888
- askableFields(fields.data ?? []).map((field2) => {
42889
- const index = draft.questions.findIndex((q) => q.field === field2.key);
42890
- const asked3 = index >= 0;
42891
- const question = asked3 ? draft.questions[index] : null;
42892
- return /* @__PURE__ */ jsxs124("div", { className: "rounded border px-2 py-1.5", children: [
42893
- /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-2", children: [
42894
- /* @__PURE__ */ jsx134(
42895
- Checkbox15,
42877
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1.5", "data-records-form-look-editor": "", children: [
42878
+ /* @__PURE__ */ jsx133(Label23, { className: "text-xs font-medium", children: "Look" }),
42879
+ /* @__PURE__ */ jsx133(
42880
+ AccentSwatches,
42881
+ {
42882
+ value: draft.look?.accent ?? "",
42883
+ noneLabel: "Organization colour",
42884
+ onChange: (accent) => patchLook({ accent: accent || null })
42885
+ }
42886
+ ),
42887
+ /* @__PURE__ */ jsx133(ThemeEditor, { theme: draft.theme, pictures: lookOptions?.pictures ?? null, onChange: (theme) => patch({ theme }) }),
42888
+ lookOptions ? /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-2", children: [
42889
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1", children: [
42890
+ /* @__PURE__ */ jsx133("span", { className: "text-xs", children: "Logo" }),
42891
+ /* @__PURE__ */ jsx133(
42892
+ PublicPicturePicker,
42896
42893
  {
42897
- checked: asked3,
42898
- "aria-label": `Ask for ${fieldName(field2)}`,
42899
- onCheckedChange: (checked) => patch({
42900
- questions: checked ? [...draft.questions, { field: field2.key }] : draft.questions.filter((q) => q.field !== field2.key)
42901
- })
42894
+ label: "Logo",
42895
+ pictures: lookOptions.pictures,
42896
+ value: draft.look?.logoFileId ?? "",
42897
+ noneLabel: "Organization logo",
42898
+ onChange: (id) => patchLook({ logoFileId: id || null })
42902
42899
  }
42903
- ),
42904
- /* @__PURE__ */ jsx134("span", { className: "text-xs font-medium", children: fieldName(field2) }),
42905
- field2.required ? /* @__PURE__ */ jsx134("span", { className: "text-xs text-destructive", children: "required" }) : null
42900
+ )
42906
42901
  ] }),
42907
- asked3 && question ? /* @__PURE__ */ jsxs124("div", { className: "mt-1.5 grid grid-cols-2 gap-2 pl-6", children: [
42908
- /* @__PURE__ */ jsx134(
42909
- BasicInput28,
42910
- {
42911
- className: "h-8 text-xs",
42912
- placeholder: fieldName(field2),
42913
- "aria-label": `How to ask for ${fieldName(field2)}`,
42914
- value: question.ask ?? "",
42915
- onChange: (e) => patchQuestion(index, { ask: e.target.value || null })
42916
- }
42917
- ),
42918
- /* @__PURE__ */ jsx134(
42919
- BasicInput28,
42902
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1", children: [
42903
+ /* @__PURE__ */ jsx133("span", { className: "text-xs", children: "Cover" }),
42904
+ /* @__PURE__ */ jsx133(
42905
+ PublicPicturePicker,
42920
42906
  {
42921
- className: "h-8 text-xs",
42922
- placeholder: "Help text",
42923
- "aria-label": `Help text for ${fieldName(field2)}`,
42924
- value: question.help ?? "",
42925
- onChange: (e) => patchQuestion(index, { help: e.target.value || null })
42926
- }
42927
- ),
42928
- /* @__PURE__ */ jsxs124("label", { className: "col-span-2 flex items-center gap-2 text-xs", children: [
42929
- /* @__PURE__ */ jsx134(
42930
- Checkbox15,
42931
- {
42932
- checked: field2.required ? true : question.required === true,
42933
- disabled: field2.required === true,
42934
- "aria-label": `${fieldName(field2)} is required`,
42935
- onCheckedChange: (checked) => patchQuestion(index, { required: checked === true ? true : null })
42936
- }
42937
- ),
42938
- "Required"
42939
- ] }),
42940
- /* @__PURE__ */ jsx134(
42941
- Condition,
42942
- {
42943
- question,
42944
- fieldKeys: (fields.data ?? []).map((f) => ({
42945
- id: String(f.id),
42946
- key: f.key,
42947
- label: f.label || f.key,
42948
- field: f
42949
- })),
42950
- onChange: (showIf) => patchQuestion(index, { showIf })
42951
- }
42952
- ),
42953
- /* @__PURE__ */ jsx134(
42954
- QuestionLogic,
42955
- {
42956
- question,
42957
- index,
42958
- questions: draft.questions,
42959
- endings: draft.endings ?? [],
42960
- field: field2,
42961
- fieldKeys: conditionFields,
42962
- label: (key) => {
42963
- const f = byKey.get(key);
42964
- return draft.questions.find((q) => q.field === key)?.ask || (f ? fieldName(f) : key);
42965
- },
42966
- onChange: (part) => patchQuestion(index, part)
42907
+ label: "Cover",
42908
+ pictures: lookOptions.pictures,
42909
+ value: draft.look?.coverFileId ?? "",
42910
+ noneLabel: "No cover",
42911
+ onChange: (id) => patchLook({ coverFileId: id || null })
42967
42912
  }
42968
42913
  )
42969
- ] }) : null
42970
- ] }, field2.id);
42971
- }),
42972
- whyNotAskable(fields.data ?? []) ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: whyNotAskable(fields.data ?? []) }) : null,
42973
- requiredLeftUnasked(fields.data ?? [], draft.questions.map((q) => q.field)) ? /* @__PURE__ */ jsx134(
42974
- ErrorBox7,
42975
- {
42976
- className: "rounded border border-destructive/40 bg-destructive/5 px-2 py-1.5 text-xs text-destructive",
42977
- message: requiredLeftUnasked(fields.data ?? [], draft.questions.map((q) => q.field)) ?? "",
42978
- operation: "Build a form",
42979
- origin: "records-ui/FormBuilder"
42980
- }
42981
- ) : null,
42982
- draft.questions.filter((q) => !byKey.has(q.field)).map((q) => /* @__PURE__ */ jsxs124("p", { className: "text-xs text-destructive", children: [
42983
- 'This form asks for "',
42984
- q.field,
42985
- '" and this table has no such Field. It is shown here rather than quietly dropped.'
42986
- ] }, q.field)),
42987
- adding ? /* @__PURE__ */ jsx134("div", { className: "rounded border p-2", "data-records-form-add-question": "", children: /* @__PURE__ */ jsx134(
42988
- FieldEditor,
42914
+ ] })
42915
+ ] }) : lookUnread ? null : /* @__PURE__ */ jsx133(Skeleton29, { className: "h-12 w-full" }),
42916
+ lookUnread ? /* @__PURE__ */ jsx133(RefusalLine, { error: lookUnread }) : null,
42917
+ refused && isLookRefusal(refused) ? /* @__PURE__ */ jsx133(RefusalLine, { error: refused }) : null
42918
+ ] }),
42919
+ /* @__PURE__ */ jsx133(
42920
+ QuestionList,
42989
42921
  {
42990
42922
  tableId,
42991
- onCancel: () => {
42992
- keysBeforeAdd.current = null;
42993
- setAdding(false);
42994
- },
42995
- onSaved: () => setAdding(false)
42996
- }
42997
- ) }) : /* @__PURE__ */ jsx134("div", { children: /* @__PURE__ */ jsx134(
42998
- Button87,
42999
- {
43000
- size: "sm",
43001
- variant: "outline",
43002
- onClick: () => {
43003
- keysBeforeAdd.current = new Set((fields.data ?? []).map((f) => f.key));
43004
- setAdding(true);
42923
+ draft,
42924
+ fields: fields.data ?? [],
42925
+ byKey,
42926
+ adding,
42927
+ onAdding: (on) => {
42928
+ keysBeforeAdd.current = on ? new Set((fields.data ?? []).map((f) => f.key)) : null;
42929
+ setAdding(on);
43005
42930
  },
43006
- children: "Add a question"
42931
+ onAdded: () => setAdding(false),
42932
+ onQuestions: (questions) => patch({ questions }),
42933
+ onQuestion: patchQuestion
43007
42934
  }
43008
- ) })
42935
+ )
43009
42936
  ] })
43010
- ] })
43011
- ] }),
43012
- 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
43013
- ] });
42937
+ ] }),
42938
+ draft ? (
42939
+ // BESIDE THE EDITOR, ALWAYS IN VIEW (TYPEFORM-2): it follows the scroll and updates as you type.
42940
+ /* @__PURE__ */ jsx133(
42941
+ "aside",
42942
+ {
42943
+ 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)]",
42944
+ "data-records-form-preview": "",
42945
+ children: showResults && stored ? /* @__PURE__ */ jsx133(FormResultsPanel, { form: stored }) : /* @__PURE__ */ jsx133(FormLookFrame, { look: lookOptions?.look ?? null, title: draft.name, contained: true, showOwnerHeader: draft.showOwnerHeader ?? orgKnobs?.show_owner_header ?? true, children: /* @__PURE__ */ jsx133(FormRunner, { form: draft, preview: true, autoAdvance: draft.autoAdvance ?? orgKnobs?.choice_auto_advance ?? true }) })
42946
+ }
42947
+ )
42948
+ ) : null
42949
+ ] }) })
42950
+ );
43014
42951
  }
43015
42952
  function PublishRow({
43016
42953
  url,
@@ -43025,10 +42962,10 @@ function PublishRow({
43025
42962
  unsaved = false
43026
42963
  }) {
43027
42964
  const open = state === "open";
43028
- return /* @__PURE__ */ jsxs124("div", { className: "flex flex-col gap-1.5 rounded border px-2.5 py-2", children: [
43029
- /* @__PURE__ */ jsxs124("div", { className: "flex flex-wrap items-center gap-2", children: [
43030
- mayPublish ? /* @__PURE__ */ jsx134(
43031
- Button87,
42965
+ return /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1.5 rounded border px-2.5 py-2", children: [
42966
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-wrap items-center gap-2", children: [
42967
+ mayPublish ? /* @__PURE__ */ jsx133(
42968
+ Button86,
43032
42969
  {
43033
42970
  size: "sm",
43034
42971
  variant: open ? "ghost" : "default",
@@ -43037,8 +42974,8 @@ function PublishRow({
43037
42974
  children: busy ? "\u2026" : open ? "Unpublish" : unsaved ? "Save and publish" : "Publish"
43038
42975
  }
43039
42976
  ) : null,
43040
- state !== "draft" ? /* @__PURE__ */ jsxs124(Fragment54, { children: [
43041
- /* @__PURE__ */ jsx134(
42977
+ state !== "draft" ? /* @__PURE__ */ jsxs123(Fragment54, { children: [
42978
+ /* @__PURE__ */ jsx133(
43042
42979
  "a",
43043
42980
  {
43044
42981
  href: url,
@@ -43048,14 +42985,14 @@ function PublishRow({
43048
42985
  children: url
43049
42986
  }
43050
42987
  ),
43051
- /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: "outline", onClick: () => onCopy(url), children: copied ? "Copied" : "Copy link" })
42988
+ /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "outline", onClick: () => onCopy(url), children: copied ? "Copied" : "Copy link" })
43052
42989
  ] }) : null
43053
42990
  ] }),
43054
- /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: FORM_STATE_WORDS[state] }),
43055
- 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,
43056
- !mayPublish ? /* @__PURE__ */ jsx134("p", { className: "text-xs text-muted-foreground", children: why }) : null,
43057
- shownUrl ? /* @__PURE__ */ jsxs124("p", { className: "break-all rounded border border-dashed px-2 py-1 text-xs", children: [
43058
- "This browser would not let the page copy for you, so here it is to copy by hand: ",
42991
+ /* @__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] }),
42992
+ 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,
42993
+ !mayPublish ? /* @__PURE__ */ jsx133("p", { className: "w-fit text-xs text-muted-foreground", title: why, children: "Publishing needs table admin" }) : null,
42994
+ 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: [
42995
+ "Copy by hand: ",
43059
42996
  shownUrl
43060
42997
  ] }) : null
43061
42998
  ] });
@@ -43065,7 +43002,7 @@ function Condition({
43065
43002
  fieldKeys,
43066
43003
  onChange
43067
43004
  }) {
43068
- return /* @__PURE__ */ jsx134(
43005
+ return /* @__PURE__ */ jsx133(
43069
43006
  ConditionGroup,
43070
43007
  {
43071
43008
  lead: "Ask only when",
@@ -43076,26 +43013,348 @@ function Condition({
43076
43013
  }
43077
43014
  );
43078
43015
  }
43016
+ var FORM_STATE_LABELS = {
43017
+ draft: "Not published",
43018
+ open: "Open",
43019
+ closed: "Closed",
43020
+ full: "Full"
43021
+ };
43022
+ function QuestionList({
43023
+ tableId,
43024
+ draft,
43025
+ fields,
43026
+ byKey,
43027
+ adding,
43028
+ onAdding,
43029
+ onAdded,
43030
+ onQuestions,
43031
+ onQuestion
43032
+ }) {
43033
+ const sensors = useSensors(
43034
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
43035
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
43036
+ );
43037
+ const askable = askableFields([...fields]);
43038
+ const unasked = askable.filter((f) => !draft.questions.some((q) => q.field === f.key));
43039
+ 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 }));
43040
+ const label = (key) => {
43041
+ const f = byKey.get(key);
43042
+ return draft.questions.find((q) => q.field === key)?.ask || (f ? fieldName(f) : key);
43043
+ };
43044
+ function dragEnd(event) {
43045
+ const from = draft.questions.findIndex((q) => q.field === event.active.id);
43046
+ const to = event.over ? draft.questions.findIndex((q) => q.field === event.over.id) : -1;
43047
+ if (from < 0 || to < 0 || from === to) return;
43048
+ onQuestions(arrayMove(draft.questions, from, to));
43049
+ }
43050
+ const notAskable = whyNotAskable([...fields]);
43051
+ const leftUnasked = requiredLeftUnasked([...fields], draft.questions.map((q) => q.field));
43052
+ return /* @__PURE__ */ jsxs123("div", { className: "flex flex-col gap-1.5", "data-records-form-questions": "", children: [
43053
+ /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43054
+ /* @__PURE__ */ jsx133("span", { className: "font-medium text-foreground", children: "Questions" }),
43055
+ /* @__PURE__ */ jsx133("span", { className: "tabular-nums", children: draft.questions.length })
43056
+ ] }),
43057
+ /* @__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(
43058
+ QuestionRow,
43059
+ {
43060
+ index,
43061
+ question,
43062
+ field: byKey.get(question.field) ?? null,
43063
+ draft,
43064
+ conditionFields: asked3.filter((f) => f.key !== question.field),
43065
+ label,
43066
+ onChange: (change) => onQuestion(index, change),
43067
+ onRemove: () => onQuestions(draft.questions.filter((q) => q.field !== question.field))
43068
+ },
43069
+ question.field
43070
+ )) }) }),
43071
+ unasked.length > 0 ? /* @__PURE__ */ jsx133("div", { className: "flex flex-wrap items-center gap-1", "data-records-form-unasked": "", children: unasked.map((f) => /* @__PURE__ */ jsxs123(
43072
+ Button86,
43073
+ {
43074
+ size: "sm",
43075
+ variant: "outline",
43076
+ "aria-label": `Ask for ${fieldName(f)}`,
43077
+ onClick: () => onQuestions([...draft.questions, { field: f.key }]),
43078
+ children: [
43079
+ "+ ",
43080
+ fieldName(f)
43081
+ ]
43082
+ },
43083
+ f.id
43084
+ )) }) : null,
43085
+ notAskable ? /* @__PURE__ */ jsx133("p", { className: "w-fit text-xs text-muted-foreground", title: notAskable, children: "Some fields cannot be asked publicly" }) : null,
43086
+ leftUnasked ? /* @__PURE__ */ jsx133(
43087
+ ErrorBox7,
43088
+ {
43089
+ className: "rounded border border-destructive/40 bg-destructive/5 px-2 py-1.5 text-xs text-destructive",
43090
+ message: leftUnasked,
43091
+ operation: "Build a form",
43092
+ origin: "records-ui/FormBuilder"
43093
+ }
43094
+ ) : null,
43095
+ 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" }) })
43096
+ ] });
43097
+ }
43098
+ function QuestionRow({
43099
+ index,
43100
+ question,
43101
+ field: field2,
43102
+ draft,
43103
+ conditionFields,
43104
+ label,
43105
+ onChange,
43106
+ onRemove
43107
+ }) {
43108
+ const sortable = useSortable({ id: question.field });
43109
+ const [open, setOpen] = useState110(true);
43110
+ const name = field2 ? fieldName(field2) : question.field;
43111
+ return /* @__PURE__ */ jsxs123(
43112
+ "div",
43113
+ {
43114
+ ref: sortable.setNodeRef,
43115
+ style: { transform: CSS2.Transform.toString(sortable.transform), transition: sortable.transition },
43116
+ className: cn78("rounded border bg-background px-2 py-1.5", sortable.isDragging && "z-10 shadow-md"),
43117
+ "data-records-form-question-row": question.field,
43118
+ children: [
43119
+ /* @__PURE__ */ jsxs123("div", { className: "flex items-center gap-2", children: [
43120
+ /* @__PURE__ */ jsx133(
43121
+ "button",
43122
+ {
43123
+ type: "button",
43124
+ className: "cursor-grab touch-none rounded px-1 text-muted-foreground hover:bg-muted active:cursor-grabbing",
43125
+ "aria-label": `Move ${name}`,
43126
+ title: "Drag to reorder",
43127
+ "data-records-form-drag": "",
43128
+ ...sortable.attributes,
43129
+ ...sortable.listeners,
43130
+ children: "\u22EE\u22EE"
43131
+ }
43132
+ ),
43133
+ /* @__PURE__ */ jsx133("span", { className: "w-5 text-right text-xs tabular-nums text-muted-foreground", children: index + 1 }),
43134
+ /* @__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 }),
43135
+ field2?.required || question.required ? /* @__PURE__ */ jsx133("span", { className: "text-xs text-foreground/80", children: "required" }) : null,
43136
+ !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,
43137
+ /* @__PURE__ */ jsx133(Button86, { size: "sm", variant: "ghost", onClick: onRemove, "aria-label": `Stop asking ${name}`, children: "Remove" })
43138
+ ] }),
43139
+ open && field2 ? /* @__PURE__ */ jsxs123("div", { className: "mt-1.5 grid grid-cols-2 gap-2 pl-7", children: [
43140
+ /* @__PURE__ */ jsx133(
43141
+ BasicInput28,
43142
+ {
43143
+ className: "h-8 text-xs",
43144
+ placeholder: name,
43145
+ "aria-label": `How to ask for ${name}`,
43146
+ title: "Use {{question}} to recall an earlier answer",
43147
+ value: question.ask ?? "",
43148
+ onChange: (e) => onChange({ ask: e.target.value || null })
43149
+ }
43150
+ ),
43151
+ /* @__PURE__ */ jsx133(
43152
+ BasicInput28,
43153
+ {
43154
+ className: "h-8 text-xs",
43155
+ placeholder: "Help text",
43156
+ "aria-label": `Help text for ${name}`,
43157
+ value: question.help ?? "",
43158
+ onChange: (e) => onChange({ help: e.target.value || null })
43159
+ }
43160
+ ),
43161
+ /* @__PURE__ */ jsxs123("label", { className: "col-span-2 flex items-center gap-2 text-xs", children: [
43162
+ /* @__PURE__ */ jsx133(
43163
+ Checkbox15,
43164
+ {
43165
+ checked: field2.required ? true : question.required === true,
43166
+ disabled: field2.required === true,
43167
+ "aria-label": `${name} is required`,
43168
+ onCheckedChange: (checked) => onChange({ required: checked === true ? true : null })
43169
+ }
43170
+ ),
43171
+ "Required"
43172
+ ] }),
43173
+ /* @__PURE__ */ jsx133(Condition, { question, fieldKeys: conditionFields, onChange: (showIf) => onChange({ showIf }) }),
43174
+ /* @__PURE__ */ jsx133(
43175
+ QuestionLogic,
43176
+ {
43177
+ question,
43178
+ index,
43179
+ questions: draft.questions,
43180
+ endings: draft.endings ?? [],
43181
+ field: field2,
43182
+ fieldKeys: conditionFields,
43183
+ label,
43184
+ onChange
43185
+ }
43186
+ )
43187
+ ] }) : null
43188
+ ]
43189
+ }
43190
+ );
43191
+ }
43192
+ function NotifyOnAnswer({
43193
+ tableId,
43194
+ tableName: tableName2,
43195
+ formName,
43196
+ ruleId,
43197
+ notify,
43198
+ emailQuestion,
43199
+ onRule,
43200
+ onNotify
43201
+ }) {
43202
+ const client = useRecordsClient79();
43203
+ const [subs, setSubs] = useState110(null);
43204
+ const [busy, setBusy] = useState110(false);
43205
+ const [error, setError] = useState110(null);
43206
+ const [said2, setSaid] = useState110(null);
43207
+ const [url, setUrl] = useState110(notify?.webhookUrl ?? "");
43208
+ useEffect96(() => setUrl(notify?.webhookUrl ?? ""), [notify?.webhookUrl]);
43209
+ useEffect96(() => {
43210
+ let cancelled = false;
43211
+ void client.subscriptions({ table_id: tableId }).then((r) => {
43212
+ if (!cancelled && r.ok) setSubs(r.data);
43213
+ });
43214
+ return () => {
43215
+ cancelled = true;
43216
+ };
43217
+ }, [client, tableId, ruleId]);
43218
+ const chosen = subs?.find((s) => s.rule_id === ruleId) ?? null;
43219
+ async function emailMe(on) {
43220
+ setBusy(true);
43221
+ setError(null);
43222
+ try {
43223
+ if (!on) {
43224
+ onRule(null);
43225
+ if (ruleId) await client.recordDelete({ record_id: ruleId });
43226
+ return;
43227
+ }
43228
+ const listed = await client.views({ table_id: tableId });
43229
+ if (!listed.ok) return setError(listed.error);
43230
+ let viewId = listed.data.find((v) => !v.filters || Object.keys(v.filters).length === 0)?.view_id ?? null;
43231
+ if (!viewId) {
43232
+ const made = await client.viewDeclare({ table_id: tableId, spec: { name: `Every ${tableName2}`, filters: null } });
43233
+ if (!made.ok) return setError(made.error);
43234
+ viewId = made.data;
43235
+ }
43236
+ const declared2 = await client.subscriptionDeclare({
43237
+ table_id: tableId,
43238
+ spec: {
43239
+ name: `${formName} \u2014 new answers`,
43240
+ saved_view_id: viewId,
43241
+ cadence: "instant",
43242
+ schedule: null,
43243
+ channel: "email",
43244
+ quiet_hours: null,
43245
+ recipient_user_id: client.config.actor.user_id ?? null,
43246
+ event_key: "records.changed"
43247
+ }
43248
+ });
43249
+ if (!declared2.ok) return setError(declared2.error);
43250
+ onRule(declared2.data);
43251
+ } finally {
43252
+ setBusy(false);
43253
+ }
43254
+ }
43255
+ async function saveWebhook() {
43256
+ const next = url.trim();
43257
+ if (next === (notify?.webhookUrl ?? "")) return;
43258
+ setBusy(true);
43259
+ setError(null);
43260
+ setSaid(null);
43261
+ try {
43262
+ if (next === "") {
43263
+ if (notify?.webhookAutomationId) await client.automationArchive({ automation_id: notify.webhookAutomationId });
43264
+ onNotify(notify?.emailRespondent ? { ...notify, webhookUrl: null, webhookAutomationId: null } : null);
43265
+ return;
43266
+ }
43267
+ const declared2 = await client.automationDeclare({
43268
+ table_id: tableId,
43269
+ automation_id: notify?.webhookAutomationId ?? null,
43270
+ spec: { name: `${formName} \u2014 webhook`, trigger: { on: "form_answered" }, actions: [{ do: "webhook", url: next }] }
43271
+ });
43272
+ if (!declared2.ok) return setError(declared2.error);
43273
+ if (!declared2.data.ok) {
43274
+ setSaid(Object.values(declared2.data._errors)[0] ?? "Not saved");
43275
+ return;
43276
+ }
43277
+ onNotify({ ...notify ?? {}, webhookUrl: next, webhookAutomationId: declared2.data.automation_id });
43278
+ } finally {
43279
+ setBusy(false);
43280
+ }
43281
+ }
43282
+ 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: [
43283
+ /* @__PURE__ */ jsx133("span", { className: "font-medium", children: "Notify on answer" }),
43284
+ /* @__PURE__ */ jsxs123("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1", children: [
43285
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", title: "An email to you each time somebody answers.", children: [
43286
+ /* @__PURE__ */ jsx133(Checkbox15, { checked: Boolean(ruleId), disabled: busy, onCheckedChange: (c) => void emailMe(c === true), "aria-label": "Email me" }),
43287
+ "Email me",
43288
+ chosen && chosen.channel !== "email" ? /* @__PURE__ */ jsxs123("span", { className: "text-muted-foreground", children: [
43289
+ "(",
43290
+ chosen.channel === "sms" ? "text" : "in app",
43291
+ ")"
43292
+ ] }) : null
43293
+ ] }),
43294
+ /* @__PURE__ */ jsxs123(
43295
+ "label",
43296
+ {
43297
+ className: cn78("flex items-center gap-2", !emailQuestion && "text-muted-foreground"),
43298
+ title: emailQuestion ? "An email to the person who answered, in your ending's words." : "Add an email question first.",
43299
+ children: [
43300
+ /* @__PURE__ */ jsx133(
43301
+ Checkbox15,
43302
+ {
43303
+ checked: Boolean(notify?.emailRespondent) && Boolean(emailQuestion),
43304
+ disabled: busy || !emailQuestion,
43305
+ onCheckedChange: (c) => onNotify({ ...notify ?? {}, emailRespondent: c === true ? true : null, respondentField: c === true ? emailQuestion : null }),
43306
+ "aria-label": "Email the respondent"
43307
+ }
43308
+ ),
43309
+ "Email the respondent"
43310
+ ]
43311
+ }
43312
+ )
43313
+ ] }),
43314
+ /* @__PURE__ */ jsxs123("label", { className: "flex items-center gap-2", children: [
43315
+ /* @__PURE__ */ jsx133("span", { className: "shrink-0", children: "Webhook" }),
43316
+ /* @__PURE__ */ jsx133(
43317
+ BasicInput28,
43318
+ {
43319
+ type: "url",
43320
+ inputMode: "url",
43321
+ className: "h-8 text-xs",
43322
+ placeholder: "https://",
43323
+ "aria-label": "Webhook URL",
43324
+ value: url,
43325
+ disabled: busy,
43326
+ onChange: (e) => setUrl(e.target.value),
43327
+ onBlur: () => void saveWebhook(),
43328
+ onKeyDown: (e) => {
43329
+ if (e.key === "Enter") void saveWebhook();
43330
+ }
43331
+ }
43332
+ )
43333
+ ] }),
43334
+ said2 ? /* @__PURE__ */ jsx133("p", { className: "text-destructive", children: said2 }) : null,
43335
+ error ? /* @__PURE__ */ jsx133(RefusalLine, { error }) : null
43336
+ ] });
43337
+ }
43079
43338
 
43080
43339
  // src/DocTemplate.tsx
43081
- import { useCallback as useCallback56, useEffect as useEffect98, useState as useState112 } from "react";
43082
- import { useFields as useFields26, useRecordsClient as useRecordsClient81, useTable as useTable28 } from "@ai-matrx/records/react";
43083
- import { BasicInput as BasicInput29, BasicTextarea as BasicTextarea12, Button as Button88, Label as Label23, Skeleton as Skeleton31, cn as cn80 } from "@ai-matrx/design-system";
43084
- import { jsx as jsx135, jsxs as jsxs125 } from "react/jsx-runtime";
43340
+ import { useCallback as useCallback55, useEffect as useEffect97, useState as useState111 } from "react";
43341
+ import { useFields as useFields26, useRecordsClient as useRecordsClient80, useTable as useTable27 } from "@ai-matrx/records/react";
43342
+ import { BasicInput as BasicInput29, BasicTextarea as BasicTextarea12, Button as Button87, Label as Label24, Skeleton as Skeleton30, cn as cn79 } from "@ai-matrx/design-system";
43343
+ import { jsx as jsx134, jsxs as jsxs124 } from "react/jsx-runtime";
43085
43344
  function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, className }) {
43086
- const client = useRecordsClient81();
43345
+ const client = useRecordsClient80();
43087
43346
  const claimSeed = useSeedGuard();
43088
- const table = useTable28(tableId);
43347
+ const table = useTable27(tableId);
43089
43348
  const rights = useTableRights(table.data);
43090
43349
  const fields = useFields26(tableId);
43091
- const [templates, setTemplates] = useState112(null);
43092
- const [error, setError] = useState112(null);
43093
- const [activeId, setActiveId] = useState112(activeTemplateId ?? null);
43094
- const [draftName, setDraftName] = useState112("");
43095
- const [draftBody, setDraftBody] = useState112("");
43096
- const [unresolved, setUnresolved] = useState112([]);
43097
- const [saving, setSaving] = useState112(false);
43098
- const load = useCallback56(async () => {
43350
+ const [templates, setTemplates] = useState111(null);
43351
+ const [error, setError] = useState111(null);
43352
+ const [activeId, setActiveId] = useState111(activeTemplateId ?? null);
43353
+ const [draftName, setDraftName] = useState111("");
43354
+ const [draftBody, setDraftBody] = useState111("");
43355
+ const [unresolved, setUnresolved] = useState111([]);
43356
+ const [saving, setSaving] = useState111(false);
43357
+ const load = useCallback55(async () => {
43099
43358
  const held = await client.docTemplates({ table_id: tableId });
43100
43359
  if (!held.ok) {
43101
43360
  setError(held.error);
@@ -43126,10 +43385,10 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43126
43385
  setError(null);
43127
43386
  setTemplates(rows);
43128
43387
  }, [client, tableId, seed, fields.data]);
43129
- useEffect98(() => {
43388
+ useEffect97(() => {
43130
43389
  void load();
43131
43390
  }, [load]);
43132
- useEffect98(() => {
43391
+ useEffect97(() => {
43133
43392
  if (!templates || templates.length === 0) return;
43134
43393
  const chosen = templates.find((t) => t.id === (activeTemplateId ?? activeId)) ?? templates[0];
43135
43394
  if (chosen.id !== activeId) setActiveId(chosen.id);
@@ -43137,7 +43396,7 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43137
43396
  setDraftBody(chosen.body);
43138
43397
  onActiveTemplate?.(chosen);
43139
43398
  }, [templates, activeTemplateId]);
43140
- useEffect98(() => {
43399
+ useEffect97(() => {
43141
43400
  let cancelled = false;
43142
43401
  if (draftBody.trim() === "") {
43143
43402
  setUnresolved([]);
@@ -43167,25 +43426,25 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43167
43426
  setActiveId(saved.data);
43168
43427
  await load();
43169
43428
  }
43170
- if (fields.error) return /* @__PURE__ */ jsx135(RefusalNotice, { error: fields.error, className });
43429
+ if (fields.error) return /* @__PURE__ */ jsx134(RefusalNotice, { error: fields.error, className });
43171
43430
  if (error) {
43172
- return /* @__PURE__ */ jsx135(
43431
+ return /* @__PURE__ */ jsx134(
43173
43432
  RefusalNotice,
43174
43433
  {
43175
43434
  error,
43176
43435
  className,
43177
- actions: /* @__PURE__ */ jsx135(Button88, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43436
+ actions: /* @__PURE__ */ jsx134(Button87, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43178
43437
  }
43179
43438
  );
43180
43439
  }
43181
- if (templates === null || fields.loading && !fields.data) return /* @__PURE__ */ jsx135(Skeleton31, { className: cn80("h-64 w-full", className) });
43440
+ if (templates === null || fields.loading && !fields.data) return /* @__PURE__ */ jsx134(Skeleton30, { className: cn79("h-64 w-full", className) });
43182
43441
  if (!rights.admin) {
43183
- return /* @__PURE__ */ jsx135("p", { className: cn80("text-xs text-muted-foreground", className), children: rights.why("structure") });
43442
+ return /* @__PURE__ */ jsx134("p", { className: cn79("text-xs text-muted-foreground", className), children: rights.why("structure") });
43184
43443
  }
43185
- return /* @__PURE__ */ jsxs125("div", { className: cn80("flex flex-col gap-2", className), children: [
43186
- /* @__PURE__ */ jsxs125("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Templates", children: [
43187
- templates.map((t) => /* @__PURE__ */ jsxs125(
43188
- Button88,
43444
+ return /* @__PURE__ */ jsxs124("div", { className: cn79("flex flex-col gap-2", className), children: [
43445
+ /* @__PURE__ */ jsxs124("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Templates", children: [
43446
+ templates.map((t) => /* @__PURE__ */ jsxs124(
43447
+ Button87,
43189
43448
  {
43190
43449
  size: "sm",
43191
43450
  variant: t.id === activeId ? "secondary" : "ghost",
@@ -43197,7 +43456,7 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43197
43456
  },
43198
43457
  children: [
43199
43458
  t.name,
43200
- /* @__PURE__ */ jsxs125("span", { className: "ml-1 text-[10px] opacity-70", children: [
43459
+ /* @__PURE__ */ jsxs124("span", { className: "ml-1 text-[10px] opacity-70", children: [
43201
43460
  "v",
43202
43461
  t.template_version
43203
43462
  ] })
@@ -43205,8 +43464,8 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43205
43464
  },
43206
43465
  t.id
43207
43466
  )),
43208
- /* @__PURE__ */ jsx135(
43209
- Button88,
43467
+ /* @__PURE__ */ jsx134(
43468
+ Button87,
43210
43469
  {
43211
43470
  size: "sm",
43212
43471
  variant: "ghost",
@@ -43218,16 +43477,16 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43218
43477
  children: "New template"
43219
43478
  }
43220
43479
  ),
43221
- /* @__PURE__ */ jsx135(Button88, { size: "sm", className: "ml-auto", disabled: saving, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
43480
+ /* @__PURE__ */ jsx134(Button87, { size: "sm", className: "ml-auto", disabled: saving, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" })
43222
43481
  ] }),
43223
- /* @__PURE__ */ jsxs125("label", { className: "flex flex-col gap-1", children: [
43224
- /* @__PURE__ */ jsx135(Label23, { className: "text-xs font-medium", children: "Name" }),
43225
- /* @__PURE__ */ jsx135(BasicInput29, { value: draftName, onChange: (e) => setDraftName(e.target.value), placeholder: "Agreement" })
43482
+ /* @__PURE__ */ jsxs124("label", { className: "flex flex-col gap-1", children: [
43483
+ /* @__PURE__ */ jsx134(Label24, { className: "text-xs font-medium", children: "Name" }),
43484
+ /* @__PURE__ */ jsx134(BasicInput29, { value: draftName, onChange: (e) => setDraftName(e.target.value), placeholder: "Agreement" })
43226
43485
  ] }),
43227
- /* @__PURE__ */ jsxs125("div", { className: "flex flex-wrap items-center gap-1", children: [
43228
- /* @__PURE__ */ jsx135("span", { className: "text-xs text-muted-foreground", children: "Insert" }),
43229
- (fields.data ?? []).map((field2) => /* @__PURE__ */ jsx135(
43230
- Button88,
43486
+ /* @__PURE__ */ jsxs124("div", { className: "flex flex-wrap items-center gap-1", children: [
43487
+ /* @__PURE__ */ jsx134("span", { className: "text-xs text-muted-foreground", children: "Insert" }),
43488
+ (fields.data ?? []).map((field2) => /* @__PURE__ */ jsx134(
43489
+ Button87,
43231
43490
  {
43232
43491
  size: "sm",
43233
43492
  variant: "ghost",
@@ -43238,7 +43497,7 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43238
43497
  field2.id
43239
43498
  ))
43240
43499
  ] }),
43241
- /* @__PURE__ */ jsx135(
43500
+ /* @__PURE__ */ jsx134(
43242
43501
  BasicTextarea12,
43243
43502
  {
43244
43503
  rows: 10,
@@ -43248,33 +43507,33 @@ function DocTemplate({ tableId, seed, activeTemplateId, onActiveTemplate, classN
43248
43507
  onChange: (e) => setDraftBody(e.target.value)
43249
43508
  }
43250
43509
  ),
43251
- 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: [
43252
- /* @__PURE__ */ jsx135("code", { className: "text-destructive", children: token.raw }),
43253
- /* @__PURE__ */ jsx135("span", { "aria-hidden": "true", children: "\u2014" }),
43254
- /* @__PURE__ */ jsx135(
43510
+ 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: [
43511
+ /* @__PURE__ */ jsx134("code", { className: "text-destructive", children: token.raw }),
43512
+ /* @__PURE__ */ jsx134("span", { "aria-hidden": "true", children: "\u2014" }),
43513
+ /* @__PURE__ */ jsx134(
43255
43514
  RefusalLine,
43256
43515
  {
43257
43516
  error: refusal("invalid_argument", token.why, "Change it to a Field this table has, or take it out.")
43258
43517
  }
43259
43518
  )
43260
- ] }, 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
43519
+ ] }, 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
43261
43520
  ] });
43262
43521
  }
43263
43522
 
43264
43523
  // src/DocRender.tsx
43265
- import { useCallback as useCallback57, useEffect as useEffect99, useRef as useRef57, useState as useState113 } from "react";
43266
- import { useRecordsClient as useRecordsClient82 } from "@ai-matrx/records/react";
43267
- import { Button as Button89, Skeleton as Skeleton32, cn as cn81 } from "@ai-matrx/design-system";
43268
- import { jsx as jsx136, jsxs as jsxs126 } from "react/jsx-runtime";
43524
+ import { useCallback as useCallback56, useEffect as useEffect98, useRef as useRef57, useState as useState112 } from "react";
43525
+ import { useRecordsClient as useRecordsClient81 } from "@ai-matrx/records/react";
43526
+ import { Button as Button88, Skeleton as Skeleton31, cn as cn80 } from "@ai-matrx/design-system";
43527
+ import { jsx as jsx135, jsxs as jsxs125 } from "react/jsx-runtime";
43269
43528
  function DocRender({ templateId, recordId, filename, onRendered, className }) {
43270
- const client = useRecordsClient82();
43271
- const [preview, setPreview] = useState113(null);
43272
- const [renders, setRenders] = useState113(null);
43273
- const [showing, setShowing] = useState113(null);
43274
- const [error, setError] = useState113(null);
43275
- const [busy, setBusy] = useState113(null);
43529
+ const client = useRecordsClient81();
43530
+ const [preview, setPreview] = useState112(null);
43531
+ const [renders, setRenders] = useState112(null);
43532
+ const [showing, setShowing] = useState112(null);
43533
+ const [error, setError] = useState112(null);
43534
+ const [busy, setBusy] = useState112(null);
43276
43535
  const paper = useRef57(null);
43277
- const load = useCallback57(async () => {
43536
+ const load = useCallback56(async () => {
43278
43537
  const [body, held] = await Promise.all([
43279
43538
  client.docRenderBody({ template_id: templateId, record_id: recordId }),
43280
43539
  client.docRenders({ record_id: recordId })
@@ -43291,7 +43550,7 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43291
43550
  setPreview(body.data);
43292
43551
  setRenders(held.data.filter((r) => r.template_id === templateId));
43293
43552
  }, [client, templateId, recordId]);
43294
- useEffect99(() => {
43553
+ useEffect98(() => {
43295
43554
  void load();
43296
43555
  }, [load]);
43297
43556
  async function freeze() {
@@ -43331,22 +43590,22 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43331
43590
  }
43332
43591
  }
43333
43592
  if (error) {
43334
- return /* @__PURE__ */ jsx136(
43593
+ return /* @__PURE__ */ jsx135(
43335
43594
  RefusalNotice,
43336
43595
  {
43337
43596
  error,
43338
43597
  className,
43339
- actions: /* @__PURE__ */ jsx136(Button89, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43598
+ actions: /* @__PURE__ */ jsx135(Button88, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43340
43599
  }
43341
43600
  );
43342
43601
  }
43343
- if (preview === null || renders === null) return /* @__PURE__ */ jsx136(Skeleton32, { className: cn81("h-64 w-full", className) });
43602
+ if (preview === null || renders === null) return /* @__PURE__ */ jsx135(Skeleton31, { className: cn80("h-64 w-full", className) });
43344
43603
  const frozen = showing ? renders.find((r) => r.id === showing) ?? null : null;
43345
43604
  const text2 = frozen ? frozen.body : preview;
43346
- return /* @__PURE__ */ jsxs126("div", { className: cn81("flex flex-col gap-2", className), children: [
43347
- /* @__PURE__ */ jsxs126("div", { className: "flex items-center gap-1 text-xs", children: [
43348
- /* @__PURE__ */ jsx136(
43349
- Button89,
43605
+ return /* @__PURE__ */ jsxs125("div", { className: cn80("flex flex-col gap-2", className), children: [
43606
+ /* @__PURE__ */ jsxs125("div", { className: "flex items-center gap-1 text-xs", children: [
43607
+ /* @__PURE__ */ jsx135(
43608
+ Button88,
43350
43609
  {
43351
43610
  size: "sm",
43352
43611
  variant: frozen ? "ghost" : "secondary",
@@ -43355,8 +43614,8 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43355
43614
  children: "As it reads now"
43356
43615
  }
43357
43616
  ),
43358
- renders.map((render) => /* @__PURE__ */ jsx136(
43359
- Button89,
43617
+ renders.map((render) => /* @__PURE__ */ jsx135(
43618
+ Button88,
43360
43619
  {
43361
43620
  size: "sm",
43362
43621
  variant: render.id === showing ? "secondary" : "ghost",
@@ -43367,33 +43626,33 @@ function DocRender({ templateId, recordId, filename, onRendered, className }) {
43367
43626
  },
43368
43627
  render.id
43369
43628
  )),
43370
- /* @__PURE__ */ jsxs126("span", { className: "ml-auto flex items-center gap-1", children: [
43371
- /* @__PURE__ */ jsx136(Button89, { size: "sm", disabled: busy !== null, onClick: () => void freeze(), children: busy === "freeze" ? "Freezing\u2026" : "Freeze this version" }),
43372
- /* @__PURE__ */ jsx136(Button89, { size: "sm", variant: "ghost", disabled: busy !== null, onClick: () => void toPdf(), children: busy === "pdf" ? "Making the PDF\u2026" : "PDF" })
43629
+ /* @__PURE__ */ jsxs125("span", { className: "ml-auto flex items-center gap-1", children: [
43630
+ /* @__PURE__ */ jsx135(Button88, { size: "sm", disabled: busy !== null, onClick: () => void freeze(), children: busy === "freeze" ? "Freezing\u2026" : "Freeze this version" }),
43631
+ /* @__PURE__ */ jsx135(Button88, { size: "sm", variant: "ghost", disabled: busy !== null, onClick: () => void toPdf(), children: busy === "pdf" ? "Making the PDF\u2026" : "PDF" })
43373
43632
  ] })
43374
43633
  ] }),
43375
- /* @__PURE__ */ jsx136("div", { ref: paper, className: "whitespace-pre-wrap rounded border bg-background p-4 text-sm text-foreground", children: text2 }),
43376
- /* @__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." })
43634
+ /* @__PURE__ */ jsx135("div", { ref: paper, className: "whitespace-pre-wrap rounded border bg-background p-4 text-sm text-foreground", children: text2 }),
43635
+ /* @__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." })
43377
43636
  ] });
43378
43637
  }
43379
43638
 
43380
43639
  // src/SignBlock.tsx
43381
- import { useCallback as useCallback58, useEffect as useEffect100, useState as useState114 } from "react";
43382
- import { useFields as useFields27, useRecordsClient as useRecordsClient83 } from "@ai-matrx/records/react";
43383
- import { BasicInput as BasicInput30, Button as Button90, Skeleton as Skeleton33, cn as cn82 } from "@ai-matrx/design-system";
43384
- import { useTable as useTable29 } from "@ai-matrx/records/react";
43385
- import { jsx as jsx137, jsxs as jsxs127 } from "react/jsx-runtime";
43640
+ import { useCallback as useCallback57, useEffect as useEffect99, useState as useState113 } from "react";
43641
+ import { useFields as useFields27, useRecordsClient as useRecordsClient82 } from "@ai-matrx/records/react";
43642
+ import { BasicInput as BasicInput30, Button as Button89, Skeleton as Skeleton32, cn as cn81 } from "@ai-matrx/design-system";
43643
+ import { useTable as useTable28 } from "@ai-matrx/records/react";
43644
+ import { jsx as jsx136, jsxs as jsxs126 } from "react/jsx-runtime";
43386
43645
  function SignBlock({ tableId, recordId, render, className }) {
43387
- const client = useRecordsClient83();
43388
- const table = useTable29(tableId);
43646
+ const client = useRecordsClient82();
43647
+ const table = useTable28(tableId);
43389
43648
  const rights = useTableRights(table.data);
43390
43649
  const fields = useFields27(tableId);
43391
- const [signatures, setSignatures] = useState114(null);
43392
- const [verdicts, setVerdicts] = useState114({});
43393
- const [error, setError] = useState114(null);
43394
- const [name, setName] = useState114("");
43395
- const [busy, setBusy] = useState114(false);
43396
- const load = useCallback58(async () => {
43650
+ const [signatures, setSignatures] = useState113(null);
43651
+ const [verdicts, setVerdicts] = useState113({});
43652
+ const [error, setError] = useState113(null);
43653
+ const [name, setName] = useState113("");
43654
+ const [busy, setBusy] = useState113(false);
43655
+ const load = useCallback57(async () => {
43397
43656
  const held = await client.docSignatures({ record_id: recordId });
43398
43657
  if (!held.ok) {
43399
43658
  setError(held.error);
@@ -43408,7 +43667,7 @@ function SignBlock({ tableId, recordId, render, className }) {
43408
43667
  }
43409
43668
  setVerdicts(answers);
43410
43669
  }, [client, recordId]);
43411
- useEffect100(() => {
43670
+ useEffect99(() => {
43412
43671
  void load();
43413
43672
  }, [load]);
43414
43673
  async function sign(field2) {
@@ -43428,36 +43687,36 @@ function SignBlock({ tableId, recordId, render, className }) {
43428
43687
  setName("");
43429
43688
  await load();
43430
43689
  }
43431
- if (fields.error) return /* @__PURE__ */ jsx137(RefusalNotice, { error: fields.error, className });
43690
+ if (fields.error) return /* @__PURE__ */ jsx136(RefusalNotice, { error: fields.error, className });
43432
43691
  if (error) {
43433
- return /* @__PURE__ */ jsx137(
43692
+ return /* @__PURE__ */ jsx136(
43434
43693
  RefusalNotice,
43435
43694
  {
43436
43695
  error,
43437
43696
  className,
43438
- actions: /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43697
+ actions: /* @__PURE__ */ jsx136(Button89, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43439
43698
  }
43440
43699
  );
43441
43700
  }
43442
- if (fields.loading || signatures === null) return /* @__PURE__ */ jsx137(Skeleton33, { className: cn82("h-32 w-full", className) });
43701
+ if (fields.loading || signatures === null) return /* @__PURE__ */ jsx136(Skeleton32, { className: cn81("h-32 w-full", className) });
43443
43702
  const signable = (fields.data ?? []).filter(isSignatureField);
43444
- return /* @__PURE__ */ jsxs127("div", { className: cn82("flex flex-col gap-2", className), children: [
43445
- /* @__PURE__ */ jsxs127("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43446
- /* @__PURE__ */ jsx137("span", { className: "font-medium text-foreground", children: "Signatures" }),
43447
- /* @__PURE__ */ jsxs127("span", { children: [
43703
+ return /* @__PURE__ */ jsxs126("div", { className: cn81("flex flex-col gap-2", className), children: [
43704
+ /* @__PURE__ */ jsxs126("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43705
+ /* @__PURE__ */ jsx136("span", { className: "font-medium text-foreground", children: "Signatures" }),
43706
+ /* @__PURE__ */ jsxs126("span", { children: [
43448
43707
  signatures.length,
43449
43708
  " on this record"
43450
43709
  ] })
43451
43710
  ] }),
43452
- signatures.length > 0 ? /* @__PURE__ */ jsx137("ul", { className: "flex flex-col gap-1", children: signatures.map((signature) => {
43711
+ signatures.length > 0 ? /* @__PURE__ */ jsx136("ul", { className: "flex flex-col gap-1", children: signatures.map((signature) => {
43453
43712
  const verdict = verdicts[signature.id];
43454
43713
  const intact = typeof verdict === "object" && verdict !== null ? verdict.intact : void 0;
43455
- return /* @__PURE__ */ jsxs127("li", { className: "rounded border px-2 py-1.5 text-xs", children: [
43456
- /* @__PURE__ */ jsxs127("div", { className: "flex items-center gap-2", children: [
43457
- /* @__PURE__ */ jsx137("span", { className: "font-medium", children: signature.signer_name }),
43458
- /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: signature.field_key }),
43459
- /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: new Date(signature.signed_at).toLocaleString() }),
43460
- /* @__PURE__ */ jsxs127("span", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: [
43714
+ return /* @__PURE__ */ jsxs126("li", { className: "rounded border px-2 py-1.5 text-xs", children: [
43715
+ /* @__PURE__ */ jsxs126("div", { className: "flex items-center gap-2", children: [
43716
+ /* @__PURE__ */ jsx136("span", { className: "font-medium", children: signature.signer_name }),
43717
+ /* @__PURE__ */ jsx136("span", { className: "text-muted-foreground", children: signature.field_key }),
43718
+ /* @__PURE__ */ jsx136("span", { className: "text-muted-foreground", children: new Date(signature.signed_at).toLocaleString() }),
43719
+ /* @__PURE__ */ jsxs126("span", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: [
43461
43720
  "v",
43462
43721
  signature.document_version,
43463
43722
  " \xB7 ",
@@ -43467,7 +43726,7 @@ function SignBlock({ tableId, recordId, render, className }) {
43467
43726
  intact === false ? (
43468
43727
  // THE STORE REFUSED TO CALL IT INTACT: its reason goes through the one
43469
43728
  // formatter, with what to do next, never raw in red (lane REFUSAL-SWEEP).
43470
- /* @__PURE__ */ jsx137(
43729
+ /* @__PURE__ */ jsx136(
43471
43730
  RefusalLine,
43472
43731
  {
43473
43732
  className: "mt-0.5",
@@ -43478,22 +43737,22 @@ function SignBlock({ tableId, recordId, render, className }) {
43478
43737
  )
43479
43738
  }
43480
43739
  )
43481
- ) : /* @__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 ?? "") })
43740
+ ) : /* @__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 ?? "") })
43482
43741
  ] }, signature.id);
43483
43742
  }) }) : null,
43484
- !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: [
43743
+ !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: [
43485
43744
  "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 ",
43486
- /* @__PURE__ */ jsx137("code", { children: "signature" }),
43745
+ /* @__PURE__ */ jsx136("code", { children: "signature" }),
43487
43746
  " \u2014 add one and the signing appears."
43488
- ] }) : !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) => {
43747
+ ] }) : !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) => {
43489
43748
  const already = signatures.find((s) => s.field_key === field2.key);
43490
43749
  if (already) {
43491
- return /* @__PURE__ */ jsxs127("p", { className: "text-xs text-muted-foreground", children: [
43750
+ return /* @__PURE__ */ jsxs126("p", { className: "text-xs text-muted-foreground", children: [
43492
43751
  fieldName(field2),
43493
43752
  " 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."
43494
43753
  ] }, field2.id);
43495
43754
  }
43496
- return /* @__PURE__ */ jsxs127(
43755
+ return /* @__PURE__ */ jsxs126(
43497
43756
  "form",
43498
43757
  {
43499
43758
  className: "flex items-center gap-1",
@@ -43502,7 +43761,7 @@ function SignBlock({ tableId, recordId, render, className }) {
43502
43761
  if (name.trim() !== "") void sign(field2);
43503
43762
  },
43504
43763
  children: [
43505
- /* @__PURE__ */ jsx137(
43764
+ /* @__PURE__ */ jsx136(
43506
43765
  BasicInput30,
43507
43766
  {
43508
43767
  className: "h-8 w-56 text-xs",
@@ -43512,8 +43771,8 @@ function SignBlock({ tableId, recordId, render, className }) {
43512
43771
  onChange: (e) => setName(e.target.value)
43513
43772
  }
43514
43773
  ),
43515
- /* @__PURE__ */ jsx137(Button90, { size: "sm", type: "submit", disabled: busy || name.trim() === "", children: "Sign" }),
43516
- /* @__PURE__ */ jsxs127("span", { className: "text-xs text-muted-foreground", children: [
43774
+ /* @__PURE__ */ jsx136(Button89, { size: "sm", type: "submit", disabled: busy || name.trim() === "", children: "Sign" }),
43775
+ /* @__PURE__ */ jsxs126("span", { className: "text-xs text-muted-foreground", children: [
43517
43776
  "seals version ",
43518
43777
  render.template_version,
43519
43778
  " \xB7 ",
@@ -43528,6 +43787,261 @@ function SignBlock({ tableId, recordId, render, className }) {
43528
43787
  ] });
43529
43788
  }
43530
43789
 
43790
+ // src/NotifyRuleEditor.tsx
43791
+ import { useCallback as useCallback58, useEffect as useEffect100, useState as useState114 } from "react";
43792
+ import { useRecordsClient as useRecordsClient83, useTable as useTable29 } from "@ai-matrx/records/react";
43793
+ import { hasQuietHours as hasQuietHours3 } from "@ai-matrx/records";
43794
+ import { Button as Button90, Skeleton as Skeleton33, cn as cn82 } from "@ai-matrx/design-system";
43795
+ import { jsx as jsx137, jsxs as jsxs127 } from "react/jsx-runtime";
43796
+ var CADENCE_WORDS2 = SUBSCRIPTION_CADENCE_LABEL;
43797
+ var CHANNEL_WORDS3 = SUBSCRIPTION_CHANNEL_LABEL;
43798
+ function NotifyRuleEditor({ tableId, seed, chosenRuleId, onChoose, className }) {
43799
+ const client = useRecordsClient83();
43800
+ const host = useRecordsUi();
43801
+ const table = useTable29(tableId);
43802
+ const rights = useTableRights(table.data);
43803
+ const [subscriptions, setSubscriptions] = useState114(null);
43804
+ const [cadences, setCadences] = useState114([]);
43805
+ const [views, setViews] = useState114(null);
43806
+ const [error, setError] = useState114(null);
43807
+ const [busy, setBusy] = useState114(false);
43808
+ const [stopping, setStopping] = useState114(null);
43809
+ const load = useCallback58(async () => {
43810
+ const [held, offered] = await Promise.all([
43811
+ // THE PERSON'S OWN DOOR, not the notifier's. It answers what is addressed
43812
+ // to them plus — only where they hold admin on this Table — anyone's over
43813
+ // it, and it is narrowed to Tables they can already open.
43814
+ client.subscriptions({ table_id: tableId }),
43815
+ // The cadences are ASKED of the store, never a list typed into a picker
43816
+ // here that could drift from what the digest runner understands.
43817
+ client.subscriptionCadences()
43818
+ ]);
43819
+ if (!held.ok) {
43820
+ setError(held.error);
43821
+ return;
43822
+ }
43823
+ setError(null);
43824
+ setSubscriptions(held.data);
43825
+ if (offered.ok) setCadences(offered.data);
43826
+ if (host.savedViews) {
43827
+ setViews(await host.savedViews());
43828
+ return;
43829
+ }
43830
+ const listed = await client.views({ table_id: tableId });
43831
+ if (!listed.ok) {
43832
+ setError(listed.error);
43833
+ return;
43834
+ }
43835
+ setViews(listed.data.map((v) => ({ id: v.view_id, name: v.name })));
43836
+ }, [client, host, tableId]);
43837
+ useEffect100(() => {
43838
+ void load();
43839
+ }, [load]);
43840
+ const write = useCallback58(
43841
+ async (spec) => {
43842
+ const declared2 = await client.subscriptionDeclare({
43843
+ table_id: tableId,
43844
+ spec: {
43845
+ name: spec.name,
43846
+ saved_view_id: spec.savedViewId,
43847
+ cadence: spec.cadence ?? "instant",
43848
+ schedule: spec.schedule ?? null,
43849
+ channel: spec.channel ?? "in_app",
43850
+ quiet_hours: spec.quietHours ?? null,
43851
+ // "Tell me" is the default, and the store refuses a subscription with
43852
+ // nobody to tell rather than writing one that fires at nobody.
43853
+ recipient_user_id: spec.recipientUserId ?? client.config.actor.user_id ?? null,
43854
+ event_key: spec.eventKey ?? "records.changed"
43855
+ }
43856
+ });
43857
+ if (!declared2.ok) {
43858
+ setError(declared2.error);
43859
+ return null;
43860
+ }
43861
+ return declared2.data;
43862
+ },
43863
+ [client, tableId]
43864
+ );
43865
+ useEffect100(() => {
43866
+ if (!subscriptions || !seed || seed.length === 0) return;
43867
+ const missing = seed.filter((s) => !subscriptions.some((held) => held.name === s.name));
43868
+ if (missing.length === 0) return;
43869
+ void (async () => {
43870
+ for (const spec of missing) {
43871
+ if (!await write(spec)) return;
43872
+ }
43873
+ await load();
43874
+ })();
43875
+ }, [subscriptions, seed, write, load]);
43876
+ async function subscribe(viewId, viewName, cadence, channel, schedule, quiet2) {
43877
+ setBusy(true);
43878
+ const ok = await write({
43879
+ name: `${viewName} \u2014 ${CADENCE_WORDS2[cadence] ?? cadence}`,
43880
+ savedViewId: viewId,
43881
+ cadence,
43882
+ channel,
43883
+ schedule: schedule.trim() === "" ? null : schedule.trim(),
43884
+ quietHours: quiet2
43885
+ });
43886
+ setBusy(false);
43887
+ if (!ok) return;
43888
+ onChoose?.(ok);
43889
+ await load();
43890
+ }
43891
+ async function makeEveryRecordView() {
43892
+ setBusy(true);
43893
+ const made = await client.viewDeclare({
43894
+ table_id: tableId,
43895
+ spec: { name: `Every ${table.data?.name ?? "record"}`, filters: null }
43896
+ });
43897
+ setBusy(false);
43898
+ if (!made.ok) {
43899
+ setError(made.error);
43900
+ return;
43901
+ }
43902
+ await load();
43903
+ }
43904
+ async function unsubscribe(ruleId) {
43905
+ setStopping(null);
43906
+ setBusy(true);
43907
+ const deleted = await client.recordDelete({ record_id: ruleId });
43908
+ setBusy(false);
43909
+ if (!deleted.ok) {
43910
+ setError(deleted.error);
43911
+ return;
43912
+ }
43913
+ await load();
43914
+ }
43915
+ if (error) {
43916
+ return /* @__PURE__ */ jsx137(
43917
+ RefusalNotice,
43918
+ {
43919
+ error,
43920
+ className,
43921
+ actions: /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", onClick: () => void load(), children: "Try again" })
43922
+ }
43923
+ );
43924
+ }
43925
+ if (subscriptions === null) return /* @__PURE__ */ jsx137(Skeleton33, { className: cn82("h-40 w-full", className) });
43926
+ return /* @__PURE__ */ jsxs127("div", { className: cn82("flex flex-col gap-2", className), children: [
43927
+ /* @__PURE__ */ jsxs127("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
43928
+ /* @__PURE__ */ jsx137("span", { className: "font-medium text-foreground", children: "Tell me when" }),
43929
+ /* @__PURE__ */ jsxs127("span", { children: [
43930
+ subscriptions.length,
43931
+ " subscription",
43932
+ subscriptions.length === 1 ? "" : "s"
43933
+ ] })
43934
+ ] }),
43935
+ 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: [
43936
+ /* @__PURE__ */ jsx137("span", { className: "truncate font-medium", children: held.name }),
43937
+ /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: CADENCE_WORDS2[held.cadence] ?? held.cadence }),
43938
+ /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: CHANNEL_WORDS3[held.channel] ?? held.channel }),
43939
+ held.schedule ? /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: scheduleWords(held.schedule, held.quiet_hours?.tz) }) : null,
43940
+ held.next_digest_at ? /* @__PURE__ */ jsxs127("span", { className: "text-muted-foreground", children: [
43941
+ "next ",
43942
+ new Date(held.next_digest_at).toLocaleString()
43943
+ ] }) : null,
43944
+ hasQuietHours3(held.quiet_hours) ? /* @__PURE__ */ jsxs127("span", { className: "text-muted-foreground", children: [
43945
+ "quiet ",
43946
+ held.quiet_hours.start,
43947
+ "\u2013",
43948
+ held.quiet_hours.end
43949
+ ] }) : null,
43950
+ held.recipient_user_id === null ? /* @__PURE__ */ jsx137("span", { className: "text-destructive", children: "This one has nobody to tell, so it fires at nobody." }) : null,
43951
+ 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,
43952
+ held.muted ? /* @__PURE__ */ jsx137("span", { className: "text-muted-foreground", children: "Switched off \u2014 it tells nobody until somebody switches it back on." }) : null,
43953
+ onChoose ? held.rule_id === chosenRuleId ? /* @__PURE__ */ jsxs127("span", { className: "ml-auto flex items-center gap-1", children: [
43954
+ /* @__PURE__ */ jsx137("span", { className: "font-medium text-foreground", children: "Used for this form" }),
43955
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", disabled: busy, onClick: () => onChoose(null), children: "Stop using" })
43956
+ ] }) : /* @__PURE__ */ jsx137(
43957
+ Button90,
43958
+ {
43959
+ size: "sm",
43960
+ variant: "outline",
43961
+ className: "ml-auto",
43962
+ disabled: busy,
43963
+ onClick: () => onChoose(held.rule_id),
43964
+ children: "Use for this form"
43965
+ }
43966
+ ) : null,
43967
+ 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: [
43968
+ /* @__PURE__ */ jsx137("span", { className: "text-xs text-destructive", children: archiveConfirmSentence("this subscription") }),
43969
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "destructive", disabled: busy, onClick: () => void unsubscribe(held.rule_id), children: "Stop telling me" }),
43970
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "ghost", onClick: () => setStopping(null), children: "Keep it" })
43971
+ ] }) : rights.write && !(onChoose && held.rule_id === chosenRuleId) ? /* @__PURE__ */ jsx137(
43972
+ Button90,
43973
+ {
43974
+ size: "sm",
43975
+ variant: "ghost",
43976
+ className: onChoose ? void 0 : "ml-auto",
43977
+ disabled: busy,
43978
+ onClick: () => setStopping(held.rule_id),
43979
+ children: "Stop telling me"
43980
+ }
43981
+ ) : null
43982
+ ] }, held.rule_id)) }),
43983
+ 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,
43984
+ 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: [
43985
+ /* @__PURE__ */ jsx137("span", { children: "A subscription watches a saved view, and this table has none yet." }),
43986
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", variant: "outline", disabled: busy, onClick: () => void makeEveryRecordView(), children: "Watch every record" })
43987
+ ] }) : /* @__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(
43988
+ "form",
43989
+ {
43990
+ className: "flex flex-wrap items-center gap-1",
43991
+ onSubmit: (event) => {
43992
+ event.preventDefault();
43993
+ const form = event.currentTarget;
43994
+ const view = form.elements.namedItem("view").value;
43995
+ const cadence = form.elements.namedItem("cadence").value;
43996
+ const channel = form.elements.namedItem("channel").value;
43997
+ const schedule = form.elements.namedItem("schedule").value;
43998
+ const start = form.elements.namedItem("quiet_start").value;
43999
+ const end = form.elements.namedItem("quiet_end").value;
44000
+ const name = views.find((v) => v.id === view)?.name ?? "View";
44001
+ const quiet2 = start && end ? { start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone } : null;
44002
+ if (view) void subscribe(view, name, cadence, channel, schedule, quiet2);
44003
+ },
44004
+ children: [
44005
+ /* @__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)) }),
44006
+ /* @__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)) }),
44007
+ /* @__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)) }),
44008
+ /* @__PURE__ */ jsx137(
44009
+ "input",
44010
+ {
44011
+ name: "schedule",
44012
+ "aria-label": "When the summary goes out",
44013
+ placeholder: "monday 08:00",
44014
+ className: "h-7 w-28 rounded border bg-background px-1 text-xs"
44015
+ }
44016
+ ),
44017
+ /* @__PURE__ */ jsx137("span", { className: "text-xs text-muted-foreground", children: "not between" }),
44018
+ /* @__PURE__ */ jsx137(
44019
+ "input",
44020
+ {
44021
+ name: "quiet_start",
44022
+ type: "time",
44023
+ "aria-label": "Quiet hours start",
44024
+ className: "h-7 rounded border bg-background px-1 text-xs"
44025
+ }
44026
+ ),
44027
+ /* @__PURE__ */ jsx137("span", { className: "text-xs text-muted-foreground", children: "and" }),
44028
+ /* @__PURE__ */ jsx137(
44029
+ "input",
44030
+ {
44031
+ name: "quiet_end",
44032
+ type: "time",
44033
+ "aria-label": "Quiet hours end",
44034
+ className: "h-7 rounded border bg-background px-1 text-xs"
44035
+ }
44036
+ ),
44037
+ /* @__PURE__ */ jsx137(Button90, { size: "sm", type: "submit", disabled: busy, children: "Tell me" }),
44038
+ /* @__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." })
44039
+ ]
44040
+ }
44041
+ ) : /* @__PURE__ */ jsx137("p", { className: "text-xs text-muted-foreground", children: rights.why("write") })
44042
+ ] });
44043
+ }
44044
+
43531
44045
  // src/DashboardCanvas.tsx
43532
44046
  import { useCallback as useCallback59, useEffect as useEffect102, useMemo as useMemo71, useState as useState117 } from "react";
43533
44047
  import { useFields as useFields30, useRecordsClient as useRecordsClient85, useTable as useTable31 } from "@ai-matrx/records/react";
@@ -43548,20 +44062,20 @@ import {
43548
44062
 
43549
44063
  // src/DashboardGrid.tsx
43550
44064
  import {
43551
- DndContext,
43552
- KeyboardSensor,
43553
- PointerSensor,
44065
+ DndContext as DndContext2,
44066
+ KeyboardSensor as KeyboardSensor2,
44067
+ PointerSensor as PointerSensor2,
43554
44068
  pointerWithin,
43555
- useSensor,
43556
- useSensors
44069
+ useSensor as useSensor2,
44070
+ useSensors as useSensors2
43557
44071
  } from "@dnd-kit/core";
43558
44072
  import {
43559
- SortableContext,
44073
+ SortableContext as SortableContext2,
43560
44074
  rectSortingStrategy,
43561
- sortableKeyboardCoordinates,
43562
- useSortable
44075
+ sortableKeyboardCoordinates as sortableKeyboardCoordinates2,
44076
+ useSortable as useSortable2
43563
44077
  } from "@dnd-kit/sortable";
43564
- import { CSS } from "@dnd-kit/utilities";
44078
+ import { CSS as CSS3 } from "@dnd-kit/utilities";
43565
44079
  import { cn as cn83 } from "@ai-matrx/design-system";
43566
44080
 
43567
44081
  // src/dashboardLayout.ts
@@ -43625,9 +44139,9 @@ var dashboardCollision = (args) => {
43625
44139
  }).sort((a, b) => a.data.value - b.data.value);
43626
44140
  };
43627
44141
  function DashboardGrid({ items, arranging, onMove, onSpan }) {
43628
- const sensors = useSensors(
43629
- useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
43630
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
44142
+ const sensors = useSensors2(
44143
+ useSensor2(PointerSensor2, { activationConstraint: { distance: 4 } }),
44144
+ useSensor2(KeyboardSensor2, { coordinateGetter: sortableKeyboardCoordinates2 })
43631
44145
  );
43632
44146
  const ids = items.map((item) => item.id);
43633
44147
  function dragEnd(event) {
@@ -43637,7 +44151,7 @@ function DashboardGrid({ items, arranging, onMove, onSpan }) {
43637
44151
  }
43638
44152
  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)) });
43639
44153
  if (!arranging) return grid;
43640
- return /* @__PURE__ */ jsx138(DndContext, { sensors, collisionDetection: dashboardCollision, onDragEnd: dragEnd, children: /* @__PURE__ */ jsx138(SortableContext, { items: ids, strategy: rectSortingStrategy, children: grid }) });
44154
+ return /* @__PURE__ */ jsx138(DndContext2, { sensors, collisionDetection: dashboardCollision, onDragEnd: dragEnd, children: /* @__PURE__ */ jsx138(SortableContext2, { items: ids, strategy: rectSortingStrategy, children: grid }) });
43641
44155
  }
43642
44156
  function GridCell2({
43643
44157
  item,
@@ -43645,8 +44159,8 @@ function GridCell2({
43645
44159
  arranging,
43646
44160
  onSpan
43647
44161
  }) {
43648
- const sortable = useSortable({ id: item.id, disabled: !arranging });
43649
- const style = arranging ? { transform: CSS.Transform.toString(sortable.transform), transition: sortable.transition } : void 0;
44162
+ const sortable = useSortable2({ id: item.id, disabled: !arranging });
44163
+ const style = arranging ? { transform: CSS3.Transform.toString(sortable.transform), transition: sortable.transition } : void 0;
43650
44164
  return /* @__PURE__ */ jsxs128(
43651
44165
  "div",
43652
44166
  {
@@ -44852,7 +45366,7 @@ function TargetControl({
44852
45366
  // src/QuestionPanel.tsx
44853
45367
  import { copyText as copyText8 } from "@ai-matrx/kit/clipboard";
44854
45368
  import { useEffect as useEffect103, useMemo as useMemo72, useRef as useRef59, useState as useState118 } from "react";
44855
- 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";
45369
+ 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";
44856
45370
  import { useFields as useFields31, useRecordsClient as useRecordsClient86 } from "@ai-matrx/records/react";
44857
45371
 
44858
45372
  // src/question.ts
@@ -45434,9 +45948,9 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45434
45948
  linkMissed ? /* @__PURE__ */ jsx143("p", { className: "text-muted-foreground", children: "That question is not here or not shared with you." }) : null,
45435
45949
  saveError ? /* @__PURE__ */ jsx143(RefusalNotice, { error: saveError }) : null,
45436
45950
  /* @__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: [
45437
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Measure" }),
45951
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Measure" }),
45438
45952
  /* @__PURE__ */ jsx143(Measures, { measures: spec.measures, fields: offered, onChange: (measures) => patch({ measures }) }),
45439
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Filter" }),
45953
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Filter" }),
45440
45954
  /* @__PURE__ */ jsxs132("div", { className: "flex flex-col gap-1.5", children: [
45441
45955
  /* @__PURE__ */ jsx143(
45442
45956
  ConditionGroup,
@@ -45450,9 +45964,9 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45450
45964
  ),
45451
45965
  /* @__PURE__ */ jsx143(Through, { conditions: spec.through, related, onChange: (through) => patch({ through }) })
45452
45966
  ] }),
45453
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Summarize by" }),
45967
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Summarize by" }),
45454
45968
  /* @__PURE__ */ jsx143(GroupPicker, { spec, fields: offered, related, onChange: (group) => patch({ group }) }),
45455
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "As of" }),
45969
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "As of" }),
45456
45970
  /* @__PURE__ */ jsxs132("div", { className: "flex items-center gap-1.5", children: [
45457
45971
  /* @__PURE__ */ jsx143(
45458
45972
  Checkbox16,
@@ -45476,7 +45990,7 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45476
45990
  ) : /* @__PURE__ */ jsx143("span", { className: "text-muted-foreground", children: "Now" })
45477
45991
  ] }),
45478
45992
  spec.group ? /* @__PURE__ */ jsxs132(Fragment57, { children: [
45479
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Sort" }),
45993
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Sort" }),
45480
45994
  /* @__PURE__ */ jsxs132("div", { className: "flex flex-wrap items-center gap-1.5", children: [
45481
45995
  /* @__PURE__ */ jsx143(
45482
45996
  PickOne,
@@ -45507,7 +46021,7 @@ function QuestionPanel({ tableId, activeQuestionId, draft, className }) {
45507
46021
  }
45508
46022
  )
45509
46023
  ] }),
45510
- /* @__PURE__ */ jsx143(Label24, { className: "pt-1.5 text-xs text-muted-foreground", children: "Show as" }),
46024
+ /* @__PURE__ */ jsx143(Label25, { className: "pt-1.5 text-xs text-muted-foreground", children: "Show as" }),
45511
46025
  /* @__PURE__ */ jsx143("div", { className: "flex flex-wrap gap-1", role: "radiogroup", "aria-label": "Show as", children: SHOWS.map((s) => /* @__PURE__ */ jsx143(
45512
46026
  Button94,
45513
46027
  {