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

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,15 +712,22 @@ var Z = 2147483e3;
712
712
  function EditablePage({
713
713
  doc,
714
714
  registry = defaultRegistry,
715
- timelineDock
715
+ timelineDock,
716
+ transforms,
717
+ onNodeTransform,
718
+ onSelect,
719
+ frames = 1,
720
+ onProgress
716
721
  }) {
717
722
  const ed = useEditor(doc);
718
723
  const [editing, setEditing] = react.useState(false);
719
724
  const [revealed, setRevealed] = react.useState(false);
720
725
  const pageRef = react.useRef(null);
726
+ const spacerRef = react.useRef(null);
721
727
  const [box, setBox] = react.useState(null);
722
728
  const { selection } = ed.state;
723
729
  const selNode = selection ? ed.state.doc.nodes[selection] : null;
730
+ react.useEffect(() => onSelect?.(selection), [selection, onSelect]);
724
731
  react.useEffect(() => {
725
732
  const onKey = (e) => setRevealed(e.ctrlKey && e.shiftKey);
726
733
  const onBlur = () => setRevealed(false);
@@ -733,36 +740,67 @@ function EditablePage({
733
740
  window.removeEventListener("blur", onBlur);
734
741
  };
735
742
  }, []);
736
- 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 });
743
+ react.useEffect(() => {
744
+ const root = pageRef.current;
745
+ if (!root) return;
746
+ root.querySelectorAll("[data-cms]").forEach((el) => {
747
+ const id = el.dataset.cms;
748
+ const t = id ? transforms?.[id] : void 0;
749
+ if (t) {
750
+ el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1})`;
751
+ el.style.opacity = t.opacity != null ? String(t.opacity) : "";
752
+ } else {
753
+ el.style.transform = "";
754
+ el.style.opacity = "";
755
+ }
756
+ });
757
+ }, [transforms, ed.state.doc]);
758
+ react.useEffect(() => {
759
+ const spacer = spacerRef.current;
760
+ if (!spacer || !onProgress) return;
761
+ let raf = 0;
762
+ const compute = () => {
763
+ raf = 0;
764
+ const len = spacer.offsetHeight - window.innerHeight;
765
+ const top = spacer.getBoundingClientRect().top;
766
+ onProgress(len > 0 ? Math.min(1, Math.max(0, -top / len)) : 0);
767
+ };
768
+ const onScroll = () => {
769
+ if (!raf) raf = requestAnimationFrame(compute);
746
770
  };
747
- update();
748
- window.addEventListener("scroll", update, { passive: true });
749
- window.addEventListener("resize", update);
771
+ compute();
772
+ window.addEventListener("scroll", onScroll, { passive: true });
773
+ window.addEventListener("resize", onScroll);
750
774
  return () => {
751
- window.removeEventListener("scroll", update);
752
- window.removeEventListener("resize", update);
775
+ window.removeEventListener("scroll", onScroll);
776
+ window.removeEventListener("resize", onScroll);
777
+ if (raf) cancelAnimationFrame(raf);
753
778
  };
754
- }, [editing, selection, ed.state.doc]);
779
+ }, [onProgress, frames]);
780
+ const measureBox = react.useCallback(() => {
781
+ if (!editing || !selection || !pageRef.current) return setBox(null);
782
+ const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
783
+ if (!el) return setBox(null);
784
+ const r = el.getBoundingClientRect();
785
+ setBox({ x: r.left, y: r.top, w: r.width, h: r.height });
786
+ }, [editing, selection]);
787
+ react.useLayoutEffect(() => {
788
+ measureBox();
789
+ window.addEventListener("scroll", measureBox, { passive: true });
790
+ window.addEventListener("resize", measureBox);
791
+ return () => {
792
+ window.removeEventListener("scroll", measureBox);
793
+ window.removeEventListener("resize", measureBox);
794
+ };
795
+ }, [measureBox, ed.state.doc, transforms]);
755
796
  react.useEffect(() => {
756
- if (!editing || !selection || !pageRef.current) return;
757
- if (selNode?.type !== "text") return;
797
+ if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
758
798
  const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
759
799
  if (!el) return;
760
800
  el.setAttribute("contenteditable", "true");
761
801
  el.style.outline = "none";
762
802
  el.focus();
763
- const commit = () => {
764
- ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
765
- };
803
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
766
804
  el.addEventListener("blur", commit);
767
805
  return () => {
768
806
  el.removeEventListener("blur", commit);
@@ -784,29 +822,21 @@ function EditablePage({
784
822
  const siblings = childrenOf(ed.state.doc, parent);
785
823
  const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
786
824
  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
- });
825
+ ed.apply({ t: "insert_node", node: { id, type: "text", parent, order, props: { content: "New text" }, style: { base: { color: "inherit" } } } });
791
826
  ed.select(id);
792
827
  }, [ed, selNode]);
793
828
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
794
829
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
795
- /* @__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",
830
+ /* @__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: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) }) }) }),
831
+ editing && box && selection ? /* @__PURE__ */ jsxRuntime.jsx(
832
+ SelectionOverlay,
798
833
  {
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
- }
834
+ box,
835
+ base: transforms?.[selection] ?? {},
836
+ movable: Boolean(onNodeTransform),
837
+ getEl: () => pageRef.current?.querySelector(`[data-cms="${cssEscape2(selection)}"]`) ?? null,
838
+ onTransform: (t) => onNodeTransform?.(selection, t),
839
+ onLive: measureBox
810
840
  }
811
841
  ) : null,
812
842
  editing && box && selNode ? /* @__PURE__ */ jsxRuntime.jsx(FloatingToolbar, { box, isText: selNode.type === "text", base: selNode.style.base, setStyle }) : null,
@@ -828,17 +858,102 @@ function EditablePage({
828
858
  editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z }, children: timelineDock }) : null
829
859
  ] });
830
860
  }
861
+ function SelectionOverlay({
862
+ box,
863
+ base,
864
+ movable,
865
+ getEl,
866
+ onTransform,
867
+ onLive
868
+ }) {
869
+ const drag = react.useRef(null);
870
+ const apply = (t) => {
871
+ const el = getEl();
872
+ if (el) {
873
+ el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1})`;
874
+ el.style.opacity = t.opacity != null ? String(t.opacity) : "";
875
+ }
876
+ };
877
+ const start = (kind) => (e) => {
878
+ e.stopPropagation();
879
+ e.preventDefault();
880
+ e.currentTarget.setPointerCapture(e.pointerId);
881
+ drag.current = { kind, x: e.clientX, y: e.clientY, w: box.w, live: { ...base } };
882
+ };
883
+ const move = (e) => {
884
+ const d = drag.current;
885
+ if (!d) return;
886
+ if (d.kind === "move") {
887
+ d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
888
+ } else {
889
+ const factor = Math.max(0.1, (d.w + (e.clientX - d.x)) / d.w);
890
+ d.live = { ...base, scale: Number(((base.scale ?? 1) * factor).toFixed(3)) };
891
+ }
892
+ apply(d.live);
893
+ onLive();
894
+ };
895
+ const end = () => {
896
+ const d = drag.current;
897
+ if (d) onTransform(d.live);
898
+ drag.current = null;
899
+ };
900
+ const outline = {
901
+ position: "fixed",
902
+ left: box.x - 1,
903
+ top: box.y - 1,
904
+ width: box.w + 2,
905
+ height: box.h + 2,
906
+ outline: "2px solid #8b5cf6",
907
+ borderRadius: 4,
908
+ pointerEvents: "none",
909
+ zIndex: Z
910
+ };
911
+ const handle = {
912
+ position: "fixed",
913
+ width: 16,
914
+ height: 16,
915
+ background: "#8b5cf6",
916
+ border: "2px solid #fff",
917
+ borderRadius: 5,
918
+ zIndex: Z + 1,
919
+ touchAction: "none"
920
+ };
921
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
922
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: outline }),
923
+ movable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
924
+ /* @__PURE__ */ jsxRuntime.jsx(
925
+ "div",
926
+ {
927
+ title: "Drag to move",
928
+ onPointerDown: start("move"),
929
+ onPointerMove: move,
930
+ onPointerUp: end,
931
+ style: { ...handle, left: box.x - 9, top: box.y - 9, cursor: "move", borderRadius: 9 }
932
+ }
933
+ ),
934
+ /* @__PURE__ */ jsxRuntime.jsx(
935
+ "div",
936
+ {
937
+ title: "Drag to resize",
938
+ onPointerDown: start("resize"),
939
+ onPointerMove: move,
940
+ onPointerUp: end,
941
+ style: { ...handle, left: box.x + box.w - 7, top: box.y + box.h - 7, cursor: "nwse-resize" }
942
+ }
943
+ )
944
+ ] }) : null
945
+ ] });
946
+ }
831
947
  function FloatingToolbar({
832
948
  box,
833
949
  isText,
834
950
  base,
835
951
  setStyle
836
952
  }) {
837
- const top = Math.max(8, box.y - 46);
838
953
  const wrap = {
839
954
  position: "fixed",
840
955
  left: box.x,
841
- top,
956
+ top: Math.max(8, box.y - 46),
842
957
  display: "flex",
843
958
  alignItems: "center",
844
959
  gap: 8,
@@ -859,17 +974,7 @@ function FloatingToolbar({
859
974
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "A" }),
860
975
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: base.color ?? "#0f172a", onChange: (e) => setStyle({ color: e.target.value }) })
861
976
  ] }) : 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,
977
+ 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
978
  /* @__PURE__ */ jsxRuntime.jsxs("label", { title: "Background", style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
874
979
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "BG" }),
875
980
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: normalizeColor(base.background), onChange: (e) => setStyle({ background: e.target.value }) })
@@ -885,22 +990,7 @@ function EditBar({
885
990
  onRedo,
886
991
  onAddText
887
992
  }) {
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
- };
993
+ 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
994
  const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
905
995
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: bar, children: [
906
996
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),