@particle-academy/fancy-cms-ui 0.0.1-dev.6 → 0.0.1-dev.8

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.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { keyBetween } from './chunk-7YYLYBLI.js';
2
- import { CmsPage, defaultRegistry } from './chunk-5PWZGIYZ.js';
2
+ import { CmsPage, defaultRegistry } from './chunk-5O6U4VIU.js';
3
3
  import { reduce, childrenOf } from './chunk-HYHNMW47.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 };
@@ -364,7 +365,306 @@ function Toolbar({ ed }) {
364
365
  }
365
366
  );
366
367
  }
368
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
369
+ function NodeInspector({
370
+ node,
371
+ transform,
372
+ measured,
373
+ onProps,
374
+ onStyle,
375
+ onTransform,
376
+ onRemove,
377
+ onClose
378
+ }) {
379
+ const base = node.style.base ?? {};
380
+ const setT = (patch) => onTransform({ ...transform, ...patch });
381
+ const handled = /* @__PURE__ */ new Set(["content", "label"]);
382
+ const extraKeys = Object.keys(node.props).filter(
383
+ (k) => !handled.has(k) && ["string", "number", "boolean"].includes(typeof node.props[k])
384
+ );
385
+ return /* @__PURE__ */ jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
386
+ /* @__PURE__ */ jsxs("div", { style: headRow, children: [
387
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }, children: [
388
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 12, textTransform: "capitalize" }, children: node.type }),
389
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, opacity: 0.5, fontFamily: "ui-monospace, monospace", overflow: "hidden", textOverflow: "ellipsis" }, children: node.id })
390
+ ] }),
391
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 } }),
392
+ /* @__PURE__ */ jsx("button", { type: "button", style: { ...iconBtn, color: "#fca5a5" }, title: "Delete element", onClick: onRemove, children: "\u{1F5D1}" }),
393
+ /* @__PURE__ */ jsx("button", { type: "button", style: iconBtn, title: "Close", onClick: onClose, children: "\u2715" })
394
+ ] }),
395
+ /* @__PURE__ */ jsxs("div", { style: body, children: [
396
+ "content" in node.props ? /* @__PURE__ */ jsx(Group, { label: "Content", children: /* @__PURE__ */ jsx(
397
+ "textarea",
398
+ {
399
+ style: { ...input2, minHeight: 64, resize: "vertical", lineHeight: 1.4 },
400
+ value: String(node.props.content ?? ""),
401
+ onChange: (e) => onProps({ content: e.target.value })
402
+ }
403
+ ) }) : null,
404
+ "label" in node.props ? /* @__PURE__ */ jsx(Group, { label: "Label", children: /* @__PURE__ */ jsx("input", { style: input2, value: String(node.props.label ?? ""), onChange: (e) => onProps({ label: e.target.value }) }) }) : null,
405
+ extraKeys.length ? /* @__PURE__ */ jsx(Group, { label: "Properties", children: extraKeys.map((k) => /* @__PURE__ */ jsx(PropField, { name: k, value: node.props[k], onChange: (v) => onProps({ [k]: v }) }, k)) }) : null,
406
+ /* @__PURE__ */ jsxs(Group, { label: "Position & size", children: [
407
+ /* @__PURE__ */ jsxs(Row, { children: [
408
+ /* @__PURE__ */ jsx(Num, { label: "X", value: transform.x, placeholder: "0", onChange: (v) => setT({ x: v }) }),
409
+ /* @__PURE__ */ jsx(Num, { label: "Y", value: transform.y, placeholder: "0", onChange: (v) => setT({ y: v }) })
410
+ ] }),
411
+ /* @__PURE__ */ jsxs(Row, { children: [
412
+ /* @__PURE__ */ jsx(Num, { label: "W", value: transform.w, placeholder: `${Math.round(measured.w)}`, onChange: (v) => setT({ w: v }) }),
413
+ /* @__PURE__ */ jsx(Num, { label: "H", value: transform.h, placeholder: `${Math.round(measured.h)}`, onChange: (v) => setT({ h: v }) })
414
+ ] }),
415
+ /* @__PURE__ */ jsxs(Row, { children: [
416
+ /* @__PURE__ */ jsx(Num, { label: "Scale", value: transform.scale, placeholder: "1", step: 0.05, onChange: (v) => setT({ scale: v }) }),
417
+ /* @__PURE__ */ jsx(Num, { label: "Rotate", value: transform.rotate, placeholder: "0", onChange: (v) => setT({ rotate: v }) })
418
+ ] }),
419
+ /* @__PURE__ */ jsxs(Row, { children: [
420
+ /* @__PURE__ */ jsx(Num, { label: "Opacity", value: transform.opacity, placeholder: "1", min: 0, max: 1, step: 0.05, onChange: (v) => setT({ opacity: v }) }),
421
+ /* @__PURE__ */ jsx("button", { type: "button", style: { ...miniBtn, alignSelf: "end" }, onClick: () => onTransform({}), title: "Clear transform", children: "Reset" })
422
+ ] })
423
+ ] }),
424
+ !CONTAINER_TYPES.has(node.type) ? /* @__PURE__ */ jsxs(Group, { label: "Text", children: [
425
+ /* @__PURE__ */ jsxs(Row, { children: [
426
+ /* @__PURE__ */ jsx(Color, { label: "Color", value: base.color ?? "#0f172a", onChange: (v) => onStyle({ color: v }) }),
427
+ /* @__PURE__ */ jsx(Num, { label: "Size", value: base.fontSize?.value, placeholder: "16", onChange: (v) => onStyle({ fontSize: v == null ? void 0 : len(v) }) })
428
+ ] }),
429
+ /* @__PURE__ */ jsxs(Row, { children: [
430
+ /* @__PURE__ */ jsx(
431
+ Sel,
432
+ {
433
+ label: "Weight",
434
+ value: String(base.fontWeight ?? ""),
435
+ options: [["", "\u2014"], ["400", "Regular"], ["500", "Medium"], ["600", "Semibold"], ["700", "Bold"], ["800", "Heavy"]],
436
+ onChange: (v) => onStyle({ fontWeight: v ? Number(v) : void 0 })
437
+ }
438
+ ),
439
+ /* @__PURE__ */ jsx(
440
+ Sel,
441
+ {
442
+ label: "Align",
443
+ value: base.textAlign ?? "",
444
+ options: [["", "\u2014"], ["left", "Left"], ["center", "Center"], ["right", "Right"], ["justify", "Justify"]],
445
+ onChange: (v) => onStyle({ textAlign: v || void 0 })
446
+ }
447
+ )
448
+ ] })
449
+ ] }) : null,
450
+ /* @__PURE__ */ jsxs(Group, { label: "Box", children: [
451
+ /* @__PURE__ */ jsxs(Row, { children: [
452
+ /* @__PURE__ */ jsx(Color, { label: "Background", value: hex(base.background), onChange: (v) => onStyle({ background: v }) }),
453
+ /* @__PURE__ */ jsx(Num, { label: "Radius", value: base.radius?.value, placeholder: "0", onChange: (v) => onStyle({ radius: v == null ? void 0 : len(v) }) })
454
+ ] }),
455
+ /* @__PURE__ */ jsxs(Row, { children: [
456
+ /* @__PURE__ */ jsx(Num, { label: "Padding", value: base.padding?.value, placeholder: "0", onChange: (v) => onStyle({ padding: v == null ? void 0 : len(v) }) }),
457
+ /* @__PURE__ */ jsx(Num, { label: "Gap", value: base.gap?.value, placeholder: "0", onChange: (v) => onStyle({ gap: v == null ? void 0 : len(v) }) })
458
+ ] })
459
+ ] })
460
+ ] })
461
+ ] });
462
+ }
463
+ function Group({ label: label2, children }) {
464
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, paddingBottom: 12, marginBottom: 12, borderBottom: "1px solid #1e293b" }, children: [
465
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5 }, children: label2 }),
466
+ children
467
+ ] });
468
+ }
469
+ function Row({ children }) {
470
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8 }, children });
471
+ }
472
+ function PropField({ name, value, onChange }) {
473
+ if (typeof value === "boolean") {
474
+ return /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
475
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
476
+ /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize" }, children: name })
477
+ ] });
478
+ }
479
+ if (name === "variant") {
480
+ return /* @__PURE__ */ jsx(Sel, { label: name, value: String(value), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
481
+ }
482
+ const isNum = typeof value === "number";
483
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
484
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: name }),
485
+ /* @__PURE__ */ jsx(
486
+ "input",
487
+ {
488
+ style: input2,
489
+ type: isNum ? "number" : "text",
490
+ value: String(value),
491
+ onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
492
+ }
493
+ )
494
+ ] });
495
+ }
496
+ function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
497
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
498
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
499
+ /* @__PURE__ */ jsx(
500
+ "input",
501
+ {
502
+ style: input2,
503
+ type: "number",
504
+ step,
505
+ min,
506
+ max,
507
+ value: value ?? "",
508
+ placeholder,
509
+ onChange: (e) => onChange(e.target.value === "" ? void 0 : Number(e.target.value))
510
+ }
511
+ )
512
+ ] });
513
+ }
514
+ function Color({ label: label2, value, onChange }) {
515
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
516
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
517
+ /* @__PURE__ */ jsx("input", { style: { ...input2, padding: 2, height: 30 }, type: "color", value, onChange: (e) => onChange(e.target.value) })
518
+ ] });
519
+ }
520
+ function Sel({ label: label2, value, options, onChange }) {
521
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
522
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
523
+ /* @__PURE__ */ jsx("select", { style: input2, value, onChange: (e) => onChange(e.target.value), children: options.map(([v, l]) => /* @__PURE__ */ jsx("option", { value: v, children: l }, v)) })
524
+ ] });
525
+ }
526
+ var len = (value) => ({ value, unit: "px" });
527
+ var hex = (v) => v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : "#ffffff";
367
528
  var Z = 2147483e3;
529
+ var panel = {
530
+ position: "fixed",
531
+ top: 60,
532
+ right: 12,
533
+ bottom: 96,
534
+ width: 264,
535
+ display: "flex",
536
+ flexDirection: "column",
537
+ background: "#0b1220",
538
+ color: "#e2e8f0",
539
+ border: "1px solid #1e293b",
540
+ borderRadius: 12,
541
+ boxShadow: "0 18px 48px -16px rgba(0,0,0,0.6)",
542
+ zIndex: Z + 2,
543
+ fontFamily: "system-ui, sans-serif",
544
+ overflow: "hidden"
545
+ };
546
+ var headRow = { display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderBottom: "1px solid #1e293b" };
547
+ var body = { padding: 12, overflowY: "auto" };
548
+ var field = { display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 0 };
549
+ var fieldLabel = { fontSize: 10, opacity: 0.6 };
550
+ var input2 = {
551
+ width: "100%",
552
+ boxSizing: "border-box",
553
+ background: "#0f172a",
554
+ color: "#e2e8f0",
555
+ border: "1px solid #334155",
556
+ borderRadius: 6,
557
+ padding: "5px 7px",
558
+ font: "inherit",
559
+ fontSize: 12
560
+ };
561
+ var iconBtn = { background: "transparent", border: "none", color: "#e2e8f0", cursor: "pointer", fontSize: 13, padding: 4, borderRadius: 6 };
562
+ var miniBtn = { font: "inherit", fontSize: 11, color: "#e2e8f0", background: "#334155", border: "1px solid #475569", borderRadius: 6, padding: "5px 10px", cursor: "pointer" };
563
+
564
+ // src/editor/editorOps.ts
565
+ function childrenOfMap(nodes, parent) {
566
+ return Object.values(nodes).filter((n) => n.parent === parent).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
567
+ }
568
+ function subtreeBfs(nodes, root) {
569
+ const out = [];
570
+ const queue = [root];
571
+ while (queue.length) {
572
+ const cur = queue.shift();
573
+ out.push(cur);
574
+ for (const child of childrenOfMap(nodes, cur)) queue.push(child.id);
575
+ }
576
+ return out;
577
+ }
578
+ function cloneFrom(nodes, rootId, newParent, newOrder, seqBase) {
579
+ if (!nodes[rootId]) return { ops: [], newRootId: null };
580
+ const order = subtreeBfs(nodes, rootId);
581
+ const idMap = {};
582
+ order.forEach((old, i) => idMap[old] = `${old}_c${seqBase}_${i}`);
583
+ const ops = order.map((old) => {
584
+ const src = nodes[old];
585
+ const cloned = JSON.parse(JSON.stringify(src));
586
+ cloned.id = idMap[old];
587
+ cloned.parent = old === rootId ? newParent : idMap[src.parent];
588
+ if (old === rootId) cloned.order = newOrder;
589
+ return { t: "insert_node", node: cloned };
590
+ });
591
+ return { ops, newRootId: idMap[rootId] };
592
+ }
593
+ function duplicateOps(doc, id) {
594
+ const node = doc.nodes[id];
595
+ if (!node) return { ops: [], newRootId: null };
596
+ const sibs = childrenOf(doc, node.parent);
597
+ const idx = sibs.findIndex((s) => s.id === id);
598
+ const newOrder = keyBetween(node.order, sibs[idx + 1]?.order ?? null);
599
+ const { ops, newRootId } = cloneFrom(doc.nodes, id, node.parent, newOrder, doc.seq);
600
+ if (node.parent === null && newRootId) {
601
+ const base = doc.sections;
602
+ const at = base.indexOf(id);
603
+ ops.push({ t: "reorder_sections", order: [...base.slice(0, at + 1), newRootId, ...base.slice(at + 1)] });
604
+ }
605
+ return { ops, newRootId };
606
+ }
607
+ function pasteOps(doc, clip, target, containerTypes) {
608
+ const sel = target ? doc.nodes[target] : null;
609
+ const parent = sel ? containerTypes.has(sel.type) ? sel.id : sel.parent : doc.sections[doc.sections.length - 1] ?? null;
610
+ const sibs = childrenOf(doc, parent);
611
+ const newOrder = keyBetween(sibs.length ? sibs[sibs.length - 1].order : null, null);
612
+ const { ops, newRootId } = cloneFrom(clip.nodes, clip.rootId, parent, newOrder, doc.seq);
613
+ return { ops, newRootId };
614
+ }
615
+ function reorderOps(doc, id, dir) {
616
+ const node = doc.nodes[id];
617
+ if (!node) return [];
618
+ if (node.parent === null) {
619
+ const s = [...doc.sections];
620
+ const i = s.indexOf(id);
621
+ if (i < 0) return [];
622
+ s.splice(i, 1);
623
+ if (dir === "front") s.unshift(id);
624
+ else if (dir === "back") s.push(id);
625
+ else s.splice(Math.max(0, Math.min(s.length, dir === "up" ? i - 1 : i + 1)), 0, id);
626
+ return [{ t: "reorder_sections", order: s }];
627
+ }
628
+ const sibs = childrenOf(doc, node.parent);
629
+ const idx = sibs.findIndex((sb) => sb.id === id);
630
+ if (idx < 0) return [];
631
+ const last = sibs.length - 1;
632
+ let newOrder;
633
+ if (dir === "up") {
634
+ if (idx <= 0) return [];
635
+ newOrder = keyBetween(sibs[idx - 2]?.order ?? null, sibs[idx - 1].order);
636
+ } else if (dir === "down") {
637
+ if (idx >= last) return [];
638
+ newOrder = keyBetween(sibs[idx + 1].order, sibs[idx + 2]?.order ?? null);
639
+ } else if (dir === "front") {
640
+ if (idx >= last) return [];
641
+ newOrder = keyBetween(sibs[last].order, null);
642
+ } else {
643
+ if (idx <= 0) return [];
644
+ newOrder = keyBetween(null, sibs[0].order);
645
+ }
646
+ return [{ t: "move_node", id, parent: node.parent, order: newOrder }];
647
+ }
648
+ function wrapInBoxOps(doc, id) {
649
+ const node = doc.nodes[id];
650
+ if (!node || node.parent === null) return { ops: [], newRootId: null };
651
+ const frameId = `box_${doc.seq}`;
652
+ return {
653
+ ops: [
654
+ { t: "insert_node", node: { id: frameId, type: "frame", parent: node.parent, order: node.order, props: {}, style: { base: { padding: { value: 16, unit: "px" } } } } },
655
+ { t: "move_node", id, parent: frameId, order: "a" }
656
+ ],
657
+ newRootId: frameId
658
+ };
659
+ }
660
+ function snapshotSubtree(doc, id) {
661
+ if (!doc.nodes[id]) return null;
662
+ const ids = subtreeBfs(doc.nodes, id);
663
+ const nodes = {};
664
+ for (const nid of ids) nodes[nid] = JSON.parse(JSON.stringify(doc.nodes[nid]));
665
+ return { rootId: id, nodes };
666
+ }
667
+ var Z2 = 2147483e3;
368
668
  function EditablePage({
369
669
  doc,
370
670
  registry = defaultRegistry,
@@ -373,7 +673,8 @@ function EditablePage({
373
673
  onNodeTransform,
374
674
  onSelect,
375
675
  frames = 1,
376
- onProgress
676
+ onProgress,
677
+ pinned = true
377
678
  }) {
378
679
  const ed = useEditor(doc);
379
680
  const [editing, setEditing] = useState(false);
@@ -402,22 +703,16 @@ function EditablePage({
402
703
  root.querySelectorAll("[data-cms]").forEach((el) => {
403
704
  const id = el.dataset.cms;
404
705
  const t = id ? transforms?.[id] : void 0;
405
- if (t) {
406
- el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1})`;
407
- el.style.opacity = t.opacity != null ? String(t.opacity) : "";
408
- } else {
409
- el.style.transform = "";
410
- el.style.opacity = "";
411
- }
706
+ applyTransform(el, t);
412
707
  });
413
708
  }, [transforms, ed.state.doc]);
414
709
  useEffect(() => {
415
710
  const spacer = spacerRef.current;
416
711
  if (!spacer || !onProgress) return;
417
712
  const compute = () => {
418
- const len = spacer.offsetHeight - window.innerHeight;
713
+ const len2 = spacer.offsetHeight - window.innerHeight;
419
714
  const top = spacer.getBoundingClientRect().top;
420
- onProgress(len > 0 ? Math.min(1, Math.max(0, -top / len)) : 0);
715
+ onProgress(len2 > 0 ? Math.min(1, Math.max(0, -top / len2)) : 0);
421
716
  };
422
717
  compute();
423
718
  window.addEventListener("scroll", compute, { passive: true });
@@ -442,7 +737,7 @@ function EditablePage({
442
737
  window.removeEventListener("scroll", measureBox);
443
738
  window.removeEventListener("resize", measureBox);
444
739
  };
445
- }, [measureBox, ed.state.doc, transforms]);
740
+ }, [measureBox, ed.state.doc]);
446
741
  useEffect(() => {
447
742
  if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
448
743
  const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
@@ -467,17 +762,96 @@ function EditablePage({
467
762
  ed.select(el.getAttribute("data-cms"));
468
763
  }
469
764
  };
470
- const addText = useCallback(() => {
471
- const parent = selNode && selNode.type !== "text" ? selNode.id : ed.state.doc.sections[0] ?? null;
472
- const siblings = childrenOf(ed.state.doc, parent);
473
- const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
474
- const id = `n${ed.state.doc.seq + 1}-${Math.floor(performance.now())}`;
475
- ed.apply({ t: "insert_node", node: { id, type: "text", parent, order, props: { content: "New text" }, style: { base: { color: "inherit" } } } });
476
- ed.select(id);
477
- }, [ed, selNode]);
765
+ const addNode = useCallback(
766
+ (kind) => {
767
+ const doc2 = ed.state.doc;
768
+ const parent = selNode ? CONTAINER_TYPES2.has(selNode.type) ? selNode.id : selNode.parent : doc2.sections[doc2.sections.length - 1] ?? null;
769
+ const siblings = childrenOf(doc2, parent);
770
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
771
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
772
+ const def = ADD_DEFAULTS[kind];
773
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
774
+ ed.select(id);
775
+ },
776
+ [ed, selNode]
777
+ );
478
778
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
779
+ const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
780
+ const removeSelected = () => {
781
+ if (!selection) return;
782
+ ed.apply({ t: "remove_node", id: selection });
783
+ ed.select(null);
784
+ };
785
+ const clip = useRef(null);
786
+ const runOps = (result) => {
787
+ result.ops.forEach((op) => ed.apply(op));
788
+ if (result.newRootId) ed.select(result.newRootId);
789
+ };
790
+ const duplicate = (id) => id && runOps(duplicateOps(ed.state.doc, id));
791
+ const reorder = (id, dir) => id && reorderOps(ed.state.doc, id, dir).forEach((op) => ed.apply(op));
792
+ const wrapInBox = (id) => id && runOps(wrapInBoxOps(ed.state.doc, id));
793
+ const copy = (id) => {
794
+ if (id) clip.current = snapshotSubtree(ed.state.doc, id);
795
+ };
796
+ const paste = (target) => {
797
+ if (clip.current) runOps(pasteOps(ed.state.doc, clip.current, target, CONTAINER_TYPES2));
798
+ };
799
+ const startEditText = (id) => {
800
+ if (!id) return;
801
+ ed.select(id);
802
+ requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
803
+ };
804
+ useEffect(() => {
805
+ if (!editing) return;
806
+ const onKey = (e) => {
807
+ const t = e.target;
808
+ const typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
809
+ if (e.key === "Escape") return void ed.select(null);
810
+ if (typing) return;
811
+ if ((e.key === "Delete" || e.key === "Backspace") && selection) {
812
+ e.preventDefault();
813
+ removeSelected();
814
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d" && selection) {
815
+ e.preventDefault();
816
+ duplicate(selection);
817
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selection) {
818
+ copy(selection);
819
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
820
+ e.preventDefault();
821
+ paste(selection);
822
+ }
823
+ };
824
+ window.addEventListener("keydown", onKey);
825
+ return () => window.removeEventListener("keydown", onKey);
826
+ }, [editing, selection]);
827
+ const onContextMenuSelect = (e) => {
828
+ if (!editing) return;
829
+ const el = e.target.closest("[data-cms]");
830
+ if (el) ed.select(el.getAttribute("data-cms"));
831
+ else ed.select(null);
832
+ };
833
+ const page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
834
+ 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;
479
835
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
480
- /* @__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: /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) }) }) }),
836
+ editing ? /* @__PURE__ */ jsxs(ContextMenu, { children: [
837
+ /* @__PURE__ */ jsx(ContextMenu.Trigger, { children: pageShell }),
838
+ /* @__PURE__ */ jsx(ContextMenu.Content, { className: "!z-[2147483646]", children: /* @__PURE__ */ jsx(
839
+ EditMenuItems,
840
+ {
841
+ node: selNode ?? null,
842
+ hasClipboard: Boolean(clip.current),
843
+ isContainer: selNode ? CONTAINER_TYPES2.has(selNode.type) : false,
844
+ onEditText: () => startEditText(selection),
845
+ onAdd: addNode,
846
+ onDuplicate: () => duplicate(selection),
847
+ onCopy: () => copy(selection),
848
+ onPaste: () => paste(selection),
849
+ onWrap: () => wrapInBox(selection),
850
+ onReorder: (d) => reorder(selection, d),
851
+ onDelete: removeSelected
852
+ }
853
+ ) })
854
+ ] }) : pageShell,
481
855
  editing && box && selection ? /* @__PURE__ */ jsx(
482
856
  SelectionOverlay,
483
857
  {
@@ -489,7 +863,20 @@ function EditablePage({
489
863
  onLive: measureBox
490
864
  }
491
865
  ) : null,
492
- editing && box && selNode ? /* @__PURE__ */ jsx(FloatingToolbar, { box, isText: selNode.type === "text", base: selNode.style.base, setStyle }) : null,
866
+ editing && box && selNode ? /* @__PURE__ */ jsx(FloatingToolbar, { box, isText: selNode.type === "text" || selNode.type === "heading", base: selNode.style.base, setStyle }) : null,
867
+ editing && selNode && box ? /* @__PURE__ */ jsx(
868
+ NodeInspector,
869
+ {
870
+ node: selNode,
871
+ transform: (selection ? transforms?.[selection] : void 0) ?? {},
872
+ measured: { w: box.w, h: box.h },
873
+ onProps: setProps,
874
+ onStyle: setStyle,
875
+ onTransform: (t) => selection && onNodeTransform?.(selection, t),
876
+ onRemove: removeSelected,
877
+ onClose: () => ed.select(null)
878
+ }
879
+ ) : null,
493
880
  revealed || editing ? /* @__PURE__ */ jsx(
494
881
  EditBar,
495
882
  {
@@ -502,12 +889,30 @@ function EditablePage({
502
889
  canRedo: ed.canRedo,
503
890
  onUndo: ed.undo,
504
891
  onRedo: ed.redo,
505
- onAddText: addText
892
+ onAdd: addNode
506
893
  }
507
894
  ) : null,
508
- editing && timelineDock ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z }, children: timelineDock }) : null
895
+ editing && timelineDock ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
509
896
  ] });
510
897
  }
898
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
899
+ var ADD_DEFAULTS = {
900
+ text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
901
+ heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
902
+ button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
903
+ image: { type: "image", props: { src: "", alt: "" }, style: {} },
904
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } }
905
+ };
906
+ var GRIPS = [
907
+ { key: "nw", north: true, west: true, cursor: "nwse-resize" },
908
+ { key: "n", north: true, cursor: "ns-resize" },
909
+ { key: "ne", north: true, east: true, cursor: "nesw-resize" },
910
+ { key: "e", east: true, cursor: "ew-resize" },
911
+ { key: "se", south: true, east: true, cursor: "nwse-resize" },
912
+ { key: "s", south: true, cursor: "ns-resize" },
913
+ { key: "sw", south: true, west: true, cursor: "nesw-resize" },
914
+ { key: "w", west: true, cursor: "ew-resize" }
915
+ ];
511
916
  function SelectionOverlay({
512
917
  box,
513
918
  base,
@@ -519,29 +924,48 @@ function SelectionOverlay({
519
924
  const drag = useRef(null);
520
925
  const apply = (t) => {
521
926
  const el = getEl();
522
- if (el) {
523
- el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1})`;
524
- el.style.opacity = t.opacity != null ? String(t.opacity) : "";
525
- }
927
+ if (el) applyTransform(el, t);
526
928
  };
527
- const start = (kind) => (e) => {
929
+ const startMove = (e) => {
528
930
  e.stopPropagation();
529
931
  e.preventDefault();
530
932
  e.currentTarget.setPointerCapture(e.pointerId);
531
- drag.current = { kind, x: e.clientX, y: e.clientY, w: box.w, live: { ...base } };
933
+ drag.current = { x: e.clientX, y: e.clientY, w: box.w, h: box.h, live: { ...base } };
532
934
  };
533
- const move = (e) => {
935
+ const moveMove = (e) => {
534
936
  const d = drag.current;
535
937
  if (!d) return;
536
- if (d.kind === "move") {
537
- d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
538
- } else {
539
- const factor = Math.max(0.1, (d.w + (e.clientX - d.x)) / d.w);
540
- d.live = { ...base, scale: Number(((base.scale ?? 1) * factor).toFixed(3)) };
541
- }
938
+ d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
542
939
  apply(d.live);
543
940
  onLive();
544
941
  };
942
+ const startResize = (g) => (e) => {
943
+ e.stopPropagation();
944
+ e.preventDefault();
945
+ e.currentTarget.setPointerCapture(e.pointerId);
946
+ drag.current = { x: e.clientX, y: e.clientY, w: base.w ?? box.w, h: base.h ?? box.h, live: { ...base } };
947
+ e.currentTarget.dataset.grip = g.key;
948
+ };
949
+ const resizeMove = (g) => (e) => {
950
+ const d = drag.current;
951
+ if (!d) return;
952
+ const dx = e.clientX - d.x;
953
+ const dy = e.clientY - d.y;
954
+ const live = { ...base };
955
+ if (g.east) live.w = Math.max(16, Math.round(d.w + dx));
956
+ if (g.west) {
957
+ live.w = Math.max(16, Math.round(d.w - dx));
958
+ live.x = (base.x ?? 0) + (d.w - live.w);
959
+ }
960
+ if (g.south) live.h = Math.max(16, Math.round(d.h + dy));
961
+ if (g.north) {
962
+ live.h = Math.max(16, Math.round(d.h - dy));
963
+ live.y = (base.y ?? 0) + (d.h - live.h);
964
+ }
965
+ d.live = live;
966
+ apply(live);
967
+ onLive();
968
+ };
545
969
  const end = () => {
546
970
  const d = drag.current;
547
971
  if (d) onTransform(d.live);
@@ -556,17 +980,23 @@ function SelectionOverlay({
556
980
  outline: "2px solid #8b5cf6",
557
981
  borderRadius: 4,
558
982
  pointerEvents: "none",
559
- zIndex: Z
983
+ zIndex: Z2
560
984
  };
561
985
  const handle = {
562
986
  position: "fixed",
563
- width: 16,
564
- height: 16,
565
- background: "#8b5cf6",
566
- border: "2px solid #fff",
567
- borderRadius: 5,
568
- zIndex: Z + 1,
569
- touchAction: "none"
987
+ width: 12,
988
+ height: 12,
989
+ background: "#fff",
990
+ border: "2px solid #8b5cf6",
991
+ borderRadius: 3,
992
+ zIndex: Z2 + 1,
993
+ touchAction: "none",
994
+ boxSizing: "border-box"
995
+ };
996
+ const gripPos = (g) => {
997
+ const cx = g.west ? box.x : g.east ? box.x + box.w : box.x + box.w / 2;
998
+ const cy = g.north ? box.y : g.south ? box.y + box.h : box.y + box.h / 2;
999
+ return { left: cx - 6, top: cy - 6, cursor: g.cursor };
570
1000
  };
571
1001
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
572
1002
  /* @__PURE__ */ jsx("div", { style: outline }),
@@ -575,22 +1005,23 @@ function SelectionOverlay({
575
1005
  "div",
576
1006
  {
577
1007
  title: "Drag to move",
578
- onPointerDown: start("move"),
579
- onPointerMove: move,
1008
+ onPointerDown: startMove,
1009
+ onPointerMove: moveMove,
580
1010
  onPointerUp: end,
581
- style: { ...handle, left: box.x - 9, top: box.y - 9, cursor: "move", borderRadius: 9 }
1011
+ style: { ...handle, width: 18, height: 18, borderRadius: 9, background: "#8b5cf6", border: "2px solid #fff", left: box.x - 22, top: box.y - 22, cursor: "move" }
582
1012
  }
583
1013
  ),
584
- /* @__PURE__ */ jsx(
1014
+ GRIPS.map((g) => /* @__PURE__ */ jsx(
585
1015
  "div",
586
1016
  {
587
1017
  title: "Drag to resize",
588
- onPointerDown: start("resize"),
589
- onPointerMove: move,
1018
+ onPointerDown: startResize(g),
1019
+ onPointerMove: resizeMove(g),
590
1020
  onPointerUp: end,
591
- style: { ...handle, left: box.x + box.w - 7, top: box.y + box.h - 7, cursor: "nwse-resize" }
592
- }
593
- )
1021
+ style: { ...handle, ...gripPos(g) }
1022
+ },
1023
+ g.key
1024
+ ))
594
1025
  ] }) : null
595
1026
  ] });
596
1027
  }
@@ -613,7 +1044,7 @@ function FloatingToolbar({
613
1044
  border: "1px solid rgba(255,255,255,0.12)",
614
1045
  borderRadius: 999,
615
1046
  boxShadow: "0 8px 24px -8px rgba(0,0,0,0.5)",
616
- zIndex: Z + 2,
1047
+ zIndex: Z2 + 2,
617
1048
  fontFamily: "system-ui, sans-serif",
618
1049
  fontSize: 12
619
1050
  };
@@ -631,6 +1062,13 @@ function FloatingToolbar({
631
1062
  ] })
632
1063
  ] });
