@particle-academy/fancy-cms-ui 0.0.1-dev.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 ADDED
@@ -0,0 +1,369 @@
1
+ import { CmsPage } from './chunk-MRWUUNJR.js';
2
+ import { reduce, childrenOf } from './chunk-NWH2AJLH.js';
3
+ import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useEffect } from 'react';
4
+ import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
5
+
6
+ function initEditor(doc) {
7
+ return { doc, past: [], future: [], selection: null };
8
+ }
9
+ function editorReduce(state, action) {
10
+ switch (action.type) {
11
+ case "apply": {
12
+ const next = reduce(state.doc, action.op);
13
+ if (next === state.doc) return state;
14
+ return {
15
+ ...state,
16
+ doc: next,
17
+ past: [...state.past, state.doc].slice(-100),
18
+ future: []
19
+ };
20
+ }
21
+ case "undo": {
22
+ if (state.past.length === 0) return state;
23
+ const prev = state.past[state.past.length - 1];
24
+ return {
25
+ ...state,
26
+ doc: prev,
27
+ past: state.past.slice(0, -1),
28
+ future: [state.doc, ...state.future]
29
+ };
30
+ }
31
+ case "redo": {
32
+ if (state.future.length === 0) return state;
33
+ const next = state.future[0];
34
+ return {
35
+ ...state,
36
+ doc: next,
37
+ past: [...state.past, state.doc],
38
+ future: state.future.slice(1)
39
+ };
40
+ }
41
+ case "select":
42
+ return state.selection === action.id ? state : { ...state, selection: action.id };
43
+ case "replace":
44
+ return state.doc === action.doc ? state : { ...state, doc: action.doc, future: [] };
45
+ default:
46
+ return state;
47
+ }
48
+ }
49
+
50
+ // src/editor/useEditor.ts
51
+ function useEditor(initialDoc) {
52
+ const [state, dispatch] = useReducer(editorReduce, initialDoc, initEditor);
53
+ const apply = useCallback((op) => dispatch({ type: "apply", op }), []);
54
+ const undo = useCallback(() => dispatch({ type: "undo" }), []);
55
+ const redo = useCallback(() => dispatch({ type: "redo" }), []);
56
+ const select = useCallback((id) => dispatch({ type: "select", id }), []);
57
+ const replace = useCallback((doc) => dispatch({ type: "replace", doc }), []);
58
+ return useMemo(
59
+ () => ({
60
+ state,
61
+ apply,
62
+ undo,
63
+ redo,
64
+ select,
65
+ replace,
66
+ canUndo: state.past.length > 0,
67
+ canRedo: state.future.length > 0
68
+ }),
69
+ [state, apply, undo, redo, select, replace]
70
+ );
71
+ }
72
+ function Canvas({ doc, selection, onSelect, apply }) {
73
+ const ref = useRef(null);
74
+ const [box, setBox] = useState(null);
75
+ const drag = useRef(null);
76
+ useLayoutEffect(() => {
77
+ const root = ref.current;
78
+ if (!root || !selection) {
79
+ setBox(null);
80
+ return;
81
+ }
82
+ const el = root.querySelector(`[data-cms="${cssEscape(selection)}"]`);
83
+ if (!el) {
84
+ setBox(null);
85
+ return;
86
+ }
87
+ const r = el.getBoundingClientRect();
88
+ const rootRect = root.getBoundingClientRect();
89
+ setBox({
90
+ x: r.left - rootRect.left + root.scrollLeft,
91
+ y: r.top - rootRect.top + root.scrollTop,
92
+ w: r.width,
93
+ h: r.height
94
+ });
95
+ }, [selection, doc]);
96
+ const handleClick = (e) => {
97
+ const target = e.target.closest("[data-cms]");
98
+ onSelect(target ? target.getAttribute("data-cms") : null);
99
+ };
100
+ const handlePointerDown = (e) => {
101
+ if (!selection) return;
102
+ const c = doc.nodes[selection]?.constraints?.base;
103
+ const left = c && c.left && typeof c.left === "object" ? c.left.value : 0;
104
+ const top = c && c.top && typeof c.top === "object" ? c.top.value : 0;
105
+ drag.current = { x: e.clientX, y: e.clientY, left, top };
106
+ e.currentTarget.setPointerCapture(e.pointerId);
107
+ e.stopPropagation();
108
+ };
109
+ const handlePointerMove = (e) => {
110
+ const d = drag.current;
111
+ if (!d || !selection) return;
112
+ apply({
113
+ t: "set_constraints",
114
+ id: selection,
115
+ breakpoint: "base",
116
+ patch: {
117
+ left: { value: Math.round(d.left + (e.clientX - d.x)), unit: "px" },
118
+ top: { value: Math.round(d.top + (e.clientY - d.y)), unit: "px" }
119
+ }
120
+ });
121
+ };
122
+ const handlePointerUp = () => {
123
+ drag.current = null;
124
+ };
125
+ const surface = {
126
+ position: "relative",
127
+ overflow: "auto",
128
+ height: "100%",
129
+ background: "#f8fafc"
130
+ };
131
+ const overlay = box ? {
132
+ position: "absolute",
133
+ left: box.x,
134
+ top: box.y,
135
+ width: box.w,
136
+ height: box.h,
137
+ outline: "2px solid #8b5cf6",
138
+ outlineOffset: -1,
139
+ cursor: "move",
140
+ touchAction: "none"
141
+ } : { display: "none" };
142
+ return /* @__PURE__ */ jsxs("div", { ref, onClick: handleClick, style: surface, children: [
143
+ /* @__PURE__ */ jsx(CmsPage, { doc }),
144
+ /* @__PURE__ */ jsx(
145
+ "div",
146
+ {
147
+ onPointerDown: handlePointerDown,
148
+ onPointerMove: handlePointerMove,
149
+ onPointerUp: handlePointerUp,
150
+ style: overlay
151
+ }
152
+ )
153
+ ] });
154
+ }
155
+ function cssEscape(value) {
156
+ return value.replace(/["\\]/g, "\\$&");
157
+ }
158
+ function LayersPanel({ doc, selection, onSelect }) {
159
+ 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)) });
160
+ }
161
+ function LayerRow({ doc, id, depth, selection, onSelect }) {
162
+ const node = doc.nodes[id];
163
+ if (!node) return null;
164
+ const selected = selection === id;
165
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
166
+ /* @__PURE__ */ jsxs(
167
+ "button",
168
+ {
169
+ type: "button",
170
+ onClick: () => onSelect(id),
171
+ style: {
172
+ display: "block",
173
+ width: "100%",
174
+ textAlign: "left",
175
+ border: "none",
176
+ borderRadius: 6,
177
+ cursor: "pointer",
178
+ padding: "4px 8px",
179
+ paddingLeft: 8 + depth * 14,
180
+ background: selected ? "#ede9fe" : "transparent",
181
+ color: selected ? "#5b21b6" : "#334155",
182
+ font: "inherit"
183
+ },
184
+ children: [
185
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: node.type }),
186
+ " \xB7 ",
187
+ id
188
+ ]
189
+ }
190
+ ),
191
+ childrenOf(doc, id).map((child) => /* @__PURE__ */ jsx(
192
+ LayerRow,
193
+ {
194
+ doc,
195
+ id: child.id,
196
+ depth: depth + 1,
197
+ selection,
198
+ onSelect
199
+ },
200
+ child.id
201
+ ))
202
+ ] });
203
+ }
204
+ var label = {
205
+ display: "block",
206
+ fontSize: 11,
207
+ textTransform: "uppercase",
208
+ letterSpacing: "0.04em",
209
+ color: "#94a3b8",
210
+ margin: "10px 0 4px"
211
+ };
212
+ var input = {
213
+ width: "100%",
214
+ boxSizing: "border-box",
215
+ padding: "6px 8px",
216
+ borderRadius: 6,
217
+ border: "1px solid #e2e8f0",
218
+ font: "inherit",
219
+ fontSize: 13
220
+ };
221
+ function Inspector({ doc, selection, apply }) {
222
+ const node = selection ? doc.nodes[selection] : null;
223
+ if (!node) {
224
+ return /* @__PURE__ */ jsx("div", { style: { padding: 16, color: "#94a3b8", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
225
+ }
226
+ const s = node.style.base;
227
+ const setStyle = (patch) => apply({ t: "set_style", id: node.id, breakpoint: "base", patch });
228
+ const setProp = (key, value) => apply({ t: "set_props", id: node.id, patch: { [key]: value } });
229
+ return /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", padding: 16, fontFamily: "system-ui, sans-serif", height: "100%", boxSizing: "border-box" }, children: [
230
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a" }, children: node.type }),
231
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#94a3b8", marginBottom: 8 }, children: node.id }),
232
+ node.type === "text" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
233
+ /* @__PURE__ */ jsx("label", { style: label, children: "Content" }),
234
+ /* @__PURE__ */ jsx(
235
+ "textarea",
236
+ {
237
+ style: { ...input, minHeight: 64, resize: "vertical" },
238
+ value: typeof node.props.content === "string" ? node.props.content : "",
239
+ onChange: (e) => setProp("content", e.target.value)
240
+ }
241
+ )
242
+ ] }) : null,
243
+ node.type === "image" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
244
+ /* @__PURE__ */ jsx("label", { style: label, children: "Source" }),
245
+ /* @__PURE__ */ jsx(
246
+ "input",
247
+ {
248
+ style: input,
249
+ value: typeof node.props.src === "string" ? node.props.src : "",
250
+ onChange: (e) => setProp("src", e.target.value)
251
+ }
252
+ )
253
+ ] }) : null,
254
+ /* @__PURE__ */ jsx("label", { style: label, children: "Background" }),
255
+ /* @__PURE__ */ jsx(
256
+ "input",
257
+ {
258
+ style: input,
259
+ placeholder: "#ffffff | gradient | url(\u2026)",
260
+ value: s.background ?? "",
261
+ onChange: (e) => setStyle({ background: e.target.value })
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx("label", { style: label, children: "Text color" }),
265
+ /* @__PURE__ */ jsx(
266
+ "input",
267
+ {
268
+ style: input,
269
+ placeholder: "#0f172a",
270
+ value: s.color ?? "",
271
+ onChange: (e) => setStyle({ color: e.target.value })
272
+ }
273
+ ),
274
+ /* @__PURE__ */ jsx("label", { style: label, children: "Font size (px)" }),
275
+ /* @__PURE__ */ jsx(
276
+ "input",
277
+ {
278
+ type: "number",
279
+ style: input,
280
+ value: s.fontSize?.value ?? "",
281
+ onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsx("label", { style: label, children: "Padding (px)" }),
285
+ /* @__PURE__ */ jsx(
286
+ "input",
287
+ {
288
+ type: "number",
289
+ style: input,
290
+ value: s.padding?.value ?? "",
291
+ onChange: (e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: "px" } })
292
+ }
293
+ ),
294
+ /* @__PURE__ */ jsx("label", { style: label, children: "Opacity" }),
295
+ /* @__PURE__ */ jsx(
296
+ "input",
297
+ {
298
+ type: "number",
299
+ step: "0.1",
300
+ min: "0",
301
+ max: "1",
302
+ style: input,
303
+ value: s.opacity ?? "",
304
+ onChange: (e) => setStyle({ opacity: Number(e.target.value) })
305
+ }
306
+ )
307
+ ] });
308
+ }
309
+ function Editor({ defaultValue, onChange }) {
310
+ const ed = useEditor(defaultValue);
311
+ const first = useRef(true);
312
+ useEffect(() => {
313
+ if (first.current) {
314
+ first.current = false;
315
+ return;
316
+ }
317
+ onChange?.(ed.state.doc);
318
+ }, [ed.state.doc, onChange]);
319
+ const shell = {
320
+ display: "grid",
321
+ gridTemplateColumns: "220px 1fr 300px",
322
+ height: "100%",
323
+ minHeight: 0,
324
+ background: "#fff",
325
+ color: "#0f172a",
326
+ border: "1px solid #e2e8f0",
327
+ borderRadius: 12,
328
+ overflow: "hidden"
329
+ };
330
+ return /* @__PURE__ */ jsxs("div", { style: shell, children: [
331
+ /* @__PURE__ */ jsxs("div", { style: { borderRight: "1px solid #e2e8f0", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
332
+ /* @__PURE__ */ jsx(Toolbar, { ed }),
333
+ /* @__PURE__ */ jsx(LayersPanel, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select })
334
+ ] }),
335
+ /* @__PURE__ */ jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply }),
336
+ /* @__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 }) })
337
+ ] });
338
+ }
339
+ function Toolbar({ ed }) {
340
+ const btn = {
341
+ font: "inherit",
342
+ fontSize: 12,
343
+ padding: "4px 10px",
344
+ borderRadius: 6,
345
+ border: "1px solid #e2e8f0",
346
+ background: "#fff",
347
+ cursor: "pointer"
348
+ };
349
+ return /* @__PURE__ */ jsxs(
350
+ "div",
351
+ {
352
+ style: {
353
+ display: "flex",
354
+ gap: 6,
355
+ padding: 8,
356
+ borderBottom: "1px solid #e2e8f0",
357
+ fontFamily: "system-ui, sans-serif"
358
+ },
359
+ children: [
360
+ /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: ed.canUndo ? 1 : 0.4 }, disabled: !ed.canUndo, onClick: ed.undo, children: "Undo" }),
361
+ /* @__PURE__ */ jsx("button", { type: "button", style: { ...btn, opacity: ed.canRedo ? 1 : 0.4 }, disabled: !ed.canRedo, onClick: ed.redo, children: "Redo" })
362
+ ]
363
+ }
364
+ );
365
+ }
366
+
367
+ export { Canvas, Editor, Inspector, LayersPanel, editorReduce, initEditor, useEditor };
368
+ //# sourceMappingURL=editor.js.map
369
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/editor/state.ts","../src/editor/useEditor.ts","../src/editor/Canvas.tsx","../src/editor/LayersPanel.tsx","../src/editor/Inspector.tsx","../src/editor/Editor.tsx"],"names":["jsx","jsxs","Fragment","useRef"],"mappings":"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","file":"editor.js","sourcesContent":["/**\n * Editor state engine — pure and framework-agnostic so it's unit-testable.\n * `useEditor` wraps this with `useReducer`. Undo/redo uses document snapshots\n * (simple + robust); the op-level `invert` in the spine is for collab/op-stream\n * undo.\n */\nimport type { PageDoc } from \"../document/types\";\nimport type { PageOp } from \"../document/ops\";\nimport { reduce } from \"../document/reduce\";\n\nexport interface EditorState {\n doc: PageDoc;\n past: PageDoc[];\n future: PageDoc[];\n selection: string | null;\n}\n\nexport type EditorAction =\n | { type: \"apply\"; op: PageOp }\n | { type: \"undo\" }\n | { type: \"redo\" }\n | { type: \"select\"; id: string | null }\n | { type: \"replace\"; doc: PageDoc };\n\nconst HISTORY_LIMIT = 100;\n\nexport function initEditor(doc: PageDoc): EditorState {\n return { doc, past: [], future: [], selection: null };\n}\n\nexport function editorReduce(state: EditorState, action: EditorAction): EditorState {\n switch (action.type) {\n case \"apply\": {\n const next = reduce(state.doc, action.op);\n if (next === state.doc) return state; // invalid op → no-op, no history entry\n return {\n ...state,\n doc: next,\n past: [...state.past, state.doc].slice(-HISTORY_LIMIT),\n future: [],\n };\n }\n case \"undo\": {\n if (state.past.length === 0) return state;\n const prev = state.past[state.past.length - 1]!;\n return {\n ...state,\n doc: prev,\n past: state.past.slice(0, -1),\n future: [state.doc, ...state.future],\n };\n }\n case \"redo\": {\n if (state.future.length === 0) return state;\n const next = state.future[0]!;\n return {\n ...state,\n doc: next,\n past: [...state.past, state.doc],\n future: state.future.slice(1),\n };\n }\n case \"select\":\n return state.selection === action.id ? state : { ...state, selection: action.id };\n case \"replace\":\n return state.doc === action.doc ? state : { ...state, doc: action.doc, future: [] };\n default:\n return state;\n }\n}\n","import { useCallback, useMemo, useReducer } from \"react\";\nimport type { PageDoc } from \"../document/types\";\nimport type { PageOp } from \"../document/ops\";\nimport { editorReduce, initEditor, type EditorState } from \"./state\";\n\nexport interface EditorApi {\n state: EditorState;\n /** Dispatch a mutation through the op-spine (records an undo snapshot). */\n apply: (op: PageOp) => void;\n undo: () => void;\n redo: () => void;\n select: (id: string | null) => void;\n /** Replace the document wholesale (e.g. a controlled `value` change). */\n replace: (doc: PageDoc) => void;\n canUndo: boolean;\n canRedo: boolean;\n}\n\nexport function useEditor(initialDoc: PageDoc): EditorApi {\n const [state, dispatch] = useReducer(editorReduce, initialDoc, initEditor);\n const apply = useCallback((op: PageOp) => dispatch({ type: \"apply\", op }), []);\n const undo = useCallback(() => dispatch({ type: \"undo\" }), []);\n const redo = useCallback(() => dispatch({ type: \"redo\" }), []);\n const select = useCallback((id: string | null) => dispatch({ type: \"select\", id }), []);\n const replace = useCallback((doc: PageDoc) => dispatch({ type: \"replace\", doc }), []);\n\n return useMemo(\n () => ({\n state,\n apply,\n undo,\n redo,\n select,\n replace,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n }),\n [state, apply, undo, redo, select, replace],\n );\n}\n","import {\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type MouseEvent as ReactMouseEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactElement,\n} from \"react\";\nimport type { PageDoc } from \"../document/types\";\nimport type { PageOp } from \"../document/ops\";\nimport { CmsPage } from \"../react/CmsPage\";\n\nexport interface CanvasProps {\n doc: PageDoc;\n selection: string | null;\n onSelect: (id: string | null) => void;\n apply: (op: PageOp) => void;\n}\n\ninterface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * The edit surface: renders the live page and overlays a selection box. Click to\n * select; drag the box to move (px constraints). Phase 1 cut: free-parent\n * move + resize handles, stack/grid reordering, and snapping come next.\n */\nexport function Canvas({ doc, selection, onSelect, apply }: CanvasProps): ReactElement {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<Box | null>(null);\n const drag = useRef<{ x: number; y: number; left: number; top: number } | null>(null);\n\n useLayoutEffect(() => {\n const root = ref.current;\n if (!root || !selection) {\n setBox(null);\n return;\n }\n const el = root.querySelector(`[data-cms=\"${cssEscape(selection)}\"]`);\n if (!el) {\n setBox(null);\n return;\n }\n const r = el.getBoundingClientRect();\n const rootRect = root.getBoundingClientRect();\n setBox({\n x: r.left - rootRect.left + root.scrollLeft,\n y: r.top - rootRect.top + root.scrollTop,\n w: r.width,\n h: r.height,\n });\n }, [selection, doc]);\n\n const handleClick = (e: ReactMouseEvent<HTMLDivElement>) => {\n const target = (e.target as HTMLElement).closest(\"[data-cms]\");\n onSelect(target ? target.getAttribute(\"data-cms\") : null);\n };\n\n const handlePointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {\n if (!selection) return;\n const c = doc.nodes[selection]?.constraints?.base;\n const left = c && c.left && typeof c.left === \"object\" ? c.left.value : 0;\n const top = c && c.top && typeof c.top === \"object\" ? c.top.value : 0;\n drag.current = { x: e.clientX, y: e.clientY, left, top };\n e.currentTarget.setPointerCapture(e.pointerId);\n e.stopPropagation();\n };\n\n const handlePointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {\n const d = drag.current;\n if (!d || !selection) return;\n apply({\n t: \"set_constraints\",\n id: selection,\n breakpoint: \"base\",\n patch: {\n left: { value: Math.round(d.left + (e.clientX - d.x)), unit: \"px\" },\n top: { value: Math.round(d.top + (e.clientY - d.y)), unit: \"px\" },\n },\n });\n };\n\n const handlePointerUp = () => {\n drag.current = null;\n };\n\n const surface: CSSProperties = {\n position: \"relative\",\n overflow: \"auto\",\n height: \"100%\",\n background: \"#f8fafc\",\n };\n const overlay: CSSProperties = box\n ? {\n position: \"absolute\",\n left: box.x,\n top: box.y,\n width: box.w,\n height: box.h,\n outline: \"2px solid #8b5cf6\",\n outlineOffset: -1,\n cursor: \"move\",\n touchAction: \"none\",\n }\n : { display: \"none\" };\n\n return (\n <div ref={ref} onClick={handleClick} style={surface}>\n <CmsPage doc={doc} />\n <div\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n style={overlay}\n />\n </div>\n );\n}\n\nfunction cssEscape(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n","import { Fragment, type ReactElement } from \"react\";\nimport type { PageDoc } from \"../document/types\";\nimport { childrenOf } from \"../document/reduce\";\n\nexport interface LayersPanelProps {\n doc: PageDoc;\n selection: string | null;\n onSelect: (id: string) => void;\n}\n\n/** The section/layer tree. Click a row to select. */\nexport function LayersPanel({ doc, selection, onSelect }: LayersPanelProps): ReactElement {\n return (\n <div style={{ overflow: \"auto\", padding: 8, fontSize: 13, fontFamily: \"system-ui, sans-serif\" }}>\n {doc.sections.map((id) => (\n <LayerRow key={id} doc={doc} id={id} depth={0} selection={selection} onSelect={onSelect} />\n ))}\n </div>\n );\n}\n\ninterface LayerRowProps extends LayersPanelProps {\n id: string;\n depth: number;\n}\n\nfunction LayerRow({ doc, id, depth, selection, onSelect }: LayerRowProps): ReactElement | null {\n const node = doc.nodes[id];\n if (!node) return null;\n const selected = selection === id;\n return (\n <Fragment>\n <button\n type=\"button\"\n onClick={() => onSelect(id)}\n style={{\n display: \"block\",\n width: \"100%\",\n textAlign: \"left\",\n border: \"none\",\n borderRadius: 6,\n cursor: \"pointer\",\n padding: \"4px 8px\",\n paddingLeft: 8 + depth * 14,\n background: selected ? \"#ede9fe\" : \"transparent\",\n color: selected ? \"#5b21b6\" : \"#334155\",\n font: \"inherit\",\n }}\n >\n <span style={{ opacity: 0.6 }}>{node.type}</span> · {id}\n </button>\n {childrenOf(doc, id).map((child) => (\n <LayerRow\n key={child.id}\n doc={doc}\n id={child.id}\n depth={depth + 1}\n selection={selection}\n onSelect={onSelect}\n />\n ))}\n </Fragment>\n );\n}\n","import type { CSSProperties, ReactElement } from \"react\";\nimport type { PageDoc, StyleProps } from \"../document/types\";\nimport type { PageOp } from \"../document/ops\";\n\nexport interface InspectorProps {\n doc: PageDoc;\n selection: string | null;\n apply: (op: PageOp) => void;\n}\n\nconst label: CSSProperties = {\n display: \"block\",\n fontSize: 11,\n textTransform: \"uppercase\",\n letterSpacing: \"0.04em\",\n color: \"#94a3b8\",\n margin: \"10px 0 4px\",\n};\nconst input: CSSProperties = {\n width: \"100%\",\n boxSizing: \"border-box\",\n padding: \"6px 8px\",\n borderRadius: 6,\n border: \"1px solid #e2e8f0\",\n font: \"inherit\",\n fontSize: 13,\n};\n\n/** Contextual property editor for the selected node. Every change is one op. */\nexport function Inspector({ doc, selection, apply }: InspectorProps): ReactElement {\n const node = selection ? doc.nodes[selection] : null;\n if (!node) {\n return (\n <div style={{ padding: 16, color: \"#94a3b8\", fontFamily: \"system-ui, sans-serif\", fontSize: 13 }}>\n Select an element to edit.\n </div>\n );\n }\n\n const s = node.style.base;\n const setStyle = (patch: Partial<StyleProps>) =>\n apply({ t: \"set_style\", id: node.id, breakpoint: \"base\", patch });\n const setProp = (key: string, value: unknown) =>\n apply({ t: \"set_props\", id: node.id, patch: { [key]: value } });\n\n return (\n <div style={{ overflow: \"auto\", padding: 16, fontFamily: \"system-ui, sans-serif\", height: \"100%\", boxSizing: \"border-box\" }}>\n <div style={{ fontSize: 13, fontWeight: 600, color: \"#0f172a\" }}>{node.type}</div>\n <div style={{ fontSize: 11, color: \"#94a3b8\", marginBottom: 8 }}>{node.id}</div>\n\n {node.type === \"text\" ? (\n <>\n <label style={label}>Content</label>\n <textarea\n style={{ ...input, minHeight: 64, resize: \"vertical\" }}\n value={typeof node.props.content === \"string\" ? node.props.content : \"\"}\n onChange={(e) => setProp(\"content\", e.target.value)}\n />\n </>\n ) : null}\n\n {node.type === \"image\" ? (\n <>\n <label style={label}>Source</label>\n <input\n style={input}\n value={typeof node.props.src === \"string\" ? node.props.src : \"\"}\n onChange={(e) => setProp(\"src\", e.target.value)}\n />\n </>\n ) : null}\n\n <label style={label}>Background</label>\n <input\n style={input}\n placeholder=\"#ffffff | gradient | url(…)\"\n value={s.background ?? \"\"}\n onChange={(e) => setStyle({ background: e.target.value })}\n />\n\n <label style={label}>Text color</label>\n <input\n style={input}\n placeholder=\"#0f172a\"\n value={s.color ?? \"\"}\n onChange={(e) => setStyle({ color: e.target.value })}\n />\n\n <label style={label}>Font size (px)</label>\n <input\n type=\"number\"\n style={input}\n value={s.fontSize?.value ?? \"\"}\n onChange={(e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: \"px\" } })}\n />\n\n <label style={label}>Padding (px)</label>\n <input\n type=\"number\"\n style={input}\n value={s.padding?.value ?? \"\"}\n onChange={(e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: \"px\" } })}\n />\n\n <label style={label}>Opacity</label>\n <input\n type=\"number\"\n step=\"0.1\"\n min=\"0\"\n max=\"1\"\n style={input}\n value={s.opacity ?? \"\"}\n onChange={(e) => setStyle({ opacity: Number(e.target.value) })}\n />\n </div>\n );\n}\n","import { useEffect, useRef, type CSSProperties, type ReactElement } from \"react\";\nimport type { PageDoc } from \"../document/types\";\nimport { useEditor, type EditorApi } from \"./useEditor\";\nimport { Canvas } from \"./Canvas\";\nimport { LayersPanel } from \"./LayersPanel\";\nimport { Inspector } from \"./Inspector\";\n\nexport interface EditorProps {\n /** Initial document; edits are surfaced via {@link EditorProps.onChange}. */\n defaultValue: PageDoc;\n onChange?: (doc: PageDoc) => void;\n}\n\n/**\n * The fancy-cms editor: layers · canvas · inspector over the op-spine. Phase 1\n * cut — chrome is plain markup for now; it graduates to react-fancy next.\n */\nexport function Editor({ defaultValue, onChange }: EditorProps): ReactElement {\n const ed = useEditor(defaultValue);\n const first = useRef(true);\n\n useEffect(() => {\n if (first.current) {\n first.current = false;\n return;\n }\n onChange?.(ed.state.doc);\n }, [ed.state.doc, onChange]);\n\n const shell: CSSProperties = {\n display: \"grid\",\n gridTemplateColumns: \"220px 1fr 300px\",\n height: \"100%\",\n minHeight: 0,\n background: \"#fff\",\n color: \"#0f172a\",\n border: \"1px solid #e2e8f0\",\n borderRadius: 12,\n overflow: \"hidden\",\n };\n\n return (\n <div style={shell}>\n <div style={{ borderRight: \"1px solid #e2e8f0\", display: \"flex\", flexDirection: \"column\", minHeight: 0 }}>\n <Toolbar ed={ed} />\n <LayersPanel doc={ed.state.doc} selection={ed.state.selection} onSelect={ed.select} />\n </div>\n <Canvas doc={ed.state.doc} selection={ed.state.selection} onSelect={ed.select} apply={ed.apply} />\n <div style={{ borderLeft: \"1px solid #e2e8f0\", minHeight: 0 }}>\n <Inspector doc={ed.state.doc} selection={ed.state.selection} apply={ed.apply} />\n </div>\n </div>\n );\n}\n\nfunction Toolbar({ ed }: { ed: EditorApi }): ReactElement {\n const btn: CSSProperties = {\n font: \"inherit\",\n fontSize: 12,\n padding: \"4px 10px\",\n borderRadius: 6,\n border: \"1px solid #e2e8f0\",\n background: \"#fff\",\n cursor: \"pointer\",\n };\n return (\n <div\n style={{\n display: \"flex\",\n gap: 6,\n padding: 8,\n borderBottom: \"1px solid #e2e8f0\",\n fontFamily: \"system-ui, sans-serif\",\n }}\n >\n <button type=\"button\" style={{ ...btn, opacity: ed.canUndo ? 1 : 0.4 }} disabled={!ed.canUndo} onClick={ed.undo}>\n Undo\n </button>\n <button type=\"button\" style={{ ...btn, opacity: ed.canRedo ? 1 : 0.4 }} disabled={!ed.canRedo} onClick={ed.redo}>\n Redo\n </button>\n </div>\n );\n}\n"]}