@broadpaper/editor 0.2.1 → 0.4.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",
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,
3780
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,30 @@ 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
 
4530
+ // src/inspector/font-options.ts
4531
+ function fontOptions(theme, current) {
4532
+ const first = (stack) => stack.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
4533
+ const options = [
4534
+ { value: "$typography.headingFont", label: `Heading font (${first(theme.typography.headingFont)})` },
4535
+ { value: "$typography.bodyFont", label: `Body font (${first(theme.typography.bodyFont)})` },
4536
+ ...theme.typography.monoFont ? [{ value: "$typography.monoFont", label: `Mono font (${first(theme.typography.monoFont)})` }] : []
4537
+ ];
4538
+ for (const face of theme.fonts ?? []) {
4539
+ if (!options.some((o) => o.value === face.family)) options.push({ value: face.family, label: face.family });
4540
+ }
4541
+ if (current && !options.some((o) => o.value === current)) {
4542
+ options.push({ value: current, label: `${current.startsWith("$typography.") ? current.slice("$typography.".length) : current} (not in this theme)` });
4543
+ }
4544
+ return options;
4545
+ }
4546
+
4098
4547
  // src/inspector/fields/FieldRenderer.tsx
4099
- import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
4548
+ import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
4100
4549
  function getPath(obj, path) {
4101
4550
  let cur = obj;
4102
4551
  for (const p of path.split(".")) {
@@ -4105,18 +4554,24 @@ function getPath(obj, path) {
4105
4554
  }
4106
4555
  return cur;
4107
4556
  }
4108
- 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
4557
  function FieldRenderer({ field, value, onChange, ctx }) {
4110
4558
  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 });
4559
+ const humanise = useTokenText(ctx.dataSources, ctx.scopeVars);
4560
+ 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
4561
  const str = typeof value === "string" ? value : value === void 0 || value === null ? "" : String(value);
4113
4562
  switch (field.kind) {
4114
4563
  case "text": {
4115
4564
  const tokens = field.allowTokens !== false && ctx.allowExpressions;
4565
+ if (!tokens && str.includes("{{")) {
4566
+ return wrap(
4567
+ /* @__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." }) }),
4568
+ { hint: description ?? "Fills itself in from your data." }
4569
+ );
4570
+ }
4116
4571
  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) => {
4572
+ /* @__PURE__ */ jsxs20("div", { className: "bpe-binding", children: [
4573
+ /* @__PURE__ */ jsx21(TextInput, { value: str, onChange: (v) => onChange(v), lazy: true, placeholder: field.placeholder }),
4574
+ 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
4575
  onChange(`${str}${str && !str.endsWith(" ") ? " " : ""}{{ ${expr} }}`);
4121
4576
  close();
4122
4577
  } }) }) : null
@@ -4124,41 +4579,46 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4124
4579
  );
4125
4580
  }
4126
4581
  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 });
4582
+ return wrap(/* @__PURE__ */ jsx21(TextArea, { value: str, rows: field.rows ?? 3, placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) }), { stacked: true });
4128
4583
  case "richtext": {
4129
4584
  const doc = isRichTextDoc3(value) ? value : richTextFromTemplate3(str);
4130
4585
  const plain = richTextToPlain2(doc);
4131
4586
  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." }),
4587
+ /* @__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
4588
  { stacked: true, hint: "Double-click the block on the canvas for formatting, lists and links." }
4134
4589
  );
4135
4590
  }
4136
4591
  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" }));
4592
+ 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
4593
  case "toggle":
4139
- return wrap(/* @__PURE__ */ jsx20(Switch, { checked: !!value, onChange: (v) => onChange(v), label: field.label }));
4594
+ return wrap(/* @__PURE__ */ jsx21(Switch, { checked: !!value, onChange: (v) => onChange(v), label: field.label }));
4140
4595
  case "select": {
4141
4596
  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 })) }));
4597
+ 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
4598
  }
4144
4599
  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 }));
4600
+ 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
4601
  case "color":
4147
- return wrap(/* @__PURE__ */ jsx20(ColorField, { value: str || void 0, onChange: (v) => onChange(v ?? ""), theme: ctx.theme, allowTransparent: field.allowTransparent }));
4602
+ return wrap(/* @__PURE__ */ jsx21(ColorField, { value: str || void 0, onChange: (v) => onChange(v ?? ""), theme: ctx.theme, allowTransparent: field.allowTransparent }));
4148
4603
  case "font":
4149
- return wrap(/* @__PURE__ */ jsx20(FontSelect, { value: str, onChange, theme: ctx.theme }));
4604
+ return wrap(/* @__PURE__ */ jsx21(FontSelect, { value: str, onChange, theme: ctx.theme }));
4150
4605
  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) })) }));
4606
+ 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
4607
  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 }));
4608
+ 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
4609
  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 }));
4610
+ 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
4611
  case "expression": {
4612
+ if (!ctx.allowExpressions) {
4613
+ return wrap(
4614
+ /* @__PURE__ */ jsx21(BindingField, { value: str, onChange: (v) => onChange(v), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, placeholder: field.placeholder, allowExpression: false, allowCalculation: false })
4615
+ );
4616
+ }
4157
4617
  const err = str.trim() ? checkExpressionSyntax3(str) : null;
4158
4618
  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) => {
4619
+ /* @__PURE__ */ jsxs20("div", { className: "bpe-binding", children: [
4620
+ /* @__PURE__ */ jsx21(TextInput, { value: str, onChange: (v) => onChange(v), lazy: true, mono: true, placeholder: field.placeholder, invalid: !!err }),
4621
+ /* @__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
4622
  onChange(expr);
4163
4623
  close();
4164
4624
  } }) })
@@ -4167,7 +4627,7 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4167
4627
  );
4168
4628
  }
4169
4629
  case "image":
4170
- return /* @__PURE__ */ jsx20(
4630
+ return /* @__PURE__ */ jsx21(
4171
4631
  ImageFieldRow,
4172
4632
  {
4173
4633
  field,
@@ -4178,16 +4638,28 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4178
4638
  }
4179
4639
  );
4180
4640
  case "condition":
4181
- return wrap(/* @__PURE__ */ jsx20(ConditionBuilder, { value, onChange: (v) => onChange(v ?? ""), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, allowExpression: ctx.allowExpressions }), { stacked: true });
4641
+ return wrap(/* @__PURE__ */ jsx21(ConditionBuilder, { value, onChange: (v) => onChange(v ?? ""), dataSources: ctx.dataSources, scopeVars: ctx.scopeVars, allowExpression: ctx.allowExpressions }), { stacked: true });
4182
4642
  case "columns":
4183
- return wrap(/* @__PURE__ */ jsx20(ColumnsEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars }), { stacked: true });
4643
+ return wrap(/* @__PURE__ */ jsx21(ColumnsEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars }), { stacked: true });
4184
4644
  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 });
4645
+ 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
4646
  case "keyValues":
4187
- return wrap(/* @__PURE__ */ jsx20(KeyValuesEditor, { value: Array.isArray(value) ? value : [], onChange, dataSources: ctx.dataSources, scopeVars: ctx.scopeVars }), { stacked: true });
4647
+ return wrap(
4648
+ /* @__PURE__ */ jsx21(
4649
+ KeyValuesEditor,
4650
+ {
4651
+ value: Array.isArray(value) ? value : [],
4652
+ onChange,
4653
+ dataSources: ctx.dataSources,
4654
+ scopeVars: ctx.scopeVars,
4655
+ allowExpressions: ctx.allowExpressions
4656
+ }
4657
+ ),
4658
+ { stacked: true }
4659
+ );
4188
4660
  case "list":
4189
4661
  return wrap(
4190
- /* @__PURE__ */ jsx20(
4662
+ /* @__PURE__ */ jsx21(
4191
4663
  SubFormList,
4192
4664
  {
4193
4665
  value: Array.isArray(value) ? value : [],
@@ -4195,13 +4667,13 @@ function FieldRenderer({ field, value, onChange, ctx }) {
4195
4667
  fields: field.fields,
4196
4668
  itemLabel: field.itemLabel,
4197
4669
  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 })
4670
+ renderField: (f, v, set) => /* @__PURE__ */ jsx21(FieldRenderer, { field: f, value: v, onChange: set, ctx })
4199
4671
  }
4200
4672
  ),
4201
4673
  { stacked: true }
4202
4674
  );
4203
4675
  case "custom":
4204
- return wrap(/* @__PURE__ */ jsx20(CustomFieldHost, { field, value, onChange, ctx }), { stacked: true });
4676
+ return wrap(/* @__PURE__ */ jsx21(CustomFieldHost, { field, value, onChange, ctx }), { stacked: true });
4205
4677
  }
4206
4678
  }
4207
4679
  function coerce(v, options) {
@@ -4209,18 +4681,12 @@ function coerce(v, options) {
4209
4681
  return match ? match.value : v;
4210
4682
  }
4211
4683
  function FontSelect({ value, onChange, theme }) {
4212
- const themeFonts = [
4213
- { value: "$typography.headingFont", label: `Heading font (${theme.typography.headingFont.split(",")[0]})` },
4214
- { value: "$typography.bodyFont", label: `Body font (${theme.typography.bodyFont.split(",")[0]})` },
4215
- ...theme.typography.monoFont ? [{ value: "$typography.monoFont", label: "Mono font" }] : []
4216
- ];
4217
- 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 }))] });
4684
+ return /* @__PURE__ */ jsx21(Select, { value, onChange, placeholder: "Default", options: fontOptions(theme, value) });
4219
4685
  }
4220
4686
  function CustomFieldHost({ field, value, onChange, ctx }) {
4221
4687
  const session = useSession();
4222
- const ref = useRef7(null);
4223
- const mounted = useRef7(false);
4688
+ const ref = useRef8(null);
4689
+ const mounted = useRef8(false);
4224
4690
  const api = () => ({
4225
4691
  value,
4226
4692
  props: ctx.props,
@@ -4228,10 +4694,10 @@ function CustomFieldHost({ field, value, onChange, ctx }) {
4228
4694
  setValue: onChange,
4229
4695
  setProps: ctx.setProps,
4230
4696
  dataSources: ctx.dataSources,
4231
- schemaScope: createSchemaScope4(ctx.dataSources).child(ctx.scopeVars ?? {}),
4697
+ schemaScope: createSchemaScope6(ctx.dataSources).child(ctx.scopeVars ?? {}),
4232
4698
  theme: ctx.theme
4233
4699
  });
4234
- useEffect6(() => {
4700
+ useEffect7(() => {
4235
4701
  const el = ref.current;
4236
4702
  if (!el) return;
4237
4703
  if (!mounted.current) {
@@ -4239,14 +4705,14 @@ function CustomFieldHost({ field, value, onChange, ctx }) {
4239
4705
  mounted.current = true;
4240
4706
  } else field.component.update?.(api());
4241
4707
  });
4242
- useEffect6(() => {
4708
+ useEffect7(() => {
4243
4709
  const el = ref.current;
4244
4710
  return () => {
4245
4711
  if (el && mounted.current) field.component.unmount?.(el);
4246
4712
  mounted.current = false;
4247
4713
  };
4248
4714
  }, [field.component, session]);
4249
- return /* @__PURE__ */ jsx20("div", { ref, style: { width: "100%" } });
4715
+ return /* @__PURE__ */ jsx21("div", { ref, style: { width: "100%" } });
4250
4716
  }
4251
4717
  function ImageFieldRow({
4252
4718
  field,
@@ -4255,9 +4721,9 @@ function ImageFieldRow({
4255
4721
  ctx,
4256
4722
  render
4257
4723
  }) {
4258
- const [error, setError] = useState15(null);
4724
+ const [error, setError] = useState17(null);
4259
4725
  return render(
4260
- /* @__PURE__ */ jsx20(
4726
+ /* @__PURE__ */ jsx21(
4261
4727
  ImageField,
4262
4728
  {
4263
4729
  value,
@@ -4277,13 +4743,13 @@ function ImageFieldRow({
4277
4743
 
4278
4744
  // src/inspector/StyleGroups.tsx
4279
4745
  import { textStyleLabel as textStyleLabel2 } from "@broadpaper/core";
4280
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
4746
+ import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
4281
4747
  var SIDES = ["top", "right", "bottom", "left"];
4282
4748
  function SpacingGrid({ value, onChange }) {
4283
4749
  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)) })
4750
+ return /* @__PURE__ */ jsxs21("div", { children: [
4751
+ /* @__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)) }),
4752
+ /* @__PURE__ */ jsx22("div", { className: "bpe-spacing-grid", children: SIDES.map((s) => /* @__PURE__ */ jsx22("div", { className: "bpe-spacing-grid__lbl", children: s }, s)) })
4287
4753
  ] });
4288
4754
  }
4289
4755
  function StyleGroups({ node, groups, theme, textStyles, setStyle, setFlow, allowOverrides }) {
@@ -4291,62 +4757,62 @@ function StyleGroups({ node, groups, theme, textStyles, setStyle, setFlow, allow
4291
4757
  const f = node.flow ?? {};
4292
4758
  const has = (g) => groups.includes(g);
4293
4759
  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" }) })
4760
+ return /* @__PURE__ */ jsxs21(Fragment4, { children: [
4761
+ 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: [
4762
+ 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,
4763
+ has("typography") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4764
+ /* @__PURE__ */ jsx22(Field, { label: "Font", children: /* @__PURE__ */ jsx22(Select, { value: s.fontFamily ?? "", onChange: (v) => setStyle({ fontFamily: v || void 0 }), placeholder: "Inherit", options: fontOptions(theme, s.fontFamily) }) }),
4765
+ /* @__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" }) }),
4766
+ /* @__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) })) }) }),
4767
+ /* @__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" }) }),
4768
+ /* @__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" }) }),
4769
+ /* @__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" }] }) }),
4770
+ /* @__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" }] }) }),
4771
+ /* @__PURE__ */ jsx22(Field, { label: "Colour", children: /* @__PURE__ */ jsx22(ColorField, { value: s.color, onChange: (v) => setStyle({ color: v }), theme, allowTransparent: false, placeholder: "Inherit" }) })
4306
4772
  ] }) : 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
4773
+ 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
4774
  ] }) }) : 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 }) }) })
4775
+ allowOverrides && (has("spacing") || has("size")) ? /* @__PURE__ */ jsx22(Section, { title: "Spacing & size", defaultOpen: false, children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4776
+ has("spacing") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4777
+ /* @__PURE__ */ jsx22(Field, { label: "Margin", stacked: true, children: /* @__PURE__ */ jsx22(SpacingGrid, { value: s.margin, onChange: (v) => setStyle({ margin: v }) }) }),
4778
+ /* @__PURE__ */ jsx22(Field, { label: "Padding", stacked: true, children: /* @__PURE__ */ jsx22(SpacingGrid, { value: s.padding, onChange: (v) => setStyle({ padding: v }) }) })
4313
4779
  ] }) : 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" }] }) })
4780
+ has("size") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4781
+ /* @__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" }) }),
4782
+ /* @__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
4783
  ] }) : null
4318
4784
  ] }) }) : 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) => {
4785
+ allowOverrides && (has("background") || has("border")) ? /* @__PURE__ */ jsx22(Section, { title: "Background & border", defaultOpen: false, children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4786
+ has("background") ? /* @__PURE__ */ jsx22(Field, { label: "Background", children: /* @__PURE__ */ jsx22(ColorField, { value: s.background, onChange: (v) => setStyle({ background: v }), theme, placeholder: "None" }) }) : null,
4787
+ has("border") ? /* @__PURE__ */ jsxs21(Fragment4, { children: [
4788
+ /* @__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" }) }),
4789
+ /* @__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" }) }),
4790
+ /* @__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" }] }) }),
4791
+ /* @__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" }) }),
4792
+ /* @__PURE__ */ jsx22(Field, { label: "Sides", children: /* @__PURE__ */ jsx22("div", { style: { display: "flex", gap: 4 }, children: SIDES.map((side) => {
4327
4793
  const sides = s.border?.sides ?? [...SIDES];
4328
4794
  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);
4795
+ 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
4796
  }) }) })
4331
4797
  ] }) : 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" }) })
4798
+ /* @__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
4799
  ] }) }) : 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" }) })
4800
+ has("flow") ? /* @__PURE__ */ jsx22(Section, { title: "Page flow", defaultOpen: false, icon: "file-output", children: /* @__PURE__ */ jsxs21("div", { className: "bpe-prop-grid", children: [
4801
+ /* @__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 }) }) }),
4802
+ /* @__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 }) }) }),
4803
+ /* @__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" }] }) }),
4804
+ /* @__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" }] }) }),
4805
+ /* @__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" }) }),
4806
+ /* @__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
4807
  ] }) }) : null
4342
4808
  ] });
4343
4809
  }
4344
4810
 
4345
4811
  // src/inspector/NodeInspector.tsx
4346
- import { Fragment as Fragment5, jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
4812
+ import { Fragment as Fragment5, jsx as jsx23, jsxs as jsxs22 } from "react/jsx-runtime";
4347
4813
  function scopeVarsFor(entry, session) {
4348
4814
  const chain = ancestors2(session.store.index(), entry.node.id);
4349
- const root = createSchemaScope5(session.dataSources);
4815
+ const root = createSchemaScope7(session.dataSources);
4350
4816
  let vars = {};
4351
4817
  let scope = root;
4352
4818
  for (const e of chain) {
@@ -4376,8 +4842,8 @@ function NodeInspector({ entry, actions }) {
4376
4842
  const def = session.registry.get(node.type);
4377
4843
  const theme = session.activeTheme;
4378
4844
  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]);
4845
+ const scopeVars = useMemo8(() => scopeVarsFor(entry, session), [entry, session]);
4846
+ const props = useMemo8(() => ({ ...def ? typeof def.defaultProps === "function" ? def.defaultProps() : def.defaultProps : {}, ...node.props }), [def, node.props]);
4381
4847
  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
4848
  const ctx = {
4383
4849
  dataSources: session.dataSources,
@@ -4391,53 +4857,53 @@ function NodeInspector({ entry, actions }) {
4391
4857
  };
4392
4858
  const label = nodeLabel(session, node);
4393
4859
  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: [
4860
+ return /* @__PURE__ */ jsxs22(Fragment5, { children: [
4861
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__header", children: [
4862
+ /* @__PURE__ */ jsx23("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx23(Icon, { name: def?.icon ?? "box" }) }),
4863
+ /* @__PURE__ */ jsxs22("div", { style: { minWidth: 0 }, children: [
4864
+ /* @__PURE__ */ jsx23("div", { className: "bpe-inspector__title", title: label, children: label }),
4865
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__subtitle", children: [
4400
4866
  def?.name ?? node.type,
4401
4867
  def?.license && def.license !== "community" ? ` \xB7 ${def.license}` : ""
4402
4868
  ] })
4403
4869
  ] }),
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
4870
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__actions", children: [
4871
+ /* @__PURE__ */ jsx23(Button, { size: "sm", variant: "ghost", icon: node.locked ? "lock" : "unlock", tooltip: node.locked ? "Unlock" : "Lock", onClick: () => actions.toggleLock() }),
4872
+ node.type !== "column" ? /* @__PURE__ */ jsx23(Button, { size: "sm", variant: "ghost", icon: "trash", tooltip: "Delete", onClick: () => actions.deleteSelection(), disabled: !!node.locked }) : null
4407
4873
  ] })
4408
4874
  ] }),
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 })
4875
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-inspector__body", children: [
4876
+ 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: [
4877
+ /* @__PURE__ */ jsx23(Icon, { name: i.severity === "error" ? "alert-circle" : i.severity === "warning" ? "alert-triangle" : "info" }),
4878
+ /* @__PURE__ */ jsx23("span", { children: i.message })
4413
4879
  ] }, 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: [
4880
+ !def ? /* @__PURE__ */ jsxs22("div", { className: "bpe-empty", children: [
4881
+ /* @__PURE__ */ jsx23(Icon, { name: "alert-triangle" }),
4882
+ /* @__PURE__ */ jsxs22("div", { className: "bpe-empty__title", children: [
4417
4883
  "Unknown block \u201C",
4418
4884
  node.type,
4419
4885
  "\u201D"
4420
4886
  ] }),
4421
4887
  "This block is not registered in this editor. Its settings are preserved."
4422
4888
  ] }) : 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 })
