@particle-academy/fancy-cms-ui 0.0.1-dev.13 → 0.0.1-dev.15

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.
@@ -0,0 +1,93 @@
1
+ import { childrenOf, emitDocCss, isBinding } from './chunk-3YZMELJJ.js';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { Fragment } from 'react';
4
+
5
+ function getPath(data, path) {
6
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], data);
7
+ }
8
+ function resolveValue(v, data) {
9
+ if (v === void 0 || v === null) return v;
10
+ if (isBinding(v)) return getPath(data, v.$bind);
11
+ return v;
12
+ }
13
+ var defaultRegistry = {
14
+ section: ({ children }) => children,
15
+ stack: ({ children }) => children,
16
+ grid: ({ children }) => children,
17
+ frame: ({ children }) => children,
18
+ shape: ({ children }) => children,
19
+ card: ({ children }) => children,
20
+ text: ({ node, text }) => text(node.props.content),
21
+ heading: ({ node, text }) => text(node.props.content) || "Heading",
22
+ link: ({ node, text }) => /* @__PURE__ */ jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
23
+ divider: () => /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
24
+ callout: ({ node, text }) => {
25
+ const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
26
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 10, padding: "12px 14px", borderRadius: 10, background: `${tone}14`, borderLeft: `3px solid ${tone}` }, children: /* @__PURE__ */ jsx("span", { style: { fontSize: 13 }, children: text(node.props.content) || "Callout" }) });
27
+ },
28
+ code: ({ node, text }) => /* @__PURE__ */ jsx("pre", { style: { margin: 0, padding: "12px 14px", borderRadius: 10, background: "#0b1220", color: "#e2e8f0", fontFamily: "ui-monospace, monospace", fontSize: 13, overflowX: "auto" }, children: /* @__PURE__ */ jsx("code", { children: text(node.props.content) }) }),
29
+ button: ({ node, text }) => {
30
+ const label = text(node.props.label) || text(node.props.content) || "Button";
31
+ const variant = text(node.props.variant) || "primary";
32
+ return /* @__PURE__ */ jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label });
33
+ },
34
+ image: ({ node, text }) => /* @__PURE__ */ jsx(
35
+ "img",
36
+ {
37
+ src: text(node.props.src),
38
+ alt: text(node.props.alt),
39
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
40
+ }
41
+ )
42
+ };
43
+ function buttonStyle(variant) {
44
+ const base = {
45
+ display: "inline-flex",
46
+ alignItems: "center",
47
+ gap: 6,
48
+ padding: "9px 16px",
49
+ borderRadius: 8,
50
+ fontWeight: 600,
51
+ fontSize: 14,
52
+ lineHeight: 1,
53
+ cursor: "pointer",
54
+ userSelect: "none"
55
+ };
56
+ if (variant === "ghost") return { ...base, background: "transparent", color: "inherit", border: "1px solid currentColor" };
57
+ if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
58
+ return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
59
+ }
60
+ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
61
+ const node = doc.nodes[id];
62
+ if (!node) return null;
63
+ if (node.type === "repeater") {
64
+ const arr = resolveValue(node.props.items, data);
65
+ const items = Array.isArray(arr) ? arr : [];
66
+ const template = childrenOf(doc, id)[0];
67
+ return /* @__PURE__ */ jsx("div", { "data-cms": node.id, className: node.className, children: template ? items.map((item, index) => /* @__PURE__ */ jsx(RenderNode, { doc, id: template.id, registry, data: { ...data, item, index } }, index)) : null });
68
+ }
69
+ const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsx(RenderNode, { doc, id: child.id, registry, data }, child.id));
70
+ const resolve = (v) => resolveValue(v, data);
71
+ const text = (v) => {
72
+ const r = resolve(v);
73
+ return r === void 0 || r === null ? "" : String(r);
74
+ };
75
+ const renderer = registry[node.type];
76
+ const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
77
+ return /* @__PURE__ */ jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
78
+ }
79
+ function CmsPage({
80
+ doc,
81
+ registry = defaultRegistry,
82
+ data,
83
+ includeStyles = true
84
+ }) {
85
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
86
+ includeStyles ? /* @__PURE__ */ jsx("style", { "data-cms-styles": "", children: emitDocCss(doc) }) : null,
87
+ doc.sections.map((id) => /* @__PURE__ */ jsx(RenderNode, { doc, id, registry, data }, id))
88
+ ] });
89
+ }
90
+
91
+ export { CmsPage, RenderNode, defaultRegistry };
92
+ //# sourceMappingURL=chunk-IXPYY4S2.js.map
93
+ //# sourceMappingURL=chunk-IXPYY4S2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/registry.tsx","../src/react/RenderNode.tsx","../src/react/CmsPage.tsx"],"names":["jsx"],"mappings":"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","file":"chunk-IXPYY4S2.js","sourcesContent":["import type { ReactNode } from \"react\";\nimport { isBinding, type Bound, type Json, type Node, type PageDoc } from \"../document/types\";\n\n/** The page data context bindings resolve against (e.g. server props, repeater `item`). */\nexport type DataContext = Record<string, unknown>;\n\nexport interface ElementContext {\n node: Node;\n doc: PageDoc;\n /** Pre-rendered child nodes, in order. */\n children: ReactNode;\n /** The data context this node renders in (page data + any repeater scope). */\n data?: DataContext;\n /** Resolve a (possibly bound) value to its raw resolved value. */\n resolve: (v: Bound<Json> | undefined) => unknown;\n /** Resolve a (possibly bound) value to a display string. */\n text: (v: Bound<Json> | undefined) => string;\n}\n\n/**\n * Renders the INNER content of a node. The outer `[data-cms]` wrapper is added\n * by {@link RenderNode}, so a renderer only returns what goes inside.\n */\nexport type ElementRenderer = (ctx: ElementContext) => ReactNode;\n\nexport type ElementRegistry = Record<string, ElementRenderer>;\n\n/** Read a dotted path (`a.b.c`) out of the data context. */\nexport function getPath(data: DataContext | undefined, path: string): unknown {\n return path.split(\".\").reduce<unknown>((o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]), data);\n}\n\n/** Resolve a value: a `{ $bind }` reads from the data context; anything else is literal. */\nexport function resolveValue(v: Bound<Json> | undefined, data?: DataContext): unknown {\n if (v === undefined || v === null) return v;\n if (isBinding(v)) return getPath(data, v.$bind);\n return v;\n}\n\n/** Built-in native elements. Containers render their children; leaves render content. */\nexport const defaultRegistry: ElementRegistry = {\n section: ({ children }) => children,\n stack: ({ children }) => children,\n grid: ({ children }) => children,\n frame: ({ children }) => children,\n shape: ({ children }) => children,\n card: ({ children }) => children,\n text: ({ node, text }) => text(node.props.content),\n heading: ({ node, text }) => text(node.props.content) || \"Heading\",\n link: ({ node, text }) => (\n <a href={text(node.props.href) || \"#\"} style={{ color: \"inherit\", textDecoration: \"underline\" }}>\n {text(node.props.content) || \"link\"}\n </a>\n ),\n divider: () => <hr style={{ border: \"none\", borderTop: \"1px solid #e2e8f0\", margin: 0 }} />,\n callout: ({ node, text }) => {\n const tone = ({ info: \"#3b82f6\", success: \"#10b981\", warning: \"#f59e0b\", danger: \"#ef4444\" } as Record<string, string>)[text(node.props.variant)] ?? \"#3b82f6\";\n return (\n <div style={{ display: \"flex\", gap: 10, padding: \"12px 14px\", borderRadius: 10, background: `${tone}14`, borderLeft: `3px solid ${tone}` }}>\n <span style={{ fontSize: 13 }}>{text(node.props.content) || \"Callout\"}</span>\n </div>\n );\n },\n code: ({ node, text }) => (\n <pre style={{ margin: 0, padding: \"12px 14px\", borderRadius: 10, background: \"#0b1220\", color: \"#e2e8f0\", fontFamily: \"ui-monospace, monospace\", fontSize: 13, overflowX: \"auto\" }}>\n <code>{text(node.props.content)}</code>\n </pre>\n ),\n button: ({ node, text }) => {\n const label = text(node.props.label) || text(node.props.content) || \"Button\";\n const variant = text(node.props.variant) || \"primary\";\n return <span data-cms-button={variant} style={buttonStyle(variant)}>{label}</span>;\n },\n image: ({ node, text }) => (\n <img\n src={text(node.props.src)}\n alt={text(node.props.alt)}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\", display: \"block\" }}\n />\n ),\n};\n\n/**\n * A self-contained button look so the `button` primitive renders sensibly with\n * zero host CSS. Hosts that want their own button classes override the `button`\n * renderer in their registry (e.g. the sandbox maps it to `.btn .btn-primary`).\n */\nfunction buttonStyle(variant: string): import(\"react\").CSSProperties {\n const base: import(\"react\").CSSProperties = {\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"9px 16px\",\n borderRadius: 8,\n fontWeight: 600,\n fontSize: 14,\n lineHeight: 1,\n cursor: \"pointer\",\n userSelect: \"none\",\n };\n if (variant === \"ghost\") return { ...base, background: \"transparent\", color: \"inherit\", border: \"1px solid currentColor\" };\n if (variant === \"outline\") return { ...base, background: \"transparent\", color: \"#7c3aed\", border: \"1px solid #7c3aed\" };\n return { ...base, background: \"#7c3aed\", color: \"#fff\", border: \"1px solid #7c3aed\" };\n}\n","import type { ReactElement } from \"react\";\nimport type { Bound, Json, PageDoc } from \"../document/types\";\nimport { childrenOf } from \"../document/reduce\";\nimport { defaultRegistry, resolveValue, type DataContext, type ElementRegistry } from \"./registry\";\n\nexport interface RenderNodeProps {\n doc: PageDoc;\n id: string;\n registry?: ElementRegistry;\n /** Data context bindings resolve against (page data + any repeater scope). */\n data?: DataContext;\n}\n\n/**\n * Render one node (and its subtree) to a `[data-cms]`-tagged element. The CSS\n * emitted by `emitDocCss` targets these `data-cms` handles. Interactive /\n * 3rd-party **islands** with no registered renderer become an empty, sized\n * placeholder the host hydrates client-side — mirroring the PHP renderer.\n *\n * `repeater` nodes are special-cased: their first child is a template rendered\n * once per item of the bound `items` array, each in a scope that exposes `item`\n * and `index` to nested `{ $bind: \"item.…\" }` bindings.\n */\nexport function RenderNode({ doc, id, registry = defaultRegistry, data }: RenderNodeProps): ReactElement | null {\n const node = doc.nodes[id];\n if (!node) return null;\n\n if (node.type === \"repeater\") {\n const arr = resolveValue(node.props.items as Bound<Json> | undefined, data);\n const items = Array.isArray(arr) ? arr : [];\n const template = childrenOf(doc, id)[0];\n return (\n <div data-cms={node.id} className={node.className}>\n {template\n ? items.map((item, index) => (\n <RenderNode key={index} doc={doc} id={template.id} registry={registry} data={{ ...data, item, index }} />\n ))\n : null}\n </div>\n );\n }\n\n const kids = childrenOf(doc, id).map((child) => (\n <RenderNode key={child.id} doc={doc} id={child.id} registry={registry} data={data} />\n ));\n\n const resolve = (v: Bound<Json> | undefined) => resolveValue(v, data);\n const text = (v: Bound<Json> | undefined): string => {\n const r = resolve(v);\n return r === undefined || r === null ? \"\" : String(r);\n };\n\n const renderer = registry[node.type];\n const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;\n\n return (\n <div data-cms={node.id} data-cms-island={node.island ? node.type : undefined} className={node.className}>\n {inner}\n </div>\n );\n}\n","import { Fragment, type ReactElement } from \"react\";\nimport type { PageDoc } from \"../document/types\";\nimport { emitDocCss } from \"../render/css\";\nimport { RenderNode } from \"./RenderNode\";\nimport { defaultRegistry, type DataContext, type ElementRegistry } from \"./registry\";\n\nexport interface CmsPageProps {\n doc: PageDoc;\n registry?: ElementRegistry;\n /** Data context that `{ $bind: \"…\" }` props + repeaters resolve against. */\n data?: DataContext;\n /** Inject the compiled stylesheet. Set false when the host emits CSS itself. */\n includeStyles?: boolean;\n}\n\n/** Render a full page document: the compiled stylesheet + every section. */\nexport function CmsPage({\n doc,\n registry = defaultRegistry,\n data,\n includeStyles = true,\n}: CmsPageProps): ReactElement {\n return (\n <Fragment>\n {includeStyles ? <style data-cms-styles=\"\">{emitDocCss(doc)}</style> : null}\n {doc.sections.map((id) => (\n <RenderNode key={id} doc={doc} id={id} registry={registry} data={data} />\n ))}\n </Fragment>\n );\n}\n"]}
package/dist/editor.cjs CHANGED
@@ -361,10 +361,13 @@ ${inner}
361
361
  }
