@particle-academy/fancy-cms-ui 0.0.1-dev.13 → 0.0.1-dev.15

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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
2
  import { P as PageDoc, i as NodeId, N as Node, m as StyleProps, L as LayoutMode } from './types-CpfPEq3V.cjs';
3
- import { a as ElementRegistry } from './registry-Bk7GN1rA.cjs';
3
+ import { a as ElementRegistry, D as DataContext } from './registry-ni9y0dSw.cjs';
4
4
  import { P as PageOp } from './ops-CdBZarAT.cjs';
5
5
 
6
6
  interface EditorProps {
@@ -28,6 +28,8 @@ interface NodeTransform {
28
28
  interface EditablePageProps {
29
29
  doc: PageDoc;
30
30
  registry?: ElementRegistry;
31
+ /** Data context for `{ $bind }` props + repeaters (server props, etc.). */
32
+ data?: DataContext;
31
33
  /** Slot for the fancy-motion timeline dock (rendered at the bottom in EditMode). */
32
34
  timelineDock?: ReactNode;
33
35
  /** Per-node transforms applied to the page (e.g. the sampled keyframe snapshot). */
@@ -54,7 +56,7 @@ interface EditablePageProps {
54
56
  * toolbar styles it, and the selection can be **dragged to move** / **resized**
55
57
  * — committed as transforms the host captures into a timeline keyframe.
56
58
  */
57
- declare function EditablePage({ doc, registry, timelineDock, transforms, onNodeTransform, onSelect, frames, onProgress, pinned, }: EditablePageProps): ReactElement;
59
+ declare function EditablePage({ doc, registry, data, timelineDock, transforms, onNodeTransform, onSelect, frames, onProgress, pinned, }: EditablePageProps): ReactElement;
58
60
 
59
61
  interface CanvasProps {
60
62
  doc: PageDoc;
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
2
  import { P as PageDoc, i as NodeId, N as Node, m as StyleProps, L as LayoutMode } from './types-CpfPEq3V.js';
3
- import { a as ElementRegistry } from './registry-Dht9JBLU.js';
3
+ import { a as ElementRegistry, D as DataContext } from './registry-C3PioMnL.js';
4
4
  import { P as PageOp } from './ops-BsM6kZxY.js';
5
5
 
6
6
  interface EditorProps {
@@ -28,6 +28,8 @@ interface NodeTransform {
28
28
  interface EditablePageProps {
29
29
  doc: PageDoc;
30
30
  registry?: ElementRegistry;
31
+ /** Data context for `{ $bind }` props + repeaters (server props, etc.). */
32
+ data?: DataContext;
31
33
  /** Slot for the fancy-motion timeline dock (rendered at the bottom in EditMode). */
32
34
  timelineDock?: ReactNode;
33
35
  /** Per-node transforms applied to the page (e.g. the sampled keyframe snapshot). */
@@ -54,7 +56,7 @@ interface EditablePageProps {
54
56
  * toolbar styles it, and the selection can be **dragged to move** / **resized**
55
57
  * — committed as transforms the host captures into a timeline keyframe.
56
58
  */
57
- declare function EditablePage({ doc, registry, timelineDock, transforms, onNodeTransform, onSelect, frames, onProgress, pinned, }: EditablePageProps): ReactElement;
59
+ declare function EditablePage({ doc, registry, data, timelineDock, transforms, onNodeTransform, onSelect, frames, onProgress, pinned, }: EditablePageProps): ReactElement;
58
60
 
59
61
  interface CanvasProps {
60
62
  doc: PageDoc;
package/dist/editor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { keyBetween } from './chunk-7YYLYBLI.js';
2
- import { CmsPage, defaultRegistry } from './chunk-E5AAQX7P.js';
3
- import { reduce, childrenOf } from './chunk-3YZMELJJ.js';
2
+ import { CmsPage, defaultRegistry } from './chunk-IXPYY4S2.js';
3
+ import { reduce, childrenOf, isBinding } from './chunk-3YZMELJJ.js';
4
4
  import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useEffect } from 'react';
5
5
  import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
6
6
  import { ContextMenu } from '@particle-academy/react-fancy';
@@ -414,7 +414,7 @@ function NodeInspector({
414
414
  const setT = (patch) => onTransform({ ...transform, ...patch });
415
415
  const handled = /* @__PURE__ */ new Set(["content", "label"]);
416
416
  const extraKeys = Object.keys(node.props).filter(
417
- (k) => !handled.has(k) && ["string", "number", "boolean"].includes(typeof node.props[k])
417
+ (k) => !handled.has(k) && (["string", "number", "boolean"].includes(typeof node.props[k]) || isBinding(node.props[k]))
418
418
  );
419
419
  return /* @__PURE__ */ jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
420
420
  /* @__PURE__ */ jsxs("div", { style: headRow, children: [
@@ -539,24 +539,50 @@ function Row({ children }) {
539
539
  return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8 }, children });
540
540
  }
541
541
  function PropField({ name, value, onChange }) {
542
+ const bound = isBinding(value);
543
+ const link = /* @__PURE__ */ jsx(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ title: bound ? "Unbind (use a literal value)" : "Bind to data (e.g. item.name)",
548
+ onClick: () => onChange(bound ? "" : { $bind: "" }),
549
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontSize: 11, color: bound ? "#a78bfa" : "#64748b" },
550
+ children: "\u{1F517}"
551
+ }
552
+ );
553
+ if (bound) {
554
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
555
+ /* @__PURE__ */ jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
556
+ name,
557
+ " ",
558
+ link
559
+ ] }),
560
+ /* @__PURE__ */ jsx("input", { style: { ...input2, borderColor: "#7c3aed" }, placeholder: "data path \xB7 e.g. item.name", value: value.$bind, onChange: (e) => onChange({ $bind: e.target.value }) })
561
+ ] });
562
+ }
542
563
  if (typeof value === "boolean") {
543
564
  return /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
544
565
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
545
- /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize" }, children: name })
566
+ /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize", flex: 1 }, children: name }),
567
+ link
546
568
  ] });
547
569
  }
548
570
  if (name === "variant") {
549
- return /* @__PURE__ */ jsx(Sel, { label: name, value: String(value), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
571
+ return /* @__PURE__ */ jsx(Sel, { label: name, value: String(value ?? ""), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
550
572
  }
551
573
  const isNum = typeof value === "number";
552
574
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
553
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: name }),
575
+ /* @__PURE__ */ jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
576
+ name,
577
+ " ",
578
+ link
579
+ ] }),
554
580
  /* @__PURE__ */ jsx(
555
581
  "input",
556
582
  {
557
583
  style: input2,
558
584
  type: isNum ? "number" : "text",
559
- value: String(value),
585
+ value: String(value ?? ""),
560
586
  onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
561
587
  }
562
588
  )
