@particle-academy/fancy-cms-ui 0.0.1-dev.8 → 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.js CHANGED
@@ -1,386 +1,27 @@
1
- import { keyBetween } from './chunk-7YYLYBLI.js';
2
- import { CmsPage, defaultRegistry } from './chunk-5O6U4VIU.js';
3
- import { reduce, childrenOf } from './chunk-HYHNMW47.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, keyBetween } from './chunk-N22BQAY3.js';
2
+ export { Canvas, Editor, Inspector, LayersPanel, editorReduce, initEditor, useEditor } from './chunk-N22BQAY3.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
- function LayersPanel({ doc, selection, onSelect }) {
161
- 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 }, id)) });
162
- }
163
- function LayerRow({ doc, id, depth, selection, onSelect }) {
164
- const node = doc.nodes[id];
165
- if (!node) return null;
166
- const selected = selection === id;
167
- return /* @__PURE__ */ jsxs(Fragment, { children: [
168
- /* @__PURE__ */ jsxs(
169
- "button",
170
- {
171
- type: "button",
172
- onClick: () => onSelect(id),
173
- style: {
174
- display: "block",
175
- width: "100%",
176
- textAlign: "left",
177
- border: "none",
178
- borderRadius: 6,
179
- cursor: "pointer",
180
- padding: "4px 8px",
181
- paddingLeft: 8 + depth * 14,
182
- background: selected ? "#ede9fe" : "transparent",
183
- color: selected ? "#5b21b6" : "#334155",
184
- font: "inherit"
185
- },
186
- children: [
187
- /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: node.type }),
188
- " \xB7 ",
189
- id
190
- ]
191
- }
192
- ),
193
- childrenOf(doc, id).map((child) => /* @__PURE__ */ jsx(
194
- LayerRow,
195
- {
196
- doc,
197
- id: child.id,
198
- depth: depth + 1,
199
- selection,
200
- onSelect
201
- },
202
- child.id
203
- ))
204
- ] });
205
- }
206
- var label = {
207
- display: "block",
208
- fontSize: 11,
209
- textTransform: "uppercase",
210
- letterSpacing: "0.04em",
211
- color: "#94a3b8",
212
- margin: "10px 0 4px"
213
- };
214
- var input = {
215
- width: "100%",
216
- boxSizing: "border-box",
217
- padding: "6px 8px",
218
- borderRadius: 6,
219
- border: "1px solid #e2e8f0",
220
- font: "inherit",
221
- fontSize: 13
222
- };
223
- function Inspector({ doc, selection, apply }) {
224
- const node = selection ? doc.nodes[selection] : null;
225
- if (!node) {
226
- return /* @__PURE__ */ jsx("div", { style: { padding: 16, color: "#94a3b8", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
227
- }
228
- const s = node.style.base;
229
- const setStyle = (patch) => apply({ t: "set_style", id: node.id, breakpoint: "base", patch });
230
- const setProp = (key, value) => apply({ t: "set_props", id: node.id, patch: { [key]: value } });
231
- return /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", padding: 16, fontFamily: "system-ui, sans-serif", height: "100%", boxSizing: "border-box" }, children: [
232
- /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a" }, children: node.type }),
233
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#94a3b8", marginBottom: 8 }, children: node.id }),
234
- node.type === "text" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
235
- /* @__PURE__ */ jsx("label", { style: label, children: "Content" }),
236
- /* @__PURE__ */ jsx(
237
- "textarea",
238
- {
239
- style: { ...input, minHeight: 64, resize: "vertical" },
240
- value: typeof node.props.content === "string" ? node.props.content : "",
241
- onChange: (e) => setProp("content", e.target.value)
242
- }
243
- )
244
- ] }) : null,
245
- node.type === "image" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
246
- /* @__PURE__ */ jsx("label", { style: label, children: "Source" }),
247
- /* @__PURE__ */ jsx(
248
- "input",
249
- {
250
- style: input,
251
- value: typeof node.props.src === "string" ? node.props.src : "",
252
- onChange: (e) => setProp("src", e.target.value)
253
- }
254
- )
255
- ] }) : null,
256
- /* @__PURE__ */ jsx("label", { style: label, children: "Background" }),
257
- /* @__PURE__ */ jsx(
258
- "input",
259
- {
260
- style: input,
261
- placeholder: "#ffffff | gradient | url(\u2026)",
262
- value: s.background ?? "",
263
- onChange: (e) => setStyle({ background: e.target.value })
264
- }
265
- ),
266
- /* @__PURE__ */ jsx("label", { style: label, children: "Text color" }),
267
- /* @__PURE__ */ jsx(
268
- "input",
269
- {
270
- style: input,
271
- placeholder: "#0f172a",
272
- value: s.color ?? "",
273
- onChange: (e) => setStyle({ color: e.target.value })
274
- }
275
- ),
276
- /* @__PURE__ */ jsx("label", { style: label, children: "Font size (px)" }),
277
- /* @__PURE__ */ jsx(
278
- "input",
279
- {
280
- type: "number",
281
- style: input,
282
- value: s.fontSize?.value ?? "",
283
- onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
284
- }
285
- ),
286
- /* @__PURE__ */ jsx("label", { style: label, children: "Padding (px)" }),
287
- /* @__PURE__ */ jsx(
288
- "input",
289
- {
290
- type: "number",
291
- style: input,
292
- value: s.padding?.value ?? "",
293
- onChange: (e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: "px" } })
294
- }
295
- ),
296
- /* @__PURE__ */ jsx("label", { style: label, children: "Opacity" }),
297
- /* @__PURE__ */ jsx(
298
- "input",
299
- {
300
- type: "number",
301
- step: "0.1",
302
- min: "0",
303
- max: "1",
304
- style: input,
305
- value: s.opacity ?? "",
306
- onChange: (e) => setStyle({ opacity: Number(e.target.value) })
307
- }
308
- )
309
- ] });
310
- }
311
- function Editor({ defaultValue, onChange }) {
312
- const ed = useEditor(defaultValue);
313
- const first = useRef(true);
314
- useEffect(() => {
315
- if (first.current) {
316
- first.current = false;
317
- return;
318
- }
319
- onChange?.(ed.state.doc);
320
- }, [ed.state.doc, onChange]);
321
- const shell = {
322
- display: "grid",
323
- gridTemplateColumns: "220px 1fr 300px",
324
- height: "100%",
325
- minHeight: 0,
326
- background: "#fff",
327
- color: "#0f172a",
328
- border: "1px solid #e2e8f0",
329
- borderRadius: 12,
330
- overflow: "hidden"
331
- };
332
- return /* @__PURE__ */ jsxs("div", { style: shell, children: [
333
- /* @__PURE__ */ jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
334
- /* @__PURE__ */ jsx(Toolbar, { ed }),
335
- /* @__PURE__ */ jsx(LayersPanel, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select })
336
- ] }),
337
- /* @__PURE__ */ jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
338
- /* @__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 }) })
339
- ] });
340
- }
341
- function Toolbar({ ed }) {
342
- const btn = {
343
- font: "inherit",
344
- fontSize: 12,
345
- padding: "4px 10px",
346
- borderRadius: 6,
347
- border: "1px solid #e2e8f0",
348
- background: "#fff",
349
- cursor: "pointer"
350
- };
351
- return /* @__PURE__ */ jsxs(
352
- "div",
353
- {
354
- style: {
355
- display: "flex",
356
- gap: 6,
357
- padding: 8,
358
- borderBottom: "1px solid #e2e8f0",
359
- fontFamily: "system-ui, sans-serif"
360
- },
361
- children: [
362
- /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: ed.canUndo ? 1 : 0.4 }, disabled: !ed.canUndo, onClick: ed.undo, children: "Undo" }),
363
- /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" })
364
- ]
365
- }
366
- );
367
- }
368
- 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"]);
369
9
  function NodeInspector({
370
10
  node,
371
11
  transform,
372
12
  measured,
373
13
  onProps,
374
14
  onStyle,
15
+ onLayout,
375
16
  onTransform,
376
17
  onRemove,
377
18
  onClose
378
19
  }) {
379
20
  const base = node.style.base ?? {};
380
21
  const setT = (patch) => onTransform({ ...transform, ...patch });
381
- const handled = /* @__PURE__ */ new Set(["content", "label"]);
22
+ const handled = /* @__PURE__ */ new Set(["content", "label", "html"]);
382
23
  const extraKeys = Object.keys(node.props).filter(
383
- (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]))
384
25
  );
