@particle-academy/fancy-cms-ui 0.0.1-dev.9 → 0.1.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/editor.js CHANGED
@@ -1,404 +1,11 @@
1
- import { keyBetween } from './chunk-7YYLYBLI.js';
2
- import { CmsPage, defaultRegistry } from './chunk-ZV7CV54P.js';
3
- import { reduce, childrenOf } from './chunk-3YZMELJJ.js';
4
- import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useEffect } from 'react';
5
- import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
1
+ import { useEditor, useLatestRef, keyBetween } from './chunk-524NYOHK.js';
2
+ export { Canvas, Editor, Inspector, LayersPanel, editorReduce, initEditor, useEditor } from './chunk-524NYOHK.js';
3
+ import { isBinding, childrenOf, CmsPage, defaultRegistry } from './chunk-GOXVGEKU.js';
4
+ import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
6
5
  import { ContextMenu } from '@particle-academy/react-fancy';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
 
8
- function initEditor(doc) {
9
- return { doc, past: [], future: [], selection: null };
10
- }
11
- function editorReduce(state, action) {
12
- switch (action.type) {
13
- case "apply": {
14
- const next = reduce(state.doc, action.op);
15
- if (next === state.doc) return state;
16
- return {
17
- ...state,
18
- doc: next,
19
- past: [...state.past, state.doc].slice(-100),
20
- future: []
21
- };
22
- }
23
- case "undo": {
24
- if (state.past.length === 0) return state;
25
- const prev = state.past[state.past.length - 1];
26
- return {
27
- ...state,
28
- doc: prev,
29
- past: state.past.slice(0, -1),
30
- future: [state.doc, ...state.future]
31
- };
32
- }
33
- case "redo": {
34
- if (state.future.length === 0) return state;
35
- const next = state.future[0];
36
- return {
37
- ...state,
38
- doc: next,
39
- past: [...state.past, state.doc],
40
- future: state.future.slice(1)
41
- };
42
- }
43
- case "select":
44
- return state.selection === action.id ? state : { ...state, selection: action.id };
45
- case "replace":
46
- return state.doc === action.doc ? state : { ...state, doc: action.doc, future: [] };
47
- default:
48
- return state;
49
- }
50
- }
51
-
52
- // src/editor/useEditor.ts
53
- function useEditor(initialDoc) {
54
- const [state, dispatch] = useReducer(editorReduce, initialDoc, initEditor);
55
- const apply = useCallback((op) => dispatch({ type: "apply", op }), []);
56
- const undo = useCallback(() => dispatch({ type: "undo" }), []);
57
- const redo = useCallback(() => dispatch({ type: "redo" }), []);
58
- const select = useCallback((id) => dispatch({ type: "select", id }), []);
59
- const replace = useCallback((doc) => dispatch({ type: "replace", doc }), []);
60
- return useMemo(
61
- () => ({
62
- state,
63
- apply,
64
- undo,
65
- redo,
66
- select,
67
- replace,
68
- canUndo: state.past.length > 0,
69
- canRedo: state.future.length > 0
70
- }),
71
- [state, apply, undo, redo, select, replace]
72
- );
73
- }
74
- function Canvas({ doc, selection, onSelect, apply }) {
75
- const ref = useRef(null);
76
- const [box, setBox] = useState(null);
77
- const drag = useRef(null);
78
- useLayoutEffect(() => {
79
- const root = ref.current;
80
- if (!root || !selection) {
81
- setBox(null);
82
- return;
83
- }
84
- const el = root.querySelector(`[data-cms="${cssEscape(selection)}"]`);
85
- if (!el) {
86
- setBox(null);
87
- return;
88
- }
89
- const r = el.getBoundingClientRect();
90
- const rootRect = root.getBoundingClientRect();
91
- setBox({
92
- x: r.left - rootRect.left + root.scrollLeft,
93
- y: r.top - rootRect.top + root.scrollTop,
94
- w: r.width,
95
- h: r.height
96
- });
97
- }, [selection, doc]);
98
- const handleClick = (e) => {
99
- const target = e.target.closest("[data-cms]");
100
- onSelect(target ? target.getAttribute("data-cms") : null);
101
- };
102
- const handlePointerDown = (e) => {
103
- if (!selection) return;
104
- const c = doc.nodes[selection]?.constraints?.base;
105
- const left = c && c.left && typeof c.left === "object" ? c.left.value : 0;
106
- const top = c && c.top && typeof c.top === "object" ? c.top.value : 0;
107
- drag.current = { x: e.clientX, y: e.clientY, left, top };
108
- e.currentTarget.setPointerCapture(e.pointerId);
109
- e.stopPropagation();
110
- };
111
- const handlePointerMove = (e) => {
112
- const d = drag.current;
113
- if (!d || !selection) return;
114
- apply({
115
- t: "set_constraints",
116
- id: selection,
117
- breakpoint: "base",
118
- patch: {
119
- left: { value: Math.round(d.left + (e.clientX - d.x)), unit: "px" },
120
- top: { value: Math.round(d.top + (e.clientY - d.y)), unit: "px" }
121
- }
122
- });
123
- };
124
- const handlePointerUp = () => {
125
- drag.current = null;
126
- };
127
- const surface = {
128
- position: "relative",
129
- overflow: "auto",
130
- height: "100%",
131
- background: "#f8fafc"
132
- };
133
- const overlay = box ? {
134
- position: "absolute",
135
- left: box.x,
136
- top: box.y,
137
- width: box.w,
138
- height: box.h,
139
- outline: "2px solid #8b5cf6",
140
- outlineOffset: -1,
141
- cursor: "move",
142
- touchAction: "none"
143
- } : { display: "none" };
144
- return /* @__PURE__ */ jsxs("div", { ref, onClick: handleClick, style: surface, children: [
145
- /* @__PURE__ */ jsx(CmsPage, { doc }),
146
- /* @__PURE__ */ jsx(
147
- "div",
148
- {
149
- onPointerDown: handlePointerDown,
150
- onPointerMove: handlePointerMove,
151
- onPointerUp: handlePointerUp,
152
- style: overlay
153
- }
154
- )
155
- ] });
156
- }
157
- function cssEscape(value) {
158
- return value.replace(/["\\]/g, "\\$&");
159
- }
160
- var MIME = "application/x-cms-node";
161
- function isAncestorOrSelf(doc, id, maybeAncestor) {
162
- for (let cur = id; cur; cur = doc.nodes[cur]?.parent ?? null) {
163
- if (cur === maybeAncestor) return true;
164
- }
165
- return false;
166
- }
167
- function LayersPanel({ doc, selection, onSelect, onMove }) {
168
- const [overId, setOverId] = useState(null);
169
- return /* @__PURE__ */ jsx("div", { style: { overflow: "auto", padding: 8, fontSize: 13, fontFamily: "system-ui, sans-serif" }, children: doc.sections.map((id) => /* @__PURE__ */ jsx(LayerRow, { doc, id, depth: 0, selection, onSelect, onMove, overId, setOverId }, id)) });
170
- }
171
- function LayerRow({ doc, id, depth, selection, onSelect, onMove, overId, setOverId }) {
172
- const node = doc.nodes[id];
173
- if (!node) return null;
174
- const selected = selection === id;
175
- const draggable = Boolean(onMove);
176
- const drop = (e) => {
177
- e.preventDefault();
178
- e.stopPropagation();
179
- setOverId(null);
180
- if (!onMove) return;
181
- const dragId = e.dataTransfer.getData(MIME);
182
- if (!dragId || dragId === id || isAncestorOrSelf(doc, id, dragId)) return;
183
- const siblings = childrenOf(doc, node.parent);
184
- const idx = siblings.findIndex((n) => n.id === id);
185
- const next = siblings[idx + 1];
186
- onMove(dragId, node.parent, keyBetween(node.order, next ? next.order : null));
187
- };
188
- return /* @__PURE__ */ jsxs(Fragment, { children: [
189
- /* @__PURE__ */ jsxs(
190
- "button",
191
- {
192
- type: "button",
193
- draggable,
194
- onClick: () => onSelect(id),
195
- onDragStart: (e) => {
196
- e.dataTransfer.setData(MIME, id);
197
- e.dataTransfer.effectAllowed = "move";
198
- },
199
- onDragOver: (e) => {
200
- if (!onMove) return;
201
- e.preventDefault();
202
- e.dataTransfer.dropEffect = "move";
203
- if (overId !== id) setOverId(id);
204
- },
205
- onDragLeave: () => overId === id && setOverId(null),
206
- onDrop: drop,
207
- style: {
208
- display: "block",
209
- width: "100%",
210
- textAlign: "left",
211
- border: "none",
212
- borderRadius: 6,
213
- cursor: draggable ? "grab" : "pointer",
214
- padding: "4px 8px",
215
- paddingLeft: 8 + depth * 14,
216
- background: selected ? "#ede9fe" : "transparent",
217
- color: selected ? "#5b21b6" : "#334155",
218
- font: "inherit",
219
- boxShadow: overId === id ? "inset 0 -2px 0 0 #8b5cf6" : "none"
220
- },
221
- children: [
222
- /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: node.type }),
223
- " \xB7 ",
224
- id
225
- ]
226
- }
227
- ),
228
- childrenOf(doc, id).map((child) => /* @__PURE__ */ jsx(LayerRow, { doc, id: child.id, depth: depth + 1, selection, onSelect, onMove, overId, setOverId }, child.id))
229
- ] });
230
- }
231
- var label = {
232
- display: "block",
233
- fontSize: 11,
234
- textTransform: "uppercase",
235
- letterSpacing: "0.04em",
236
- color: "#94a3b8",
237
- margin: "10px 0 4px"
238
- };
239
- var input = {
240
- width: "100%",
241
- boxSizing: "border-box",
242
- padding: "6px 8px",
243
- borderRadius: 6,
244
- border: "1px solid #e2e8f0",
245
- font: "inherit",
246
- fontSize: 13
247
- };
248
- function Inspector({ doc, selection, apply }) {
249
- const node = selection ? doc.nodes[selection] : null;
250
- if (!node) {
251
- return /* @__PURE__ */ jsx("div", { style: { padding: 16, color: "#94a3b8", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
252
- }
253
- const s = node.style.base;
254
- const setStyle = (patch) => apply({ t: "set_style", id: node.id, breakpoint: "base", patch });
255
- const setProp = (key, value) => apply({ t: "set_props", id: node.id, patch: { [key]: value } });
256
- return /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", padding: 16, fontFamily: "system-ui, sans-serif", height: "100%", boxSizing: "border-box" }, children: [
257
- /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a" }, children: node.type }),
258
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#94a3b8", marginBottom: 8 }, children: node.id }),
259
- node.type === "text" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
260
- /* @__PURE__ */ jsx("label", { style: label, children: "Content" }),
261
- /* @__PURE__ */ jsx(
262
- "textarea",
263
- {
264
- style: { ...input, minHeight: 64, resize: "vertical" },
265
- value: typeof node.props.content === "string" ? node.props.content : "",
266
- onChange: (e) => setProp("content", e.target.value)
267
- }
268
- )
269
- ] }) : null,
270
- node.type === "image" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
271
- /* @__PURE__ */ jsx("label", { style: label, children: "Source" }),
272
- /* @__PURE__ */ jsx(
273
- "input",
274
- {
275
- style: input,
276
- value: typeof node.props.src === "string" ? node.props.src : "",
277
- onChange: (e) => setProp("src", e.target.value)
278
- }
279
- )
280
- ] }) : null,
281
- /* @__PURE__ */ jsx("label", { style: label, children: "Background" }),
282
- /* @__PURE__ */ jsx(
283
- "input",
284
- {
285
- style: input,
286
- placeholder: "#ffffff | gradient | url(\u2026)",
287
- value: s.background ?? "",
288
- onChange: (e) => setStyle({ background: e.target.value })
289
- }
290
- ),
291
- /* @__PURE__ */ jsx("label", { style: label, children: "Text color" }),
292
- /* @__PURE__ */ jsx(
293
- "input",
294
- {
295
- style: input,
296
- placeholder: "#0f172a",
297
- value: s.color ?? "",
298
- onChange: (e) => setStyle({ color: e.target.value })
299
- }
300
- ),
301
- /* @__PURE__ */ jsx("label", { style: label, children: "Font size (px)" }),
302
- /* @__PURE__ */ jsx(
303
- "input",
304
- {
305
- type: "number",
306
- style: input,
307
- value: s.fontSize?.value ?? "",
308
- onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
309
- }
310
- ),
311
- /* @__PURE__ */ jsx("label", { style: label, children: "Padding (px)" }),
312
- /* @__PURE__ */ jsx(
313
- "input",
314
- {
315
- type: "number",
316
- style: input,
317
- value: s.padding?.value ?? "",
318
- onChange: (e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: "px" } })
319
- }
320
- ),
321
- /* @__PURE__ */ jsx("label", { style: label, children: "Opacity" }),
322
- /* @__PURE__ */ jsx(
323
- "input",
324
- {
325
- type: "number",
326
- step: "0.1",
327
- min: "0",
328
- max: "1",
329
- style: input,
330
- value: s.opacity ?? "",
331
- onChange: (e) => setStyle({ opacity: Number(e.target.value) })
332
- }
333
- )
334
- ] });
335
- }
336
- function Editor({ defaultValue, onChange }) {
337
- const ed = useEditor(defaultValue);
338
- const first = useRef(true);
339
- useEffect(() => {
340
- if (first.current) {
341
- first.current = false;
342
- return;
343
- }
344
- onChange?.(ed.state.doc);
345
- }, [ed.state.doc, onChange]);
346
- const shell = {
347
- display: "grid",
348
- gridTemplateColumns: "220px 1fr 300px",
349
- height: "100%",
350
- minHeight: 0,
351
- background: "#fff",
352
- color: "#0f172a",
353
- border: "1px solid #e2e8f0",
354
- borderRadius: 12,
355
- overflow: "hidden"
356
- };
357
- return /* @__PURE__ */ jsxs("div", { style: shell, children: [
358
- /* @__PURE__ */ jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
359
- /* @__PURE__ */ jsx(Toolbar, { ed }),
360
- /* @__PURE__ */ jsx(
361
- LayersPanel,
362
- {
363
- doc: ed.state.doc,
364
- selection: ed.state.selection,
365
- onSelect: ed.select,
366
- onMove: (id, parent, order) => ed.apply({ t: "move_node", id, parent, order })
367
- }
368
- )
369
- ] }),
370
- /* @__PURE__ */ jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
371
- /* @__PURE__ */ jsx("div", { style: { borderLeft: "1px solid #e2e8f0", minHeight: 0 }, children: /* @__PURE__ */ jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply }) })
372
- ] });
373
- }
374
- function Toolbar({ ed }) {
375
- const btn = {
376
- font: "inherit",
377
- fontSize: 12,
378
- padding: "4px 10px",
379
- borderRadius: 6,
380
- border: "1px solid #e2e8f0",
381
- background: "#fff",
382
- cursor: "pointer"
383
- };
384
- return /* @__PURE__ */ jsxs(
385
- "div",
386
- {
387
- style: {
388
- display: "flex",
389
- gap: 6,
390
- padding: 8,
391
- borderBottom: "1px solid #e2e8f0",
392
- fontFamily: "system-ui, sans-serif"
393
- },
394
- children: [
395
- /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: ed.canUndo ? 1 : 0.4 }, disabled: !ed.canUndo, onClick: ed.undo, children: "Undo" }),
396
- /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" })
397
- ]
398
- }
399
- );
400
- }
401
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
8
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device"]);
402
9
  function NodeInspector({
403
10
  node,
404
11
  transform,
@@ -412,9 +19,9 @@ function NodeInspector({
412
19
  }) {
413
20
  const base = node.style.base ?? {};
414
21
  const setT = (patch) => onTransform({ ...transform, ...patch });
415
- const handled = /* @__PURE__ */ new Set(["content", "label"]);
22
+ const handled = /* @__PURE__ */ new Set(["content", "label", "html"]);
416
23
  const extraKeys = Object.keys(node.props).filter(
417
- (k) => !handled.has(k) && ["string", "number", "boolean"].includes(typeof node.props[k])
24
+ (k) => !handled.has(k) && (["string", "number", "boolean"].includes(typeof node.props[k]) || isBinding(node.props[k]))
418
25
  );
419
26
  return /* @__PURE__ */ jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
420
27
  /* @__PURE__ */ jsxs("div", { style: headRow, children: [
@@ -427,15 +34,12 @@ function NodeInspector({
427
34
  /* @__PURE__ */ jsx("button", { type: "button", style: iconBtn, title: "Close", onClick: onClose, children: "\u2715" })
428
35
  ] }),
429
36
  /* @__PURE__ */ jsxs("div", { style: body, children: [
430
- "content" in node.props ? /* @__PURE__ */ jsx(Group, { label: "Content", children: /* @__PURE__ */ jsx(
431
- "textarea",
432
- {
433
- style: { ...input2, minHeight: 64, resize: "vertical", lineHeight: 1.4 },
434
- value: String(node.props.content ?? ""),
435
- onChange: (e) => onProps({ content: e.target.value })
436
- }
437
- ) }) : null,
438
- "label" in node.props ? /* @__PURE__ */ jsx(Group, { label: "Label", children: /* @__PURE__ */ jsx("input", { style: input2, value: String(node.props.label ?? ""), onChange: (e) => onProps({ label: e.target.value }) }) }) : null,
37
+ "content" in node.props || node.type === "richtext" ? /* @__PURE__ */ jsxs("p", { style: { fontSize: 11, color: "var(--fg-3, #94a3b8)", margin: "0 0 12px", lineHeight: 1.45, paddingBottom: 12, borderBottom: "1px solid #1e293b" }, children: [
38
+ "\u270E Edit the text ",
39
+ /* @__PURE__ */ jsx("strong", { children: "directly on the page" }),
40
+ " \u2014 click in and type. This panel is for layout, style, and bindings."
41
+ ] }) : null,
42
+ "label" in node.props ? /* @__PURE__ */ jsx(Group, { label: "Label", children: /* @__PURE__ */ jsx("input", { style: input, value: String(node.props.label ?? ""), onChange: (e) => onProps({ label: e.target.value }) }) }) : null,
439
43
  extraKeys.length ? /* @__PURE__ */ jsx(Group, { label: "Properties", children: extraKeys.map((k) => /* @__PURE__ */ jsx(PropField, { name: k, value: node.props[k], onChange: (v) => onProps({ [k]: v }) }, k)) }) : null,
440
44
  /* @__PURE__ */ jsxs(Group, { label: "Position & size", children: [
441
45
  /* @__PURE__ */ jsxs(Row, { children: [
@@ -529,9 +133,9 @@ function NodeInspector({
529
133
  ] })
530
134
  ] });
531
135
  }
532
- function Group({ label: label2, children }) {
136
+ function Group({ label, children }) {
533
137
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, paddingBottom: 12, marginBottom: 12, borderBottom: "1px solid #1e293b" }, children: [
534
- /* @__PURE__ */ jsx("span", { style: { fontSize: 10, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5 }, children: label2 }),
138
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5 }, children: label }),
535
139
  children
536
140
  ] });
537
141
  }
@@ -539,36 +143,62 @@ function Row({ children }) {
539
143
  return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8 }, children });
540
144
  }
541
145
  function PropField({ name, value, onChange }) {
146
+ const bound = isBinding(value);
147
+ const link = /* @__PURE__ */ jsx(
148
+ "button",
149
+ {
150
+ type: "button",
151
+ title: bound ? "Unbind (use a literal value)" : "Bind to data (e.g. item.name)",
152
+ onClick: () => onChange(bound ? "" : { $bind: "" }),
153
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontSize: 11, color: bound ? "#a78bfa" : "#64748b" },
154
+ children: "\u{1F517}"
155
+ }
156
+ );
157
+ if (bound) {
158
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
159
+ /* @__PURE__ */ jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
160
+ name,
161
+ " ",
162
+ link
163
+ ] }),
164
+ /* @__PURE__ */ jsx("input", { style: { ...input, borderColor: "#7c3aed" }, placeholder: "data path \xB7 e.g. item.name", value: value.$bind, onChange: (e) => onChange({ $bind: e.target.value }) })
165
+ ] });
166
+ }
542
167
  if (typeof value === "boolean") {
543
168
  return /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
544
169
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
545
- /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize" }, children: name })
170
+ /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize", flex: 1 }, children: name }),
171
+ link
546
172
  ] });
