@particle-academy/fancy-cms-ui 0.0.1-dev.7 → 0.0.1-dev.8

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
@@ -2,6 +2,7 @@
2
2
 
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
+ var reactFancy = require('@particle-academy/react-fancy');
5
6
 
6
7
  // src/editor/Editor.tsx
7
8
 
@@ -725,11 +726,44 @@ function Toolbar({ ed }) {
725
726
  var BASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
726
727
  var RADIX = BASE.length;
727
728
  var MID = Math.floor(RADIX / 2);
729
+ function val(c) {
730
+ const i = BASE.indexOf(c);
731
+ return i < 0 ? 0 : i;
732
+ }
728
733
  function keyBetween(a, b) {
729
734
  if (a === null) a = "";
730
- {
735
+ if (b === null) {
731
736
  return a + BASE[MID];
732
737
  }
738
+ if (a >= b) {
739
+ throw new Error(`keyBetween: lower bound "${a}" is not below upper bound "${b}"`);
740
+ }
741
+ let result = "";
742
+ let i = 0;
743
+ for (; ; ) {
744
+ const ca = i < a.length ? val(a[i]) : 0;
745
+ const cb = i < b.length ? val(b[i]) : RADIX;
746
+ if (cb - ca > 1) {
747
+ result += BASE[ca + Math.floor((cb - ca) / 2)];
748
+ return result;
749
+ }
750
+ if (ca === cb) {
751
+ result += BASE[ca];
752
+ i++;
753
+ continue;
754
+ }
755
+ result += BASE[ca];
756
+ i++;
757
+ for (; ; ) {
758
+ const da = i < a.length ? val(a[i]) : 0;
759
+ if (RADIX - da > 1) {
760
+ result += BASE[da + Math.floor((RADIX - da) / 2)];
761
+ return result;
762
+ }
763
+ result += BASE[da];
764
+ i++;
765
+ }
766
+ }
733
767
  }
734
768
  var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
735
769
  function NodeInspector({
@@ -926,6 +960,110 @@ var input2 = {
926
960
  };
927
961
  var iconBtn = { background: "transparent", border: "none", color: "#e2e8f0", cursor: "pointer", fontSize: 13, padding: 4, borderRadius: 6 };
928
962
  var miniBtn = { font: "inherit", fontSize: 11, color: "#e2e8f0", background: "#334155", border: "1px solid #475569", borderRadius: 6, padding: "5px 10px", cursor: "pointer" };
963
+
964
+ // src/editor/editorOps.ts
965
+ function childrenOfMap(nodes, parent) {
966
+ return Object.values(nodes).filter((n) => n.parent === parent).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
967
+ }
968
+ function subtreeBfs(nodes, root) {
969
+ const out = [];
970
+ const queue = [root];
971
+ while (queue.length) {
972
+ const cur = queue.shift();
973
+ out.push(cur);
974
+ for (const child of childrenOfMap(nodes, cur)) queue.push(child.id);
975
+ }
976
+ return out;
977
+ }
978
+ function cloneFrom(nodes, rootId, newParent, newOrder, seqBase) {
979
+ if (!nodes[rootId]) return { ops: [], newRootId: null };
980
+ const order = subtreeBfs(nodes, rootId);
981
+ const idMap = {};
982
+ order.forEach((old, i) => idMap[old] = `${old}_c${seqBase}_${i}`);
983
+ const ops = order.map((old) => {
984
+ const src = nodes[old];
985
+ const cloned = JSON.parse(JSON.stringify(src));
986
+ cloned.id = idMap[old];
987
+ cloned.parent = old === rootId ? newParent : idMap[src.parent];
988
+ if (old === rootId) cloned.order = newOrder;
989
+ return { t: "insert_node", node: cloned };
990
+ });
991
+ return { ops, newRootId: idMap[rootId] };
992
+ }
993
+ function duplicateOps(doc, id) {
994
+ const node = doc.nodes[id];
995
+ if (!node) return { ops: [], newRootId: null };
996
+ const sibs = childrenOf(doc, node.parent);
997
+ const idx = sibs.findIndex((s) => s.id === id);
998
+ const newOrder = keyBetween(node.order, sibs[idx + 1]?.order ?? null);
999
+ const { ops, newRootId } = cloneFrom(doc.nodes, id, node.parent, newOrder, doc.seq);
1000
+ if (node.parent === null && newRootId) {
1001
+ const base = doc.sections;
1002
+ const at = base.indexOf(id);
1003
+ ops.push({ t: "reorder_sections", order: [...base.slice(0, at + 1), newRootId, ...base.slice(at + 1)] });
1004
+ }
1005
+ return { ops, newRootId };
1006
+ }
1007
+ function pasteOps(doc, clip, target, containerTypes) {
1008
+ const sel = target ? doc.nodes[target] : null;
1009
+ const parent = sel ? containerTypes.has(sel.type) ? sel.id : sel.parent : doc.sections[doc.sections.length - 1] ?? null;
1010
+ const sibs = childrenOf(doc, parent);
1011
+ const newOrder = keyBetween(sibs.length ? sibs[sibs.length - 1].order : null, null);
1012
+ const { ops, newRootId } = cloneFrom(clip.nodes, clip.rootId, parent, newOrder, doc.seq);
1013
+ return { ops, newRootId };
1014
+ }
1015
+ function reorderOps(doc, id, dir) {
1016
+ const node = doc.nodes[id];
1017
+ if (!node) return [];
1018
+ if (node.parent === null) {
1019
+ const s = [...doc.sections];
1020
+ const i = s.indexOf(id);
1021
+ if (i < 0) return [];
1022
+ s.splice(i, 1);
1023
+ if (dir === "front") s.unshift(id);
1024
+ else if (dir === "back") s.push(id);
1025
+ else s.splice(Math.max(0, Math.min(s.length, dir === "up" ? i - 1 : i + 1)), 0, id);
1026
+ return [{ t: "reorder_sections", order: s }];
1027
+ }
1028
+ const sibs = childrenOf(doc, node.parent);
1029
+ const idx = sibs.findIndex((sb) => sb.id === id);
1030
+ if (idx < 0) return [];
1031
+ const last = sibs.length - 1;
1032
+ let newOrder;
1033
+ if (dir === "up") {
1034
+ if (idx <= 0) return [];
1035
+ newOrder = keyBetween(sibs[idx - 2]?.order ?? null, sibs[idx - 1].order);
1036
+ } else if (dir === "down") {
1037
+ if (idx >= last) return [];
1038
+ newOrder = keyBetween(sibs[idx + 1].order, sibs[idx + 2]?.order ?? null);
1039
+ } else if (dir === "front") {
1040
+ if (idx >= last) return [];
1041
+ newOrder = keyBetween(sibs[last].order, null);
1042
+ } else {
1043
+ if (idx <= 0) return [];
1044
+ newOrder = keyBetween(null, sibs[0].order);
1045
+ }
1046
+ return [{ t: "move_node", id, parent: node.parent, order: newOrder }];
1047
+ }
1048
+ function wrapInBoxOps(doc, id) {
1049
+ const node = doc.nodes[id];
1050
+ if (!node || node.parent === null) return { ops: [], newRootId: null };
1051
+ const frameId = `box_${doc.seq}`;
1052
+ return {
1053
+ ops: [
1054
+ { t: "insert_node", node: { id: frameId, type: "frame", parent: node.parent, order: node.order, props: {}, style: { base: { padding: { value: 16, unit: "px" } } } } },
1055
+ { t: "move_node", id, parent: frameId, order: "a" }
1056
+ ],
1057
+ newRootId: frameId
1058
+ };
1059
+ }
1060
+ function snapshotSubtree(doc, id) {
1061
+ if (!doc.nodes[id]) return null;
1062
+ const ids = subtreeBfs(doc.nodes, id);
1063
+ const nodes = {};
1064
+ for (const nid of ids) nodes[nid] = JSON.parse(JSON.stringify(doc.nodes[nid]));
1065
+ return { rootId: id, nodes };
1066
+ }
929
1067
  var Z2 = 2147483e3;
930
1068
  function EditablePage({
931
1069
  doc,
@@ -1029,7 +1167,7 @@ function EditablePage({
1029
1167
  const doc2 = ed.state.doc;
1030
1168
  const parent = selNode ? CONTAINER_TYPES2.has(selNode.type) ? selNode.id : selNode.parent : doc2.sections[doc2.sections.length - 1] ?? null;
1031
1169
  const siblings = childrenOf(doc2, parent);
1032
- const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null);
1170
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
1033
1171
  const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
1034
1172
  const def = ADD_DEFAULTS[kind];
1035
1173
  ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
@@ -1044,9 +1182,76 @@ function EditablePage({
1044
1182
  ed.apply({ t: "remove_node", id: selection });
1045
1183
  ed.select(null);
1046
1184
  };
1047
- const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
1185
+ const clip = react.useRef(null);
1186
+ const runOps = (result) => {
1187
+ result.ops.forEach((op) => ed.apply(op));
1188
+ if (result.newRootId) ed.select(result.newRootId);
1189
+ };
1190
+ const duplicate = (id) => id && runOps(duplicateOps(ed.state.doc, id));
1191
+ const reorder = (id, dir) => id && reorderOps(ed.state.doc, id, dir).forEach((op) => ed.apply(op));
1192
+ const wrapInBox = (id) => id && runOps(wrapInBoxOps(ed.state.doc, id));
1193
+ const copy = (id) => {
1194
+ if (id) clip.current = snapshotSubtree(ed.state.doc, id);
1195
+ };
1196
+ const paste = (target) => {
1197
+ if (clip.current) runOps(pasteOps(ed.state.doc, clip.current, target, CONTAINER_TYPES2));
1198
+ };
1199
+ const startEditText = (id) => {
1200
+ if (!id) return;
1201
+ ed.select(id);
1202
+ requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
1203
+ };
1204
+ react.useEffect(() => {
1205
+ if (!editing) return;
1206
+ const onKey = (e) => {
1207
+ const t = e.target;
1208
+ const typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
1209
+ if (e.key === "Escape") return void ed.select(null);
1210
+ if (typing) return;
1211
+ if ((e.key === "Delete" || e.key === "Backspace") && selection) {
1212
+ e.preventDefault();
1213
+ removeSelected();
1214
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d" && selection) {
1215
+ e.preventDefault();
1216
+ duplicate(selection);
1217
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selection) {
1218
+ copy(selection);
1219
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
1220
+ e.preventDefault();
1221
+ paste(selection);
1222
+ }
1223
+ };
1224
+ window.addEventListener("keydown", onKey);
1225
+ return () => window.removeEventListener("keydown", onKey);
1226
+ }, [editing, selection]);
1227
+ const onContextMenuSelect = (e) => {
1228
+ if (!editing) return;
1229
+ const el = e.target.closest("[data-cms]");
1230
+ if (el) ed.select(el.getAttribute("data-cms"));
1231
+ else ed.select(null);
1232
+ };
1233
+ const page = /* @__PURE__ */ jsxRuntime.jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsxRuntime.jsx(CmsPage, { doc: ed.state.doc, registry }) });
1234
+ 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;
1048
1235
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
1049
- 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,
1236
+ editing ? /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu, { children: [
1237
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Trigger, { children: pageShell }),
1238
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Content, { className: "!z-[2147483646]", children: /* @__PURE__ */ jsxRuntime.jsx(
1239
+ EditMenuItems,
1240
+ {
1241
+ node: selNode ?? null,
1242
+ hasClipboard: Boolean(clip.current),
1243
+ isContainer: selNode ? CONTAINER_TYPES2.has(selNode.type) : false,
1244
+ onEditText: () => startEditText(selection),
1245
+ onAdd: addNode,
1246
+ onDuplicate: () => duplicate(selection),
1247
+ onCopy: () => copy(selection),
1248
+ onPaste: () => paste(selection),
1249
+ onWrap: () => wrapInBox(selection),
1250
+ onReorder: (d) => reorder(selection, d),
1251
+ onDelete: removeSelected
1252
+ }
1253
+ ) })
1254
+ ] }) : pageShell,
1050
1255
  editing && box && selection ? /* @__PURE__ */ jsxRuntime.jsx(
1051
1256
  SelectionOverlay,
1052
1257
  {
@@ -1300,6 +1505,45 @@ function EditBar({
1300
1505
  ] }) : null
1301
1506
  ] });
1302
1507
  }
