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

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.
@@ -0,0 +1,47 @@
1
+ // src/document/fractional.ts
2
+ var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3
+ var RADIX = BASE.length;
4
+ var MID = Math.floor(RADIX / 2);
5
+ function val(c) {
6
+ const i = BASE.indexOf(c);
7
+ return i < 0 ? 0 : i;
8
+ }
9
+ function keyBetween(a, b) {
10
+ if (a === null) a = "";
11
+ if (b === null) {
12
+ return a + BASE[MID];
13
+ }
14
+ if (a >= b) {
15
+ throw new Error(`keyBetween: lower bound "${a}" is not below upper bound "${b}"`);
16
+ }
17
+ let result = "";
18
+ let i = 0;
19
+ for (; ; ) {
20
+ const ca = i < a.length ? val(a[i]) : 0;
21
+ const cb = i < b.length ? val(b[i]) : RADIX;
22
+ if (cb - ca > 1) {
23
+ result += BASE[ca + Math.floor((cb - ca) / 2)];
24
+ return result;
25
+ }
26
+ if (ca === cb) {
27
+ result += BASE[ca];
28
+ i++;
29
+ continue;
30
+ }
31
+ result += BASE[ca];
32
+ i++;
33
+ for (; ; ) {
34
+ const da = i < a.length ? val(a[i]) : 0;
35
+ if (RADIX - da > 1) {
36
+ result += BASE[da + Math.floor((RADIX - da) / 2)];
37
+ return result;
38
+ }
39
+ result += BASE[da];
40
+ i++;
41
+ }
42
+ }
43
+ }
44
+
45
+ export { keyBetween };
46
+ //# sourceMappingURL=chunk-7YYLYBLI.js.map
47
+ //# sourceMappingURL=chunk-7YYLYBLI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/document/fractional.ts"],"names":[],"mappings":";AAaA,IAAM,IAAA,GAAO,gEAAA;AACb,IAAM,QAAQ,IAAA,CAAK,MAAA;AACnB,IAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,CAAC,CAAA;AAEhC,SAAS,IAAI,CAAA,EAAmB;AAC9B,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,CAAC,CAAA;AACxB,EAAA,OAAO,CAAA,GAAI,IAAI,CAAA,GAAI,CAAA;AACrB;AAOO,SAAS,UAAA,CAAW,GAAkB,CAAA,EAA0B;AACrE,EAAA,IAAI,CAAA,KAAM,MAAM,CAAA,GAAI,EAAA;AACpB,EAAA,IAAI,MAAM,IAAA,EAAM;AAEd,IAAA,OAAO,CAAA,GAAI,KAAK,GAAG,CAAA;AAAA,EACrB;AACA,EAAA,IAAI,KAAK,CAAA,EAAG;AACV,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,CAAC,CAAA,4BAAA,EAA+B,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,EAClF;AAEA,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,IAAI,CAAA,GAAI,CAAA;AAER,EAAA,WAAS;AACP,IAAA,MAAM,EAAA,GAAK,IAAI,CAAA,CAAE,MAAA,GAAS,IAAI,CAAA,CAAE,CAAC,CAAW,CAAA,GAAI,CAAA;AAChD,IAAA,MAAM,EAAA,GAAK,IAAI,CAAA,CAAE,MAAA,GAAS,IAAI,CAAA,CAAE,CAAC,CAAW,CAAA,GAAI,KAAA;AAChD,IAAA,IAAI,EAAA,GAAK,KAAK,CAAA,EAAG;AACf,MAAA,MAAA,IAAU,KAAK,EAAA,GAAK,IAAA,CAAK,OAAO,EAAA,GAAK,EAAA,IAAM,CAAC,CAAC,CAAA;AAC7C,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,IAAI,OAAO,EAAA,EAAI;AACb,MAAA,MAAA,IAAU,KAAK,EAAE,CAAA;AACjB,MAAA,CAAA,EAAA;AACA,MAAA;AAAA,IACF;AAGA,IAAA,MAAA,IAAU,KAAK,EAAE,CAAA;AACjB,IAAA,CAAA,EAAA;AACA,IAAA,WAAS;AACP,MAAA,MAAM,EAAA,GAAK,IAAI,CAAA,CAAE,MAAA,GAAS,IAAI,CAAA,CAAE,CAAC,CAAW,CAAA,GAAI,CAAA;AAChD,MAAA,IAAI,KAAA,GAAQ,KAAK,CAAA,EAAG;AAClB,QAAA,MAAA,IAAU,KAAK,EAAA,GAAK,IAAA,CAAK,OAAO,KAAA,GAAQ,EAAA,IAAM,CAAC,CAAC,CAAA;AAChD,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,MAAA,IAAU,KAAK,EAAE,CAAA;AACjB,MAAA,CAAA,EAAA;AAAA,IACF;AAAA,EACF;AACF","file":"chunk-7YYLYBLI.js","sourcesContent":["/**\n * Minimal collab-safe fractional ordering keys.\n *\n * Sibling `order` is a string over a base-62 alphabet interpreted as a fraction\n * in (0, 1). `keyBetween(a, b)` returns a key that sorts strictly between `a`\n * and `b` (lexicographically), extending precision when the bounds are\n * adjacent. Concurrent inserts at the same spot produce different keys (vary by\n * actor), so reorders never collide.\n *\n * This is intentionally compact for Phase 0. Before live collaboration ships it\n * should be swapped for a hardened implementation with jitter + bucket bounds.\n */\n\nconst BASE = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\nconst RADIX = BASE.length; // 62\nconst MID = Math.floor(RADIX / 2);\n\nfunction val(c: string): number {\n const i = BASE.indexOf(c);\n return i < 0 ? 0 : i;\n}\n\n/**\n * Return a key strictly between `a` and `b`. Pass `null` for an open bound\n * (`keyBetween(null, null)` for the first key, `keyBetween(last, null)` to\n * append, `keyBetween(null, first)` to prepend).\n */\nexport function keyBetween(a: string | null, b: string | null): string {\n if (a === null) a = \"\";\n if (b === null) {\n // Append: a key greater than `a`. Appending a mid digit always sorts after.\n return a + BASE[MID];\n }\n if (a >= b) {\n throw new Error(`keyBetween: lower bound \"${a}\" is not below upper bound \"${b}\"`);\n }\n\n let result = \"\";\n let i = 0;\n // Walk the common prefix, then split at the first differing digit.\n for (;;) {\n const ca = i < a.length ? val(a[i] as string) : 0;\n const cb = i < b.length ? val(b[i] as string) : RADIX;\n if (cb - ca > 1) {\n result += BASE[ca + Math.floor((cb - ca) / 2)];\n return result;\n }\n if (ca === cb) {\n result += BASE[ca];\n i++;\n continue;\n }\n // Adjacent digits (cb - ca === 1): take the lower path, then descend into\n // `a`'s remainder with RADIX as the upper bound until a gap opens.\n result += BASE[ca];\n i++;\n for (;;) {\n const da = i < a.length ? val(a[i] as string) : 0;\n if (RADIX - da > 1) {\n result += BASE[da + Math.floor((RADIX - da) / 2)];\n return result;\n }\n result += BASE[da];\n i++;\n }\n }\n}\n"]}
package/dist/editor.cjs CHANGED
@@ -697,17 +697,42 @@ function Toolbar({ ed }) {
697
697
  }
