@particle-academy/fancy-cms-ui 0.2.0 → 0.3.1

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
@@ -753,6 +753,8 @@ var input = {
753
753
  fontSize: 13
754
754
  };
755
755
  function Inspector({ doc, selection, apply }) {
756
+ const uid = react.useId();
757
+ const fieldId = (key) => `${uid}-${key}`;
756
758
  const node = selection ? doc.nodes[selection] : null;
757
759
  if (!node) {
758
760
  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." });
@@ -764,10 +766,12 @@ function Inspector({ doc, selection, apply }) {
764
766
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--fcms-fg)" }, children: node.type }),
765
767
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "var(--fcms-muted)", marginBottom: 8 }, children: node.id }),
766
768
  node.type === "text" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
767
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Content" }),
769
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("content"), children: "Content" }),
768
770
  /* @__PURE__ */ jsxRuntime.jsx(
769
771
  "textarea",
770
772
  {
773
+ id: fieldId("content"),
774
+ "data-cms-field": "content",
771
775
  style: { ...input, minHeight: 64, resize: "vertical" },
772
776
  value: typeof node.props.content === "string" ? node.props.content : "",
773
777
  onChange: (e) => setProp("content", e.target.value)
@@ -775,60 +779,72 @@ function Inspector({ doc, selection, apply }) {
775
779
  )
776
780
  ] }) : null,
777
781
  node.type === "image" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
778
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Source" }),
782
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("src"), children: "Source" }),
779
783
  /* @__PURE__ */ jsxRuntime.jsx(
780
784
  "input",
781
785
  {
786
+ id: fieldId("src"),
787
+ "data-cms-field": "src",
782
788
  style: input,
783
789
  value: typeof node.props.src === "string" ? node.props.src : "",
784
790
  onChange: (e) => setProp("src", e.target.value)
785
791
  }
786
792
  )
787
793
  ] }) : null,
788
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Background" }),
794
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("background"), children: "Background" }),
789
795
  /* @__PURE__ */ jsxRuntime.jsx(
790
796
  "input",
791
797
  {
798
+ id: fieldId("background"),
799
+ "data-cms-field": "background",
792
800
  style: input,
793
801
  placeholder: "#ffffff | gradient | url(\u2026)",
794
802
  value: s.background ?? "",
795
803
  onChange: (e) => setStyle({ background: e.target.value })
796
804
  }
797
805
  ),
798
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Text color" }),
806
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("color"), children: "Text color" }),
799
807
  /* @__PURE__ */ jsxRuntime.jsx(
800
808
  "input",
801
809
  {
810
+ id: fieldId("color"),
811
+ "data-cms-field": "color",
802
812
  style: input,
803
813
  placeholder: "#0f172a",
804
814
  value: s.color ?? "",
805
815
  onChange: (e) => setStyle({ color: e.target.value })
806
816
  }
807
817
  ),
808
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Font size (px)" }),
818
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("fontSize"), children: "Font size (px)" }),
809
819
  /* @__PURE__ */ jsxRuntime.jsx(
810
820
  "input",
811
821
  {
822
+ id: fieldId("fontSize"),
823
+ "data-cms-field": "fontSize",
812
824
  type: "number",
813
825
  style: input,
814
826
  value: s.fontSize?.value ?? "",
815
827
  onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
816
828
  }
817
829
  ),
818
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Padding (px)" }),
830
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("padding"), children: "Padding (px)" }),
819
831
  /* @__PURE__ */ jsxRuntime.jsx(
820
832
  "input",
821
833
  {
834
+ id: fieldId("padding"),
835
+ "data-cms-field": "padding",
822
836
  type: "number",
823
837
  style: input,
824
838
  value: s.padding?.value ?? "",
825
839
  onChange: (e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: "px" } })
826
840
  }
827
841
  ),
828
- /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, children: "Opacity" }),
842
+ /* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("opacity"), children: "Opacity" }),
829
843
  /* @__PURE__ */ jsxRuntime.jsx(
830
844
  "input",
831
845
  {
846
+ id: fieldId("opacity"),
847
+ "data-cms-field": "opacity",
832
848
  type: "number",
833
849
  step: "0.1",
834
850
  min: "0",
@@ -840,8 +856,82 @@ function Inspector({ doc, selection, apply }) {
840
856
  )
841
857
  ] });
