@particle-academy/fancy-cms-ui 0.1.1 → 0.3.0

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/index.cjs CHANGED
@@ -576,7 +576,7 @@ function useLatestRef(value) {
576
576
  });
577
577
  return ref;
578
578
  }
579
- function Canvas({ doc, selection, onSelect, apply }) {
579
+ function Canvas({ doc, selection, onSelect, apply, registry, data }) {
580
580
  const ref = react.useRef(null);
581
581
  const [box, setBox] = react.useState(null);
582
582
  const drag = react.useRef(null);
@@ -633,7 +633,7 @@ function Canvas({ doc, selection, onSelect, apply }) {
633
633
  position: "relative",
634
634
  overflow: "auto",
635
635
  height: "100%",
636
- background: "#f8fafc"
636
+ background: "var(--fcms-canvas)"
637
637
  };
638
638
  const overlay = box ? {
639
639
  position: "absolute",
@@ -641,13 +641,13 @@ function Canvas({ doc, selection, onSelect, apply }) {
641
641
  top: box.y,
642
642
  width: box.w,
643
643
  height: box.h,
644
- outline: "2px solid #8b5cf6",
644
+ outline: "2px solid var(--fcms-accent, #8b5cf6)",
645
645
  outlineOffset: -1,
646
646
  cursor: "move",
647
647
  touchAction: "none"
648
648
  } : { display: "none" };
649
649
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, onClick: handleClick, style: surface, children: [
650
- /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc }),
650
+ /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc, registry, data }),
651
651
  /* @__PURE__ */ jsxRuntime.jsx(
652
652
  "div",
653
653
  {
@@ -718,10 +718,10 @@ function LayerRow({ doc, id, depth, selection, onSelect, onMove, overId, setOver
718
718
  cursor: draggable ? "grab" : "pointer",
719
719
  padding: "4px 8px",
720
720
  paddingLeft: 8 + depth * 14,
721
- background: selected ? "#ede9fe" : "transparent",
722
- color: selected ? "#5b21b6" : "#334155",
721
+ background: selected ? "var(--fcms-sel-bg)" : "transparent",
722
+ color: selected ? "var(--fcms-sel-fg)" : "var(--fcms-row-fg)",
723
723
  font: "inherit",
724
- boxShadow: overId === id ? "inset 0 -2px 0 0 #8b5cf6" : "none"
724
+ boxShadow: overId === id ? "inset 0 -2px 0 0 var(--fcms-accent, #8b5cf6)" : "none"
725
725
  },
726
726
  children: [
727
727
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.6 }, children: node.type }),
@@ -738,7 +738,7 @@ var label = {
738
738
  fontSize: 11,
739
739
  textTransform: "uppercase",
740
740
  letterSpacing: "0.04em",
741
- color: "#94a3b8",
741
+ color: "var(--fcms-muted)",
742
742
  margin: "10px 0 4px"
743
743
  };
744
744
  var input = {
@@ -746,21 +746,23 @@ var input = {
746
746
  boxSizing: "border-box",
747
747
  padding: "6px 8px",
748
748
  borderRadius: 6,
749
- border: "1px solid #e2e8f0",
749
+ border: "1px solid var(--fcms-border)",
750
+ background: "var(--fcms-input-bg)",
751
+ color: "var(--fcms-fg)",
750
752
  font: "inherit",
751
753
  fontSize: 13
752
754
  };
753
755
  function Inspector({ doc, selection, apply }) {
754
756
  const node = selection ? doc.nodes[selection] : null;
755
757
  if (!node) {
756
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 16, color: "#94a3b8", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
758
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 16, color: "var(--fcms-muted)", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
757
759
  }
758
760
  const s = node.style.base;
759
761
  const setStyle = (patch) => apply({ t: "set_style", id: node.id, breakpoint: "base", patch });
760
762
  const setProp = (key, value) => apply({ t: "set_props", id: node.id, patch: { [key]: value } });
761
763
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { overflow: "auto", padding: 16, fontFamily: "system-ui, sans-serif", height: "100%", boxSizing: "border-box" }, children: [
762
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a" }, children: node.type }),
763
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "#94a3b8", marginBottom: 8 }, children: node.id }),
764
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--fcms-fg)" }, children: node.type }),
765
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "var(--fcms-muted)", marginBottom: 8 }, children: node.id }),
764
766
  node.type === "text" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