1508
+ function EditMenuItems({
1509
+ node,
1510
+ hasClipboard,
1511
+ isContainer,
1512
+ onEditText,
1513
+ onAdd,
1514
+ onDuplicate,
1515
+ onCopy,
1516
+ onPaste,
1517
+ onWrap,
1518
+ onReorder,
1519
+ onDelete
1520
+ }) {
1521
+ const editable = node && (node.type === "text" || node.type === "heading");
1522
+ const nested = Boolean(node && node.parent !== null);
1523
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1524
+ editable ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1525
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.ContextMenu.Sub, { children: [
1526
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1527
+ /* @__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)) })
1528
+ ] }),
1529
+ node ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1530
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
1531
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
1532
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
1533
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" }),
1534
+ nested ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: onWrap, children: "Wrap in Box" }) : null,
1535
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
1536
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
1537
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
1538
+ nested ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1539
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
1540
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
1541
+ ] }) : null,
1542
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Separator, {}),
1543
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { danger: true, onClick: onDelete, children: "Delete" })
1544
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContextMenu.Item, { disabled: !hasClipboard, onClick: onPaste, children: "Paste" })
1545
+ ] });
1546
+ }
1303
1547
  function applyTransform(el, t) {
1304
1548
  if (t && (t.x != null || t.y != null || t.scale != null || t.rotate != null || t.opacity != null || t.w != null || t.h != null)) {
1305
1549
  el.style.transform = `translate3d(${t.x ?? 0}px, ${t.y ?? 0}px, 0) scale(${t.scale ?? 1}) rotate(${t.rotate ?? 0}deg)`;