698
698
  );
699
699
  }
700
+
701
+ // src/document/fractional.ts
702
+ var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
703
+ var RADIX = BASE.length;
704
+ var MID = Math.floor(RADIX / 2);
705
+ function keyBetween(a, b) {
706
+ if (a === null) a = "";
707
+ {
708
+ return a + BASE[MID];
709
+ }
710
+ }
711
+ var Z = 2147483e3;
700
712
  function EditablePage({
701
713
  doc,
702
714
  registry = defaultRegistry,
703
- defaultEditing = false,
704
715
  timelineDock
705
716
  }) {
706
717
  const ed = useEditor(doc);
707
- const [editing, setEditing] = react.useState(defaultEditing);
718
+ const [editing, setEditing] = react.useState(false);
719
+ const [revealed, setRevealed] = react.useState(false);
708
720
  const pageRef = react.useRef(null);
709
721
  const [box, setBox] = react.useState(null);
710
722
  const { selection } = ed.state;
723
+ const selNode = selection ? ed.state.doc.nodes[selection] : null;
724
+ react.useEffect(() => {
725
+ const onKey = (e) => setRevealed(e.ctrlKey && e.shiftKey);
726
+ const onBlur = () => setRevealed(false);
727
+ window.addEventListener("keydown", onKey);
728
+ window.addEventListener("keyup", onKey);
729
+ window.addEventListener("blur", onBlur);
730
+ return () => {
731
+ window.removeEventListener("keydown", onKey);
732
+ window.removeEventListener("keyup", onKey);
733
+ window.removeEventListener("blur", onBlur);
734
+ };
735
+ }, []);
711
736
  react.useLayoutEffect(() => {
712
737
  if (!editing || !selection || !pageRef.current) {
713
738
  setBox(null);
@@ -727,65 +752,130 @@ function EditablePage({
727
752
  window.removeEventListener("resize", update);
728
753
  };
729
754
  }, [editing, selection, ed.state.doc]);
755
+ react.useEffect(() => {
756
+ if (!editing || !selection || !pageRef.current) return;
757
+ if (selNode?.type !== "text") return;
758
+ const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
759
+ if (!el) return;
760
+ el.setAttribute("contenteditable", "true");
761
+ el.style.outline = "none";
762
+ el.focus();
763
+ const commit = () => {
764
+ ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
765
+ };
766
+ el.addEventListener("blur", commit);
767
+ return () => {
768
+ el.removeEventListener("blur", commit);
769
+ el.removeAttribute("contenteditable");
770
+ };
771
+ }, [editing, selection, selNode?.type, ed]);
730
772
  const onClickCapture = (e) => {
731
773
  if (!editing) return;
732
774
  const el = e.target.closest("[data-cms]");
733
775
  if (el) {
776
+ if (el.getAttribute("contenteditable") === "true") return;
734
777
  e.preventDefault();
735
778
  e.stopPropagation();
736
779
  ed.select(el.getAttribute("data-cms"));
737
780
  }
738
781
  };
739
- const outline = box ? {
740
- position: "fixed",
741
- left: box.x,
742
- top: box.y,
743
- width: box.w,
744
- height: box.h,
745
- outline: "2px solid #8b5cf6",
746
- outlineOffset: -1,
747
- pointerEvents: "none",
748
- zIndex: 2147483e3
749
- } : null;
782
+ const addText = react.useCallback(() => {
783
+ const parent = selNode && selNode.type !== "text" ? selNode.id : ed.state.doc.sections[0] ?? null;
784
+ const siblings = childrenOf(ed.state.doc, parent);
785
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
786
+ 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
+ });
791
+ ed.select(id);
792
+ }, [ed, selNode]);
793
+ const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
750
794
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
751
795
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) }),
752
- editing && outline ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: outline }) : null,
753
- /* @__PURE__ */ jsxRuntime.jsx(
796
+ editing && box ? /* @__PURE__ */ jsxRuntime.jsx(
797
+ "div",
798
+ {
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
+ }
810
+ }
811
+ ) : null,
812
+ editing && box && selNode ? /* @__PURE__ */ jsxRuntime.jsx(FloatingToolbar, { box, isText: selNode.type === "text", base: selNode.style.base, setStyle }) : null,
813
+ revealed || editing ? /* @__PURE__ */ jsxRuntime.jsx(
754
814
  EditBar,
755
815
  {
756
816
  editing,
757
- onToggle: () => setEditing((v) => !v),
817
+ onToggle: () => {
818
+ setEditing((v) => !v);
819
+ ed.select(null);
820
+ },
758
821
  canUndo: ed.canUndo,
759
822
  canRedo: ed.canRedo,
760
823
  onUndo: ed.undo,
761
824
  onRedo: ed.redo,
762
- hasDock: Boolean(timelineDock)
825
+ onAddText: addText
763
826
  }
764
- ),
765
- editing && selection ? /* @__PURE__ */ jsxRuntime.jsx("aside", { style: inspectorPanel, children: /* @__PURE__ */ jsxRuntime.jsx(Inspector, { doc: ed.state.doc, selection, apply: ed.apply }) }) : null,
766
- editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: dockSlot, children: timelineDock }) : null
827
+ ) : null,
828
+ editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z }, children: timelineDock }) : null
829
+ ] });
830
+ }
831
+ function FloatingToolbar({
832
+ box,
833
+ isText,
834
+ base,
835
+ setStyle
836
+ }) {
837
+ const top = Math.max(8, box.y - 46);
838
+ const wrap = {
839
+ position: "fixed",
840
+ left: box.x,
841
+ top,
842
+ display: "flex",
843
+ alignItems: "center",
844
+ gap: 8,
845
+ padding: "6px 10px",
846
+ background: "#0f172a",
847
+ color: "#fff",
848
+ border: "1px solid rgba(255,255,255,0.12)",
849
+ borderRadius: 999,
850
+ boxShadow: "0 8px 24px -8px rgba(0,0,0,0.5)",
851
+ zIndex: Z + 2,
852
+ fontFamily: "system-ui, sans-serif",
853
+ fontSize: 12
854
+ };
855
+ const swatch = { width: 22, height: 22, border: "none", background: "transparent", padding: 0, cursor: "pointer" };
856
+ const num = { width: 52, background: "#1e293b", color: "#fff", border: "1px solid #334155", borderRadius: 6, padding: "3px 6px", font: "inherit" };
857
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: wrap, onMouseDown: (e) => e.preventDefault(), children: [
858
+ isText ? /* @__PURE__ */ jsxRuntime.jsxs("label", { title: "Text color", style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
859
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "A" }),
860
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: base.color ?? "#0f172a", onChange: (e) => setStyle({ color: e.target.value }) })
861
+ ] }) : 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,
873
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { title: "Background", style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
874
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "BG" }),
875
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: normalizeColor(base.background), onChange: (e) => setStyle({ background: e.target.value }) })
876
+ ] })
767
877
  ] });