765
767
  /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Content" }),
766
768
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -838,8 +840,82 @@ function Inspector({ doc, selection, apply }) {
838
840
  )
839
841
  ] });
840
842
  }
841
- function Editor({ defaultValue, onChange }) {
842
- const ed = useEditor(defaultValue);
843
+
844
+ // src/editor/insert.ts
845
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set([
846
+ "section",
847
+ "frame",
848
+ "stack",
849
+ "grid",
850
+ "shape",
851
+ "card",
852
+ "device",
853
+ "repeater"
854
+ ]);
855
+ var ADD_DEFAULTS = {
856
+ text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
857
+ heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
858
+ button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
859
+ link: { type: "link", props: { content: "link text", href: "#" }, style: { color: "#7c3aed" } },
860
+ image: { type: "image", props: { src: "", alt: "" }, style: {} },
861
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } },
862
+ stack: { type: "stack", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
863
+ grid: { type: "grid", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "grid" },
864
+ card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
865
+ callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
866
+ divider: { type: "divider", props: {}, style: {} },
867
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
868
+ richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
869
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
870
+ };
871
+ var ADD_MENU = [
872
+ { kind: "heading", label: "Heading" },
873
+ { kind: "text", label: "Text" },
874
+ { kind: "button", label: "Button" },
875
+ { kind: "link", label: "Link" },
876
+ { kind: "image", label: "Image" },
877
+ { kind: "card", label: "Card" },
878
+ { kind: "callout", label: "Callout" },
879
+ { kind: "stack", label: "Stack" },
880
+ { kind: "grid", label: "Grid" },
881
+ { kind: "box", label: "Box" },
882
+ { kind: "divider", label: "Divider" },
883
+ { kind: "code", label: "Code" },
884
+ { kind: "richtext", label: "Rich text" },
885
+ { kind: "repeater", label: "Repeater" }
886
+ ];
887
+ function resolveInsertParent(doc, selectedId) {
888
+ const selected = selectedId ? doc.nodes[selectedId] : null;
889
+ if (selected) {
890
+ return CONTAINER_TYPES.has(selected.type) ? selected.id : selected.parent;
891
+ }
892
+ return doc.sections[doc.sections.length - 1] ?? null;
893
+ }
894
+ function buildInsertOp(doc, kind, selectedId, targetId) {
895
+ const parent = targetId !== void 0 ? targetId && doc.nodes[targetId] ? CONTAINER_TYPES.has(doc.nodes[targetId].type) ? targetId : doc.nodes[targetId].parent : null : resolveInsertParent(doc, selectedId);
896
+ const siblings = childrenOf(doc, parent);
897
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
898
+ const id = `n${doc.seq + 1}-${Math.floor(performance.now())}`;
899
+ const def = ADD_DEFAULTS[kind];
900
+ return {
901
+ id,
902
+ op: {
903
+ t: "insert_node",
904
+ node: {
905
+ id,
906
+ type: def.type,
907
+ parent,
908
+ order,
909
+ layout: def.layout,
910
+ props: { ...def.props },
911
+ style: { base: { ...def.style } }
912
+ }
913
+ }
914
+ };
915
+ }
916
+ function Editor({ defaultValue, onChange, registry, data }) {
917
+ const initial = react.useMemo(() => defaultValue ?? emptyDoc("untitled"), [defaultValue]);
918
+ const ed = useEditor(initial);
843
919
  const first = react.useRef(true);
844
920
  const onChangeRef = useLatestRef(onChange);
845
921
  react.useEffect(() => {
@@ -854,14 +930,15 @@ function Editor({ defaultValue, onChange }) {
854
930
  gridTemplateColumns: "220px 1fr 300px",
855
931
  height: "100%",
856
932
  minHeight: 0,
857
- background: "#fff",
858
- color: "#0f172a",
859
- border: "1px solid #e2e8f0",
933
+ background: "var(--fcms-bg)",
934
+ color: "var(--fcms-fg)",
935
+ border: "1px solid var(--fcms-border)",
860
936
  borderRadius: 12,
861
937
  overflow: "hidden"
862
938
  };
863
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: shell, children: [
864
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
939
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fancy-cms-editor", style: shell, children: [
940
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: CHROME_CSS }),
941
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderRight: "1px solid var(--fcms-border)", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
865
942
  /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { ed }),
866
943
  /* @__PURE__ */ jsxRuntime.jsx(
867
944
  LayersPanel,
@@ -873,8 +950,8 @@ function Editor({ defaultValue, onChange }) {
873
950
  }
874
951
  )
875
952
  ] }),
