@particle-academy/fancy-cms-ui 0.0.1-dev.9 → 0.1.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/editor.cjs CHANGED
@@ -361,10 +361,13 @@ ${inner}
361
361
  }
362
362
  return blocks.join("\n\n") + (blocks.length ? "\n" : "");
363
363
  }
364
- function literal(v) {
365
- if (v === void 0 || v === null) return "";
366
- if (isBinding(v)) return "";
367
- return String(v);
364
+ function getPath(data, path) {
365
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], data);
366
+ }
367
+ function resolveValue(v, data) {
368
+ if (v === void 0 || v === null) return v;
369
+ if (isBinding(v)) return getPath(data, v.$bind);
370
+ return v;
368
371
  }
369
372
  var defaultRegistry = {
370
373
  section: ({ children }) => children,
@@ -372,18 +375,29 @@ var defaultRegistry = {
372
375
  grid: ({ children }) => children,
373
376
  frame: ({ children }) => children,
374
377
  shape: ({ children }) => children,
375
- text: ({ node }) => literal(node.props.content),
376
- heading: ({ node }) => literal(node.props.content) || "Heading",
377
- button: ({ node }) => {
378
- const label2 = literal(node.props.label) || literal(node.props.content) || "Button";
379
- const variant = literal(node.props.variant) || "primary";
378
+ card: ({ children }) => children,
379
+ text: ({ node, text }) => text(node.props.content),
380
+ heading: ({ node, text }) => text(node.props.content) || "Heading",
381
+ // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
382
+ // plain `text` can't express. The author owns the markup (it's their content).
383
+ richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
384
+ link: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
385
+ divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
386
+ callout: ({ node, text }) => {
387
+ const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
388
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 10, padding: "12px 14px", borderRadius: 10, background: `${tone}14`, borderLeft: `3px solid ${tone}` }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 13 }, children: text(node.props.content) || "Callout" }) });
389
+ },
390
+ code: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { style: { margin: 0, padding: "12px 14px", borderRadius: 10, background: "#0b1220", color: "#e2e8f0", fontFamily: "ui-monospace, monospace", fontSize: 13, overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: text(node.props.content) }) }),
391
+ button: ({ node, text }) => {
392
+ const label2 = text(node.props.label) || text(node.props.content) || "Button";
393
+ const variant = text(node.props.variant) || "primary";
380
394
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
381
395
  },
382
- image: ({ node }) => /* @__PURE__ */ jsxRuntime.jsx(
396
+ image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
383
397
  "img",
384
398
  {
385
- src: literal(node.props.src),
386
- alt: literal(node.props.alt),
399
+ src: text(node.props.src),
400
+ alt: text(node.props.alt),
387
401
  style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
388
402
  }
389
403
  )
@@ -405,30 +419,34 @@ function buttonStyle(variant) {
405
419
  if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
406
420
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
407
421
  }
408
- function RenderNode({ doc, id, registry = defaultRegistry }) {
422
+ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
409
423
  const node = doc.nodes[id];
410
424
  if (!node) return null;
411
- const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: child.id, registry }, child.id));
425
+ if (node.type === "repeater") {
426
+ const arr = resolveValue(node.props.items, data);
427
+ const items = Array.isArray(arr) ? arr : [];
428
+ const template = childrenOf(doc, id)[0];
429
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, className: node.className, children: template ? items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: template.id, registry, data: { ...data, item, index } }, index)) : null });
430
+ }
431
+ const kids = childrenOf(doc, id).map((child) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id: child.id, registry, data }, child.id));
432
+ const resolve = (v) => resolveValue(v, data);
433
+ const text = (v) => {
434
+ const r = resolve(v);
435
+ return r === void 0 || r === null ? "" : String(r);
436
+ };
412
437
  const renderer = registry[node.type];