362
362
  return blocks.join("\n\n") + (blocks.length ? "\n" : "");
363
363
  }
364
- function literal(v) {
365
- if (v === void 0 || v === null) return "";
366
- if (isBinding(v)) return "";
367
- return String(v);
364
+ function getPath(data, path) {
365
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], data);
366
+ }
367
+ function resolveValue(v, data) {
368
+ if (v === void 0 || v === null) return v;
369
+ if (isBinding(v)) return getPath(data, v.$bind);
370
+ return v;
368
371
  }
369
372
  var defaultRegistry = {
370
373
  section: ({ children }) => children,
@@ -373,25 +376,25 @@ var defaultRegistry = {
373
376
  frame: ({ children }) => children,
374
377
  shape: ({ children }) => children,
375
378
  card: ({ children }) => children,
376
- text: ({ node }) => literal(node.props.content),
377
- heading: ({ node }) => literal(node.props.content) || "Heading",
378
- link: ({ node }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: literal(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: literal(node.props.content) || "link" }),
379
+ text: ({ node, text }) => text(node.props.content),
380
+ heading: ({ node, text }) => text(node.props.content) || "Heading",
381
+ link: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
379
382
  divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
380
- callout: ({ node }) => {
381
- const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[literal(node.props.variant)] ?? "#3b82f6";
382
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 10, padding: "12px 14px", borderRadius: 10, background: `${tone}14`, borderLeft: `3px solid ${tone}` }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 13 }, children: literal(node.props.content) || "Callout" }) });
383
+ callout: ({ node, text }) => {
384
+ const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
385
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 10, padding: "12px 14px", borderRadius: 10, background: `${tone}14`, borderLeft: `3px solid ${tone}` }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 13 }, children: text(node.props.content) || "Callout" }) });
383
386
  },