385
26
  return /* @__PURE__ */ jsxs("div", { style: panel, onPointerDown: (e) => e.stopPropagation(), children: [
386
27
  /* @__PURE__ */ jsxs("div", { style: headRow, children: [
@@ -393,15 +34,12 @@ function NodeInspector({
393
34
  /* @__PURE__ */ jsx("button", { type: "button", style: iconBtn, title: "Close", onClick: onClose, children: "\u2715" })
394
35
  ] }),
395
36
  /* @__PURE__ */ jsxs("div", { style: body, children: [
396
- "content" in node.props ? /* @__PURE__ */ jsx(Group, { label: "Content", children: /* @__PURE__ */ jsx(
397
- "textarea",
398
- {
399
- style: { ...input2, minHeight: 64, resize: "vertical", lineHeight: 1.4 },
400
- value: String(node.props.content ?? ""),
401
- onChange: (e) => onProps({ content: e.target.value })
402
- }
403
- ) }) : null,
404
- "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,
405
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,
406
44
  /* @__PURE__ */ jsxs(Group, { label: "Position & size", children: [
407
45
  /* @__PURE__ */ jsxs(Row, { children: [
@@ -454,15 +92,50 @@ function NodeInspector({
454
92
  ] }),
455
93
  /* @__PURE__ */ jsxs(Row, { children: [
456
94
  /* @__PURE__ */ jsx(Num, { label: "Padding", value: base.padding?.value, placeholder: "0", onChange: (v) => onStyle({ padding: v == null ? void 0 : len(v) }) }),
95
+ /* @__PURE__ */ jsx(Num, { label: "Margin", value: base.margin?.value, placeholder: "0", onChange: (v) => onStyle({ margin: v == null ? void 0 : len(v) }) })
96
+ ] }),
97
+ /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Txt, { label: "Border", value: base.border ?? "", placeholder: "1px solid #e2e8f0", onChange: (v) => onStyle({ border: v || void 0 }) }) })
98
+ ] }),
99
+ CONTAINER_TYPES.has(node.type) ? /* @__PURE__ */ jsxs(Group, { label: "Layout", children: [
100
+ /* @__PURE__ */ jsxs(Row, { children: [
101
+ /* @__PURE__ */ jsx(
102
+ Sel,
103
+ {
104
+ label: "Mode",
105
+ value: node.layout ?? "free",
106
+ options: [["free", "Free"], ["stack", "Stack"], ["grid", "Grid"]],
107
+ onChange: (v) => onLayout?.(v)
108
+ }
109
+ ),
457
110
  /* @__PURE__ */ jsx(Num, { label: "Gap", value: base.gap?.value, placeholder: "0", onChange: (v) => onStyle({ gap: v == null ? void 0 : len(v) }) })
111
+ ] }),
112
+ /* @__PURE__ */ jsxs(Row, { children: [
113
+ /* @__PURE__ */ jsx(
114
+ Sel,
115
+ {
116
+ label: "Align",
117
+ value: base.align ?? "",
118
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["stretch", "Stretch"]],
119
+ onChange: (v) => onStyle({ align: v || void 0 })
120
+ }
121
+ ),
122
+ /* @__PURE__ */ jsx(
123
+ Sel,
124
+ {
125
+ label: "Justify",
126
+ value: base.justify ?? "",
127
+ options: [["", "\u2014"], ["start", "Start"], ["center", "Center"], ["end", "End"], ["between", "Between"], ["around", "Around"]],
128
+ onChange: (v) => onStyle({ justify: v || void 0 })
129
+ }
130
+ )
458
131
  ] })
459
- ] })
132
+ ] }) : null
460
133
  ] })