413
- const inner = renderer ? renderer({ node, doc, children: kids }) : node.island ? null : kids;
414
- return /* @__PURE__ */ jsxRuntime.jsx(
415
- "div",
416
- {
417
- "data-cms": node.id,
418
- "data-cms-island": node.island ? node.type : void 0,
419
- className: node.className,
420
- children: inner
421
- }
422
- );
438
+ const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
439
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
423
440
  }
424
441
  function CmsPage({
425
442
  doc,
426
443
  registry = defaultRegistry,
444
+ data,
427
445
  includeStyles = true
428
446
  }) {
429
447
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
430
448
  includeStyles ? /* @__PURE__ */ jsxRuntime.jsx("style", { "data-cms-styles": "", children: emitDocCss(doc) }) : null,
431
- doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id, registry }, id))
449
+ doc.sections.map((id) => /* @__PURE__ */ jsxRuntime.jsx(RenderNode, { doc, id, registry, data }, id))
432
450
  ] });
433
451
  }
434
452
  function Canvas({ doc, selection, onSelect, apply }) {
@@ -802,7 +820,7 @@ function Toolbar({ ed }) {
802
820
  }
803
821
  );
804
822
  }
805
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
823
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device"]);
806
824
  function NodeInspector({
807
825
  node,
808
826
  transform,
@@ -816,9 +834,9 @@ function NodeInspector({
816
834
  }) {
817
835
  const base = node.style.base ?? {};
818
836
  const setT = (patch) => onTransform({ ...transform, ...patch });
819
- const handled = /* @__PURE__ */ new Set(["content", "label"]);
837
+ const handled = /* @__PURE__ */ new Set(["content", "label", "html"]);
820
838
  const extraKeys = Object.keys(node.props).filter(
821
- (k) => !handled.has(k) && ["string", "number", "boolean"].includes(typeof node.props[k])
839
+ (k) => !handled.has(k) && (["string", "number", "boolean"].includes(typeof node.props[k]) || isBinding(node.props[k]))
822
840
  );
823
841
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
824
842
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headRow, children: [
@@ -831,14 +849,11 @@ function NodeInspector({
831
849
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: iconBtn, title: "Close", onClick: onClose, children: "\u2715" })
832
850
  ] }),
833
851
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: body, children: [
834
- "content" in node.props ? /* @__PURE__ */ jsxRuntime.jsx(Group, { label: "Content", children: /* @__PURE__ */ jsxRuntime.jsx(
835
- "textarea",
836
- {
837
- style: { ...input2, minHeight: 64, resize: "vertical", lineHeight: 1.4 },
838
- value: String(node.props.content ?? ""),
839
- onChange: (e) => onProps({ content: e.target.value })
840
- }
841
- ) }) : null,
852
+ "content" in node.props || node.type === "richtext" ? /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: 11, color: "var(--fg-3, #94a3b8)", margin: "0 0 12px", lineHeight: 1.45, paddingBottom: 12, borderBottom: "1px solid #1e293b" }, children: [
853
+ "\u270E Edit the text ",
854
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "directly on the page" }),
855
+ " \u2014 click in and type. This panel is for layout, style, and bindings."
856
+ ] }) : null,
842
857
  "label" in node.props ? /* @__PURE__ */ jsxRuntime.jsx(Group, { label: "Label", children: /* @__PURE__ */ jsxRuntime.jsx("input", { style: input2, value: String(node.props.label ?? ""), onChange: (e) => onProps({ label: e.target.value }) }) }) : null,
843
858
  extraKeys.length ? /* @__PURE__ */ jsxRuntime.jsx(Group, { label: "Properties", children: extraKeys.map((k) => /* @__PURE__ */ jsxRuntime.jsx(PropField, { name: k, value: node.props[k], onChange: (v) => onProps({ [k]: v }) }, k)) }) : null,
844
859
  /* @__PURE__ */ jsxRuntime.jsxs(Group, { label: "Position & size", children: [
@@ -943,24 +958,50 @@ function Row({ children }) {
943
958
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 8 }, children });
944
959
  }