384
- code: ({ node }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { style: { margin: 0, padding: "12px 14px", borderRadius: 10, background: "#0b1220", color: "#e2e8f0", fontFamily: "ui-monospace, monospace", fontSize: 13, overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: literal(node.props.content) }) }),
385
- button: ({ node }) => {
386
- const label2 = literal(node.props.label) || literal(node.props.content) || "Button";
387
- const variant = literal(node.props.variant) || "primary";
387
+ code: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { style: { margin: 0, padding: "12px 14px", borderRadius: 10, background: "#0b1220", color: "#e2e8f0", fontFamily: "ui-monospace, monospace", fontSize: 13, overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: text(node.props.content) }) }),
388
+ button: ({ node, text }) => {
389
+ const label2 = text(node.props.label) || text(node.props.content) || "Button";
390
+ const variant = text(node.props.variant) || "primary";
388
391
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
389
392
  },
390
- image: ({ node }) => /* @__PURE__ */ jsxRuntime.jsx(
393
+ image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
391
394
  "img",
392
395
  {
393
- src: literal(node.props.src),
394
- alt: literal(node.props.alt),
396
+ src: text(node.props.src),
397
+ alt: text(node.props.alt),
395
398
  style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
396
399
  }
397
400
  )
@@ -413,30 +416,34 @@ function buttonStyle(variant) {
413
416
  if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
414
417
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
415
418
  }
416
- function RenderNode({ doc, id, registry = defaultRegistry }) {
419
+ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
417
420
  const node = doc.nodes[id];
418
421
  if (!node) return null;
419
- const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: child.id, registry }, child.id));
422
+ if (node.type === "repeater") {
423
+ const arr = resolveValue(node.props.items, data);
424
+ const items = Array.isArray(arr) ? arr : [];
425
+ const template = childrenOf(doc, id)[0];
426
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, className: node.className, children: template ? items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: template.id, registry, data: { ...data, item, index } }, index)) : null });
427
+ }
428
+ const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: child.id, registry, data }, child.id));
429
+ const resolve = (v) => resolveValue(v, data);
430
+ const text = (v) => {
431
+ const r = resolve(v);
432
+ return r === void 0 || r === null ? "" : String(r);
433
+ };
420
434
  const renderer = registry[node.type];
421
- const inner = renderer ? renderer({ node, doc, children: kids }) : node.island ? null : kids;
422
- return /* @__PURE__ */ jsxRuntime.jsx(
423
- "div",
424
- {
425
- "data-cms": node.id,
426
- "data-cms-island": node.island ? node.type : void 0,
427
- className: node.className,
428
- children: inner
429
- }
430
- );
435
+ const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
436
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
431
437
  }
432
438
  function CmsPage({
433
439
  doc,
434
440
  registry = defaultRegistry,
441
+ data,
435
442
  includeStyles = true
436
443
  }) {
437
444
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
438
445
  includeStyles ? /* @__PURE__ */ jsxRuntime.jsx("style", { "data-cms-styles": "", children: emitDocCss(doc) }) : null,
439
- doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id, registry }, id))
446
+ doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id, registry, data }, id))
440
447
  ] });