461
134
  ] });
462
135
  }
463
- function Group({ label: label2, children }) {
136
+ function Group({ label, children }) {
464
137
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, paddingBottom: 12, marginBottom: 12, borderBottom: "1px solid #1e293b" }, children: [
465
- /* @__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 }),
466
139
  children
467
140
  ] });
468
141
  }
@@ -470,36 +143,62 @@ function Row({ children }) {
470
143
  return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8 }, children });
471
144
  }
472
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
+ }
473
167
  if (typeof value === "boolean") {
474
168
  return /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
475
169
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked) }),
476
- /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize" }, children: name })
170
+ /* @__PURE__ */ jsx("span", { style: { textTransform: "capitalize", flex: 1 }, children: name }),
171
+ link
477
172
  ] });
478
173
  }
479
174
  if (name === "variant") {
480
- 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 });
481
176
  }
482
177
  const isNum = typeof value === "number";
483
178
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
484
- /* @__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
+ ] }),
485
184
  /* @__PURE__ */ jsx(
486
185
  "input",
487
186
  {
488
- style: input2,
187
+ style: input,
489
188
  type: isNum ? "number" : "text",
490
- value: String(value),
189
+ value: String(value ?? ""),
491
190
  onChange: (e) => onChange(isNum ? Number(e.target.value) : e.target.value)
492
191
  }
493
192
  )
494
193
  ] });
495
194
  }
496
- function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
195
+ function Num({ label, value, placeholder, onChange, step, min, max }) {
497
196
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
498
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
197
+ /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label }),
499
198
  /* @__PURE__ */ jsx(
500
199
  "input",
501
200
  {
502
- style: input2,
201
+ style: input,
503
202
  type: "number",
504
203
  step,
505
204
  min,
@@ -511,16 +210,22 @@ function Num({ label: label2, value, placeholder, onChange, step, min, max }) {
511
210
  )
512
211
  ] });
513
212
  }
514
- function Color({ label: label2, value, onChange }) {
213
+ function Txt({ label, value, placeholder, onChange }) {
515
214
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
516
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
517
- /* @__PURE__ */ jsx("input", { style: { ...input2, padding: 2, height: 30 }, type: "color", value, 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) })
518
217
  ] });
519
218
  }
520
- function Sel({ label: label2, value, options, onChange }) {
219
+ function Color({ label, value, onChange }) {
521
220
  return /* @__PURE__ */ jsxs("label", { style: field, children: [
522
- /* @__PURE__ */ jsx("span", { style: fieldLabel, children: label2 }),
523
- /* @__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)) })
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) })
223
+ ] });
224
+ }
225
+ function Sel({ label, value, options, onChange }) {
226
+ return /* @__PURE__ */ jsxs("label", { style: field, children: [
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)) })
524
229
  ] });
525
230
  }
