@particle-academy/fancy-cms-ui 0.0.1-dev.2 → 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.
@@ -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,138 +697,288 @@ 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
- timelineDock
715
+ timelineDock,
716
+ transforms,
717
+ onNodeTransform,
718
+ onSelect
705
719
  }) {
706
720
  const ed = useEditor(doc);
707
- const [editing, setEditing] = react.useState(defaultEditing);
721
+ const [editing, setEditing] = react.useState(false);
722
+ const [revealed, setRevealed] = react.useState(false);
708
723
  const pageRef = react.useRef(null);
709
724
  const [box, setBox] = react.useState(null);
710
725
  const { selection } = ed.state;
726
+ const selNode = selection ? ed.state.doc.nodes[selection] : null;
727
+ react.useEffect(() => onSelect?.(selection), [selection, onSelect]);
728
+ react.useEffect(() => {
729
+ const onKey = (e) => setRevealed(e.ctrlKey && e.shiftKey);
730
+ const onBlur = () => setRevealed(false);
731
+ window.addEventListener("keydown", onKey);
732
+ window.addEventListener("keyup", onKey);
733
+ window.addEventListener("blur", onBlur);
734
+ return () => {
735
+ window.removeEventListener("keydown", onKey);
736
+ window.removeEventListener("keyup", onKey);
737
+ window.removeEventListener("blur", onBlur);
738
+ };
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]);
711
762
  react.useLayoutEffect(() => {
712
- if (!editing || !selection || !pageRef.current) {
713
- setBox(null);
714
- return;
715
- }
716
- const update = () => {
717
- const el = pageRef.current?.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
718
- if (!el) return setBox(null);
719
- const r = el.getBoundingClientRect();
720
- setBox({ x: r.left, y: r.top, w: r.width, h: r.height });
763
+ measureBox();
764
+ window.addEventListener("scroll", measureBox, { passive: true });
765
+ window.addEventListener("resize", measureBox);
766
+ return () => {
767
+ window.removeEventListener("scroll", measureBox);
768
+ window.removeEventListener("resize", measureBox);
721
769
  };
722
- update();
723
- window.addEventListener("scroll", update, { passive: true });
724
- window.addEventListener("resize", update);
770
+ }, [measureBox, ed.state.doc, transforms]);
771
+ react.useEffect(() => {
772
+ if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
773
+ const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
774
+ if (!el) return;
775
+ el.setAttribute("contenteditable", "true");
776
+ el.style.outline = "none";
777
+ el.focus();
778
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
779
+ el.addEventListener("blur", commit);
725
780
  return () => {
726
- window.removeEventListener("scroll", update);
727
- window.removeEventListener("resize", update);
781
+ el.removeEventListener("blur", commit);
782
+ el.removeAttribute("contenteditable");
728
783
  };
729
- }, [editing, selection, ed.state.doc]);
784
+ }, [editing, selection, selNode?.type, ed]);
730
785
  const onClickCapture = (e) => {
731
786
  if (!editing) return;
732
787
  const el = e.target.closest("[data-cms]");
733
788
  if (el) {
789
+ if (el.getAttribute("contenteditable") === "true") return;
734
790
  e.preventDefault();
735
791
  e.stopPropagation();
736
792
  ed.select(el.getAttribute("data-cms"));
737
793
  }
738
794
  };
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;
795
+ const addText = react.useCallback(() => {
796
+ const parent = selNode && selNode.type !== "text" ? selNode.id : ed.state.doc.sections[0] ?? null;
797
+ const siblings = childrenOf(ed.state.doc, parent);
798
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
799
+ const id = `n${ed.state.doc.seq + 1}-${Math.floor(performance.now())}`;
800
+ ed.apply({ t: "insert_node", node: { id, type: "text", parent, order, props: { content: "New text" }, style: { base: { color: "inherit" } } } });
801
+ ed.select(id);
802
+ }, [ed, selNode]);
803
+ const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
750
804
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
751
805
  /* @__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(
806
+ editing && box && selection ? /* @__PURE__ */ jsxRuntime.jsx(
807
+ SelectionOverlay,
808
+ {
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
815
+ }
816
+ ) : null,
817
+ editing && box && selNode ? /* @__PURE__ */ jsxRuntime.jsx(FloatingToolbar, { box, isText: selNode.type === "text", base: selNode.style.base, setStyle }) : null,
818
+ revealed || editing ? /* @__PURE__ */ jsxRuntime.jsx(
754
819
  EditBar,
755
820
  {
756
821
  editing,
757
- onToggle: () => setEditing((v) => !v),
822
+ onToggle: () => {
823
+ setEditing((v) => !v);
824
+ ed.select(null);
825
+ },
758
826
  canUndo: ed.canUndo,
759
827
  canRedo: ed.canRedo,
760
828
  onUndo: ed.undo,
761
829
  onRedo: ed.redo,
762
- hasDock: Boolean(timelineDock)
830
+ onAddText: addText
763
831
  }
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
832
+ ) : null,
833
+ editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z }, children: timelineDock }) : null
767
834
  ] });