547
173
  }
548
174
  if (name === "variant") {
549
- return /* @__PURE__ */ jsx(Sel, { label: name, value: String(value), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
175
+ return /* @__PURE__ */ jsx(Sel, { label: name, value: String(value ?? ""), options: [["primary", "Primary"], ["ghost", "Ghost"], ["outline", "Outline"]], onChange });
550
176
  }
551
177
  const isNum = typeof value === "number";
552
178
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
553
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: name }),
179
+ /* @__PURE__ */ jsxs("span", { style: { ...fieldLabel, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
180
+ name,
181
+ " ",
182
+ link
183
+ ] }),
554
184
  /* @__PURE__ */ jsx(
555
185
  "input",
556
186
  {
557
- style: input2,
187
+ style: input,
558
188
  type: isNum ? "number" : "text",
559
- value: String(value),
189
+ value: String(value ?? ""),
560
190
  onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
561
191
  }
562
192
  )
563
193
  ] });
564
194
  }
565
- function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
195
+ function Num({ label, value, placeholder, onChange, step, min, max }) {
566
196
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
567
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
197
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label }),
568
198
  /* @__PURE__ */ jsx(
569
199
  "input",
570
200
  {
571
- style: input2,
201
+ style: input,
572
202
  type: "number",
573
203
  step,
574
204
  min,
@@ -580,22 +210,22 @@ function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
580
210
  )
581
211
  ] });