945
960
  function PropField({ name, value, onChange }) {
961
+ const bound = isBinding(value);
962
+ const link = /* @__PURE__ */ jsxRuntime.jsx(
963
+ "button",
964
+ {
965
+ type: "button",
966
+ title: bound ? "Unbind (use a literal value)" : "Bind to data (e.g. item.name)",
967
+ onClick: () => onChange(bound ? "" : { $bind: "" }),
968
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontSize: 11, color: bound ? "#a78bfa" : "#64748b" },
969
+ children: "\u{1F517}"
970
+ }
971
+ );
972
+ if (bound) {
973
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
974
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
975
+ name,
976
+ " ",
977
+ link
978
+ ] }),
979
+ /* @__PURE__ */ jsxRuntime.jsx("input", { style: { ...input2, borderColor: "#7c3aed" }, placeholder: "data path \xB7 e.g. item.name", value: value.$bind, onChange: (e) => onChange({ $bind: e.target.value }) })
980
+ ] });
981
+ }
946
982
  if (typeof value === "boolean") {
947
983
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
948
984
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
949
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textTransform: "capitalize" }, children: name })
985
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { textTransform: "capitalize", flex: 1 }, children: name }),
986
+ link
950
987
  ] });
951
988
  }
952
989
  if (name === "variant") {
953
- return /* @__PURE__ */ jsxRuntime.jsx(Sel, { label: name, value: String(value), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
990
+ return /* @__PURE__ */ jsxRuntime.jsx(Sel, { label: name, value: String(value ?? ""), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
954
991
  }
955
992
  const isNum = typeof value === "number";
956
993
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { style: field, children: [
957
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: fieldLabel, children: name }),
994
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
995
+ name,
996
+ " ",
997
+ link
998
+ ] }),
958
999
  /* @__PURE__ */ jsxRuntime.jsx(
959
1000
  "input",
960
1001
  {
961
1002
  style: input2,
962
1003
  type: isNum ? "number" : "text",
963
- value: String(value),
1004
+ value: String(value ?? ""),
964
1005
  onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
965
1006
  }
966
1007
  )
@@ -1144,9 +1185,11 @@ function snapshotSubtree(doc, id) {
1144
1185
  return { rootId: id, nodes };
1145
1186
  }
1146
1187
  var Z2 = 2147483e3;
1188
+ var SNAP = 6;
1147
1189
  function EditablePage({
1148
1190
  doc,
1149
1191
  registry = defaultRegistry,
1192
+ data,
1150
1193
  timelineDock,
1151
1194
  transforms,
1152
1195
  onNodeTransform,
@@ -1218,13 +1261,17 @@ function EditablePage({
1218
1261
  };
1219
1262
  }, [measureBox, ed.state.doc]);
1220
1263
  react.useEffect(() => {
1221
- if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
1222
- const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
1223
- if (!el) return;
1264
+ const t = selNode?.type;
1265
+ const editable = t === "text" || t === "heading" || t === "richtext";
1266
+ if (!editing || !selection || !pageRef.current || !editable) return;
1267
+ const wrapper = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
1268
+ if (!wrapper) return;
1269
+ const isRich = t === "richtext";
1270
+ const el = (isRich ? wrapper.firstElementChild : wrapper) ?? wrapper;
1224
1271
  el.setAttribute("contenteditable", "true");
1225
1272
  el.style.outline = "none";
1226
1273
  el.focus();
1227
- const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
1274
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: isRich ? { html: el.innerHTML } : { content: el.innerText } });
1228
1275
  el.addEventListener("blur", commit);
1229
1276
  return () => {
1230
1277
  el.removeEventListener("blur", commit);
@@ -1249,11 +1296,84 @@ function EditablePage({
1249
1296
  const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
1250
1297
  const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1251
1298
  const def = ADD_DEFAULTS[kind];
1252
- ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
1299
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1253
1300
  ed.select(id);
1254
1301
  },
1255
1302
  [ed, selNode]
1256
1303
  );
1304
+ react.useCallback(
1305
+ (kind, targetId) => {
1306
+ const doc2 = ed.state.doc;
1307
+ const target = targetId ? doc2.nodes[targetId] : null;
1308
+ const parent = target ? CONTAINER_TYPES2.has(target.type) ? target.id : target.parent : doc2.sections[doc2.sections.length - 1] ?? null;
1309
+ const siblings = childrenOf(doc2, parent);
1310
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
1311
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1312
+ const def = ADD_DEFAULTS[kind];
1313
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1314
+ ed.select(id);
1315
+ },
1316
+ [ed]
1317
+ );
1318
+ const [paletteOpen, setPaletteOpen] = react.useState(false);
1319
+ const [draggingEl, setDraggingEl] = react.useState(false);
1320
+ react.useEffect(() => {
1321
+ if (!editing) {
1322
+ setPaletteOpen(false);
1323
+ return;
1324
+ }
1325
+ const onMove = (e) => {
1326
+ if (draggingEl) return;
1327
+ if (e.clientX >= window.innerWidth - 26) setPaletteOpen(true);
1328
+ else if (e.clientX < window.innerWidth - 236) setPaletteOpen(false);
1329
+ };
1330
+ window.addEventListener("mousemove", onMove);
1331
+ return () => window.removeEventListener("mousemove", onMove);
1332
+ }, [editing, draggingEl]);
1333
+ const [dropHint, setDropHint] = react.useState(null);
1334
+ const computeDropTarget = (clientX, clientY) => {
1335
+ const el = document.elementFromPoint(clientX, clientY)?.closest("[data-cms]");
1336
+ if (!el) return { id: null, edge: "inside", rect: null };
1337
+ const id = el.getAttribute("data-cms");
1338
+ const node = ed.state.doc.nodes[id];
1339
+ const r = el.getBoundingClientRect();
1340
+ const rect = { x: r.left, y: r.top, w: r.width, h: r.height };
1341
+ if (node && CONTAINER_TYPES2.has(node.type)) return { id, edge: "inside", rect };
1342
+ return { id, edge: clientY < r.top + r.height / 2 ? "before" : "after", rect };
1343
+ };
1344
+ const onPageDragOver = (e) => {
1345
+ e.preventDefault();
1346
+ e.dataTransfer.dropEffect = "copy";
1347
+ setDropHint(computeDropTarget(e.clientX, e.clientY));
1348
+ };
1349
+ const insertRelative = (kind, hint) => {
1350
+ const doc2 = ed.state.doc;
1351
+ let parent;
1352
+ let order;
1353
+ if (!hint.id || hint.edge === "inside") {
1354
+ parent = hint.id ?? (doc2.sections[doc2.sections.length - 1] ?? null);
1355
+ const sib = childrenOf(doc2, parent);
1356
+ order = keyBetween(sib.length ? sib[sib.length - 1].order : null, null);
1357
+ } else {
1358
+ const target = doc2.nodes[hint.id];
1359
+ parent = target.parent;
1360
+ const sib = childrenOf(doc2, parent);
1361
+ const idx = sib.findIndex((n) => n.id === hint.id);
1362
+ 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);
1363
+ }
1364
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1365
+ const def = ADD_DEFAULTS[kind];
1366
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
1367
+ ed.select(id);
1368
+ };
1369
+ const onPageDrop = (e) => {
1370
+ const kind = e.dataTransfer.getData("application/x-cms-element");
1371
+ if (!kind) return;
1372
+ e.preventDefault();
1373
+ insertRelative(kind, computeDropTarget(e.clientX, e.clientY));
1374
+ setDropHint(null);
1375
+ setDraggingEl(false);
1376
+ };
1257
1377
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
1258
1378
  const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
1259
1379
  const setLayout = (layout) => selection && ed.apply({ t: "set_layout", id: selection, layout });
@@ -1321,8 +1441,23 @@ function EditablePage({
1321
1441
  if (el) ed.select(el.getAttribute("data-cms"));
1322
1442
  else ed.select(null);
1323
1443
  };
1324
- const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
1325
- const pageShell = pinned ? /* @__PURE__ */ jsxRuntime.jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : page;
1444
+ const page = /* @__PURE__ */ jsxRuntime.jsx(
1445
+ "div",
1446
+ {
1447
+ ref: pageRef,
1448
+ onClickCapture,
1449
+ onContextMenu: onContextMenuSelect,
1450
+ onDragOver: editing ? onPageDragOver : void 0,
1451
+ onDragLeave: editing ? () => setDropHint(null) : void 0,
1452
+ onDrop: editing ? onPageDrop : void 0,
1453
+ children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry, data })
1454
+ }
1455
+ );
1456
+ const pageShell = pinned ? /* @__PURE__ */ jsxRuntime.jsx("div", { ref: spacerRef, style: { height: `${Math.max(1, frames) * 100}vh` }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "sticky", top: 0, height: "100vh", overflow: "hidden" }, children: page }) }) : frames > 1 ? (
1457
+ // Normal flow, but "expanded" — extra scroll length below the content so a
1458
+ // page can be made longer than its content (the + Frame control).
1459
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { minHeight: `${frames * 100}vh` }, children: page })
1460
+ ) : page;
1326
1461
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1327
1462
  editing ? /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu, { children: [
1328
1463
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Trigger, { children: pageShell }),
@@ -1384,16 +1519,101 @@ function EditablePage({
1384
1519
  onAdd: addNode
1385
1520
  }
1386
1521
  ) : null,
1522
+ editing && dropHint?.rect ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, { hint: dropHint }) : null,
1523
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(ElementPalette, { open: paletteOpen, onDragChange: setDraggingEl }) : null,
1387
1524
  editing && timelineDock ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
