@particle-academy/fancy-cms-ui 0.0.1-dev.8 → 0.1.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/editor.cjs CHANGED
@@ -98,6 +98,10 @@ function reduce(doc, op, opts = {}) {
98
98
  props: { ...doc.nodes[op.id].props, ...op.patch }
99
99
  });
100
100
  }
101
+ case "set_layout": {
102
+ if (!doc.nodes[op.id]) return invalid("node not found");
103
+ return patchNode(doc, op.id, { layout: op.layout });
104
+ }
101
105
  case "set_style": {
102
106
  const node = doc.nodes[op.id];
103
107
  if (!node) return invalid("node not found");
@@ -357,10 +361,13 @@ ${inner}
357
361
  }
358
362
  return blocks.join("\n\n") + (blocks.length ? "\n" : "");
359
363
  }
360
- function literal(v) {
361
- if (v === void 0 || v === null) return "";
362
- if (isBinding(v)) return "";
363
- 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;
364
371
  }
365
372
  var defaultRegistry = {
366
373
  section: ({ children }) => children,
@@ -368,18 +375,29 @@ var defaultRegistry = {
368
375
  grid: ({ children }) => children,
369
376
  frame: ({ children }) => children,
370
377
  shape: ({ children }) => children,
371
- text: ({ node }) => literal(node.props.content),
372
- heading: ({ node }) => literal(node.props.content) || "Heading",
373
- button: ({ node }) => {
374
- const label2 = literal(node.props.label) || literal(node.props.content) || "Button";
375
- const variant = literal(node.props.variant) || "primary";
378
+ card: ({ children }) => children,
379
+ text: ({ node, text }) => text(node.props.content),
380
+ heading: ({ node, text }) => text(node.props.content) || "Heading",
381
+ // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
382
+ // plain `text` can't express. The author owns the markup (it's their content).
383
+ richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
384
+ link: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
385
+ divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
386
+ callout: ({ node, text }) => {
387
+ const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
388
+ 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" }) });
389
+ },
390
+ 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) }) }),
391
+ button: ({ node, text }) => {
392
+ const label2 = text(node.props.label) || text(node.props.content) || "Button";
393
+ const variant = text(node.props.variant) || "primary";
376
394
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
377
395
  },
378
- image: ({ node }) => /* @__PURE__ */ jsxRuntime.jsx(
396
+ image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
379
397
  "img",
380
398
  {
381
- src: literal(node.props.src),
382
- alt: literal(node.props.alt),
399
+ src: text(node.props.src),
400
+ alt: text(node.props.alt),
383
401
  style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
384
402
  }
385
403
  )
@@ -401,30 +419,34 @@ function buttonStyle(variant) {
401
419
  if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
402
420
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
403
421
  }
404
- function RenderNode({ doc, id, registry = defaultRegistry }) {
422
+ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
405
423
  const node = doc.nodes[id];
406
424
  if (!node) return null;
407
- const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: child.id, registry }, child.id));
425
+ if (node.type === "repeater") {
426
+ const arr = resolveValue(node.props.items, data);
427
+ const items = Array.isArray(arr) ? arr : [];
428
+ const template = childrenOf(doc, id)[0];
429
+ 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 });
430
+ }
431
+ const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: child.id, registry, data }, child.id));
432
+ const resolve = (v) => resolveValue(v, data);
433
+ const text = (v) => {
434
+ const r = resolve(v);
435
+ return r === void 0 || r === null ? "" : String(r);
436
+ };
408
437
  const renderer = registry[node.type];
409
- const inner = renderer ? renderer({ node, doc, children: kids }) : node.island ? null : kids;
410
- return /* @__PURE__ */ jsxRuntime.jsx(
411
- "div",
412
- {
413
- "data-cms": node.id,
414
- "data-cms-island": node.island ? node.type : void 0,
415
- className: node.className,
416
- children: inner
417
- }
418
- );
438
+ const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
439
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
419
440
  }
420
441
  function CmsPage({
421
442
  doc,
422
443
  registry = defaultRegistry,
444
+ data,
423
445
  includeStyles = true
424
446
  }) {
425
447
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
426
448
  includeStyles ? /* @__PURE__ */ jsxRuntime.jsx("style", { "data-cms-styles": "", children: emitDocCss(doc) }) : null,
427
- doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id, registry }, id))
449
+ doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id, registry, data }, id))
428
450
  ] });
429
451
  }
