@particle-academy/fancy-cms-ui 0.0.1-dev.3 → 0.0.1-dev.4

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
@@ -712,7 +712,10 @@ var Z = 2147483e3;
712
712
  function EditablePage({
713
713
  doc,
714
714
  registry = defaultRegistry,
715
- timelineDock
715
+ timelineDock,
716
+ transforms,
717
+ onNodeTransform,
718
+ onSelect
716
719
  }) {
717
720
  const ed = useEditor(doc);
718
721
  const [editing, setEditing] = react.useState(false);
@@ -721,6 +724,7 @@ function EditablePage({
721
724
  const [box, setBox] = react.useState(null);
722
725
  const { selection } = ed.state;
723
726
  const selNode = selection ? ed.state.doc.nodes[selection] : null;
727
+ react.useEffect(() => onSelect?.(selection), [selection, onSelect]);
724
728
  react.useEffect(() => {
725
729
  const onKey = (e) => setRevealed(e.ctrlKey && e.shiftKey);
726
730
  const onBlur = () => setRevealed(false);
@@ -733,36 +737,45 @@ function EditablePage({
733
737
  window.removeEventListener("blur", onBlur);
734
738
  };
735
739
  }, []);
740
+ react.useEffect(() => {
741
+ const root = pageRef.current;
742
+ if (!root) return;
743
+ root.querySelectorAll("[data-cms]").forEach((el) => {
744
+ const id = el.dataset.cms;
745
+ const t = id ? transforms?.[id] : void 0;
746
+ if (t) {
747
+ el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1})`;
748
+ el.style.opacity = t.opacity != null ? String(t.opacity) : "";
749
+ } else {
750
+ el.style.transform = "";
751
+ el.style.opacity = "";
752
+ }
753
+ });
754
+ }, [transforms, ed.state.doc]);
755
+ const measureBox = react.useCallback(() => {
756
+ if (!editing || !selection || !pageRef.current) return setBox(null);
757
+ const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
758
+ if (!el) return setBox(null);
759
+ const r = el.getBoundingClientRect();
760
+ setBox({ x: r.left, y: r.top, w: r.width, h: r.height });
761
+ }, [editing, selection]);
736
762
  react.useLayoutEffect(() => {
737
- if (!editing || !selection || !pageRef.current) {
738
- setBox(null);
739
- return;
740
- }
741
- const update = () => {
742
- const el = pageRef.current?.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
743
- if (!el) return setBox(null);
744
- const r = el.getBoundingClientRect();
745
- setBox({ x: r.left, y: r.top, w: r.width, h: r.height });
746
- };
747
- update();
748
- window.addEventListener("scroll", update, { passive: true });
749
- window.addEventListener("resize", update);
763
+ measureBox();
764
+ window.addEventListener("scroll", measureBox, { passive: true });
765
+ window.addEventListener("resize", measureBox);
750
766
  return () => {
751
- window.removeEventListener("scroll", update);
752
- window.removeEventListener("resize", update);
767
+ window.removeEventListener("scroll", measureBox);
768
+ window.removeEventListener("resize", measureBox);
753
769
  };
754
- }, [editing, selection, ed.state.doc]);
770
+ }, [measureBox, ed.state.doc, transforms]);
755
771
  react.useEffect(() => {
756
- if (!editing || !selection || !pageRef.current) return;
757
- if (selNode?.type !== "text") return;
772
+ if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
758
773
  const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
759
774
  if (!el) return;
760
775
  el.setAttribute("contenteditable", "true");
761
776
  el.style.outline = "none";
762
777
  el.focus();
763
- const commit = () => {
764
- ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
765
- };
778
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
766
779
  el.addEventListener("blur", commit);
767
780
  return () => {
768
781
  el.removeEventListener("blur", commit);
@@ -784,29 +797,21 @@ function EditablePage({
784
797
  const siblings = childrenOf(ed.state.doc, parent);
785
798
  const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
786
799
  const id = `n${ed.state.doc.seq + 1}-${Math.floor(performance.now())}`;
787
- ed.apply({
788
- t: "insert_node",
789
- node: { id, type: "text", parent, order, props: { content: "New text" }, style: { base: { color: "inherit" } } }
790
- });
800
+ ed.apply({ t: "insert_node", node: { id, type: "text", parent, order, props: { content: "New text" }, style: { base: { color: "inherit" } } } });
791
801
  ed.select(id);
792
802
  }, [ed, selNode]);
793
803
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
794
804
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
795
805
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) }),
796
- editing && box ? /* @__PURE__ */ jsxRuntime.jsx(
797
- "div",
806
+ editing && box && selection ? /* @__PURE__ */ jsxRuntime.jsx(
807
+ SelectionOverlay,
798
808
  {
799
- style: {
800
- position: "fixed",
801
- left: box.x - 1,
802
- top: box.y - 1,
803
- width: box.w + 2,
804
- height: box.h + 2,
805
- outline: "2px solid #8b5cf6",
806
- borderRadius: 4,
807
- pointerEvents: "none",
808
- zIndex: Z
809
- }
809
+ box,
810
+ base: transforms?.[selection] ?? {},
811
+ movable: Boolean(onNodeTransform),
812
+ getEl: () => pageRef.current?.querySelector(`[data-cms="${cssEscape2(selection)}"]`) ?? null,
813
+ onTransform: (t) => onNodeTransform?.(selection, t),
814
+ onLive: measureBox
810
815
  }
811
816
  ) : null,
812
817
  editing && box && selNode ? /* @__PURE__ */ jsxRuntime.jsx(FloatingToolbar, { box, isText: selNode.type === "text", base: selNode.style.base, setStyle }) : null,
@@ -828,17 +833,102 @@ function EditablePage({
828
833
  editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z }, children: timelineDock }) : null
829
834
  ] });
830
835
  }
836
+ function SelectionOverlay({
837
+ box,
838
+ base,
839
+ movable,
840
+ getEl,
841
+ onTransform,
842
+ onLive
843
+ }) {
844
+ const drag = react.useRef(null);
845
+ const apply = (t) => {
846
+ const el = getEl();
847
+ if (el) {
848
+ el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1})`;
849
+ el.style.opacity = t.opacity != null ? String(t.opacity) : "";
850
+ }
851
+ };
852
+ const start = (kind) => (e) => {
853
+ e.stopPropagation();
854
+ e.preventDefault();
855
+ e.currentTarget.setPointerCapture(e.pointerId);
856
+ drag.current = { kind, x: e.clientX, y: e.clientY, w: box.w, live: { ...base } };
857
+ };
858
+ const move = (e) => {
859
+ const d = drag.current;
860
+ if (!d) return;
861
+ if (d.kind === "move") {
862
+ d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
863
+ } else {
864
+ const factor = Math.max(0.1, (d.w + (e.clientX - d.x)) / d.w);
865
+ d.live = { ...base, scale: Number(((base.scale ?? 1) * factor).toFixed(3)) };
866
+ }
867
+ apply(d.live);
868
+ onLive();
869
+ };
870
+ const end = () => {
871
+ const d = drag.current;
872
+ if (d) onTransform(d.live);
873
+ drag.current = null;
874
+ };
875
+ const outline = {
876
+ position: "fixed",
877
+ left: box.x - 1,
878
+ top: box.y - 1,
879
+ width: box.w + 2,
880
+ height: box.h + 2,
881
+ outline: "2px solid #8b5cf6",
882
+ borderRadius: 4,
883
+ pointerEvents: "none",
884
+ zIndex: Z
885
+ };
886
+ const handle = {
887
+ position: "fixed",
888
+ width: 16,
889
+ height: 16,
890
+ background: "#8b5cf6",
891
+ border: "2px solid #fff",
892
+ borderRadius: 5,
893
+ zIndex: Z + 1,
894
+ touchAction: "none"
895
+ };
896
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
897
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: outline }),
898
+ movable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
899
+ /* @__PURE__ */ jsxRuntime.jsx(
900
+ "div",
901
+ {
902
+ title: "Drag to move",
903
+ onPointerDown: start("move"),
904
+ onPointerMove: move,
905
+ onPointerUp: end,
906
+ style: { ...handle, left: box.x - 9, top: box.y - 9, cursor: "move", borderRadius: 9 }
907
+ }
908
+ ),
909
+ /* @__PURE__ */ jsxRuntime.jsx(
910
+ "div",
911
+ {
912
+ title: "Drag to resize",
913
+ onPointerDown: start("resize"),
914
+ onPointerMove: move,
915
+ onPointerUp: end,
916
+ style: { ...handle, left: box.x + box.w - 7, top: box.y + box.h - 7, cursor: "nwse-resize" }
917
+ }
918
+ )
919
+ ] }) : null
920
+ ] });
921
+ }
831
922
  function FloatingToolbar({
832
923
  box,
833
924
  isText,
834
925
  base,
835
926
  setStyle
836
927
  }) {
837
- const top = Math.max(8, box.y - 46);
838
928
  const wrap = {
839
929
  position: "fixed",
840
930
  left: box.x,
841
- top,
931
+ top: Math.max(8, box.y - 46),
842
932
  display: "flex",
843
933
  alignItems: "center",
844
934
  gap: 8,
@@ -859,17 +949,7 @@ function FloatingToolbar({
859
949
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "A" }),
860
950
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: base.color ?? "#0f172a", onChange: (e) => setStyle({ color: e.target.value }) })
861
951
  ] }) : null,
862
- isText ? /* @__PURE__ */ jsxRuntime.jsx(
863
- "input",
864
- {
865
- type: "number",
866
- title: "Font size (px)",
867
- style: num,
868
- placeholder: "size",
869
- value: base.fontSize?.value ?? "",
870
- onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
871
- }
872
- ) : null,
952
+ isText ? /* @__PURE__ */ jsxRuntime.jsx("input", { type: "number", title: "Font size (px)", style: num, placeholder: "size", value: base.fontSize?.value ?? "", onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } }) }) : null,
873
953
  /* @__PURE__ */ jsxRuntime.jsxs("label", { title: "Background", style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
874
954
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "BG" }),
875
955
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: normalizeColor(base.background), onChange: (e) => setStyle({ background: e.target.value }) })
@@ -885,22 +965,7 @@ function EditBar({
885
965
  onRedo,
886
966
  onAddText
887
967
  }) {
888
- const bar = {
889
- position: "fixed",
890
- top: 12,
891
- right: 12,
892
- display: "flex",
893
- gap: 6,
894
- alignItems: "center",
895
- padding: 6,
896
- background: "#0f172a",
897
- color: "#fff",
898
- borderRadius: 999,
899
- boxShadow: "0 8px 24px -8px rgba(0,0,0,0.4)",
900
- zIndex: Z + 3,
901
- fontFamily: "system-ui, sans-serif",
902
- fontSize: 12
903
- };
968
+ 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 };
904
969
  const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
905
970
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: bar, children: [
906
971
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),