@ai-matrx/records-ui 0.106.0 → 0.107.0

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