1388
1525
  ] });
1389
1526
  }
1390
- var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
1527
+ function DropIndicator({
1528
+ hint
1529
+ }) {
1530
+ const r = hint.rect;
1531
+ if (!r) return null;
1532
+ const color = "#ec4899";
1533
+ if (hint.edge === "inside") {
1534
+ return /* @__PURE__ */ jsxRuntime.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 } });
1535
+ }
1536
+ const y = hint.edge === "before" ? r.y : r.y + r.h;
1537
+ return /* @__PURE__ */ jsxRuntime.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 } });
1538
+ }
1539
+ function ElementPalette({ open, onDragChange }) {
1540
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1541
+ "div",
1542
+ {
1543
+ style: {
1544
+ position: "fixed",
1545
+ top: 64,
1546
+ bottom: 96,
1547
+ right: 0,
1548
+ width: 216,
1549
+ transform: open ? "translateX(0)" : "translateX(100%)",
1550
+ transition: "transform 0.18s ease",
1551
+ background: "#0b1220",
1552
+ color: "#e2e8f0",
1553
+ borderLeft: "1px solid #1e293b",
1554
+ borderTopLeftRadius: 12,
1555
+ borderBottomLeftRadius: 12,
1556
+ boxShadow: "-18px 0 48px -20px rgba(0,0,0,0.6)",
1557
+ zIndex: Z2 + 2,
1558
+ fontFamily: "system-ui, sans-serif",
1559
+ padding: 12,
1560
+ overflowY: "auto"
1561
+ },
1562
+ children: [
1563
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5, marginBottom: 10 }, children: "Elements" }),
1564
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }, children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsxs(
1565
+ "div",
1566
+ {
1567
+ draggable: true,
1568
+ onDragStart: (e) => {
1569
+ e.dataTransfer.setData("application/x-cms-element", m.kind);
1570
+ e.dataTransfer.effectAllowed = "copy";
1571
+ onDragChange(true);
1572
+ },
1573
+ onDragEnd: () => onDragChange(false),
1574
+ title: `Drag "${m.label}" onto the page`,
1575
+ style: {
1576
+ display: "flex",
1577
+ flexDirection: "column",
1578
+ alignItems: "center",
1579
+ justifyContent: "center",
1580
+ gap: 4,
1581
+ height: 56,
1582
+ borderRadius: 8,
1583
+ background: "#0f172a",
1584
+ border: "1px solid #1e293b",
1585
+ cursor: "grab",
1586
+ fontSize: 12,
1587
+ userSelect: "none"
1588
+ },
1589
+ children: [
1590
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 16, opacity: 0.7 }, children: "\uFF0B" }),
1591
+ m.label
1592
+ ]
1593
+ },
1594
+ m.kind
1595
+ )) }),
1596
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: 10.5, opacity: 0.45, marginTop: 12, lineHeight: 1.4 }, children: "Drag an Element onto the page. It drops into the container you release over." })
1597
+ ]
1598
+ }
1599
+ );
1600
+ }
1601
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device", "repeater"]);
1391
1602
  var ADD_DEFAULTS = {
1392
1603
  text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
1393
1604
  heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
1394
1605
  button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
1606
+ link: { type: "link", props: { content: "link text", href: "#" }, style: { color: "#7c3aed" } },
1395
1607
  image: { type: "image", props: { src: "", alt: "" }, style: {} },
1396
- box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } }
1608
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } },
1609
+ stack: { type: "stack", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
1610
+ grid: { type: "grid", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "grid" },
1611
+ card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
1612
+ callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
1613
+ divider: { type: "divider", props: {}, style: {} },
1614
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
1615
+ richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
1616
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
1397
1617
  };