441
448
  }
442
449
  function Canvas({ doc, selection, onSelect, apply }) {
@@ -826,7 +833,7 @@ function NodeInspector({
826
833
  const setT = (patch) => onTransform({ ...transform, ...patch });
827
834
  const handled = /* @__PURE__ */ new Set(["content", "label"]);
828
835
  const extraKeys = Object.keys(node.props).filter(
829
- (k) => !handled.has(k) && ["string", "number", "boolean"].includes(typeof node.props[k])
836
+ (k) => !handled.has(k) && (["string", "number", "boolean"].includes(typeof node.props[k]) || isBinding(node.props[k]))
830
837
  );
831
838
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
832
839
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headRow, children: [
@@ -951,24 +958,50 @@ function Row({ children }) {
951
958
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 8 }, children });
952
959
  }
953
960
  function PropField({ name, value, onChange }) {
961
+ const bound = isBinding(value);
962
+ const link = /* @__PURE__ */ jsxRuntime.jsx(
963
+ "button",
964
+ {
965
+ type: "button",
966
+ title: bound ? "Unbind (use a literal value)" : "Bind to data (e.g. item.name)",
967
+ onClick: () => onChange(bound ? "" : { $bind: "" }),
968
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontSize: 11, color: bound ? "#a78bfa" : "#64748b" },
969
+ children: "\u{1F517}"
970
+ }
971
+ );
972
+ if (bound) {
973
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
974
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
975
+ name,
976
+ " ",
977
+ link
978
+ ] }),
979
+ /* @__PURE__ */ jsxRuntime.jsx("input", { style: { ...input2, borderColor: "#7c3aed" }, placeholder: "data path \xB7 e.g. item.name", value: value.$bind, onChange: (e) => onChange({ $bind: e.target.value }) })
980
+ ] });
981
+ }
954
982
  if (typeof value === "boolean") {
955
983
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
956
984
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
957
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textTransform: "capitalize" }, children: name })
985
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textTransform: "capitalize", flex: 1 }, children: name }),
986
+ link
958
987
  ] });