430
452
  function Canvas({ doc, selection, onSelect, apply }) {
@@ -513,31 +535,110 @@ function Canvas({ doc, selection, onSelect, apply }) {
513
535
  function cssEscape(value) {
514
536
  return value.replace(/["\\]/g, "\\$&");
515
537
  }
516
- function LayersPanel({ doc, selection, onSelect }) {
517
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflow: "auto", padding: 8, fontSize: 13, fontFamily: "system-ui, sans-serif" }, children: doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(LayerRow, { doc, id, depth: 0, selection, onSelect }, id)) });
538
+
539
+ // src/document/fractional.ts
540
+ var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
541
+ var RADIX = BASE.length;
542
+ var MID = Math.floor(RADIX / 2);
543
+ function val(c) {
544
+ const i = BASE.indexOf(c);
545
+ return i < 0 ? 0 : i;
546
+ }
547
+ function keyBetween(a, b) {
548
+ if (a === null) a = "";
549
+ if (b === null) {
550
+ return a + BASE[MID];
551
+ }
552
+ if (a >= b) {
553
+ throw new Error(`keyBetween: lower bound "${a}" is not below upper bound "${b}"`);
554
+ }
555
+ let result = "";
556
+ let i = 0;
557
+ for (; ; ) {
558
+ const ca = i < a.length ? val(a[i]) : 0;
559
+ const cb = i < b.length ? val(b[i]) : RADIX;
560
+ if (cb - ca > 1) {
561
+ result += BASE[ca + Math.floor((cb - ca) / 2)];
562
+ return result;
563
+ }
564
+ if (ca === cb) {
565
+ result += BASE[ca];
566
+ i++;
567
+ continue;
568
+ }
569
+ result += BASE[ca];
570
+ i++;
571
+ for (; ; ) {
572
+ const da = i < a.length ? val(a[i]) : 0;
573
+ if (RADIX - da > 1) {
574
+ result += BASE[da + Math.floor((RADIX - da) / 2)];
575
+ return result;
576
+ }
577
+ result += BASE[da];
578
+ i++;
579
+ }
580
+ }
581
+ }
582
+ var MIME = "application/x-cms-node";
583
+ function isAncestorOrSelf(doc, id, maybeAncestor) {
584
+ for (let cur = id; cur; cur = doc.nodes[cur]?.parent ?? null) {
585
+ if (cur === maybeAncestor) return true;
586
+ }
587
+ return false;
588
+ }
589
+ function LayersPanel({ doc, selection, onSelect, onMove }) {
590
+ const [overId, setOverId] = react.useState(null);
591
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflow: "auto", padding: 8, fontSize: 13, fontFamily: "system-ui, sans-serif" }, children: doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(LayerRow, { doc, id, depth: 0, selection, onSelect, onMove, overId, setOverId }, id)) });
518
592
  }
519
- function LayerRow({ doc, id, depth, selection, onSelect }) {
593
+ function LayerRow({ doc, id, depth, selection, onSelect, onMove, overId, setOverId }) {
520
594
  const node = doc.nodes[id];
521
595
  if (!node) return null;
522
596
  const selected = selection === id;
597
+ const draggable = Boolean(onMove);
598
+ const drop = (e) => {
599
+ e.preventDefault();
600
+ e.stopPropagation();
601
+ setOverId(null);
602
+ if (!onMove) return;
603
+ const dragId = e.dataTransfer.getData(MIME);
604
+ if (!dragId || dragId === id || isAncestorOrSelf(doc, id, dragId)) return;
605
+ const siblings = childrenOf(doc, node.parent);
606
+ const idx = siblings.findIndex((n) => n.id === id);
607
+ const next = siblings[idx + 1];
608
+ onMove(dragId, node.parent, keyBetween(node.order, next ? next.order : null));
609
+ };
523
610
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
524
611
  /* @__PURE__ */ jsxRuntime.jsxs(
525
612
  "button",
526
613
  {
527
614
  type: "button",
615
+ draggable,
528
616
  onClick: () => onSelect(id),
617
+ onDragStart: (e) => {
618
+ e.dataTransfer.setData(MIME, id);
619
+ e.dataTransfer.effectAllowed = "move";
620
+ },
621
+ onDragOver: (e) => {
622
+ if (!onMove) return;
623
+ e.preventDefault();
624
+ e.dataTransfer.dropEffect = "move";
625
+ if (overId !== id) setOverId(id);
626
+ },
627
+ onDragLeave: () => overId === id && setOverId(null),
628
+ onDrop: drop,
529
629
  style: {
530
630
  display: "block",
531
631
  width: "100%",
532
632
  textAlign: "left",
533
633
  border: "none",
534
634
  borderRadius: 6,
535
- cursor: "pointer",
635
+ cursor: draggable ? "grab" : "pointer",
536
636
  padding: "4px 8px",
537
637
  paddingLeft: 8 + depth * 14,
538
638
  background: selected ? "#ede9fe" : "transparent",
539
639
  color: selected ? "#5b21b6" : "#334155",
540
- font: "inherit"
640
+ font: "inherit",
641
+ boxShadow: overId === id ? "inset 0 -2px 0 0 #8b5cf6" : "none"
541
642
  },
542
643
  children: [
543
644
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.6 }, children: node.type }),
@@ -546,17 +647,7 @@ function LayerRow({ doc, id, depth, selection, onSelect }) {
546
647
  ]
547
648
  }
548
649
  ),