1398
1618
  var GRIPS = [
1399
1619
  { key: "nw", north: true, west: true, cursor: "nwse-resize" },
@@ -1414,20 +1634,55 @@ function SelectionOverlay({
1414
1634
  onLive
1415
1635
  }) {
1416
1636
  const drag = react.useRef(null);
1637
+ const targets = react.useRef({ vx: [], hy: [] });
1638
+ const [guides, setGuides] = react.useState({ x: null, y: null });
1417
1639
  const apply = (t) => {
1418
1640
  const el = getEl();
1419
1641
  if (el) applyTransform(el, t);
1420
1642
  };
1643
+ const snap = (cands, lines) => {
1644
+ let best = { delta: 0, at: null, dist: SNAP + 1 };
1645
+ for (const line of lines) {
1646
+ for (const c of cands) {
1647
+ const diff = line - c;
1648
+ if (Math.abs(diff) < best.dist) best = { delta: diff, at: line, dist: Math.abs(diff) };
1649
+ }
1650
+ }
1651
+ return best.dist <= SNAP ? { delta: best.delta, at: best.at } : { delta: 0, at: null };
1652
+ };
1421
1653
  const startMove = (e) => {
1422
1654
  e.stopPropagation();
1423
1655
  e.preventDefault();
1424
1656
  e.currentTarget.setPointerCapture(e.pointerId);
1425
1657
  drag.current = { x: e.clientX, y: e.clientY, w: box.w, h: box.h, live: { ...base } };
1658
+ const sel = getEl();
1659
+ const vx = /* @__PURE__ */ new Set();
1660
+ const hy = /* @__PURE__ */ new Set();
1661
+ document.querySelectorAll("[data-cms]").forEach((el) => {
1662
+ if (sel && (el === sel || sel.contains(el) || el.contains(sel))) return;
1663
+ const r = el.getBoundingClientRect();
1664
+ vx.add(r.left).add(r.left + r.width / 2).add(r.right);
1665
+ hy.add(r.top).add(r.top + r.height / 2).add(r.bottom);
1666
+ });
1667
+ targets.current = { vx: [...vx], hy: [...hy] };
1426
1668
  };
1427
1669
  const moveMove = (e) => {
1428
1670
  const d = drag.current;
1429
1671
  if (!d) return;
1430
- d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
1672
+ let dx = e.clientX - d.x;
1673
+ let dy = e.clientY - d.y;
1674
+ if (!e.altKey) {
1675
+ const l = box.x + dx;
1676
+ const t = box.y + dy;
1677
+ const sx = snap([l, l + box.w / 2, l + box.w], targets.current.vx);
1678
+ const sy = snap([t, t + box.h / 2, t + box.h], targets.current.hy);
1679
+ dx += sx.delta;
1680
+ dy += sy.delta;
1681
+ setGuides({ x: sx.at, y: sy.at });
1682
+ } else if (guides.x !== null || guides.y !== null) {
1683
+ setGuides({ x: null, y: null });
1684
+ }
1685
+ d.live = { ...base, x: (base.x ?? 0) + dx, y: (base.y ?? 0) + dy };
1431
1686
  apply(d.live);
1432
1687
  onLive();
1433
1688
  };
@@ -1462,6 +1717,7 @@ function SelectionOverlay({
1462
1717
  const d = drag.current;
1463
1718
  if (d) onTransform(d.live);
1464
1719
  drag.current = null;
1720
+ setGuides({ x: null, y: null });
1465
1721
  };
1466
1722
  const outline = {
1467
1723
  position: "fixed",
@@ -1491,6 +1747,8 @@ function SelectionOverlay({
1491
1747
  return { left: cx - 6, top: cy - 6, cursor: g.cursor };
1492
1748
  };
1493
1749
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1750
+ guides.x !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", left: guides.x, top: 0, bottom: 0, width: 1, background: "#ec4899", zIndex: Z2 + 3, pointerEvents: "none" } }) : null,
1751
+ guides.y !== null ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "fixed", top: guides.y, left: 0, right: 0, height: 1, background: "#ec4899", zIndex: Z2 + 3, pointerEvents: "none" } }) : null,
1494
1752
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: outline }),
1495
1753
  movable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1496