582
212
  }
583
- function Txt({ label: label2, value, placeholder, onChange }) {
213
+ function Txt({ label, value, placeholder, onChange }) {
584
214
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
585
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
586
- /* @__PURE__ */ jsx("input", { style: input2, type: "text", value, placeholder, onChange: (e) => onChange(e.target.value) })
215
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label }),
216
+ /* @__PURE__ */ jsx("input", { style: input, type: "text", value, placeholder, onChange: (e) => onChange(e.target.value) })
587
217
  ] });
588
218
  }
589
- function Color({ label: label2, value, onChange }) {
219
+ function Color({ label, value, onChange }) {
590
220
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
591
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
592
- /* @__PURE__ */ jsx("input", { style: { ...input2, padding: 2, height: 30 }, type: "color", value, onChange: (e) => onChange(e.target.value) })
221
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label }),
222
+ /* @__PURE__ */ jsx("input", { style: { ...input, padding: 2, height: 30 }, type: "color", value, onChange: (e) => onChange(e.target.value) })
593
223
  ] });
594
224
  }
595
- function Sel({ label: label2, value, options, onChange }) {
225
+ function Sel({ label, value, options, onChange }) {
596
226
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
597
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
598
- /* @__PURE__ */ jsx("select", { style: input2, value, onChange: (e) => onChange(e.target.value), children: options.map(([v, l]) => /* @__PURE__ */ jsx("option", { value: v, children: l }, v)) })
227
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label }),
228
+ /* @__PURE__ */ jsx("select", { style: input, value, onChange: (e) => onChange(e.target.value), children: options.map(([v, l]) => /* @__PURE__ */ jsx("option", { value: v, children: l }, v)) })
599
229
  ] });