549
- childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(
550
- LayerRow,
551
- {
552
- doc,
553
- id: child.id,
554
- depth: depth + 1,
555
- selection,
556
- onSelect
557
- },
558
- child.id
559
- ))
650
+ childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(LayerRow, { doc, id: child.id, depth: depth + 1, selection, onSelect, onMove, overId, setOverId }, child.id))
560
651
  ] });
561
652
  }
562
653
  var label = {
@@ -688,7 +779,15 @@ function Editor({ defaultValue, onChange }) {
688
779
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: shell, children: [
689
780
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
690
781
  /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { ed }),
691
- /* @__PURE__ */ jsxRuntime.jsx(LayersPanel, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select })
782
+ /* @__PURE__ */ jsxRuntime.jsx(
783
+ LayersPanel,
784
+ {
785
+ doc: ed.state.doc,
786
+ selection: ed.state.selection,
787
+ onSelect: ed.select,
788
+ onMove: (id, parent, order) => ed.apply({ t: "move_node", id, parent, order })
789
+ }
790
+ )
692
791
  ] }),
693
792
  /* @__PURE__ */ jsxRuntime.jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
694
793
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { borderLeft: "1px solid #e2e8f0", minHeight: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply }) })
@@ -721,66 +820,23 @@ function Toolbar({ ed }) {
721
820
  }
722
821
  );
723
822
  }
724
-
725
- // src/document/fractional.ts
726
- var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
727
- var RADIX = BASE.length;
728
- var MID = Math.floor(RADIX / 2);
729
- function val(c) {
730
- const i = BASE.indexOf(c);
731
- return i < 0 ? 0 : i;
732
- }
733
- function keyBetween(a, b) {
734
- if (a === null) a = "";
735
- if (b === null) {
736
- return a + BASE[MID];
737
- }
738
- if (a >= b) {
739
- throw new Error(`keyBetween: lower bound "${a}" is not below upper bound "${b}"`);
740
- }
741
- let result = "";
742
- let i = 0;
743
- for (; ; ) {
744
- const ca = i < a.length ? val(a[i]) : 0;
745
- const cb = i < b.length ? val(b[i]) : RADIX;
746
- if (cb - ca > 1) {
747
- result += BASE[ca + Math.floor((cb - ca) / 2)];
748
- return result;
749
- }
750
- if (ca === cb) {
751
- result += BASE[ca];
752
- i++;
753
- continue;
754
- }
755
- result += BASE[ca];
756
- i++;
757
- for (; ; ) {
758
- const da = i < a.length ? val(a[i]) : 0;
759
- if (RADIX - da > 1) {
760
- result += BASE[da + Math.floor((RADIX - da) / 2)];
761
- return result;
762
- }
763
- result += BASE[da];
764
- i++;
765
- }
766
- }
767
- }
768
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
823
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device"]);
769
824
  function NodeInspector({
770
825
  node,
771
826
  transform,
772
827
  measured,
773
828
  onProps,
774
829
  onStyle,
830
+ onLayout,
775
831
  onTransform,
776
832
  onRemove,
777
833
  onClose
778
834
  }) {
779
835
  const base = node.style.base ?? {};
780
836
  const setT = (patch) => onTransform({ ...transform, ...patch });
781
- const handled = /* @__PURE__ */ new Set(["content", "label"]);
837
+ const handled = /* @__PURE__ */ new Set(["content", "label", "html"]);
782
838
  const extraKeys = Object.keys(node.props).filter(
783
- (k) => !handled.has(k) && ["string", "number", "boolean"].includes(typeof node.props[k])
839
+ (k) => !handled.has(k) && (["string", "number", "boolean"].includes(typeof node.props[k]) || isBinding(node.props[k]))
784
840
  );
785
841
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
786
842
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headRow, children: [
@@ -793,14 +849,11 @@ function NodeInspector({
793
849
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: iconBtn, title: "Close", onClick: onClose, children: "\u2715" })
794
850
  ] }),
795
851
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: body, children: [
796
- "content" in node.props ? /* @__PURE__ */ jsxRuntime.jsx(Group, { label: "Content", children: /* @__PURE__ */ jsxRuntime.jsx(
797
- "textarea",
798
- {
799
- style: { ...input2, minHeight: 64, resize: "vertical", lineHeight: 1.4 },
800
- value: String(node.props.content ?? ""),
801
- onChange: (e) => onProps({ content: e.target.value })
802
- }
803
- ) }) : null,
852
+ "content" in node.props || node.type === "richtext" ? /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: 11, color: "var(--fg-3, #94a3b8)", margin: "0 0 12px", lineHeight: 1.45, paddingBottom: 12, borderBottom: "1px solid #1e293b" }, children: [
853
+ "\u270E Edit the text ",
854
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "directly on the page" }),
855
+ " \u2014 click in and type. This panel is for layout, style, and bindings."
856
+ ] }) : null,
804
857
  "label" in node.props ? /* @__PURE__ */ jsxRuntime.jsx(Group, { label: "Label", children: /* @__PURE__ */ jsxRuntime.jsx("input", { style: input2, value: String(node.props.label ?? ""), onChange: (e) => onProps({ label: e.target.value }) }) }) : null,