768
878
  }
769
- var inspectorPanel = {
770
- position: "fixed",
771
- top: 56,
772
- right: 12,
773
- bottom: 12,
774
- width: 300,
775
- background: "#fff",
776
- border: "1px solid #e2e8f0",
777
- borderRadius: 12,
778
- boxShadow: "0 10px 30px -10px rgba(0,0,0,0.25)",
779
- overflow: "auto",
780
- zIndex: 2147483001
781
- };
782
- var dockSlot = {
783
- position: "fixed",
784
- left: 0,
785
- right: 0,
786
- bottom: 0,
787
- zIndex: 2147483e3
788
- };
789
879
  function EditBar({
790
880
  editing,
791
881
  onToggle,
@@ -793,7 +883,7 @@ function EditBar({
793
883
  canRedo,
794
884
  onUndo,
795
885
  onRedo,
796
- hasDock
886
+ onAddText
797
887
  }) {
798
888
  const bar = {
799
889
  position: "fixed",
@@ -807,28 +897,23 @@ function EditBar({
807
897
  color: "#fff",
808
898
  borderRadius: 999,
809
899
  boxShadow: "0 8px 24px -8px rgba(0,0,0,0.4)",
810
- zIndex: 2147483002,
900
+ zIndex: Z + 3,
811
901
  fontFamily: "system-ui, sans-serif",
812
902
  fontSize: 12
813
903
  };
814
- const btn = {
815
- font: "inherit",
816
- border: "none",
817
- background: "transparent",
818
- color: "inherit",
819
- cursor: "pointer",
820
- padding: "4px 8px",
821
- borderRadius: 999
822
- };
904
+ const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
823
905
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: bar, children: [
824
906
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
825
907
  editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
908
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: btn, onClick: onAddText, title: "Add a text element", children: "\uFF0B Text" }),
826
909
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: canUndo ? 1 : 0.4 }, disabled: !canUndo, onClick: onUndo, children: "\u21B6" }),
827
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: canRedo ? 1 : 0.4 }, disabled: !canRedo, onClick: onRedo, children: "\u21B7" }),
828
- !hasDock ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.5, paddingRight: 6 }, children: "timeline: fancy-motion \u2192" }) : null
910
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: canRedo ? 1 : 0.4 }, disabled: !canRedo, onClick: onRedo, children: "\u21B7" })
829
911
  ] }) : null
830
912
  ] });
831
913
  }
914
+ function normalizeColor(v) {
915
+ return v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : "#ffffff";
916
+ }
832
917
  function cssEscape2(value) {
833
918
  return value.replace(/["\\]/g, "\\$&");
834
919
  }