633
1064
  }
1065
+ var ADD_MENU = [
1066
+ { kind: "text", label: "Text" },
1067
+ { kind: "heading", label: "Heading" },
1068
+ { kind: "button", label: "Button" },
1069
+ { kind: "image", label: "Image" },
1070
+ { kind: "box", label: "Box" }
1071
+ ];
634
1072
  function EditBar({
635
1073
  editing,
636
1074
  onToggle,
@@ -638,19 +1076,87 @@ function EditBar({
638
1076
  canRedo,
639
1077
  onUndo,
640
1078
  onRedo,
641
- onAddText
1079
+ onAdd
642
1080
  }) {
643
- const bar = { position: "fixed", top: 12, right: 12, display: "flex", gap: 6, alignItems: "center", padding: 6, background: "#0f172a", color: "#fff", borderRadius: 999, boxShadow: "0 8px 24px -8px rgba(0,0,0,0.4)", zIndex: Z + 3, fontFamily: "system-ui, sans-serif", fontSize: 12 };
1081
+ const [open, setOpen] = useState(false);
1082
+ const bar = { position: "fixed", top: 12, right: 12, display: "flex", gap: 6, alignItems: "center", padding: 6, background: "#0f172a", color: "#fff", borderRadius: 999, boxShadow: "0 8px 24px -8px rgba(0,0,0,0.4)", zIndex: Z2 + 3, fontFamily: "system-ui, sans-serif", fontSize: 12 };
644
1083
  const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
645
1084
  return /* @__PURE__ */ jsxs("div", { style: bar, children: [
646
1085
  /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
647
1086
  editing ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
648
- /* @__PURE__ */ jsx("button", { type: "button", style: btn, onClick: onAddText, title: "Add a text element", children: "\uFF0B Text" }),
1087
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1088
+ /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an element", children: "\uFF0B Add \u25BE" }),
1089
+ open ? /* @__PURE__ */ 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__ */ jsx(
1090
+ "button",
1091
+ {
1092
+ type: "button",
1093
+ style: { ...btn, textAlign: "left", borderRadius: 6, padding: "6px 10px" },
1094
+ onClick: () => {
1095
+ onAdd(m.kind);
1096
+ setOpen(false);
1097
+ },
1098
+ children: m.label
1099
+ },
1100
+ m.kind
1101
+ )) }) : null
1102
+ ] }),
649
1103
  /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: canUndo ? 1 : 0.4 }, disabled: !canUndo, onClick: onUndo, children: "\u21B6" }),