4889
+ 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: [
4890
+ /* @__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)) }),
4891
+ /* @__PURE__ */ jsx23("span", { className: "bpe-layout-item__name", children: l.label })
4426
4892
  ] }, 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: [
4893
+ node.type === "column" ? /* @__PURE__ */ jsx23(Section, { title: "Column", children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4894
+ /* @__PURE__ */ jsx23(Field, { label: "Width", hint: "Twelfths of the row. Change the row layout to resize columns.", children: /* @__PURE__ */ jsxs22(Badge, { children: [
4429
4895
  String(node.props.span),
4430
4896
  " / 12"
4431
4897
  ] }) }),
4432
- /* @__PURE__ */ jsx22(Field, { label: "Row", children: /* @__PURE__ */ jsx22(Button, { size: "sm", onClick: () => actions.selectParent(), children: "Select row" }) })
4898
+ /* @__PURE__ */ jsx23(Field, { label: "Row", children: /* @__PURE__ */ jsx23(Button, { size: "sm", onClick: () => actions.selectParent(), children: "Select row" }) })
4433
4899
  ] }) }) : null,
4434
4900
  (def?.inspector ?? []).map((section, si) => {
4435
4901
  if (section.when && !section.when(props)) return null;
4436
4902
  const fields = section.fields.filter((f) => !f.when || f.when(props));
4437
4903
  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);
4904
+ 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
4905
  }),