768
835
  }
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
- function EditBar({
790
- editing,
791
- onToggle,
792
- canUndo,
793
- canRedo,
794
- onUndo,
795
- onRedo,
796
- hasDock
836
+ function SelectionOverlay({
837
+ box,
838
+ base,
839
+ movable,
840
+ getEl,
841
+ onTransform,
842
+ onLive
797
843
  }) {
798
- const bar = {
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
+ }
922
+ function FloatingToolbar({
923
+ box,
924
+ isText,
925
+ base,
926
+ setStyle
927
+ }) {
928
+ const wrap = {
799
929
  position: "fixed",
800
- top: 12,
801
- right: 12,
930
+ left: box.x,
931
+ top: Math.max(8, box.y - 46),
802
932
  display: "flex",
803
- gap: 6,
804
933
  alignItems: "center",
805
- padding: 6,
934
+ gap: 8,
935
+ padding: "6px 10px",
806
936
  background: "#0f172a",
807
937
  color: "#fff",
938
+ border: "1px solid rgba(255,255,255,0.12)",
808
939
  borderRadius: 999,
809
- boxShadow: "0 8px 24px -8px rgba(0,0,0,0.4)",
810
- zIndex: 2147483002,
940
+ boxShadow: "0 8px 24px -8px rgba(0,0,0,0.5)",
941
+ zIndex: Z + 2,
811
942
  fontFamily: "system-ui, sans-serif",
812
943
  fontSize: 12
813
944
  };
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
- };
945
+ const swatch = { width: 22, height: 22, border: "none", background: "transparent", padding: 0, cursor: "pointer" };
946
+ const num = { width: 52, background: "#1e293b", color: "#fff", border: "1px solid #334155", borderRadius: 6, padding: "3px 6px", font: "inherit" };
947
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: wrap, onMouseDown: (e) => e.preventDefault(), children: [
948
+ isText ? /* @__PURE__ */ jsxRuntime.jsxs("label", { title: "Text color", style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
949
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "A" }),
950
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: base.color ?? "#0f172a", onChange: (e) => setStyle({ color: e.target.value }) })
951
+ ] }) : 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,
953
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { title: "Background", style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [
954
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.7 }, children: "BG" }),
955
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "color", style: swatch, value: normalizeColor(base.background), onChange: (e) => setStyle({ background: e.target.value }) })
956
+ ] })
957
+ ] });
958
+ }
959
+ function EditBar({
960
+ editing,
961
+ onToggle,
962
+ canUndo,
963
+ canRedo,
964
+ onUndo,
965
+ onRedo,
966
+ onAddText
967
+ }) {
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 };
969
+ const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
823
970
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: bar, children: [
824
971
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
825
972
  editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
973
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: btn, onClick: onAddText, title: "Add a text element", children: "\uFF0B Text" }),
826
974
  /* @__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
975
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: canRedo ? 1 : 0.4 }, disabled: !canRedo, onClick: onRedo, children: "\u21B7" })
829
976
  ] }) : null
830
977
  ] });
831
978
  }
979
+ function normalizeColor(v) {
980
+ return v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : "#ffffff";
981
+ }
832
982
  function cssEscape2(value) {
833
983
  return value.replace(/["\\]/g, "\\$&");
834
984
  }