876
- /* @__PURE__ */ jsxRuntime.jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
877
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { borderLeft: "1px solid #e2e8f0", minHeight: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply }) })
953
+ /* @__PURE__ */ jsxRuntime.jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply, registry, data }),
954
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { borderLeft: "1px solid var(--fcms-border)", minHeight: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply }) })
878
955
  ] });
879
956
  }
880
957
  function Toolbar({ ed }) {
@@ -883,8 +960,9 @@ function Toolbar({ ed }) {
883
960
  fontSize: 12,
884
961
  padding: "4px 10px",
885
962
  borderRadius: 6,
886
- border: "1px solid #e2e8f0",
887
- background: "#fff",
963
+ border: "1px solid var(--fcms-border)",
964
+ background: "var(--fcms-bg)",
965
+ color: "var(--fcms-fg)",
888
966
  cursor: "pointer"
889
967
  };
890
968
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -894,17 +972,98 @@ function Toolbar({ ed }) {
894
972
  display: "flex",
895
973
  gap: 6,
896
974
  padding: 8,
897
- borderBottom: "1px solid #e2e8f0",
975
+ borderBottom: "1px solid var(--fcms-border)",
898
976
  fontFamily: "system-ui, sans-serif"
899
977
  },
900
978
  children: [
901
979
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: ed.canUndo ? 1 : 0.4 }, disabled: !ed.canUndo, onClick: ed.undo, children: "Undo" }),
902
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" })
980
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" }),
981
+ /* @__PURE__ */ jsxRuntime.jsx(AddElementMenu, { ed, btn })
903
982
  ]
904
983
  }
905
984
  );
906
985
  }
986
+ function AddElementMenu({ ed, btn }) {
987
+ const [open, setOpen] = react.useState(false);
988
+ react.useEffect(() => {
989
+ if (!open) return;
990
+ const close = () => setOpen(false);
991
+ const onKey = (e) => e.key === "Escape" && setOpen(false);
992
+ window.addEventListener("click", close);
993
+ window.addEventListener("keydown", onKey);
994
+ return () => {
995
+ window.removeEventListener("click", close);
996
+ window.removeEventListener("keydown", onKey);
997
+ };
998
+ }, [open]);
999
+ const add = (kind) => {
1000
+ const { op, id } = buildInsertOp(ed.state.doc, kind, ed.state.selection);
1001
+ ed.apply(op);
1002
+ ed.select(id);
1003
+ setOpen(false);
1004
+ };
1005
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", marginLeft: "auto" }, onClick: (e) => e.stopPropagation(), children: [
1006
+ /* @__PURE__ */ jsxRuntime.jsx(
1007
+ "button",
1008
+ {
1009
+ type: "button",
1010
+ style: { ...btn, borderColor: "var(--fcms-accent, #7c3aed)" },
1011
+ "aria-haspopup": "menu",
1012
+ "aria-expanded": open,
1013
+ "data-cms-add-trigger": "",
1014
+ onClick: () => setOpen((v) => !v),
1015
+ children: "+ Add"
1016
+ }
1017
+ ),
1018
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1019
+ "div",
1020
+ {
1021
+ role: "menu",
1022
+ "data-cms-add-menu": "",
1023
+ style: {
1024
+ position: "absolute",
1025
+ top: "calc(100% + 4px)",
1026
+ left: 0,
1027
+ zIndex: 20,
1028
+ minWidth: 150,
1029
+ maxHeight: 280,
1030
+ overflowY: "auto",
1031
+ padding: 4,
1032
+ borderRadius: 8,
1033
+ border: "1px solid var(--fcms-border)",
1034
+ background: "var(--fcms-bg)",
1035
+ boxShadow: "0 12px 32px rgba(0,0,0,.28)"
1036
+ },
1037
+ children: ADD_MENU.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1038
+ "button",
1039
+ {
1040
+ type: "button",
1041
+ role: "menuitem",
1042
+ "data-cms-add": item.kind,
1043
+ style: {
1044
+ ...btn,
1045
+ display: "block",
1046
+ width: "100%",
1047
+ textAlign: "left",
1048
+ border: 0,
1049
+ background: "transparent"
1050
+ },
1051
+ onClick: () => add(item.kind),
1052
+ children: item.label
1053
+ },
1054
+ item.kind
1055
+ ))
1056
+ }
1057
+ )
1058
+ ] });
1059
+ }
1060
+ var DARK_VARS = "color-scheme:dark;--fcms-bg:#0b0f19;--fcms-fg:#e5e7eb;--fcms-muted:#94a3b8;--fcms-border:#27272a;--fcms-canvas:#0f141f;--fcms-input-bg:#111827;--fcms-row-fg:#cbd5e1;--fcms-sel-bg:color-mix(in oklab, #8b5cf6 24%, transparent);--fcms-sel-fg:#ddd6fe;";
1061
+ var LIGHT_VARS = "color-scheme:light;--fcms-bg:#ffffff;--fcms-fg:#0f172a;--fcms-muted:#64748b;--fcms-border:#e2e8f0;--fcms-canvas:#f8fafc;--fcms-input-bg:#ffffff;--fcms-row-fg:#334155;--fcms-sel-bg:#ede9fe;--fcms-sel-fg:#5b21b6;";
1062
+ var CHROME_CSS = `.fancy-cms-editor{--fcms-accent:#8b5cf6;${LIGHT_VARS}}@media (prefers-color-scheme:dark){.fancy-cms-editor{${DARK_VARS}}}:where(.light,[data-theme="light"]) .fancy-cms-editor{${LIGHT_VARS}}:where(.dark,[data-theme="dark"]) .fancy-cms-editor{${DARK_VARS}}`;
907
1063
 
1064
+ exports.ADD_DEFAULTS = ADD_DEFAULTS;
1065
+ exports.ADD_MENU = ADD_MENU;
1066
+ exports.CONTAINER_TYPES = CONTAINER_TYPES;
908
1067
  exports.Canvas = Canvas;
909
1068
  exports.CmsPage = CmsPage;
910
1069
  exports.CmsRegion = CmsRegion;
@@ -913,6 +1072,7 @@ exports.Editor = Editor;
913
1072
  exports.Inspector = Inspector;
914
1073
  exports.LayersPanel = LayersPanel;
915
1074
  exports.RenderNode = RenderNode;
1075
+ exports.buildInsertOp = buildInsertOp;
916
1076
  exports.childrenOf = childrenOf;
917
1077
  exports.defaultRegistry = defaultRegistry;
918
1078
  exports.descendantIds = descendantIds;
@@ -926,6 +1086,7 @@ exports.isBinding = isBinding;
926
1086
  exports.keyBetween = keyBetween;
927
1087
  exports.reduce = reduce;
928
1088
  exports.reduceAll = reduceAll;
1089
+ exports.resolveInsertParent = resolveInsertParent;
929
1090
  exports.resolveValue = resolveValue;
930
1091
  exports.useEditor = useEditor;
931
1092
  //# sourceMappingURL=index.cjs.map