526
231
  var len = (value) => ({ value, unit: "px" });
@@ -547,7 +252,7 @@ var headRow = { display: "flex", alignItems: "center", gap: 8, padding: "10px 12
547
252
  var body = { padding: 12, overflowY: "auto" };
548
253
  var field = { display: "flex", flexDirection: "column", gap: 4, flex: 1, minWidth: 0 };
549
254
  var fieldLabel = { fontSize: 10, opacity: 0.6 };
550
- var input2 = {
255
+ var input = {
551
256
  width: "100%",
552
257
  boxSizing: "border-box",
553
258
  background: "#0f172a",
@@ -665,9 +370,11 @@ function snapshotSubtree(doc, id) {
665
370
  return { rootId: id, nodes };
666
371
  }
667
372
  var Z2 = 2147483e3;
373
+ var SNAP = 6;
668
374
  function EditablePage({
669
375
  doc,
670
376
  registry = defaultRegistry,
377
+ data,
671
378
  timelineDock,
672
379
  transforms,
673
380
  onNodeTransform,
@@ -724,7 +431,7 @@ function EditablePage({
724
431
  }, [onProgress, frames]);
725
432
  const measureBox = useCallback(() => {
726
433
  if (!editing || !selection || !pageRef.current) return setBox(null);
727
- const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
434
+ const el = pageRef.current.querySelector(`[data-cms="${cssEscape(selection)}"]`);
728
435
  if (!el) return setBox(null);
729
436
  const r = el.getBoundingClientRect();
730
437
  setBox({ x: r.left, y: r.top, w: r.width, h: r.height });
@@ -739,13 +446,17 @@ function EditablePage({
739
446
  };
740
447
  }, [measureBox, ed.state.doc]);
741
448
  useEffect(() => {
742
- if (!editing || !selection || !pageRef.current || selNode?.type !== "text") return;
743
- const el = pageRef.current.querySelector(`[data-cms="${cssEscape2(selection)}"]`);
744
- if (!el) return;
449
+ const t = selNode?.type;
450
+ const editable = t === "text" || t === "heading" || t === "richtext";
451
+ if (!editing || !selection || !pageRef.current || !editable) return;
452
+ const wrapper = pageRef.current.querySelector(`[data-cms="${cssEscape(selection)}"]`);
453
+ if (!wrapper) return;
454
+ const isRich = t === "richtext";
455
+ const el = (isRich ? wrapper.firstElementChild : wrapper) ?? wrapper;
745
456
  el.setAttribute("contenteditable", "true");
746
457
  el.style.outline = "none";
747
458
  el.focus();
748
- const commit = () => ed.apply({ t: "set_props", id: selection, patch: { content: el.innerText } });
459
+ const commit = () => ed.apply({ t: "set_props", id: selection, patch: isRich ? { html: el.innerHTML } : { content: el.innerText } });
749
460
  el.addEventListener("blur", commit);
750
461
  return () => {
751
462
  el.removeEventListener("blur", commit);
@@ -770,13 +481,87 @@ function EditablePage({
770
481
  const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
771
482
  const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
772
483
  const def = ADD_DEFAULTS[kind];
773
- ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, props: { ...def.props }, style: { base: { ...def.style } } } });
484
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
774
485
  ed.select(id);
775
486
  },
776
487
  [ed, selNode]
777
488
  );
489
+ useCallback(
490
+ (kind, targetId) => {
491
+ const doc2 = ed.state.doc;
492
+ const target = targetId ? doc2.nodes[targetId] : null;
493
+ const parent = target ? CONTAINER_TYPES2.has(target.type) ? target.id : target.parent : doc2.sections[doc2.sections.length - 1] ?? null;
494
+ const siblings = childrenOf(doc2, parent);
495
+ const order = keyBetween(siblings.length ? siblings[siblings.length - 1].order : null, null);
496
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
497
+ const def = ADD_DEFAULTS[kind];
498
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
499
+ ed.select(id);
500
+ },
501
+ [ed]
502
+ );
503
+ const [paletteOpen, setPaletteOpen] = useState(false);
504
+ const [draggingEl, setDraggingEl] = useState(false);
505
+ useEffect(() => {
506
+ if (!editing) {
507
+ setPaletteOpen(false);
508
+ return;
509
+ }
510
+ const onMove = (e) => {
511
+ if (draggingEl) return;
512
+ if (e.clientX >= window.innerWidth - 26) setPaletteOpen(true);
513
+ else if (e.clientX < window.innerWidth - 236) setPaletteOpen(false);
514
+ };
515
+ window.addEventListener("mousemove", onMove);
516
+ return () => window.removeEventListener("mousemove", onMove);
517
+ }, [editing, draggingEl]);
518
+ const [dropHint, setDropHint] = useState(null);
519
+ const computeDropTarget = (clientX, clientY) => {
520
+ const el = document.elementFromPoint(clientX, clientY)?.closest("[data-cms]");
521
+ if (!el) return { id: null, edge: "inside", rect: null };
522
+ const id = el.getAttribute("data-cms");
523
+ const node = ed.state.doc.nodes[id];
524
+ const r = el.getBoundingClientRect();
525
+ const rect = { x: r.left, y: r.top, w: r.width, h: r.height };
526
+ if (node && CONTAINER_TYPES2.has(node.type)) return { id, edge: "inside", rect };
527
+ return { id, edge: clientY < r.top + r.height / 2 ? "before" : "after", rect };
528
+ };
529
+ const onPageDragOver = (e) => {
530
+ e.preventDefault();
531
+ e.dataTransfer.dropEffect = "copy";
532
+ setDropHint(computeDropTarget(e.clientX, e.clientY));
533
+ };
534
+ const insertRelative = (kind, hint) => {
535
+ const doc2 = ed.state.doc;
536
+ let parent;
537
+ let order;
538
+ if (!hint.id || hint.edge === "inside") {
539
+ parent = hint.id ?? (doc2.sections[doc2.sections.length - 1] ?? null);
540
+ const sib = childrenOf(doc2, parent);
541
+ order = keyBetween(sib.length ? sib[sib.length - 1].order : null, null);
542
+ } else {
543
+ const target = doc2.nodes[hint.id];
544
+ parent = target.parent;
545
+ const sib = childrenOf(doc2, parent);
546
+ const idx = sib.findIndex((n) => n.id === hint.id);
547
+ 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);
548
+ }
549
+ const id = `n${doc2.seq + 1}-${Math.floor(performance.now())}`;
550
+ const def = ADD_DEFAULTS[kind];
551
+ ed.apply({ t: "insert_node", node: { id, type: def.type, parent, order, layout: def.layout, props: { ...def.props }, style: { base: { ...def.style } } } });
552
+ ed.select(id);
553
+ };
554
+ const onPageDrop = (e) => {
555
+ const kind = e.dataTransfer.getData("application/x-cms-element");
556
+ if (!kind) return;
557
+ e.preventDefault();
558
+ insertRelative(kind, computeDropTarget(e.clientX, e.clientY));
559
+ setDropHint(null);
560
+ setDraggingEl(false);
561
+ };
778
562
  const setStyle = (patch) => selection && ed.apply({ t: "set_style", id: selection, breakpoint: "base", patch });
779
563
  const setProps = (patch) => selection && ed.apply({ t: "set_props", id: selection, patch });
564
+ const setLayout = (layout) => selection && ed.apply({ t: "set_layout", id: selection, layout });
780
565
  const removeSelected = () => {
781
566
  if (!selection) return;
782
567
  ed.apply({ t: "remove_node", id: selection });
@@ -799,8 +584,10 @@ function EditablePage({
799
584
  const startEditText = (id) => {
800
585
  if (!id) return;
801
586
  ed.select(id);
802
- requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape2(id)}"]`)?.focus());
587
+ requestAnimationFrame(() => pageRef.current?.querySelector(`[data-cms="${cssEscape(id)}"]`)?.focus());
803
588
  };
589
+ const nudgeRef = useRef({ transforms, onNodeTransform });
590
+ nudgeRef.current = { transforms, onNodeTransform };
804
591
  useEffect(() => {
805
592
  if (!editing) return;
806
593
  const onKey = (e) => {
@@ -819,6 +606,15 @@ function EditablePage({
819
606
  } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "v") {
820
607
  e.preventDefault();
821
608
  paste(selection);
609
+ } else if (selection && /^Arrow(Left|Right|Up|Down)$/.test(e.key)) {
610
+ const { transforms: tf, onNodeTransform: commit } = nudgeRef.current;
611
+ if (!commit) return;
612
+ e.preventDefault();
613
+ const step = e.shiftKey ? 10 : 1;
614
+ const dx = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
615
+ const dy = e.key === "ArrowUp" ? -step : e.key === "ArrowDown" ? step : 0;
616
+ const cur = tf?.[selection] ?? {};
617
+ commit(selection, { ...cur, x: (cur.x ?? 0) + dx, y: (cur.y ?? 0) + dy });
822
618
  }
823
619
  };
824
620
  window.addEventListener("keydown", onKey);
@@ -830,8 +626,23 @@ function EditablePage({
830
626
  if (el) ed.select(el.getAttribute("data-cms"));
831
627
  else ed.select(null);
832
628
  };
833
- const page = /* @__PURE__ */ jsx("div", { ref: pageRef, onClickCapture, onContextMenu: onContextMenuSelect, children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry }) });
834
- 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;
629
+ const page = /* @__PURE__ */ jsx(
630
+ "div",
631
+ {
632
+ ref: pageRef,
633
+ onClickCapture,
634
+ onContextMenu: onContextMenuSelect,
635
+ onDragOver: editing ? onPageDragOver : void 0,
636
+ onDragLeave: editing ? () => setDropHint(null) : void 0,
637
+ onDrop: editing ? onPageDrop : void 0,
638
+ children: /* @__PURE__ */ jsx(CmsPage, { doc: ed.state.doc, registry, data })
639
+ }
640
+ );
641
+ 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 ? (
642
+ // Normal flow, but "expanded" — extra scroll length below the content so a
643
+ // page can be made longer than its content (the + Frame control).
644
+ /* @__PURE__ */ jsx("div", { style: { minHeight: `${frames * 100}vh` }, children: page })
645
+ ) : page;
835
646
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
836
647
  editing ? /* @__PURE__ */ jsxs(ContextMenu, { children: [
837
648
  /* @__PURE__ */ jsx(ContextMenu.Trigger, { children: pageShell }),
@@ -858,7 +669,7 @@ function EditablePage({
858
669
  box,
859
670
  base: transforms?.[selection] ?? {},
860
671
  movable: Boolean(onNodeTransform),
861
- getEl: () => pageRef.current?.querySelector(`[data-cms="${cssEscape2(selection)}"]`) ?? null,
672
+ getEl: () => pageRef.current?.querySelector(`[data-cms="${cssEscape(selection)}"]`) ?? null,
862
673
  onTransform: (t) => onNodeTransform?.(selection, t),
863
674
  onLive: measureBox
864
675
  }
@@ -872,6 +683,7 @@ function EditablePage({
872
683
  measured: { w: box.w, h: box.h },
873
684
  onProps: setProps,
874
685
  onStyle: setStyle,
686
+ onLayout: setLayout,
875
687
  onTransform: (t) => selection && onNodeTransform?.(selection, t),
876
688
  onRemove: removeSelected,
877
689
  onClose: () => ed.select(null)
@@ -892,16 +704,101 @@ function EditablePage({
892
704
  onAdd: addNode
893
705
  }
894
706
  ) : null,
707
+ editing && dropHint?.rect ? /* @__PURE__ */ jsx(DropIndicator, { hint: dropHint }) : null,
708
+ editing ? /* @__PURE__ */ jsx(ElementPalette, { open: paletteOpen, onDragChange: setDraggingEl }) : null,
895
709
  editing && timelineDock ? /* @__PURE__ */ jsx("div", { style: { position: "fixed", left: 0, right: 0, bottom: 0, zIndex: Z2 }, children: timelineDock }) : null
896
710
  ] });
897
711
  }
898
- var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape"]);
712
+ function DropIndicator({
713
+ hint
714
+ }) {
715
+ const r = hint.rect;
716
+ if (!r) return null;
717
+ const color = "#ec4899";
718
+ if (hint.edge === "inside") {
719
+ 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 } });
720
+ }
721
+ const y = hint.edge === "before" ? r.y : r.y + r.h;
722
+ 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 } });
723
+ }
724
+ function ElementPalette({ open, onDragChange }) {
725
+ return /* @__PURE__ */ jsxs(
726
+ "div",
727
+ {
728
+ style: {
729
+ position: "fixed",
730
+ top: 64,
731
+ bottom: 96,
732
+ right: 0,
733
+ width: 216,
734
+ transform: open ? "translateX(0)" : "translateX(100%)",
735
+ transition: "transform 0.18s ease",
736
+ background: "#0b1220",
737
+ color: "#e2e8f0",
738
+ borderLeft: "1px solid #1e293b",
739
+ borderTopLeftRadius: 12,
740
+ borderBottomLeftRadius: 12,
741
+ boxShadow: "-18px 0 48px -20px rgba(0,0,0,0.6)",
742
+ zIndex: Z2 + 2,
743
+ fontFamily: "system-ui, sans-serif",
744
+ padding: 12,
745
+ overflowY: "auto"
746
+ },
747
+ children: [
748
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.5, marginBottom: 10 }, children: "Elements" }),
749
+ /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }, children: ADD_MENU.map((m) => /* @__PURE__ */ jsxs(
750
+ "div",
751
+ {
752
+ draggable: true,
753
+ onDragStart: (e) => {
754
+ e.dataTransfer.setData("application/x-cms-element", m.kind);
755
+ e.dataTransfer.effectAllowed = "copy";
756
+ onDragChange(true);
757
+ },
758
+ onDragEnd: () => onDragChange(false),
759
+ title: `Drag "${m.label}" onto the page`,
760
+ style: {
761
+ display: "flex",
762
+ flexDirection: "column",
763
+ alignItems: "center",
764
+ justifyContent: "center",
765
+ gap: 4,
766
+ height: 56,
767
+ borderRadius: 8,
768
+ background: "#0f172a",
769
+ border: "1px solid #1e293b",
770
+ cursor: "grab",
771
+ fontSize: 12,
772
+ userSelect: "none"
773
+ },
774
+ children: [
775
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 16, opacity: 0.7 }, children: "\uFF0B" }),
776
+ m.label
777
+ ]
778
+ },
779
+ m.kind
780
+ )) }),
781
+ /* @__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." })
782
+ ]
783
+ }
784
+ );
785
+ }
786
+ var CONTAINER_TYPES2 = /* @__PURE__ */ new Set(["section", "frame", "stack", "grid", "shape", "card", "device", "repeater"]);
899
787
  var ADD_DEFAULTS = {
900
788
  text: { type: "text", props: { content: "New text" }, style: { color: "inherit" } },
901
789
  heading: { type: "heading", props: { content: "New heading" }, style: { fontSize: { value: 28, unit: "px" }, fontWeight: 700 } },
902
790
  button: { type: "button", props: { label: "Button", href: "#", variant: "primary" }, style: {} },
791
+ link: { type: "link", props: { content: "link text", href: "#" }, style: { color: "#7c3aed" } },
903
792
  image: { type: "image", props: { src: "", alt: "" }, style: {} },
904
- box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } }
793
+ box: { type: "frame", props: {}, style: { padding: { value: 16, unit: "px" } } },
794
+ stack: { type: "stack", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
795
+ grid: { type: "grid", props: {}, style: { gap: { value: 12, unit: "px" } }, layout: "grid" },
796
+ card: { type: "card", props: {}, style: { padding: { value: 16, unit: "px" }, radius: { value: 12, unit: "px" }, border: "1px solid #e2e8f0" } },
797
+ callout: { type: "callout", props: { content: "Heads up \u2014 this is a callout.", variant: "info" }, style: {} },
798
+ divider: { type: "divider", props: {}, style: {} },
799
+ code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
800
+ richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
801
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
905
802
  };