600
230
  }
601
231
  var len = (value) => ({ value, unit: "px" });
@@ -622,7 +252,7 @@ var headRow = { display: "flex", alignItems: "center", gap: 8, padding: "10px 12
622
252
  var body = { padding: 12, overflowY: "auto" };
623
253
  var field = { display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 0 };
624
254
  var fieldLabel = { fontSize: 10, opacity: 0.6 };
625
- var input2 = {
255
+ var input = {
626
256
  width: "100%",
627
257
  boxSizing: "border-box",
628
258
  background: "#0f172a",
@@ -740,9 +370,11 @@ function snapshotSubtree(doc, id) {
740
370
  return { rootId: id, nodes };
741
371
  }
742
372
  var Z2 = 2147483e3;
373
+ var SNAP = 6;
743
374
  function EditablePage({
744
375
  doc,
745
376
  registry = defaultRegistry,
377
+ data,
746
378
  timelineDock,
747
379
  transforms,
748
380
  onNodeTransform,
@@ -759,7 +391,8 @@ function EditablePage({
759
391
  const [box, setBox] = useState(null);
760
392
  const { selection } = ed.state;
761
393
  const selNode = selection ? ed.state.doc.nodes[selection] : null;
762
- useEffect(() => onSelect?.(selection), [selection, onSelect]);
394
+ const onSelectRef = useLatestRef(onSelect);
395
+ useEffect(() => onSelectRef.current?.(selection), [selection, onSelectRef]);
763
396
  useEffect(() => {
764
397
  const onKey = (e) => setRevealed(e.ctrlKey && e.shiftKey);
765
398
  const onBlur = () => setRevealed(false);
@@ -781,13 +414,15 @@ function EditablePage({
781
414
  applyTransform(el, t);
782
415
  });
783
416
  }, [transforms, ed.state.doc]);
417
+ const onProgressRef = useLatestRef(onProgress);
418
+ const hasProgress = !!onProgress;
784
419
  useEffect(() => {
785
420
  const spacer = spacerRef.current;
786
- if (!spacer || !onProgress) return;
421
+ if (!spacer || !hasProgress) return;
787
422
  const compute = () => {
788
423
  const len2 = spacer.offsetHeight - window.innerHeight;
789
424
  const top = spacer.getBoundingClientRect().top;
790
- onProgress(len2 > 0 ? Math.min(1, Math.max(0, -top / len2)) : 0);
425
+ onProgressRef.current?.(len2 > 0 ? Math.min(1, Math.max(0, -top / len2)) : 0);
791
426
  };
792
427
  compute();
793
428
  window.addEventListener("scroll", compute, { passive: true });
@@ -796,10 +431,10 @@ function EditablePage({
796
431
  window.removeEventListener("scroll", compute);
797
432
  window.removeEventListener("resize", compute);
798
433
  };
799
- }, [onProgress, frames]);
434
+ }, [hasProgress, onProgressRef, frames]);
800
435
  const measureBox = useCallback(() => {
801
436
  if (!editing || !selection || !pageRef.current) return setBox(null);
802
- const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
437
+ const el = pageRef.current.querySelector(`[data-cms="${cssEscape(selection)}"]`);
803
438
  if (!el) return setBox(null);
804
439
  const r = el.getBoundingClientRect();
805
440
  setBox({ x: r.left, y: r.top, w: r.width, h: r.height });
@@ -814,13 +449,17 @@ function EditablePage({
814
449
  };
815
450
  }, [measureBox, ed.state.doc]);
816
451
  useEffect(() => {
817
- if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
818
- const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
819
- if (!el) return;
452
+ const t = selNode?.type;
453
+ const editable = t === "text" || t === "heading" || t === "richtext";
454
+ if (!editing || !selection || !pageRef.current || !editable) return;
455
+ const wrapper = pageRef.current.querySelector(`[data-cms="${cssEscape(selection)}"]`);
456
+ if (!wrapper) return;
457
+ const isRich = t === "richtext";
458
+ const el = (isRich ? wrapper.firstElementChild : wrapper) ?? wrapper;
820
459
  el.setAttribute("contenteditable", "true");
821
460
  el.style.outline = "none";
822
461
  el.focus();
823
- const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
462
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: isRich ? { html: el.innerHTML } : { content: el.innerText } });
824
463
  el.addEventListener("blur", commit);
825
464
  return () => {
826
465
  el.removeEventListener("blur", commit);
@@ -845,11 +484,84 @@ function EditablePage({
845
484
  const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
846
485
  const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
847
486
  const def = ADD_DEFAULTS[kind];
848
- ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
487
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
849
488
  ed.select(id);
850
489
  },
851
490
  [ed, selNode]
852
491
  );
492
+ useCallback(
493
+ (kind, targetId) => {
494
+ const doc2 = ed.state.doc;
495
+ const target = targetId ? doc2.nodes[targetId] : null;
496
+ const parent = target ? CONTAINER_TYPES2.has(target.type) ? target.id : target.parent : doc2.sections[doc2.sections.length - 1] ?? null;
497
+ const siblings = childrenOf(doc2, parent);
498
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
499
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
500
+ const def = ADD_DEFAULTS[kind];
501
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
502
+ ed.select(id);
503
+ },
504
+ [ed]
505
+ );
506
+ const [paletteOpen, setPaletteOpen] = useState(false);
507
+ const [draggingEl, setDraggingEl] = useState(false);
508
+ useEffect(() => {
509
+ if (!editing) {
510
+ setPaletteOpen(false);
511
+ return;
512
+ }
513
+ const onMove = (e) => {
514
+ if (draggingEl) return;
515
+ if (e.clientX >= window.innerWidth - 26) setPaletteOpen(true);
516
+ else if (e.clientX < window.innerWidth - 236) setPaletteOpen(false);
517
+ };
518
+ window.addEventListener("mousemove", onMove);
519
+ return () => window.removeEventListener("mousemove", onMove);
520
+ }, [editing, draggingEl]);
521
+ const [dropHint, setDropHint] = useState(null);
522
+ const computeDropTarget = (clientX, clientY) => {
523
+ const el = document.elementFromPoint(clientX, clientY)?.closest("[data-cms]");
524
+ if (!el) return { id: null, edge: "inside", rect: null };
525
+ const id = el.getAttribute("data-cms");
526
+ const node = ed.state.doc.nodes[id];
527
+ const r = el.getBoundingClientRect();
528
+ const rect = { x: r.left, y: r.top, w: r.width, h: r.height };
529
+ if (node && CONTAINER_TYPES2.has(node.type)) return { id, edge: "inside", rect };
530
+ return { id, edge: clientY < r.top + r.height / 2 ? "before" : "after", rect };
531
+ };
532
+ const onPageDragOver = (e) => {
533
+ e.preventDefault();
534
+ e.dataTransfer.dropEffect = "copy";
535
+ setDropHint(computeDropTarget(e.clientX, e.clientY));
536
+ };
537
+ const insertRelative = (kind, hint) => {
538
+ const doc2 = ed.state.doc;
539
+ let parent;
540
+ let order;
541
+ if (!hint.id || hint.edge === "inside") {
542
+ parent = hint.id ?? (doc2.sections[doc2.sections.length - 1] ?? null);
543
+ const sib = childrenOf(doc2, parent);
544
+ order = keyBetween(sib.length ? sib[sib.length - 1].order : null, null);
545
+ } else {
546
+ const target = doc2.nodes[hint.id];
547
+ parent = target.parent;
548
+ const sib = childrenOf(doc2, parent);
549
+ const idx = sib.findIndex((n) => n.id === hint.id);
550
+ 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);
551
+ }
552
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
553
+ const def = ADD_DEFAULTS[kind];
554
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
555
+ ed.select(id);
556
+ };
557
+ const onPageDrop = (e) => {
558
+ const kind = e.dataTransfer.getData("application/x-cms-element");
559
+ if (!kind) return;
560
+ e.preventDefault();
561
+ insertRelative(kind, computeDropTarget(e.clientX, e.clientY));
562
+ setDropHint(null);
563
+ setDraggingEl(false);
564
+ };
853
565
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
854
566
  const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
855
567
  const setLayout = (layout) => selection && ed.apply({ t: "set_layout", id: selection, layout });
@@ -875,7 +587,7 @@ function EditablePage({
875
587
  const startEditText = (id) => {
876
588
  if (!id) return;
877
589
  ed.select(id);
878
- requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
590
+ requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape(id)}"]`)?.focus());
879
591
  };
880
592
  const nudgeRef = useRef({ transforms, onNodeTransform });
881
593
  nudgeRef.current = { transforms, onNodeTransform };
@@ -917,8 +629,23 @@ function EditablePage({
917
629
  if (el) ed.select(el.getAttribute("data-cms"));
918
630
  else ed.select(null);
919
631
  };
920
- const page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
921
- 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 }) }) : page;
632
+ const page = /* @__PURE__ */ jsx(
633
+ "div",
634
+ {
635
+ ref: pageRef,
636
+ onClickCapture,
637
+ onContextMenu: onContextMenuSelect,
638
+ onDragOver: editing ? onPageDragOver : void 0,
639
+ onDragLeave: editing ? () => setDropHint(null) : void 0,
640
+ onDrop: editing ? onPageDrop : void 0,
641
+ children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry, data })
642
+ }
643
+ );
644
+ 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 ? (
645
+ // Normal flow, but "expanded" — extra scroll length below the content so a
646
+ // page can be made longer than its content (the + Frame control).
647
+ /* @__PURE__ */ jsx("div", { style: { minHeight: `${frames * 100}vh` }, children: page })
648
+ ) : page;
922
649
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
923
650
  editing ? /* @__PURE__ */ jsxs(ContextMenu, { children: [
924
651
  /* @__PURE__ */ jsx(ContextMenu.Trigger, { children: pageShell }),
@@ -945,7 +672,7 @@ function EditablePage({
945
672
  box,
946
673
  base: transforms?.[selection] ?? {},
947
674
  movable: Boolean(onNodeTransform),
948
- getEl: () => pageRef.current?.querySelector(`[data-cms="${cssEscape2(selection)}"]`) ?? null,
675
+ getEl: () => pageRef.current?.querySelector(`[data-cms="${cssEscape(selection)}"]`) ?? null,
949
676
  onTransform: (t) => onNodeTransform?.(selection, t),
950
677
  onLive: measureBox
951
678
  }
@@ -980,16 +707,101 @@ function EditablePage({
980
707
  onAdd: addNode
981
708
  }
982
709
  ) : null,
710
+ editing && dropHint?.rect ? /* @__PURE__ */ jsx(DropIndicator, { hint: dropHint }) : null,
711
+ editing ? /* @__PURE__ */ jsx(ElementPalette, { open: paletteOpen, onDragChange: setDraggingEl }) : null,
983
712
  editing && timelineDock ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
984
713
  ] });
985
714
  }
986
- var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
715
+ function DropIndicator({
716
+ hint
717
+ }) {
718
+ const r = hint.rect;
719
+ if (!r) return null;
720
+ const color = "#ec4899";
721
+ if (hint.edge === "inside") {
722
+ 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 } });
723
+ }
724
+ const y = hint.edge === "before" ? r.y : r.y + r.h;
725
+ 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 } });
726
+ }
727
+ function ElementPalette({ open, onDragChange }) {
728
+ return /* @__PURE__ */ jsxs(
729
+ "div",
730
+ {
731
+ style: {
732
+ position: "fixed",
733
+ top: 64,
734
+ bottom: 96,
735
+ right: 0,
736
+ width: 216,
737
+ transform: open ? "translateX(0)" : "translateX(100%)",
738
+ transition: "transform 0.18s ease",
739
+ background: "#0b1220",
740
+ color: "#e2e8f0",
741
+ borderLeft: "1px solid #1e293b",
742
+ borderTopLeftRadius: 12,
743
+ borderBottomLeftRadius: 12,
744
+ boxShadow: "-18px 0 48px -20px rgba(0,0,0,0.6)",
745
+ zIndex: Z2 + 2,
746
+ fontFamily: "system-ui, sans-serif",
747
+ padding: 12,
748
+ overflowY: "auto"
749
+ },
750
+ children: [
751
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5, marginBottom: 10 }, children: "Elements" }),
752
+ /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }, children: ADD_MENU.map((m) => /* @__PURE__ */ jsxs(
753
+ "div",
754
+ {
755
+ draggable: true,
756
+ onDragStart: (e) => {
757
+ e.dataTransfer.setData("application/x-cms-element", m.kind);
758
+ e.dataTransfer.effectAllowed = "copy";
759
+ onDragChange(true);
760
+ },
761
+ onDragEnd: () => onDragChange(false),
762
+ title: `Drag "${m.label}" onto the page`,
763
+ style: {
764
+ display: "flex",
765
+ flexDirection: "column",
766
+ alignItems: "center",
767
+ justifyContent: "center",
768
+ gap: 4,
769
+ height: 56,
770
+ borderRadius: 8,
771
+ background: "#0f172a",
772
+ border: "1px solid #1e293b",
773
+ cursor: "grab",
774
+ fontSize: 12,
775
+ userSelect: "none"
776
+ },
777
+ children: [
778
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 16, opacity: 0.7 }, children: "\uFF0B" }),
779
+ m.label
780
+ ]
781
+ },
782
+ m.kind
783
+ )) }),
784
+ /* @__PURE__ */ 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." })
785
+ ]
786
+ }
787
+ );
788
+ }
789
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device", "repeater"]);
987
790
  var ADD_DEFAULTS = {
988
791
  text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
989
792
  heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
990
793
  button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
794
+ link: { type: "link", props: { content: "link text", href: "#" }, style: { color: "#7c3aed" } },
991
795
  image: { type: "image", props: { src: "", alt: "" }, style: {} },
992
- box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } }
796
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } },
797
+ stack: { type: "stack", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
798
+ grid: { type: "grid", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "grid" },
799
+ card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
800
+ callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
801
+ divider: { type: "divider", props: {}, style: {} },
802
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
803
+ richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
804
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
993
805
  };