4440
- def ? /* @__PURE__ */ jsx22(
4906
+ def ? /* @__PURE__ */ jsx23(
4441
4907
  StyleGroups,
4442
4908
  {
4443
4909
  node,
@@ -4449,22 +4915,22 @@ function NodeInspector({ entry, actions }) {
4449
4915
  setFlow: (patch) => session.store.dispatch({ type: "setFlow", id: node.id, patch }, { label: "Edit page flow" })
4450
4916
  }
4451
4917
  ) : 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
4918
+ 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,
4919
+ /* @__PURE__ */ jsx23(Section, { title: "Advanced", defaultOpen: false, icon: "settings", children: /* @__PURE__ */ jsxs22("div", { className: "bpe-prop-grid", children: [
4920
+ /* @__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 }) }),
4921
+ /* @__PURE__ */ jsx23(Field, { label: "Locked", children: /* @__PURE__ */ jsx23(Switch, { checked: !!node.locked, onChange: (v) => session.store.dispatch({ type: "setLocked", id: node.id, locked: v }) }) }),
4922
+ /* @__PURE__ */ jsx23(Field, { label: "ID", children: /* @__PURE__ */ jsx23("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: node.id }) }),
4923
+ /* @__PURE__ */ jsx23(Field, { label: "Type", children: /* @__PURE__ */ jsx23("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: node.type }) }),
4924
+ 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
4925
  ] }) })
4460
4926
  ] })
4461
4927
  ] });
4462
4928
  }
4463
4929
 
4464
4930
  // src/inspector/DocumentInspector.tsx
4465
- import { useState as useState16 } from "react";
4931
+ import { useState as useState18 } from "react";
4466
4932
  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";
4933
+ import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
4468
4934
  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
4935
  function DocumentInspector() {
4470
4936
  const session = useSession();
@@ -4474,67 +4940,67 @@ function DocumentInspector() {
4474
4940
  const page = template.page;
4475
4941
  const setPage = (patch, label = "Page settings") => session.store.dispatch({ type: "setPage", patch }, { label, mergeKey: "page" });
4476
4942
  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" })
4943
+ const [editingStyle, setEditingStyle] = useState18(null);
4944
+ return /* @__PURE__ */ jsxs23(Fragment6, { children: [
4945
+ /* @__PURE__ */ jsxs23("div", { className: "bpe-inspector__header", children: [
4946
+ /* @__PURE__ */ jsx24("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx24(Icon, { name: "file-text" }) }),
4947
+ /* @__PURE__ */ jsxs23("div", { style: { minWidth: 0 }, children: [
4948
+ /* @__PURE__ */ jsx24("div", { className: "bpe-inspector__title", children: "Document" }),
4949
+ /* @__PURE__ */ jsx24("div", { className: "bpe-inspector__subtitle", children: "Page, styles and settings" })
4484
4950
  ] })
4485
4951
  ] }),
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" }) })
4952
+ /* @__PURE__ */ jsxs23("div", { className: "bpe-inspector__body", children: [
4953
+ features.pageSettings ? /* @__PURE__ */ jsx24(Section, { title: "Page", icon: "layout", children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4954
+ /* @__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 }) }),
4955
+ sizeName === "custom" && typeof page.size === "object" ? /* @__PURE__ */ jsxs23(Fragment6, { children: [
4956
+ /* @__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" }) }),
4957
+ /* @__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
4958
  ] }) : 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" })
4959
+ /* @__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" }] }) }),
4960
+ /* @__PURE__ */ jsx24(Field, { label: "Margins", stacked: true, children: /* @__PURE__ */ jsxs23("div", { style: { width: "100%" }, children: [
4961
+ /* @__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)) }),
4962
+ /* @__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)) }),
4963
+ /* @__PURE__ */ jsx24("div", { className: "bpe-field__hint", children: "Millimetres" })
4498
4964
  ] }) }),
4499
- /* @__PURE__ */ jsx23(Field, { label: "Background", children: /* @__PURE__ */ jsx23(ColorField, { value: page.background, onChange: (v) => setPage({ background: v }), theme, placeholder: "White" }) })
4965
+ /* @__PURE__ */ jsx24(Field, { label: "Background", children: /* @__PURE__ */ jsx24(ColorField, { value: page.background, onChange: (v) => setPage({ background: v }), theme, placeholder: "White" }) })
4500
4966
  ] }) }) : 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." })
4967
+ features.headerFooter ? /* @__PURE__ */ jsx24(Section, { title: "Header & footer", icon: "panel-top", children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4968
+ /* @__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" }) }),
4969
+ /* @__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" }) }),
4970
+ /* @__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 }) }) }),
4971
+ /* @__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 }) }) }),
4972
+ /* @__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
4973
  ] }) }) : 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: [
4974
+ 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: [
4975
+ Object.entries(template.styles.text).map(([id, s]) => /* @__PURE__ */ jsxs23("button", { type: "button", className: "bpe-doc-styles__item", onClick: () => setEditingStyle(id), children: [
4976
+ /* @__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) }),
4977
+ /* @__PURE__ */ jsxs23("span", { className: "bpe-doc-styles__id", children: [
4512
4978
  id,
4513
4979
  " \xB7 ",
4514
4980
  s.fontSize ?? "\u2013",
4515
4981
  "px"
4516
4982
  ] }),
4517
- /* @__PURE__ */ jsx23(Icon, { name: "chevron-right", size: 12 })
4983
+ /* @__PURE__ */ jsx24(Icon, { name: "chevron-right", size: 12 })
4518
4984
  ] }, id)),
4519
- /* @__PURE__ */ jsx23("div", { className: "bpe-field__hint", style: { padding: "4px 12px 8px" }, children: "Changing a style updates every block that uses it." })
4985
+ /* @__PURE__ */ jsx24("div", { className: "bpe-field__hint", style: { padding: "4px 12px 8px" }, children: "Changing a style updates every block that uses it." })
4520
4986
  ] }) }) : 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" }) })
4987
+ features.documentStyles ? /* @__PURE__ */ jsx24(Section, { title: "Tables & cards", icon: "table", defaultOpen: false, children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
4988
+ /* @__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" }] }) }),
4989
+ /* @__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 }) }),
4990
+ /* @__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 }) }),
4991
+ /* @__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" }) }),
4992
+ /* @__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" }) }),
4993
+ /* @__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" }) }),
4994
+ /* @__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" }) }),
4995
+ /* @__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 } } }) }) }),
4996
+ /* @__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" }) }),
4997
+ /* @__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
4998
  ] }) }) : 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 }) })
4999
+ /* @__PURE__ */ jsx24(Section, { title: "Settings", icon: "settings", defaultOpen: false, children: /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
5000
+ /* @__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" }) }),
5001
+ /* @__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" }) }),
5002
+ /* @__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" }) }),
5003
+ /* @__PURE__ */ jsx24(Field, { label: "Template ID", children: /* @__PURE__ */ jsx24("code", { className: "bpe-mono bpe-muted", style: { fontSize: 10 }, children: template.id }) })
4538
5004
  ] }) })
4539
5005
  ] })
4540
5006
  ] });
@@ -4542,69 +5008,69 @@ function DocumentInspector() {
4542
5008
  function TextStyleEditor({ id, style, onChange, onBack }) {
4543
5009
  const session = useSession();
4544
5010
  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 })
5011
+ return /* @__PURE__ */ jsxs23("div", { className: "bpe-prop-grid", children: [
5012
+ /* @__PURE__ */ jsxs23("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
5013
+ /* @__PURE__ */ jsx24(Button, { size: "sm", variant: "ghost", icon: "chevron-right", style: { transform: "rotate(180deg)" }, onClick: onBack, tooltip: "Back" }),
5014
+ /* @__PURE__ */ jsx24("strong", { children: id })
4549
5015
  ] }),
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 }) }) })
5016
+ /* @__PURE__ */ jsx24(Field, { label: "Font", children: /* @__PURE__ */ jsx24(Select, { value: style.fontFamily ?? "", onChange: (v) => onChange({ fontFamily: v || void 0 }), placeholder: "Theme body font", options: fontOptions(theme, style.fontFamily) }) }),
5017
+ /* @__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" }) }),
5018
+ /* @__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) })) }) }),
5019
+ /* @__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 }) }),
5020
+ /* @__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" }) }),
5021
+ /* @__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" }] }) }),
5022
+ /* @__PURE__ */ jsx24(Field, { label: "Colour", children: /* @__PURE__ */ jsx24(ColorField, { value: style.color, onChange: (v) => onChange({ color: v }), theme, allowTransparent: false, placeholder: "Theme text" }) }),
5023
+ /* @__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" }) }),
5024
+ /* @__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" }) }),
5025
+ /* @__PURE__ */ jsx24(Field, { label: "Keep with next", children: /* @__PURE__ */ jsx24(Switch, { checked: !!style.keepWithNext, onChange: (v) => onChange({ keepWithNext: v || void 0 }) }) })
4560
5026
  ] });
4561
5027
  }
4562
5028
 
4563
5029
  // src/inspector/Inspector.tsx
4564
- import { jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
5030
+ import { jsx as jsx25, jsxs as jsxs24 } from "react/jsx-runtime";
4565
5031
  function Inspector({ actions }) {
4566
5032
  const session = useSession();
4567
5033
  const selection = useEditorState((s) => s.selection);
4568
5034
  const mode = useEditorState((s) => s.mode);
4569
5035
  const entry = useNode(selection[0] ?? null);
4570
5036
  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" }),
5037
+ return /* @__PURE__ */ jsx25("aside", { className: "bpe-inspector", "aria-label": "Properties", children: /* @__PURE__ */ jsxs24("div", { className: "bpe-empty", style: { marginTop: 40 }, children: [
5038
+ /* @__PURE__ */ jsx25(Icon, { name: "eye" }),
5039
+ /* @__PURE__ */ jsx25("div", { className: "bpe-empty__title", children: "Preview mode" }),
4574
5040
  "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" })
5041
+ /* @__PURE__ */ jsx25(Button, { size: "sm", onClick: () => session.setMode("design"), style: { marginTop: 8 }, children: "Back to design" })
4576
5042
  ] }) });
4577
5043
  }
4578
5044
  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: [
5045
+ return /* @__PURE__ */ jsxs24("aside", { className: "bpe-inspector", "aria-label": "Properties", children: [
5046
+ /* @__PURE__ */ jsxs24("div", { className: "bpe-inspector__header", children: [
5047
+ /* @__PURE__ */ jsx25("span", { className: "bpe-inspector__icon", children: /* @__PURE__ */ jsx25(Icon, { name: "square-stack" }) }),
5048
+ /* @__PURE__ */ jsxs24("div", { children: [
5049
+ /* @__PURE__ */ jsxs24("div", { className: "bpe-inspector__title", children: [
4584
5050
  selection.length,
4585
5051
  " blocks selected"
4586
5052
  ] }),
4587
- /* @__PURE__ */ jsx24("div", { className: "bpe-inspector__subtitle", children: "Actions apply to all" })
5053
+ /* @__PURE__ */ jsx25("div", { className: "bpe-inspector__subtitle", children: "Actions apply to all" })
4588
5054
  ] })
4589
5055
  ] }),
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" })
5056
+ /* @__PURE__ */ jsxs24("div", { className: "bpe-prop-grid", children: [
5057
+ /* @__PURE__ */ jsx25(Button, { icon: "copy", onClick: () => actions.duplicateSelection(), children: "Duplicate all" }),
5058
+ /* @__PURE__ */ jsx25(Button, { icon: "panel-top", onClick: () => actions.wrapSelectionIn("card"), children: "Wrap in card" }),
5059
+ /* @__PURE__ */ jsx25(Button, { icon: "link", onClick: () => actions.wrapSelectionIn("keepTogether"), children: "Keep together" }),
5060
+ /* @__PURE__ */ jsx25(Button, { icon: "trash", variant: "danger", onClick: () => actions.deleteSelection(), children: "Delete all" })
4595
5061
  ] })
4596
5062
  ] });
4597
5063
  }
4598
- return /* @__PURE__ */ jsx24("aside", { className: "bpe-inspector", "aria-label": "Properties", children: entry ? /* @__PURE__ */ jsx24(NodeInspector, { entry, actions }, entry.node.id) : /* @__PURE__ */ jsx24(DocumentInspector, {}) });
5064
+ return /* @__PURE__ */ jsx25("aside", { className: "bpe-inspector", "aria-label": "Properties", children: entry ? /* @__PURE__ */ jsx25(NodeInspector, { entry, actions }, entry.node.id) : /* @__PURE__ */ jsx25(DocumentInspector, {}) });
4599
5065
  }
4600
5066
 
4601
5067
  // src/ui/PanelResizer.tsx
4602
- import { useRef as useRef8 } from "react";
4603
- import { jsx as jsx25 } from "react/jsx-runtime";
5068
+ import { useRef as useRef9 } from "react";
5069
+ import { jsx as jsx26 } from "react/jsx-runtime";
4604
5070
  function PanelResizer({ side, width, min, max, onChange, label }) {
4605
- const start = useRef8(null);
5071
+ const start = useRef9(null);
4606
5072
  const clamp = (w) => Math.round(Math.min(max, Math.max(min, w)));
4607
- return /* @__PURE__ */ jsx25(
5073
+ return /* @__PURE__ */ jsx26(
4608
5074
  "div",
4609
5075
  {
4610
5076
  className: `bpe-resizer bpe-resizer--${side}`,
@@ -4642,7 +5108,7 @@ function PanelResizer({ side, width, min, max, onChange, label }) {
4642
5108
 
4643
5109
  // src/ui/Boundary.tsx
4644
5110
  import { Component } from "react";
4645
- import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
5111
+ import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
4646
5112
  var Boundary = class extends Component {
4647
5113
  state = { error: null };
4648
5114
  static getDerivedStateFromError(error) {
@@ -4654,48 +5120,48 @@ var Boundary = class extends Component {
4654
5120
  render() {
4655
5121
  const { error } = this.state;
4656
5122
  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: [
5123
+ return /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary", role: "alert", style: this.props.slot ? { gridArea: this.props.slot } : void 0, children: [
5124
+ /* @__PURE__ */ jsx27(Icon, { name: "alert-triangle", size: 16 }),
5125
+ /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary__body", children: [
5126
+ /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary__title", children: [
4661
5127
  this.props.area,
4662
5128
  " stopped"
4663
5129
  ] }),
4664
- /* @__PURE__ */ jsxs24("div", { className: "bpe-boundary__detail", children: [
5130
+ /* @__PURE__ */ jsxs25("div", { className: "bpe-boundary__detail", children: [
4665
5131
  error.message || "Something went wrong.",
4666
5132
  " The rest of the designer, and your document, are unaffected."
4667
5133
  ] })
4668
5134
  ] }),
4669
- /* @__PURE__ */ jsx26(Button, { size: "sm", icon: "rotate-ccw", onClick: () => this.setState({ error: null }), children: "Try again" })
5135
+ /* @__PURE__ */ jsx27(Button, { size: "sm", icon: "rotate-ccw", onClick: () => this.setState({ error: null }), children: "Try again" })
4670
5136
  ] });
4671
5137
  }
4672
5138
  };
4673
5139
 
4674
5140
  // src/ReportDesigner.tsx
4675
- import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
5141
+ import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
4676
5142
  var NARROW_BELOW = 820;
4677
5143
  function ReportDesignerRoot({ session }) {
4678
- const actions = useMemo8(() => createActions(session), [session]);
4679
- return /* @__PURE__ */ jsx27(SessionContext.Provider, { value: session, children: /* @__PURE__ */ jsx27(Shell, { actions }) });
5144
+ const actions = useMemo9(() => createActions(session), [session]);
5145
+ return /* @__PURE__ */ jsx28(SessionContext.Provider, { value: session, children: /* @__PURE__ */ jsx28(Shell, { actions }) });
4680
5146
  }
4681
5147
  function Shell({ actions }) {
4682
5148
  const options = useOptions();
4683
- const [portal, setPortal] = useState17(null);
5149
+ const [portal, setPortal] = useState19(null);
4684
5150
  const session = useSession();
4685
5151
  const leftOpen = useUi((u) => u.leftOpen);
4686
5152
  const rightOpen = useUi((u) => u.rightOpen);
4687
5153
  const leftWidth = useUi((u) => u.leftWidth);
4688
5154
  const rightWidth = useUi((u) => u.rightWidth);
4689
5155
  const narrow = useUi((u) => u.narrow);
4690
- const rootRef = useRef9(null);
5156
+ const rootRef = useRef10(null);
4691
5157
  const width = useElementWidth(rootRef);
4692
5158
  const isNarrow = width > 0 && width < NARROW_BELOW;
4693
- useEffect7(() => {
5159
+ useEffect8(() => {
4694
5160
  if (isNarrow === narrow) return;
4695
5161
  session.ui.patch(isNarrow ? { narrow: true, leftOpen: false, rightOpen: false } : { narrow: false });
4696
5162
  }, [isNarrow, narrow, session]);
4697
5163
  const appearance = options.appearance === "system" ? typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : options.appearance ?? "light";
4698
- useEffect7(() => {
5164
+ useEffect8(() => {
4699
5165
  const onKey = (e) => {
4700
5166
  const mod = e.metaKey || e.ctrlKey;
4701
5167
  const target = e.target;
@@ -4725,18 +5191,18 @@ function Shell({ actions }) {
4725
5191
  options.className
4726
5192
  ].filter(Boolean).join(" ");
4727
5193
  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(
5194
+ 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: [
5195
+ /* @__PURE__ */ jsx28(Boundary, { area: "The toolbar", slot: "toolbar", children: /* @__PURE__ */ jsx28(Toolbar, { actions }) }),
5196
+ leftOpen ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
5197
+ /* @__PURE__ */ jsx28(Boundary, { area: "The insert panel", slot: "left", children: /* @__PURE__ */ jsx28(LeftPanel, { actions }) }),
5198
+ !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
5199
+ ] }) : /* @__PURE__ */ jsx28("div", {}),
5200
+ /* @__PURE__ */ jsx28(Boundary, { area: "The canvas", slot: "canvas", children: /* @__PURE__ */ jsx28(Canvas, { actions }) }),
5201
+ rightOpen ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
5202
+ !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,
5203
+ /* @__PURE__ */ jsx28(Boundary, { area: "The properties panel", slot: "right", children: /* @__PURE__ */ jsx28(Inspector, { actions }) })
5204
+ ] }) : /* @__PURE__ */ jsx28("div", {}),
5205
+ scrim ? /* @__PURE__ */ jsx28(
4740
5206
  "button",
4741
5207
  {
4742
5208
  type: "button",
@@ -4745,7 +5211,7 @@ function Shell({ actions }) {
4745
5211
  onClick: () => session.ui.patch({ leftOpen: false, rightOpen: false })
4746
5212
  }
4747
5213
  ) : null,
4748
- /* @__PURE__ */ jsx27("div", { className: "bpe-portal bpe-scope", "data-appearance": appearance, ref: setPortal })
5214
+ /* @__PURE__ */ jsx28("div", { className: "bpe-portal bpe-scope", "data-appearance": appearance, ref: setPortal })
4749
5215
  ] }) });
4750
5216
  }
4751
5217
 
@@ -4940,7 +5406,11 @@ var EDITOR_CSS = `/* BroadPaper editor design tokens.
4940
5406
  .bpe-input--invalid { border-color: var(--bpe-danger) !important; }
4941
5407
  .bpe-textarea { height: auto; min-height: 56px; padding: 6px 8px; resize: vertical; line-height: 1.45; }
4942
5408
  .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; }
5409
+ /* \`width: 100%\`, because a flex container sizes to its content and this one holds
5410
+ an input. Without it every field with a unit or a suffix \u2014 a width in \`%\`, a
5411
+ size in \`px\` \u2014 shrank to about twenty characters and left the rest of the row
5412
+ blank, while the plain inputs beside it filled. */
5413
+ .bpe-input-group { position: relative; display: flex; align-items: center; width: 100%; }
4944
5414
  .bpe-input-group .bpe-input { padding-right: 28px; }
4945
5415
  .bpe-input-group__suffix { position: absolute; right: 8px; font-size: var(--bpe-text-xs); color: var(--bpe-fg-faint); pointer-events: none; }
4946
5416
  .bpe-input-group__btn { position: absolute; right: 2px; top: 2px; }
@@ -5288,14 +5758,49 @@ var EDITOR_CSS = `/* BroadPaper editor design tokens.
5288
5758
  .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
5759
  .bpe-binding__btn:hover { border-color: var(--bpe-border-strong); }
5290
5760
  .bpe-binding__btn--empty { color: var(--bpe-fg-faint); }
5761
+ /* A value the document works out. Shown, named after the fields it reads, and not
5762
+ editable where there is no expression editor to edit it with. */
5763
+ .bpe-binding__btn--locked { background: var(--bpe-bg-subtle); color: var(--bpe-fg-muted); cursor: default; }
5764
+ .bpe-binding__btn--locked:hover { border-color: var(--bpe-border); }
5291
5765
  .bpe-binding__btn svg { width: 13px; height: 13px; flex-shrink: 0; color: var(--bpe-fg-muted); }
5292
5766
  .bpe-expr { font-family: var(--bpe-font-mono); font-size: 11px; }
5293
5767
  .bpe-picker { width: 300px; max-height: 380px; display: flex; flex-direction: column; padding: 0; overflow: hidden; }
5768
+ /* Wider when it is holding the calculation builder, which is three controls on a
5769
+ row plus a field name. At 300px every one of them truncated \u2014 "a total",
5770
+ "Total of", "In\u2026" \u2014 so the panel read as an interface that had been broken
5771
+ rather than one that was cramped. The field list on the other tab is a single
5772
+ column and does not need it, but one width for the panel is better than a
5773
+ popover that changes size when you change tab. */
5774
+ .bpe-picker--wide { width: 420px; max-height: 460px; }
5294
5775
  .bpe-picker__search { padding: 8px; border-bottom: 1px solid var(--bpe-border); }
5295
5776
  .bpe-picker__list { flex: 1; overflow-y: auto; padding: 4px; }
5296
5777
  .bpe-picker__foot { display: flex; gap: 6px; align-items: center; padding: 8px; border-top: 1px solid var(--bpe-border); }
5297
5778
  .bpe-picker__foot .bpe-input { flex: 1; }
5298
5779
  .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); }
