@particle-academy/fancy-cms-ui 0.0.1-dev.7 → 0.0.1-dev.9

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
@@ -2,6 +2,7 @@
2
2
 
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
+ var reactFancy = require('@particle-academy/react-fancy');
5
6
 
6
7
  // src/editor/Editor.tsx
7
8
 
@@ -97,6 +98,10 @@ function reduce(doc, op, opts = {}) {
97
98
  props: { ...doc.nodes[op.id].props, ...op.patch }
98
99
  });
99
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
+ }
100
105
  case "set_style": {
101
106
  const node = doc.nodes[op.id];
102
107
  if (!node) return invalid("node not found");
@@ -512,31 +517,110 @@ function Canvas({ doc, selection, onSelect, apply }) {
512
517
  function cssEscape(value) {
513
518
  return value.replace(/["\\]/g, "\\$&");
514
519
  }
515
- function LayersPanel({ doc, selection, onSelect }) {
516
- 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)) });
520
+
521
+ // src/document/fractional.ts
522
+ var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
523
+ var RADIX = BASE.length;
524
+ var MID = Math.floor(RADIX / 2);
525
+ function val(c) {
526
+ const i = BASE.indexOf(c);
527
+ return i < 0 ? 0 : i;
528
+ }
529
+ function keyBetween(a, b) {
530
+ if (a === null) a = "";
531
+ if (b === null) {
532
+ return a + BASE[MID];
533
+ }
534
+ if (a >= b) {
535
+ throw new Error(`keyBetween: lower bound "${a}" is not below upper bound "${b}"`);
536
+ }
537
+ let result = "";
538
+ let i = 0;
539
+ for (; ; ) {
540
+ const ca = i < a.length ? val(a[i]) : 0;
541
+ const cb = i < b.length ? val(b[i]) : RADIX;
542
+ if (cb - ca > 1) {
543
+ result += BASE[ca + Math.floor((cb - ca) / 2)];
544
+ return result;
545
+ }
546
+ if (ca === cb) {
547
+ result += BASE[ca];
548
+ i++;
549
+ continue;
550
+ }
551
+ result += BASE[ca];
552
+ i++;
553
+ for (; ; ) {
554
+ const da = i < a.length ? val(a[i]) : 0;
555
+ if (RADIX - da > 1) {
556
+ result += BASE[da + Math.floor((RADIX - da) / 2)];
557
+ return result;
558
+ }
559
+ result += BASE[da];
560
+ i++;
561
+ }
562
+ }
563
+ }
564
+ var MIME = "application/x-cms-node";
565
+ function isAncestorOrSelf(doc, id, maybeAncestor) {
566
+ for (let cur = id; cur; cur = doc.nodes[cur]?.parent ?? null) {
567
+ if (cur === maybeAncestor) return true;
568
+ }
569
+ return false;
570
+ }
571
+ function LayersPanel({ doc, selection, onSelect, onMove }) {
572
+ const [overId, setOverId] = react.useState(null);
573
+ 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)) });
517
574
  }