994
806
  var GRIPS = [
995
807
  { key: "nw", north: true, west: true, cursor: "nwse-resize" },
@@ -1010,20 +822,55 @@ function SelectionOverlay({
1010
822
  onLive
1011
823
  }) {
1012
824
  const drag = useRef(null);
825
+ const targets = useRef({ vx: [], hy: [] });
826
+ const [guides, setGuides] = useState({ x: null, y: null });
1013
827
  const apply = (t) => {
1014
828
  const el = getEl();
1015
829
  if (el) applyTransform(el, t);
1016
830
  };
831
+ const snap = (cands, lines) => {
832
+ let best = { delta: 0, at: null, dist: SNAP + 1 };
833
+ for (const line of lines) {
834
+ for (const c of cands) {
835
+ const diff = line - c;
836
+ if (Math.abs(diff) < best.dist) best = { delta: diff, at: line, dist: Math.abs(diff) };
837
+ }
838
+ }
839
+ return best.dist <= SNAP ? { delta: best.delta, at: best.at } : { delta: 0, at: null };
840
+ };
1017
841
  const startMove = (e) => {
1018
842
  e.stopPropagation();
1019
843
  e.preventDefault();
1020
844
  e.currentTarget.setPointerCapture(e.pointerId);
1021
845
  drag.current = { x: e.clientX, y: e.clientY, w: box.w, h: box.h, live: { ...base } };
846
+ const sel = getEl();
847
+ const vx = /* @__PURE__ */ new Set();
848
+ const hy = /* @__PURE__ */ new Set();
849
+ document.querySelectorAll("[data-cms]").forEach((el) => {
850
+ if (sel && (el === sel || sel.contains(el) || el.contains(sel))) return;
851
+ const r = el.getBoundingClientRect();
852
+ vx.add(r.left).add(r.left + r.width / 2).add(r.right);
853
+ hy.add(r.top).add(r.top + r.height / 2).add(r.bottom);
854
+ });
855
+ targets.current = { vx: [...vx], hy: [...hy] };
1022
856
  };
1023
857
  const moveMove = (e) => {
1024
858
  const d = drag.current;
1025
859
  if (!d) return;
1026
- d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
860
+ let dx = e.clientX - d.x;
861
+ let dy = e.clientY - d.y;
862
+ if (!e.altKey) {
863
+ const l = box.x + dx;
864
+ const t = box.y + dy;
865
+ const sx = snap([l, l + box.w / 2, l + box.w], targets.current.vx);
866
+ const sy = snap([t, t + box.h / 2, t + box.h], targets.current.hy);
867
+ dx += sx.delta;
868
+ dy += sy.delta;
869
+ setGuides({ x: sx.at, y: sy.at });
870
+ } else if (guides.x !== null || guides.y !== null) {
871
+ setGuides({ x: null, y: null });
872
+ }
873
+ d.live = { ...base, x: (base.x ?? 0) + dx, y: (base.y ?? 0) + dy };
1027
874
  apply(d.live);
1028
875
  onLive();
1029
876
  };
@@ -1058,6 +905,7 @@ function SelectionOverlay({
1058
905
  const d = drag.current;
1059
906
  if (d) onTransform(d.live);
1060
907
  drag.current = null;
908
+ setGuides({ x: null, y: null });
1061
909
  };
1062
910
  const outline = {
1063
911
  position: "fixed",
@@ -1086,9 +934,11 @@ function SelectionOverlay({
1086
934
  const cy = g.north ? box.y : g.south ? box.y + box.h : box.y + box.h / 2;
1087
935
  return { left: cx - 6, top: cy - 6, cursor: g.cursor };
1088
936
  };
1089
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
937
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
938
+ guides.x !== null ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: guides.x, top: 0, bottom: 0, width: 1, background: "#ec4899", zIndex: Z2 + 3, pointerEvents: "none" } }) : null,
939
+ guides.y !== null ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", top: guides.y, left: 0, right: 0, height: 1, background: "#ec4899", zIndex: Z2 + 3, pointerEvents: "none" } }) : null,
1090
940
  /* @__PURE__ */ jsx("div", { style: outline }),
1091
- movable ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
941
+ movable ? /* @__PURE__ */ jsxs(Fragment, { children: [
1092
942
  /* @__PURE__ */ jsx(
1093
943
  "div",
1094
944
  {
@@ -1151,11 +1001,20 @@ function FloatingToolbar({
1151
1001
  ] });
1152
1002
  }
1153
1003
  var ADD_MENU = [
1154
- { kind: "text", label: "Text" },
1155
1004
  { kind: "heading", label: "Heading" },
1005
+ { kind: "text", label: "Text" },
1156
1006
  { kind: "button", label: "Button" },
1007
+ { kind: "link", label: "Link" },
1157
1008
  { kind: "image", label: "Image" },
1158
- { kind: "box", label: "Box" }
1009
+ { kind: "card", label: "Card" },
1010
+ { kind: "callout", label: "Callout" },
1011
+ { kind: "stack", label: "Stack" },
1012
+ { kind: "grid", label: "Grid" },
1013
+ { kind: "box", label: "Box" },
1014
+ { kind: "divider", label: "Divider" },
1015
+ { kind: "code", label: "Code" },
1016
+ { kind: "richtext", label: "Rich text" },
1017
+ { kind: "repeater", label: "Repeater" }
1159
1018
  ];
1160
1019
  function EditBar({
1161
1020
  editing,
@@ -1171,9 +1030,9 @@ function EditBar({
1171
1030
  const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
1172
1031
  return /* @__PURE__ */ jsxs("div", { style: bar, children: [
1173
1032
  /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
1174
- editing ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1033
+ editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
1175
1034
  /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1176
- /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an element", children: "\uFF0B Add \u25BE" }),
1035
+ /* @__PURE__ */ 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" }),
1177
1036
  open ? /* @__PURE__ */ 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__ */ jsx(
1178
1037
  "button",
1179
1038
  {
@@ -1208,13 +1067,13 @@ function EditMenuItems({
1208
1067
  }) {
1209
1068
  const editable = node && (node.type === "text" || node.type === "heading");
1210
1069
  const nested = Boolean(node && node.parent !== null);
1211
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1070
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1212
1071
  editable ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1213
1072
  /* @__PURE__ */ jsxs(ContextMenu.Sub, { children: [
1214
- /* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1073
+ /* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add Element inside" : "Add Element" }),
1215
1074
  /* @__PURE__ */ jsx(ContextMenu.SubContent, { className: "!z-[2147483647]", children: ADD_MENU.map((m) => /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onAdd(m.kind), children: m.label }, m.kind)) })
1216
1075
  ] }),
1217
- node ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1076
+ node ? /* @__PURE__ */ jsxs(Fragment, { children: [
1218
1077
  /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1219
1078
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
1220
1079
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
@@ -1223,7 +1082,7 @@ function EditMenuItems({
1223
1082
  /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1224
1083
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
1225
1084
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
1226
- nested ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1085
+ nested ? /* @__PURE__ */ jsxs(Fragment, { children: [
1227
1086
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
1228
1087
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
1229
1088
  ] }) : null,
@@ -1248,10 +1107,10 @@ function applyTransform(el, t) {
1248
1107
  function normalizeColor(v) {
1249
1108
  return v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : "#ffffff";
1250
1109
  }
1251
- function cssEscape2(value) {
1110
+ function cssEscape(value) {
1252
1111
  return value.replace(/["\\]/g, "\\$&");
1253
1112
  }
1254
1113
 
1255
- export { Canvas, EditablePage, Editor, Inspector, LayersPanel, NodeInspector, editorReduce, initEditor, useEditor };
1114
+ export { EditablePage, NodeInspector };
1256
1115
  //# sourceMappingURL=editor.js.map
1257
1116
  //# sourceMappingURL=editor.js.map