5780
+ /* The value panel's two answers: a field, or a value worked out. Shorter than the
5781
+ inspector's own tabs because these carry a word rather than a word and an icon. */
5782
+ .bpe-picker__tabs { display: flex; gap: 2px; padding: 6px 6px 0; border-bottom: 1px solid var(--bpe-border); }
5783
+ .bpe-picker__tabs .bpe-tab { height: 28px; }
5784
+
5785
+ /* The calculation builder. A column of rows, each reading as part of a sentence,
5786
+ and every control allowed to shrink \u2014 the properties panel is resizable, so
5787
+ anything in it can be squeezed and \`min-width: 0\` is what keeps the remove
5788
+ button inside the panel rather than out through its \`overflow: hidden\`. */
5789
+ .bpe-calc { display: flex; flex-direction: column; gap: 10px; padding: 10px; min-width: 0; }
5790
+ /* A step is a labelled block, not a line of controls.
5791
+ Three selects and a field name across one row is what made this unreadable:
5792
+ every control shrank to its truncation point and the sentence the builder is
5793
+ supposed to read as was unrecoverable from the abbreviations. */
5794
+ .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); }
5795
+ .bpe-calc__row { display: flex; align-items: center; gap: 6px; min-width: 0; }
5796
+ .bpe-grow { flex: 1 1 auto; }
5797
+ .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); }
5798
+ .bpe-calc__row .bpe-select { flex: 1 1 auto; min-width: 0; }
5799
+ .bpe-calc__term { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
5800
+ .bpe-calc__term .bpe-binding { width: 100%; }
5801
+ .bpe-calc__term .bpe-binding__btn { flex: 1 1 auto; min-width: 0; }
5802
+ .bpe-calc__term .bpe-input { width: 100%; }
5803
+ .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
5804
  .bpe-cond { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
5300
5805
  .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
5806
  .bpe-cond__row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; min-width: 0; }
@@ -5307,6 +5812,17 @@ var EDITOR_CSS = `/* BroadPaper editor design tokens.
5307
5812
  .bpe-list-editor__head .bpe-truncate { flex: 1 1 0; min-width: 0; }
5308
5813
  .bpe-list-editor__item { min-width: 0; }
5309
5814
  .bpe-list-editor__body { padding: 6px 8px 8px; border-top: 1px solid var(--bpe-border); display: flex; flex-direction: column; gap: 6px; }
5815
+ /*
5816
+ * Inside a list item, every field is stacked.
5817
+ *
5818
+ * The two-column field \u2014 a 96px label and the control beside it \u2014 is right in the
5819
+ * panel and wrong one level in: the item is already indented and bordered, so the
5820
+ * label column eats a third of what is left and the control ends in the middle of
5821
+ * the row with the rest blank. Stacked gives the control the whole width, and it
5822
+ * matches the fields in there that were already stacked because they needed it.
5823
+ */
5824
+ .bpe-list-editor__body .bpe-field { grid-template-columns: 1fr; gap: 4px; }
5825
+ .bpe-list-editor__body .bpe-field__label { white-space: normal; overflow: visible; }
5310
5826
  .bpe-list-editor__drag { cursor: grab; color: var(--bpe-fg-faint); display: inline-flex; }
5311
5827
  .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
5828
  .bpe-issue:hover { background: var(--bpe-bg-muted); }