805
858
  extraKeys.length ? /* @__PURE__ */ jsxRuntime.jsx(Group, { label: "Properties", children: extraKeys.map((k) => /* @__PURE__ */ jsxRuntime.jsx(PropField, { name: k, value: node.props[k], onChange: (v) => onProps({ [k]: v }) }, k)) }) : null,
806
859
  /* @__PURE__ */ jsxRuntime.jsxs(Group, { label: "Position & size", children: [
@@ -854,9 +907,44 @@ function NodeInspector({
854
907
  ] }),
855
908
  /* @__PURE__ */ jsxRuntime.jsxs(Row, { children: [
856
909
  /* @__PURE__ */ jsxRuntime.jsx(Num, { label: "Padding", value: base.padding?.value, placeholder: "0", onChange: (v) => onStyle({ padding: v == null ? void 0 : len(v) }) }),
910
+ /* @__PURE__ */ jsxRuntime.jsx(Num, { label: "Margin", value: base.margin?.value, placeholder: "0", onChange: (v) => onStyle({ margin: v == null ? void 0 : len(v) }) })
911
+ ] }),
912
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { children: /* @__PURE__ */ jsxRuntime.jsx(Txt, { label: "Border", value: base.border ?? "", placeholder: "1px solid #e2e8f0", onChange: (v) => onStyle({ border: v || void 0 }) }) })
913
+ ] }),
914
+ CONTAINER_TYPES.has(node.type) ? /* @__PURE__ */ jsxRuntime.jsxs(Group, { label: "Layout", children: [
915
+ /* @__PURE__ */ jsxRuntime.jsxs(Row, { children: [
916
+ /* @__PURE__ */ jsxRuntime.jsx(
917
+ Sel,
918
+ {
919
+ label: "Mode",
920
+ value: node.layout ?? "free",
921
+ options: [["free", "Free"], ["stack", "Stack"], ["grid", "Grid"]],
922
+ onChange: (v) => onLayout?.(v)
923
+ }
924
+ ),
857
925
  /* @__PURE__ */ jsxRuntime.jsx(Num, { label: "Gap", value: base.gap?.value, placeholder: "0", onChange: (v) => onStyle({ gap: v == null ? void 0 : len(v) }) })
926
+ ] }),
927
+ /* @__PURE__ */ jsxRuntime.jsxs(Row, { children: [
928
+ /* @__PURE__ */ jsxRuntime.jsx(
929
+ Sel,
930
+ {
931
+ label: "Align",
932
+ value: base.align ?? "",
933
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["stretch", "Stretch"]],
934
+ onChange: (v) => onStyle({ align: v || void 0 })
935
+ }
936
+ ),
937
+ /* @__PURE__ */ jsxRuntime.jsx(
938
+ Sel,
939
+ {
940
+ label: "Justify",
941
+ value: base.justify ?? "",
942
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["between", "Between"], ["around", "Around"]],
943
+ onChange: (v) => onStyle({ justify: v || void 0 })
944
+ }
945
+ )
858
946
  ] })
859
- ] })
947
+ ] }) : null
860
948
  ] })
861
949
  ] });
862
950
  }
@@ -870,24 +958,50 @@ function Row({ children }) {
870
958
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 8 }, children });
871
959
  }
872
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
+ }
873
982
  if (typeof value === "boolean") {
874
983
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
875
984
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
876
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textTransform: "capitalize" }, children: name })
985
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textTransform: "capitalize", flex: 1 }, children: name }),
986
+ link
877
987
  ] });
878
988
  }
879
989
  if (name === "variant") {
880
- 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 });
881
991
  }
882
992
  const isNum = typeof value === "number";
883
993
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
884
- /* @__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
+ ] }),
885
999
  /* @__PURE__ */ jsxRuntime.jsx(
886
1000
  "input",
887
1001
  {
888
1002
  style: input2,
889
1003
  type: isNum ? "number" : "text",
890
- value: String(value),
1004
+ value: String(value ?? ""),
891
1005
  onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
892
1006
  }
893
1007
  )
@@ -911,6 +1025,12 @@ function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
911
1025
  )
912
1026
  ] });
913
1027
  }