518
- function LayerRow({ doc, id, depth, selection, onSelect }) {
575
+ function LayerRow({ doc, id, depth, selection, onSelect, onMove, overId, setOverId }) {
519
576
  const node = doc.nodes[id];
520
577
  if (!node) return null;
521
578
  const selected = selection === id;
579
+ const draggable = Boolean(onMove);
580
+ const drop = (e) => {
581
+ e.preventDefault();
582
+ e.stopPropagation();
583
+ setOverId(null);
584
+ if (!onMove) return;
585
+ const dragId = e.dataTransfer.getData(MIME);
586
+ if (!dragId || dragId === id || isAncestorOrSelf(doc, id, dragId)) return;
587
+ const siblings = childrenOf(doc, node.parent);
588
+ const idx = siblings.findIndex((n) => n.id === id);
589
+ const next = siblings[idx + 1];
590
+ onMove(dragId, node.parent, keyBetween(node.order, next ? next.order : null));
591
+ };
522
592
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
523
593
  /* @__PURE__ */ jsxRuntime.jsxs(
524
594
  "button",
525
595
  {
526
596
  type: "button",
597
+ draggable,
527
598
  onClick: () => onSelect(id),
599
+ onDragStart: (e) => {
600
+ e.dataTransfer.setData(MIME, id);
601
+ e.dataTransfer.effectAllowed = "move";
602
+ },
603
+ onDragOver: (e) => {
604
+ if (!onMove) return;
605
+ e.preventDefault();
606
+ e.dataTransfer.dropEffect = "move";
607
+ if (overId !== id) setOverId(id);
608
+ },
609
+ onDragLeave: () => overId === id && setOverId(null),
610
+ onDrop: drop,
528
611
  style: {
529
612
  display: "block",
530
613
  width: "100%",
531
614
  textAlign: "left",
532
615
  border: "none",
533
616
  borderRadius: 6,
534
- cursor: "pointer",
617
+ cursor: draggable ? "grab" : "pointer",
535
618
  padding: "4px 8px",
536
619
  paddingLeft: 8 + depth * 14,
537
620
  background: selected ? "#ede9fe" : "transparent",
538
621
  color: selected ? "#5b21b6" : "#334155",
539
- font: "inherit"
622
+ font: "inherit",
623
+ boxShadow: overId === id ? "inset 0 -2px 0 0 #8b5cf6" : "none"
540
624
  },
541
625
  children: [
542
626
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.6 }, children: node.type }),
@@ -545,17 +629,7 @@ function LayerRow({ doc, id, depth, selection, onSelect }) {
545
629
  ]
546
630
  }
547
631
  ),
548
- childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(
549
- LayerRow,
550
- {
551
- doc,
552
- id: child.id,
553
- depth: depth + 1,
554
- selection,
555
- onSelect
556
- },
557
- child.id
558
- ))
632
+ childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(LayerRow, { doc, id: child.id, depth: depth + 1, selection, onSelect, onMove, overId, setOverId }, child.id))
559
633
  ] });
560
634
  }
561
635
  var label = {
@@ -687,7 +761,15 @@ function Editor({ defaultValue, onChange }) {
687
761
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: shell, children: [
688
762
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
689
763
  /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { ed }),
690
- /* @__PURE__ */ jsxRuntime.jsx(LayersPanel, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select })
764
+ /* @__PURE__ */ jsxRuntime.jsx(
765
+ LayersPanel,
766
+ {
767
+ doc: ed.state.doc,
768
+ selection: ed.state.selection,
769
+ onSelect: ed.select,
770
+ onMove: (id, parent, order) => ed.apply({ t: "move_node", id, parent, order })
771
+ }
772
+ )
691
773
  ] }),
692
774
  /* @__PURE__ */ jsxRuntime.jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
