@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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
- import { P as PageDoc, N as Node, m as StyleProps } from './types-CpfPEq3V.cjs';
2
+ import { P as PageDoc, i as NodeId, N as Node, m as StyleProps, L as LayoutMode } from './types-CpfPEq3V.cjs';
3
3
  import { a as ElementRegistry } from './registry-Bk7GN1rA.cjs';
4
- import { P as PageOp } from './ops-C_s0FGeV.cjs';
4
+ import { P as PageOp } from './ops-CdBZarAT.cjs';
5
5
 
6
6
  interface EditorProps {
7
7
  /** Initial document; edits are surfaced via {@link EditorProps.onChange}. */
@@ -73,9 +73,11 @@ interface LayersPanelProps {
73
73
  doc: PageDoc;
74
74
  selection: string | null;
75
75
  onSelect: (id: string) => void;
76
+ /** Reparent/reorder a node (drag-to-reorder). Omit to disable dragging. */
77
+ onMove?: (id: NodeId, parent: NodeId | null, order: string) => void;
76
78
  }
77
- /** The section/layer tree. Click a row to select. */
78
- declare function LayersPanel({ doc, selection, onSelect }: LayersPanelProps): ReactElement;
79
+ /** The section/layer tree. Click a row to select; drag a row onto another to reorder. */
80
+ declare function LayersPanel({ doc, selection, onSelect, onMove }: LayersPanelProps): ReactElement;
79
81
 
80
82
  interface InspectorProps {
81
83
  doc: PageDoc;
@@ -98,6 +100,8 @@ interface NodeInspectorProps {
98
100
  onProps: (patch: Record<string, unknown>) => void;
99
101
  /** Patch the node's base style. */
100
102
  onStyle: (patch: Partial<StyleProps>) => void;
103
+ /** Set how a container arranges its children (free / stack / grid). */
104
+ onLayout?: (layout: LayoutMode | undefined) => void;
101
105
  /** Commit a transform (position / size / opacity / scale / rotate) into the active keyframe. */
102
106
  onTransform: (t: NodeTransform) => void;
103
107
  onRemove: () => void;
@@ -111,7 +115,7 @@ interface NodeInspectorProps {
111
115
  * box. Every control writes through the op-spine or the active keyframe, so edits
112
116
  * are undoable and animatable.
113
117
  */
114
- declare function NodeInspector({ node, transform, measured, onProps, onStyle, onTransform, onRemove, onClose, }: NodeInspectorProps): ReactElement;
118
+ declare function NodeInspector({ node, transform, measured, onProps, onStyle, onLayout, onTransform, onRemove, onClose, }: NodeInspectorProps): ReactElement;
115
119
 
116
120
  /**
117
121
  * Editor state engine — pure and framework-agnostic so it's unit-testable.
package/dist/editor.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
- import { P as PageDoc, N as Node, m as StyleProps } from './types-CpfPEq3V.js';
2
+ import { P as PageDoc, i as NodeId, N as Node, m as StyleProps, L as LayoutMode } from './types-CpfPEq3V.js';
3
3
  import { a as ElementRegistry } from './registry-Dht9JBLU.js';
4
- import { P as PageOp } from './ops-nevYkDzK.js';
4
+ import { P as PageOp } from './ops-BsM6kZxY.js';
5
5
 
6
6
  interface EditorProps {
7
7
  /** Initial document; edits are surfaced via {@link EditorProps.onChange}. */
@@ -73,9 +73,11 @@ interface LayersPanelProps {
73
73
  doc: PageDoc;
74
74
  selection: string | null;
75
75
  onSelect: (id: string) => void;
76
+ /** Reparent/reorder a node (drag-to-reorder). Omit to disable dragging. */
77
+ onMove?: (id: NodeId, parent: NodeId | null, order: string) => void;
76
78
  }
77
- /** The section/layer tree. Click a row to select. */
78
- declare function LayersPanel({ doc, selection, onSelect }: LayersPanelProps): ReactElement;
79
+ /** The section/layer tree. Click a row to select; drag a row onto another to reorder. */
80
+ declare function LayersPanel({ doc, selection, onSelect, onMove }: LayersPanelProps): ReactElement;
79
81
 
80
82
  interface InspectorProps {
81
83
  doc: PageDoc;
@@ -98,6 +100,8 @@ interface NodeInspectorProps {
98
100
  onProps: (patch: Record<string, unknown>) => void;
99
101
  /** Patch the node's base style. */
100
102
  onStyle: (patch: Partial<StyleProps>) => void;
103
+ /** Set how a container arranges its children (free / stack / grid). */
104
+ onLayout?: (layout: LayoutMode | undefined) => void;
101
105
  /** Commit a transform (position / size / opacity / scale / rotate) into the active keyframe. */
102
106
  onTransform: (t: NodeTransform) => void;
103
107
  onRemove: () => void;
@@ -111,7 +115,7 @@ interface NodeInspectorProps {
111
115
  * box. Every control writes through the op-spine or the active keyframe, so edits
112
116
  * are undoable and animatable.
113
117
  */
114
- declare function NodeInspector({ node, transform, measured, onProps, onStyle, onTransform, onRemove, onClose, }: NodeInspectorProps): ReactElement;
118
+ declare function NodeInspector({ node, transform, measured, onProps, onStyle, onLayout, onTransform, onRemove, onClose, }: NodeInspectorProps): ReactElement;
115
119
 
116
120
  /**
117
121
  * Editor state engine — pure and framework-agnostic so it's unit-testable.
package/dist/editor.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { keyBetween } from './chunk-7YYLYBLI.js';
2
- import { CmsPage, defaultRegistry } from './chunk-5O6U4VIU.js';
3
- import { reduce, childrenOf } from './chunk-HYHNMW47.js';
2
+ import { CmsPage, defaultRegistry } from './chunk-ZV7CV54P.js';
3
+ import { reduce, childrenOf } from './chunk-3YZMELJJ.js';
4
4
  import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useEffect } from 'react';
5
5
  import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
6
+ import { ContextMenu } from '@particle-academy/react-fancy';
6
7
 
7
8
  function initEditor(doc) {
8
9
  return { doc, past: [], future: [], selection: null };
@@ -156,31 +157,66 @@ function Canvas({ doc, selection, onSelect, apply }) {
156
157
  function cssEscape(value) {
157
158
  return value.replace(/["\\]/g, "\\$&");
158
159
  }
159
- function LayersPanel({ doc, selection, onSelect }) {
160
- return /* @__PURE__ */ jsx("div", { style: { overflow: "auto", padding: 8, fontSize: 13, fontFamily: "system-ui, sans-serif" }, children: doc.sections.map((id) => /* @__PURE__ */ jsx(LayerRow, { doc, id, depth: 0, selection, onSelect }, id)) });
160
+ var MIME = "application/x-cms-node";
161
+ function isAncestorOrSelf(doc, id, maybeAncestor) {
162
+ for (let cur = id; cur; cur = doc.nodes[cur]?.parent ?? null) {
163
+ if (cur === maybeAncestor) return true;
164
+ }
165
+ return false;
166
+ }
167
+ function LayersPanel({ doc, selection, onSelect, onMove }) {
168
+ const [overId, setOverId] = useState(null);
169
+ return /* @__PURE__ */ jsx("div", { style: { overflow: "auto", padding: 8, fontSize: 13, fontFamily: "system-ui, sans-serif" }, children: doc.sections.map((id) => /* @__PURE__ */ jsx(LayerRow, { doc, id, depth: 0, selection, onSelect, onMove, overId, setOverId }, id)) });
161
170
  }
162
- function LayerRow({ doc, id, depth, selection, onSelect }) {
171
+ function LayerRow({ doc, id, depth, selection, onSelect, onMove, overId, setOverId }) {
163
172
  const node = doc.nodes[id];
164
173
  if (!node) return null;
165
174
  const selected = selection === id;
175
+ const draggable = Boolean(onMove);
176
+ const drop = (e) => {
177
+ e.preventDefault();
178
+ e.stopPropagation();
179
+ setOverId(null);
180
+ if (!onMove) return;
181
+ const dragId = e.dataTransfer.getData(MIME);
182
+ if (!dragId || dragId === id || isAncestorOrSelf(doc, id, dragId)) return;
183
+ const siblings = childrenOf(doc, node.parent);
184
+ const idx = siblings.findIndex((n) => n.id === id);
185
+ const next = siblings[idx + 1];
186
+ onMove(dragId, node.parent, keyBetween(node.order, next ? next.order : null));
187
+ };
166
188
  return /* @__PURE__ */ jsxs(Fragment, { children: [
167
189
  /* @__PURE__ */ jsxs(
168
190
  "button",
169
191
  {
170
192
  type: "button",
193
+ draggable,
171
194
  onClick: () => onSelect(id),
195
+ onDragStart: (e) => {
196
+ e.dataTransfer.setData(MIME, id);
197
+ e.dataTransfer.effectAllowed = "move";
198
+ },
199
+ onDragOver: (e) => {
200
+ if (!onMove) return;
201
+ e.preventDefault();
202
+ e.dataTransfer.dropEffect = "move";
203
+ if (overId !== id) setOverId(id);
204
+ },
205
+ onDragLeave: () => overId === id && setOverId(null),
206
+ onDrop: drop,
172
207
  style: {
173
208
  display: "block",
174
209
  width: "100%",
175
210
  textAlign: "left",
176
211
  border: "none",
177
212
  borderRadius: 6,
178
- cursor: "pointer",
213
+ cursor: draggable ? "grab" : "pointer",
179
214
  padding: "4px 8px",
180
215
  paddingLeft: 8 + depth * 14,
181
216
  background: selected ? "#ede9fe" : "transparent",
182
217
  color: selected ? "#5b21b6" : "#334155",
183
- font: "inherit"
218
+ font: "inherit",
219
+ boxShadow: overId === id ? "inset 0 -2px 0 0 #8b5cf6" : "none"
184
220
  },
185
221
  children: [
186
222
  /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: node.type }),
@@ -189,17 +225,7 @@ function LayerRow({ doc, id, depth, selection, onSelect }) {
189
225
  ]
190
226
  }
191
227
  ),
192
- childrenOf(doc, id).map((child) => /* @__PURE__ */ jsx(
193
- LayerRow,
194
- {
195
- doc,
196
- id: child.id,
197
- depth: depth + 1,
198
- selection,
199
- onSelect
200
- },
201
- child.id
202
- ))
228
+ childrenOf(doc, id).map((child) => /* @__PURE__ */ jsx(LayerRow, { doc, id: child.id, depth: depth + 1, selection, onSelect, onMove, overId, setOverId }, child.id))
203
229
  ] });
204
230
  }
205
231
  var label = {
@@ -331,7 +357,15 @@ function Editor({ defaultValue, onChange }) {
331
357
  return /* @__PURE__ */ jsxs("div", { style: shell, children: [
332
358
  /* @__PURE__ */ jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
333
359
  /* @__PURE__ */ jsx(Toolbar, { ed }),
334
- /* @__PURE__ */ jsx(LayersPanel, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select })
360
+ /* @__PURE__ */ jsx(
361
+ LayersPanel,
362
+ {
363
+ doc: ed.state.doc,
364
+ selection: ed.state.selection,
365
+ onSelect: ed.select,
366
+ onMove: (id, parent, order) => ed.apply({ t: "move_node", id, parent, order })
367
+ }
368
+ )
335
369
  ] }),
336
370
  /* @__PURE__ */ jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
337
371
  /* @__PURE__ */ jsx("div", { style: { borderLeft: "1px solid #e2e8f0", minHeight: 0 }, children: /* @__PURE__ */ jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply }) })
@@ -371,6 +405,7 @@ function NodeInspector({
371
405
  measured,
372
406
  onProps,
373
407
  onStyle,
408
+ onLayout,
374
409
  onTransform,
375
410
  onRemove,
376
411
  onClose
@@ -453,9 +488,44 @@ function NodeInspector({
453
488
  ] }),
454
489
  /* @__PURE__ */ jsxs(Row, { children: [
455
490
  /* @__PURE__ */ jsx(Num, { label: "Padding", value: base.padding?.value, placeholder: "0", onChange: (v) => onStyle({ padding: v == null ? void 0 : len(v) }) }),
491
+ /* @__PURE__ */ jsx(Num, { label: "Margin", value: base.margin?.value, placeholder: "0", onChange: (v) => onStyle({ margin: v == null ? void 0 : len(v) }) })
492
+ ] }),
493
+ /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Txt, { label: "Border", value: base.border ?? "", placeholder: "1px solid #e2e8f0", onChange: (v) => onStyle({ border: v || void 0 }) }) })
494
+ ] }),
495
+ CONTAINER_TYPES.has(node.type) ? /* @__PURE__ */ jsxs(Group, { label: "Layout", children: [
496
+ /* @__PURE__ */ jsxs(Row, { children: [
497
+ /* @__PURE__ */ jsx(
498
+ Sel,
499
+ {
500
+ label: "Mode",
501
+ value: node.layout ?? "free",
502
+ options: [["free", "Free"], ["stack", "Stack"], ["grid", "Grid"]],
503
+ onChange: (v) => onLayout?.(v)
504
+ }
505
+ ),
456
506
  /* @__PURE__ */ jsx(Num, { label: "Gap", value: base.gap?.value, placeholder: "0", onChange: (v) => onStyle({ gap: v == null ? void 0 : len(v) }) })
507
+ ] }),
508
+ /* @__PURE__ */ jsxs(Row, { children: [
509
+ /* @__PURE__ */ jsx(
510
+ Sel,
511
+ {
512
+ label: "Align",
513
+ value: base.align ?? "",
514
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["stretch", "Stretch"]],
515
+ onChange: (v) => onStyle({ align: v || void 0 })
516
+ }
517
+ ),
518
+ /* @__PURE__ */ jsx(
519
+ Sel,
520
+ {
521
+ label: "Justify",
522
+ value: base.justify ?? "",
523
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["between", "Between"], ["around", "Around"]],
524
+ onChange: (v) => onStyle({ justify: v || void 0 })
525
+ }
526
+ )
457
527
  ] })
458
- ] })
528
+ ] }) : null
459
529
  ] })