1028
+ function Txt({ label: label2, value, placeholder, onChange }) {
1029
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
1030
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: fieldLabel, children: label2 }),
1031
+ /* @__PURE__ */ jsxRuntime.jsx("input", { style: input2, type: "text", value, placeholder, onChange: (e) => onChange(e.target.value) })
1032
+ ] });
1033
+ }
914
1034
  function Color({ label: label2, value, onChange }) {
915
1035
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
916
1036
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: fieldLabel, children: label2 }),
@@ -1065,9 +1185,11 @@ function snapshotSubtree(doc, id) {
1065
1185
  return { rootId: id, nodes };
1066
1186
  }
1067
1187
  var Z2 = 2147483e3;
1188
+ var SNAP = 6;
1068
1189
  function EditablePage({
1069
1190
  doc,
1070
1191
  registry = defaultRegistry,
1192
+ data,
1071
1193
  timelineDock,
1072
1194
  transforms,
1073
1195
  onNodeTransform,
@@ -1139,13 +1261,17 @@ function EditablePage({
1139
1261
  };
1140
1262
  }, [measureBox, ed.state.doc]);
1141
1263
  react.useEffect(() => {
1142
- if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
1143
- const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
1144
- if (!el) return;
1264
+ const t = selNode?.type;
1265
+ const editable = t === "text" || t === "heading" || t === "richtext";
1266
+ if (!editing || !selection || !pageRef.current || !editable) return;
1267
+ const wrapper = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
1268
+ if (!wrapper) return;
1269
+ const isRich = t === "richtext";
1270
+ const el = (isRich ? wrapper.firstElementChild : wrapper) ?? wrapper;
1145
1271
  el.setAttribute("contenteditable", "true");
1146
1272
  el.style.outline = "none";
1147
1273
  el.focus();
1148
- const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
1274
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: isRich ? { html: el.innerHTML } : { content: el.innerText } });
1149
1275
  el.addEventListener("blur", commit);
1150
1276
  return () => {
1151
1277
  el.removeEventListener("blur", commit);
@@ -1170,13 +1296,87 @@ function EditablePage({
1170
1296
  const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
1171
1297
  const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1172
1298
  const def = ADD_DEFAULTS[kind];
1173
- ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
1299
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1174
1300
  ed.select(id);
1175
1301
  },
1176
1302
  [ed, selNode]
1177
1303
  );
1304
+ react.useCallback(
1305
+ (kind, targetId) => {
1306
+ const doc2 = ed.state.doc;
1307
+ const target = targetId ? doc2.nodes[targetId] : null;
1308
+ const parent = target ? CONTAINER_TYPES2.has(target.type) ? target.id : target.parent : doc2.sections[doc2.sections.length - 1] ?? null;
1309
+ const siblings = childrenOf(doc2, parent);
1310
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
1311
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1312
+ const def = ADD_DEFAULTS[kind];
1313
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1314
+ ed.select(id);
1315
+ },
1316
+ [ed]
1317
+ );
1318
+ const [paletteOpen, setPaletteOpen] = react.useState(false);
1319
+ const [draggingEl, setDraggingEl] = react.useState(false);
1320
+ react.useEffect(() => {
1321
+ if (!editing) {
1322
+ setPaletteOpen(false);
1323
+ return;
1324
+ }
1325
+ const onMove = (e) => {
1326
+ if (draggingEl) return;
1327
+ if (e.clientX >= window.innerWidth - 26) setPaletteOpen(true);
1328
+ else if (e.clientX < window.innerWidth - 236) setPaletteOpen(false);
1329
+ };
1330
+ window.addEventListener("mousemove", onMove);
1331
+ return () => window.removeEventListener("mousemove", onMove);
1332
+ }, [editing, draggingEl]);
1333
+ const [dropHint, setDropHint] = react.useState(null);
1334
+ const computeDropTarget = (clientX, clientY) => {
1335
+ const el = document.elementFromPoint(clientX, clientY)?.closest("[data-cms]");
1336
+ if (!el) return { id: null, edge: "inside", rect: null };
1337
+ const id = el.getAttribute("data-cms");
1338
+ const node = ed.state.doc.nodes[id];
1339
+ const r = el.getBoundingClientRect();
1340
+ const rect = { x: r.left, y: r.top, w: r.width, h: r.height };
1341
+ if (node && CONTAINER_TYPES2.has(node.type)) return { id, edge: "inside", rect };
1342
+ return { id, edge: clientY < r.top + r.height / 2 ? "before" : "after", rect };
1343
+ };
1344
+ const onPageDragOver = (e) => {
1345
+ e.preventDefault();
1346
+ e.dataTransfer.dropEffect = "copy";
1347
+ setDropHint(computeDropTarget(e.clientX, e.clientY));
1348
+ };
1349
+ const insertRelative = (kind, hint) => {
1350
+ const doc2 = ed.state.doc;
1351
+ let parent;
1352
+ let order;
1353
+ if (!hint.id || hint.edge === "inside") {
1354
+ parent = hint.id ?? (doc2.sections[doc2.sections.length - 1] ?? null);
1355
+ const sib = childrenOf(doc2, parent);
1356
+ order = keyBetween(sib.length ? sib[sib.length - 1].order : null, null);
1357
+ } else {
1358
+ const target = doc2.nodes[hint.id];
1359
+ parent = target.parent;
1360
+ const sib = childrenOf(doc2, parent);
1361
+ const idx = sib.findIndex((n) => n.id === hint.id);
1362
+ 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);
1363
+ }
1364
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1365
+ const def = ADD_DEFAULTS[kind];
1366
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1367
+ ed.select(id);
1368
+ };
1369
+ const onPageDrop = (e) => {
1370
+ const kind = e.dataTransfer.getData("application/x-cms-element");
1371
+ if (!kind) return;
1372
+ e.preventDefault();
1373
+ insertRelative(kind, computeDropTarget(e.clientX, e.clientY));
1374
+ setDropHint(null);
1375
+ setDraggingEl(false);
1376
+ };
1178
1377
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
1179
1378
  const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
1379
+ const setLayout = (layout) => selection && ed.apply({ t: "set_layout", id: selection, layout });
1180
1380
  const removeSelected = () => {
1181
1381
  if (!selection) return;
1182
1382
  ed.apply({ t: "remove_node", id: selection });
@@ -1201,6 +1401,8 @@ function EditablePage({
1201
1401
  ed.select(id);
1202
1402
  requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
1203
1403
  };
1404
+ const nudgeRef = react.useRef({ transforms, onNodeTransform });
1405
+ nudgeRef.current = { transforms, onNodeTransform };
1204
1406
  react.useEffect(() => {
1205
1407
  if (!editing) return;
1206
1408
  const onKey = (e) => {
@@ -1219,6 +1421,15 @@ function EditablePage({
1219
1421
  } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
1220
1422
  e.preventDefault();
1221
1423
  paste(selection);
1424
+ } else if (selection && /^Arrow(Left|Right|Up|Down)$/.test(e.key)) {
1425
+ const { transforms: tf, onNodeTransform: commit } = nudgeRef.current;
1426
+ if (!commit) return;
1427
+ e.preventDefault();
1428
+ const step = e.shiftKey ? 10 : 1;
1429
+ const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
1430
+ const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
1431
+ const cur = tf?.[selection] ?? {};
1432
+ commit(selection, { ...cur, x: (cur.x ?? 0) + dx, y: (cur.y ?? 0) + dy });
1222
1433
  }
1223
1434
  };
1224
1435
  window.addEventListener("keydown", onKey);
@@ -1230,8 +1441,23 @@ function EditablePage({
1230
1441
  if (el) ed.select(el.getAttribute("data-cms"));
1231
1442
  else ed.select(null);
1232
1443
  };
1233
- const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
1234
- 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 }) }) : page;
1444
+ const page = /* @__PURE__ */ jsxRuntime.jsx(
1445
+ "div",
1446
+ {
1447
+ ref: pageRef,
1448
+ onClickCapture,
1449
+ onContextMenu: onContextMenuSelect,
1450
+ onDragOver: editing ? onPageDragOver : void 0,
1451
+ onDragLeave: editing ? () => setDropHint(null) : void 0,
1452
+ onDrop: editing ? onPageDrop : void 0,
1453
+ children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry, data })
1454
+ }
1455
+ );
1456
+ 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 ? (
1457
+ // Normal flow, but "expanded" — extra scroll length below the content so a
1458
+ // page can be made longer than its content (the + Frame control).
1459
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { minHeight: `${frames * 100}vh` }, children: page })
1460
+ ) : page;
1235
1461
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1236
1462
  editing ? /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu, { children: [
1237
1463
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Trigger, { children: pageShell }),
@@ -1272,6 +1498,7 @@ function EditablePage({
1272
1498
  measured: { w: box.w, h: box.h },
1273
1499
  onProps: setProps,
1274
1500
  onStyle: setStyle,
1501
+ onLayout: setLayout,
1275
1502
  onTransform: (t) => selection && onNodeTransform?.(selection, t),
1276
1503
  onRemove: removeSelected,
1277
1504
  onClose: () => ed.select(null)
@@ -1292,16 +1519,101 @@ function EditablePage({
1292
1519
  onAdd: addNode
1293
1520
  }
1294
1521
  ) : null,
1522
+ editing && dropHint?.rect ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, { hint: dropHint }) : null,
1523
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(ElementPalette, { open: paletteOpen, onDragChange: setDraggingEl }) : null,
1295
1524
  editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
1296
1525
  ] });