693
775
  /* @__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 }) })
@@ -720,17 +802,6 @@ function Toolbar({ ed }) {
720
802
  }
721
803
  );
722
804
  }
723
-
724
- // src/document/fractional.ts
725
- var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
726
- var RADIX = BASE.length;
727
- var MID = Math.floor(RADIX / 2);
728
- function keyBetween(a, b) {
729
- if (a === null) a = "";
730
- {
731
- return a + BASE[MID];
732
- }
733
- }
734
805
  var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
735
806
  function NodeInspector({
736
807
  node,
@@ -738,6 +809,7 @@ function NodeInspector({
738
809
  measured,
739
810
  onProps,
740
811
  onStyle,
812
+ onLayout,
741
813
  onTransform,
742
814
  onRemove,
743
815
  onClose
@@ -820,9 +892,44 @@ function NodeInspector({
820
892
  ] }),
821
893
  /* @__PURE__ */ jsxRuntime.jsxs(Row, { children: [
822
894
  /* @__PURE__ */ jsxRuntime.jsx(Num, { label: "Padding", value: base.padding?.value, placeholder: "0", onChange: (v) => onStyle({ padding: v == null ? void 0 : len(v) }) }),
895
+ /* @__PURE__ */ jsxRuntime.jsx(Num, { label: "Margin", value: base.margin?.value, placeholder: "0", onChange: (v) => onStyle({ margin: v == null ? void 0 : len(v) }) })
896
+ ] }),
897
+ /* @__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 }) }) })
898
+ ] }),
899
+ CONTAINER_TYPES.has(node.type) ? /* @__PURE__ */ jsxRuntime.jsxs(Group, { label: "Layout", children: [
900
+ /* @__PURE__ */ jsxRuntime.jsxs(Row, { children: [
901
+ /* @__PURE__ */ jsxRuntime.jsx(
902
+ Sel,
903
+ {
904
+ label: "Mode",
905
+ value: node.layout ?? "free",
906
+ options: [["free", "Free"], ["stack", "Stack"], ["grid", "Grid"]],
907
+ onChange: (v) => onLayout?.(v)
908
+ }
909
+ ),
823
910
  /* @__PURE__ */ jsxRuntime.jsx(Num, { label: "Gap", value: base.gap?.value, placeholder: "0", onChange: (v) => onStyle({ gap: v == null ? void 0 : len(v) }) })
911
+ ] }),
912
+ /* @__PURE__ */ jsxRuntime.jsxs(Row, { children: [
913
+ /* @__PURE__ */ jsxRuntime.jsx(
914
+ Sel,
915
+ {
916
+ label: "Align",
917
+ value: base.align ?? "",
918
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["stretch", "Stretch"]],
919
+ onChange: (v) => onStyle({ align: v || void 0 })
920
+ }
921
+ ),
922
+ /* @__PURE__ */ jsxRuntime.jsx(
923
+ Sel,
924
+ {
925
+ label: "Justify",
926
+ value: base.justify ?? "",
927
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["between", "Between"], ["around", "Around"]],
928
+ onChange: (v) => onStyle({ justify: v || void 0 })
929
+ }
930
+ )
824
931
  ] })
825
- ] })
932
+ ] }) : null
826
933
  ] })
827
934
  ] });
828
935
  }
@@ -877,6 +984,12 @@ function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
877
984
  )
878
985
  ] });
879
986
  }
987
+ function Txt({ label: label2, value, placeholder, onChange }) {
988
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
989
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: fieldLabel, children: label2 }),
990
+ /* @__PURE__ */ jsxRuntime.jsx("input", { style: input2, type: "text", value, placeholder, onChange: (e) => onChange(e.target.value) })
991
+ ] });
992
+ }
880
993
  function Color({ label: label2, value, onChange }) {
881
994
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
882
995
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: fieldLabel, children: label2 }),
@@ -926,6 +1039,110 @@ var input2 = {
926
1039
  };
927
1040
  var iconBtn = { background: "transparent", border: "none", color: "#e2e8f0", cursor: "pointer", fontSize: 13, padding: 4, borderRadius: 6 };
928
1041
  var miniBtn = { font: "inherit", fontSize: 11, color: "#e2e8f0", background: "#334155", border: "1px solid #475569", borderRadius: 6, padding: "5px 10px", cursor: "pointer" };