906
803
  var GRIPS = [
907
804
  { key: "nw", north: true, west: true, cursor: "nwse-resize" },
@@ -922,20 +819,55 @@ function SelectionOverlay({
922
819
  onLive
923
820
  }) {
924
821
  const drag = useRef(null);
822
+ const targets = useRef({ vx: [], hy: [] });
823
+ const [guides, setGuides] = useState({ x: null, y: null });
925
824
  const apply = (t) => {
926
825
  const el = getEl();
927
826
  if (el) applyTransform(el, t);
928
827
  };
828
+ const snap = (cands, lines) => {
829
+ let best = { delta: 0, at: null, dist: SNAP + 1 };
830
+ for (const line of lines) {
831
+ for (const c of cands) {
832
+ const diff = line - c;
833
+ if (Math.abs(diff) < best.dist) best = { delta: diff, at: line, dist: Math.abs(diff) };
834
+ }
835
+ }
836
+ return best.dist <= SNAP ? { delta: best.delta, at: best.at } : { delta: 0, at: null };
837
+ };
929
838
  const startMove = (e) => {
930
839
  e.stopPropagation();
931
840
  e.preventDefault();
932
841
  e.currentTarget.setPointerCapture(e.pointerId);
933
842
  drag.current = { x: e.clientX, y: e.clientY, w: box.w, h: box.h, live: { ...base } };
843
+ const sel = getEl();
844
+ const vx = /* @__PURE__ */ new Set();
845
+ const hy = /* @__PURE__ */ new Set();
846
+ document.querySelectorAll("[data-cms]").forEach((el) => {
847
+ if (sel && (el === sel || sel.contains(el) || el.contains(sel))) return;
848
+ const r = el.getBoundingClientRect();
849
+ vx.add(r.left).add(r.left + r.width / 2).add(r.right);
850
+ hy.add(r.top).add(r.top + r.height / 2).add(r.bottom);
851
+ });
852
+ targets.current = { vx: [...vx], hy: [...hy] };
934
853
  };
935
854
  const moveMove = (e) => {
936
855
  const d = drag.current;
937
856
  if (!d) return;
938
- d.live = { ...base, x: (base.x ?? 0) + (e.clientX - d.x), y: (base.y ?? 0) + (e.clientY - d.y) };
857
+ let dx = e.clientX - d.x;
858
+ let dy = e.clientY - d.y;
859
+ if (!e.altKey) {
860
+ const l = box.x + dx;
861
+ const t = box.y + dy;
862
+ const sx = snap([l, l + box.w / 2, l + box.w], targets.current.vx);
863
+ const sy = snap([t, t + box.h / 2, t + box.h], targets.current.hy);
864
+ dx += sx.delta;
865
+ dy += sy.delta;
866
+ setGuides({ x: sx.at, y: sy.at });
867
+ } else if (guides.x !== null || guides.y !== null) {
868
+ setGuides({ x: null, y: null });
869
+ }
870
+ d.live = { ...base, x: (base.x ?? 0) + dx, y: (base.y ?? 0) + dy };
939
871
  apply(d.live);
940
872
  onLive();
941
873
  };
@@ -970,6 +902,7 @@ function SelectionOverlay({
970
902
  const d = drag.current;
971
903
  if (d) onTransform(d.live);
972
904
  drag.current = null;
905
+ setGuides({ x: null, y: null });
973
906
  };
974
907
  const outline = {
975
908
  position: "fixed",
@@ -998,9 +931,11 @@ function SelectionOverlay({
998
931
  const cy = g.north ? box.y : g.south ? box.y + box.h : box.y + box.h / 2;
999
932
  return { left: cx - 6, top: cy - 6, cursor: g.cursor };
1000
933
  };
1001
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
934
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
935
+ 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,
936
+ 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,
1002
937
  /* @__PURE__ */ jsx("div", { style: outline }),
1003
- movable ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
938
+ movable ? /* @__PURE__ */ jsxs(Fragment, { children: [
1004
939
  /* @__PURE__ */ jsx(
1005
940
  "div",
1006
941
  {
@@ -1063,11 +998,20 @@ function FloatingToolbar({
1063
998
  ] });
1064
999
  }
1065
1000
  var ADD_MENU = [
1066
- { kind: "text", label: "Text" },
1067
1001
  { kind: "heading", label: "Heading" },
1002
+ { kind: "text", label: "Text" },
1068
1003
  { kind: "button", label: "Button" },
1004
+ { kind: "link", label: "Link" },
1069
1005
  { kind: "image", label: "Image" },
1070
- { kind: "box", label: "Box" }
1006
+ { kind: "card", label: "Card" },
1007
+ { kind: "callout", label: "Callout" },
1008
+ { kind: "stack", label: "Stack" },
1009
+ { kind: "grid", label: "Grid" },
1010
+ { kind: "box", label: "Box" },
1011
+ { kind: "divider", label: "Divider" },
1012
+ { kind: "code", label: "Code" },
1013
+ { kind: "richtext", label: "Rich text" },
1014
+ { kind: "repeater", label: "Repeater" }
1071
1015
  ];
1072
1016
  function EditBar({
1073
1017
  editing,
@@ -1083,9 +1027,9 @@ function EditBar({
1083
1027
  const btn = { font: "inherit", border: "none", background: "transparent", color: "inherit", cursor: "pointer", padding: "4px 8px", borderRadius: 999 };
1084
1028
  return /* @__PURE__ */ jsxs("div", { style: bar, children: [
1085
1029
  /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, background: editing ? "#8b5cf6" : "transparent" }, onClick: onToggle, children: editing ? "\u25CF Editing" : "Edit" }),
1086
- editing ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1030
+ editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
1087
1031
  /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
1088
- /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, background: open ? "#1e293b" : "transparent" }, onClick: () => setOpen((v) => !v), title: "Add an element", children: "\uFF0B Add \u25BE" }),
1032
+ /* @__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" }),
1089
1033
  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(
1090
1034
  "button",
1091
1035
  {
@@ -1120,13 +1064,13 @@ function EditMenuItems({
1120
1064
  }) {
1121
1065
  const editable = node && (node.type === "text" || node.type === "heading");
1122
1066
  const nested = Boolean(node && node.parent !== null);
1123
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1067
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1124
1068
  editable ? /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onEditText, children: "Edit text" }) : null,
1125
1069
  /* @__PURE__ */ jsxs(ContextMenu.Sub, { children: [
1126
- /* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add inside" : "Add" }),
1070
+ /* @__PURE__ */ jsx(ContextMenu.SubTrigger, { children: isContainer ? "Add Element inside" : "Add Element" }),
1127
1071
  /* @__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)) })
1128
1072
  ] }),
1129
- node ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1073
+ node ? /* @__PURE__ */ jsxs(Fragment, { children: [
1130
1074
  /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1131
1075
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onDuplicate, children: "Duplicate" }),
1132
1076
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: onCopy, children: "Copy" }),
@@ -1135,7 +1079,7 @@ function EditMenuItems({
1135
1079
  /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1136
1080
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("up"), children: "Move up" }),
1137
1081
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("down"), children: "Move down" }),
1138
- nested ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1082
+ nested ? /* @__PURE__ */ jsxs(Fragment, { children: [
1139
1083
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("front"), children: "Bring to front" }),
1140
1084
  /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onReorder("back"), children: "Send to back" })
1141
1085
  ] }) : null,
@@ -1160,10 +1104,10 @@ function applyTransform(el, t) {
1160
1104
  function normalizeColor(v) {
1161
1105
  return v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : "#ffffff";
1162
1106
  }
1163
- function cssEscape2(value) {
1107
+ function cssEscape(value) {
1164
1108
  return value.replace(/["\\]/g, "\\$&");
1165
1109
  }
1166
1110
 
1167
- export { Canvas, EditablePage, Editor, Inspector, LayersPanel, NodeInspector, editorReduce, initEditor, useEditor };
1111
+ export { EditablePage, NodeInspector };
1168
1112
  //# sourceMappingURL=editor.js.map
1169
1113
  //# sourceMappingURL=editor.js.map