650
1104
  /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: canRedo ? 1 : 0.4 }, disabled: !canRedo, onClick: onRedo, children: "\u21B7" })
651
1105
  ] }) : null
652
1106
  ] });
653
1107
  }
1108
+ function EditMenuItems({
1109
+ node,
1110
+ hasClipboard,
1111
+ isContainer,
1112
+ onEditText,
1113
+ onAdd,
1114
+ onDuplicate,
1115
+ onCopy,
1116
+ onPaste,
1117
+ onWrap,
1118
+ onReorder,
1119
+ onDelete
1120
+ }) {
1121
+ const editable = node && (node.type === "text" || node.type === "heading");
1122
+ const nested = Boolean(node && node.parent !== null);
1123
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1124
+ editable ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1125
+ /* @__PURE__ */ jsxs(ContextMenu.Sub, { children: [
1126
+ /* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1127
+ /* @__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)) })
1128
+ ] }),
1129
+ node ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1130
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1131
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
1132
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
1133
+ /* @__PURE__ */ jsx(ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" }),
1134
+ nested ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onWrap, children: "Wrap in Box" }) : null,
1135
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1136
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
1137
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
1138
+ nested ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1139
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
1140
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
1141
+ ] }) : null,
1142
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1143
+ /* @__PURE__ */ jsx(ContextMenu.Item, { danger: true, onClick: onDelete, children: "Delete" })
1144
+ ] }) : /* @__PURE__ */ jsx(ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" })
1145
+ ] });
1146
+ }
1147
+ function applyTransform(el, t) {
1148
+ if (t && (t.x != null || t.y != null || t.scale != null || t.rotate != null || t.opacity != null || t.w != null || t.h != null)) {
1149
+ el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1}) rotate(${t.rotate ?? 0}deg)`;
1150
+ el.style.opacity = t.opacity != null ? String(t.opacity) : "";
1151
+ el.style.width = t.w != null ? `${t.w}px` : "";
1152
+ el.style.height = t.h != null ? `${t.h}px` : "";
1153
+ } else {
1154
+ el.style.transform = "";
1155
+ el.style.opacity = "";
1156
+ el.style.width = "";
1157
+ el.style.height = "";
1158
+ }
1159
+ }
654
1160
  function normalizeColor(v) {
655
1161
  return v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : "#ffffff";
656
1162
  }
@@ -658,6 +1164,6 @@ function cssEscape2(value) {
658
1164
  return value.replace(/["\\]/g, "\\$&");
659
1165
  }
660
1166
 
661
- export { Canvas, EditablePage, Editor, Inspector, LayersPanel, editorReduce, initEditor, useEditor };
1167
+ export { Canvas, EditablePage, Editor, Inspector, LayersPanel, NodeInspector, editorReduce, initEditor, useEditor };
662
1168
  //# sourceMappingURL=editor.js.map
663
1169
  //# sourceMappingURL=editor.js.map