1297
1526
  }
1298
- var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
1527
+ function DropIndicator({
1528
+ hint
1529
+ }) {
1530
+ const r = hint.rect;
1531
+ if (!r) return null;
1532
+ const color = "#ec4899";
1533
+ if (hint.edge === "inside") {
1534
+ 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 } });
1535
+ }
1536
+ const y = hint.edge === "before" ? r.y : r.y + r.h;
1537
+ 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 } });
1538
+ }
1539
+ function ElementPalette({ open, onDragChange }) {
1540
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1541
+ "div",
1542
+ {
1543
+ style: {
1544
+ position: "fixed",
1545
+ top: 64,
1546
+ bottom: 96,
1547
+ right: 0,
1548
+ width: 216,
1549
+ transform: open ? "translateX(0)" : "translateX(100%)",
1550
+ transition: "transform 0.18s ease",
1551
+ background: "#0b1220",
1552
+ color: "#e2e8f0",
1553
+ borderLeft: "1px solid #1e293b",
1554
+ borderTopLeftRadius: 12,
1555
+ borderBottomLeftRadius: 12,
1556
+ boxShadow: "-18px 0 48px -20px rgba(0,0,0,0.6)",
1557
+ zIndex: Z2 + 2,
1558
+ fontFamily: "system-ui, sans-serif",
1559
+ padding: 12,
1560
+ overflowY: "auto"
1561
+ },
1562
+ children: [
1563
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5, marginBottom: 10 }, children: "Elements" }),
1564
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }, children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsxs(
1565
+ "div",
1566
+ {
1567
+ draggable: true,
1568
+ onDragStart: (e) => {
1569
+ e.dataTransfer.setData("application/x-cms-element", m.kind);
1570
+ e.dataTransfer.effectAllowed = "copy";
1571
+ onDragChange(true);
1572
+ },
1573
+ onDragEnd: () => onDragChange(false),
1574
+ title: `Drag "${m.label}" onto the page`,
1575
+ style: {
1576
+ display: "flex",
1577
+ flexDirection: "column",
1578
+ alignItems: "center",
1579
+ justifyContent: "center",
1580
+ gap: 4,
1581
+ height: 56,
1582
+ borderRadius: 8,
1583
+ background: "#0f172a",
1584
+ border: "1px solid #1e293b",
1585
+ cursor: "grab",
1586
+ fontSize: 12,
1587
+ userSelect: "none"
1588
+ },
1589
+ children: [
1590
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 16, opacity: 0.7 }, children: "\uFF0B" }),
1591
+ m.label
1592
+ ]
1593
+ },
1594
+ m.kind
1595
+ )) }),
1596
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: 10.5, opacity: 0.45, marginTop: 12, lineHeight: 1.4 }, children: "Drag an Element onto the page. It drops into the container you release over." })
1597
+ ]
1598
+ }
1599
+ );
1600
+ }
1601
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device", "repeater"]);
1299
1602
  var ADD_DEFAULTS = {
1300
1603
  text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
1301
1604
  heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
1302
1605
  button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
1606
+ link: { type: "link", props: { content: "link text", href: "#" }, style: { color: "#7c3aed" } },
1303
1607
  image: { type: "image", props: { src: "", alt: "" }, style: {} },
1304
- box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } }
1608
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } },
1609
+ stack: { type: "stack", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
1610
+ grid: { type: "grid", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "grid" },
1611
+ card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
1612
+ callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
1613
+ divider: { type: "divider", props: {}, style: {} },
1614
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
1615
+ richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
1616
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
1305
1617
  };