842
858
  }
859
+
860
+ // src/editor/insert.ts
861
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set([
862
+ "section",
863
+ "frame",
864
+ "stack",
865
+ "grid",
866
+ "shape",
867
+ "card",
868
+ "device",
869
+ "repeater"
870
+ ]);
871
+ var ADD_DEFAULTS = {
872
+ text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
873
+ heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
874
+ button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
875
+ link: { type: "link", props: { content: "link text", href: "#" }, style: { color: "#7c3aed" } },
876
+ image: { type: "image", props: { src: "", alt: "" }, style: {} },
877
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } },
878
+ stack: { type: "stack", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
879
+ grid: { type: "grid", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "grid" },
880
+ card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
881
+ callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
882
+ divider: { type: "divider", props: {}, style: {} },
883
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
884
+ richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
885
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
886
+ };
887
+ var ADD_MENU = [
888
+ { kind: "heading", label: "Heading" },
889
+ { kind: "text", label: "Text" },
890
+ { kind: "button", label: "Button" },
891
+ { kind: "link", label: "Link" },
892
+ { kind: "image", label: "Image" },
893
+ { kind: "card", label: "Card" },
894
+ { kind: "callout", label: "Callout" },
895
+ { kind: "stack", label: "Stack" },
896
+ { kind: "grid", label: "Grid" },
897
+ { kind: "box", label: "Box" },
898
+ { kind: "divider", label: "Divider" },
899
+ { kind: "code", label: "Code" },
900
+ { kind: "richtext", label: "Rich text" },
901
+ { kind: "repeater", label: "Repeater" }
902
+ ];
903
+ function resolveInsertParent(doc, selectedId) {
904
+ const selected = selectedId ? doc.nodes[selectedId] : null;
905
+ if (selected) {
906
+ return CONTAINER_TYPES.has(selected.type) ? selected.id : selected.parent;
907
+ }
908
+ return doc.sections[doc.sections.length - 1] ?? null;
909
+ }
910
+ function buildInsertOp(doc, kind, selectedId, targetId) {
911
+ 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);
912
+ const siblings = childrenOf(doc, parent);
913
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
914
+ const id = `n${doc.seq + 1}-${Math.floor(performance.now())}`;
915
+ const def = ADD_DEFAULTS[kind];
916
+ return {
917
+ id,
918
+ op: {
919
+ t: "insert_node",
920
+ node: {
921
+ id,
922
+ type: def.type,
923
+ parent,
924
+ order,
925
+ layout: def.layout,
926
+ props: { ...def.props },
927
+ style: { base: { ...def.style } }
928
+ }
929
+ }
930
+ };
931
+ }
843
932
  function Editor({ defaultValue, onChange, registry, data }) {
844
- const ed = useEditor(defaultValue);
933
+ const initial = react.useMemo(() => defaultValue ?? emptyDoc("untitled"), [defaultValue]);
934
+ const ed = useEditor(initial);
845
935
  const first = react.useRef(true);
846
936
  const onChangeRef = useLatestRef(onChange);
847
937
  react.useEffect(() => {
@@ -903,15 +993,93 @@ function Toolbar({ ed }) {
903
993
  },
904
994
  children: [
905
995
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: ed.canUndo ? 1 : 0.4 }, disabled: !ed.canUndo, onClick: ed.undo, children: "Undo" }),
906
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" })
996
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" }),
997
+ /* @__PURE__ */ jsxRuntime.jsx(AddElementMenu, { ed, btn })
907
998
  ]
908
999
  }
909
1000
  );
910
1001
  }