@@ -744,6 +770,7 @@ var SNAP = 6;
744
770
  function EditablePage({
745
771
  doc,
746
772
  registry = defaultRegistry,
773
+ data,
747
774
  timelineDock,
748
775
  transforms,
749
776
  onNodeTransform,
@@ -851,7 +878,7 @@ function EditablePage({
851
878
  },
852
879
  [ed, selNode]
853
880
  );
854
- const addNodeAt = useCallback(
881
+ useCallback(
855
882
  (kind, targetId) => {
856
883
  const doc2 = ed.state.doc;
857
884
  const target = targetId ? doc2.nodes[targetId] : null;
@@ -880,12 +907,48 @@ function EditablePage({
880
907
  window.addEventListener("mousemove", onMove);
881
908
  return () => window.removeEventListener("mousemove", onMove);
882
909
  }, [editing, draggingEl]);
910
+ const [dropHint, setDropHint] = useState(null);
911
+ const computeDropTarget = (clientX, clientY) => {
912
+ const el = document.elementFromPoint(clientX, clientY)?.closest("[data-cms]");
913
+ if (!el) return { id: null, edge: "inside", rect: null };
914
+ const id = el.getAttribute("data-cms");
915
+ const node = ed.state.doc.nodes[id];
916
+ const r = el.getBoundingClientRect();
917
+ const rect = { x: r.left, y: r.top, w: r.width, h: r.height };
918
+ if (node && CONTAINER_TYPES2.has(node.type)) return { id, edge: "inside", rect };
919
+ return { id, edge: clientY < r.top + r.height / 2 ? "before" : "after", rect };
920
+ };
921
+ const onPageDragOver = (e) => {
922
+ e.preventDefault();
923
+ e.dataTransfer.dropEffect = "copy";
924
+ setDropHint(computeDropTarget(e.clientX, e.clientY));
925
+ };
926
+ const insertRelative = (kind, hint) => {
927
+ const doc2 = ed.state.doc;
928
+ let parent;
929
+ let order;
930
+ if (!hint.id || hint.edge === "inside") {
931
+ parent = hint.id ?? (doc2.sections[doc2.sections.length - 1] ?? null);
932
+ const sib = childrenOf(doc2, parent);
933
+ order = keyBetween(sib.length ? sib[sib.length - 1].order : null, null);
934
+ } else {
935
+ const target = doc2.nodes[hint.id];
936
+ parent = target.parent;
937
+ const sib = childrenOf(doc2, parent);
938
+ const idx = sib.findIndex((n) => n.id === hint.id);
939
+ order = hint.edge === "before" ? keyBetween(idx > 0 ? sib[idx - 1].order : null, target.order) : keyBetween(target.order, idx < sib.length - 1 ? sib[idx + 1].order : null);
940
+ }
941
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
942
+ const def = ADD_DEFAULTS[kind];
943
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
944
+ ed.select(id);
945
+ };
883
946
  const onPageDrop = (e) => {
884
947
  const kind = e.dataTransfer.getData("application/x-cms-element");
885
948
  if (!kind) return;
886
949
  e.preventDefault();
887
- const el = document.elementFromPoint(e.clientX, e.clientY)?.closest("[data-cms]");
888
- addNodeAt(kind, el?.getAttribute("data-cms") ?? null);
950
+ insertRelative(kind, computeDropTarget(e.clientX, e.clientY));
951
+ setDropHint(null);
889
952
  setDraggingEl(false);
890
953
  };
891
954
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
@@ -961,9 +1024,10 @@ function EditablePage({
961
1024
  ref: pageRef,
962
1025
  onClickCapture,
963
1026
  onContextMenu: onContextMenuSelect,
964
- onDragOver: editing ? (e) => e.preventDefault() : void 0,
1027
+ onDragOver: editing ? onPageDragOver : void 0,
1028
+ onDragLeave: editing ? () => setDropHint(null) : void 0,
965
1029
  onDrop: editing ? onPageDrop : void 0,
966
- children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry })
1030
+ children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry, data })
967
1031
  }
968
1032
  );
969
1033
  const pageShell = pinned ? /* @__PURE__ */ jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : frames > 1 ? (
@@ -1032,10 +1096,23 @@ function EditablePage({
1032
1096
  onAdd: addNode
1033
1097
  }
1034
1098
  ) : null,
1099
+ editing && dropHint?.rect ? /* @__PURE__ */ jsx(DropIndicator, { hint: dropHint }) : null,
1035
1100
  editing ? /* @__PURE__ */ jsx(ElementPalette, { open: paletteOpen, onDragChange: setDraggingEl }) : null,
1036
1101
  editing && timelineDock ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
1037
1102
  ] });
1038
1103
  }
1104
+ function DropIndicator({
1105
+ hint
1106
+ }) {
1107
+ const r = hint.rect;
1108
+ if (!r) return null;
1109
+ const color = "#ec4899";
1110
+ if (hint.edge === "inside") {
1111
+ return /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: r.x, top: r.y, width: r.w, height: r.h, outline: `2px dashed ${color}`, outlineOffset: -2, borderRadius: 6, pointerEvents: "none", zIndex: Z2 + 4 } });
1112
+ }
1113
+ const y = hint.edge === "before" ? r.y : r.y + r.h;
1114
+ return /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: r.x, top: y - 1.5, width: r.w, height: 3, background: color, borderRadius: 2, boxShadow: "0 0 0 1px #fff", pointerEvents: "none", zIndex: Z2 + 4 } });
1115
+ }
1039
1116
  function ElementPalette({ open, onDragChange }) {
1040
1117
  return /* @__PURE__ */ jsxs(
1041
1118
  "div",
@@ -1098,7 +1175,7 @@ function ElementPalette({ open, onDragChange }) {
1098
1175
  }
1099
1176
  );
1100
1177
  }
1101
- var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device"]);
1178
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device", "repeater"]);
1102
1179
  var ADD_DEFAULTS = {
1103
1180
  text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
1104
1181
  heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
@@ -1111,7 +1188,8 @@ var ADD_DEFAULTS = {
1111
1188
  card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
1112
1189
  callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
1113
1190
  divider: { type: "divider", props: {}, style: {} },
1114
- code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} }
1191
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
1192
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
1115
1193
  };
1116
1194
  var GRIPS = [
1117
1195
  { key: "nw", north: true, west: true, cursor: "nwse-resize" },
@@ -1322,7 +1400,8 @@ var ADD_MENU = [
1322
1400
  { kind: "grid", label: "Grid" },
1323
1401
  { kind: "box", label: "Box" },
1324
1402
  { kind: "divider", label: "Divider" },
1325
- { kind: "code", label: "Code" }
1403
+ { kind: "code", label: "Code" },
1404
+ { kind: "repeater", label: "Repeater" }
1326
1405
  ];
1327
1406
  function EditBar({
1328
1407
  editing,