1754
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1555,11 +1813,20 @@ function FloatingToolbar({
1555
1813
  ] });
1556
1814
  }
1557
1815
  var ADD_MENU = [
1558
- { kind: "text", label: "Text" },
1559
1816
  { kind: "heading", label: "Heading" },
1817
+ { kind: "text", label: "Text" },
1560
1818
  { kind: "button", label: "Button" },
1819
+ { kind: "link", label: "Link" },
1561
1820
  { kind: "image", label: "Image" },
1562
- { kind: "box", label: "Box" }
1821
+ { kind: "card", label: "Card" },
1822
+ { kind: "callout", label: "Callout" },
1823
+ { kind: "stack", label: "Stack" },
1824
+ { kind: "grid", label: "Grid" },
1825
+ { kind: "box", label: "Box" },
1826
+ { kind: "divider", label: "Divider" },
1827
+ { kind: "code", label: "Code" },
1828
+ { kind: "richtext", label: "Rich text" },
1829
+ { kind: "repeater", label: "Repeater" }
1563
1830
  ];
1564
1831
  function EditBar({
1565
1832
  editing,
@@ -1577,7 +1844,7 @@ function EditBar({
1577
1844
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
1578
1845
  editing ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1579
1846
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1580
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an element", children: "\uFF0B Add \u25BE" }),
1847
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an Element (or drag from the right-edge palette)", children: "\uFF0B Element \u25BE" }),
1581
1848
  open ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: "calc(100% + 6px)", right: 0, background: "#0f172a", border: "1px solid #1e293b", borderRadius: 10, padding: 4, display: "flex", flexDirection: "column", minWidth: 132, boxShadow: "0 12px 32px -10px rgba(0,0,0,0.6)" }, children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsx(
1582
1849
  "button",
1583
1850
  {
@@ -1615,7 +1882,7 @@ function EditMenuItems({
1615
1882
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1616
1883
  editable ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1617
1884
  /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu.Sub, { children: [
1618
- /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1885
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add Element inside" : "Add Element" }),
1619
1886
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubContent, { className: "!z-[2147483647]", children: ADD_MENU.map((m) => /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onAdd(m.kind), children: m.label }, m.kind)) })
1620
1887
  ] }),
1621
1888
  node ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [