@ai-matrx/records-ui 0.53.0 → 0.57.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
@@ -2539,7 +2539,7 @@ function EnrichPanel({ tableId, fieldId, className }) {
2539
2539
  import { useState as useState8 } from "react";
2540
2540
  import { Badge as Badge4, Button as Button6, Separator } from "@ai-matrx/design-system";
2541
2541
  import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2542
- function PastePreview({ plan, noun, onConfirm, onCancel }) {
2542
+ function PastePreview({ plan, noun, nounPlural, onConfirm, onCancel }) {
2543
2543
  const [busy, setBusy] = useState8(false);
2544
2544
  const [outcome, setOutcome] = useState8(null);
2545
2545
  const nothingToDo = plan.cells === 0;
@@ -2559,7 +2559,7 @@ function PastePreview({ plan, noun, onConfirm, onCancel }) {
2559
2559
  /* @__PURE__ */ jsx11(Separator, {}),
2560
2560
  outcome === null ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
2561
2561
  nothingToDo ? /* @__PURE__ */ jsx11("p", { className: "text-sm", children: "Nothing in what you pasted can be written to this table. Every cell is listed below with the reason." }) : /* @__PURE__ */ jsxs7("p", { className: "text-sm", children: [
2562
- sentence(plan, noun),
2562
+ sentence(plan, noun, nounPlural),
2563
2563
  " The values below are exactly what will be stored."
2564
2564
  ] }),
2565
2565
  /* @__PURE__ */ jsx11(PlanTable, { plan }),
@@ -2599,7 +2599,7 @@ function PastePreview({ plan, noun, onConfirm, onCancel }) {
2599
2599
  setBusy(true);
2600
2600
  void onConfirm().then(setOutcome).finally(() => setBusy(false));
2601
2601
  },
2602
- children: busy ? "Writing\u2026" : confirmLabel(plan, noun)
2602
+ children: busy ? "Writing\u2026" : confirmLabel(plan, noun, nounPlural)
2603
2603
  }
2604
2604
  )
2605
2605
  ] })
@@ -2654,19 +2654,19 @@ function plural(n, one, many) {
2654
2654
  function summarize(plan) {
2655
2655
  return `${plural(plan.cells, "value", "values")} across ${plural(plan.rows.length, "row", "rows")}`;
2656
2656
  }
2657
- function sentence(plan, noun) {
2657
+ function sentence(plan, noun, nounPlural) {
2658
2658
  const parts = [];
2659
- if (plan.updates > 0) parts.push(`change ${plural(plan.updates, noun, `${noun}s`)}`);
2660
- if (plan.creates > 0) parts.push(`create ${plural(plan.creates, `new ${noun}`, `new ${noun}s`)}`);
2659
+ if (plan.updates > 0) parts.push(`change ${plural(plan.updates, noun, nounPlural)}`);
2660
+ if (plan.creates > 0) parts.push(`create ${plural(plan.creates, `new ${noun}`, `new ${nounPlural}`)}`);
2661
2661
  const what = parts.length === 2 ? `${parts[0]} and ${parts[1]}` : parts[0] ?? "write nothing";
2662
2662
  return `This will ${what}.`;
2663
2663
  }
2664
- function confirmLabel(plan, noun) {
2664
+ function confirmLabel(plan, noun, nounPlural) {
2665
2665
  if (plan.creates > 0 && plan.updates === 0) {
2666
- return `Create ${plural(plan.creates, noun, `${noun}s`)}`;
2666
+ return `Create ${plural(plan.creates, noun, nounPlural)}`;
2667
2667
  }
2668
2668
  if (plan.creates > 0) return `Write ${plural(plan.rows.length, "row", "rows")}`;
2669
- return `Update ${plural(plan.updates, noun, `${noun}s`)}`;
2669
+ return `Update ${plural(plan.updates, noun, nounPlural)}`;
2670
2670
  }
2671
2671
 
2672
2672
  // src/pasteValues.ts
@@ -2701,13 +2701,21 @@ function valueFromPastedText(field, raw) {
2701
2701
  return { value: negative ? -n : n };
2702
2702
  }
2703
2703
  case "datetime": {
2704
+ const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
2705
+ const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
2706
+ if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
2704
2707
  const at = Date.parse(text);
2705
2708
  if (Number.isNaN(at)) {
2706
2709
  return {
2707
2710
  refusal: `${label} holds a date, and \u201C${raw}\u201D is not one this can read. Paste it as 2026-09-21, or as your spreadsheet's own date format.`
2708
2711
  };
2709
2712
  }
2710
- return { value: new Date(at).toISOString() };
2713
+ const parsed = new Date(at);
2714
+ if (wantsTime) return { value: parsed.toISOString() };
2715
+ const y = parsed.getFullYear();
2716
+ const m = String(parsed.getMonth() + 1).padStart(2, "0");
2717
+ const d = String(parsed.getDate()).padStart(2, "0");
2718
+ return { value: `${y}-${m}-${d}` };
2711
2719
  }
2712
2720
  case "json": {
2713
2721
  try {
@@ -2898,42 +2906,55 @@ function Grid({
2898
2906
  const fieldList = fields.data ?? [];
2899
2907
  const byRecord = /* @__PURE__ */ new Map();
2900
2908
  const order = [];
2901
- for (const patch of sheetPlan.accepted) {
2902
- let held = byRecord.get(patch.rowId);
2909
+ const take = (rowId, columnId, value) => {
2910
+ let held = byRecord.get(rowId);
2903
2911
  if (held === void 0) {
2904
2912
  held = {};
2905
- byRecord.set(patch.rowId, held);
2906
- order.push(patch.rowId);
2913
+ byRecord.set(rowId, held);
2914
+ order.push(rowId);
2907
2915
  }
2908
- held[patch.columnId] = patch.value === null ? "" : String(patch.value);
2916
+ held[columnId] = value === null || value === void 0 ? "" : String(value);
2917
+ };
2918
+ for (const patch of sheetPlan.accepted) take(patch.rowId, patch.columnId, patch.value);
2919
+ for (const refused of sheetPlan.refusals) {
2920
+ if (refused.rowId === null) continue;
2921
+ take(refused.rowId, refused.columnId, refused.value);
2909
2922
  }
2923
+ const onScreen = rows.map((r) => r.id).filter((id) => byRecord.has(id));
2924
+ const touched = [...onScreen, ...order.filter((id) => !onScreen.includes(id))];
2910
2925
  const columnKeys = Array.from(
2911
2926
  /* @__PURE__ */ new Set([
2912
2927
  ...sheetPlan.accepted.map((p) => p.columnId),
2928
+ ...sheetPlan.refusals.map((r) => r.columnId),
2913
2929
  ...sheetPlan.overflow.flatMap((row) => Object.keys(row))
2914
2930
  ])
2915
2931
  );
2916
2932
  const built = planPastedBlock({
2917
2933
  fields: fieldList,
2918
2934
  columnKeys,
2919
- onExisting: order.map((recordId) => ({
2935
+ onExisting: touched.map((recordId) => ({
2920
2936
  recordId,
2921
2937
  raw: columnKeys.map((key) => byRecord.get(recordId)?.[key])
2922
2938
  })),
2923
2939
  onNew: sheetPlan.overflow.map((row) => columnKeys.map((key) => row[key])),
2924
2940
  pastRightEdge: sheetPlan.pastRightEdge
2925
2941
  });
2942
+ const notWritable = /* @__PURE__ */ new Map();
2926
2943
  for (const refused of sheetPlan.refusals) {
2944
+ if (built.refusals.some((r) => r.column === columnLabel(fieldList, refused.columnId))) continue;
2945
+ notWritable.set(refused.columnId, refused.reason);
2946
+ }
2947
+ for (const [columnId, why] of notWritable) {
2927
2948
  built.refusals.push({
2928
2949
  fromLine: 0,
2929
- column: refused.columnId,
2950
+ column: columnLabel(fieldList, columnId),
2930
2951
  raw: "",
2931
- why: refused.reason
2952
+ why
2932
2953
  });
2933
2954
  }
2934
2955
  setPastePlan({ plan: built });
2935
2956
  },
2936
- [fields.data]
2957
+ [fields.data, rows]
2937
2958
  );
2938
2959
  const applyPaste = useCallback6(async () => {
2939
2960
  const held = pastePlan;
@@ -3244,6 +3265,7 @@ function Grid({
3244
3265
  {
3245
3266
  plan: pastePlan.plan,
3246
3267
  noun: table.data?.label_singular ?? "record",
3268
+ nounPlural: table.data?.label_plural ?? `${table.data?.label_singular ?? "record"}s`,
3247
3269
  onConfirm: applyPaste,
3248
3270
  onCancel: () => setPastePlan(null)
3249
3271
  }
@@ -3261,6 +3283,10 @@ function Grid({
3261
3283
  ] })
3262
3284
  ] }) });
3263
3285
  }
3286
+ function columnLabel(fields, columnId) {
3287
+ const field = fields.find((f) => f.key === columnId);
3288
+ return field ? fieldName(field) : humanize(columnId);
3289
+ }
3264
3290
  function AddFieldButton({ onClick }) {
3265
3291
  return /* @__PURE__ */ jsx12(
3266
3292
  Button7,
@@ -4621,6 +4647,60 @@ var CONDITION_OPS = [
4621
4647
  { op: "gt", label: "is more than" },
4622
4648
  { op: "lt", label: "is less than" }
4623
4649
  ];
4650
+ function conditionIsSimple(expr) {
4651
+ if (expr === null || expr === void 0) return true;
4652
+ const node = expr;
4653
+ if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
4654
+ if (!Array.isArray(node.args)) return false;
4655
+ const first = node.args[0];
4656
+ if (!first || typeof first.field !== "string") return false;
4657
+ if (node.op === "present") return node.args.length === 1;
4658
+ const second = node.args[1];
4659
+ return node.args.length === 2 && !!second && "const" in second;
4660
+ }
4661
+ function conditionInWords(expr, fields) {
4662
+ const name = (id) => {
4663
+ const f = fields.find((x) => x.id === id);
4664
+ return f ? f.label || f.key : "a column";
4665
+ };
4666
+ const said = {
4667
+ eq: "is",
4668
+ ne: "is not",
4669
+ gt: "is more than",
4670
+ lt: "is less than",
4671
+ gte: "is at least",
4672
+ lte: "is at most",
4673
+ and: "and",
4674
+ or: "or",
4675
+ matches: "matches"
4676
+ };
4677
+ const walk = (node) => {
4678
+ if (node === null || node === void 0) return "nothing";
4679
+ const n = node;
4680
+ if ("const" in n) return JSON.stringify(n["const"]);
4681
+ if (typeof n["field"] === "string" && !("op" in n)) return name(n["field"]);
4682
+ const op = typeof n["op"] === "string" ? n["op"] : "";
4683
+ if (op === "previous" && typeof n["field"] === "string") return `what ${name(n["field"])} used to say`;
4684
+ if (op === "sibling_count") {
4685
+ const same = n["same"] ?? [];
4686
+ const differs = n["differs"] ?? [];
4687
+ const sameWords = same.length > 0 ? ` for the same ${same.map(name).join(" and ")}` : "";
4688
+ const diffWords = differs.length > 0 ? ` from a different ${differs.map(name).join(" and ")}` : "";
4689
+ return `how many others there are${sameWords}${diffWords}`;
4690
+ }
4691
+ if (op === "stage_count") return "how many are in that column already";
4692
+ if (op === "actor_at_least") return `whoever is moving it is at least ${String(n["level"] ?? "somebody")}`;
4693
+ const args = Array.isArray(n["args"]) ? n["args"] : [];
4694
+ if (op === "not") return `not ${walk(args[0])}`;
4695
+ if (op === "present") return `${walk(args[0])} has been answered`;
4696
+ const word = said[op] ?? op;
4697
+ if (op === "and" || op === "or") return args.map(walk).join(` ${word} `);
4698
+ if (args.length === 2) return `${walk(args[0])} ${word} ${walk(args[1])}`;
4699
+ if (args.length === 1) return `${walk(args[0])} ${word}`;
4700
+ return op;
4701
+ };
4702
+ return walk(expr);
4703
+ }
4624
4704
  function conditionValue(typed) {
4625
4705
  const trimmed = typed.trim();
4626
4706
  if (trimmed === "") return typed;
@@ -4692,6 +4772,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
4692
4772
  import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4693
4773
  var NO_RULES_YET = "Nothing has to be true to get here yet.";
4694
4774
  var NEEDS_A_SENTENCE = "Write the sentence somebody reads when this stops them, the way you would say it out loud.";
4775
+ var NOT_DRAWN_HERE = "This part of the rule is not something this builder can draw yet";
4695
4776
  var ON_FAIL = [
4696
4777
  { value: "refuse", label: "Turn it away", note: "The card stays where it is and the person is told why." },
4697
4778
  {
@@ -4872,7 +4953,7 @@ function StageRulesSection({ tableId, stage, className }) {
4872
4953
  }
4873
4954
  ),
4874
4955
  /* @__PURE__ */ jsx18(
4875
- ConditionRow,
4956
+ Clause,
4876
4957
  {
4877
4958
  lead: "When",
4878
4959
  emptyLabel: "every card",
@@ -4882,7 +4963,7 @@ function StageRulesSection({ tableId, stage, className }) {
4882
4963
  }
4883
4964
  ),
4884
4965
  /* @__PURE__ */ jsx18(
4885
- ConditionRow,
4966
+ Clause,
4886
4967
  {
4887
4968
  lead: "it cannot get here unless",
4888
4969
  emptyLabel: "\u2014 pick a column \u2014",
@@ -4926,6 +5007,50 @@ function StageRulesSection({ tableId, stage, className }) {
4926
5007
  /* @__PURE__ */ jsx18(Separator5, {})
4927
5008
  ] });
4928
5009
  }
5010
+ function Clause({
5011
+ lead,
5012
+ emptyLabel,
5013
+ expr,
5014
+ fields,
5015
+ onChange
5016
+ }) {
5017
+ const [replacing, setReplacing] = useState14(false);
5018
+ const drawable = conditionIsSimple(expr) || replacing;
5019
+ if (drawable) {
5020
+ return /* @__PURE__ */ jsx18(
5021
+ ConditionRow,
5022
+ {
5023
+ lead,
5024
+ emptyLabel,
5025
+ expr: replacing && !conditionIsSimple(expr) ? null : expr,
5026
+ fields,
5027
+ onChange
5028
+ }
5029
+ );
5030
+ }
5031
+ return /* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap items-center gap-1 text-xs", "data-testid": "stage-rule-clause-in-words", children: [
5032
+ /* @__PURE__ */ jsx18("span", { className: "text-muted-foreground", children: lead }),
5033
+ /* @__PURE__ */ jsx18("span", { className: "font-medium", children: conditionInWords(expr, fields) }),
5034
+ /* @__PURE__ */ jsxs14("span", { className: "text-muted-foreground", children: [
5035
+ "\u2014 ",
5036
+ NOT_DRAWN_HERE,
5037
+ ", so it is shown as it reads."
5038
+ ] }),
5039
+ /* @__PURE__ */ jsx18(
5040
+ Button12,
5041
+ {
5042
+ size: "sm",
5043
+ variant: "ghost",
5044
+ className: "h-6",
5045
+ onClick: () => {
5046
+ setReplacing(true);
5047
+ onChange(null);
5048
+ },
5049
+ children: "Replace it"
5050
+ }
5051
+ )
5052
+ ] });
5053
+ }
4929
5054
 
4930
5055
  // src/TableSettings.tsx
4931
5056
  import { useState as useState15 } from "react";
@@ -8685,7 +8810,7 @@ function PortalBuilder({ tableId, portalId, onSaved, onClose, className }) {
8685
8810
  const all = tables.data ?? [];
8686
8811
  const missingTie = ticked.filter((e) => e.namesVia.trim() === "");
8687
8812
  const ready = clientTableId !== null && ticked.length > 0 && missingTie.length === 0;
8688
- return /* @__PURE__ */ jsxs29("section", { className: cn27("flex flex-col gap-3", className), children: [
8813
+ return /* @__PURE__ */ jsxs29("section", { className: cn27("@container flex flex-col gap-3", className), children: [
8689
8814
  /* @__PURE__ */ jsxs29("header", { className: "flex items-center gap-2", children: [
8690
8815
  /* @__PURE__ */ jsx33("h3", { className: "text-sm font-medium", children: existing ? "Portal" : "New client portal" }),
8691
8816
  /* @__PURE__ */ jsx33("div", { className: "flex-1" }),
@@ -8826,7 +8951,7 @@ function PortalBuilder({ tableId, portalId, onSaved, onClose, className }) {
8826
8951
  /* @__PURE__ */ jsx33(
8827
8952
  BasicInput8,
8828
8953
  {
8829
- className: "h-8 w-56",
8954
+ className: "h-8 w-full @md:w-56",
8830
8955
  value: inviteEmail,
8831
8956
  onChange: (e) => setInviteEmail(e.target.value)
8832
8957
  }
@@ -8837,7 +8962,7 @@ function PortalBuilder({ tableId, portalId, onSaved, onClose, className }) {
8837
8962
  /* @__PURE__ */ jsx33(
8838
8963
  BasicInput8,
8839
8964
  {
8840
- className: "h-8 w-80",
8965
+ className: "h-8 w-full @md:w-80",
8841
8966
  placeholder: "the record id",
8842
8967
  value: inviteRecordId,
8843
8968
  onChange: (e) => setInviteRecordId(e.target.value)
@@ -9536,7 +9661,7 @@ function DigestScheduler({
9536
9661
  setPreview(answered.data);
9537
9662
  }
9538
9663
  if (views === null && !error) return /* @__PURE__ */ jsx36(Skeleton16, { className: cn30("h-56 w-full", className) });
9539
- return /* @__PURE__ */ jsxs32("section", { className: cn30("flex flex-col gap-3", className), children: [
9664
+ return /* @__PURE__ */ jsxs32("section", { className: cn30("@container flex flex-col gap-3", className), children: [
9540
9665
  /* @__PURE__ */ jsxs32("header", { className: "flex items-center gap-2", children: [
9541
9666
  /* @__PURE__ */ jsx36("h3", { className: "text-sm font-medium", children: "Send this on a schedule" }),
9542
9667
  /* @__PURE__ */ jsx36("div", { className: "flex-1" }),
@@ -9571,7 +9696,7 @@ function DigestScheduler({
9571
9696
  ),
9572
9697
  /* @__PURE__ */ jsx36("span", { className: "text-xs text-muted-foreground", children: "The first choice saves what is on screen as a view, so the summary and the screen stay the same thing." })
9573
9698
  ] }),
9574
- /* @__PURE__ */ jsxs32("div", { className: "grid grid-cols-2 gap-2", children: [
9699
+ /* @__PURE__ */ jsxs32("div", { className: "grid grid-cols-1 gap-2 @md:grid-cols-2", children: [
9575
9700
  /* @__PURE__ */ jsxs32("label", { className: "flex flex-col gap-1", children: [
9576
9701
  /* @__PURE__ */ jsx36(Label5, { className: "text-xs font-medium", children: "How often" }),
9577
9702
  /* @__PURE__ */ jsx36(
@@ -10320,7 +10445,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
10320
10445
  if (!rights.admin) {
10321
10446
  return /* @__PURE__ */ jsx39("p", { className: cn33("text-xs text-muted-foreground", className), children: rights.why("structure") });
10322
10447
  }
10323
- return /* @__PURE__ */ jsxs35("div", { className: cn33("flex min-h-0 gap-3", className), children: [
10448
+ return /* @__PURE__ */ jsxs35("div", { className: cn33("@container flex min-h-0 flex-col gap-3 @3xl:flex-row", className), children: [
10324
10449
  /* @__PURE__ */ jsxs35("div", { className: "flex min-w-0 flex-1 flex-col gap-2 overflow-auto", children: [
10325
10450
  /* @__PURE__ */ jsxs35("div", { className: "flex items-center gap-1 overflow-x-auto", role: "group", "aria-label": "Forms", children: [
10326
10451
  forms.map((form) => /* @__PURE__ */ jsx39(
@@ -10481,7 +10606,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
10481
10606
  ] })
10482
10607
  ] })
10483
10608
  ] }),
10484
- draft ? /* @__PURE__ */ jsxs35("aside", { className: "w-96 shrink-0 overflow-auto rounded border p-2", children: [
10609
+ draft ? /* @__PURE__ */ jsxs35("aside", { className: "w-full shrink-0 overflow-auto rounded border p-2 @3xl:w-96", children: [
10485
10610
  /* @__PURE__ */ jsx39("h4", { className: "px-1 text-sm font-medium", children: draft.name }),
10486
10611
  /* @__PURE__ */ jsx39(FormRunner, { form: draft, preview: true })
10487
10612
  ] }) : null
@@ -12269,195 +12394,200 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
12269
12394
  }
12270
12395
  const url = formId ? `${publicOrigin}${bookingPath(formId)}` : null;
12271
12396
  const shown = offer ?? null;
12272
- return /* @__PURE__ */ jsxs44("section", { className: cn42("flex flex-col gap-3", className), children: [
12273
- /* @__PURE__ */ jsxs44("header", { className: "flex items-center gap-2", children: [
12274
- /* @__PURE__ */ jsx48("h3", { className: "text-sm font-medium", children: existing ? "Booking page" : "New booking page" }),
12275
- /* @__PURE__ */ jsx48("div", { className: "flex-1" }),
12276
- /* @__PURE__ */ jsx48(Button39, { size: "sm", disabled: saving, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" }),
12277
- onClose ? /* @__PURE__ */ jsx48(Button39, { size: "sm", variant: "ghost", onClick: onClose, children: "Close" }) : null
12278
- ] }),
12279
- error ? /* @__PURE__ */ jsx48(RefusalNotice, { error }) : null,
12280
- /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12281
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "What it is called" }),
12282
- /* @__PURE__ */ jsx48(BasicInput15, { value: title, onChange: (e) => setTitle(e.target.value) })
12283
- ] }),
12284
- /* @__PURE__ */ jsxs44("div", { className: "grid grid-cols-2 gap-2", children: [
12285
- /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12286
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "How long one appointment is" }),
12287
- /* @__PURE__ */ jsx48(
12288
- "select",
12289
- {
12290
- className: "h-9 rounded border bg-background px-2 text-sm",
12291
- value: minutes,
12292
- onChange: (e) => setMinutes(Number(e.target.value)),
12293
- children: LENGTHS.map((n) => /* @__PURE__ */ jsxs44("option", { value: n, children: [
12294
- n,
12295
- " minutes"
12296
- ] }, n))
12297
- }
12298
- )
12299
- ] }),
12300
- /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12301
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Gap kept after each one" }),
12302
- /* @__PURE__ */ jsx48(
12303
- "select",
12304
- {
12305
- className: "h-9 rounded border bg-background px-2 text-sm",
12306
- value: buffer,
12307
- onChange: (e) => setBuffer(Number(e.target.value)),
12308
- children: [0, 5, 10, 15, 30].map((n) => /* @__PURE__ */ jsx48("option", { value: n, children: n === 0 ? "No gap \u2014 back to back" : `${n} minutes` }, n))
12309
- }
12310
- )
12311
- ] }),
12312
- /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12313
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Earliest somebody may book" }),
12314
- /* @__PURE__ */ jsx48(
12315
- "select",
12316
- {
12317
- className: "h-9 rounded border bg-background px-2 text-sm",
12318
- value: lead,
12319
- onChange: (e) => setLead(Number(e.target.value)),
12320
- children: [0, 60, 120, 240, 1440].map((n) => /* @__PURE__ */ jsx48("option", { value: n, children: n === 0 ? "Right away" : n === 1440 ? "A day from now" : `${n / 60} hour${n === 60 ? "" : "s"} from now` }, n))
12321
- }
12322
- )
12397
+ return (
12398
+ // THE RAIL IS 384px WIDE AND THE VIEWPORT IS NOT (walk 1, 2026-09-21):
12399
+ // every split below is a CONTAINER query, so this screen is the same
12400
+ // screen in a rail, a dialog and a full page.
12401
+ /* @__PURE__ */ jsxs44("section", { className: cn42("@container flex flex-col gap-3", className), children: [
12402
+ /* @__PURE__ */ jsxs44("header", { className: "flex items-center gap-2", children: [
12403
+ /* @__PURE__ */ jsx48("h3", { className: "text-sm font-medium", children: existing ? "Booking page" : "New booking page" }),
12404
+ /* @__PURE__ */ jsx48("div", { className: "flex-1" }),
12405
+ /* @__PURE__ */ jsx48(Button39, { size: "sm", disabled: saving, onClick: () => void save(), children: saving ? "Saving\u2026" : "Save" }),
12406
+ onClose ? /* @__PURE__ */ jsx48(Button39, { size: "sm", variant: "ghost", onClick: onClose, children: "Close" }) : null
12323
12407
  ] }),
12408
+ error ? /* @__PURE__ */ jsx48(RefusalNotice, { error }) : null,
12324
12409
  /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12325
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Most in one day" }),
12326
- /* @__PURE__ */ jsx48(
12327
- BasicInput15,
12328
- {
12329
- type: "number",
12330
- min: 1,
12331
- value: perDay,
12332
- onChange: (e) => setPerDay(Math.max(1, Number(e.target.value) || 1))
12333
- }
12334
- )
12410
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "What it is called" }),
12411
+ /* @__PURE__ */ jsx48(BasicInput15, { value: title, onChange: (e) => setTitle(e.target.value) })
12335
12412
  ] }),
12336
- /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12337
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "How far ahead the page offers" }),
12338
- /* @__PURE__ */ jsx48(
12339
- "select",
12340
- {
12341
- className: "h-9 rounded border bg-background px-2 text-sm",
12342
- value: days,
12343
- onChange: (e) => setDays(Number(e.target.value)),
12344
- children: [7, 14, 30, 60, 90].map((n) => /* @__PURE__ */ jsxs44("option", { value: n, children: [
12345
- n,
12346
- " days"
12347
- ] }, n))
12348
- }
12349
- )
12350
- ] })
12351
- ] }),
12352
- /* @__PURE__ */ jsxs44("div", { className: "flex flex-col gap-1.5", children: [
12353
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Hours you are free" }),
12354
- windows.map((w, i) => /* @__PURE__ */ jsxs44("div", { className: "flex items-center gap-2", children: [
12355
- /* @__PURE__ */ jsxs44("label", { className: "flex w-20 items-center gap-1.5 text-xs", children: [
12413
+ /* @__PURE__ */ jsxs44("div", { className: "grid grid-cols-1 gap-2 @md:grid-cols-2", children: [
12414
+ /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12415
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "How long one appointment is" }),
12356
12416
  /* @__PURE__ */ jsx48(
12357
- Checkbox7,
12417
+ "select",
12358
12418
  {
12359
- checked: w.on,
12360
- onCheckedChange: (on) => setWindows((prev) => prev.map((x, j) => i === j ? { ...x, on: Boolean(on) } : x))
12419
+ className: "h-9 rounded border bg-background px-2 text-sm",
12420
+ value: minutes,
12421
+ onChange: (e) => setMinutes(Number(e.target.value)),
12422
+ children: LENGTHS.map((n) => /* @__PURE__ */ jsxs44("option", { value: n, children: [
12423
+ n,
12424
+ " minutes"
12425
+ ] }, n))
12361
12426
  }
12362
- ),
12363
- DAYS[i]?.label
12427
+ )
12364
12428
  ] }),
12365
- w.on ? /* @__PURE__ */ jsxs44(Fragment22, { children: [
12429
+ /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12430
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Gap kept after each one" }),
12366
12431
  /* @__PURE__ */ jsx48(
12367
- BasicInput15,
12432
+ "select",
12368
12433
  {
12369
- type: "time",
12370
- className: "h-8 w-28",
12371
- value: w.from,
12372
- onChange: (e) => setWindows((prev) => prev.map((x, j) => i === j ? { ...x, from: e.target.value } : x))
12434
+ className: "h-9 rounded border bg-background px-2 text-sm",
12435
+ value: buffer,
12436
+ onChange: (e) => setBuffer(Number(e.target.value)),
12437
+ children: [0, 5, 10, 15, 30].map((n) => /* @__PURE__ */ jsx48("option", { value: n, children: n === 0 ? "No gap \u2014 back to back" : `${n} minutes` }, n))
12373
12438
  }
12374
- ),
12375
- /* @__PURE__ */ jsx48("span", { className: "text-xs text-muted-foreground", children: "to" }),
12439
+ )
12440
+ ] }),
12441
+ /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12442
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Earliest somebody may book" }),
12443
+ /* @__PURE__ */ jsx48(
12444
+ "select",
12445
+ {
12446
+ className: "h-9 rounded border bg-background px-2 text-sm",
12447
+ value: lead,
12448
+ onChange: (e) => setLead(Number(e.target.value)),
12449
+ children: [0, 60, 120, 240, 1440].map((n) => /* @__PURE__ */ jsx48("option", { value: n, children: n === 0 ? "Right away" : n === 1440 ? "A day from now" : `${n / 60} hour${n === 60 ? "" : "s"} from now` }, n))
12450
+ }
12451
+ )
12452
+ ] }),
12453
+ /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12454
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Most in one day" }),
12376
12455
  /* @__PURE__ */ jsx48(
12377
12456
  BasicInput15,
12378
12457
  {
12379
- type: "time",
12380
- className: "h-8 w-28",
12381
- value: w.to,
12382
- onChange: (e) => setWindows((prev) => prev.map((x, j) => i === j ? { ...x, to: e.target.value } : x))
12458
+ type: "number",
12459
+ min: 1,
12460
+ value: perDay,
12461
+ onChange: (e) => setPerDay(Math.max(1, Number(e.target.value) || 1))
12383
12462
  }
12384
12463
  )
12385
- ] }) : /* @__PURE__ */ jsx48("span", { className: "text-xs text-muted-foreground", children: "not taking bookings" })
12386
- ] }, w.weekday))
12387
- ] }),
12388
- /* @__PURE__ */ jsxs44("div", { className: "flex flex-col gap-1.5", children: [
12389
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "What to ask the person booking" }),
12390
- askable.length === 0 ? /* @__PURE__ */ jsx48("p", { className: "text-xs text-muted-foreground", children: "This table has no fields yet, so there is nothing a booking page could ask for. Add a field first \u2014 every question is one of this table's own fields." }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-wrap gap-x-4 gap-y-1.5", children: askable.map((f) => /* @__PURE__ */ jsxs44("label", { className: "flex items-center gap-1.5 text-xs", children: [
12464
+ ] }),
12465
+ /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12466
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "How far ahead the page offers" }),
12467
+ /* @__PURE__ */ jsx48(
12468
+ "select",
12469
+ {
12470
+ className: "h-9 rounded border bg-background px-2 text-sm",
12471
+ value: days,
12472
+ onChange: (e) => setDays(Number(e.target.value)),
12473
+ children: [7, 14, 30, 60, 90].map((n) => /* @__PURE__ */ jsxs44("option", { value: n, children: [
12474
+ n,
12475
+ " days"
12476
+ ] }, n))
12477
+ }
12478
+ )
12479
+ ] })
12480
+ ] }),
12481
+ /* @__PURE__ */ jsxs44("div", { className: "flex flex-col gap-1.5", children: [
12482
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "Hours you are free" }),
12483
+ windows.map((w, i) => /* @__PURE__ */ jsxs44("div", { className: "flex items-center gap-2", children: [
12484
+ /* @__PURE__ */ jsxs44("label", { className: "flex w-20 items-center gap-1.5 text-xs", children: [
12485
+ /* @__PURE__ */ jsx48(
12486
+ Checkbox7,
12487
+ {
12488
+ checked: w.on,
12489
+ onCheckedChange: (on) => setWindows((prev) => prev.map((x, j) => i === j ? { ...x, on: Boolean(on) } : x))
12490
+ }
12491
+ ),
12492
+ DAYS[i]?.label
12493
+ ] }),
12494
+ w.on ? /* @__PURE__ */ jsxs44(Fragment22, { children: [
12495
+ /* @__PURE__ */ jsx48(
12496
+ BasicInput15,
12497
+ {
12498
+ type: "time",
12499
+ className: "h-8 w-28",
12500
+ value: w.from,
12501
+ onChange: (e) => setWindows((prev) => prev.map((x, j) => i === j ? { ...x, from: e.target.value } : x))
12502
+ }
12503
+ ),
12504
+ /* @__PURE__ */ jsx48("span", { className: "text-xs text-muted-foreground", children: "to" }),
12505
+ /* @__PURE__ */ jsx48(
12506
+ BasicInput15,
12507
+ {
12508
+ type: "time",
12509
+ className: "h-8 w-28",
12510
+ value: w.to,
12511
+ onChange: (e) => setWindows((prev) => prev.map((x, j) => i === j ? { ...x, to: e.target.value } : x))
12512
+ }
12513
+ )
12514
+ ] }) : /* @__PURE__ */ jsx48("span", { className: "text-xs text-muted-foreground", children: "not taking bookings" })
12515
+ ] }, w.weekday))
12516
+ ] }),
12517
+ /* @__PURE__ */ jsxs44("div", { className: "flex flex-col gap-1.5", children: [
12518
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "What to ask the person booking" }),
12519
+ askable.length === 0 ? /* @__PURE__ */ jsx48("p", { className: "text-xs text-muted-foreground", children: "This table has no fields yet, so there is nothing a booking page could ask for. Add a field first \u2014 every question is one of this table's own fields." }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-wrap gap-x-4 gap-y-1.5", children: askable.map((f) => /* @__PURE__ */ jsxs44("label", { className: "flex items-center gap-1.5 text-xs", children: [
12520
+ /* @__PURE__ */ jsx48(
12521
+ Checkbox7,
12522
+ {
12523
+ checked: asked.includes(f.key),
12524
+ onCheckedChange: (on) => setAsked((prev) => on ? [...prev, f.key] : prev.filter((k) => k !== f.key))
12525
+ }
12526
+ ),
12527
+ fieldName(f)
12528
+ ] }, f.key)) }),
12529
+ /* @__PURE__ */ jsx48("p", { className: "text-xs text-muted-foreground", children: "The time, whether it is booked or cancelled, and who it is with are filled in by the store \u2014 a booking page cannot ask a visitor for any of the three." })
12530
+ ] }),
12531
+ /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12532
+ /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "What they see after booking" }),
12391
12533
  /* @__PURE__ */ jsx48(
12392
- Checkbox7,
12534
+ BasicTextarea7,
12393
12535
  {
12394
- checked: asked.includes(f.key),
12395
- onCheckedChange: (on) => setAsked((prev) => on ? [...prev, f.key] : prev.filter((k) => k !== f.key))
12536
+ rows: 2,
12537
+ value: confirmation,
12538
+ placeholder: "You're booked. We'll send a reminder the day before \u2014 reply to that message to move it.",
12539
+ onChange: (e) => setConfirmation(e.target.value)
12540
+ }
12541
+ )
12542
+ ] }),
12543
+ shown ? /* @__PURE__ */ jsxs44("p", { className: "rounded-md border bg-muted/40 p-2.5 text-xs", children: [
12544
+ "Saved. The page offers ",
12545
+ shown.slot_minutes,
12546
+ "-minute appointments in ",
12547
+ shown.timezone,
12548
+ ", up to",
12549
+ " ",
12550
+ shown.max_per_day,
12551
+ " a day, ",
12552
+ shown.days,
12553
+ " days ahead",
12554
+ shown.buffer_minutes > 0 ? `, with ${shown.buffer_minutes} minutes between them` : "",
12555
+ ":",
12556
+ " ",
12557
+ shown.windows.map(
12558
+ (w) => `${DAYS.find((d) => d.weekday === w.weekday)?.label ?? w.weekday} ${w.from}\u2013${w.to}`
12559
+ ).join(", "),
12560
+ "."
12561
+ ] }) : null,
12562
+ url ? /* @__PURE__ */ jsxs44("div", { className: "flex flex-wrap items-center gap-2 rounded-md border p-2.5", children: [
12563
+ /* @__PURE__ */ jsx48("span", { className: "min-w-0 flex-1 break-all text-xs", children: url }),
12564
+ /* @__PURE__ */ jsx48(
12565
+ Button39,
12566
+ {
12567
+ size: "sm",
12568
+ variant: "outline",
12569
+ onClick: () => {
12570
+ void navigator.clipboard.writeText(url).then(() => {
12571
+ setCopied(true);
12572
+ window.setTimeout(() => setCopied(false), 1600);
12573
+ }).catch(() => setCopied(false));
12574
+ },
12575
+ children: copied ? "Copied" : "Copy link"
12396
12576
  }
12397
12577
  ),
12398
- fieldName(f)
12399
- ] }, f.key)) }),
12400
- /* @__PURE__ */ jsx48("p", { className: "text-xs text-muted-foreground", children: "The time, whether it is booked or cancelled, and who it is with are filled in by the store \u2014 a booking page cannot ask a visitor for any of the three." })
12401
- ] }),
12402
- /* @__PURE__ */ jsxs44("label", { className: "flex flex-col gap-1", children: [
12403
- /* @__PURE__ */ jsx48(Label8, { className: "text-xs font-medium", children: "What they see after booking" }),
12404
- /* @__PURE__ */ jsx48(
12405
- BasicTextarea7,
12406
- {
12407
- rows: 2,
12408
- value: confirmation,
12409
- placeholder: "You're booked. We'll send a reminder the day before \u2014 reply to that message to move it.",
12410
- onChange: (e) => setConfirmation(e.target.value)
12411
- }
12412
- )
12413
- ] }),
12414
- shown ? /* @__PURE__ */ jsxs44("p", { className: "rounded-md border bg-muted/40 p-2.5 text-xs", children: [
12415
- "Saved. The page offers ",
12416
- shown.slot_minutes,
12417
- "-minute appointments in ",
12418
- shown.timezone,
12419
- ", up to",
12420
- " ",
12421
- shown.max_per_day,
12422
- " a day, ",
12423
- shown.days,
12424
- " days ahead",
12425
- shown.buffer_minutes > 0 ? `, with ${shown.buffer_minutes} minutes between them` : "",
12426
- ":",
12427
- " ",
12428
- shown.windows.map(
12429
- (w) => `${DAYS.find((d) => d.weekday === w.weekday)?.label ?? w.weekday} ${w.from}\u2013${w.to}`
12430
- ).join(", "),
12431
- "."
12432
- ] }) : null,
12433
- url ? /* @__PURE__ */ jsxs44("div", { className: "flex flex-wrap items-center gap-2 rounded-md border p-2.5", children: [
12434
- /* @__PURE__ */ jsx48("span", { className: "min-w-0 flex-1 break-all text-xs", children: url }),
12435
- /* @__PURE__ */ jsx48(
12436
- Button39,
12437
- {
12438
- size: "sm",
12439
- variant: "outline",
12440
- onClick: () => {
12441
- void navigator.clipboard.writeText(url).then(() => {
12442
- setCopied(true);
12443
- window.setTimeout(() => setCopied(false), 1600);
12444
- }).catch(() => setCopied(false));
12445
- },
12446
- children: copied ? "Copied" : "Copy link"
12447
- }
12448
- ),
12449
- /* @__PURE__ */ jsx48(
12450
- Button39,
12451
- {
12452
- size: "sm",
12453
- disabled: publishing,
12454
- onClick: () => void publish(!(existing?.published_at && !existing.closed_at)),
12455
- children: publishing ? "\u2026" : existing?.published_at && !existing.closed_at ? "Unpublish" : "Publish"
12456
- }
12457
- ),
12458
- /* @__PURE__ */ jsx48("p", { className: "w-full text-xs text-muted-foreground", children: existing?.published_at && !existing.closed_at ? "Open. Anyone with this link can book a time." : "Not open yet \u2014 the link does not take bookings until you publish it." })
12459
- ] }) : null
12460
- ] });
12578
+ /* @__PURE__ */ jsx48(
12579
+ Button39,
12580
+ {
12581
+ size: "sm",
12582
+ disabled: publishing,
12583
+ onClick: () => void publish(!(existing?.published_at && !existing.closed_at)),
12584
+ children: publishing ? "\u2026" : existing?.published_at && !existing.closed_at ? "Unpublish" : "Publish"
12585
+ }
12586
+ ),
12587
+ /* @__PURE__ */ jsx48("p", { className: "w-full text-xs text-muted-foreground", children: existing?.published_at && !existing.closed_at ? "Open. Anyone with this link can book a time." : "Not open yet \u2014 the link does not take bookings until you publish it." })
12588
+ ] }) : null
12589
+ ] })
12590
+ );
12461
12591
  }
12462
12592
 
12463
12593
  // src/BookingSlots.tsx
@@ -14268,6 +14398,7 @@ export {
14268
14398
  MyChecklistSteps,
14269
14399
  NEEDS_A_SENTENCE,
14270
14400
  NOT_ANSWERED_YET,
14401
+ NOT_DRAWN_HERE,
14271
14402
  NO_AGENT_PORT,
14272
14403
  NO_CHAT_REASON,
14273
14404
  NO_COLUMNS_WHY,
@@ -14336,6 +14467,8 @@ export {
14336
14467
  chooseSurface,
14337
14468
  colorFromTheValue,
14338
14469
  columnForField,
14470
+ conditionInWords,
14471
+ conditionIsSimple,
14339
14472
  conditionValue,
14340
14473
  controlFor,
14341
14474
  currencyCodeFor,