911
- 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;";
912
- 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;";
1002
+ function AddElementMenu({ ed, btn }) {
1003
+ const [open, setOpen] = react.useState(false);
1004
+ react.useEffect(() => {
1005
+ if (!open) return;
1006
+ const close = () => setOpen(false);
1007
+ const onKey = (e) => e.key === "Escape" && setOpen(false);
1008
+ window.addEventListener("click", close);
1009
+ window.addEventListener("keydown", onKey);
1010
+ return () => {
1011
+ window.removeEventListener("click", close);
1012
+ window.removeEventListener("keydown", onKey);
1013
+ };
1014
+ }, [open]);
1015
+ const add = (kind) => {
1016
+ const { op, id } = buildInsertOp(ed.state.doc, kind, ed.state.selection);
1017
+ ed.apply(op);
1018
+ ed.select(id);
1019
+ setOpen(false);
1020
+ };
1021
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", marginLeft: "auto" }, onClick: (e) => e.stopPropagation(), children: [
1022
+ /* @__PURE__ */ jsxRuntime.jsx(
1023
+ "button",
1024
+ {
1025
+ type: "button",
1026
+ style: { ...btn, borderColor: "var(--fcms-accent, #7c3aed)" },
1027
+ "aria-haspopup": "menu",
1028
+ "aria-expanded": open,
1029
+ "data-cms-add-trigger": "",
1030
+ onClick: () => setOpen((v) => !v),
1031
+ children: "+ Add"
1032
+ }
1033
+ ),
1034
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1035
+ "div",
1036
+ {
1037
+ role: "menu",
1038
+ "data-cms-add-menu": "",
1039
+ style: {
1040
+ position: "absolute",
1041
+ top: "calc(100% + 4px)",
1042
+ left: 0,
1043
+ zIndex: 20,
1044
+ minWidth: 150,
1045
+ maxHeight: 280,
1046
+ overflowY: "auto",
1047
+ padding: 4,
1048
+ borderRadius: 8,
1049
+ border: "1px solid var(--fcms-border)",
1050
+ background: "var(--fcms-bg)",
1051
+ boxShadow: "0 12px 32px rgba(0,0,0,.28)"
1052
+ },
1053
+ children: ADD_MENU.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
1054
+ "button",
1055
+ {
1056
+ type: "button",
1057
+ role: "menuitem",
1058
+ "data-cms-add": item.kind,
1059
+ style: {
1060
+ ...btn,
1061
+ display: "block",
1062
+ width: "100%",
1063
+ textAlign: "left",
1064
+ border: 0,
1065
+ background: "transparent"
1066
+ },
1067
+ onClick: () => add(item.kind),
1068
+ children: item.label
1069
+ },
1070
+ item.kind
1071
+ ))
1072
+ }
1073
+ )
1074
+ ] });
1075
+ }
1076
+ 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;--fcms-drop:#ec4899;--fcms-danger:#fca5a5;";
1077
+ 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;--fcms-drop:#db2777;--fcms-danger:#dc2626;";
913
1078
  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}}`;
914
1079
 
1080
+ exports.ADD_DEFAULTS = ADD_DEFAULTS;
1081
+ exports.ADD_MENU = ADD_MENU;
1082
+ exports.CONTAINER_TYPES = CONTAINER_TYPES;
915
1083
  exports.Canvas = Canvas;
916
1084
  exports.CmsPage = CmsPage;
917
1085
  exports.CmsRegion = CmsRegion;
@@ -920,6 +1088,7 @@ exports.Editor = Editor;
920
1088
  exports.Inspector = Inspector;
921
1089
  exports.LayersPanel = LayersPanel;
922
1090
  exports.RenderNode = RenderNode;
1091
+ exports.buildInsertOp = buildInsertOp;
923
1092
  exports.childrenOf = childrenOf;
924
1093
  exports.defaultRegistry = defaultRegistry;
925
1094
  exports.descendantIds = descendantIds;
@@ -933,6 +1102,7 @@ exports.isBinding = isBinding;
933
1102
  exports.keyBetween = keyBetween;
934
1103
  exports.reduce = reduce;
935
1104
  exports.reduceAll = reduceAll;
1105
+ exports.resolveInsertParent = resolveInsertParent;
936
1106
  exports.resolveValue = resolveValue;
937
1107
  exports.useEditor = useEditor;
938
1108
  //# sourceMappingURL=index.cjs.map