959
988
  }
960
989
  if (name === "variant") {
961
- return /* @__PURE__ */ jsxRuntime.jsx(Sel, { label: name, value: String(value), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
990
+ return /* @__PURE__ */ jsxRuntime.jsx(Sel, { label: name, value: String(value ?? ""), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
962
991
  }
963
992
  const isNum = typeof value === "number";
964
993
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
965
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: fieldLabel, children: name }),
994
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
995
+ name,
996
+ " ",
997
+ link
998
+ ] }),
966
999
  /* @__PURE__ */ jsxRuntime.jsx(
967
1000
  "input",
968
1001
  {
969
1002
  style: input2,
970
1003
  type: isNum ? "number" : "text",
971
- value: String(value),
1004
+ value: String(value ?? ""),
972
1005
  onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
973
1006
  }
974
1007
  )
@@ -1156,6 +1189,7 @@ var SNAP = 6;
1156
1189
  function EditablePage({
1157
1190
  doc,
1158
1191
  registry = defaultRegistry,
1192
+ data,
1159
1193
  timelineDock,
1160
1194
  transforms,
1161
1195
  onNodeTransform,
@@ -1263,7 +1297,7 @@ function EditablePage({
1263
1297
  },
1264
1298
  [ed, selNode]
1265
1299
  );
1266
- const addNodeAt = react.useCallback(
1300
+ react.useCallback(
1267
1301
  (kind, targetId) => {
1268
1302
  const doc2 = ed.state.doc;
1269
1303
  const target = targetId ? doc2.nodes[targetId] : null;
@@ -1292,12 +1326,48 @@ function EditablePage({
1292
1326
  window.addEventListener("mousemove", onMove);
1293
1327
  return () => window.removeEventListener("mousemove", onMove);
1294
1328
  }, [editing, draggingEl]);
1329
+ const [dropHint, setDropHint] = react.useState(null);
1330
+ const computeDropTarget = (clientX, clientY) => {
1331
+ const el = document.elementFromPoint(clientX, clientY)?.closest("[data-cms]");
1332
+ if (!el) return { id: null, edge: "inside", rect: null };
1333
+ const id = el.getAttribute("data-cms");
1334
+ const node = ed.state.doc.nodes[id];
1335
+ const r = el.getBoundingClientRect();
1336
+ const rect = { x: r.left, y: r.top, w: r.width, h: r.height };
1337
+ if (node && CONTAINER_TYPES2.has(node.type)) return { id, edge: "inside", rect };
1338
+ return { id, edge: clientY < r.top + r.height / 2 ? "before" : "after", rect };
1339
+ };
1340
+ const onPageDragOver = (e) => {
1341
+ e.preventDefault();
1342
+ e.dataTransfer.dropEffect = "copy";
1343
+ setDropHint(computeDropTarget(e.clientX, e.clientY));
1344
+ };
1345
+ const insertRelative = (kind, hint) => {
1346
+ const doc2 = ed.state.doc;
1347
+ let parent;
1348
+ let order;
1349
+ if (!hint.id || hint.edge === "inside") {
1350
+ parent = hint.id ?? (doc2.sections[doc2.sections.length - 1] ?? null);
1351
+ const sib = childrenOf(doc2, parent);
1352
+ order = keyBetween(sib.length ? sib[sib.length - 1].order : null, null);
1353
+ } else {
1354
+ const target = doc2.nodes[hint.id];
1355
+ parent = target.parent;
1356
+ const sib = childrenOf(doc2, parent);
1357
+ const idx = sib.findIndex((n) => n.id === hint.id);
1358
+ order = hint.edge === "before" ? keyBetween(idx > 0 ? sib[idx - 1].order : null, target.order) : keyBetween(target.order, idx < sib.length - 1 ? sib[idx + 1].order : null);
1359
+ }
1360
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1361
+ const def = ADD_DEFAULTS[kind];
1362
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1363
+ ed.select(id);
1364
+ };
1295
1365
  const onPageDrop = (e) => {
1296
1366
  const kind = e.dataTransfer.getData("application/x-cms-element");
1297
1367
  if (!kind) return;
1298
1368
  e.preventDefault();
1299
- const el = document.elementFromPoint(e.clientX, e.clientY)?.closest("[data-cms]");
1300
- addNodeAt(kind, el?.getAttribute("data-cms") ?? null);
1369
+ insertRelative(kind, computeDropTarget(e.clientX, e.clientY));
1370
+ setDropHint(null);
1301
1371
  setDraggingEl(false);
1302
1372
  };
1303
1373
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
@@ -1373,9 +1443,10 @@ function EditablePage({
1373
1443
  ref: pageRef,
1374
1444
  onClickCapture,
1375
1445
  onContextMenu: onContextMenuSelect,
1376
- onDragOver: editing ? (e) => e.preventDefault() : void 0,
1446
+ onDragOver: editing ? onPageDragOver : void 0,
1447
+ onDragLeave: editing ? () => setDropHint(null) : void 0,
1377
1448
  onDrop: editing ? onPageDrop : void 0,
1378
- children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry })
1449
+ children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry, data })
1379
1450
  }
1380
1451
  );
1381
1452
  const pageShell = pinned ? /* @__PURE__ */ jsxRuntime.jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : frames > 1 ? (
@@ -1444,10 +1515,23 @@ function EditablePage({
1444
1515
  onAdd: addNode
1445
1516
  }
1446
1517
  ) : null,
1518
+ editing && dropHint?.rect ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, { hint: dropHint }) : null,
1447
1519
  editing ? /* @__PURE__ */ jsxRuntime.jsx(ElementPalette, { open: paletteOpen, onDragChange: setDraggingEl }) : null,
1448
1520
  editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
1449
1521
  ] });