1042
+
1043
+ // src/editor/editorOps.ts
1044
+ function childrenOfMap(nodes, parent) {
1045
+ return Object.values(nodes).filter((n) => n.parent === parent).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
1046
+ }
1047
+ function subtreeBfs(nodes, root) {
1048
+ const out = [];
1049
+ const queue = [root];
1050
+ while (queue.length) {
1051
+ const cur = queue.shift();
1052
+ out.push(cur);
1053
+ for (const child of childrenOfMap(nodes, cur)) queue.push(child.id);
1054
+ }
1055
+ return out;
1056
+ }
1057
+ function cloneFrom(nodes, rootId, newParent, newOrder, seqBase) {
1058
+ if (!nodes[rootId]) return { ops: [], newRootId: null };
1059
+ const order = subtreeBfs(nodes, rootId);
1060
+ const idMap = {};
1061
+ order.forEach((old, i) => idMap[old] = `${old}_c${seqBase}_${i}`);
1062
+ const ops = order.map((old) => {
1063
+ const src = nodes[old];
1064
+ const cloned = JSON.parse(JSON.stringify(src));
1065
+ cloned.id = idMap[old];
1066
+ cloned.parent = old === rootId ? newParent : idMap[src.parent];
1067
+ if (old === rootId) cloned.order = newOrder;
1068
+ return { t: "insert_node", node: cloned };
1069
+ });
1070
+ return { ops, newRootId: idMap[rootId] };
1071
+ }
1072
+ function duplicateOps(doc, id) {
1073
+ const node = doc.nodes[id];
1074
+ if (!node) return { ops: [], newRootId: null };
1075
+ const sibs = childrenOf(doc, node.parent);
1076
+ const idx = sibs.findIndex((s) => s.id === id);
1077
+ const newOrder = keyBetween(node.order, sibs[idx + 1]?.order ?? null);
1078
+ const { ops, newRootId } = cloneFrom(doc.nodes, id, node.parent, newOrder, doc.seq);
1079
+ if (node.parent === null && newRootId) {
1080
+ const base = doc.sections;
1081
+ const at = base.indexOf(id);
1082
+ ops.push({ t: "reorder_sections", order: [...base.slice(0, at + 1), newRootId, ...base.slice(at + 1)] });
1083
+ }
1084
+ return { ops, newRootId };
1085
+ }
1086
+ function pasteOps(doc, clip, target, containerTypes) {
1087
+ const sel = target ? doc.nodes[target] : null;
1088
+ const parent = sel ? containerTypes.has(sel.type) ? sel.id : sel.parent : doc.sections[doc.sections.length - 1] ?? null;
1089
+ const sibs = childrenOf(doc, parent);
1090
+ const newOrder = keyBetween(sibs.length ? sibs[sibs.length - 1].order : null, null);
1091
+ const { ops, newRootId } = cloneFrom(clip.nodes, clip.rootId, parent, newOrder, doc.seq);
1092
+ return { ops, newRootId };
1093
+ }
1094
+ function reorderOps(doc, id, dir) {
1095
+ const node = doc.nodes[id];
1096
+ if (!node) return [];
1097
+ if (node.parent === null) {
1098
+ const s = [...doc.sections];
1099
+ const i = s.indexOf(id);
1100
+ if (i < 0) return [];
1101
+ s.splice(i, 1);
1102
+ if (dir === "front") s.unshift(id);
1103
+ else if (dir === "back") s.push(id);
1104
+ else s.splice(Math.max(0, Math.min(s.length, dir === "up" ? i - 1 : i + 1)), 0, id);
1105
+ return [{ t: "reorder_sections", order: s }];
1106
+ }
1107
+ const sibs = childrenOf(doc, node.parent);
1108
+ const idx = sibs.findIndex((sb) => sb.id === id);
1109
+ if (idx < 0) return [];
1110
+ const last = sibs.length - 1;
1111
+ let newOrder;
1112
+ if (dir === "up") {
1113
+ if (idx <= 0) return [];
1114
+ newOrder = keyBetween(sibs[idx - 2]?.order ?? null, sibs[idx - 1].order);
1115
+ } else if (dir === "down") {
1116
+ if (idx >= last) return [];
1117
+ newOrder = keyBetween(sibs[idx + 1].order, sibs[idx + 2]?.order ?? null);
1118
+ } else if (dir === "front") {
1119
+ if (idx >= last) return [];
1120
+ newOrder = keyBetween(sibs[last].order, null);
1121
+ } else {
1122
+ if (idx <= 0) return [];
1123
+ newOrder = keyBetween(null, sibs[0].order);
1124
+ }
1125
+ return [{ t: "move_node", id, parent: node.parent, order: newOrder }];
1126
+ }
1127
+ function wrapInBoxOps(doc, id) {
1128
+ const node = doc.nodes[id];
1129
+ if (!node || node.parent === null) return { ops: [], newRootId: null };
1130
+ const frameId = `box_${doc.seq}`;
1131
+ return {
1132
+ ops: [
1133
+ { t: "insert_node", node: { id: frameId, type: "frame", parent: node.parent, order: node.order, props: {}, style: { base: { padding: { value: 16, unit: "px" } } } } },
1134
+ { t: "move_node", id, parent: frameId, order: "a" }
1135
+ ],
1136
+ newRootId: frameId
1137
+ };
1138
+ }
1139
+ function snapshotSubtree(doc, id) {
1140
+ if (!doc.nodes[id]) return null;
1141
+ const ids = subtreeBfs(doc.nodes, id);
1142
+ const nodes = {};
1143
+ for (const nid of ids) nodes[nid] = JSON.parse(JSON.stringify(doc.nodes[nid]));
1144
+ return { rootId: id, nodes };
1145
+ }
929
1146
  var Z2 = 2147483e3;