460
530
  ] });
461
531
  }
@@ -510,6 +580,12 @@ function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
510
580
  )
511
581
  ] });
512
582
  }
583
+ function Txt({ label: label2, value, placeholder, onChange }) {
584
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
585
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
586
+ /* @__PURE__ */ jsx("input", { style: input2, type: "text", value, placeholder, onChange: (e) => onChange(e.target.value) })
587
+ ] });
588
+ }
513
589
  function Color({ label: label2, value, onChange }) {
514
590
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
515
591
  /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
@@ -559,6 +635,110 @@ var input2 = {
559
635
  };
560
636
  var iconBtn = { background: "transparent", border: "none", color: "#e2e8f0", cursor: "pointer", fontSize: 13, padding: 4, borderRadius: 6 };
561
637
  var miniBtn = { font: "inherit", fontSize: 11, color: "#e2e8f0", background: "#334155", border: "1px solid #475569", borderRadius: 6, padding: "5px 10px", cursor: "pointer" };
638
+
639
+ // src/editor/editorOps.ts
640
+ function childrenOfMap(nodes, parent) {
641
+ return Object.values(nodes).filter((n) => n.parent === parent).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
642
+ }
643
+ function subtreeBfs(nodes, root) {
644
+ const out = [];
645
+ const queue = [root];
646
+ while (queue.length) {
647
+ const cur = queue.shift();
648
+ out.push(cur);
649
+ for (const child of childrenOfMap(nodes, cur)) queue.push(child.id);
650
+ }
651
+ return out;
652
+ }
653
+ function cloneFrom(nodes, rootId, newParent, newOrder, seqBase) {
654
+ if (!nodes[rootId]) return { ops: [], newRootId: null };
655
+ const order = subtreeBfs(nodes, rootId);
656
+ const idMap = {};
657
+ order.forEach((old, i) => idMap[old] = `${old}_c${seqBase}_${i}`);
658
+ const ops = order.map((old) => {
659
+ const src = nodes[old];
660
+ const cloned = JSON.parse(JSON.stringify(src));
661
+ cloned.id = idMap[old];
662
+ cloned.parent = old === rootId ? newParent : idMap[src.parent];
663
+ if (old === rootId) cloned.order = newOrder;
664
+ return { t: "insert_node", node: cloned };
665
+ });
666
+ return { ops, newRootId: idMap[rootId] };
667
+ }
668
+ function duplicateOps(doc, id) {
669
+ const node = doc.nodes[id];
670
+ if (!node) return { ops: [], newRootId: null };
671
+ const sibs = childrenOf(doc, node.parent);
672
+ const idx = sibs.findIndex((s) => s.id === id);
673
+ const newOrder = keyBetween(node.order, sibs[idx + 1]?.order ?? null);
674
+ const { ops, newRootId } = cloneFrom(doc.nodes, id, node.parent, newOrder, doc.seq);
675
+ if (node.parent === null && newRootId) {
676
+ const base = doc.sections;
677
+ const at = base.indexOf(id);
678
+ ops.push({ t: "reorder_sections", order: [...base.slice(0, at + 1), newRootId, ...base.slice(at + 1)] });
679
+ }
680
+ return { ops, newRootId };
681
+ }
682
+ function pasteOps(doc, clip, target, containerTypes) {
683
+ const sel = target ? doc.nodes[target] : null;
684
+ const parent = sel ? containerTypes.has(sel.type) ? sel.id : sel.parent : doc.sections[doc.sections.length - 1] ?? null;
685
+ const sibs = childrenOf(doc, parent);
686
+ const newOrder = keyBetween(sibs.length ? sibs[sibs.length - 1].order : null, null);
687
+ const { ops, newRootId } = cloneFrom(clip.nodes, clip.rootId, parent, newOrder, doc.seq);
688
+ return { ops, newRootId };
689
+ }
690
+ function reorderOps(doc, id, dir) {
691
+ const node = doc.nodes[id];
692
+ if (!node) return [];
693
+ if (node.parent === null) {
694
+ const s = [...doc.sections];
695
+ const i = s.indexOf(id);
696
+ if (i < 0) return [];
697
+ s.splice(i, 1);
698
+ if (dir === "front") s.unshift(id);
699
+ else if (dir === "back") s.push(id);
700
+ else s.splice(Math.max(0, Math.min(s.length, dir === "up" ? i - 1 : i + 1)), 0, id);
701
+ return [{ t: "reorder_sections", order: s }];
702
+ }
703
+ const sibs = childrenOf(doc, node.parent);
704
+ const idx = sibs.findIndex((sb) => sb.id === id);
705
+ if (idx < 0) return [];
706
+ const last = sibs.length - 1;
707
+ let newOrder;
708
+ if (dir === "up") {
709
+ if (idx <= 0) return [];
710
+ newOrder = keyBetween(sibs[idx - 2]?.order ?? null, sibs[idx - 1].order);
711
+ } else if (dir === "down") {
712
+ if (idx >= last) return [];
713
+ newOrder = keyBetween(sibs[idx + 1].order, sibs[idx + 2]?.order ?? null);
714
+ } else if (dir === "front") {
715
+ if (idx >= last) return [];
716
+ newOrder = keyBetween(sibs[last].order, null);
717
+ } else {
718
+ if (idx <= 0) return [];
719
+ newOrder = keyBetween(null, sibs[0].order);
720
+ }
721
+ return [{ t: "move_node", id, parent: node.parent, order: newOrder }];
722
+ }
723
+ function wrapInBoxOps(doc, id) {
724
+ const node = doc.nodes[id];
725
+ if (!node || node.parent === null) return { ops: [], newRootId: null };
726
+ const frameId = `box_${doc.seq}`;
727
+ return {
728
+ ops: [
729
+ { t: "insert_node", node: { id: frameId, type: "frame", parent: node.parent, order: node.order, props: {}, style: { base: { padding: { value: 16, unit: "px" } } } } },
730
+ { t: "move_node", id, parent: frameId, order: "a" }
731
+ ],
732
+ newRootId: frameId
733
+ };
734
+ }
735
+ function snapshotSubtree(doc, id) {
736
+ if (!doc.nodes[id]) return null;
737
+ const ids = subtreeBfs(doc.nodes, id);
738
+ const nodes = {};
739
+ for (const nid of ids) nodes[nid] = JSON.parse(JSON.stringify(doc.nodes[nid]));
740
+ return { rootId: id, nodes };
741
+ }
562
742
  var Z2 = 2147483e3;
563
743
  function EditablePage({
564
744
  doc,
@@ -672,14 +852,93 @@ function EditablePage({
672
852
  );
673
853
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
674
854
  const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
855
+ const setLayout = (layout) => selection && ed.apply({ t: "set_layout", id: selection, layout });
675
856
  const removeSelected = () => {
676
857
  if (!selection) return;
677
858
  ed.apply({ t: "remove_node", id: selection });
678
859
  ed.select(null);
679
860
  };
680
- const page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
861
+ const clip = useRef(null);
862
+ const runOps = (result) => {
863
+ result.ops.forEach((op) => ed.apply(op));
864
+ if (result.newRootId) ed.select(result.newRootId);
865
+ };
866
+ const duplicate = (id) => id && runOps(duplicateOps(ed.state.doc, id));
867
+ const reorder = (id, dir) => id && reorderOps(ed.state.doc, id, dir).forEach((op) => ed.apply(op));
868
+ const wrapInBox = (id) => id && runOps(wrapInBoxOps(ed.state.doc, id));
869
+ const copy = (id) => {
870
+ if (id) clip.current = snapshotSubtree(ed.state.doc, id);
871
+ };
872
+ const paste = (target) => {
873
+ if (clip.current) runOps(pasteOps(ed.state.doc, clip.current, target, CONTAINER_TYPES2));
874
+ };
875
+ const startEditText = (id) => {
876
+ if (!id) return;
877
+ ed.select(id);
878
+ requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
879
+ };
880
+ const nudgeRef = useRef({ transforms, onNodeTransform });
881
+ nudgeRef.current = { transforms, onNodeTransform };
882
+ useEffect(() => {
883
+ if (!editing) return;
884
+ const onKey = (e) => {
885
+ const t = e.target;
886
+ const typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
887
+ if (e.key === "Escape") return void ed.select(null);
888
+ if (typing) return;
889
+ if ((e.key === "Delete" || e.key === "Backspace") && selection) {
890
+ e.preventDefault();
891
+ removeSelected();
892
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d" && selection) {
893
+ e.preventDefault();
894
+ duplicate(selection);
895
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selection) {
896
+ copy(selection);
897
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
898
+ e.preventDefault();
899
+ paste(selection);
900
+ } else if (selection && /^Arrow(Left|Right|Up|Down)$/.test(e.key)) {
901
+ const { transforms: tf, onNodeTransform: commit } = nudgeRef.current;
902
+ if (!commit) return;
903
+ e.preventDefault();
904
+ const step = e.shiftKey ? 10 : 1;
905
+ const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
906
+ const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
907
+ const cur = tf?.[selection] ?? {};
908
+ commit(selection, { ...cur, x: (cur.x ?? 0) + dx, y: (cur.y ?? 0) + dy });
909
+ }
910
+ };
911
+ window.addEventListener("keydown", onKey);
912
+ return () => window.removeEventListener("keydown", onKey);
913
+ }, [editing, selection]);
914
+ const onContextMenuSelect = (e) => {
915
+ if (!editing) return;
916
+ const el = e.target.closest("[data-cms]");
917
+ if (el) ed.select(el.getAttribute("data-cms"));
918
+ else ed.select(null);
919
+ };
920
+ const page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
921
+ const pageShell = pinned ? /* @__PURE__ */ jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : page;
681
922
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
682
- pinned ? /* @__PURE__ */ jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : page,
923
+ editing ? /* @__PURE__ */ jsxs(ContextMenu, { children: [
924
+ /* @__PURE__ */ jsx(ContextMenu.Trigger, { children: pageShell }),
925
+ /* @__PURE__ */ jsx(ContextMenu.Content, { className: "!z-[2147483646]", children: /* @__PURE__ */ jsx(
926
+ EditMenuItems,
927
+ {
928
+ node: selNode ?? null,
929
+ hasClipboard: Boolean(clip.current),
930
+ isContainer: selNode ? CONTAINER_TYPES2.has(selNode.type) : false,
931
+ onEditText: () => startEditText(selection),
932
+ onAdd: addNode,
933
+ onDuplicate: () => duplicate(selection),
934
+ onCopy: () => copy(selection),
935
+ onPaste: () => paste(selection),
936
+ onWrap: () => wrapInBox(selection),
937
+ onReorder: (d) => reorder(selection, d),
938
+ onDelete: removeSelected
939
+ }
940
+ ) })
941
+ ] }) : pageShell,
683
942
  editing && box && selection ? /* @__PURE__ */ jsx(
684
943
  SelectionOverlay,
685
944
  {
@@ -700,6 +959,7 @@ function EditablePage({
700
959
  measured: { w: box.w, h: box.h },
701
960
  onProps: setProps,
702
961
  onStyle: setStyle,
962
+ onLayout: setLayout,
703
963
  onTransform: (t) => selection && onNodeTransform?.(selection, t),
704
964
  onRemove: removeSelected,
705
965
  onClose: () => ed.select(null)
@@ -933,6 +1193,45 @@ function EditBar({
933
1193
  ] }) : null
934
1194
  ] });
935
1195
  }
1196
+ function EditMenuItems({
1197
+ node,
1198
+ hasClipboard,
1199
+ isContainer,
1200
+ onEditText,
1201
+ onAdd,
1202
+ onDuplicate,
1203
+ onCopy,
1204
+ onPaste,
1205
+ onWrap,
1206
+ onReorder,
1207
+ onDelete
1208
+ }) {
1209
+ const editable = node && (node.type === "text" || node.type === "heading");
1210
+ const nested = Boolean(node && node.parent !== null);
1211
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1212
+ editable ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1213
+ /* @__PURE__ */ jsxs(ContextMenu.Sub, { children: [
1214
+ /* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1215
+ /* @__PURE__ */ jsx(ContextMenu.SubContent, { className: "!z-[2147483647]", children: ADD_MENU.map((m) => /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onAdd(m.kind), children: m.label }, m.kind)) })
1216
+ ] }),
1217
+ node ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1218
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1219
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
1220
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
1221
+ /* @__PURE__ */ jsx(ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" }),
1222
+ nested ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onWrap, children: "Wrap in Box" }) : null,
1223
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1224
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
1225
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
1226
+ nested ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1227
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
1228
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
1229
+ ] }) : null,
1230
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1231
+ /* @__PURE__ */ jsx(ContextMenu.Item, { danger: true, onClick: onDelete, children: "Delete" })
1232
+ ] }) : /* @__PURE__ */ jsx(ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" })
1233
+ ] });
1234
+ }
936
1235
  function applyTransform(el, t) {
937
1236
  if (t && (t.x != null || t.y != null || t.scale != null || t.rotate != null || t.opacity != null || t.w != null || t.h != null)) {
938
1237
  el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1}) rotate(${t.rotate ?? 0}deg)`;