1306
1618
  var GRIPS = [
1307
1619
  { key: "nw", north: true, west: true, cursor: "nwse-resize" },
@@ -1322,20 +1634,55 @@ function SelectionOverlay({
1322
1634
  onLive
1323
1635
  }) {
1324
1636
  const drag = react.useRef(null);
1637
+ const targets = react.useRef({ vx: [], hy: [] });
1638
+ const [guides, setGuides] = react.useState({ x: null, y: null });
1325
1639
  const apply = (t) => {
1326
1640
  const el = getEl();
1327
1641
  if (el) applyTransform(el, t);
1328
1642
  };
1643
+ const snap = (cands, lines) => {
1644
+ let best = { delta: 0, at: null, dist: SNAP + 1 };
1645
+ for (const line of lines) {
1646
+ for (const c of cands) {
1647
+ const diff = line - c;
1648
+ if (Math.abs(diff) < best.dist) best = { delta: diff, at: line, dist: Math.abs(diff) };
1649
+ }
1650
+ }
1651
+ return best.dist <= SNAP ? { delta: best.delta, at: best.at } : { delta: 0, at: null };
1652
+ };
1329
1653
  const startMove = (e) => {
1330
1654
  e.stopPropagation();
1331
1655
  e.preventDefault();
1332
1656
  e.currentTarget.setPointerCapture(e.pointerId);
1333
1657
  drag.current = { x: e.clientX, y: e.clientY, w: box.w, h: box.h, live: { ...base } };
1658
+ const sel = getEl();
1659
+ const vx = /* @__PURE__ */ new Set();
1660
+ const hy = /* @__PURE__ */ new Set();
1661
+ document.querySelectorAll("[data-cms]").forEach((el) => {
1662
+ if (sel && (el === sel || sel.contains(el) || el.contains(sel))) return;
1663
+ const r = el.getBoundingClientRect();
1664
+ vx.add(r.left).add(r.left + r.width / 2).add(r.right);
1665
+ hy.add(r.top).add(r.top + r.height / 2).add(r.bottom);
1666
+ });
1667
+ targets.current = { vx: [...vx], hy: [...hy] };
1334
1668
  };
1335
1669
  const moveMove = (e) => {
1336
1670
  const d = drag.current;
1337
1671
  if (!d) return;
1338
- d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
1672
+ let dx = e.clientX - d.x;
1673
+ let dy = e.clientY - d.y;
1674
+ if (!e.altKey) {
1675
+ const l = box.x + dx;
1676
+ const t = box.y + dy;
1677
+ const sx = snap([l, l + box.w / 2, l + box.w], targets.current.vx);
1678
+ const sy = snap([t, t + box.h / 2, t + box.h], targets.current.hy);
1679
+ dx += sx.delta;
1680
+ dy += sy.delta;
1681
+ setGuides({ x: sx.at, y: sy.at });
1682
+ } else if (guides.x !== null || guides.y !== null) {
1683
+ setGuides({ x: null, y: null });
1684
+ }
1685
+ d.live = { ...base, x: (base.x ?? 0) + dx, y: (base.y ?? 0) + dy };
1339
1686
  apply(d.live);
1340
1687
  onLive();
1341
1688
  };
@@ -1370,6 +1717,7 @@ function SelectionOverlay({
1370
1717
  const d = drag.current;
1371
1718
  if (d) onTransform(d.live);
1372
1719
  drag.current = null;
1720
+ setGuides({ x: null, y: null });
1373
1721
  };
1374
1722
  const outline = {
1375
1723
  position: "fixed",
@@ -1399,6 +1747,8 @@ function SelectionOverlay({
1399
1747
  return { left: cx - 6, top: cy - 6, cursor: g.cursor };
1400
1748
  };
1401
1749
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1750
+ guides.x !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: guides.x, top: 0, bottom: 0, width: 1, background: "#ec4899", zIndex: Z2 + 3, pointerEvents: "none" } }) : null,
1751
+ guides.y !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", top: guides.y, left: 0, right: 0, height: 1, background: "#ec4899", zIndex: Z2 + 3, pointerEvents: "none" } }) : null,
1402
1752
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: outline }),
1403
1753
  movable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1404
1754
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1463,11 +1813,20 @@ function FloatingToolbar({
1463
1813
  ] });
1464
1814
  }
1465
1815
  var ADD_MENU = [
1466
- { kind: "text", label: "Text" },
1467
1816
  { kind: "heading", label: "Heading" },
1817
+ { kind: "text", label: "Text" },
1468
1818
  { kind: "button", label: "Button" },
1819
+ { kind: "link", label: "Link" },
1469
1820
  { kind: "image", label: "Image" },
1470
- { kind: "box", label: "Box" }
1821
+ { kind: "card", label: "Card" },
1822
+ { kind: "callout", label: "Callout" },
1823
+ { kind: "stack", label: "Stack" },
1824
+ { kind: "grid", label: "Grid" },
1825
+ { kind: "box", label: "Box" },
1826
+ { kind: "divider", label: "Divider" },
1827
+ { kind: "code", label: "Code" },
1828
+ { kind: "richtext", label: "Rich text" },
1829
+ { kind: "repeater", label: "Repeater" }
1471
1830
  ];
1472
1831
  function EditBar({
1473
1832
  editing,
@@ -1485,7 +1844,7 @@ function EditBar({
1485
1844
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
1486
1845
  editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1487
1846
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1488
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an element", children: "\uFF0B Add \u25BE" }),
1847
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an Element (or drag from the right-edge palette)", children: "\uFF0B Element \u25BE" }),
1489
1848
  open ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: "calc(100% + 6px)", right: 0, background: "#0f172a", border: "1px solid #1e293b", borderRadius: 10, padding: 4, display: "flex", flexDirection: "column", minWidth: 132, boxShadow: "0 12px 32px -10px rgba(0,0,0,0.6)" }, children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsx(
1490
1849
  "button",
1491
1850
  {
@@ -1523,7 +1882,7 @@ function EditMenuItems({
1523
1882
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1524
1883
  editable ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1525
1884
  /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu.Sub, { children: [
1526
- /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1885
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add Element inside" : "Add Element" }),
1527
1886
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubContent, { className: "!z-[2147483647]", children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onAdd(m.kind), children: m.label }, m.kind)) })
1528
1887
  ] }),
1529
1888
  node ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [