@broadpaper/editor 0.2.0 → 0.3.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
@@ -300,6 +300,11 @@ var EditorSession = class {
300
300
  if (seq !== this.layoutSeq || this.disposed) return;
301
301
  this.layout.set(result);
302
302
  this.layoutVersion.set((v) => v + 1);
303
+ if (m.settled) {
304
+ void m.settled().then((arrived) => {
305
+ if (arrived && seq === this.layoutSeq && !this.disposed) this.requestLayout();
306
+ });
307
+ }
303
308
  } catch (e) {
304
309
  console.error("[broadpaper] layout failed", e);
305
310
  }
@@ -389,7 +394,7 @@ function loadTemplate(input) {
389
394
  }
390
395
 
391
396
  // src/ReportDesigner.tsx
392
- import { useEffect as useEffect7, useMemo as useMemo8, useRef as useRef9, useState as useState17 } from "react";
397
+ import { useEffect as useEffect8, useMemo as useMemo9, useRef as useRef10, useState as useState19 } from "react";
393
398
 
394
399
  // src/state/context.tsx
395
400
  import { createContext, useContext, useSyncExternalStore as useSyncExternalStore2, useMemo } from "react";
@@ -892,6 +897,28 @@ function createActions(session) {
892
897
  session.reveal(node.id);
893
898
  return node.id;
894
899
  },
900
+ /**
901
+ * Puts a data field on the page.
902
+ *
903
+ * Shared with the drop handler on purpose: clicking a field in the data
904
+ * panel and dragging it onto the page have to produce the same block, and
905
+ * they did not when each built its own props. The block itself decides what
906
+ * to build, through its `fromField` hook, so a list becomes a finished
907
+ * table and a custom block can answer for itself.
908
+ */
909
+ insertField(field, at) {
910
+ const type = field.type === "array" ? "table" : "field";
911
+ const def = session.registry.get(type);
912
+ const props = def?.fromField?.({
913
+ expr: field.expr,
914
+ formatted: field.bound ?? field.expr,
915
+ schema: field.schema ?? { type: field.type ?? "string" },
916
+ label: field.label
917
+ });
918
+ const leaf = field.label.split(" \u203A ").pop() ?? field.label;
919
+ const fallback = type === "table" ? { source: field.expr } : { label: leaf, value: field.bound ?? field.expr };
920
+ return this.insertBlock(type, at, props ?? fallback);
921
+ },
895
922
  insertRow(layout, at) {
896
923
  const row = createRow(layout);
897
924
  store.transaction("Add columns", () => store.dispatch({ type: "insertNode", node: row, at: at ?? insertionPoint() }));
@@ -1677,7 +1704,7 @@ function IssueGroup({ title, blurb, emptyTitle, emptyBody, issues, close, sessio
1677
1704
 
1678
1705
  // src/panels/LeftPanel.tsx
1679
1706
  import { useMemo as useMemo2, useState as useState5 } from "react";
1680
- import { ROW_LAYOUTS } from "@broadpaper/core";
1707
+ import { ROW_LAYOUTS, withDefaultPipe } from "@broadpaper/core";
1681
1708
 
1682
1709
  // src/dnd/drag-manager.ts
1683
1710
  var handlers = /* @__PURE__ */ new WeakMap();
@@ -1902,7 +1929,7 @@ function buildNodes(fields, prefix, labels) {
1902
1929
  const expr = prefix + "." + key;
1903
1930
  const label = [...labels, name].join(" \u203A ");
1904
1931
  if (f.type === "object" && f.fields) {
1905
- return { key, label, name, type: f.type, kind: "group", children: buildNodes(f.fields, expr, [...labels, name]) };
1932
+ return { key, label, name, type: f.type, schema: f, kind: "group", children: buildNodes(f.fields, expr, [...labels, name]) };
1906
1933
  }
1907
1934
  if (f.type === "array" && f.itemSchema?.type === "object" && f.itemSchema.fields) {
1908
1935
  const children = Object.entries(f.itemSchema.fields).map(([k2, f2]) => ({
@@ -1910,13 +1937,14 @@ function buildNodes(fields, prefix, labels) {
1910
1937
  label: [...labels, name, f2.label ?? k2].join(" \u203A "),
1911
1938
  name: f2.label ?? k2,
1912
1939
  type: f2.type,
1940
+ schema: f2,
1913
1941
  itemOf: name,
1914
1942
  kind: "field",
1915
1943
  children: []
1916
1944
  }));
1917
- return { key, label, name, type: f.type, expr, kind: "list", children };
1945
+ return { key, label, name, type: f.type, schema: f, expr, kind: "list", children };
1918
1946
  }
1919
- return { key, label, name, type: f.type, expr, kind: "field", children: [] };
1947
+ return { key, label, name, type: f.type, schema: f, expr, kind: "field", children: [] };
1920
1948
  });
1921
1949
  }
1922
1950
  function filterNodes(nodes, q) {
@@ -1987,6 +2015,7 @@ function FieldList({ nodes, actions, depth = 0 }) {
1987
2015
  }
1988
2016
  const expr = n.expr;
1989
2017
  const isList = n.kind === "list";
2018
+ const bound = isList ? expr : withDefaultPipe(expr, n.schema);
1990
2019
  return /* @__PURE__ */ jsxs5("div", { children: [
1991
2020
  /* @__PURE__ */ jsxs5(
1992
2021
  "button",
@@ -1994,8 +2023,8 @@ function FieldList({ nodes, actions, depth = 0 }) {
1994
2023
  type: "button",
1995
2024
  className: "bpe-data-field",
1996
2025
  title: n.label + "\n" + expr + "\nClick to insert, or drag onto the page.",
1997
- onPointerDown: (e) => beginDrag(session, e, { kind: "field", payload: { expr, label: n.label, type: n.type }, label: n.name, icon: FIELD_TYPE_ICONS[n.type] }),
1998
- onClick: () => isList ? actions.insertBlock("table", void 0, { source: expr }) : actions.insertBlock("field", void 0, { label: n.name, value: expr }),
2026
+ onPointerDown: (e) => beginDrag(session, e, { kind: "field", payload: { expr, bound, label: n.label, type: n.type, schema: n.schema }, label: n.name, icon: FIELD_TYPE_ICONS[n.type] }),
2027
+ onClick: () => actions.insertField({ expr, bound, label: n.label, type: n.type, schema: n.schema }),
1999
2028
  children: [
2000
2029
  /* @__PURE__ */ jsx6(Icon, { name: FIELD_TYPE_ICONS[n.type] ?? "box", className: "bpe-data-field__icon" }),
2001
2030
  /* @__PURE__ */ jsx6("span", { className: "bpe-truncate", children: n.name }),
@@ -2698,7 +2727,7 @@ import { useEffect as useEffect3, useImperativeHandle, useMemo as useMemo4, useR
2698
2727
 
2699
2728
  // src/inspector/DataPicker.tsx
2700
2729
  import { useMemo as useMemo3, useState as useState7 } from "react";
2701
- import { GLOBAL_SCHEMAS, PIPES, checkExpressionSyntax } from "@broadpaper/core";
2730
+ import { GLOBAL_SCHEMAS, PIPES, checkExpressionSyntax, humanizeKey } from "@broadpaper/core";
2702
2731
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2703
2732
  function flattenSchemas(dataSources, extra = {}, opts = {}) {
2704
2733
  const out = [];
@@ -2721,6 +2750,19 @@ function flattenSchemas(dataSources, extra = {}, opts = {}) {
2721
2750
  if (name === "theme") continue;
2722
2751
  visit(name, schema, [schema.label ?? name], 0, "Document");
2723
2752
  }
2753
+ for (const key of Object.keys(opts.themeMeta ?? {})) {
2754
+ const value = (opts.themeMeta ?? {})[key];
2755
+ if (value !== null && typeof value === "object") continue;
2756
+ out.push({
2757
+ expr: `theme.meta.${key}`,
2758
+ label: humanizeKey(key),
2759
+ path: ["Your brand", humanizeKey(key)],
2760
+ type: "string",
2761
+ schema: { type: "string", label: humanizeKey(key) },
2762
+ depth: 1,
2763
+ group: "Your brand"
2764
+ });
2765
+ }
2724
2766
  }
2725
2767
  return out;
2726
2768
  }
@@ -2738,7 +2780,11 @@ function DataPicker({ dataSources, scopeVars, arraysOnly, types, value, onPick,
2738
2780
  const [q, setQ] = useState7("");
2739
2781
  const [expr, setExpr] = useState7(value ?? "");
2740
2782
  const [pending, setPending] = useState7(null);
2741
- const entries = useMemo3(() => flattenSchemas(dataSources, scopeVars, { arraysOnly, types }), [dataSources, scopeVars, arraysOnly, types]);
2783
+ const themeMeta = useSession().activeTheme.meta;
2784
+ const entries = useMemo3(
2785
+ () => flattenSchemas(dataSources, scopeVars, { arraysOnly, types, ...themeMeta ? { themeMeta } : {} }),
2786
+ [dataSources, scopeVars, arraysOnly, types, themeMeta]
2787
+ );
2742
2788
  const filtered = useMemo3(() => {
2743
2789
  const needle = q.trim().toLowerCase();
2744
2790
  if (!needle) return entries;
@@ -2767,10 +2813,10 @@ function DataPicker({ dataSources, scopeVars, arraysOnly, types, value, onPick,
2767
2813
  /* @__PURE__ */ jsx9(Icon, { name: "wand", className: "bpe-data-field__icon" }),
2768
2814
  " ",
2769
2815
  s.label,
2770
- /* @__PURE__ */ jsxs8("span", { className: "bpe-data-field__type bpe-mono", style: { textTransform: "none" }, children: [
2816
+ allowExpression ? /* @__PURE__ */ jsxs8("span", { className: "bpe-data-field__type bpe-mono", style: { textTransform: "none" }, children: [
2771
2817
  "| ",
2772
2818
  s.pipe
2773
- ] })
2819
+ ] }) : null
2774
2820
  ] }, s.pipe)),
2775
2821
  pending.type !== "array" && /* @__PURE__ */ jsxs8("button", { type: "button", className: "bpe-data-field", onClick: () => onPick(`${pending.expr} | default:"\u2014"`, pending.label, pending), children: [
2776
2822
  /* @__PURE__ */ jsx9(Icon, { name: "eraser", className: "bpe-data-field__icon" }),
@@ -2785,7 +2831,7 @@ function DataPicker({ dataSources, scopeVars, arraysOnly, types, value, onPick,
2785
2831
  groups.length === 0 ? /* @__PURE__ */ jsx9("div", { className: "bpe-empty", children: "No matching fields" }) : null,
2786
2832
  groups.map(([group, items]) => /* @__PURE__ */ jsxs8("div", { children: [
2787
2833
  /* @__PURE__ */ jsx9("div", { className: "bpe-picker__group", children: group }),
2788
- items.map((e) => /* @__PURE__ */ jsxs8("button", { type: "button", role: "option", "aria-selected": value === e.expr, className: "bpe-data-field", style: { paddingLeft: 6 + Math.max(0, e.depth - 1) * 10 }, title: e.expr, onClick: () => arraysOnly || e.type === "array" || e.type === "object" ? onPick(e.expr, e.label, e) : setPending(e), children: [
2834
+ items.map((e) => /* @__PURE__ */ jsxs8("button", { type: "button", role: "option", "aria-selected": value === e.expr, className: "bpe-data-field", style: { paddingLeft: 6 + Math.max(0, e.depth - 1) * 10 }, title: allowExpression ? e.expr : e.label, onClick: () => arraysOnly || e.type === "array" || e.type === "object" ? onPick(e.expr, e.label, e) : setPending(e), children: [
2789
2835
  /* @__PURE__ */ jsx9(Icon, { name: FIELD_TYPE_ICONS[e.type] ?? "box", className: "bpe-data-field__icon" }),
2790
2836
  /* @__PURE__ */ jsx9("span", { className: "bpe-truncate", children: e.path[e.path.length - 1] }),
2791
2837
  /* @__PURE__ */ jsx9("span", { className: "bpe-data-field__type", children: e.type })
@@ -3257,9 +3303,11 @@ function nodeForDrop(drag, ctx) {
3257
3303
  return ctx.cloneLibrary(String(drag.payload));
3258
3304
  case "field": {
3259
3305
  const p = drag.payload;
3260
- const isList = p.type === "array";
3261
- if (isList) return ctx.createNode("table", { source: p.expr });
3262
- return ctx.createNode("field", { label: p.label.split(" \u203A ").pop() ?? p.label, value: p.expr });
3306
+ const type = p.type === "array" ? "table" : "field";
3307
+ const props = ctx.registry.get(type)?.fromField?.({ expr: p.expr, formatted: p.bound ?? p.expr, schema: p.schema ?? { type: p.type ?? "string" }, label: p.label });
3308
+ if (props) return ctx.createNode(type, props);
3309
+ if (type === "table") return ctx.createNode("table", { source: p.expr });
3310
+ return ctx.createNode("field", { label: p.label.split(" \u203A ").pop() ?? p.label, value: p.bound ?? p.expr });
3263
3311
  }
3264
3312
  default:
3265
3313
  return void 0;
@@ -3629,18 +3677,19 @@ function Canvas({ actions }) {
3629
3677
  drop: (target, d) => {
3630
3678
  if (target.textTargetId && d.kind === "field") {
3631
3679
  const p = d.payload;
3680
+ const expr = p.bound ?? p.expr;
3632
3681
  const node2 = session.store.getNode(target.textTargetId);
3633
3682
  if (!node2) return;
3634
3683
  if (node2.type === "heading") {
3635
3684
  const text = String(node2.props.text ?? "");
3636
- session.store.dispatch({ type: "setProp", id: node2.id, path: "text", value: `${text}${text && !text.endsWith(" ") ? " " : ""}{{ ${p.expr} }}` }, { label: "Insert field" });
3685
+ session.store.dispatch({ type: "setProp", id: node2.id, path: "text", value: `${text}${text && !text.endsWith(" ") ? " " : ""}{{ ${expr} }}` }, { label: "Insert field" });
3637
3686
  } else {
3638
3687
  const doc = isRichTextDoc2(node2.props.content) ? structuredClone(node2.props.content) : richTextFromTemplate2("");
3639
3688
  const last = doc.content[doc.content.length - 1];
3640
3689
  if (last && last.type === "paragraph") {
3641
3690
  if (last.content.length) last.content.push({ type: "text", text: " " });
3642
- last.content.push({ type: "token", expr: p.expr });
3643
- } else doc.content.push({ type: "paragraph", content: [{ type: "token", expr: p.expr }] });
3691
+ last.content.push({ type: "token", expr });
3692
+ } else doc.content.push({ type: "paragraph", content: [{ type: "token", expr }] });
3644
3693
  session.store.dispatch({ type: "setProp", id: node2.id, path: "content", value: doc }, { label: "Insert field" });
3645
3694
  }
3646
3695
  session.store.select([node2.id]);
@@ -3735,75 +3784,385 @@ function Canvas({ actions }) {
3735
3784
  }
3736
3785
 
3737
3786
  // src/inspector/NodeInspector.tsx
3738
- import { useMemo as useMemo7 } from "react";
3739
- import { ROW_LAYOUTS as ROW_LAYOUTS2, itemSchemaOf, createSchemaScope as createSchemaScope5, ancestors as ancestors2 } from "@broadpaper/core";
3787
+ import { useMemo as useMemo8 } from "react";
3788
+ import { ROW_LAYOUTS as ROW_LAYOUTS2, itemSchemaOf, createSchemaScope as createSchemaScope7, ancestors as ancestors2 } from "@broadpaper/core";
3740
3789
 
3741
3790
  // src/inspector/fields/FieldRenderer.tsx
3742
- import { useEffect as useEffect6, useRef as useRef7, useState as useState15 } from "react";
3743
- import { checkExpressionSyntax as checkExpressionSyntax3, createSchemaScope as createSchemaScope4, textStyleLabel, richTextToPlain as richTextToPlain2, richTextFromTemplate as richTextFromTemplate3, isRichTextDoc as isRichTextDoc3 } from "@broadpaper/core";
3791
+ import { useEffect as useEffect7, useRef as useRef8, useState as useState17 } from "react";
3792
+ import { checkExpressionSyntax as checkExpressionSyntax3, createSchemaScope as createSchemaScope6, textStyleLabel, richTextToPlain as richTextToPlain2, richTextFromTemplate as richTextFromTemplate3, isRichTextDoc as isRichTextDoc3 } from "@broadpaper/core";
3744
3793
 
3745
3794
  // src/inspector/fields/BindingField.tsx
3746
- import { useMemo as useMemo6 } from "react";
3747
- import { createSchemaScope as createSchemaScope3, typeCheck, parseExpression as parseExpression2, labelForPath as labelForPath2 } from "@broadpaper/core";
3795
+ import { useMemo as useMemo7, useState as useState13 } from "react";
3796
+ import {
3797
+ createSchemaScope as createSchemaScope4,
3798
+ describeExpression,
3799
+ fromExpression as fromExpression2,
3800
+ humaniseLabel as humaniseLabel2,
3801
+ parseExpression as parseExpression2,
3802
+ typeCheck
3803
+ } from "@broadpaper/core";
3804
+
3805
+ // src/inspector/fields/CalculationBuilder.tsx
3806
+ import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef6, useState as useState12 } from "react";
3807
+ import {
3808
+ createSchemaScope as createSchemaScope3,
3809
+ fromExpression,
3810
+ labelForPath as labelForPath2,
3811
+ toExpression
3812
+ } from "@broadpaper/core";
3748
3813
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
3814
+ var NUMERIC = ["number", "currency", "percent"];
3815
+ var OPS = [
3816
+ { value: "add", label: "add" },
3817
+ { value: "subtract", label: "subtract" },
3818
+ { value: "multiply", label: "multiply by" },
3819
+ { value: "divide", label: "divide by" },
3820
+ { value: "addPercent", label: "add, as a percentage" },
3821
+ { value: "subtractPercent", label: "subtract, as a percentage" }
3822
+ ];
3823
+ var AGGREGATES = [
3824
+ { value: "sum", label: "Total of", of: "added up across every row" },
3825
+ { value: "avg", label: "Average of", of: "the mean across every row" },
3826
+ { value: "count", label: "How many", of: "how many rows there are" },
3827
+ { value: "max", label: "Highest", of: "the largest in any row" },
3828
+ { value: "min", label: "Lowest", of: "the smallest in any row" }
3829
+ ];
3830
+ var EMPTY = { start: { kind: "field", path: "" }, steps: [] };
3831
+ function complete(calc) {
3832
+ const ok = (term) => term.kind === "number" || term.path.trim() !== "";
3833
+ return ok(calc.start) && calc.steps.every((s) => ok(s.term));
3834
+ }
3835
+ function CalculationBuilder({ value, onChange, dataSources, scopeVars, formats = [] }) {
3836
+ const entries = useMemo6(() => flattenSchemas(dataSources, scopeVars), [dataSources, scopeVars]);
3837
+ const scope = useMemo6(() => createSchemaScope3(dataSources).child(scopeVars ?? {}), [dataSources, scopeVars]);
3838
+ const [calc, setCalc] = useState12(() => fromExpression(value) ?? EMPTY);
3839
+ const mine = useRef6(null);
3840
+ useEffect6(() => {
3841
+ if (value !== mine.current) setCalc(fromExpression(value) ?? EMPTY);
3842
+ }, [value]);
3843
+ const commit = (next) => {
3844
+ setCalc(next);
3845
+ if (!complete(next)) return;
3846
+ const expr = toExpression(next);
3847
+ mine.current = expr;
3848
+ onChange(expr);
3849
+ };
3850
+ const naming = (term) => termLabel(term, entries, scope);
3851
+ return /* @__PURE__ */ jsxs15("div", { className: "bpe-calc", children: [
3852
+ /* @__PURE__ */ jsxs15("div", { className: "bpe-calc__step", children: [
3853
+ /* @__PURE__ */ jsx16("span", { className: "bpe-calc__lead", children: "Start with" }),
3854
+ /* @__PURE__ */ jsx16(TermField, { term: calc.start, entries, scope, onChange: (start) => commit({ ...calc, start }) })
3855
+ ] }),
3856
+ calc.steps.map((step, i) => /* @__PURE__ */ jsxs15("div", { className: "bpe-calc__step", children: [
3857
+ /* @__PURE__ */ jsxs15("div", { className: "bpe-calc__row", children: [
3858
+ /* @__PURE__ */ jsx16("span", { className: "bpe-calc__lead", children: "then" }),
3859
+ /* @__PURE__ */ jsx16("div", { className: "bpe-grow" }),
3860
+ /* @__PURE__ */ jsx16(
3861
+ Button,
3862
+ {
3863
+ size: "sm",
3864
+ variant: "ghost",
3865
+ icon: "x",
3866
+ tooltip: "Remove this step",
3867
+ onClick: () => commit({ ...calc, steps: calc.steps.filter((_, j) => j !== i) })
3868
+ }
3869
+ )
3870
+ ] }),
3871
+ /* @__PURE__ */ jsx16(
3872
+ Select,
3873
+ {
3874
+ value: step.op,
3875
+ onChange: (op) => commit({ ...calc, steps: calc.steps.map((s, j) => i === j ? { ...s, op } : s) }),
3876
+ options: OPS
3877
+ }
3878
+ ),
3879
+ /* @__PURE__ */ jsx16(
3880
+ TermField,
3881
+ {
3882
+ term: step.term,
3883
+ entries,
3884
+ scope,
3885
+ onChange: (term) => commit({ ...calc, steps: calc.steps.map((s, j) => i === j ? { ...s, term } : s) })
3886
+ }
3887
+ )
3888
+ ] }, i)),
3889
+ /* @__PURE__ */ jsx16("div", { className: "bpe-calc__row", children: /* @__PURE__ */ jsx16(
3890
+ Button,
3891
+ {
3892
+ size: "sm",
3893
+ icon: "plus",
3894
+ onClick: () => commit({ ...calc, steps: [...calc.steps, { op: "add", term: { kind: "field", path: "" } }] }),
3895
+ children: "Add a step"
3896
+ }
3897
+ ) }),
3898
+ formats.length ? /* @__PURE__ */ jsxs15("div", { className: "bpe-calc__row", children: [
3899
+ /* @__PURE__ */ jsx16("span", { className: "bpe-calc__lead", children: "Shown as" }),
3900
+ /* @__PURE__ */ jsx16(
3901
+ Select,
3902
+ {
3903
+ value: calc.format ?? "",
3904
+ onChange: (pipe) => commit({ ...calc, ...pipe ? { format: pipe } : { format: void 0 } }),
3905
+ options: [{ value: "", label: "As it comes" }, ...formats.map((f) => ({ value: f.pipe, label: f.label }))]
3906
+ }
3907
+ )
3908
+ ] }) : null,
3909
+ /* @__PURE__ */ jsxs15("p", { className: "bpe-calc__reads", children: [
3910
+ naming(calc.start),
3911
+ calc.steps.map((step, i) => /* @__PURE__ */ jsxs15("span", { children: [
3912
+ ", ",
3913
+ OPS.find((o) => o.value === step.op)?.label,
3914
+ " ",
3915
+ naming(step.term)
3916
+ ] }, i))
3917
+ ] })
3918
+ ] });
3919
+ }
3920
+ function TermField({
3921
+ term,
3922
+ entries,
3923
+ scope,
3924
+ onChange
3925
+ }) {
3926
+ return /* @__PURE__ */ jsxs15("div", { className: "bpe-calc__term", children: [
3927
+ /* @__PURE__ */ jsx16(
3928
+ Select,
3929
+ {
3930
+ value: term.kind,
3931
+ onChange: (kind) => onChange(blankTerm(kind, term)),
3932
+ options: [
3933
+ { value: "field", label: "a field" },
3934
+ { value: "aggregate", label: "a total" },
3935
+ { value: "number", label: "a number" }
3936
+ ]
3937
+ }
3938
+ ),
3939
+ term.kind === "number" ? /* @__PURE__ */ jsx16(NumberInput, { value: term.value, onChange: (v) => onChange({ kind: "number", value: v ?? 0 }), placeholder: "0" }) : null,
3940
+ term.kind === "aggregate" ? /* @__PURE__ */ jsx16(
3941
+ Select,
3942
+ {
3943
+ value: term.fn,
3944
+ onChange: (fn) => onChange({ ...term, fn }),
3945
+ options: AGGREGATES.map((a) => ({ value: a.value, label: a.label }))
3946
+ }
3947
+ ) : null,
3948
+ term.kind !== "number" ? /* @__PURE__ */ jsx16(
3949
+ Popover,
3950
+ {
3951
+ className: "bpe-picker",
3952
+ trigger: /* @__PURE__ */ jsxs15("button", { type: "button", className: `bpe-binding__btn ${term.path ? "" : "bpe-binding__btn--empty"}`, children: [
3953
+ /* @__PURE__ */ jsx16(Icon, { name: term.kind === "aggregate" ? "sigma" : "braces" }),
3954
+ /* @__PURE__ */ jsx16("span", { className: "bpe-truncate", children: term.path ? pathLabel(term.path, scope) : term.kind === "aggregate" ? "Choose a column" : "Choose a field" })
3955
+ ] }),
3956
+ children: (close) => /* @__PURE__ */ jsx16(
3957
+ TermPicker,
3958
+ {
3959
+ entries,
3960
+ wants: term.kind,
3961
+ numericOnly: term.kind === "aggregate" ? term.fn !== "count" : true,
3962
+ onPick: (path) => {
3963
+ onChange({ ...term, path });
3964
+ close();
3965
+ }
3966
+ }
3967
+ )
3968
+ }
3969
+ ) : null
3970
+ ] });
3971
+ }
3972
+ function blankTerm(kind, from) {
3973
+ const path = from.kind === "number" ? "" : from.path;
3974
+ if (kind === "number") return { kind: "number", value: 0 };
3975
+ if (kind === "aggregate") return { kind: "aggregate", fn: "sum", path };
3976
+ return { kind: "field", path };
3977
+ }
3978
+ function termOptions(entries, wants, numericOnly) {
3979
+ return entries.filter((e) => {
3980
+ if (wants === "aggregate") {
3981
+ if (e.type !== "array") return false;
3982
+ const item = e.schema.itemSchema;
3983
+ return numericOnly ? Boolean(item && NUMERIC.includes(item.type)) : true;
3984
+ }
3985
+ return numericOnly ? NUMERIC.includes(e.type) : e.type !== "array" && e.type !== "object";
3986
+ });
3987
+ }
3988
+ function TermPicker({
3989
+ entries,
3990
+ wants,
3991
+ numericOnly,
3992
+ onPick
3993
+ }) {
3994
+ const shown = termOptions(entries, wants, numericOnly);
3995
+ const groups = /* @__PURE__ */ new Map();
3996
+ for (const e of shown) groups.set(e.group, [...groups.get(e.group) ?? [], e]);
3997
+ return /* @__PURE__ */ jsx16("div", { children: /* @__PURE__ */ jsxs15("div", { className: "bpe-picker__list", role: "listbox", children: [
3998
+ !shown.length ? /* @__PURE__ */ jsx16("div", { className: "bpe-empty", children: wants === "aggregate" ? "No lists of numbers in your data to total up." : "No numbers in your data to use here." }) : null,
3999
+ [...groups].map(([group, items]) => /* @__PURE__ */ jsxs15("div", { children: [
4000
+ /* @__PURE__ */ jsx16("div", { className: "bpe-picker__group", children: group }),
4001
+ items.map((e) => /* @__PURE__ */ jsxs15("button", { type: "button", role: "option", className: "bpe-data-field", onClick: () => onPick(e.expr), children: [
4002
+ /* @__PURE__ */ jsx16(Icon, { name: wants === "aggregate" ? "list-tree" : "hash", className: "bpe-data-field__icon" }),
4003
+ /* @__PURE__ */ jsx16("span", { className: "bpe-truncate", children: e.label })
4004
+ ] }, e.expr))
4005
+ ] }, group))
4006
+ ] }) });
4007
+ }
4008
+ function pathLabel(path, scope) {
4009
+ return labelForPath2(
4010
+ path.split(".").map((name) => ({ kind: "prop", name })),
4011
+ scope
4012
+ );
4013
+ }
4014
+ function termLabel(term, entries, scope) {
4015
+ if (term.kind === "number") return String(term.value);
4016
+ if (!term.path) return "\u2026";
4017
+ const named = entries.find((e) => e.expr === term.path)?.label ?? pathLabel(term.path, scope);
4018
+ if (term.kind === "field") return named;
4019
+ return `${AGGREGATES.find((a) => a.value === term.fn)?.label ?? "Total of"} ${named}`;
4020
+ }
4021
+ function canCalculate(types) {
4022
+ return !types || types.some((t) => NUMERIC.includes(t));
4023
+ }
4024
+
4025
+ // src/inspector/fields/BindingField.tsx
4026
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
3749
4027
  function useExpressionLabel(dataSources, scopeVars) {
3750
- return useMemo6(() => {
3751
- const scope = createSchemaScope3(dataSources).child(scopeVars ?? {});
4028
+ return useMemo7(() => {
4029
+ const scope = createSchemaScope4(dataSources).child(scopeVars ?? {});
3752
4030
  return (expr) => {
3753
- if (!expr.trim()) return { label: "", error: null };
3754
- const r = typeCheck(expr, scope);
3755
- const error = r.issues[0]?.message ?? null;
4031
+ if (!expr.trim()) return { label: "", error: null, computed: false };
4032
+ const error = typeCheck(expr, scope).issues[0]?.message ?? null;
4033
+ let computed = true;
3756
4034
  try {
3757
4035
  const ast = parseExpression2(expr);
3758
- if (ast.kind === "path") return { label: labelForPath2(ast.segments, scope), error };
3759
- if (ast.kind === "pipe" && ast.input.kind === "path") return { label: `${labelForPath2(ast.input.segments, scope)} \xB7 ${ast.name}`, error };
4036
+ computed = !(ast.kind === "path" || ast.kind === "pipe" && ast.input.kind === "path");
3760
4037
  } catch {
4038
+ computed = false;
3761
4039
  }
3762
- return { label: expr, error };
4040
+ return { label: describeExpression(expr, scope).label, error, computed };
3763
4041
  };
3764
4042
  }, [dataSources, scopeVars]);
3765
4043
  }
3766
- function BindingField({ value, onChange, dataSources, scopeVars, arraysOnly, types, placeholder, allowExpression = true }) {
4044
+ function useTokenText(dataSources, scopeVars) {
4045
+ return useMemo7(() => {
4046
+ const scope = createSchemaScope4(dataSources).child(scopeVars ?? {});
4047
+ return (text) => humaniseLabel2(text, scope);
4048
+ }, [dataSources, scopeVars]);
4049
+ }
4050
+ function BindingField({ value, onChange, dataSources, scopeVars, arraysOnly, types, placeholder, allowExpression = true, allowCalculation = true }) {
3767
4051
  const describe = useExpressionLabel(dataSources, scopeVars);
3768
- const { label, error } = describe(value);
3769
- return /* @__PURE__ */ jsxs15("div", { className: "bpe-binding", children: [
3770
- /* @__PURE__ */ jsx16(
4052
+ const { label, error, computed } = describe(value);
4053
+ const editable = allowCalculation && !arraysOnly && canCalculate(types) && (value.trim() === "" || fromExpression2(value) !== null);
4054
+ if (value && computed && !allowExpression && !editable) {
4055
+ return /* @__PURE__ */ jsx17("div", { className: "bpe-binding", children: /* @__PURE__ */ jsxs16("span", { className: "bpe-binding__btn bpe-binding__btn--locked", title: `Worked out for you, from ${label.replace(/^ƒ\s*/, "")}. Change those and this changes with them.`, children: [
4056
+ /* @__PURE__ */ jsx17(Icon, { name: "sigma" }),
4057
+ /* @__PURE__ */ jsx17("span", { className: "bpe-truncate", children: label.replace(/^ƒ\s*/, "") || "Worked out for you" })
4058
+ ] }) });
4059
+ }
4060
+ return /* @__PURE__ */ jsxs16("div", { className: "bpe-binding", children: [
4061
+ /* @__PURE__ */ jsx17(
3771
4062
  Popover,
3772
4063
  {
3773
- className: "bpe-picker",
3774
- trigger: /* @__PURE__ */ jsxs15("button", { type: "button", className: `bpe-binding__btn ${value ? "" : "bpe-binding__btn--empty"} ${error ? "bpe-input--invalid" : ""}`, title: error ?? value, children: [
3775
- /* @__PURE__ */ jsx16(Icon, { name: error ? "alert-triangle" : arraysOnly ? "list-tree" : "braces", style: error ? { color: "var(--bpe-danger)" } : void 0 }),
3776
- /* @__PURE__ */ jsx16("span", { className: "bpe-truncate", children: value ? label : placeholder ?? (arraysOnly ? "Choose a list" : "Choose a field") })
3777
- ] }),
3778
- children: (close) => /* @__PURE__ */ jsx16(
3779
- DataPicker,
4064
+ className: `bpe-picker${editable ? " bpe-picker--wide" : ""}`,
4065
+ trigger: /* @__PURE__ */ jsxs16(
4066
+ "button",
3780
4067
  {
4068
+ type: "button",
4069
+ className: `bpe-binding__btn ${value ? "" : "bpe-binding__btn--empty"} ${error ? "bpe-input--invalid" : ""}`,
4070
+ title: error ?? (allowExpression ? value : label),
4071
+ children: [
4072
+ /* @__PURE__ */ jsx17(Icon, { name: error ? "alert-triangle" : computed ? "sigma" : arraysOnly ? "list-tree" : "braces", style: error ? { color: "var(--bpe-danger)" } : void 0 }),
4073
+ /* @__PURE__ */ jsx17("span", { className: "bpe-truncate", children: value ? label.replace(/^ƒ\s*/, "") : placeholder ?? (arraysOnly ? "Choose a list" : "Choose a field") })
4074
+ ]
4075
+ }
4076
+ ),
4077
+ children: (close) => /* @__PURE__ */ jsx17(
4078
+ ValuePanel,
4079
+ {
4080
+ value,
4081
+ onChange,
3781
4082
  dataSources,
3782
4083
  scopeVars,
3783
4084
  arraysOnly,
3784
4085
  types,
3785
- value,
3786
4086
  allowExpression,
3787
- onPick: (expr) => {
3788
- onChange(expr);
3789
- close();
3790
- },
3791
- onClear: () => {
3792
- onChange("");
3793
- close();
3794
- }
4087
+ offerCalculation: editable,
4088
+ startOnCalculation: computed && editable,
4089
+ close
3795
4090
  }
3796
4091
  )
3797
4092
  }
3798
4093
  ),
3799
- value ? /* @__PURE__ */ jsx16(Button, { size: "sm", variant: "ghost", icon: "x", tooltip: "Clear", onClick: () => onChange("") }) : null
4094
+ value ? /* @__PURE__ */ jsx17(Button, { size: "sm", variant: "ghost", icon: "x", tooltip: "Clear", onClick: () => onChange("") }) : null
4095
+ ] });
4096
+ }
4097
+ function ValuePanel({
4098
+ value,
4099
+ onChange,
4100
+ dataSources,
4101
+ scopeVars,
4102
+ arraysOnly,
4103
+ types,
4104
+ allowExpression,
4105
+ offerCalculation,
4106
+ startOnCalculation,
4107
+ close
4108
+ }) {
4109
+ const [tab, setTab] = useState13(startOnCalculation ? "calc" : "field");
4110
+ const formats = useMemo7(() => {
4111
+ const wanted = types?.length ? types : ["currency", "number", "percent"];
4112
+ const seen = /* @__PURE__ */ new Set();
4113
+ return wanted.flatMap((t) => FORMAT_SUGGESTIONS[t] ?? []).filter((f) => !seen.has(f.pipe) && seen.add(f.pipe));
4114
+ }, [types]);
4115
+ if (!offerCalculation) {
4116
+ return /* @__PURE__ */ jsx17(
4117
+ DataPicker,
4118
+ {
4119
+ dataSources,
4120
+ scopeVars,
4121
+ arraysOnly,
4122
+ types,
4123
+ value,
4124
+ allowExpression,
4125
+ onPick: (expr) => {
4126
+ onChange(expr);
4127
+ close();
4128
+ },
4129
+ onClear: () => {
4130
+ onChange("");
4131
+ close();
4132
+ }
4133
+ }
4134
+ );
4135
+ }
4136
+ return /* @__PURE__ */ jsxs16("div", { children: [
4137
+ /* @__PURE__ */ jsxs16("div", { className: "bpe-picker__tabs", role: "tablist", children: [
4138
+ /* @__PURE__ */ jsx17("button", { type: "button", role: "tab", "aria-selected": tab === "field", className: "bpe-tab", onClick: () => setTab("field"), children: "A field" }),
4139
+ /* @__PURE__ */ jsx17("button", { type: "button", role: "tab", "aria-selected": tab === "calc", className: "bpe-tab", onClick: () => setTab("calc"), children: "Worked out" })
4140
+ ] }),
4141
+ tab === "field" ? /* @__PURE__ */ jsx17(
4142
+ DataPicker,
4143
+ {
4144
+ dataSources,
4145
+ scopeVars,
4146
+ types,
4147
+ value,
4148
+ allowExpression,
4149
+ onPick: (expr) => {
4150
+ onChange(expr);
4151
+ close();
4152
+ },
4153
+ onClear: () => {
4154
+ onChange("");
4155
+ close();
4156
+ }
4157
+ }
4158
+ ) : /* @__PURE__ */ jsx17(CalculationBuilder, { value, onChange, dataSources, scopeVars, formats })
3800
4159
  ] });
3801
4160
  }
3802
4161
 
3803
4162
  // src/inspector/fields/ImageField.tsx
3804
- import { useRef as useRef6, useState as useState12 } from "react";
4163
+ import { useRef as useRef7, useState as useState14 } from "react";
3805
4164
  import { isSafeUrl } from "@broadpaper/core";
3806
- import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
4165
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
3807
4166
  var MAX_INLINE_IMAGE_BYTES = 1e6;
3808
4167
  var ACCEPT = "image/png,image/jpeg,image/gif,image/webp,image/svg+xml";
3809
4168
  var BINDABLE = ["image", "url", "string"];
@@ -3827,8 +4186,8 @@ function ImageField({
3827
4186
  onError
3828
4187
  }) {
3829
4188
  const options = useOptions();
3830
- const fileRef = useRef6(null);
3831
- const [busy, setBusy] = useState12(false);
4189
+ const fileRef = useRef7(null);
4190
+ const [busy, setBusy] = useState14(false);
3832
4191
  const bound = /^\{\{[\s\S]*\}\}$/.test(value.trim());
3833
4192
  async function accept(file) {
3834
4193
  onError(null);
@@ -3859,8 +4218,8 @@ function ImageField({
3859
4218
  if (fileRef.current) fileRef.current.value = "";
3860
4219
  }
3861
4220
  }
3862
- return /* @__PURE__ */ jsxs16("div", { className: "bpe-binding bpe-image", children: [
3863
- bound ? /* @__PURE__ */ jsx17(
4221
+ return /* @__PURE__ */ jsxs17("div", { className: "bpe-binding bpe-image", children: [
4222
+ bound ? /* @__PURE__ */ jsx18(
3864
4223
  BindingField,
3865
4224
  {
3866
4225
  value: value.replace(/^\{\{\s*|\s*\}\}$/g, ""),
@@ -3869,8 +4228,8 @@ function ImageField({
3869
4228
  scopeVars,
3870
4229
  types: BINDABLE
3871
4230
  }
3872
- ) : /* @__PURE__ */ jsx17(TextInput, { value, onChange, lazy: true, placeholder: "https://\u2026 or upload" }),
3873
- /* @__PURE__ */ jsx17(
4231
+ ) : /* @__PURE__ */ jsx18(TextInput, { value, onChange, lazy: true, placeholder: "https://\u2026 or upload" }),
4232
+ /* @__PURE__ */ jsx18(
3874
4233
  "input",
3875
4234
  {
3876
4235
  ref: fileRef,
@@ -3885,7 +4244,7 @@ function ImageField({
3885
4244
  }
3886
4245
  }
3887
4246
  ),
3888
- !bound ? /* @__PURE__ */ jsx17(
4247
+ !bound ? /* @__PURE__ */ jsx18(
3889
4248
  Button,
3890
4249
  {
3891
4250
  size: "sm",
@@ -3896,7 +4255,7 @@ function ImageField({
3896
4255
  onClick: () => fileRef.current?.click()
3897
4256
  }
3898
4257
  ) : null,
3899
- allowBinding ? /* @__PURE__ */ jsx17(Popover, { className: "bpe-picker", trigger: /* @__PURE__ */ jsx17(Button, { size: "sm", variant: "ghost", icon: "braces", tooltip: "Bind to a field" }), children: (close) => /* @__PURE__ */ jsx17(
4258
+ allowBinding ? /* @__PURE__ */ jsx18(Popover, { className: "bpe-picker", trigger: /* @__PURE__ */ jsx18(Button, { size: "sm", variant: "ghost", icon: "braces", tooltip: "Bind to a field" }), children: (close) => /* @__PURE__ */ jsx18(
3900
4259
  DataPicker,
3901
4260
  {
3902
4261
  dataSources,
@@ -3912,64 +4271,64 @@ function ImageField({
3912
4271
  }
3913
4272
 
3914
4273
  // src/inspector/fields/ConditionBuilder.tsx
3915
- import { CONDITION_OPERATORS, compileCondition, checkExpressionSyntax as checkExpressionSyntax2 } from "@broadpaper/core";
3916
- import { useState as useState13 } from "react";
3917
- import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
4274
+ import { CONDITION_OPERATORS, compileCondition, checkExpressionSyntax as checkExpressionSyntax2, createSchemaScope as createSchemaScope5, humaniseLabel as humaniseLabel3 } from "@broadpaper/core";
4275
+ import { useState as useState15 } from "react";
4276
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
3918
4277
  function isGroup(v) {
3919
4278
  return !!v && typeof v === "object" && "op" in v;
3920
4279
  }
3921
4280
  function ConditionBuilder({ value, onChange, dataSources, scopeVars, allowExpression = true }) {
3922
- const [advanced, setAdvanced] = useState13(typeof value === "string" && value.trim() !== "");
4281
+ const [advanced, setAdvanced] = useState15(typeof value === "string" && value.trim() !== "");
3923
4282
  const group = isGroup(value) ? value : { op: "and", rules: [] };
3924
4283
  const compiled = compileCondition(value);
3925
4284
  if (advanced) {
3926
4285
  const src = typeof value === "string" ? value : compiled ?? "";
3927
4286
  const err = src.trim() ? checkExpressionSyntax2(src) : null;
3928
- return /* @__PURE__ */ jsxs17("div", { className: "bpe-cond", children: [
3929
- /* @__PURE__ */ jsx18(TextInput, { value: src, onChange: (v) => onChange(v.trim() ? v : void 0), mono: true, lazy: true, placeholder: "e.g. client.riskScore > 7 and client.isRetired", invalid: !!err }),
3930
- err ? /* @__PURE__ */ jsx18("div", { className: "bpe-field__error", children: err.message }) : null,
3931
- /* @__PURE__ */ jsxs17("div", { style: { display: "flex", gap: 6 }, children: [
3932
- /* @__PURE__ */ jsx18(Button, { size: "sm", onClick: () => {
4287
+ return /* @__PURE__ */ jsxs18("div", { className: "bpe-cond", children: [
4288
+ /* @__PURE__ */ jsx19(TextInput, { value: src, onChange: (v) => onChange(v.trim() ? v : void 0), mono: true, lazy: true, placeholder: "e.g. client.riskScore > 7 and client.isRetired", invalid: !!err }),
4289
+ err ? /* @__PURE__ */ jsx19("div", { className: "bpe-field__error", children: err.message }) : null,
4290
+ /* @__PURE__ */ jsxs18("div", { style: { display: "flex", gap: 6 }, children: [
4291
+ /* @__PURE__ */ jsx19(Button, { size: "sm", onClick: () => {
3933
4292
  setAdvanced(false);
3934
4293
  onChange({ op: "and", rules: [] });
3935
4294
  }, children: "Use rule builder" }),
3936
- value ? /* @__PURE__ */ jsx18(Button, { size: "sm", variant: "ghost", icon: "x", onClick: () => onChange(void 0), children: "Clear" }) : null
4295
+ value ? /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", icon: "x", onClick: () => onChange(void 0), children: "Clear" }) : null
3937
4296
  ] })
3938
4297
  ] });
3939
4298
  }
3940
4299
  const update = (g) => onChange(g.rules.length ? g : void 0);
3941
- return /* @__PURE__ */ jsxs17("div", { className: "bpe-cond", children: [
3942
- /* @__PURE__ */ jsx18(GroupEditor, { group, onChange: update, dataSources, scopeVars, depth: 0 }),
3943
- /* @__PURE__ */ jsxs17("div", { style: { display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }, children: [
3944
- /* @__PURE__ */ jsx18(Button, { size: "sm", icon: "plus", onClick: () => update({ ...group, rules: [...group.rules, { left: "", operator: "eq", right: "" }] }), children: "Rule" }),
3945
- /* @__PURE__ */ jsx18(Button, { size: "sm", icon: "plus", onClick: () => update({ ...group, rules: [...group.rules, { op: "or", rules: [{ left: "", operator: "eq", right: "" }] }] }), children: "Group" }),
3946
- allowExpression ? /* @__PURE__ */ jsx18(Button, { size: "sm", variant: "ghost", icon: "braces", onClick: () => setAdvanced(true), children: "Expression" }) : null
4300
+ return /* @__PURE__ */ jsxs18("div", { className: "bpe-cond", children: [
4301
+ /* @__PURE__ */ jsx19(GroupEditor, { group, onChange: update, dataSources, scopeVars, depth: 0 }),
4302
+ /* @__PURE__ */ jsxs18("div", { style: { display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }, children: [
4303
+ /* @__PURE__ */ jsx19(Button, { size: "sm", icon: "plus", onClick: () => update({ ...group, rules: [...group.rules, { left: "", operator: "eq", right: "" }] }), children: "Rule" }),
4304
+ /* @__PURE__ */ jsx19(Button, { size: "sm", icon: "plus", onClick: () => update({ ...group, rules: [...group.rules, { op: "or", rules: [{ left: "", operator: "eq", right: "" }] }] }), children: "Group" }),
4305
+ allowExpression ? /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", icon: "braces", onClick: () => setAdvanced(true), children: "Expression" }) : null
3947
4306
  ] }),
3948
- compiled ? /* @__PURE__ */ jsx18("div", { className: "bpe-field__hint bpe-mono", title: compiled, children: compiled }) : /* @__PURE__ */ jsx18("div", { className: "bpe-field__hint", children: "Always shown" })
4307
+ compiled ? allowExpression ? /* @__PURE__ */ jsx19("div", { className: "bpe-field__hint bpe-mono", title: compiled, children: compiled }) : /* @__PURE__ */ jsx19("div", { className: "bpe-field__hint", children: humaniseLabel3(compiled, createSchemaScope5(dataSources).child(scopeVars ?? {})) }) : /* @__PURE__ */ jsx19("div", { className: "bpe-field__hint", children: "Always shown" })
3949
4308
  ] });
3950
4309
  }
3951
4310
  function GroupEditor({ group, onChange, dataSources, scopeVars, depth }) {
3952
4311
  const setRule = (i, r) => onChange({ ...group, rules: group.rules.map((x, j) => j === i ? r : x) });
3953
4312
  const remove = (i) => onChange({ ...group, rules: group.rules.filter((_, j) => j !== i) });
3954
- return /* @__PURE__ */ jsxs17("div", { className: depth > 0 ? "bpe-cond__group" : void 0, children: [
3955
- group.rules.length > 1 ? /* @__PURE__ */ jsxs17("div", { className: "bpe-cond__op", style: { marginBottom: 6 }, children: [
3956
- /* @__PURE__ */ jsx18("span", { className: "bpe-muted", style: { fontSize: "var(--bpe-text-xs)" }, children: "Match" }),
3957
- /* @__PURE__ */ jsx18(Segmented, { value: group.op, onChange: (op) => onChange({ ...group, op }), options: [{ value: "and", label: "All (AND)" }, { value: "or", label: "Any (OR)" }], className: "bpe-mode" })
4313
+ return /* @__PURE__ */ jsxs18("div", { className: depth > 0 ? "bpe-cond__group" : void 0, children: [
4314
+ group.rules.length > 1 ? /* @__PURE__ */ jsxs18("div", { className: "bpe-cond__op", style: { marginBottom: 6 }, children: [
4315
+ /* @__PURE__ */ jsx19("span", { className: "bpe-muted", style: { fontSize: "var(--bpe-text-xs)" }, children: "Match" }),
4316
+ /* @__PURE__ */ jsx19(Segmented, { value: group.op, onChange: (op) => onChange({ ...group, op }), options: [{ value: "and", label: "All (AND)" }, { value: "or", label: "Any (OR)" }], className: "bpe-mode" })
3958
4317
  ] }) : null,
3959
- group.rules.map((r, i) => /* @__PURE__ */ jsxs17("div", { className: "bpe-cond__rule", style: { marginBottom: 6 }, children: [
3960
- "op" in r ? /* @__PURE__ */ jsx18(GroupEditor, { group: r, onChange: (g) => setRule(i, g), dataSources, scopeVars, depth: depth + 1 }) : /* @__PURE__ */ jsx18(RuleEditor, { rule: r, onChange: (x) => setRule(i, x), dataSources, scopeVars }),
3961
- /* @__PURE__ */ jsx18(Button, { size: "sm", variant: "ghost", icon: "x", tooltip: "Remove", onClick: () => remove(i) })
4318
+ group.rules.map((r, i) => /* @__PURE__ */ jsxs18("div", { className: "bpe-cond__rule", style: { marginBottom: 6 }, children: [
4319
+ "op" in r ? /* @__PURE__ */ jsx19(GroupEditor, { group: r, onChange: (g) => setRule(i, g), dataSources, scopeVars, depth: depth + 1 }) : /* @__PURE__ */ jsx19(RuleEditor, { rule: r, onChange: (x) => setRule(i, x), dataSources, scopeVars }),
4320
+ /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", icon: "x", tooltip: "Remove", onClick: () => remove(i) })
3962
4321
  ] }, i)),
3963
- depth > 0 ? /* @__PURE__ */ jsx18(Button, { size: "sm", icon: "plus", onClick: () => onChange({ ...group, rules: [...group.rules, { left: "", operator: "eq", right: "" }] }), children: "Rule" }) : null
4322
+ depth > 0 ? /* @__PURE__ */ jsx19(Button, { size: "sm", icon: "plus", onClick: () => onChange({ ...group, rules: [...group.rules, { left: "", operator: "eq", right: "" }] }), children: "Rule" }) : null
3964
4323
  ] });
3965
4324
  }
3966
4325
  function RuleEditor({ rule, onChange, dataSources, scopeVars }) {
3967
4326
  const op = CONDITION_OPERATORS.find((o) => o.value === rule.operator) ?? CONDITION_OPERATORS[0];
3968
4327
  const rightStr = rule.right === void 0 || rule.right === null ? "" : String(rule.right);
3969
- return /* @__PURE__ */ jsxs17("div", { className: "bpe-cond__row", children: [
3970
- /* @__PURE__ */ jsx18(BindingField, { value: rule.left, onChange: (left) => onChange({ ...rule, left }), dataSources, scopeVars, placeholder: "Choose a field" }),
3971
- /* @__PURE__ */ jsx18(Select, { value: rule.operator, onChange: (v) => onChange({ ...rule, operator: v }), options: CONDITION_OPERATORS.map((o) => ({ value: o.value, label: o.label })) }),
3972
- op.needsValue ? /* @__PURE__ */ jsx18(
4328
+ return /* @__PURE__ */ jsxs18("div", { className: "bpe-cond__row", children: [
4329
+ /* @__PURE__ */ jsx19(BindingField, { value: rule.left, onChange: (left) => onChange({ ...rule, left }), dataSources, scopeVars, placeholder: "Choose a field" }),
4330
+ /* @__PURE__ */ jsx19(Select, { value: rule.operator, onChange: (v) => onChange({ ...rule, operator: v }), options: CONDITION_OPERATORS.map((o) => ({ value: o.value, label: o.label })) }),
4331
+ op.needsValue ? /* @__PURE__ */ jsx19(
3973
4332
  TextInput,
3974
4333
  {
3975
4334
  value: rightStr,
@@ -3989,11 +4348,11 @@ function RuleEditor({ rule, onChange, dataSources, scopeVars }) {
3989
4348
  }
3990
4349
 
3991
4350
  // src/inspector/fields/ListEditors.tsx
3992
- import { useState as useState14 } from "react";
4351
+ import { useState as useState16 } from "react";
3993
4352
  import { createId as createId2 } from "@broadpaper/core";
3994
- import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
4353
+ import { Fragment as Fragment3, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
3995
4354
  function ListEditor({ items, onChange, renderItem, label, itemLabel = "Item", create, summary }) {
3996
- const [open, setOpen] = useState14(null);
4355
+ const [open, setOpen] = useState16(null);
3997
4356
  const update = (i, patch) => onChange(items.map((it, j) => j === i ? { ...it, ...patch } : it));
3998
4357
  const noun = itemLabel.toLowerCase();
3999
4358
  const name = (item, i) => summary?.(item, i) || `${itemLabel} ${i + 1}`;
@@ -4005,18 +4364,18 @@ function ListEditor({ items, onChange, renderItem, label, itemLabel = "Item", cr
4005
4364
  onChange(next);
4006
4365
  setOpen(j);
4007
4366
  };
4008
- return /* @__PURE__ */ jsxs18("div", { className: "bpe-list-editor", children: [
4009
- items.map((it, i) => /* @__PURE__ */ jsxs18("div", { className: "bpe-list-editor__item", children: [
4010
- /* @__PURE__ */ jsxs18("div", { className: "bpe-list-editor__head", children: [
4011
- /* @__PURE__ */ jsx19("button", { type: "button", className: "bpe-layer__toggle", "aria-expanded": open === i, "aria-label": `${open === i ? "Collapse" : "Expand"} ${name(it, i)}`, onClick: () => setOpen(open === i ? null : i), children: /* @__PURE__ */ jsx19(Icon, { name: "chevron-down" }) }),
4012
- /* @__PURE__ */ jsx19("span", { className: "bpe-truncate", title: summary?.(it, i), children: name(it, i) }),
4013
- /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", icon: "arrow-up", tooltip: `Move ${noun} up`, disabled: i === 0, onClick: () => move(i, -1) }),
4014
- /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", icon: "arrow-down", tooltip: `Move ${noun} down`, disabled: i === items.length - 1, onClick: () => move(i, 1) }),
4015
- /* @__PURE__ */ jsx19(Button, { size: "sm", variant: "ghost", icon: "trash", tooltip: `Remove ${noun}`, onClick: () => onChange(items.filter((_, j) => j !== i)) })
4367
+ return /* @__PURE__ */ jsxs19("div", { className: "bpe-list-editor", children: [
4368
+ items.map((it, i) => /* @__PURE__ */ jsxs19("div", { className: "bpe-list-editor__item", children: [
4369
+ /* @__PURE__ */ jsxs19("div", { className: "bpe-list-editor__head", children: [
4370
+ /* @__PURE__ */ jsx20("button", { type: "button", className: "bpe-layer__toggle", "aria-expanded": open === i, "aria-label": `${open === i ? "Collapse" : "Expand"} ${name(it, i)}`, onClick: () => setOpen(open === i ? null : i), children: /* @__PURE__ */ jsx20(Icon, { name: "chevron-down" }) }),
4371
+ /* @__PURE__ */ jsx20("span", { className: "bpe-truncate", title: summary?.(it, i), children: name(it, i) }),
4372
+ /* @__PURE__ */ jsx20(Button, { size: "sm", variant: "ghost", icon: "arrow-up", tooltip: `Move ${noun} up`, disabled: i === 0, onClick: () => move(i, -1) }),
4373
+ /* @__PURE__ */ jsx20(Button, { size: "sm", variant: "ghost", icon: "arrow-down", tooltip: `Move ${noun} down`, disabled: i === items.length - 1, onClick: () => move(i, 1) }),
4374
+ /* @__PURE__ */ jsx20(Button, { size: "sm", variant: "ghost", icon: "trash", tooltip: `Remove ${noun}`, onClick: () => onChange(items.filter((_, j) => j !== i)) })
4016
4375
  ] }),
4017
- open === i ? /* @__PURE__ */ jsx19("div", { className: "bpe-list-editor__body", children: renderItem(it, (patch) => update(i, patch), i) }) : null
4376
+ open === i ? /* @__PURE__ */ jsx20("div", { className: "bpe-list-editor__body", children: renderItem(it, (patch) => update(i, patch), i) }) : null
4018
4377
  ] }, it.id ?? i)),
4019
- /* @__PURE__ */ jsxs18(Button, { size: "sm", icon: "plus", onClick: () => {
4378
+ /* @__PURE__ */ jsxs19(Button, { size: "sm", icon: "plus", onClick: () => {
4020
4379
  onChange([...items, create()]);
4021
4380
  setOpen(items.length);
4022
4381
  }, children: [
@@ -4025,8 +4384,9 @@ function ListEditor({ items, onChange, renderItem, label, itemLabel = "Item", cr
4025
4384
  ] })
4026
4385
  ] });
4027
4386
  }
4387
+ var TOTAL_FORMATS = [...FORMAT_SUGGESTIONS.currency ?? [], ...FORMAT_SUGGESTIONS.number ?? [], ...FORMAT_SUGGESTIONS.percent ?? []];
4028
4388
  function ColumnsEditor({ value, onChange, dataSources, scopeVars }) {
4029
- return /* @__PURE__ */ jsx19(
4389
+ return /* @__PURE__ */ jsx20(
4030
4390
  ListEditor,
4031
4391
  {
4032
4392
  items: value,
@@ -4034,21 +4394,28 @@ function ColumnsEditor({ value, onChange, dataSources, scopeVars }) {
4034
4394
  itemLabel: "Column",
4035
4395
  summary: (c) => c.header || c.value || "Column",
4036
4396
  create: () => ({ id: createId2("c"), header: "New column", value: "", align: "left", width: null }),
4037
- renderItem: (c, update) => /* @__PURE__ */ jsxs18(Fragment3, { children: [
4038
- /* @__PURE__ */ jsx19(Field, { label: "Heading", stacked: true, children: /* @__PURE__ */ jsx19(TextInput, { value: c.header, onChange: (header) => update({ header }), lazy: true }) }),
4039
- /* @__PURE__ */ jsx19(Field, { label: "Value", stacked: true, children: /* @__PURE__ */ jsx19(BindingField, { value: c.value, onChange: (v) => update({ value: v }), dataSources, scopeVars, placeholder: "Field per row" }) }),
4040
- /* @__PURE__ */ jsx19(Field, { label: "Align", children: /* @__PURE__ */ jsx19(Select, { value: c.align ?? "left", onChange: (v) => update({ align: v }), options: [{ value: "left", label: "Left" }, { value: "center", label: "Centre" }, { value: "right", label: "Right" }] }) }),
4041
- /* @__PURE__ */ jsx19(Field, { label: "Width", children: /* @__PURE__ */ jsx19(NumberInput, { value: c.width ?? null, onChange: (w) => update({ width: w }), min: 3, max: 100, unit: "%", placeholder: "auto" }) }),
4042
- /* @__PURE__ */ jsx19(Field, { label: "Total", children: /* @__PURE__ */ jsx19(Select, { value: c.total ?? "none", onChange: (v) => update({ total: v }), options: [{ value: "none", label: "None" }, { value: "sum", label: "Sum" }, { value: "avg", label: "Average" }, { value: "count", label: "Count" }, { value: "min", label: "Minimum" }, { value: "max", label: "Maximum" }] }) }),
4043
- c.total && c.total !== "none" ? /* @__PURE__ */ jsx19(Field, { label: "Total format", children: /* @__PURE__ */ jsx19(TextInput, { value: c.totalFormat ?? "", onChange: (totalFormat) => update({ totalFormat }), mono: true, lazy: true, placeholder: 'e.g. currency:"GBP":0' }) }) : null,
4044
- /* @__PURE__ */ jsx19(Field, { label: "Bold", children: /* @__PURE__ */ jsx19(Switch, { checked: !!c.bold, onChange: (bold) => update({ bold }) }) }),
4045
- /* @__PURE__ */ jsx19(Field, { label: "Wrap text", children: /* @__PURE__ */ jsx19(Switch, { checked: c.wrap !== false, onChange: (wrap) => update({ wrap }) }) })
4397
+ renderItem: (c, update) => /* @__PURE__ */ jsxs19(Fragment3, { children: [
4398
+ /* @__PURE__ */ jsx20(Field, { label: "Heading", stacked: true, children: /* @__PURE__ */ jsx20(TextInput, { value: c.header, onChange: (header) => update({ header }), lazy: true }) }),
4399
+ /* @__PURE__ */ jsx20(Field, { label: "Value", stacked: true, children: /* @__PURE__ */ jsx20(BindingField, { value: c.value, onChange: (v) => update({ value: v }), dataSources, scopeVars, placeholder: "Field per row" }) }),
4400
+ /* @__PURE__ */ jsx20(Field, { label: "Align", children: /* @__PURE__ */ jsx20(Select, { value: c.align ?? "left", onChange: (v) => update({ align: v }), options: [{ value: "left", label: "Left" }, { value: "center", label: "Centre" }, { value: "right", label: "Right" }] }) }),
4401
+ /* @__PURE__ */ jsx20(Field, { label: "Width", children: /* @__PURE__ */ jsx20(NumberInput, { value: c.width ?? null, onChange: (w) => update({ width: w }), min: 3, max: 100, unit: "%", placeholder: "auto" }) }),
4402
+ /* @__PURE__ */ jsx20(Field, { label: "Total", children: /* @__PURE__ */ jsx20(Select, { value: c.total ?? "none", onChange: (v) => update({ total: v }), options: [{ value: "none", label: "None" }, { value: "sum", label: "Sum" }, { value: "avg", label: "Average" }, { value: "count", label: "Count" }, { value: "min", label: "Minimum" }, { value: "max", label: "Maximum" }] }) }),
4403
+ c.total && c.total !== "none" ? /* @__PURE__ */ jsx20(Field, { label: "Total shown as", children: /* @__PURE__ */ jsx20(
4404
+ Select,
4405
+ {
4406
+ value: c.totalFormat ?? "",
4407
+ onChange: (totalFormat) => update({ totalFormat: totalFormat || void 0 }),
4408
+ options: [{ value: "", label: "As it comes" }, ...TOTAL_FORMATS.map((f) => ({ value: f.pipe, label: f.label }))]
4409
+ }
4410
+ ) }) : null,
4411
+ /* @__PURE__ */ jsx20(Field, { label: "Bold", children: /* @__PURE__ */ jsx20(Switch, { checked: !!c.bold, onChange: (bold) => update({ bold }) }) }),
4412
+ /* @__PURE__ */ jsx20(Field, { label: "Wrap text", children: /* @__PURE__ */ jsx20(Switch, { checked: c.wrap !== false, onChange: (wrap) => update({ wrap }) }) })
4046
4413
  ] })
4047
4414
  }
4048
4415
  );
4049
4416
  }
4050
4417
  function SeriesEditor({ value, onChange, dataSources, scopeVars, renderColor }) {
4051
- return /* @__PURE__ */ jsx19(
4418
+ return /* @__PURE__ */ jsx20(
4052
4419
  ListEditor,
4053
4420
  {
4054
4421
  items: value,
@@ -4056,33 +4423,98 @@ function SeriesEditor({ value, onChange, dataSources, scopeVars, renderColor })
4056
4423
  itemLabel: "Series",
4057
4424
  summary: (s) => s.label || s.value || "Series",
4058
4425
  create: () => ({ id: createId2("s"), label: `Series ${value.length + 1}`, value: "" }),
4059
- renderItem: (s, update) => /* @__PURE__ */ jsxs18(Fragment3, { children: [
4060
- /* @__PURE__ */ jsx19(Field, { label: "Label", children: /* @__PURE__ */ jsx19(TextInput, { value: s.label, onChange: (label) => update({ label }), lazy: true }) }),
4061
- /* @__PURE__ */ jsx19(Field, { label: "Value", stacked: true, children: /* @__PURE__ */ jsx19(BindingField, { value: s.value, onChange: (v) => update({ value: v }), dataSources, scopeVars, placeholder: "Numeric field per item" }) }),
4062
- /* @__PURE__ */ jsx19(Field, { label: "Colour", children: renderColor(s.color, (color) => update({ color })) })
4426
+ renderItem: (s, update) => /* @__PURE__ */ jsxs19(Fragment3, { children: [
4427
+ /* @__PURE__ */ jsx20(Field, { label: "Label", children: /* @__PURE__ */ jsx20(TextInput, { value: s.label, onChange: (label) => update({ label }), lazy: true }) }),
4428
+ /* @__PURE__ */ jsx20(Field, { label: "Value", stacked: true, children: /* @__PURE__ */ jsx20(BindingField, { value: s.value, onChange: (v) => update({ value: v }), dataSources, scopeVars, placeholder: "Numeric field per item" }) }),
4429
+ /* @__PURE__ */ jsx20(Field, { label: "Colour", children: renderColor(s.color, (color) => update({ color })) })
4063
4430
  ] })
4064
4431
  }
4065
4432
  );
4066
4433
  }
4067
- function KeyValuesEditor({ value, onChange, dataSources, scopeVars }) {
4068
- return /* @__PURE__ */ jsx19(
4434
+ function KeyValuesEditor({
4435
+ value,
4436
+ onChange,
4437
+ dataSources,
4438
+ scopeVars,
4439
+ allowExpressions = true
4440
+ }) {
4441
+ const humanise = useTokenText(dataSources, scopeVars);
4442
+ const summarise = (text) => allowExpressions ? text : humanise(text);
4443
+ return /* @__PURE__ */ jsx20(
4069
4444
  ListEditor,
4070
4445
  {
4071
4446
  items: value,
4072
4447
  onChange,
4073
4448
  itemLabel: "Item",
4074
- summary: (k) => k.label || "Item",
4449
+ summary: (k) => summarise(k.label) || "Item",
4075
4450
  create: () => ({ id: createId2("k"), label: "Label", value: "" }),
4076
- renderItem: (k, update) => /* @__PURE__ */ jsxs18(Fragment3, { children: [
4077
- /* @__PURE__ */ jsx19(Field, { label: "Label", children: /* @__PURE__ */ jsx19(TextInput, { value: k.label, onChange: (label) => update({ label }), lazy: true }) }),
4078
- /* @__PURE__ */ jsx19(Field, { label: "Value", stacked: true, children: /* @__PURE__ */ jsx19(BindingField, { value: k.value.replace(/^\{\{\s*|\s*\}\}$/g, ""), onChange: (v) => update({ value: v ? `{{ ${v} }}` : "" }), dataSources, scopeVars, placeholder: "Field or text" }) }),
4079
- /* @__PURE__ */ jsx19(Field, { label: "Text", stacked: true, children: /* @__PURE__ */ jsx19(TextInput, { value: k.value, onChange: (value2) => update({ value: value2 }), lazy: true, placeholder: "Static text or {{ field }}" }) })
4080
- ] })
4451
+ renderItem: (k, update) => {
4452
+ const bound = /^\{\{[^}]*\}\}$/.test(k.value.trim());
4453
+ return /* @__PURE__ */ jsxs19(Fragment3, { children: [
4454
+ /* @__PURE__ */ jsx20(Field, { label: "Label", children: /* @__PURE__ */ jsx20(
4455
+ TextOrTokens,
4456
+ {
4457
+ value: k.label,
4458
+ onChange: (label) => update({ label }),
4459
+ dataSources,
4460
+ scopeVars,
4461
+ allowExpressions
4462
+ }
4463
+ ) }),
4464
+ /* @__PURE__ */ jsx20(Field, { label: "Shows", stacked: true, children: /* @__PURE__ */ jsx20(
4465
+ Segmented,
4466
+ {
4467
+ value: bound ? "field" : "text",
4468
+ ariaLabel: "What this row shows",
4469
+ onChange: (mode) => update({ value: mode === "field" ? "" : k.value.replace(/\{\{|\}\}/g, "").trim() }),
4470
+ options: [
4471
+ { value: "field", label: "A field" },
4472
+ { value: "text", label: "Some text" }
4473
+ ]
4474
+ }
4475
+ ) }),
4476
+ bound ? /* @__PURE__ */ jsx20(Field, { label: "Field", stacked: true, children: /* @__PURE__ */ jsx20(
4477
+ BindingField,
4478
+ {
4479
+ value: k.value.replace(/^\{\{\s*|\s*\}\}$/g, ""),
4480
+ onChange: (v) => update({ value: v ? `{{ ${v} }}` : "" }),
4481
+ dataSources,
4482
+ scopeVars,
4483
+ placeholder: "Choose a field",
4484
+ allowExpression: allowExpressions
4485
+ }
4486
+ ) }) : /* @__PURE__ */ jsx20(Field, { label: "Text", stacked: true, children: /* @__PURE__ */ jsx20(
4487
+ TextOrTokens,
4488
+ {
4489
+ value: k.value,
4490
+ onChange: (value2) => update({ value: value2 }),
4491
+ dataSources,
4492
+ scopeVars,
4493
+ allowExpressions,
4494
+ placeholder: "Type what this row should say"
4495
+ }
4496
+ ) })
4497
+ ] });
4498
+ }
4081
4499
  }
4082
4500
  );
4083
4501
  }
4502
+ function TextOrTokens({
4503
+ value,
4504
+ onChange,
4505
+ dataSources,
4506
+ scopeVars,
4507
+ allowExpressions,
4508
+ placeholder
4509
+ }) {
4510
+ const humanise = useTokenText(dataSources, scopeVars);
4511
+ if (!allowExpressions && value.includes("{{")) {
4512
+ return /* @__PURE__ */ jsx20(TextInput, { value: humanise(value), onChange: () => void 0, readOnly: true, title: "This line fills itself in from your data." });
4513
+ }
4514
+ return /* @__PURE__ */ jsx20(TextInput, { value, onChange, lazy: true, ...placeholder ? { placeholder } : {} });
4515
+ }
4084
4516
  function SubFormList({ value, onChange, fields, itemLabel, summary, renderField }) {
4085
- return /* @__PURE__ */ jsx19(
4517
+ return /* @__PURE__ */ jsx20(
4086
4518
  ListEditor,
4087
4519
  {
4088
4520
  items: value,
@@ -4090,13 +4522,13 @@ function SubFormList({ value, onChange, fields, itemLabel, summary, renderField
4090
4522
  itemLabel: itemLabel ?? "Item",
4091
4523
  summary: (it, i) => summary?.(it, i) || String(it.label ?? it.header ?? `${itemLabel ?? "Item"} ${i + 1}`),
4092
4524
  create: () => ({ id: createId2("i") }),
4093
- renderItem: (it, update) => /* @__PURE__ */ jsx19(Fragment3, { children: fields.map((f) => /* @__PURE__ */ jsx19("div", { children: renderField(f, it[f.prop], (v) => update({ [f.prop]: v })) }, f.prop)) })
4525
+ renderItem: (it, update) => /* @__PURE__ */ jsx20(Fragment3, { children: fields.map((f) => /* @__PURE__ */ jsx20("div", { children: renderField(f, it[f.prop], (v) => update({ [f.prop]: v })) }, f.prop)) })
4094
4526
  }
4095
4527
  );
4096
4528
  }
4097
4529
 
4098
4530
  // src/inspector/fields/FieldRenderer.tsx
4099
- import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
4531
+ import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
4100
4532
  function getPath(obj, path) {
4101
4533
  let cur = obj;
4102
4534
  for (const p of path.split(".")) {
@@ -4108,15 +4540,22 @@ function getPath(obj, path) {
4108
4540
  var COMMON_FONTS = ["Inter", "Roboto", "Open Sans", "Lato", "Source Sans 3", "Helvetica Neue", "Arial", "Georgia", "Times New Roman", "Merriweather", "Playfair Display", "Courier New", "JetBrains Mono"];
4109
4541
  function FieldRenderer({ field, value, onChange, ctx }) {
4110
4542
  const description = typeof field.description === "function" ? field.description(ctx.props) : field.description;
4111
- const wrap = (control, opts = {}) => /* @__PURE__ */ jsx20(Field, { label: field.label, stacked: opts.stacked ?? field.fullWidth, hint: opts.hint ?? description, error: opts.error, children: control });
4543
+ const humanise = useTokenText(ctx.dataSources, ctx.scopeVars);
4544
+ const wrap = (control, opts = {}) => /* @__PURE__ */ jsx21(Field, { label: field.label, stacked: opts.stacked ?? field.fullWidth, hint: opts.hint ?? description, error: opts.error, children: control });
4112
4545
  const str = typeof value === "string" ? value : value === void 0 || value === null ? "" : String(value);
4113
4546
  switch (field.kind) {
4114
4547
  case "text": {
4115
4548
  const tokens = field.allowTokens !== false && ctx.allowExpressions;
4549
+ if (!tokens && str.includes("{{")) {
4550
+ return wrap(
4551
+ /* @__PURE__ */ jsx21("div", { className: "bpe-binding", children: /* @__PURE__ */ jsx21(TextInput, { value: humanise(str), onChange: () => void 0, readOnly: true, title: "This line fills itself in from your data." }) }),
4552
+ { hint: description ?? "Fills itself in from your data." }
4553
+ );
4554
+ }
4116
4555
  return wrap(
4117
- /* @__PURE__ */ jsxs19("div", { className: "bpe-binding", children: [
4118
- /* @__PURE__ */ jsx20(TextInput, { value: str, onChange: (v) => onChange(v), lazy: true, placeholder: field.placeholder }),
4119
- tokens ? /* @__PURE__ */ jsx20(Popover, { className: "bpe-picker", trigger: /* @__PURE__ */ jsx20(Button, { size: "sm", variant: "ghost", icon: "braces", tooltip: "Insert field" }), children: (close) => /* @__PURE__ */ jsx20(DataPicker, { dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, onPick: (expr) => {
4556
+ /* @__PURE__ */ jsxs20("div", { className: "bpe-binding", children: [
4557
+ /* @__PURE__ */ jsx21(TextInput, { value: str, onChange: (v) => onChange(v), lazy: true, placeholder: field.placeholder }),
4558
+ tokens ? /* @__PURE__ */ jsx21(Popover, { className: "bpe-picker", trigger: /* @__PURE__ */ jsx21(Button, { size: "sm", variant: "ghost", icon: "braces", tooltip: "Insert field" }), children: (close) => /* @__PURE__ */ jsx21(DataPicker, { dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, onPick: (expr) => {
4120
4559
  onChange(`${str}${str && !str.endsWith(" ") ? " " : ""}{{ ${expr} }}`);
4121
4560
  close();
4122
4561
  } }) }) : null
@@ -4124,41 +4563,46 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4124
4563
  );
4125
4564
  }
4126
4565
  case "textarea":
4127
- return wrap(/* @__PURE__ */ jsx20(TextArea, { value: str, rows: field.rows ?? 3, placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) }), { stacked: true });
4566
+ return wrap(/* @__PURE__ */ jsx21(TextArea, { value: str, rows: field.rows ?? 3, placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) }), { stacked: true });
4128
4567
  case "richtext": {
4129
4568
  const doc = isRichTextDoc3(value) ? value : richTextFromTemplate3(str);
4130
4569
  const plain = richTextToPlain2(doc);
4131
4570
  return wrap(
4132
- /* @__PURE__ */ jsx20(TextArea, { value: plain, rows: 4, onChange: (e) => onChange(richTextFromTemplate3(e.target.value)), placeholder: "Type text, use {{ field }} for data. Double-click the block on the canvas for rich formatting." }),
4571
+ /* @__PURE__ */ jsx21(TextArea, { value: plain, rows: 4, onChange: (e) => onChange(richTextFromTemplate3(e.target.value)), placeholder: "Type text, use {{ field }} for data. Double-click the block on the canvas for rich formatting." }),
4133
4572
  { stacked: true, hint: "Double-click the block on the canvas for formatting, lists and links." }
4134
4573
  );
4135
4574
  }
4136
4575
  case "number":
4137
- return wrap(/* @__PURE__ */ jsx20(NumberInput, { value: typeof value === "number" ? value : null, onChange: (v) => onChange(v), min: field.min, max: field.max, step: field.step, unit: field.unit, placeholder: "auto" }));
4576
+ return wrap(/* @__PURE__ */ jsx21(NumberInput, { value: typeof value === "number" ? value : null, onChange: (v) => onChange(v), min: field.min, max: field.max, step: field.step, unit: field.unit, placeholder: "auto" }));
4138
4577
  case "toggle":
4139
- return wrap(/* @__PURE__ */ jsx20(Switch, { checked: !!value, onChange: (v) => onChange(v), label: field.label }));
4578
+ return wrap(/* @__PURE__ */ jsx21(Switch, { checked: !!value, onChange: (v) => onChange(v), label: field.label }));
4140
4579
  case "select": {
4141
4580
  const options = field.optionsFrom ? field.optionsFrom(ctx.props) : field.options ?? [];
4142
- return wrap(/* @__PURE__ */ jsx20(Select, { value: str, onChange: (v) => onChange(coerce(v, options)), options: options.map((o) => ({ value: String(o.value), label: o.label })) }));
4581
+ return wrap(/* @__PURE__ */ jsx21(Select, { value: str, onChange: (v) => onChange(coerce(v, options)), options: options.map((o) => ({ value: String(o.value), label: o.label })) }));
4143
4582
  }
4144
4583
  case "segmented":
4145
- return wrap(/* @__PURE__ */ jsx20(Segmented, { value: str, onChange: (v) => onChange(coerce(v, field.options)), options: field.options.map((o) => ({ value: String(o.value), label: o.label, icon: o.icon })), iconOnly: field.options.every((o) => !!o.icon) && field.options.length > 3, ariaLabel: field.label }));
4584
+ return wrap(/* @__PURE__ */ jsx21(Segmented, { value: str, onChange: (v) => onChange(coerce(v, field.options)), options: field.options.map((o) => ({ value: String(o.value), label: o.label, icon: o.icon })), iconOnly: field.options.every((o) => !!o.icon) && field.options.length > 3, ariaLabel: field.label }));
4146
4585
  case "color":
4147
- return wrap(/* @__PURE__ */ jsx20(ColorField, { value: str || void 0, onChange: (v) => onChange(v ?? ""), theme: ctx.theme, allowTransparent: field.allowTransparent }));
4586
+ return wrap(/* @__PURE__ */ jsx21(ColorField, { value: str || void 0, onChange: (v) => onChange(v ?? ""), theme: ctx.theme, allowTransparent: field.allowTransparent }));
4148
4587
  case "font":
4149
- return wrap(/* @__PURE__ */ jsx20(FontSelect, { value: str, onChange, theme: ctx.theme }));
4588
+ return wrap(/* @__PURE__ */ jsx21(FontSelect, { value: str, onChange, theme: ctx.theme }));
4150
4589
  case "textStyle":
4151
- return wrap(/* @__PURE__ */ jsx20(Select, { value: str, onChange: (v) => onChange(v || void 0), placeholder: "Default", options: ctx.textStyles.map((s) => ({ value: s, label: textStyleLabel(s) })) }));
4590
+ return wrap(/* @__PURE__ */ jsx21(Select, { value: str, onChange: (v) => onChange(v || void 0), placeholder: "Default", options: ctx.textStyles.map((s) => ({ value: s, label: textStyleLabel(s) })) }));
4152
4591
  case "binding":
4153
- return wrap(/* @__PURE__ */ jsx20(BindingField, { value: str, onChange: (v) => onChange(v), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, types: field.valueType ? Array.isArray(field.valueType) ? field.valueType : [field.valueType] : void 0, placeholder: field.placeholder, allowExpression: ctx.allowExpressions }));
4592
+ return wrap(/* @__PURE__ */ jsx21(BindingField, { value: str, onChange: (v) => onChange(v), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, types: field.valueType ? Array.isArray(field.valueType) ? field.valueType : [field.valueType] : void 0, placeholder: field.placeholder, allowExpression: ctx.allowExpressions }));
4154
4593
  case "collection":
4155
- return wrap(/* @__PURE__ */ jsx20(BindingField, { value: str, onChange: (v) => onChange(v), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, arraysOnly: true, placeholder: field.placeholder, allowExpression: ctx.allowExpressions }));
4594
+ return wrap(/* @__PURE__ */ jsx21(BindingField, { value: str, onChange: (v) => onChange(v), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, arraysOnly: true, placeholder: field.placeholder, allowExpression: ctx.allowExpressions }));
4156
4595
  case "expression": {
4596
+ if (!ctx.allowExpressions) {
4597
+ return wrap(
4598
+ /* @__PURE__ */ jsx21(BindingField, { value: str, onChange: (v) => onChange(v), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, placeholder: field.placeholder, allowExpression: false, allowCalculation: false })
4599
+ );
4600
+ }
4157
4601
  const err = str.trim() ? checkExpressionSyntax3(str) : null;
4158
4602
  return wrap(
4159
- /* @__PURE__ */ jsxs19("div", { className: "bpe-binding", children: [
4160
- /* @__PURE__ */ jsx20(TextInput, { value: str, onChange: (v) => onChange(v), lazy: true, mono: true, placeholder: field.placeholder, invalid: !!err }),
4161
- /* @__PURE__ */ jsx20(Popover, { className: "bpe-picker", trigger: /* @__PURE__ */ jsx20(Button, { size: "sm", variant: "ghost", icon: "braces", tooltip: "Pick field" }), children: (close) => /* @__PURE__ */ jsx20(DataPicker, { dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, value: str, onPick: (expr) => {
4603
+ /* @__PURE__ */ jsxs20("div", { className: "bpe-binding", children: [
4604
+ /* @__PURE__ */ jsx21(TextInput, { value: str, onChange: (v) => onChange(v), lazy: true, mono: true, placeholder: field.placeholder, invalid: !!err }),
4605
+ /* @__PURE__ */ jsx21(Popover, { className: "bpe-picker", trigger: /* @__PURE__ */ jsx21(Button, { size: "sm", variant: "ghost", icon: "braces", tooltip: "Pick field" }), children: (close) => /* @__PURE__ */ jsx21(DataPicker, { dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, value: str, onPick: (expr) => {
4162
4606
  onChange(expr);
4163
4607
  close();
4164
4608
  } }) })
@@ -4167,7 +4611,7 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4167
4611
  );
4168
4612
  }
4169
4613
  case "image":
4170
- return /* @__PURE__ */ jsx20(
4614
+ return /* @__PURE__ */ jsx21(
4171
4615
  ImageFieldRow,
4172
4616
  {
4173
4617
  field,
@@ -4178,16 +4622,28 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4178
4622
  }
4179
4623
  );
4180
4624
  case "condition":
4181
- return wrap(/* @__PURE__ */ jsx20(ConditionBuilder, { value, onChange: (v) => onChange(v ?? ""), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, allowExpression: ctx.allowExpressions }), { stacked: true });
4625
+ return wrap(/* @__PURE__ */ jsx21(ConditionBuilder, { value, onChange: (v) => onChange(v ?? ""), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, allowExpression: ctx.allowExpressions }), { stacked: true });
4182
4626
  case "columns":
4183
- return wrap(/* @__PURE__ */ jsx20(ColumnsEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars }), { stacked: true });
4627
+ return wrap(/* @__PURE__ */ jsx21(ColumnsEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars }), { stacked: true });
4184
4628
  case "series":
4185
- return wrap(/* @__PURE__ */ jsx20(SeriesEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, renderColor: (v, set) => /* @__PURE__ */ jsx20(ColorField, { value: v, onChange: set, theme: ctx.theme, placeholder: "Palette" }) }), { stacked: true });
4629
+ return wrap(/* @__PURE__ */ jsx21(SeriesEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, renderColor: (v, set) => /* @__PURE__ */ jsx21(ColorField, { value: v, onChange: set, theme: ctx.theme, placeholder: "Palette" }) }), { stacked: true });
4186
4630
  case "keyValues":
4187
- return wrap(/* @__PURE__ */ jsx20(KeyValuesEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars }), { stacked: true });
4631
+ return wrap(
4632
+ /* @__PURE__ */ jsx21(
4633
+ KeyValuesEditor,
4634
+ {
4635
+ value: Array.isArray(value) ? value : [],
4636
+ onChange,
4637
+ dataSources: ctx.dataSources,
4638
+ scopeVars: ctx.scopeVars,
4639
+ allowExpressions: ctx.allowExpressions
4640
+ }
4641
+ ),
4642
+ { stacked: true }
4643
+ );
4188
4644
  case "list":
4189
4645
  return wrap(
4190
- /* @__PURE__ */ jsx20(
4646
+ /* @__PURE__ */ jsx21(
4191
4647
  SubFormList,
4192
4648
  {
4193
4649
  value: Array.isArray(value) ? value : [],
@@ -4195,13 +4651,13 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4195
4651
  fields: field.fields,
4196
4652
  itemLabel: field.itemLabel,
4197
4653
  summary: field.itemSummary ? (item, i) => field.itemSummary(item, i, ctx.props) : void 0,
4198
- renderField: (f, v, set) => /* @__PURE__ */ jsx20(FieldRenderer, { field: f, value: v, onChange: set, ctx })
4654
+ renderField: (f, v, set) => /* @__PURE__ */ jsx21(FieldRenderer, { field: f, value: v, onChange: set, ctx })
4199
4655
  }
4200
4656
  ),
4201
4657
  { stacked: true }
4202
4658
  );
4203
4659
  case "custom":
4204
- return wrap(/* @__PURE__ */ jsx20(CustomFieldHost, { field, value, onChange, ctx }), { stacked: true });
4660
+ return wrap(/* @__PURE__ */ jsx21(CustomFieldHost, { field, value, onChange, ctx }), { stacked: true });
4205
4661
  }
4206
4662
  }
4207
4663
  function coerce(v, options) {
@@ -4215,12 +4671,12 @@ function FontSelect({ value, onChange, theme }) {
4215
4671
  ...theme.typography.monoFont ? [{ value: "$typography.monoFont", label: "Mono font" }] : []
4216
4672
  ];
4217
4673
  const fonts = [...(theme.fonts ?? []).map((f) => f.family), ...COMMON_FONTS].filter((f, i, a) => a.indexOf(f) === i);
4218
- return /* @__PURE__ */ jsx20(Select, { value, onChange, placeholder: "Default", options: [...themeFonts, ...fonts.map((f) => ({ value: f, label: f }))] });
4674
+ return /* @__PURE__ */ jsx21(Select, { value, onChange, placeholder: "Default", options: [...themeFonts, ...fonts.map((f) => ({ value: f, label: f }))] });
4219
4675
  }
4220
4676
  function CustomFieldHost({ field, value, onChange, ctx }) {
4221
4677
  const session = useSession();
4222
- const ref = useRef7(null);
4223
- const mounted = useRef7(false);
4678
+ const ref = useRef8(null);
4679
+ const mounted = useRef8(false);
4224
4680
  const api = () => ({
4225
4681
  value,
4226
4682
  props: ctx.props,
@@ -4228,10 +4684,10 @@ function CustomFieldHost({ field, value, onChange, ctx }) {
4228
4684
  setValue: onChange,
4229
4685
  setProps: ctx.setProps,
4230
4686
  dataSources: ctx.dataSources,
4231
- schemaScope: createSchemaScope4(ctx.dataSources).child(ctx.scopeVars ?? {}),
4687
+ schemaScope: createSchemaScope6(ctx.dataSources).child(ctx.scopeVars ?? {}),
4232
4688
  theme: ctx.theme
4233
4689
  });
4234
- useEffect6(() => {
4690
+ useEffect7(() => {
4235
4691
  const el = ref.current;
4236
4692
  if (!el) return;
4237
4693
  if (!mounted.current) {
@@ -4239,14 +4695,14 @@ function CustomFieldHost({ field, value, onChange, ctx }) {
4239
4695
  mounted.current = true;
4240
4696
  } else field.component.update?.(api());
4241
4697
  });
4242
- useEffect6(() => {
4698
+ useEffect7(() => {
4243
4699
  const el = ref.current;
4244
4700
  return () => {
4245
4701
  if (el && mounted.current) field.component.unmount?.(el);
4246
4702
  mounted.current = false;
4247
4703
  };
4248
4704
  }, [field.component, session]);
4249
- return /* @__PURE__ */ jsx20("div", { ref, style: { width: "100%" } });
4705
+ return /* @__PURE__ */ jsx21("div", { ref, style: { width: "100%" } });
4250
4706
  }
4251
4707
  function ImageFieldRow({
4252
4708
  field,
@@ -4255,9 +4711,9 @@ function ImageFieldRow({
4255
4711
  ctx,
4256
4712
  render
4257
4713
  }) {
4258
- const [error, setError] = useState15(null);
4714
+ const [error, setError] = useState17(null);
4259
4715
  return render(
4260
- /* @__PURE__ */ jsx20(
4716
+ /* @__PURE__ */ jsx21(
4261
4717
  ImageField,
4262
4718
  {
4263
4719
  value,
@@ -4277,13 +4733,13 @@ function ImageFieldRow({
4277
4733
 
4278
4734
  // src/inspector/StyleGroups.tsx
4279
4735
  import { textStyleLabel as textStyleLabel2 } from "@broadpaper/core";
4280
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
4736
+ import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
4281
4737
  var SIDES = ["top", "right", "bottom", "left"];
4282
4738
  function SpacingGrid({ value, onChange }) {
4283
4739
  const v = value ?? {};
4284
- return /* @__PURE__ */ jsxs20("div", { children: [
4285
- /* @__PURE__ */ jsx21("div", { className: "bpe-spacing-grid", children: SIDES.map((s) => /* @__PURE__ */ jsx21(NumberInput, { value: v[s] ?? null, onChange: (n) => onChange({ ...v, [s]: n ?? void 0 }), min: -200, max: 400, placeholder: "0", "aria-label": s }, s)) }),
4286
- /* @__PURE__ */ jsx21("div", { className: "bpe-spacing-grid", children: SIDES.map((s) => /* @__PURE__ */ jsx21("div", { className: "bpe-spacing-grid__lbl", children: s }, s)) })
4740
+ return /* @__PURE__ */ jsxs21("div", { children: [
4741
+ /* @__PURE__ */ jsx22("div", { className: "bpe-spacing-grid", children: SIDES.map((s) => /* @__PURE__ */ jsx22(NumberInput, { value: v[s] ?? null, onChange: (n) => onChange({ ...v, [s]: n ?? void 0 }), min: -200, max: 400, placeholder: "0", "aria-label": s }, s)) }),
4742
+ /* @__PURE__ */ jsx22("div", { className: "bpe-spacing-grid", children: SIDES.map((s) => /* @__PURE__ */ jsx22("div", { className: "bpe-spacing-grid__lbl", children: s }, s)) })
4287
4743
  ] });
4288
4744
  }
4289
4745
  function StyleGroups({ node, groups, theme, textStyles, setStyle, setFlow, allowOverrides }) {
@@ -4291,62 +4747,62 @@ function StyleGroups({ node, groups, theme, textStyles, setStyle, setFlow, allow
4291
4747
  const f = node.flow ?? {};
4292
4748
  const has = (g) => groups.includes(g);
4293
4749
  const hasOverrides = Object.keys(s).length > 0;
4294
- return /* @__PURE__ */ jsxs20(Fragment4, { children: [
4295
- allowOverrides && (has("textStyle") || has("typography") || has("alignment")) ? /* @__PURE__ */ jsx21(Section, { title: "Typography", actions: hasOverrides ? /* @__PURE__ */ jsx21(Button, { size: "sm", variant: "ghost", icon: "rotate-ccw", tooltip: "Reset style overrides", onClick: () => setStyle(null) }) : null, children: /* @__PURE__ */ jsxs20("div", { className: "bpe-prop-grid", children: [
4296
- has("textStyle") ? /* @__PURE__ */ jsx21(Field, { label: "Text style", children: /* @__PURE__ */ jsx21(Select, { value: s.textStyle ?? "", onChange: (v) => setStyle({ textStyle: v || void 0 }), placeholder: "Block default", options: textStyles.map((t) => ({ value: t, label: textStyleLabel2(t) })) }) }) : null,
4297
- has("typography") ? /* @__PURE__ */ jsxs20(Fragment4, { children: [
4298
- /* @__PURE__ */ jsx21(Field, { label: "Font", children: /* @__PURE__ */ jsx21(Select, { value: s.fontFamily ?? "", onChange: (v) => setStyle({ fontFamily: v || void 0 }), placeholder: "Inherit", options: [{ value: "$typography.headingFont", label: "Heading font" }, { value: "$typography.bodyFont", label: "Body font" }, ...(theme.fonts ?? []).map((x) => ({ value: x.family, label: x.family })), ...["Inter", "Roboto", "Georgia", "Arial", "Times New Roman"].map((x) => ({ value: x, label: x }))] }) }),
4299
- /* @__PURE__ */ jsx21(Field, { label: "Size", children: /* @__PURE__ */ jsx21(NumberInput, { value: s.fontSize ?? null, onChange: (v) => setStyle({ fontSize: v ?? void 0 }), min: 4, max: 200, unit: "px", placeholder: "inherit" }) }),
4300
- /* @__PURE__ */ jsx21(Field, { label: "Weight", children: /* @__PURE__ */ jsx21(Select, { value: s.fontWeight === void 0 ? "" : String(s.fontWeight), onChange: (v) => setStyle({ fontWeight: v ? Number(v) : void 0 }), placeholder: "Inherit", options: [300, 400, 500, 600, 700, 800].map((w) => ({ value: String(w), label: w === 400 ? "Regular" : w === 700 ? "Bold" : String(w) })) }) }),
4301
- /* @__PURE__ */ jsx21(Field, { label: "Line height", children: /* @__PURE__ */ jsx21(NumberInput, { value: s.lineHeight ?? null, onChange: (v) => setStyle({ lineHeight: v ?? void 0 }), min: 0.6, max: 4, step: 0.05, placeholder: "inherit" }) }),
4302
- /* @__PURE__ */ jsx21(Field, { label: "Spacing", children: /* @__PURE__ */ jsx21(NumberInput, { value: s.letterSpacing ?? null, onChange: (v) => setStyle({ letterSpacing: v ?? void 0 }), min: -0.2, max: 1, step: 0.01, unit: "em", placeholder: "0" }) }),
4303
- /* @__PURE__ */ jsx21(Field, { label: "Case", children: /* @__PURE__ */ jsx21(Segmented, { value: s.textTransform ?? "none", onChange: (v) => setStyle({ textTransform: v === "none" ? void 0 : v }), options: [{ value: "none", label: "Aa" }, { value: "uppercase", label: "AA" }, { value: "lowercase", label: "aa" }, { value: "capitalize", label: "Aa Bb" }] }) }),
4304
- /* @__PURE__ */ jsx21(Field, { label: "Style", children: /* @__PURE__ */ jsx21(Segmented, { value: s.fontStyle ?? "normal", onChange: (v) => setStyle({ fontStyle: v === "normal" ? void 0 : "italic" }), options: [{ value: "normal", label: "Normal" }, { value: "italic", label: "Italic" }] }) }),
4305
- /* @__PURE__ */ jsx21(Field, { label: "Colour", children: /* @__PURE__ */ jsx21(ColorField, { value: s.color, onChange: (v) => setStyle({ color: v }), theme, allowTransparent: false, placeholder: "Inherit" }) })
4750
+ return /* @__PURE__ */ jsxs21(Fragment4, { children: [
4751
+ allowOverrides && (has("textStyle") || has("typography") || has("alignment")) ? /* @__PURE__ */ jsx22(Section, { title: "Typography", actions: hasOverrides ? /* @__PURE__ */ jsx22(Button, { size: "sm", variant: "ghost", icon: "rotate-ccw", tooltip: "Reset style overrides", onClick: () => setStyle(null) }) : null, children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4752
+ has("textStyle") ? /* @__PURE__ */ jsx22(Field, { label: "Text style", children: /* @__PURE__ */ jsx22(Select, { value: s.textStyle ?? "", onChange: (v) => setStyle({ textStyle: v || void 0 }), placeholder: "Block default", options: textStyles.map((t) => ({ value: t, label: textStyleLabel2(t) })) }) }) : null,
4753
+ has("typography") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4754
+ /* @__PURE__ */ jsx22(Field, { label: "Font", children: /* @__PURE__ */ jsx22(Select, { value: s.fontFamily ?? "", onChange: (v) => setStyle({ fontFamily: v || void 0 }), placeholder: "Inherit", options: [{ value: "$typography.headingFont", label: "Heading font" }, { value: "$typography.bodyFont", label: "Body font" }, ...(theme.fonts ?? []).map((x) => ({ value: x.family, label: x.family })), ...["Inter", "Roboto", "Georgia", "Arial", "Times New Roman"].map((x) => ({ value: x, label: x }))] }) }),
4755
+ /* @__PURE__ */ jsx22(Field, { label: "Size", children: /* @__PURE__ */ jsx22(NumberInput, { value: s.fontSize ?? null, onChange: (v) => setStyle({ fontSize: v ?? void 0 }), min: 4, max: 200, unit: "px", placeholder: "inherit" }) }),
4756
+ /* @__PURE__ */ jsx22(Field, { label: "Weight", children: /* @__PURE__ */ jsx22(Select, { value: s.fontWeight === void 0 ? "" : String(s.fontWeight), onChange: (v) => setStyle({ fontWeight: v ? Number(v) : void 0 }), placeholder: "Inherit", options: [300, 400, 500, 600, 700, 800].map((w) => ({ value: String(w), label: w === 400 ? "Regular" : w === 700 ? "Bold" : String(w) })) }) }),
4757
+ /* @__PURE__ */ jsx22(Field, { label: "Line height", children: /* @__PURE__ */ jsx22(NumberInput, { value: s.lineHeight ?? null, onChange: (v) => setStyle({ lineHeight: v ?? void 0 }), min: 0.6, max: 4, step: 0.05, placeholder: "inherit" }) }),
4758
+ /* @__PURE__ */ jsx22(Field, { label: "Spacing", children: /* @__PURE__ */ jsx22(NumberInput, { value: s.letterSpacing ?? null, onChange: (v) => setStyle({ letterSpacing: v ?? void 0 }), min: -0.2, max: 1, step: 0.01, unit: "em", placeholder: "0" }) }),
4759
+ /* @__PURE__ */ jsx22(Field, { label: "Case", children: /* @__PURE__ */ jsx22(Segmented, { value: s.textTransform ?? "none", onChange: (v) => setStyle({ textTransform: v === "none" ? void 0 : v }), options: [{ value: "none", label: "Aa" }, { value: "uppercase", label: "AA" }, { value: "lowercase", label: "aa" }, { value: "capitalize", label: "Aa Bb" }] }) }),
4760
+ /* @__PURE__ */ jsx22(Field, { label: "Style", children: /* @__PURE__ */ jsx22(Segmented, { value: s.fontStyle ?? "normal", onChange: (v) => setStyle({ fontStyle: v === "normal" ? void 0 : "italic" }), options: [{ value: "normal", label: "Normal" }, { value: "italic", label: "Italic" }] }) }),
4761
+ /* @__PURE__ */ jsx22(Field, { label: "Colour", children: /* @__PURE__ */ jsx22(ColorField, { value: s.color, onChange: (v) => setStyle({ color: v }), theme, allowTransparent: false, placeholder: "Inherit" }) })
4306
4762
  ] }) : null,
4307
- has("alignment") ? /* @__PURE__ */ jsx21(Field, { label: "Align", children: /* @__PURE__ */ jsx21(Segmented, { value: s.textAlign ?? "left", onChange: (v) => setStyle({ textAlign: v === "left" ? void 0 : v }), iconOnly: true, ariaLabel: "Text alignment", options: [{ value: "left", label: "Left", icon: "align-left" }, { value: "center", label: "Centre", icon: "align-center" }, { value: "right", label: "Right", icon: "align-right" }, { value: "justify", label: "Justify", icon: "align-justify" }] }) }) : null
4763
+ has("alignment") ? /* @__PURE__ */ jsx22(Field, { label: "Align", children: /* @__PURE__ */ jsx22(Segmented, { value: s.textAlign ?? "left", onChange: (v) => setStyle({ textAlign: v === "left" ? void 0 : v }), iconOnly: true, ariaLabel: "Text alignment", options: [{ value: "left", label: "Left", icon: "align-left" }, { value: "center", label: "Centre", icon: "align-center" }, { value: "right", label: "Right", icon: "align-right" }, { value: "justify", label: "Justify", icon: "align-justify" }] }) }) : null
4308
4764
  ] }) }) : null,
4309
- allowOverrides && (has("spacing") || has("size")) ? /* @__PURE__ */ jsx21(Section, { title: "Spacing & size", defaultOpen: false, children: /* @__PURE__ */ jsxs20("div", { className: "bpe-prop-grid", children: [
4310
- has("spacing") ? /* @__PURE__ */ jsxs20(Fragment4, { children: [
4311
- /* @__PURE__ */ jsx21(Field, { label: "Margin", stacked: true, children: /* @__PURE__ */ jsx21(SpacingGrid, { value: s.margin, onChange: (v) => setStyle({ margin: v }) }) }),
4312
- /* @__PURE__ */ jsx21(Field, { label: "Padding", stacked: true, children: /* @__PURE__ */ jsx21(SpacingGrid, { value: s.padding, onChange: (v) => setStyle({ padding: v }) }) })
4765
+ allowOverrides && (has("spacing") || has("size")) ? /* @__PURE__ */ jsx22(Section, { title: "Spacing & size", defaultOpen: false, children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4766
+ has("spacing") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4767
+ /* @__PURE__ */ jsx22(Field, { label: "Margin", stacked: true, children: /* @__PURE__ */ jsx22(SpacingGrid, { value: s.margin, onChange: (v) => setStyle({ margin: v }) }) }),
4768
+ /* @__PURE__ */ jsx22(Field, { label: "Padding", stacked: true, children: /* @__PURE__ */ jsx22(SpacingGrid, { value: s.padding, onChange: (v) => setStyle({ padding: v }) }) })
4313
4769
  ] }) : null,
4314
- has("size") ? /* @__PURE__ */ jsxs20(Fragment4, { children: [
4315
- /* @__PURE__ */ jsx21(Field, { label: "Width", children: /* @__PURE__ */ jsx21(NumberInput, { value: typeof s.width === "number" ? s.width : null, onChange: (v) => setStyle({ width: v ?? void 0 }), min: 8, max: 2e3, unit: "px", placeholder: "auto" }) }),
4316
- /* @__PURE__ */ jsx21(Field, { label: "Placement", children: /* @__PURE__ */ jsx21(Segmented, { value: s.align ?? "left", onChange: (v) => setStyle({ align: v === "left" ? void 0 : v }), options: [{ value: "left", label: "Left" }, { value: "center", label: "Centre" }, { value: "right", label: "Right" }] }) })
4770
+ has("size") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4771
+ /* @__PURE__ */ jsx22(Field, { label: "Width", children: /* @__PURE__ */ jsx22(NumberInput, { value: typeof s.width === "number" ? s.width : null, onChange: (v) => setStyle({ width: v ?? void 0 }), min: 8, max: 2e3, unit: "px", placeholder: "auto" }) }),
4772
+ /* @__PURE__ */ jsx22(Field, { label: "Placement", children: /* @__PURE__ */ jsx22(Segmented, { value: s.align ?? "left", onChange: (v) => setStyle({ align: v === "left" ? void 0 : v }), options: [{ value: "left", label: "Left" }, { value: "center", label: "Centre" }, { value: "right", label: "Right" }] }) })
4317
4773
  ] }) : null
4318
4774
  ] }) }) : null,
4319
- allowOverrides && (has("background") || has("border")) ? /* @__PURE__ */ jsx21(Section, { title: "Background & border", defaultOpen: false, children: /* @__PURE__ */ jsxs20("div", { className: "bpe-prop-grid", children: [
4320
- has("background") ? /* @__PURE__ */ jsx21(Field, { label: "Background", children: /* @__PURE__ */ jsx21(ColorField, { value: s.background, onChange: (v) => setStyle({ background: v }), theme, placeholder: "None" }) }) : null,
4321
- has("border") ? /* @__PURE__ */ jsxs20(Fragment4, { children: [
4322
- /* @__PURE__ */ jsx21(Field, { label: "Border", children: /* @__PURE__ */ jsx21(NumberInput, { value: s.border?.width ?? null, onChange: (v) => setStyle({ border: { ...s.border ?? {}, width: v ?? void 0 } }), min: 0, max: 20, unit: "px", placeholder: "0" }) }),
4323
- /* @__PURE__ */ jsx21(Field, { label: "Border colour", children: /* @__PURE__ */ jsx21(ColorField, { value: s.border?.color, onChange: (v) => setStyle({ border: { ...s.border ?? {}, color: v } }), theme, placeholder: "Theme border" }) }),
4324
- /* @__PURE__ */ jsx21(Field, { label: "Line", children: /* @__PURE__ */ jsx21(Segmented, { value: s.border?.style ?? "solid", onChange: (v) => setStyle({ border: { ...s.border ?? {}, style: v } }), options: [{ value: "solid", label: "Solid" }, { value: "dashed", label: "Dashed" }, { value: "dotted", label: "Dotted" }] }) }),
4325
- /* @__PURE__ */ jsx21(Field, { label: "Radius", children: /* @__PURE__ */ jsx21(NumberInput, { value: s.border?.radius ?? null, onChange: (v) => setStyle({ border: { ...s.border ?? {}, radius: v ?? void 0 } }), min: 0, max: 100, unit: "px", placeholder: "0" }) }),
4326
- /* @__PURE__ */ jsx21(Field, { label: "Sides", children: /* @__PURE__ */ jsx21("div", { style: { display: "flex", gap: 4 }, children: SIDES.map((side) => {
4775
+ allowOverrides && (has("background") || has("border")) ? /* @__PURE__ */ jsx22(Section, { title: "Background & border", defaultOpen: false, children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4776
+ has("background") ? /* @__PURE__ */ jsx22(Field, { label: "Background", children: /* @__PURE__ */ jsx22(ColorField, { value: s.background, onChange: (v) => setStyle({ background: v }), theme, placeholder: "None" }) }) : null,
4777
+ has("border") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4778
+ /* @__PURE__ */ jsx22(Field, { label: "Border", children: /* @__PURE__ */ jsx22(NumberInput, { value: s.border?.width ?? null, onChange: (v) => setStyle({ border: { ...s.border ?? {}, width: v ?? void 0 } }), min: 0, max: 20, unit: "px", placeholder: "0" }) }),
4779
+ /* @__PURE__ */ jsx22(Field, { label: "Border colour", children: /* @__PURE__ */ jsx22(ColorField, { value: s.border?.color, onChange: (v) => setStyle({ border: { ...s.border ?? {}, color: v } }), theme, placeholder: "Theme border" }) }),
4780
+ /* @__PURE__ */ jsx22(Field, { label: "Line", children: /* @__PURE__ */ jsx22(Segmented, { value: s.border?.style ?? "solid", onChange: (v) => setStyle({ border: { ...s.border ?? {}, style: v } }), options: [{ value: "solid", label: "Solid" }, { value: "dashed", label: "Dashed" }, { value: "dotted", label: "Dotted" }] }) }),
4781
+ /* @__PURE__ */ jsx22(Field, { label: "Radius", children: /* @__PURE__ */ jsx22(NumberInput, { value: s.border?.radius ?? null, onChange: (v) => setStyle({ border: { ...s.border ?? {}, radius: v ?? void 0 } }), min: 0, max: 100, unit: "px", placeholder: "0" }) }),
4782
+ /* @__PURE__ */ jsx22(Field, { label: "Sides", children: /* @__PURE__ */ jsx22("div", { style: { display: "flex", gap: 4 }, children: SIDES.map((side) => {
4327
4783
  const sides = s.border?.sides ?? [...SIDES];
4328
4784
  const on = sides.includes(side);
4329
- return /* @__PURE__ */ jsx21(Button, { size: "sm", active: on, onClick: () => setStyle({ border: { ...s.border ?? {}, sides: on ? sides.filter((x) => x !== side) : [...sides, side] } }), style: { flex: 1, textTransform: "capitalize" }, children: side[0].toUpperCase() }, side);
4785
+ return /* @__PURE__ */ jsx22(Button, { size: "sm", active: on, onClick: () => setStyle({ border: { ...s.border ?? {}, sides: on ? sides.filter((x) => x !== side) : [...sides, side] } }), style: { flex: 1, textTransform: "capitalize" }, children: side[0].toUpperCase() }, side);
4330
4786
  }) }) })
4331
4787
  ] }) : null,
4332
- /* @__PURE__ */ jsx21(Field, { label: "Opacity", children: /* @__PURE__ */ jsx21(NumberInput, { value: s.opacity ?? null, onChange: (v) => setStyle({ opacity: v ?? void 0 }), min: 0, max: 1, step: 0.05, placeholder: "1" }) })
4788
+ /* @__PURE__ */ jsx22(Field, { label: "Opacity", children: /* @__PURE__ */ jsx22(NumberInput, { value: s.opacity ?? null, onChange: (v) => setStyle({ opacity: v ?? void 0 }), min: 0, max: 1, step: 0.05, placeholder: "1" }) })
4333
4789
  ] }) }) : null,
4334
- has("flow") ? /* @__PURE__ */ jsx21(Section, { title: "Page flow", defaultOpen: false, icon: "file-output", children: /* @__PURE__ */ jsxs20("div", { className: "bpe-prop-grid", children: [
4335
- /* @__PURE__ */ jsx21(Field, { label: "Keep together", hint: "Never split this block across pages (unless it is taller than a page).", children: /* @__PURE__ */ jsx21(Switch, { checked: !!f.keepTogether, onChange: (v) => setFlow({ keepTogether: v || void 0 }) }) }),
4336
- /* @__PURE__ */ jsx21(Field, { label: "Keep with next", hint: "Stay on the same page as the start of the next block.", children: /* @__PURE__ */ jsx21(Switch, { checked: !!f.keepWithNext, onChange: (v) => setFlow({ keepWithNext: v || void 0 }) }) }),
4337
- /* @__PURE__ */ jsx21(Field, { label: "Break before", children: /* @__PURE__ */ jsx21(Segmented, { value: f.breakBefore ?? "auto", onChange: (v) => setFlow({ breakBefore: v === "auto" ? void 0 : "page" }), options: [{ value: "auto", label: "Auto" }, { value: "page", label: "New page" }] }) }),
4338
- /* @__PURE__ */ jsx21(Field, { label: "Break after", children: /* @__PURE__ */ jsx21(Segmented, { value: f.breakAfter ?? "auto", onChange: (v) => setFlow({ breakAfter: v === "auto" ? void 0 : "page" }), options: [{ value: "auto", label: "Auto" }, { value: "page", label: "New page" }] }) }),
4339
- /* @__PURE__ */ jsx21(Field, { label: "Min. lines before", children: /* @__PURE__ */ jsx21(NumberInput, { value: f.minOrphans ?? null, onChange: (v) => setFlow({ minOrphans: v ?? void 0 }), min: 1, max: 10, placeholder: "2" }) }),
4340
- /* @__PURE__ */ jsx21(Field, { label: "Min. lines after", children: /* @__PURE__ */ jsx21(NumberInput, { value: f.minWidows ?? null, onChange: (v) => setFlow({ minWidows: v ?? void 0 }), min: 1, max: 10, placeholder: "2" }) })
4790
+ has("flow") ? /* @__PURE__ */ jsx22(Section, { title: "Page flow", defaultOpen: false, icon: "file-output", children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4791
+ /* @__PURE__ */ jsx22(Field, { label: "Keep together", hint: "Never split this block across pages (unless it is taller than a page).", children: /* @__PURE__ */ jsx22(Switch, { checked: !!f.keepTogether, onChange: (v) => setFlow({ keepTogether: v || void 0 }) }) }),
4792
+ /* @__PURE__ */ jsx22(Field, { label: "Keep with next", hint: "Stay on the same page as the start of the next block.", children: /* @__PURE__ */ jsx22(Switch, { checked: !!f.keepWithNext, onChange: (v) => setFlow({ keepWithNext: v || void 0 }) }) }),
4793
+ /* @__PURE__ */ jsx22(Field, { label: "Break before", children: /* @__PURE__ */ jsx22(Segmented, { value: f.breakBefore ?? "auto", onChange: (v) => setFlow({ breakBefore: v === "auto" ? void 0 : "page" }), options: [{ value: "auto", label: "Auto" }, { value: "page", label: "New page" }] }) }),
4794
+ /* @__PURE__ */ jsx22(Field, { label: "Break after", children: /* @__PURE__ */ jsx22(Segmented, { value: f.breakAfter ?? "auto", onChange: (v) => setFlow({ breakAfter: v === "auto" ? void 0 : "page" }), options: [{ value: "auto", label: "Auto" }, { value: "page", label: "New page" }] }) }),
4795
+ /* @__PURE__ */ jsx22(Field, { label: "Min. lines before", children: /* @__PURE__ */ jsx22(NumberInput, { value: f.minOrphans ?? null, onChange: (v) => setFlow({ minOrphans: v ?? void 0 }), min: 1, max: 10, placeholder: "2" }) }),
4796
+ /* @__PURE__ */ jsx22(Field, { label: "Min. lines after", children: /* @__PURE__ */ jsx22(NumberInput, { value: f.minWidows ?? null, onChange: (v) => setFlow({ minWidows: v ?? void 0 }), min: 1, max: 10, placeholder: "2" }) })
4341
4797
  ] }) }) : null
4342
4798
  ] });
4343
4799
  }
4344
4800
 
4345
4801
  // src/inspector/NodeInspector.tsx
4346
- import { Fragment as Fragment5, jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
4802
+ import { Fragment as Fragment5, jsx as jsx23, jsxs as jsxs22 } from "react/jsx-runtime";
4347
4803
  function scopeVarsFor(entry, session) {
4348
4804
  const chain = ancestors2(session.store.index(), entry.node.id);
4349
- const root = createSchemaScope5(session.dataSources);
4805
+ const root = createSchemaScope7(session.dataSources);
4350
4806
  let vars = {};
4351
4807
  let scope = root;
4352
4808
  for (const e of chain) {
@@ -4376,8 +4832,8 @@ function NodeInspector({ entry, actions }) {
4376
4832
  const def = session.registry.get(node.type);
4377
4833
  const theme = session.activeTheme;
4378
4834
  const textStyles = Object.keys(session.store.template.styles.text);
4379
- const scopeVars = useMemo7(() => scopeVarsFor(entry, session), [entry, session]);
4380
- const props = useMemo7(() => ({ ...def ? typeof def.defaultProps === "function" ? def.defaultProps() : def.defaultProps : {}, ...node.props }), [def, node.props]);
4835
+ const scopeVars = useMemo8(() => scopeVarsFor(entry, session), [entry, session]);
4836
+ const props = useMemo8(() => ({ ...def ? typeof def.defaultProps === "function" ? def.defaultProps() : def.defaultProps : {}, ...node.props }), [def, node.props]);
4381
4837
  const setProp = (path, value, label2 = "Edit properties") => session.store.dispatch({ type: "setProp", id: node.id, path, value }, { label: label2, mergeKey: `prop:${node.id}:${path}` });
4382
4838
  const ctx = {
4383
4839
  dataSources: session.dataSources,
@@ -4391,53 +4847,53 @@ function NodeInspector({ entry, actions }) {
4391
4847
  };
4392
4848
  const label = nodeLabel(session, node);
4393
4849
  const issues = session.ui.get().issues.filter((i) => i.nodeId === node.id);
4394
- return /* @__PURE__ */ jsxs21(Fragment5, { children: [
4395
- /* @__PURE__ */ jsxs21("div", { className: "bpe-inspector__header", children: [
4396
- /* @__PURE__ */ jsx22("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx22(Icon, { name: def?.icon ?? "box" }) }),
4397
- /* @__PURE__ */ jsxs21("div", { style: { minWidth: 0 }, children: [
4398
- /* @__PURE__ */ jsx22("div", { className: "bpe-inspector__title", title: label, children: label }),
4399
- /* @__PURE__ */ jsxs21("div", { className: "bpe-inspector__subtitle", children: [
4850
+ return /* @__PURE__ */ jsxs22(Fragment5, { children: [
4851
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__header", children: [
4852
+ /* @__PURE__ */ jsx23("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx23(Icon, { name: def?.icon ?? "box" }) }),
4853
+ /* @__PURE__ */ jsxs22("div", { style: { minWidth: 0 }, children: [
4854
+ /* @__PURE__ */ jsx23("div", { className: "bpe-inspector__title", title: label, children: label }),
4855
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__subtitle", children: [
4400
4856
  def?.name ?? node.type,
4401
4857
  def?.license && def.license !== "community" ? ` \xB7 ${def.license}` : ""
4402
4858
  ] })
4403
4859
  ] }),
4404
- /* @__PURE__ */ jsxs21("div", { className: "bpe-inspector__actions", children: [
4405
- /* @__PURE__ */ jsx22(Button, { size: "sm", variant: "ghost", icon: node.locked ? "lock" : "unlock", tooltip: node.locked ? "Unlock" : "Lock", onClick: () => actions.toggleLock() }),
4406
- node.type !== "column" ? /* @__PURE__ */ jsx22(Button, { size: "sm", variant: "ghost", icon: "trash", tooltip: "Delete", onClick: () => actions.deleteSelection(), disabled: !!node.locked }) : null
4860
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__actions", children: [
4861
+ /* @__PURE__ */ jsx23(Button, { size: "sm", variant: "ghost", icon: node.locked ? "lock" : "unlock", tooltip: node.locked ? "Unlock" : "Lock", onClick: () => actions.toggleLock() }),
4862
+ node.type !== "column" ? /* @__PURE__ */ jsx23(Button, { size: "sm", variant: "ghost", icon: "trash", tooltip: "Delete", onClick: () => actions.deleteSelection(), disabled: !!node.locked }) : null
4407
4863
  ] })
4408
4864
  ] }),
4409
- /* @__PURE__ */ jsxs21("div", { className: "bpe-inspector__body", children: [
4410
- issues.length ? /* @__PURE__ */ jsx22("div", { style: { padding: "8px 12px 0" }, children: issues.map((i, k) => /* @__PURE__ */ jsxs21("div", { className: `bpe-issue bpe-issue--${i.severity}`, style: { padding: "4px 0" }, children: [
4411
- /* @__PURE__ */ jsx22(Icon, { name: i.severity === "error" ? "alert-circle" : i.severity === "warning" ? "alert-triangle" : "info" }),
4412
- /* @__PURE__ */ jsx22("span", { children: i.message })
4865
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__body", children: [
4866
+ issues.length ? /* @__PURE__ */ jsx23("div", { style: { padding: "8px 12px 0" }, children: issues.map((i, k) => /* @__PURE__ */ jsxs22("div", { className: `bpe-issue bpe-issue--${i.severity}`, style: { padding: "4px 0" }, children: [
4867
+ /* @__PURE__ */ jsx23(Icon, { name: i.severity === "error" ? "alert-circle" : i.severity === "warning" ? "alert-triangle" : "info" }),
4868
+ /* @__PURE__ */ jsx23("span", { children: i.message })
4413
4869
  ] }, k)) }) : null,
4414
- !def ? /* @__PURE__ */ jsxs21("div", { className: "bpe-empty", children: [
4415
- /* @__PURE__ */ jsx22(Icon, { name: "alert-triangle" }),
4416
- /* @__PURE__ */ jsxs21("div", { className: "bpe-empty__title", children: [
4870
+ !def ? /* @__PURE__ */ jsxs22("div", { className: "bpe-empty", children: [
4871
+ /* @__PURE__ */ jsx23(Icon, { name: "alert-triangle" }),
4872
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-empty__title", children: [
4417
4873
  "Unknown block \u201C",
4418
4874
  node.type,
4419
4875
  "\u201D"
4420
4876
  ] }),
4421
4877
  "This block is not registered in this editor. Its settings are preserved."
4422
4878
  ] }) : null,
4423
- node.type === "row" ? /* @__PURE__ */ jsx22(Section, { title: "Layout", children: /* @__PURE__ */ jsx22("div", { className: "bpe-prop-grid", children: /* @__PURE__ */ jsx22(Field, { label: "Columns", stacked: true, children: /* @__PURE__ */ jsx22("div", { className: "bpe-layouts__grid", style: { width: "100%" }, children: ROW_LAYOUTS2.map((l) => /* @__PURE__ */ jsxs21("button", { type: "button", className: "bpe-layout-item", "aria-pressed": JSON.stringify(l.layout) === JSON.stringify(node.props.layout), style: JSON.stringify(l.layout) === JSON.stringify(node.props.layout) ? { borderColor: "var(--bpe-accent)", background: "var(--bpe-accent-soft)" } : void 0, onClick: () => session.store.dispatch({ type: "setRowLayout", id: node.id, layout: l.layout }), children: [
4424
- /* @__PURE__ */ jsx22("div", { className: "bpe-layout-item__cols", style: { gridTemplateColumns: l.layout.map((c) => `${c}fr`).join(" ") }, children: l.layout.map((_, i) => /* @__PURE__ */ jsx22("div", { className: "bpe-layout-item__col" }, i)) }),
4425
- /* @__PURE__ */ jsx22("span", { className: "bpe-layout-item__name", children: l.label })
4879
+ node.type === "row" ? /* @__PURE__ */ jsx23(Section, { title: "Layout", children: /* @__PURE__ */ jsx23("div", { className: "bpe-prop-grid", children: /* @__PURE__ */ jsx23(Field, { label: "Columns", stacked: true, children: /* @__PURE__ */ jsx23("div", { className: "bpe-layouts__grid", style: { width: "100%" }, children: ROW_LAYOUTS2.map((l) => /* @__PURE__ */ jsxs22("button", { type: "button", className: "bpe-layout-item", "aria-pressed": JSON.stringify(l.layout) === JSON.stringify(node.props.layout), style: JSON.stringify(l.layout) === JSON.stringify(node.props.layout) ? { borderColor: "var(--bpe-accent)", background: "var(--bpe-accent-soft)" } : void 0, onClick: () => session.store.dispatch({ type: "setRowLayout", id: node.id, layout: l.layout }), children: [
4880
+ /* @__PURE__ */ jsx23("div", { className: "bpe-layout-item__cols", style: { gridTemplateColumns: l.layout.map((c) => `${c}fr`).join(" ") }, children: l.layout.map((_, i) => /* @__PURE__ */ jsx23("div", { className: "bpe-layout-item__col" }, i)) }),
4881
+ /* @__PURE__ */ jsx23("span", { className: "bpe-layout-item__name", children: l.label })
4426
4882
  ] }, l.id)) }) }) }) }) : null,
4427
- node.type === "column" ? /* @__PURE__ */ jsx22(Section, { title: "Column", children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4428
- /* @__PURE__ */ jsx22(Field, { label: "Width", hint: "Twelfths of the row. Change the row layout to resize columns.", children: /* @__PURE__ */ jsxs21(Badge, { children: [
4883
+ node.type === "column" ? /* @__PURE__ */ jsx23(Section, { title: "Column", children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4884
+ /* @__PURE__ */ jsx23(Field, { label: "Width", hint: "Twelfths of the row. Change the row layout to resize columns.", children: /* @__PURE__ */ jsxs22(Badge, { children: [
4429
4885
  String(node.props.span),
4430
4886
  " / 12"
4431
4887
  ] }) }),
4432
- /* @__PURE__ */ jsx22(Field, { label: "Row", children: /* @__PURE__ */ jsx22(Button, { size: "sm", onClick: () => actions.selectParent(), children: "Select row" }) })
4888
+ /* @__PURE__ */ jsx23(Field, { label: "Row", children: /* @__PURE__ */ jsx23(Button, { size: "sm", onClick: () => actions.selectParent(), children: "Select row" }) })
4433
4889
  ] }) }) : null,
4434
4890
  (def?.inspector ?? []).map((section, si) => {
4435
4891
  if (section.when && !section.when(props)) return null;
4436
4892
  const fields = section.fields.filter((f) => !f.when || f.when(props));
4437
4893
  if (!fields.length) return null;
4438
- return /* @__PURE__ */ jsx22(Section, { title: section.title, defaultOpen: !section.collapsed, children: /* @__PURE__ */ jsx22("div", { className: "bpe-prop-grid", children: fields.map((f) => /* @__PURE__ */ jsx22(FieldRenderer, { field: f, value: getPath(props, f.prop), onChange: (v) => setProp(f.prop, v), ctx }, f.prop)) }) }, si);
4894
+ return /* @__PURE__ */ jsx23(Section, { title: section.title, defaultOpen: !section.collapsed, children: /* @__PURE__ */ jsx23("div", { className: "bpe-prop-grid", children: fields.map((f) => /* @__PURE__ */ jsx23(FieldRenderer, { field: f, value: getPath(props, f.prop), onChange: (v) => setProp(f.prop, v), ctx }, f.prop)) }) }, si);
4439
4895
  }),
4440
- def ? /* @__PURE__ */ jsx22(
4896
+ def ? /* @__PURE__ */ jsx23(
4441
4897
  StyleGroups,
4442
4898
  {
4443
4899
  node,
@@ -4449,22 +4905,22 @@ function NodeInspector({ entry, actions }) {
4449
4905
  setFlow: (patch) => session.store.dispatch({ type: "setFlow", id: node.id, patch }, { label: "Edit page flow" })
4450
4906
  }
4451
4907
  ) : null,
4452
- node.type !== "column" ? /* @__PURE__ */ jsx22(Section, { title: "Visibility", defaultOpen: !!node.visibleWhen, icon: "eye", children: /* @__PURE__ */ jsx22("div", { className: "bpe-prop-grid", children: /* @__PURE__ */ jsx22(Field, { label: "Show when", stacked: true, hint: "Leave empty to always show this block.", children: /* @__PURE__ */ jsx22(ConditionBuilder, { value: node.visibleWhen, onChange: (v) => session.store.dispatch({ type: "setVisibleWhen", id: node.id, condition: v }, { label: "Edit visibility" }), dataSources: session.dataSources, scopeVars, allowExpression: features.expressions }) }) }) }) : null,
4453
- /* @__PURE__ */ jsx22(Section, { title: "Advanced", defaultOpen: false, icon: "settings", children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4454
- /* @__PURE__ */ jsx22(Field, { label: "Name", hint: "Shown in the layers panel and breadcrumb.", children: /* @__PURE__ */ jsx22(TextInput, { value: node.name ?? "", onChange: (v) => session.store.dispatch({ type: "setName", id: node.id, name: v || void 0 }), lazy: true, placeholder: def?.name }) }),
4455
- /* @__PURE__ */ jsx22(Field, { label: "Locked", children: /* @__PURE__ */ jsx22(Switch, { checked: !!node.locked, onChange: (v) => session.store.dispatch({ type: "setLocked", id: node.id, locked: v }) }) }),
4456
- /* @__PURE__ */ jsx22(Field, { label: "ID", children: /* @__PURE__ */ jsx22("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: node.id }) }),
4457
- /* @__PURE__ */ jsx22(Field, { label: "Type", children: /* @__PURE__ */ jsx22("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: node.type }) }),
4458
- node.type !== "section" && node.type !== "column" ? /* @__PURE__ */ jsx22(Field, { label: "Wrap in", children: /* @__PURE__ */ jsx22(Segmented, { value: "", onChange: (v) => actions.wrapSelectionIn(v), options: [{ value: "card", label: "Card" }, { value: "keepTogether", label: "Keep" }, { value: "conditional", label: "If" }] }) }) : null
4908
+ node.type !== "column" ? /* @__PURE__ */ jsx23(Section, { title: "Visibility", defaultOpen: !!node.visibleWhen, icon: "eye", children: /* @__PURE__ */ jsx23("div", { className: "bpe-prop-grid", children: /* @__PURE__ */ jsx23(Field, { label: "Show when", stacked: true, hint: "Leave empty to always show this block.", children: /* @__PURE__ */ jsx23(ConditionBuilder, { value: node.visibleWhen, onChange: (v) => session.store.dispatch({ type: "setVisibleWhen", id: node.id, condition: v }, { label: "Edit visibility" }), dataSources: session.dataSources, scopeVars, allowExpression: features.expressions }) }) }) }) : null,
4909
+ /* @__PURE__ */ jsx23(Section, { title: "Advanced", defaultOpen: false, icon: "settings", children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4910
+ /* @__PURE__ */ jsx23(Field, { label: "Name", hint: "Shown in the layers panel and breadcrumb.", children: /* @__PURE__ */ jsx23(TextInput, { value: node.name ?? "", onChange: (v) => session.store.dispatch({ type: "setName", id: node.id, name: v || void 0 }), lazy: true, placeholder: def?.name }) }),
4911
+ /* @__PURE__ */ jsx23(Field, { label: "Locked", children: /* @__PURE__ */ jsx23(Switch, { checked: !!node.locked, onChange: (v) => session.store.dispatch({ type: "setLocked", id: node.id, locked: v }) }) }),
4912
+ /* @__PURE__ */ jsx23(Field, { label: "ID", children: /* @__PURE__ */ jsx23("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: node.id }) }),
4913
+ /* @__PURE__ */ jsx23(Field, { label: "Type", children: /* @__PURE__ */ jsx23("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: node.type }) }),
4914
+ node.type !== "section" && node.type !== "column" ? /* @__PURE__ */ jsx23(Field, { label: "Wrap in", children: /* @__PURE__ */ jsx23(Segmented, { value: "", onChange: (v) => actions.wrapSelectionIn(v), options: [{ value: "card", label: "Card" }, { value: "keepTogether", label: "Keep" }, { value: "conditional", label: "If" }] }) }) : null
4459
4915
  ] }) })
4460
4916
  ] })
4461
4917
  ] });
4462
4918
  }
4463
4919
 
4464
4920
  // src/inspector/DocumentInspector.tsx
4465
- import { useState as useState16 } from "react";
4921
+ import { useState as useState18 } from "react";
4466
4922
  import { PAGE_SIZES_MM, textStyleLabel as textStyleLabel3 } from "@broadpaper/core";
4467
- import { Fragment as Fragment6, jsx as jsx23, jsxs as jsxs22 } from "react/jsx-runtime";
4923
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
4468
4924
  var SIZE_OPTIONS = [...Object.keys(PAGE_SIZES_MM).map((k) => ({ value: k, label: `${k} (${PAGE_SIZES_MM[k].width} \xD7 ${PAGE_SIZES_MM[k].height} mm)` })), { value: "custom", label: "Custom" }];
4469
4925
  function DocumentInspector() {
4470
4926
  const session = useSession();
@@ -4474,67 +4930,67 @@ function DocumentInspector() {
4474
4930
  const page = template.page;
4475
4931
  const setPage = (patch, label = "Page settings") => session.store.dispatch({ type: "setPage", patch }, { label, mergeKey: "page" });
4476
4932
  const sizeName = typeof page.size === "string" ? page.size : "custom";
4477
- const [editingStyle, setEditingStyle] = useState16(null);
4478
- return /* @__PURE__ */ jsxs22(Fragment6, { children: [
4479
- /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__header", children: [
4480
- /* @__PURE__ */ jsx23("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx23(Icon, { name: "file-text" }) }),
4481
- /* @__PURE__ */ jsxs22("div", { style: { minWidth: 0 }, children: [
4482
- /* @__PURE__ */ jsx23("div", { className: "bpe-inspector__title", children: "Document" }),
4483
- /* @__PURE__ */ jsx23("div", { className: "bpe-inspector__subtitle", children: "Page, styles and settings" })
4933
+ const [editingStyle, setEditingStyle] = useState18(null);
4934
+ return /* @__PURE__ */ jsxs23(Fragment6, { children: [
4935
+ /* @__PURE__ */ jsxs23("div", { className: "bpe-inspector__header", children: [
4936
+ /* @__PURE__ */ jsx24("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx24(Icon, { name: "file-text" }) }),
4937
+ /* @__PURE__ */ jsxs23("div", { style: { minWidth: 0 }, children: [
4938
+ /* @__PURE__ */ jsx24("div", { className: "bpe-inspector__title", children: "Document" }),
4939
+ /* @__PURE__ */ jsx24("div", { className: "bpe-inspector__subtitle", children: "Page, styles and settings" })
4484
4940
  ] })
4485
4941
  ] }),
4486
- /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__body", children: [
4487
- features.pageSettings ? /* @__PURE__ */ jsx23(Section, { title: "Page", icon: "layout", children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4488
- /* @__PURE__ */ jsx23(Field, { label: "Size", children: /* @__PURE__ */ jsx23(Select, { value: sizeName, onChange: (v) => setPage({ size: v === "custom" ? { width: 210, height: 297 } : v }), options: SIZE_OPTIONS }) }),
4489
- sizeName === "custom" && typeof page.size === "object" ? /* @__PURE__ */ jsxs22(Fragment6, { children: [
4490
- /* @__PURE__ */ jsx23(Field, { label: "Width", children: /* @__PURE__ */ jsx23(NumberInput, { value: page.size.width, onChange: (v) => setPage({ size: { width: v ?? 210, height: page.size.height } }), min: 50, max: 2e3, unit: "mm" }) }),
4491
- /* @__PURE__ */ jsx23(Field, { label: "Height", children: /* @__PURE__ */ jsx23(NumberInput, { value: page.size.height, onChange: (v) => setPage({ size: { width: page.size.width, height: v ?? 297 } }), min: 50, max: 2e3, unit: "mm" }) })
4942
+ /* @__PURE__ */ jsxs23("div", { className: "bpe-inspector__body", children: [
4943
+ features.pageSettings ? /* @__PURE__ */ jsx24(Section, { title: "Page", icon: "layout", children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4944
+ /* @__PURE__ */ jsx24(Field, { label: "Size", children: /* @__PURE__ */ jsx24(Select, { value: sizeName, onChange: (v) => setPage({ size: v === "custom" ? { width: 210, height: 297 } : v }), options: SIZE_OPTIONS }) }),
4945
+ sizeName === "custom" && typeof page.size === "object" ? /* @__PURE__ */ jsxs23(Fragment6, { children: [
4946
+ /* @__PURE__ */ jsx24(Field, { label: "Width", children: /* @__PURE__ */ jsx24(NumberInput, { value: page.size.width, onChange: (v) => setPage({ size: { width: v ?? 210, height: page.size.height } }), min: 50, max: 2e3, unit: "mm" }) }),
4947
+ /* @__PURE__ */ jsx24(Field, { label: "Height", children: /* @__PURE__ */ jsx24(NumberInput, { value: page.size.height, onChange: (v) => setPage({ size: { width: page.size.width, height: v ?? 297 } }), min: 50, max: 2e3, unit: "mm" }) })
4492
4948
  ] }) : null,
4493
- /* @__PURE__ */ jsx23(Field, { label: "Orientation", children: /* @__PURE__ */ jsx23(Segmented, { value: page.orientation, onChange: (v) => setPage({ orientation: v }), options: [{ value: "portrait", label: "Portrait", icon: "rectangle-vertical" }, { value: "landscape", label: "Landscape", icon: "rectangle-horizontal" }] }) }),
4494
- /* @__PURE__ */ jsx23(Field, { label: "Margins", stacked: true, children: /* @__PURE__ */ jsxs22("div", { style: { width: "100%" }, children: [
4495
- /* @__PURE__ */ jsx23("div", { className: "bpe-spacing-grid", children: ["top", "right", "bottom", "left"].map((s) => /* @__PURE__ */ jsx23(NumberInput, { value: page.margins[s], onChange: (v) => setPage({ margins: { ...page.margins, [s]: v ?? 0 } }), min: 0, max: 80, "aria-label": `${s} margin` }, s)) }),
4496
- /* @__PURE__ */ jsx23("div", { className: "bpe-spacing-grid", children: ["top", "right", "bottom", "left"].map((s) => /* @__PURE__ */ jsx23("div", { className: "bpe-spacing-grid__lbl", children: s }, s)) }),
4497
- /* @__PURE__ */ jsx23("div", { className: "bpe-field__hint", children: "Millimetres" })
4949
+ /* @__PURE__ */ jsx24(Field, { label: "Orientation", children: /* @__PURE__ */ jsx24(Segmented, { value: page.orientation, onChange: (v) => setPage({ orientation: v }), options: [{ value: "portrait", label: "Portrait", icon: "rectangle-vertical" }, { value: "landscape", label: "Landscape", icon: "rectangle-horizontal" }] }) }),
4950
+ /* @__PURE__ */ jsx24(Field, { label: "Margins", stacked: true, children: /* @__PURE__ */ jsxs23("div", { style: { width: "100%" }, children: [
4951
+ /* @__PURE__ */ jsx24("div", { className: "bpe-spacing-grid", children: ["top", "right", "bottom", "left"].map((s) => /* @__PURE__ */ jsx24(NumberInput, { value: page.margins[s], onChange: (v) => setPage({ margins: { ...page.margins, [s]: v ?? 0 } }), min: 0, max: 80, "aria-label": `${s} margin` }, s)) }),
4952
+ /* @__PURE__ */ jsx24("div", { className: "bpe-spacing-grid", children: ["top", "right", "bottom", "left"].map((s) => /* @__PURE__ */ jsx24("div", { className: "bpe-spacing-grid__lbl", children: s }, s)) }),
4953
+ /* @__PURE__ */ jsx24("div", { className: "bpe-field__hint", children: "Millimetres" })
4498
4954
  ] }) }),
4499
- /* @__PURE__ */ jsx23(Field, { label: "Background", children: /* @__PURE__ */ jsx23(ColorField, { value: page.background, onChange: (v) => setPage({ background: v }), theme, placeholder: "White" }) })
4955
+ /* @__PURE__ */ jsx24(Field, { label: "Background", children: /* @__PURE__ */ jsx24(ColorField, { value: page.background, onChange: (v) => setPage({ background: v }), theme, placeholder: "White" }) })
4500
4956
  ] }) }) : null,
4501
- features.headerFooter ? /* @__PURE__ */ jsx23(Section, { title: "Header & footer", icon: "panel-top", children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4502
- /* @__PURE__ */ jsx23(Field, { label: "Header height", children: /* @__PURE__ */ jsx23(NumberInput, { value: page.header.height, onChange: (v) => setPage({ header: { ...page.header, height: v ?? 0 } }), min: 0, max: 100, unit: "mm" }) }),
4503
- /* @__PURE__ */ jsx23(Field, { label: "Footer height", children: /* @__PURE__ */ jsx23(NumberInput, { value: page.footer.height, onChange: (v) => setPage({ footer: { ...page.footer, height: v ?? 0 } }), min: 0, max: 100, unit: "mm" }) }),
4504
- /* @__PURE__ */ jsx23(Field, { label: "First page header", hint: "Use a different header on page 1 (e.g. a cover).", children: /* @__PURE__ */ jsx23(Switch, { checked: page.header.differentFirstPage, onChange: (v) => session.store.dispatch({ type: "setRegionFirstPage", region: "header", enabled: v }) }) }),
4505
- /* @__PURE__ */ jsx23(Field, { label: "First page footer", children: /* @__PURE__ */ jsx23(Switch, { checked: page.footer.differentFirstPage, onChange: (v) => session.store.dispatch({ type: "setRegionFirstPage", region: "footer", enabled: v }) }) }),
4506
- /* @__PURE__ */ jsx23("div", { className: "bpe-field__hint", children: "Click inside the header or footer area on the canvas to edit its content. Use the \u201CPage number\u201D block for numbering." })
4957
+ features.headerFooter ? /* @__PURE__ */ jsx24(Section, { title: "Header & footer", icon: "panel-top", children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4958
+ /* @__PURE__ */ jsx24(Field, { label: "Header height", children: /* @__PURE__ */ jsx24(NumberInput, { value: page.header.height, onChange: (v) => setPage({ header: { ...page.header, height: v ?? 0 } }), min: 0, max: 100, unit: "mm" }) }),
4959
+ /* @__PURE__ */ jsx24(Field, { label: "Footer height", children: /* @__PURE__ */ jsx24(NumberInput, { value: page.footer.height, onChange: (v) => setPage({ footer: { ...page.footer, height: v ?? 0 } }), min: 0, max: 100, unit: "mm" }) }),
4960
+ /* @__PURE__ */ jsx24(Field, { label: "First page header", hint: "Use a different header on page 1 (e.g. a cover).", children: /* @__PURE__ */ jsx24(Switch, { checked: page.header.differentFirstPage, onChange: (v) => session.store.dispatch({ type: "setRegionFirstPage", region: "header", enabled: v }) }) }),
4961
+ /* @__PURE__ */ jsx24(Field, { label: "First page footer", children: /* @__PURE__ */ jsx24(Switch, { checked: page.footer.differentFirstPage, onChange: (v) => session.store.dispatch({ type: "setRegionFirstPage", region: "footer", enabled: v }) }) }),
4962
+ /* @__PURE__ */ jsx24("div", { className: "bpe-field__hint", children: "Click inside the header or footer area on the canvas to edit its content. Use the \u201CPage number\u201D block for numbering." })
4507
4963
  ] }) }) : null,
4508
- features.documentStyles ? /* @__PURE__ */ jsx23(Section, { title: "Text styles", icon: "type", children: editingStyle ? /* @__PURE__ */ jsx23(TextStyleEditor, { id: editingStyle, style: template.styles.text[editingStyle] ?? {}, onBack: () => setEditingStyle(null), onChange: (patch) => session.store.dispatch({ type: "setTextStyle", id: editingStyle, patch }, { label: "Document styles", mergeKey: `textstyle:${editingStyle}` }) }) : /* @__PURE__ */ jsxs22("div", { children: [
4509
- Object.entries(template.styles.text).map(([id, s]) => /* @__PURE__ */ jsxs22("button", { type: "button", className: "bpe-doc-styles__item", onClick: () => setEditingStyle(id), children: [
4510
- /* @__PURE__ */ jsx23("span", { className: "bpe-doc-styles__sample", style: { fontWeight: s.fontWeight, fontSize: Math.min(16, Math.max(10, (s.fontSize ?? 11) * 0.7)), textTransform: s.textTransform }, children: textStyleLabel3(id) }),
4511
- /* @__PURE__ */ jsxs22("span", { className: "bpe-doc-styles__id", children: [
4964
+ features.documentStyles ? /* @__PURE__ */ jsx24(Section, { title: "Text styles", icon: "type", children: editingStyle ? /* @__PURE__ */ jsx24(TextStyleEditor, { id: editingStyle, style: template.styles.text[editingStyle] ?? {}, onBack: () => setEditingStyle(null), onChange: (patch) => session.store.dispatch({ type: "setTextStyle", id: editingStyle, patch }, { label: "Document styles", mergeKey: `textstyle:${editingStyle}` }) }) : /* @__PURE__ */ jsxs23("div", { children: [
4965
+ Object.entries(template.styles.text).map(([id, s]) => /* @__PURE__ */ jsxs23("button", { type: "button", className: "bpe-doc-styles__item", onClick: () => setEditingStyle(id), children: [
4966
+ /* @__PURE__ */ jsx24("span", { className: "bpe-doc-styles__sample", style: { fontWeight: s.fontWeight, fontSize: Math.min(16, Math.max(10, (s.fontSize ?? 11) * 0.7)), textTransform: s.textTransform }, children: textStyleLabel3(id) }),
4967
+ /* @__PURE__ */ jsxs23("span", { className: "bpe-doc-styles__id", children: [
4512
4968
  id,
4513
4969
  " \xB7 ",
4514
4970
  s.fontSize ?? "\u2013",
4515
4971
  "px"
4516
4972
  ] }),
4517
- /* @__PURE__ */ jsx23(Icon, { name: "chevron-right", size: 12 })
4973
+ /* @__PURE__ */ jsx24(Icon, { name: "chevron-right", size: 12 })
4518
4974
  ] }, id)),
4519
- /* @__PURE__ */ jsx23("div", { className: "bpe-field__hint", style: { padding: "4px 12px 8px" }, children: "Changing a style updates every block that uses it." })
4975
+ /* @__PURE__ */ jsx24("div", { className: "bpe-field__hint", style: { padding: "4px 12px 8px" }, children: "Changing a style updates every block that uses it." })
4520
4976
  ] }) }) : null,
4521
- features.documentStyles ? /* @__PURE__ */ jsx23(Section, { title: "Tables & cards", icon: "table", defaultOpen: false, children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4522
- /* @__PURE__ */ jsx23(Field, { label: "Table lines", children: /* @__PURE__ */ jsx23(Select, { value: template.styles.table.borders ?? "horizontal", onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { borders: v } } }), options: [{ value: "horizontal", label: "Horizontal" }, { value: "all", label: "Grid" }, { value: "outer", label: "Outer" }, { value: "none", label: "None" }] }) }),
4523
- /* @__PURE__ */ jsx23(Field, { label: "Header fill", children: /* @__PURE__ */ jsx23(ColorField, { value: template.styles.table.headerBackground, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { headerBackground: v } } }), theme }) }),
4524
- /* @__PURE__ */ jsx23(Field, { label: "Zebra fill", children: /* @__PURE__ */ jsx23(ColorField, { value: template.styles.table.zebraBackground, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { zebraBackground: v } } }), theme }) }),
4525
- /* @__PURE__ */ jsx23(Field, { label: "Table font", children: /* @__PURE__ */ jsx23(NumberInput, { value: template.styles.table.fontSize ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { fontSize: v ?? void 0 } } }), min: 6, max: 20, unit: "px" }) }),
4526
- /* @__PURE__ */ jsx23(Field, { label: "Cell padding", children: /* @__PURE__ */ jsx23(NumberInput, { value: template.styles.table.cellPaddingY ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { cellPaddingY: v ?? void 0 } } }), min: 0, max: 30, unit: "px" }) }),
4527
- /* @__PURE__ */ jsx23(Field, { label: "Card radius", children: /* @__PURE__ */ jsx23(NumberInput, { value: template.styles.card.borderRadius ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { card: { borderRadius: v ?? void 0 } } }), min: 0, max: 40, unit: "px" }) }),
4528
- /* @__PURE__ */ jsx23(Field, { label: "Card padding", children: /* @__PURE__ */ jsx23(NumberInput, { value: template.styles.card.padding ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { card: { padding: v ?? void 0 } } }), min: 0, max: 60, unit: "px" }) }),
4529
- /* @__PURE__ */ jsx23(Field, { label: "Card shadow", children: /* @__PURE__ */ jsx23(Switch, { checked: !!template.styles.card.shadow, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { card: { shadow: v } } }) }) }),
4530
- /* @__PURE__ */ jsx23(Field, { label: "Block gap", children: /* @__PURE__ */ jsx23(NumberInput, { value: template.styles.spacing.blockGap, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { spacing: { ...template.styles.spacing, blockGap: v ?? 8 } } }), min: 0, max: 60, unit: "px" }) }),
4531
- /* @__PURE__ */ jsx23(Field, { label: "Column gap", children: /* @__PURE__ */ jsx23(NumberInput, { value: template.styles.spacing.columnGap, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { spacing: { ...template.styles.spacing, columnGap: v ?? 16 } } }), min: 0, max: 80, unit: "px" }) })
4977
+ features.documentStyles ? /* @__PURE__ */ jsx24(Section, { title: "Tables & cards", icon: "table", defaultOpen: false, children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4978
+ /* @__PURE__ */ jsx24(Field, { label: "Table lines", children: /* @__PURE__ */ jsx24(Select, { value: template.styles.table.borders ?? "horizontal", onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { borders: v } } }), options: [{ value: "horizontal", label: "Horizontal" }, { value: "all", label: "Grid" }, { value: "outer", label: "Outer" }, { value: "none", label: "None" }] }) }),
4979
+ /* @__PURE__ */ jsx24(Field, { label: "Header fill", children: /* @__PURE__ */ jsx24(ColorField, { value: template.styles.table.headerBackground, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { headerBackground: v } } }), theme }) }),
4980
+ /* @__PURE__ */ jsx24(Field, { label: "Zebra fill", children: /* @__PURE__ */ jsx24(ColorField, { value: template.styles.table.zebraBackground, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { zebraBackground: v } } }), theme }) }),
4981
+ /* @__PURE__ */ jsx24(Field, { label: "Table font", children: /* @__PURE__ */ jsx24(NumberInput, { value: template.styles.table.fontSize ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { fontSize: v ?? void 0 } } }), min: 6, max: 20, unit: "px" }) }),
4982
+ /* @__PURE__ */ jsx24(Field, { label: "Cell padding", children: /* @__PURE__ */ jsx24(NumberInput, { value: template.styles.table.cellPaddingY ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { table: { cellPaddingY: v ?? void 0 } } }), min: 0, max: 30, unit: "px" }) }),
4983
+ /* @__PURE__ */ jsx24(Field, { label: "Card radius", children: /* @__PURE__ */ jsx24(NumberInput, { value: template.styles.card.borderRadius ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { card: { borderRadius: v ?? void 0 } } }), min: 0, max: 40, unit: "px" }) }),
4984
+ /* @__PURE__ */ jsx24(Field, { label: "Card padding", children: /* @__PURE__ */ jsx24(NumberInput, { value: template.styles.card.padding ?? null, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { card: { padding: v ?? void 0 } } }), min: 0, max: 60, unit: "px" }) }),
4985
+ /* @__PURE__ */ jsx24(Field, { label: "Card shadow", children: /* @__PURE__ */ jsx24(Switch, { checked: !!template.styles.card.shadow, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { card: { shadow: v } } }) }) }),
4986
+ /* @__PURE__ */ jsx24(Field, { label: "Block gap", children: /* @__PURE__ */ jsx24(NumberInput, { value: template.styles.spacing.blockGap, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { spacing: { ...template.styles.spacing, blockGap: v ?? 8 } } }), min: 0, max: 60, unit: "px" }) }),
4987
+ /* @__PURE__ */ jsx24(Field, { label: "Column gap", children: /* @__PURE__ */ jsx24(NumberInput, { value: template.styles.spacing.columnGap, onChange: (v) => session.store.dispatch({ type: "setDocumentStyles", patch: { spacing: { ...template.styles.spacing, columnGap: v ?? 16 } } }), min: 0, max: 80, unit: "px" }) })
4532
4988
  ] }) }) : null,
4533
- /* @__PURE__ */ jsx23(Section, { title: "Settings", icon: "settings", defaultOpen: false, children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4534
- /* @__PURE__ */ jsx23(Field, { label: "Locale", hint: "Number and date formatting, e.g. en-GB, en-US, de-DE.", children: /* @__PURE__ */ jsx23(TextInput, { value: template.settings?.locale ?? "", onChange: (v) => session.store.dispatch({ type: "setSettings", patch: { locale: v || void 0 } }), lazy: true, placeholder: "en-GB" }) }),
4535
- /* @__PURE__ */ jsx23(Field, { label: "Currency", children: /* @__PURE__ */ jsx23(TextInput, { value: template.settings?.currency ?? "", onChange: (v) => session.store.dispatch({ type: "setSettings", patch: { currency: v.toUpperCase() || void 0 } }), lazy: true, placeholder: "GBP" }) }),
4536
- /* @__PURE__ */ jsx23(Field, { label: "Time zone", children: /* @__PURE__ */ jsx23(TextInput, { value: template.settings?.timeZone ?? "", onChange: (v) => session.store.dispatch({ type: "setSettings", patch: { timeZone: v || void 0 } }), lazy: true, placeholder: "Europe/London" }) }),
4537
- /* @__PURE__ */ jsx23(Field, { label: "Template ID", children: /* @__PURE__ */ jsx23("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: template.id }) })
4989
+ /* @__PURE__ */ jsx24(Section, { title: "Settings", icon: "settings", defaultOpen: false, children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4990
+ /* @__PURE__ */ jsx24(Field, { label: "Locale", hint: "Number and date formatting, e.g. en-GB, en-US, de-DE.", children: /* @__PURE__ */ jsx24(TextInput, { value: template.settings?.locale ?? "", onChange: (v) => session.store.dispatch({ type: "setSettings", patch: { locale: v || void 0 } }), lazy: true, placeholder: "en-GB" }) }),
4991
+ /* @__PURE__ */ jsx24(Field, { label: "Currency", children: /* @__PURE__ */ jsx24(TextInput, { value: template.settings?.currency ?? "", onChange: (v) => session.store.dispatch({ type: "setSettings", patch: { currency: v.toUpperCase() || void 0 } }), lazy: true, placeholder: "GBP" }) }),
4992
+ /* @__PURE__ */ jsx24(Field, { label: "Time zone", children: /* @__PURE__ */ jsx24(TextInput, { value: template.settings?.timeZone ?? "", onChange: (v) => session.store.dispatch({ type: "setSettings", patch: { timeZone: v || void 0 } }), lazy: true, placeholder: "Europe/London" }) }),
4993
+ /* @__PURE__ */ jsx24(Field, { label: "Template ID", children: /* @__PURE__ */ jsx24("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: template.id }) })
4538
4994
  ] }) })
4539
4995
  ] })
4540
4996
  ] });
@@ -4542,69 +4998,69 @@ function DocumentInspector() {
4542
4998
  function TextStyleEditor({ id, style, onChange, onBack }) {
4543
4999
  const session = useSession();
4544
5000
  const theme = session.activeTheme;
4545
- return /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4546
- /* @__PURE__ */ jsxs22("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
4547
- /* @__PURE__ */ jsx23(Button, { size: "sm", variant: "ghost", icon: "chevron-right", style: { transform: "rotate(180deg)" }, onClick: onBack, tooltip: "Back" }),
4548
- /* @__PURE__ */ jsx23("strong", { children: id })
5001
+ return /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
5002
+ /* @__PURE__ */ jsxs23("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
5003
+ /* @__PURE__ */ jsx24(Button, { size: "sm", variant: "ghost", icon: "chevron-right", style: { transform: "rotate(180deg)" }, onClick: onBack, tooltip: "Back" }),
5004
+ /* @__PURE__ */ jsx24("strong", { children: id })
4549
5005
  ] }),
4550
- /* @__PURE__ */ jsx23(Field, { label: "Font", children: /* @__PURE__ */ jsx23(Select, { value: style.fontFamily ?? "", onChange: (v) => onChange({ fontFamily: v || void 0 }), placeholder: "Theme body font", options: [{ value: "$typography.headingFont", label: "Theme heading font" }, { value: "$typography.bodyFont", label: "Theme body font" }, ...(theme.fonts ?? []).map((f) => ({ value: f.family, label: f.family }))] }) }),
4551
- /* @__PURE__ */ jsx23(Field, { label: "Size", children: /* @__PURE__ */ jsx23(NumberInput, { value: style.fontSize ?? null, onChange: (v) => onChange({ fontSize: v ?? void 0 }), min: 4, max: 120, unit: "px" }) }),
4552
- /* @__PURE__ */ jsx23(Field, { label: "Weight", children: /* @__PURE__ */ jsx23(Select, { value: style.fontWeight === void 0 ? "" : String(style.fontWeight), onChange: (v) => onChange({ fontWeight: v ? Number(v) : void 0 }), placeholder: "Regular", options: [300, 400, 500, 600, 700, 800].map((w) => ({ value: String(w), label: String(w) })) }) }),
4553
- /* @__PURE__ */ jsx23(Field, { label: "Line height", children: /* @__PURE__ */ jsx23(NumberInput, { value: style.lineHeight ?? null, onChange: (v) => onChange({ lineHeight: v ?? void 0 }), min: 0.8, max: 3, step: 0.05 }) }),
4554
- /* @__PURE__ */ jsx23(Field, { label: "Letter spacing", children: /* @__PURE__ */ jsx23(NumberInput, { value: style.letterSpacing ?? null, onChange: (v) => onChange({ letterSpacing: v ?? void 0 }), min: -0.1, max: 0.5, step: 0.01, unit: "em" }) }),
4555
- /* @__PURE__ */ jsx23(Field, { label: "Case", children: /* @__PURE__ */ jsx23(Segmented, { value: style.textTransform ?? "none", onChange: (v) => onChange({ textTransform: v === "none" ? void 0 : v }), options: [{ value: "none", label: "Aa" }, { value: "uppercase", label: "AA" }, { value: "capitalize", label: "Aa Bb" }] }) }),
4556
- /* @__PURE__ */ jsx23(Field, { label: "Colour", children: /* @__PURE__ */ jsx23(ColorField, { value: style.color, onChange: (v) => onChange({ color: v }), theme, allowTransparent: false, placeholder: "Theme text" }) }),
4557
- /* @__PURE__ */ jsx23(Field, { label: "Space above", children: /* @__PURE__ */ jsx23(NumberInput, { value: style.marginTop ?? null, onChange: (v) => onChange({ marginTop: v ?? void 0 }), min: 0, max: 100, unit: "px" }) }),
4558
- /* @__PURE__ */ jsx23(Field, { label: "Space below", children: /* @__PURE__ */ jsx23(NumberInput, { value: style.marginBottom ?? null, onChange: (v) => onChange({ marginBottom: v ?? void 0 }), min: 0, max: 100, unit: "px" }) }),
4559
- /* @__PURE__ */ jsx23(Field, { label: "Keep with next", children: /* @__PURE__ */ jsx23(Switch, { checked: !!style.keepWithNext, onChange: (v) => onChange({ keepWithNext: v || void 0 }) }) })
5006
+ /* @__PURE__ */ jsx24(Field, { label: "Font", children: /* @__PURE__ */ jsx24(Select, { value: style.fontFamily ?? "", onChange: (v) => onChange({ fontFamily: v || void 0 }), placeholder: "Theme body font", options: [{ value: "$typography.headingFont", label: "Theme heading font" }, { value: "$typography.bodyFont", label: "Theme body font" }, ...(theme.fonts ?? []).map((f) => ({ value: f.family, label: f.family }))] }) }),
5007
+ /* @__PURE__ */ jsx24(Field, { label: "Size", children: /* @__PURE__ */ jsx24(NumberInput, { value: style.fontSize ?? null, onChange: (v) => onChange({ fontSize: v ?? void 0 }), min: 4, max: 120, unit: "px" }) }),
5008
+ /* @__PURE__ */ jsx24(Field, { label: "Weight", children: /* @__PURE__ */ jsx24(Select, { value: style.fontWeight === void 0 ? "" : String(style.fontWeight), onChange: (v) => onChange({ fontWeight: v ? Number(v) : void 0 }), placeholder: "Regular", options: [300, 400, 500, 600, 700, 800].map((w) => ({ value: String(w), label: String(w) })) }) }),
5009
+ /* @__PURE__ */ jsx24(Field, { label: "Line height", children: /* @__PURE__ */ jsx24(NumberInput, { value: style.lineHeight ?? null, onChange: (v) => onChange({ lineHeight: v ?? void 0 }), min: 0.8, max: 3, step: 0.05 }) }),
5010
+ /* @__PURE__ */ jsx24(Field, { label: "Letter spacing", children: /* @__PURE__ */ jsx24(NumberInput, { value: style.letterSpacing ?? null, onChange: (v) => onChange({ letterSpacing: v ?? void 0 }), min: -0.1, max: 0.5, step: 0.01, unit: "em" }) }),
5011
+ /* @__PURE__ */ jsx24(Field, { label: "Case", children: /* @__PURE__ */ jsx24(Segmented, { value: style.textTransform ?? "none", onChange: (v) => onChange({ textTransform: v === "none" ? void 0 : v }), options: [{ value: "none", label: "Aa" }, { value: "uppercase", label: "AA" }, { value: "capitalize", label: "Aa Bb" }] }) }),
5012
+ /* @__PURE__ */ jsx24(Field, { label: "Colour", children: /* @__PURE__ */ jsx24(ColorField, { value: style.color, onChange: (v) => onChange({ color: v }), theme, allowTransparent: false, placeholder: "Theme text" }) }),
5013
+ /* @__PURE__ */ jsx24(Field, { label: "Space above", children: /* @__PURE__ */ jsx24(NumberInput, { value: style.marginTop ?? null, onChange: (v) => onChange({ marginTop: v ?? void 0 }), min: 0, max: 100, unit: "px" }) }),
5014
+ /* @__PURE__ */ jsx24(Field, { label: "Space below", children: /* @__PURE__ */ jsx24(NumberInput, { value: style.marginBottom ?? null, onChange: (v) => onChange({ marginBottom: v ?? void 0 }), min: 0, max: 100, unit: "px" }) }),
5015
+ /* @__PURE__ */ jsx24(Field, { label: "Keep with next", children: /* @__PURE__ */ jsx24(Switch, { checked: !!style.keepWithNext, onChange: (v) => onChange({ keepWithNext: v || void 0 }) }) })
4560
5016
  ] });
4561
5017
  }
4562
5018
 
4563
5019
  // src/inspector/Inspector.tsx
4564
- import { jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
5020
+ import { jsx as jsx25, jsxs as jsxs24 } from "react/jsx-runtime";
4565
5021
  function Inspector({ actions }) {
4566
5022
  const session = useSession();
4567
5023
  const selection = useEditorState((s) => s.selection);
4568
5024
  const mode = useEditorState((s) => s.mode);
4569
5025
  const entry = useNode(selection[0] ?? null);
4570
5026
  if (mode === "preview") {
4571
- return /* @__PURE__ */ jsx24("aside", { className: "bpe-inspector", "aria-label": "Properties", children: /* @__PURE__ */ jsxs23("div", { className: "bpe-empty", style: { marginTop: 40 }, children: [
4572
- /* @__PURE__ */ jsx24(Icon, { name: "eye" }),
4573
- /* @__PURE__ */ jsx24("div", { className: "bpe-empty__title", children: "Preview mode" }),
5027
+ return /* @__PURE__ */ jsx25("aside", { className: "bpe-inspector", "aria-label": "Properties", children: /* @__PURE__ */ jsxs24("div", { className: "bpe-empty", style: { marginTop: 40 }, children: [
5028
+ /* @__PURE__ */ jsx25(Icon, { name: "eye" }),
5029
+ /* @__PURE__ */ jsx25("div", { className: "bpe-empty__title", children: "Preview mode" }),
4574
5030
  "Properties are hidden while previewing. Switch back to Design to edit.",
4575
- /* @__PURE__ */ jsx24(Button, { size: "sm", onClick: () => session.setMode("design"), style: { marginTop: 8 }, children: "Back to design" })
5031
+ /* @__PURE__ */ jsx25(Button, { size: "sm", onClick: () => session.setMode("design"), style: { marginTop: 8 }, children: "Back to design" })
4576
5032
  ] }) });
4577
5033
  }
4578
5034
  if (selection.length > 1) {
4579
- return /* @__PURE__ */ jsxs23("aside", { className: "bpe-inspector", "aria-label": "Properties", children: [
4580
- /* @__PURE__ */ jsxs23("div", { className: "bpe-inspector__header", children: [
4581
- /* @__PURE__ */ jsx24("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx24(Icon, { name: "square-stack" }) }),
4582
- /* @__PURE__ */ jsxs23("div", { children: [
4583
- /* @__PURE__ */ jsxs23("div", { className: "bpe-inspector__title", children: [
5035
+ return /* @__PURE__ */ jsxs24("aside", { className: "bpe-inspector", "aria-label": "Properties", children: [
5036
+ /* @__PURE__ */ jsxs24("div", { className: "bpe-inspector__header", children: [
5037
+ /* @__PURE__ */ jsx25("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx25(Icon, { name: "square-stack" }) }),
5038
+ /* @__PURE__ */ jsxs24("div", { children: [
5039
+ /* @__PURE__ */ jsxs24("div", { className: "bpe-inspector__title", children: [
4584
5040
  selection.length,
4585
5041
  " blocks selected"
4586
5042
  ] }),
4587
- /* @__PURE__ */ jsx24("div", { className: "bpe-inspector__subtitle", children: "Actions apply to all" })
5043
+ /* @__PURE__ */ jsx25("div", { className: "bpe-inspector__subtitle", children: "Actions apply to all" })
4588
5044
  ] })
4589
5045
  ] }),
4590
- /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4591
- /* @__PURE__ */ jsx24(Button, { icon: "copy", onClick: () => actions.duplicateSelection(), children: "Duplicate all" }),
4592
- /* @__PURE__ */ jsx24(Button, { icon: "panel-top", onClick: () => actions.wrapSelectionIn("card"), children: "Wrap in card" }),
4593
- /* @__PURE__ */ jsx24(Button, { icon: "link", onClick: () => actions.wrapSelectionIn("keepTogether"), children: "Keep together" }),
4594
- /* @__PURE__ */ jsx24(Button, { icon: "trash", variant: "danger", onClick: () => actions.deleteSelection(), children: "Delete all" })
5046
+ /* @__PURE__ */ jsxs24("div", { className: "bpe-prop-grid", children: [
5047
+ /* @__PURE__ */ jsx25(Button, { icon: "copy", onClick: () => actions.duplicateSelection(), children: "Duplicate all" }),
5048
+ /* @__PURE__ */ jsx25(Button, { icon: "panel-top", onClick: () => actions.wrapSelectionIn("card"), children: "Wrap in card" }),
5049
+ /* @__PURE__ */ jsx25(Button, { icon: "link", onClick: () => actions.wrapSelectionIn("keepTogether"), children: "Keep together" }),
5050
+ /* @__PURE__ */ jsx25(Button, { icon: "trash", variant: "danger", onClick: () => actions.deleteSelection(), children: "Delete all" })
4595
5051
  ] })
4596
5052
  ] });
4597
5053
  }
4598
- return /* @__PURE__ */ jsx24("aside", { className: "bpe-inspector", "aria-label": "Properties", children: entry ? /* @__PURE__ */ jsx24(NodeInspector, { entry, actions }, entry.node.id) : /* @__PURE__ */ jsx24(DocumentInspector, {}) });
5054
+ return /* @__PURE__ */ jsx25("aside", { className: "bpe-inspector", "aria-label": "Properties", children: entry ? /* @__PURE__ */ jsx25(NodeInspector, { entry, actions }, entry.node.id) : /* @__PURE__ */ jsx25(DocumentInspector, {}) });
4599
5055
  }
4600
5056
 
4601
5057
  // src/ui/PanelResizer.tsx
4602
- import { useRef as useRef8 } from "react";
4603
- import { jsx as jsx25 } from "react/jsx-runtime";
5058
+ import { useRef as useRef9 } from "react";
5059
+ import { jsx as jsx26 } from "react/jsx-runtime";
4604
5060
  function PanelResizer({ side, width, min, max, onChange, label }) {
4605
- const start = useRef8(null);
5061
+ const start = useRef9(null);
4606
5062
  const clamp = (w) => Math.round(Math.min(max, Math.max(min, w)));
4607
- return /* @__PURE__ */ jsx25(
5063
+ return /* @__PURE__ */ jsx26(
4608
5064
  "div",
4609
5065
  {
4610
5066
  className: `bpe-resizer bpe-resizer--${side}`,
@@ -4642,7 +5098,7 @@ function PanelResizer({ side, width, min, max, onChange, label }) {
4642
5098
 
4643
5099
  // src/ui/Boundary.tsx
4644
5100
  import { Component } from "react";
4645
- import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
5101
+ import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
4646
5102
  var Boundary = class extends Component {
4647
5103
  state = { error: null };
4648
5104
  static getDerivedStateFromError(error) {
@@ -4654,48 +5110,48 @@ var Boundary = class extends Component {
4654
5110
  render() {
4655
5111
  const { error } = this.state;
4656
5112
  if (!error) return this.props.children;
4657
- return /* @__PURE__ */ jsxs24("div", { className: "bpe-boundary", role: "alert", style: this.props.slot ? { gridArea: this.props.slot } : void 0, children: [
4658
- /* @__PURE__ */ jsx26(Icon, { name: "alert-triangle", size: 16 }),
4659
- /* @__PURE__ */ jsxs24("div", { className: "bpe-boundary__body", children: [
4660
- /* @__PURE__ */ jsxs24("div", { className: "bpe-boundary__title", children: [
5113
+ return /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary", role: "alert", style: this.props.slot ? { gridArea: this.props.slot } : void 0, children: [
5114
+ /* @__PURE__ */ jsx27(Icon, { name: "alert-triangle", size: 16 }),
5115
+ /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary__body", children: [
5116
+ /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary__title", children: [
4661
5117
  this.props.area,
4662
5118
  " stopped"
4663
5119
  ] }),
4664
- /* @__PURE__ */ jsxs24("div", { className: "bpe-boundary__detail", children: [
5120
+ /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary__detail", children: [
4665
5121
  error.message || "Something went wrong.",
4666
5122
  " The rest of the designer, and your document, are unaffected."
4667
5123
  ] })
4668
5124
  ] }),
4669
- /* @__PURE__ */ jsx26(Button, { size: "sm", icon: "rotate-ccw", onClick: () => this.setState({ error: null }), children: "Try again" })
5125
+ /* @__PURE__ */ jsx27(Button, { size: "sm", icon: "rotate-ccw", onClick: () => this.setState({ error: null }), children: "Try again" })
4670
5126
  ] });
4671
5127
  }
4672
5128
  };
4673
5129
 
4674
5130
  // src/ReportDesigner.tsx
4675
- import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
5131
+ import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
4676
5132
  var NARROW_BELOW = 820;
4677
5133
  function ReportDesignerRoot({ session }) {
4678
- const actions = useMemo8(() => createActions(session), [session]);
4679
- return /* @__PURE__ */ jsx27(SessionContext.Provider, { value: session, children: /* @__PURE__ */ jsx27(Shell, { actions }) });
5134
+ const actions = useMemo9(() => createActions(session), [session]);
5135
+ return /* @__PURE__ */ jsx28(SessionContext.Provider, { value: session, children: /* @__PURE__ */ jsx28(Shell, { actions }) });
4680
5136
  }
4681
5137
  function Shell({ actions }) {
4682
5138
  const options = useOptions();
4683
- const [portal, setPortal] = useState17(null);
5139
+ const [portal, setPortal] = useState19(null);
4684
5140
  const session = useSession();
4685
5141
  const leftOpen = useUi((u) => u.leftOpen);
4686
5142
  const rightOpen = useUi((u) => u.rightOpen);
4687
5143
  const leftWidth = useUi((u) => u.leftWidth);
4688
5144
  const rightWidth = useUi((u) => u.rightWidth);
4689
5145
  const narrow = useUi((u) => u.narrow);
4690
- const rootRef = useRef9(null);
5146
+ const rootRef = useRef10(null);
4691
5147
  const width = useElementWidth(rootRef);
4692
5148
  const isNarrow = width > 0 && width < NARROW_BELOW;
4693
- useEffect7(() => {
5149
+ useEffect8(() => {
4694
5150
  if (isNarrow === narrow) return;
4695
5151
  session.ui.patch(isNarrow ? { narrow: true, leftOpen: false, rightOpen: false } : { narrow: false });
4696
5152
  }, [isNarrow, narrow, session]);
4697
5153
  const appearance = options.appearance === "system" ? typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : options.appearance ?? "light";
4698
- useEffect7(() => {
5154
+ useEffect8(() => {
4699
5155
  const onKey = (e) => {
4700
5156
  const mod = e.metaKey || e.ctrlKey;
4701
5157
  const target = e.target;
@@ -4725,18 +5181,18 @@ function Shell({ actions }) {
4725
5181
  options.className
4726
5182
  ].filter(Boolean).join(" ");
4727
5183
  const scrim = narrow && (leftOpen || rightOpen);
4728
- return /* @__PURE__ */ jsx27(PortalContext.Provider, { value: portal, children: /* @__PURE__ */ jsxs25("div", { ref: rootRef, className: cls, "data-appearance": appearance, style: { "--bpe-panel-w": `${leftWidth}px`, "--bpe-inspector-w": `${rightWidth}px` }, children: [
4729
- /* @__PURE__ */ jsx27(Boundary, { area: "The toolbar", slot: "toolbar", children: /* @__PURE__ */ jsx27(Toolbar, { actions }) }),
4730
- leftOpen ? /* @__PURE__ */ jsxs25(Fragment7, { children: [
4731
- /* @__PURE__ */ jsx27(Boundary, { area: "The insert panel", slot: "left", children: /* @__PURE__ */ jsx27(LeftPanel, { actions }) }),
4732
- !narrow ? /* @__PURE__ */ jsx27(PanelResizer, { side: "left", width: leftWidth, min: 200, max: 420, label: "Resize the insert panel", onChange: (leftWidth2) => session.ui.patch({ leftWidth: leftWidth2 }) }) : null
4733
- ] }) : /* @__PURE__ */ jsx27("div", {}),
4734
- /* @__PURE__ */ jsx27(Boundary, { area: "The canvas", slot: "canvas", children: /* @__PURE__ */ jsx27(Canvas, { actions }) }),
4735
- rightOpen ? /* @__PURE__ */ jsxs25(Fragment7, { children: [
4736
- !narrow ? /* @__PURE__ */ jsx27(PanelResizer, { side: "right", width: rightWidth, min: 260, max: 560, label: "Resize the properties panel", onChange: (rightWidth2) => session.ui.patch({ rightWidth: rightWidth2 }) }) : null,
4737
- /* @__PURE__ */ jsx27(Boundary, { area: "The properties panel", slot: "right", children: /* @__PURE__ */ jsx27(Inspector, { actions }) })
4738
- ] }) : /* @__PURE__ */ jsx27("div", {}),
4739
- scrim ? /* @__PURE__ */ jsx27(
5184
+ return /* @__PURE__ */ jsx28(PortalContext.Provider, { value: portal, children: /* @__PURE__ */ jsxs26("div", { ref: rootRef, className: cls, "data-appearance": appearance, style: { "--bpe-panel-w": `${leftWidth}px`, "--bpe-inspector-w": `${rightWidth}px` }, children: [
5185
+ /* @__PURE__ */ jsx28(Boundary, { area: "The toolbar", slot: "toolbar", children: /* @__PURE__ */ jsx28(Toolbar, { actions }) }),
5186
+ leftOpen ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
5187
+ /* @__PURE__ */ jsx28(Boundary, { area: "The insert panel", slot: "left", children: /* @__PURE__ */ jsx28(LeftPanel, { actions }) }),
5188
+ !narrow ? /* @__PURE__ */ jsx28(PanelResizer, { side: "left", width: leftWidth, min: 200, max: 420, label: "Resize the insert panel", onChange: (leftWidth2) => session.ui.patch({ leftWidth: leftWidth2 }) }) : null
5189
+ ] }) : /* @__PURE__ */ jsx28("div", {}),
5190
+ /* @__PURE__ */ jsx28(Boundary, { area: "The canvas", slot: "canvas", children: /* @__PURE__ */ jsx28(Canvas, { actions }) }),
5191
+ rightOpen ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
5192
+ !narrow ? /* @__PURE__ */ jsx28(PanelResizer, { side: "right", width: rightWidth, min: 260, max: 560, label: "Resize the properties panel", onChange: (rightWidth2) => session.ui.patch({ rightWidth: rightWidth2 }) }) : null,
5193
+ /* @__PURE__ */ jsx28(Boundary, { area: "The properties panel", slot: "right", children: /* @__PURE__ */ jsx28(Inspector, { actions }) })
5194
+ ] }) : /* @__PURE__ */ jsx28("div", {}),
5195
+ scrim ? /* @__PURE__ */ jsx28(
4740
5196
  "button",
4741
5197
  {
4742
5198
  type: "button",
@@ -4745,7 +5201,7 @@ function Shell({ actions }) {
4745
5201
  onClick: () => session.ui.patch({ leftOpen: false, rightOpen: false })
4746
5202
  }
4747
5203
  ) : null,
4748
- /* @__PURE__ */ jsx27("div", { className: "bpe-portal bpe-scope", "data-appearance": appearance, ref: setPortal })
5204
+ /* @__PURE__ */ jsx28("div", { className: "bpe-portal bpe-scope", "data-appearance": appearance, ref: setPortal })
4749
5205
  ] }) });
4750
5206
  }
4751
5207
 
@@ -4940,7 +5396,11 @@ var EDITOR_CSS = `/* BroadPaper editor design tokens.
4940
5396
  .bpe-input--invalid { border-color: var(--bpe-danger) !important; }
4941
5397
  .bpe-textarea { height: auto; min-height: 56px; padding: 6px 8px; resize: vertical; line-height: 1.45; }
4942
5398
  .bpe-select { appearance: none; padding-right: 24px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round'/></svg>"); background-repeat: no-repeat; background-position: right 8px center; }
4943
- .bpe-input-group { position: relative; display: flex; align-items: center; }
5399
+ /* \`width: 100%\`, because a flex container sizes to its content and this one holds
5400
+ an input. Without it every field with a unit or a suffix \u2014 a width in \`%\`, a
5401
+ size in \`px\` \u2014 shrank to about twenty characters and left the rest of the row
5402
+ blank, while the plain inputs beside it filled. */
5403
+ .bpe-input-group { position: relative; display: flex; align-items: center; width: 100%; }
4944
5404
  .bpe-input-group .bpe-input { padding-right: 28px; }
4945
5405
  .bpe-input-group__suffix { position: absolute; right: 8px; font-size: var(--bpe-text-xs); color: var(--bpe-fg-faint); pointer-events: none; }
4946
5406
  .bpe-input-group__btn { position: absolute; right: 2px; top: 2px; }
@@ -5288,14 +5748,49 @@ var EDITOR_CSS = `/* BroadPaper editor design tokens.
5288
5748
  .bpe-binding__btn { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; height: var(--bpe-control-h); padding: 0 8px; border: 1px solid var(--bpe-border); border-radius: var(--bpe-radius); background: var(--bpe-bg); text-align: left; font-size: var(--bpe-text-sm); }
5289
5749
  .bpe-binding__btn:hover { border-color: var(--bpe-border-strong); }
5290
5750
  .bpe-binding__btn--empty { color: var(--bpe-fg-faint); }
5751
+ /* A value the document works out. Shown, named after the fields it reads, and not
5752
+ editable where there is no expression editor to edit it with. */
5753
+ .bpe-binding__btn--locked { background: var(--bpe-bg-subtle); color: var(--bpe-fg-muted); cursor: default; }
5754
+ .bpe-binding__btn--locked:hover { border-color: var(--bpe-border); }
5291
5755
  .bpe-binding__btn svg { width: 13px; height: 13px; flex-shrink: 0; color: var(--bpe-fg-muted); }
5292
5756
  .bpe-expr { font-family: var(--bpe-font-mono); font-size: 11px; }
5293
5757
  .bpe-picker { width: 300px; max-height: 380px; display: flex; flex-direction: column; padding: 0; overflow: hidden; }
5758
+ /* Wider when it is holding the calculation builder, which is three controls on a
5759
+ row plus a field name. At 300px every one of them truncated \u2014 "a total",
5760
+ "Total of", "In\u2026" \u2014 so the panel read as an interface that had been broken
5761
+ rather than one that was cramped. The field list on the other tab is a single
5762
+ column and does not need it, but one width for the panel is better than a
5763
+ popover that changes size when you change tab. */
5764
+ .bpe-picker--wide { width: 420px; max-height: 460px; }
5294
5765
  .bpe-picker__search { padding: 8px; border-bottom: 1px solid var(--bpe-border); }
5295
5766
  .bpe-picker__list { flex: 1; overflow-y: auto; padding: 4px; }
5296
5767
  .bpe-picker__foot { display: flex; gap: 6px; align-items: center; padding: 8px; border-top: 1px solid var(--bpe-border); }
5297
5768
  .bpe-picker__foot .bpe-input { flex: 1; }
5298
5769
  .bpe-picker__group { padding: 6px 6px 2px; font-size: var(--bpe-text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--bpe-fg-faint); }
5770
+ /* The value panel's two answers: a field, or a value worked out. Shorter than the
5771
+ inspector's own tabs because these carry a word rather than a word and an icon. */
5772
+ .bpe-picker__tabs { display: flex; gap: 2px; padding: 6px 6px 0; border-bottom: 1px solid var(--bpe-border); }
5773
+ .bpe-picker__tabs .bpe-tab { height: 28px; }
5774
+
5775
+ /* The calculation builder. A column of rows, each reading as part of a sentence,
5776
+ and every control allowed to shrink \u2014 the properties panel is resizable, so
5777
+ anything in it can be squeezed and \`min-width: 0\` is what keeps the remove
5778
+ button inside the panel rather than out through its \`overflow: hidden\`. */
5779
+ .bpe-calc { display: flex; flex-direction: column; gap: 10px; padding: 10px; min-width: 0; }
5780
+ /* A step is a labelled block, not a line of controls.
5781
+ Three selects and a field name across one row is what made this unreadable:
5782
+ every control shrank to its truncation point and the sentence the builder is
5783
+ supposed to read as was unrecoverable from the abbreviations. */
5784
+ .bpe-calc__step { display: flex; flex-direction: column; gap: 4px; min-width: 0; padding: 8px; border: 1px solid var(--bpe-border); border-radius: var(--bpe-radius); background: var(--bpe-bg-subtle); }
5785
+ .bpe-calc__row { display: flex; align-items: center; gap: 6px; min-width: 0; }
5786
+ .bpe-grow { flex: 1 1 auto; }
5787
+ .bpe-calc__lead { flex: 0 0 auto; font-size: var(--bpe-text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--bpe-fg-faint); }
5788
+ .bpe-calc__row .bpe-select { flex: 1 1 auto; min-width: 0; }
5789
+ .bpe-calc__term { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
5790
+ .bpe-calc__term .bpe-binding { width: 100%; }
5791
+ .bpe-calc__term .bpe-binding__btn { flex: 1 1 auto; min-width: 0; }
5792
+ .bpe-calc__term .bpe-input { width: 100%; }
5793
+ .bpe-calc__reads { margin: 2px 0 0; padding-top: 8px; border-top: 1px solid var(--bpe-border); font-size: var(--bpe-text-xs); color: var(--bpe-fg-muted); line-height: 1.5; }
5299
5794
  .bpe-cond { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
5300
5795
  .bpe-cond__rule { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px; min-width: 0; padding: 6px; border: 1px solid var(--bpe-border); border-radius: var(--bpe-radius); background: var(--bpe-bg-subtle); }
5301
5796
  .bpe-cond__row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; min-width: 0; }
@@ -5307,6 +5802,17 @@ var EDITOR_CSS = `/* BroadPaper editor design tokens.
5307
5802
  .bpe-list-editor__head .bpe-truncate { flex: 1 1 0; min-width: 0; }
5308
5803
  .bpe-list-editor__item { min-width: 0; }
5309
5804
  .bpe-list-editor__body { padding: 6px 8px 8px; border-top: 1px solid var(--bpe-border); display: flex; flex-direction: column; gap: 6px; }
5805
+ /*
5806
+ * Inside a list item, every field is stacked.
5807
+ *
5808
+ * The two-column field \u2014 a 96px label and the control beside it \u2014 is right in the
5809
+ * panel and wrong one level in: the item is already indented and bordered, so the
5810
+ * label column eats a third of what is left and the control ends in the middle of
5811
+ * the row with the rest blank. Stacked gives the control the whole width, and it
5812
+ * matches the fields in there that were already stacked because they needed it.
5813
+ */
5814
+ .bpe-list-editor__body .bpe-field { grid-template-columns: 1fr; gap: 4px; }
5815
+ .bpe-list-editor__body .bpe-field__label { white-space: normal; overflow: visible; }
5310
5816
  .bpe-list-editor__drag { cursor: grab; color: var(--bpe-fg-faint); display: inline-flex; }
5311
5817
  .bpe-issue { display: flex; gap: 8px; align-items: flex-start; width: 100%; padding: 6px 8px; border: 0; border-radius: var(--bpe-radius-sm); background: transparent; text-align: left; font-size: var(--bpe-text-sm); color: var(--bpe-fg); }
5312
5818
  .bpe-issue:hover { background: var(--bpe-bg-muted); }