930
1147
  function EditablePage({
931
1148
  doc,
@@ -1029,7 +1246,7 @@ function EditablePage({
1029
1246
  const doc2 = ed.state.doc;
1030
1247
  const parent = selNode ? CONTAINER_TYPES2.has(selNode.type) ? selNode.id : selNode.parent : doc2.sections[doc2.sections.length - 1] ?? null;
1031
1248
  const siblings = childrenOf(doc2, parent);
1032
- const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
1249
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
1033
1250
  const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1034
1251
  const def = ADD_DEFAULTS[kind];
1035
1252
  ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
@@ -1039,14 +1256,93 @@ function EditablePage({
1039
1256
  );
1040
1257
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
1041
1258
  const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
1259
+ const setLayout = (layout) => selection && ed.apply({ t: "set_layout", id: selection, layout });
1042
1260
  const removeSelected = () => {
1043
1261
  if (!selection) return;
1044
1262
  ed.apply({ t: "remove_node", id: selection });
1045
1263
  ed.select(null);
1046
1264
  };
1047
- const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
1265
+ const clip = react.useRef(null);
1266
+ const runOps = (result) => {
1267
+ result.ops.forEach((op) => ed.apply(op));
1268
+ if (result.newRootId) ed.select(result.newRootId);
1269
+ };
1270
+ const duplicate = (id) => id && runOps(duplicateOps(ed.state.doc, id));
1271
+ const reorder = (id, dir) => id && reorderOps(ed.state.doc, id, dir).forEach((op) => ed.apply(op));
1272
+ const wrapInBox = (id) => id && runOps(wrapInBoxOps(ed.state.doc, id));
1273
+ const copy = (id) => {
1274
+ if (id) clip.current = snapshotSubtree(ed.state.doc, id);
1275
+ };
1276
+ const paste = (target) => {
1277
+ if (clip.current) runOps(pasteOps(ed.state.doc, clip.current, target, CONTAINER_TYPES2));
1278
+ };
1279
+ const startEditText = (id) => {
1280
+ if (!id) return;
1281
+ ed.select(id);
1282
+ requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
1283
+ };
1284
+ const nudgeRef = react.useRef({ transforms, onNodeTransform });
1285
+ nudgeRef.current = { transforms, onNodeTransform };
1286
+ react.useEffect(() => {
1287
+ if (!editing) return;
1288
+ const onKey = (e) => {
1289
+ const t = e.target;
1290
+ const typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
1291
+ if (e.key === "Escape") return void ed.select(null);
1292
+ if (typing) return;
1293
+ if ((e.key === "Delete" || e.key === "Backspace") && selection) {
1294
+ e.preventDefault();
1295
+ removeSelected();
1296
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d" && selection) {
1297
+ e.preventDefault();
1298
+ duplicate(selection);
1299
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selection) {
1300
+ copy(selection);
1301
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
1302
+ e.preventDefault();
1303
+ paste(selection);
1304
+ } else if (selection && /^Arrow(Left|Right|Up|Down)$/.test(e.key)) {
1305
+ const { transforms: tf, onNodeTransform: commit } = nudgeRef.current;
1306
+ if (!commit) return;
1307
+ e.preventDefault();
1308
+ const step = e.shiftKey ? 10 : 1;
1309
+ const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
1310
+ const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
1311
+ const cur = tf?.[selection] ?? {};
1312
+ commit(selection, { ...cur, x: (cur.x ?? 0) + dx, y: (cur.y ?? 0) + dy });
1313
+ }
1314
+ };
1315
+ window.addEventListener("keydown", onKey);
1316
+ return () => window.removeEventListener("keydown", onKey);
1317
+ }, [editing, selection]);
1318
+ const onContextMenuSelect = (e) => {
1319
+ if (!editing) return;
1320
+ const el = e.target.closest("[data-cms]");
1321
+ if (el) ed.select(el.getAttribute("data-cms"));
1322
+ else ed.select(null);
1323
+ };
1324
+ const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
1325
+ 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;
1048
1326
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1049
- 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,
1327
+ editing ? /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu, { children: [
1328
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Trigger, { children: pageShell }),
1329
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Content, { className: "!z-[2147483646]", children: /* @__PURE__ */ jsxRuntime.jsx(
1330
+ EditMenuItems,
1331
+ {
1332
+ node: selNode ?? null,
1333
+ hasClipboard: Boolean(clip.current),
1334
+ isContainer: selNode ? CONTAINER_TYPES2.has(selNode.type) : false,
1335
+ onEditText: () => startEditText(selection),
1336
+ onAdd: addNode,
1337
+ onDuplicate: () => duplicate(selection),
1338
+ onCopy: () => copy(selection),
1339
+ onPaste: () => paste(selection),
1340
+ onWrap: () => wrapInBox(selection),
1341
+ onReorder: (d) => reorder(selection, d),
1342
+ onDelete: removeSelected
1343
+ }
1344
+ ) })
1345
+ ] }) : pageShell,
1050
1346
  editing && box && selection ? /* @__PURE__ */ jsxRuntime.jsx(
1051
1347
  SelectionOverlay,
1052
1348
  {
@@ -1067,6 +1363,7 @@ function EditablePage({
1067
1363
  measured: { w: box.w, h: box.h },
1068
1364
  onProps: setProps,
1069
1365
  onStyle: setStyle,
1366
+ onLayout: setLayout,
1070
1367
  onTransform: (t) => selection && onNodeTransform?.(selection, t),
1071
1368
  onRemove: removeSelected,
1072
1369
  onClose: () => ed.select(null)
@@ -1300,6 +1597,45 @@ function EditBar({
1300
1597
  ] }) : null
1301
1598
  ] });
1302
1599
  }
1600
+ function EditMenuItems({
1601
+ node,
1602
+ hasClipboard,
1603
+ isContainer,
1604
+ onEditText,
1605
+ onAdd,
1606
+ onDuplicate,
1607
+ onCopy,
1608
+ onPaste,
1609
+ onWrap,
1610
+ onReorder,
1611
+ onDelete
1612
+ }) {
1613
+ const editable = node && (node.type === "text" || node.type === "heading");
1614
+ const nested = Boolean(node && node.parent !== null);
1615
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1616
+ editable ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1617
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu.Sub, { children: [
1618
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1619
+ /* @__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)) })
1620
+ ] }),
1621
+ node ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1622
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
1623
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
1624
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
1625
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" }),
1626
+ nested ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onWrap, children: "Wrap in Box" }) : null,
1627
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
1628
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
1629
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
1630
+ nested ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1631
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
1632
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
1633
+ ] }) : null,
1634
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
1635
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { danger: true, onClick: onDelete, children: "Delete" })
1636
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" })
1637
+ ] });
1638
+ }
1303
1639
  function applyTransform(el, t) {
1304
1640
  if (t && (t.x != null || t.y != null || t.scale != null || t.rotate != null || t.opacity != null || t.w != null || t.h != null)) {
1305
1641
  el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1}) rotate(${t.rotate ?? 0}deg)`;