1450
1522
  }
1523
+ function DropIndicator({
1524
+ hint
1525
+ }) {
1526
+ const r = hint.rect;
1527
+ if (!r) return null;
1528
+ const color = "#ec4899";
1529
+ if (hint.edge === "inside") {
1530
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: r.x, top: r.y, width: r.w, height: r.h, outline: `2px dashed ${color}`, outlineOffset: -2, borderRadius: 6, pointerEvents: "none", zIndex: Z2 + 4 } });
1531
+ }
1532
+ const y = hint.edge === "before" ? r.y : r.y + r.h;
1533
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: r.x, top: y - 1.5, width: r.w, height: 3, background: color, borderRadius: 2, boxShadow: "0 0 0 1px #fff", pointerEvents: "none", zIndex: Z2 + 4 } });
1534
+ }
1451
1535
  function ElementPalette({ open, onDragChange }) {
1452
1536
  return /* @__PURE__ */ jsxRuntime.jsxs(
1453
1537
  "div",
@@ -1510,7 +1594,7 @@ function ElementPalette({ open, onDragChange }) {
1510
1594
  }
1511
1595
  );
1512
1596
  }
1513
- var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device"]);
1597
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device", "repeater"]);
1514
1598
  var ADD_DEFAULTS = {
1515
1599
  text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
1516
1600
  heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
@@ -1523,7 +1607,8 @@ var ADD_DEFAULTS = {
1523
1607
  card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
1524
1608
  callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
1525
1609
  divider: { type: "divider", props: {}, style: {} },
1526
- code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} }
1610
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
1611
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
1527
1612
  };
1528
1613
  var GRIPS = [
1529
1614
  { key: "nw", north: true, west: true, cursor: "nwse-resize" },
@@ -1734,7 +1819,8 @@ var ADD_MENU = [
1734
1819
  { kind: "grid", label: "Grid" },
1735
1820
  { kind: "box", label: "Box" },
1736
1821
  { kind: "divider", label: "Divider" },
1737
- { kind: "code", label: "Code" }
1822
+ { kind: "code", label: "Code" },
1823
+ { kind: "repeater", label: "Repeater" }
1738
1824
  ];
1739
1825
  function EditBar({
1740
1826
  editing,