pagiera 0.2.0-alpha.33

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.
Files changed (192) hide show
  1. package/README.md +528 -0
  2. package/dist/adapters.d.ts +89 -0
  3. package/dist/adapters.d.ts.map +1 -0
  4. package/dist/adapters.js +2 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/client.d.ts +22 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +33 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/data.js +210 -0
  11. package/dist/data.js.map +7 -0
  12. package/dist/document.d.ts +63 -0
  13. package/dist/document.d.ts.map +1 -0
  14. package/dist/document.js +25 -0
  15. package/dist/document.js.map +1 -0
  16. package/dist/editor.d.ts +14 -0
  17. package/dist/editor.d.ts.map +1 -0
  18. package/dist/editor.js +59 -0
  19. package/dist/editor.js.map +1 -0
  20. package/dist/full-editor.d.ts +53 -0
  21. package/dist/full-editor.js +11117 -0
  22. package/dist/full-editor.js.map +7 -0
  23. package/dist/full.css +3498 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +7 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/internal/app/api/ai-design/route.d.ts +3 -0
  29. package/dist/internal/app/api/ai-design/route.d.ts.map +1 -0
  30. package/dist/internal/app/api/ai-design/route.js +165 -0
  31. package/dist/internal/app/api/ai-design/route.js.map +1 -0
  32. package/dist/internal/app/p/editor/ai-panel.d.ts +27 -0
  33. package/dist/internal/app/p/editor/ai-panel.d.ts.map +1 -0
  34. package/dist/internal/app/p/editor/ai-panel.js +77 -0
  35. package/dist/internal/app/p/editor/ai-panel.js.map +1 -0
  36. package/dist/internal/app/p/editor/data-modal.d.ts +26 -0
  37. package/dist/internal/app/p/editor/data-modal.d.ts.map +1 -0
  38. package/dist/internal/app/p/editor/data-modal.js +94 -0
  39. package/dist/internal/app/p/editor/data-modal.js.map +1 -0
  40. package/dist/internal/app/p/editor/data-panel.d.ts +18 -0
  41. package/dist/internal/app/p/editor/data-panel.d.ts.map +1 -0
  42. package/dist/internal/app/p/editor/data-panel.js +45 -0
  43. package/dist/internal/app/p/editor/data-panel.js.map +1 -0
  44. package/dist/internal/app/p/editor/editor.d.ts +51 -0
  45. package/dist/internal/app/p/editor/editor.d.ts.map +1 -0
  46. package/dist/internal/app/p/editor/editor.js +1551 -0
  47. package/dist/internal/app/p/editor/editor.js.map +1 -0
  48. package/dist/internal/app/p/editor/fields.d.ts +111 -0
  49. package/dist/internal/app/p/editor/fields.d.ts.map +1 -0
  50. package/dist/internal/app/p/editor/fields.js +141 -0
  51. package/dist/internal/app/p/editor/fields.js.map +1 -0
  52. package/dist/internal/app/p/editor/inspector.d.ts +38 -0
  53. package/dist/internal/app/p/editor/inspector.d.ts.map +1 -0
  54. package/dist/internal/app/p/editor/inspector.js +336 -0
  55. package/dist/internal/app/p/editor/inspector.js.map +1 -0
  56. package/dist/internal/app/p/editor/library.d.ts +18 -0
  57. package/dist/internal/app/p/editor/library.d.ts.map +1 -0
  58. package/dist/internal/app/p/editor/library.js +61 -0
  59. package/dist/internal/app/p/editor/library.js.map +1 -0
  60. package/dist/internal/app/p/editor/parts.d.ts +75 -0
  61. package/dist/internal/app/p/editor/parts.d.ts.map +1 -0
  62. package/dist/internal/app/p/editor/parts.js +261 -0
  63. package/dist/internal/app/p/editor/parts.js.map +1 -0
  64. package/dist/internal/app/p/editor/templates-panel.d.ts +6 -0
  65. package/dist/internal/app/p/editor/templates-panel.d.ts.map +1 -0
  66. package/dist/internal/app/p/editor/templates-panel.js +12 -0
  67. package/dist/internal/app/p/editor/templates-panel.js.map +1 -0
  68. package/dist/internal/app/p/editor/use-canvas-view.d.ts +24 -0
  69. package/dist/internal/app/p/editor/use-canvas-view.d.ts.map +1 -0
  70. package/dist/internal/app/p/editor/use-canvas-view.js +242 -0
  71. package/dist/internal/app/p/editor/use-canvas-view.js.map +1 -0
  72. package/dist/internal/app/p/editor/use-editor.d.ts +48 -0
  73. package/dist/internal/app/p/editor/use-editor.d.ts.map +1 -0
  74. package/dist/internal/app/p/editor/use-editor.js +224 -0
  75. package/dist/internal/app/p/editor/use-editor.js.map +1 -0
  76. package/dist/internal/app/p/editor/variables-panel.d.ts +8 -0
  77. package/dist/internal/app/p/editor/variables-panel.d.ts.map +1 -0
  78. package/dist/internal/app/p/editor/variables-panel.js +52 -0
  79. package/dist/internal/app/p/editor/variables-panel.js.map +1 -0
  80. package/dist/internal/components/ui/select.d.ts +12 -0
  81. package/dist/internal/components/ui/select.d.ts.map +1 -0
  82. package/dist/internal/components/ui/select.js +32 -0
  83. package/dist/internal/components/ui/select.js.map +1 -0
  84. package/dist/internal/drizzle/index.d.ts +6 -0
  85. package/dist/internal/drizzle/index.d.ts.map +1 -0
  86. package/dist/internal/drizzle/index.js +20 -0
  87. package/dist/internal/drizzle/index.js.map +1 -0
  88. package/dist/internal/drizzle/schema.d.ts +405 -0
  89. package/dist/internal/drizzle/schema.d.ts.map +1 -0
  90. package/dist/internal/drizzle/schema.js +70 -0
  91. package/dist/internal/drizzle/schema.js.map +1 -0
  92. package/dist/internal/lib/data/context.d.ts +8 -0
  93. package/dist/internal/lib/data/context.d.ts.map +1 -0
  94. package/dist/internal/lib/data/context.js +17 -0
  95. package/dist/internal/lib/data/context.js.map +1 -0
  96. package/dist/internal/lib/data/source.d.ts +54 -0
  97. package/dist/internal/lib/data/source.d.ts.map +1 -0
  98. package/dist/internal/lib/data/source.js +229 -0
  99. package/dist/internal/lib/data/source.js.map +1 -0
  100. package/dist/internal/lib/editor/ai-types.d.ts +44 -0
  101. package/dist/internal/lib/editor/ai-types.d.ts.map +1 -0
  102. package/dist/internal/lib/editor/ai-types.js +2 -0
  103. package/dist/internal/lib/editor/ai-types.js.map +1 -0
  104. package/dist/internal/lib/editor/arrange.d.ts +12 -0
  105. package/dist/internal/lib/editor/arrange.d.ts.map +1 -0
  106. package/dist/internal/lib/editor/arrange.js +72 -0
  107. package/dist/internal/lib/editor/arrange.js.map +1 -0
  108. package/dist/internal/lib/editor/cascade.d.ts +39 -0
  109. package/dist/internal/lib/editor/cascade.d.ts.map +1 -0
  110. package/dist/internal/lib/editor/cascade.js +86 -0
  111. package/dist/internal/lib/editor/cascade.js.map +1 -0
  112. package/dist/internal/lib/editor/icon.d.ts +6 -0
  113. package/dist/internal/lib/editor/icon.d.ts.map +1 -0
  114. package/dist/internal/lib/editor/icon.js +13 -0
  115. package/dist/internal/lib/editor/icon.js.map +1 -0
  116. package/dist/internal/lib/editor/presets.d.ts +10 -0
  117. package/dist/internal/lib/editor/presets.d.ts.map +1 -0
  118. package/dist/internal/lib/editor/presets.js +83 -0
  119. package/dist/internal/lib/editor/presets.js.map +1 -0
  120. package/dist/internal/lib/editor/showcase.d.ts +14 -0
  121. package/dist/internal/lib/editor/showcase.d.ts.map +1 -0
  122. package/dist/internal/lib/editor/showcase.js +219 -0
  123. package/dist/internal/lib/editor/showcase.js.map +1 -0
  124. package/dist/internal/lib/editor/site-templates.d.ts +4 -0
  125. package/dist/internal/lib/editor/site-templates.d.ts.map +1 -0
  126. package/dist/internal/lib/editor/site-templates.js +156 -0
  127. package/dist/internal/lib/editor/site-templates.js.map +1 -0
  128. package/dist/internal/lib/editor/snap.d.ts +34 -0
  129. package/dist/internal/lib/editor/snap.d.ts.map +1 -0
  130. package/dist/internal/lib/editor/snap.js +70 -0
  131. package/dist/internal/lib/editor/snap.js.map +1 -0
  132. package/dist/internal/lib/editor/style.d.ts +53 -0
  133. package/dist/internal/lib/editor/style.d.ts.map +1 -0
  134. package/dist/internal/lib/editor/style.js +313 -0
  135. package/dist/internal/lib/editor/style.js.map +1 -0
  136. package/dist/internal/lib/editor/tree.d.ts +72 -0
  137. package/dist/internal/lib/editor/tree.d.ts.map +1 -0
  138. package/dist/internal/lib/editor/tree.js +301 -0
  139. package/dist/internal/lib/editor/tree.js.map +1 -0
  140. package/dist/internal/lib/editor/types.d.ts +273 -0
  141. package/dist/internal/lib/editor/types.d.ts.map +1 -0
  142. package/dist/internal/lib/editor/types.js +411 -0
  143. package/dist/internal/lib/editor/types.js.map +1 -0
  144. package/dist/internal/lib/editor/validate.d.ts +20 -0
  145. package/dist/internal/lib/editor/validate.d.ts.map +1 -0
  146. package/dist/internal/lib/editor/validate.js +467 -0
  147. package/dist/internal/lib/editor/validate.js.map +1 -0
  148. package/dist/internal/lib/pages.d.ts +83 -0
  149. package/dist/internal/lib/pages.d.ts.map +1 -0
  150. package/dist/internal/lib/pages.js +294 -0
  151. package/dist/internal/lib/pages.js.map +1 -0
  152. package/dist/internal/lib/render/bind.d.ts +18 -0
  153. package/dist/internal/lib/render/bind.d.ts.map +1 -0
  154. package/dist/internal/lib/render/bind.js +28 -0
  155. package/dist/internal/lib/render/bind.js.map +1 -0
  156. package/dist/internal/lib/render/css.d.ts +31 -0
  157. package/dist/internal/lib/render/css.d.ts.map +1 -0
  158. package/dist/internal/lib/render/css.js +239 -0
  159. package/dist/internal/lib/render/css.js.map +1 -0
  160. package/dist/internal/lib/render/load-data.d.ts +15 -0
  161. package/dist/internal/lib/render/load-data.d.ts.map +1 -0
  162. package/dist/internal/lib/render/load-data.js +39 -0
  163. package/dist/internal/lib/render/load-data.js.map +1 -0
  164. package/dist/internal/lib/render/page-render.d.ts +17 -0
  165. package/dist/internal/lib/render/page-render.d.ts.map +1 -0
  166. package/dist/internal/lib/render/page-render.js +117 -0
  167. package/dist/internal/lib/render/page-render.js.map +1 -0
  168. package/dist/internal/lib/utils.d.ts +3 -0
  169. package/dist/internal/lib/utils.d.ts.map +1 -0
  170. package/dist/internal/lib/utils.js +6 -0
  171. package/dist/internal/lib/utils.js.map +1 -0
  172. package/dist/manrope-data.d.ts +3 -0
  173. package/dist/manrope-data.d.ts.map +1 -0
  174. package/dist/manrope-data.js +3 -0
  175. package/dist/manrope-data.js.map +1 -0
  176. package/dist/page.d.ts +9 -0
  177. package/dist/page.d.ts.map +1 -0
  178. package/dist/page.js +148 -0
  179. package/dist/page.js.map +1 -0
  180. package/dist/provider.d.ts +14 -0
  181. package/dist/provider.d.ts.map +1 -0
  182. package/dist/provider.js +45 -0
  183. package/dist/provider.js.map +1 -0
  184. package/dist/runtime.d.ts +8 -0
  185. package/dist/runtime.js +717 -0
  186. package/dist/runtime.js.map +7 -0
  187. package/dist/server.d.ts +31 -0
  188. package/dist/server.js +2267 -0
  189. package/dist/server.js.map +7 -0
  190. package/dist/styles.css +300 -0
  191. package/package.json +94 -0
  192. package/thumbnail.png +0 -0
@@ -0,0 +1,1551 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { IconArrowBackUp, IconArrowForwardUp, IconArrowsMaximize, IconCommand, IconComponents, IconCopy, IconChevronLeft, IconChevronRight, IconDatabase, IconExternalLink, IconFocusCentered, IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse, IconMinus, IconPlayerPlay, IconPlus, IconPointer, IconSearch, IconSparkles, IconTrash, IconWorld, IconLayersLinked, IconLayoutColumns, IconBox, IconFile, IconLibrary, IconIcons, IconMoon, IconPalette, IconPin, IconPinFilled, IconSun, IconTemplate, IconX, } from "@tabler/icons-react";
4
+ import { AnimatePresence, motion } from "motion/react";
5
+ import { useCallback, useEffect, useMemo, useRef, useState, useTransition, } from "react";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
7
+ import { alignElements, distributeElements } from "@/lib/editor/arrange";
8
+ import { baseOf, cascadeOf } from "@/lib/editor/cascade";
9
+ import { bindElement, rowsFor } from "@/lib/render/bind";
10
+ import { resolveFont } from "@/lib/render/css";
11
+ import { createNocturneShowcase } from "@/lib/editor/showcase";
12
+ import { snapPosition } from "@/lib/editor/snap";
13
+ import { applyStyle, applyStyleIsolated, clearOverrides, isBand, resolveStyle, rootStyleToCss, splitBand, styleToCss, } from "@/lib/editor/style";
14
+ import { absolutePosition, childrenOf, cloneSubtree, containerAt, createElement, indexById, isNote, nextZ, removeSubtree, reorder, reparent, subtreeIds, wrapInContainer, } from "@/lib/editor/tree";
15
+ import { DEFAULT_BREAKPOINTS, DRAG_MIME, isContainer, isTextual, MOVE_MIME, STYLE_KEYS, } from "@/lib/editor/types";
16
+ import { Inspector, MultiSelectPanel, PageInspector } from "./inspector";
17
+ import { DataPanel } from "./data-panel";
18
+ import { LibraryPanel } from "./library";
19
+ import { Breadcrumbs, ContextMenu, ElementBody, ElementsPanel, IconsPanel, LayersPanel, PagesPanel, ResizeHandles, } from "./parts";
20
+ import { useCanvasView } from "./use-canvas-view";
21
+ import { useEditorDocument } from "./use-editor";
22
+ import { AiPanel } from "./ai-panel";
23
+ import { VariablesPanel } from "./variables-panel";
24
+ import { TemplatesPanel } from "./templates-panel";
25
+ const MIN_SIZE = 10;
26
+ const COMPONENT_MIME = "application/pagiera-component";
27
+ const defaultEditorHref = (pageId) => `/editor/${encodeURIComponent(pageId)}`;
28
+ const defaultPublishedHref = (slug) => slug === "home" || slug === "" ? "/" : `/${slug.split("/").map((part) => part.startsWith(":") ? part : encodeURIComponent(part)).join("/")}`;
29
+ const unavailable = async () => ({
30
+ status: "error",
31
+ message: "This editor action has no host adapter.",
32
+ });
33
+ export default function Editor({ page, pages, library, adapters, }) {
34
+ const [isPending, startTransition] = useTransition();
35
+ const { elements, rootStyle, dataSources, setElements, setRootStyle, setDataSources, beginTransaction, endTransaction, undo, redo, canUndo, canRedo, saveStatus, saveError, isDirty, saveNow, } = useEditorDocument({
36
+ pageId: page.id,
37
+ initialDocument: {
38
+ elements: page.elements,
39
+ rootStyle: page.rootStyle,
40
+ dataSources: page.dataSources,
41
+ },
42
+ initialVersion: page.version,
43
+ saveDocument: useCallback((pageId, document, expectedVersion) => adapters?.save?.(pageId, document, expectedVersion) ??
44
+ Promise.resolve({ status: "error", message: "No persistence adapter configured." }), [adapters?.save]),
45
+ });
46
+ /** Editor chrome theme; the palette itself lives in globals.css. */
47
+ const [chromeTheme, setChromeTheme] = useState("dark");
48
+ useEffect(() => {
49
+ const stored = localStorage.getItem("pagiera:editor-theme");
50
+ if (stored === "dark" || stored === "light")
51
+ setChromeTheme(stored);
52
+ }, []);
53
+ const toggleChromeTheme = useCallback(() => {
54
+ setChromeTheme((current) => {
55
+ const next = current === "dark" ? "light" : "dark";
56
+ try {
57
+ localStorage.setItem("pagiera:editor-theme", next);
58
+ }
59
+ catch {
60
+ // A private-mode storage failure must not block the toggle.
61
+ }
62
+ return next;
63
+ });
64
+ }, []);
65
+ const [breakpoint, setBreakpoint] = useState("desktop");
66
+ const [breakpointPanel, setBreakpointPanel] = useState(false);
67
+ const componentMode = rootStyle.documentMode === "component";
68
+ const componentMasters = elements.filter((element) => element.componentRole === "master");
69
+ const [activeComponentMasterId, setActiveComponentMasterId] = useState(null);
70
+ const activeComponentMaster = componentMasters.find((element) => element.id === activeComponentMasterId) ?? componentMasters[0];
71
+ const activeComponentVariants = activeComponentMaster
72
+ ? componentMasters.filter((element) => element.componentId === activeComponentMaster.componentId)
73
+ : [];
74
+ const breakpointDefs = rootStyle.breakpoints?.length
75
+ ? rootStyle.breakpoints
76
+ : DEFAULT_BREAKPOINTS;
77
+ const cascade = useMemo(() => cascadeOf(rootStyle.breakpoints, rootStyle.baseBreakpointId), [rootStyle.breakpoints, rootStyle.baseBreakpointId]);
78
+ const selectedBreakpoint = breakpointDefs.find((item) => item.id === breakpoint) ?? breakpointDefs[0];
79
+ const frameWidthForBreakpoint = selectedBreakpoint.width;
80
+ const frames = useMemo(() => componentMode ? [{ bp: "desktop", width: Math.max(1, activeComponentMaster?.base.w ?? 320) }] : breakpointDefs.map((item) => ({ bp: item.id, width: item.width })), [activeComponentMaster?.base.w, breakpointDefs, componentMode]);
81
+ /** Widest frame on screen; the zoom fits against this. */
82
+ const fitWidth = Math.max(320, frames.reduce((total, frame) => total + frame.width, 0) + Math.max(0, frames.length - 1) * 40);
83
+ const { viewportRef, frameRef, zoom, scale, zoomTo, stepZoom, zoomToFit, spaceHeld, isPanning, tryBeginPan, recenter, } = useCanvasView(fitWidth);
84
+ const [leftTab, setLeftTab] = useState("Layers");
85
+ const [rightTab, setRightTab] = useState("Design");
86
+ const [isLeftCollapsed, setIsLeftCollapsed] = useState(false);
87
+ const [isRightCollapsed, setIsRightCollapsed] = useState(false);
88
+ const [search, setSearch] = useState("");
89
+ const [selectedIds, setSelectedIds] = useState([]);
90
+ const [editingId, setEditingId] = useState(null);
91
+ const [hoveredEffectId, setHoveredEffectId] = useState(null);
92
+ const [pressedEffectId, setPressedEffectId] = useState(null);
93
+ const [effectsPreview, setEffectsPreview] = useState(false);
94
+ const [previewVisibility, setPreviewVisibility] = useState({});
95
+ const [marquee, setMarquee] = useState(null);
96
+ const [codeComposerOpen, setCodeComposerOpen] = useState(false);
97
+ const [codeComponentName, setCodeComponentName] = useState("Code Component");
98
+ const [codeComponentSource, setCodeComponentSource] = useState('<style>body{margin:0;font-family:system-ui;display:grid;place-items:center;height:100vh}button{border:0;border-radius:12px;padding:14px 22px;background:#4f8cff;color:white;font-weight:600}</style><button>Button</button>');
99
+ const [draggedBreakpointId, setDraggedBreakpointId] = useState(null);
100
+ const [editingBreakpointId, setEditingBreakpointId] = useState(null);
101
+ const marqueePageRef = useRef(null);
102
+ const marqueeBaseRef = useRef([]);
103
+ const [contextMenu, setContextMenu] = useState(null);
104
+ const updateBreakpoints = (next) => setRootStyle({
105
+ ...rootStyle,
106
+ breakpoints: next,
107
+ // Keep the current base unless it was the artboard just removed.
108
+ baseBreakpointId: next.some((item) => item.id === cascade.baseId)
109
+ ? cascade.baseId
110
+ : baseOf(cascadeOf(next, undefined)).id,
111
+ });
112
+ /**
113
+ * Moving the base moves where shared values live: the old base's values are
114
+ * already in `element.base`, so the new base needs its own overrides folded
115
+ * in and the old base needs an override carrying what it used to show.
116
+ */
117
+ const setBaseBreakpoint = (nextBaseId) => {
118
+ if (nextBaseId === cascade.baseId)
119
+ return;
120
+ const nextCascade = cascadeOf(breakpointDefs, nextBaseId);
121
+ setElements((els) => els.map((el) => {
122
+ // What each artboard shows today, before anything moves.
123
+ const before = new Map(breakpointDefs.map((bp) => [bp.id, resolveStyle(el, bp.id, cascade)]));
124
+ const base = before.get(nextBaseId);
125
+ if (!base)
126
+ return el;
127
+ const overrides = {};
128
+ for (const bp of breakpointDefs) {
129
+ if (bp.id === nextBaseId)
130
+ continue;
131
+ const effective = before.get(bp.id);
132
+ if (!effective)
133
+ continue;
134
+ // Keep only what actually differs from the new base, so the
135
+ // stored deltas stay meaningful rather than full copies.
136
+ const delta = {};
137
+ for (const key of STYLE_KEYS) {
138
+ if (effective[key] !== base[key]) {
139
+ delta[key] = effective[key];
140
+ }
141
+ }
142
+ if (Object.keys(delta).length > 0)
143
+ overrides[bp.id] = delta;
144
+ }
145
+ return {
146
+ ...el,
147
+ base,
148
+ overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
149
+ };
150
+ }));
151
+ setRootStyle({ ...rootStyle, baseBreakpointId: nextCascade.baseId });
152
+ };
153
+ const addBreakpoint = () => {
154
+ const id = `bp-${Date.now().toString(36)}`;
155
+ const width = Math.max(240, selectedBreakpoint.width - 160);
156
+ updateBreakpoints([
157
+ ...breakpointDefs,
158
+ { id, name: `Breakpoint ${breakpointDefs.length + 1}`, width },
159
+ ]);
160
+ setBreakpoint(id);
161
+ };
162
+ const moveBreakpoint = (sourceId, targetId) => {
163
+ if (sourceId === targetId)
164
+ return;
165
+ const next = [...breakpointDefs];
166
+ const from = next.findIndex((item) => item.id === sourceId);
167
+ const to = next.findIndex((item) => item.id === targetId);
168
+ if (from < 0 || to < 0)
169
+ return;
170
+ const [moved] = next.splice(from, 1);
171
+ next.splice(to, 0, moved);
172
+ updateBreakpoints(next);
173
+ };
174
+ const renameBreakpoint = (id, name) => {
175
+ updateBreakpoints(breakpointDefs.map((item) => item.id === id ? { ...item, name: name.slice(0, 40) } : item));
176
+ };
177
+ const commitBreakpointName = (id) => {
178
+ const item = breakpointDefs.find((entry) => entry.id === id);
179
+ if (item && !item.name.trim())
180
+ renameBreakpoint(id, "Breakpoint");
181
+ };
182
+ const resizeBreakpoint = (id, width) => {
183
+ const clamped = Math.round(Math.max(240, Math.min(3840, width)));
184
+ if (!Number.isFinite(clamped))
185
+ return;
186
+ updateBreakpoints(breakpointDefs.map((item) => item.id === id ? { ...item, width: clamped } : item));
187
+ };
188
+ const removeBreakpoint = (id) => {
189
+ if (breakpointDefs.length < 2)
190
+ return;
191
+ const next = breakpointDefs.filter((item) => item.id !== id);
192
+ // Drop the artboard's stored deltas too, otherwise they linger invisibly
193
+ // and reappear if a breakpoint is later added back under the same id.
194
+ setElements((els) => els.map((el) => {
195
+ if (!el.overrides?.[id])
196
+ return el;
197
+ const overrides = { ...el.overrides };
198
+ delete overrides[id];
199
+ return {
200
+ ...el,
201
+ overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
202
+ };
203
+ }));
204
+ updateBreakpoints(next);
205
+ if (breakpoint === id)
206
+ setBreakpoint(next[0].id);
207
+ };
208
+ /** What window widths an artboard actually governs on the published page. */
209
+ const breakpointRange = (id) => {
210
+ const item = breakpointDefs.find((entry) => entry.id === id);
211
+ if (!item)
212
+ return "";
213
+ const ceiling = breakpointDefs
214
+ .filter((other) => other.width > item.width)
215
+ .reduce((lowest, other) => Math.min(lowest, other.width), Number.POSITIVE_INFINITY);
216
+ return Number.isFinite(ceiling)
217
+ ? `${item.width} — ${ceiling - 1}`
218
+ : `${item.width}+`;
219
+ };
220
+ const beginCanvasResize = (event) => {
221
+ event.preventDefault();
222
+ event.stopPropagation();
223
+ const startY = event.clientY;
224
+ const initial = canvasHeight;
225
+ const move = (pointer) => {
226
+ const height = Math.round(Math.max(1, Math.min(12000, initial + (pointer.clientY - startY) / scale)));
227
+ setCanvasHeight(height);
228
+ if (componentMode && activeComponentMaster)
229
+ setElements((current) => current.map((element) => element.id === activeComponentMaster.id ? { ...element, base: { ...element.base, h: height, heightMode: "fixed" } } : element));
230
+ };
231
+ const up = (pointer) => {
232
+ const height = Math.round(Math.max(1, Math.min(12000, initial + (pointer.clientY - startY) / scale)));
233
+ setCanvasHeight(height);
234
+ setRootStyle({ ...rootStyle, canvasHeight: height });
235
+ window.removeEventListener("mousemove", move);
236
+ window.removeEventListener("mouseup", up);
237
+ };
238
+ window.addEventListener("mousemove", move);
239
+ window.addEventListener("mouseup", up);
240
+ };
241
+ const beginComponentWidthResize = (event) => {
242
+ event.preventDefault();
243
+ event.stopPropagation();
244
+ const startX = event.clientX;
245
+ const initial = activeComponentMaster?.base.w ?? 320;
246
+ const move = (pointer) => {
247
+ const width = Math.round(Math.max(1, Math.min(4000, initial + (pointer.clientX - startX) / scale)));
248
+ if (activeComponentMaster)
249
+ setElements((current) => current.map((element) => element.id === activeComponentMaster.id ? { ...element, base: { ...element.base, w: width, widthMode: "fixed" } } : element));
250
+ };
251
+ const up = () => {
252
+ window.removeEventListener("mousemove", move);
253
+ window.removeEventListener("mouseup", up);
254
+ };
255
+ window.addEventListener("mousemove", move);
256
+ window.addEventListener("mouseup", up);
257
+ };
258
+ const applyAiPlan = (plan) => {
259
+ const pagePatch = plan.operations
260
+ .filter((operation) => operation.kind === "page")
261
+ .reduce((patch, operation) => ({ ...patch, ...operation.style }), {});
262
+ if (Object.keys(pagePatch).length)
263
+ setRootStyle(pagePatch);
264
+ setElements((current) => {
265
+ let next = [...current];
266
+ const refs = new Map();
267
+ const addedIds = new Set();
268
+ for (const operation of plan.operations) {
269
+ if (operation.kind === "page")
270
+ continue;
271
+ if (operation.kind === "remove") {
272
+ if (next.some((element) => element.id === operation.id))
273
+ next = removeSubtree(next, operation.id);
274
+ continue;
275
+ }
276
+ if (operation.kind === "add") {
277
+ const parentId = operation.parentId
278
+ ? refs.get(operation.parentId) ?? operation.parentId
279
+ : undefined;
280
+ const created = createElement(operation.type, {
281
+ x: operation.style?.x ?? 0,
282
+ y: operation.style?.y ?? 0,
283
+ z: nextZ(next, parentId),
284
+ parentId,
285
+ });
286
+ created.base = { ...created.base, ...operation.style };
287
+ created.overrides = {
288
+ ...(operation.tabletStyle
289
+ ? { tablet: operation.tabletStyle }
290
+ : {}),
291
+ ...(operation.mobileStyle
292
+ ? { mobile: operation.mobileStyle }
293
+ : {}),
294
+ };
295
+ if (operation.hoverStyle)
296
+ created.hover = operation.hoverStyle;
297
+ if (operation.pressStyle)
298
+ created.press = operation.pressStyle;
299
+ if (operation.loop)
300
+ created.loop = operation.loop;
301
+ if (operation.draggable !== undefined)
302
+ created.draggable = operation.draggable;
303
+ if (operation.styleBindings)
304
+ created.styleBindings = operation.styleBindings;
305
+ if (operation.interaction)
306
+ created.interaction = {
307
+ ...operation.interaction,
308
+ value: operation.interaction.action === "navigate"
309
+ ? operation.interaction.value
310
+ : refs.get(operation.interaction.value) ?? operation.interaction.value,
311
+ };
312
+ if (operation.content !== undefined)
313
+ created.content = operation.content;
314
+ if (operation.src !== undefined)
315
+ created.src = operation.src;
316
+ if (operation.href !== undefined)
317
+ created.href = operation.href;
318
+ refs.set(operation.ref, created.id);
319
+ addedIds.add(created.id);
320
+ next.push(created);
321
+ continue;
322
+ }
323
+ next = next.map((element) => {
324
+ if (element.id !== operation.id)
325
+ return element;
326
+ const updated = operation.style
327
+ ? applyStyle(element, cascade.baseId, operation.style, cascade)
328
+ : element;
329
+ return {
330
+ ...updated,
331
+ overrides: {
332
+ ...updated.overrides,
333
+ ...(operation.tabletStyle
334
+ ? {
335
+ tablet: {
336
+ ...updated.overrides?.tablet,
337
+ ...operation.tabletStyle,
338
+ },
339
+ }
340
+ : {}),
341
+ ...(operation.mobileStyle
342
+ ? {
343
+ mobile: {
344
+ ...updated.overrides?.mobile,
345
+ ...operation.mobileStyle,
346
+ },
347
+ }
348
+ : {}),
349
+ },
350
+ hover: operation.hoverStyle
351
+ ? { ...updated.hover, ...operation.hoverStyle }
352
+ : updated.hover,
353
+ press: operation.pressStyle
354
+ ? { ...updated.press, ...operation.pressStyle }
355
+ : updated.press,
356
+ loop: operation.loop ?? updated.loop,
357
+ draggable: operation.draggable ?? updated.draggable,
358
+ styleBindings: operation.styleBindings
359
+ ? { ...updated.styleBindings, ...operation.styleBindings }
360
+ : updated.styleBindings,
361
+ interaction: operation.interaction ? {
362
+ ...operation.interaction,
363
+ value: operation.interaction.action === "navigate"
364
+ ? operation.interaction.value
365
+ : refs.get(operation.interaction.value) ?? operation.interaction.value,
366
+ } : updated.interaction,
367
+ ...(operation.content !== undefined
368
+ ? { content: operation.content }
369
+ : {}),
370
+ ...(operation.src !== undefined ? { src: operation.src } : {}),
371
+ ...(operation.href !== undefined ? { href: operation.href } : {}),
372
+ };
373
+ });
374
+ }
375
+ const generatedById = new Map(next.map((element) => [element.id, element]));
376
+ return next.map((element) => {
377
+ if (!addedIds.has(element.id))
378
+ return element;
379
+ const parent = element.parentId
380
+ ? generatedById.get(element.parentId)
381
+ : undefined;
382
+ const parentStyle = parent?.base;
383
+ const hasChildren = next.some((child) => child.parentId === element.id);
384
+ const base = { ...element.base };
385
+ // Keep generated compositions in the flow. Absolute children
386
+ // and fixed section heights are the main source of overlaps
387
+ // and fake-looking AI pages.
388
+ if (element.type === "Section") {
389
+ base.widthMode = "fill";
390
+ base.heightMode = "auto";
391
+ if (hasChildren)
392
+ base.layout = "stack";
393
+ }
394
+ if (hasChildren &&
395
+ ["Frame", "Stack", "Container", "Grid", "Request", "Repeat"].includes(element.type) &&
396
+ base.layout === "absolute") {
397
+ base.layout = "stack";
398
+ }
399
+ if (["Heading", "Text", "Button"].includes(element.type)) {
400
+ base.heightMode = "auto";
401
+ }
402
+ if (element.type === "Button") {
403
+ base.layout = "stack";
404
+ base.justify = "center";
405
+ base.align = "center";
406
+ if (parentStyle?.align === "center" && base.widthMode === "fill")
407
+ base.widthMode = "auto";
408
+ }
409
+ return { ...element, base };
410
+ });
411
+ });
412
+ };
413
+ const [dragInfo, setDragInfo] = useState(null);
414
+ const [resizeInfo, setResizeInfo] = useState(null);
415
+ const [guides, setGuides] = useState({ origin: { x: 0, y: 0 }, lines: [] });
416
+ const [ghost, setGhost] = useState(null);
417
+ const [dropTargetId, setDropTargetId] = useState(undefined);
418
+ const [clipboard, setClipboard] = useState(null);
419
+ /** Rows fetched by the Data panel, used to preview Repeat blocks. */
420
+ const [samples, setSamples] = useState({});
421
+ useEffect(() => {
422
+ // Page-local state must not leak into the next document. Chrome state
423
+ // such as leftTab/rightTab intentionally remains untouched.
424
+ setSelectedIds([]);
425
+ setEditingId(null);
426
+ setHoveredEffectId(null);
427
+ setPressedEffectId(null);
428
+ setPreviewVisibility({});
429
+ setSamples({});
430
+ setContextMenu(null);
431
+ }, [page.id]);
432
+ const [pageError, setPageError] = useState(null);
433
+ const canvasRef = useRef(null);
434
+ const [canvasHeight, setCanvasHeight] = useState(rootStyle.canvasHeight);
435
+ const [contentHeight, setContentHeight] = useState(rootStyle.canvasHeight);
436
+ const displayCanvasHeight = Math.max(canvasHeight, contentHeight);
437
+ const byId = useMemo(() => indexById(elements), [elements]);
438
+ const componentAssetIds = useMemo(() => {
439
+ const ids = new Set();
440
+ for (const master of elements.filter((element) => element.componentRole === "master"))
441
+ for (const id of subtreeIds(elements, master.id))
442
+ ids.add(id);
443
+ return ids;
444
+ }, [elements]);
445
+ const activeComponentIds = useMemo(() => activeComponentMaster ? subtreeIds(elements, activeComponentMaster.id) : new Set(), [activeComponentMaster, elements]);
446
+ const visibleEditorElements = useMemo(() => elements.filter((element) => componentMode ? activeComponentIds.has(element.id) : !componentAssetIds.has(element.id)), [activeComponentIds, componentAssetIds, componentMode, elements]);
447
+ const selectedId = selectedIds.length === 1 ? selectedIds[0] : null;
448
+ const selectedElement = selectedId ? byId.get(selectedId) : undefined;
449
+ const deviceWidth = frameWidthForBreakpoint;
450
+ const frameWidth = rootStyle.fullWidth
451
+ ? deviceWidth
452
+ : Math.min(deviceWidth, rootStyle.maxWidth);
453
+ useEffect(() => setCanvasHeight(rootStyle.canvasHeight), [rootStyle.canvasHeight]);
454
+ useEffect(() => {
455
+ if (!componentMode || !activeComponentMaster)
456
+ return;
457
+ setCanvasHeight(Math.max(1, activeComponentMaster.base.h));
458
+ setContentHeight(Math.max(1, activeComponentMaster.base.h));
459
+ }, [activeComponentMaster?.base.h, activeComponentMaster?.id, componentMode]);
460
+ useEffect(() => {
461
+ const node = canvasRef.current;
462
+ if (!node)
463
+ return;
464
+ const observer = new ResizeObserver(([entry]) => setContentHeight(Math.ceil(entry.contentRect.height)));
465
+ observer.observe(node);
466
+ return () => observer.disconnect();
467
+ }, [breakpoint, componentMode]);
468
+ /** How the parent of `element` arranges its children at this breakpoint. */
469
+ const contextFor = useCallback((element, bp = breakpoint) => {
470
+ const parent = element.parentId ? byId.get(element.parentId) : undefined;
471
+ const parentStyle = parent ? resolveStyle(parent, bp, cascade) : undefined;
472
+ return {
473
+ parentLayout: parentStyle?.layout ?? rootStyle.layout,
474
+ parentDirection: parentStyle?.direction ?? rootStyle.direction,
475
+ };
476
+ }, [byId, breakpoint, rootStyle.direction, rootStyle.layout]);
477
+ /* ---------------------------------------------------------------- edits */
478
+ const patchStyle = useCallback((ids, patch) => {
479
+ setElements((els) => els.map((el) => ids.includes(el.id) ? applyStyleIsolated(el, breakpoint, patch, breakpointDefs.map((definition) => definition.id), cascade) : el));
480
+ }, [breakpoint, breakpointDefs, cascade, setElements]);
481
+ const patchProps = useCallback((id, patch) => {
482
+ setElements((els) => els.map((el) => (el.id === id ? { ...el, ...patch } : el)));
483
+ }, [setElements]);
484
+ const resetOverrides = useCallback((ids, keys) => {
485
+ setElements((els) => els.map((el) => ids.includes(el.id) ? clearOverrides(el, breakpoint, keys, cascade) : el));
486
+ }, [breakpoint, cascade, setElements]);
487
+ const deleteElements = useCallback((ids) => {
488
+ // Children live inside their parent, so they have to go with it.
489
+ const doomed = new Set();
490
+ for (const id of ids) {
491
+ for (const sub of subtreeIds(elements, id))
492
+ doomed.add(sub);
493
+ }
494
+ setElements((els) => ids.reduce((acc, id) => removeSubtree(acc, id), els));
495
+ setSelectedIds((current) => current.filter((id) => !doomed.has(id)));
496
+ setEditingId((current) => (current && doomed.has(current) ? null : current));
497
+ setContextMenu(null);
498
+ }, [elements, setElements]);
499
+ const duplicateElements = useCallback((ids) => {
500
+ const additions = [];
501
+ const roots = [];
502
+ for (const id of ids) {
503
+ const clone = cloneSubtree(elements, id);
504
+ if (!clone)
505
+ continue;
506
+ additions.push(...clone.elements);
507
+ roots.push(clone.rootId);
508
+ }
509
+ if (additions.length === 0)
510
+ return;
511
+ setElements((els) => [...els, ...additions]);
512
+ setSelectedIds(roots);
513
+ setContextMenu(null);
514
+ }, [elements, setElements]);
515
+ const copyElements = useCallback((ids) => {
516
+ if (ids.length === 0)
517
+ return;
518
+ const wanted = new Set();
519
+ for (const id of ids) {
520
+ for (const sub of subtreeIds(elements, id))
521
+ wanted.add(sub);
522
+ }
523
+ setClipboard({
524
+ elements: elements.filter((el) => wanted.has(el.id)),
525
+ rootId: ids[0],
526
+ });
527
+ }, [elements]);
528
+ const pasteClipboard = useCallback(() => {
529
+ if (!clipboard)
530
+ return;
531
+ const clone = cloneSubtree(clipboard.elements, clipboard.rootId);
532
+ if (!clone)
533
+ return;
534
+ // Paste into the selection when it can hold children, otherwise beside it.
535
+ const anchor = selectedId ? byId.get(selectedId) : undefined;
536
+ const parentId = anchor
537
+ ? isContainer(anchor.type)
538
+ ? anchor.id
539
+ : anchor.parentId
540
+ : undefined;
541
+ const z = nextZ(elements, parentId);
542
+ setElements((els) => [
543
+ ...els,
544
+ ...clone.elements.map((el) => el.id === clone.rootId ? { ...el, parentId, z } : el),
545
+ ]);
546
+ setSelectedIds([clone.rootId]);
547
+ }, [byId, clipboard, elements, selectedId, setElements]);
548
+ const insertFromLibrary = useCallback((pick) => {
549
+ const clone = cloneSubtree(pick.elements, pick.rootId, { x: 0, y: 0 });
550
+ if (!clone)
551
+ return;
552
+ // Drop it into the selected container when there is one, so a block
553
+ // can be nested rather than always landing at the page root.
554
+ const anchor = selectedId ? byId.get(selectedId) : undefined;
555
+ const parentId = anchor && isContainer(anchor.type) ? anchor.id : undefined;
556
+ const z = nextZ(elements, parentId);
557
+ setElements((els) => [
558
+ ...els,
559
+ ...clone.elements.map((el) => el.id === clone.rootId ? { ...el, parentId, z } : el),
560
+ ]);
561
+ setSelectedIds([clone.rootId]);
562
+ }, [byId, elements, selectedId, setElements]);
563
+ const insertElement = useCallback((type, props) => {
564
+ const anchor = selectedId ? byId.get(selectedId) : undefined;
565
+ const parentId = anchor && isContainer(anchor.type) ? anchor.id : undefined;
566
+ const element = createElement(type, {
567
+ x: 32,
568
+ y: 32,
569
+ z: nextZ(elements, parentId),
570
+ parentId,
571
+ });
572
+ Object.assign(element, props);
573
+ setElements((current) => [...current, element]);
574
+ setSelectedIds([element.id]);
575
+ }, [byId, elements, selectedId, setElements]);
576
+ const createCodeComponent = () => {
577
+ const master = createElement("Frame", { x: 0, y: 0, z: nextZ(elements) });
578
+ master.name = codeComponentName.trim() || "Code Component";
579
+ master.code = codeComponentSource;
580
+ master.componentRole = "master";
581
+ master.componentId = master.id;
582
+ master.componentSourceId = master.id;
583
+ master.variant = "Default";
584
+ master.base = { ...master.base, w: 360, h: 220, widthMode: "fixed", heightMode: "fixed", overflow: "hidden", bg: "#ffffff", radius: 16 };
585
+ setElements((current) => [...current, master]);
586
+ setRootStyle({ ...rootStyle, documentMode: "component", maxWidth: Math.max(rootStyle.maxWidth, 760) });
587
+ setActiveComponentMasterId(master.id);
588
+ setSelectedIds([master.id]);
589
+ setLeftTab("Components");
590
+ setCodeComposerOpen(false);
591
+ };
592
+ const createBlankComponent = () => {
593
+ const master = createElement("Frame", { x: 0, y: 0, z: nextZ(elements) });
594
+ master.name = `Component ${componentMasters.length + 1}`;
595
+ master.componentRole = "master";
596
+ master.componentId = master.id;
597
+ master.componentSourceId = master.id;
598
+ master.variant = "Default";
599
+ master.base = { ...master.base, w: 320, h: 180, widthMode: "fixed", heightMode: "fixed", layout: "absolute", bg: "#ffffff", radius: 16, overflow: "hidden" };
600
+ setElements((current) => [...current, master]);
601
+ setRootStyle({ ...rootStyle, documentMode: "component", maxWidth: Math.max(rootStyle.maxWidth, 760) });
602
+ setActiveComponentMasterId(master.id);
603
+ setSelectedIds([master.id]);
604
+ setBreakpoint("desktop");
605
+ setLeftTab("Components");
606
+ };
607
+ const createComponentInstance = useCallback(() => {
608
+ if (!selectedElement?.componentId || selectedElement.componentRole !== "master")
609
+ return;
610
+ const clone = cloneSubtree(elements, selectedElement.id, { x: 32, y: 32 });
611
+ if (!clone)
612
+ return;
613
+ const copies = clone.elements.map((element) => ({
614
+ ...element,
615
+ componentRole: element.id === clone.rootId ? "instance" : undefined,
616
+ componentId: selectedElement.componentId,
617
+ variant: element.id === clone.rootId ? selectedElement.variant : undefined,
618
+ }));
619
+ setElements((current) => [...current, ...copies]);
620
+ setSelectedIds([clone.rootId]);
621
+ }, [elements, selectedElement, setElements]);
622
+ const createComponentVariant = useCallback(() => {
623
+ if (!selectedElement?.componentId || selectedElement.componentRole !== "master")
624
+ return;
625
+ const siblings = elements.filter((element) => element.componentRole === "master" &&
626
+ element.componentId === selectedElement.componentId);
627
+ const clone = cloneSubtree(elements, selectedElement.id, { x: 0, y: 0 });
628
+ if (!clone)
629
+ return;
630
+ const copies = clone.elements.map((element) => ({
631
+ ...element,
632
+ componentRole: element.id === clone.rootId ? "master" : undefined,
633
+ componentId: selectedElement.componentId,
634
+ variant: element.id === clone.rootId
635
+ ? `Variant ${siblings.length + 1}`
636
+ : undefined,
637
+ }));
638
+ setElements((current) => [...current, ...copies]);
639
+ setActiveComponentMasterId(clone.rootId);
640
+ setSelectedIds([clone.rootId]);
641
+ }, [elements, selectedElement, setElements]);
642
+ const switchInstanceVariant = useCallback((variant) => {
643
+ if (!selectedElement?.componentId ||
644
+ selectedElement.componentRole !== "instance")
645
+ return;
646
+ const master = elements.find((element) => element.componentRole === "master" &&
647
+ element.componentId === selectedElement.componentId &&
648
+ element.variant === variant);
649
+ if (!master)
650
+ return;
651
+ const masterIds = subtreeIds(elements, master.id);
652
+ const sourceBySlot = new Map(elements
653
+ .filter((element) => masterIds.has(element.id))
654
+ .map((element) => [element.componentSourceId, element]));
655
+ const instanceIds = subtreeIds(elements, selectedElement.id);
656
+ setElements((current) => current.map((element) => {
657
+ if (!instanceIds.has(element.id))
658
+ return element;
659
+ const source = sourceBySlot.get(element.componentSourceId);
660
+ if (!source)
661
+ return element;
662
+ const position = element.id === selectedElement.id
663
+ ? {
664
+ x: element.base.x,
665
+ y: element.base.y,
666
+ constraintX: element.base.constraintX,
667
+ constraintY: element.base.constraintY,
668
+ }
669
+ : {};
670
+ return {
671
+ ...element,
672
+ type: source.type,
673
+ content: source.content,
674
+ code: source.code,
675
+ src: source.src,
676
+ href: source.href,
677
+ base: { ...source.base, ...position },
678
+ overrides: source.overrides,
679
+ hover: source.hover,
680
+ press: source.press,
681
+ loop: source.loop,
682
+ draggable: source.draggable,
683
+ variant: element.id === selectedElement.id ? variant : undefined,
684
+ };
685
+ }));
686
+ }, [elements, selectedElement, setElements]);
687
+ const nudge = useCallback((dx, dy) => {
688
+ const movable = selectedIds.filter((id) => {
689
+ const el = byId.get(id);
690
+ return el && !el.locked && contextFor(el).parentLayout === "absolute";
691
+ });
692
+ if (movable.length === 0)
693
+ return;
694
+ setElements((els) => els.map((el) => {
695
+ if (!movable.includes(el.id))
696
+ return el;
697
+ const style = resolveStyle(el, breakpoint, cascade);
698
+ return applyStyleIsolated(el, breakpoint, {
699
+ x: style.x + dx,
700
+ y: style.y + dy,
701
+ }, breakpointDefs.map((definition) => definition.id), cascade);
702
+ }));
703
+ }, [breakpoint, breakpointDefs, byId, cascade, contextFor, selectedIds, setElements]);
704
+ const doReparent = useCallback((id, parentId, beforeId) => {
705
+ if (!componentMode && parentId && componentAssetIds.has(parentId))
706
+ return;
707
+ setElements((els) => reparent(els, id, parentId, breakpoint, beforeId));
708
+ }, [breakpoint, componentAssetIds, componentMode, setElements]);
709
+ const switchPageLayout = (layout) => {
710
+ if (layout === rootStyle.layout)
711
+ return;
712
+ if (layout === "absolute") {
713
+ const canvas = canvasRef.current;
714
+ const canvasRect = canvas?.getBoundingClientRect();
715
+ if (canvas && canvasRect) {
716
+ const rootIds = new Set(childrenOf(visibleEditorElements, undefined).map((element) => element.id));
717
+ setElements((current) => current.map((element) => {
718
+ if (!rootIds.has(element.id))
719
+ return element;
720
+ const node = canvas.querySelector(`[data-canvas-element="${element.id}"]`);
721
+ if (!node)
722
+ return element;
723
+ const rect = node.getBoundingClientRect();
724
+ return applyStyleIsolated(element, breakpoint, { x: (rect.left - canvasRect.left) / scale, y: (rect.top - canvasRect.top) / scale }, breakpointDefs.map((definition) => definition.id), cascade);
725
+ }));
726
+ }
727
+ setRootStyle({ ...rootStyle, layout: "absolute", padT: 0, padR: 0, padB: 0, padL: 0 });
728
+ }
729
+ else {
730
+ const rootIds = new Set(childrenOf(visibleEditorElements, undefined).map((element) => element.id));
731
+ setElements((current) => current.map((element) => rootIds.has(element.id) ? applyStyleIsolated(element, breakpoint, { x: 0, y: 0 }, breakpointDefs.map((definition) => definition.id), cascade) : element));
732
+ setRootStyle({ ...rootStyle, layout: "stack" });
733
+ }
734
+ };
735
+ const applyStyleKit = (kit) => {
736
+ const palette = {
737
+ midnight: { bg: "#090b10", surface: "#121620", text: "#f8fafc", muted: "#94a3b8", accent: "#7c5cff", border: "#252b38" },
738
+ editorial: { bg: "#f3efe7", surface: "#fffdf8", text: "#171512", muted: "#746f66", accent: "#e14b32", border: "#d8d0c2" },
739
+ cobalt: { bg: "#071425", surface: "#0d2038", text: "#f0f7ff", muted: "#91a9c2", accent: "#2f80ff", border: "#1d3a59" },
740
+ warm: { bg: "#18110f", surface: "#251916", text: "#fff7ed", muted: "#c6a99b", accent: "#f97316", border: "#44302a" },
741
+ }[kit];
742
+ setRootStyle({ ...rootStyle, bg: palette.bg, variables: [
743
+ { id: "kit-bg", name: "Background", type: "color", value: palette.bg },
744
+ { id: "kit-surface", name: "Surface", type: "color", value: palette.surface },
745
+ { id: "kit-text", name: "Text", type: "color", value: palette.text },
746
+ { id: "kit-muted", name: "Muted", type: "color", value: palette.muted },
747
+ { id: "kit-accent", name: "Accent", type: "color", value: palette.accent },
748
+ ] });
749
+ setElements((current) => current.map((element) => {
750
+ if (componentAssetIds.has(element.id))
751
+ return element;
752
+ const base = { ...element.base };
753
+ if (element.type === "Button") {
754
+ base.bg = palette.accent;
755
+ base.color = "#ffffff";
756
+ base.borderC = palette.accent;
757
+ }
758
+ else if (isTextual(element.type))
759
+ base.color = element.type === "Text" ? palette.muted : palette.text;
760
+ else if (base.bg && base.bg !== "transparent") {
761
+ base.bg = palette.surface;
762
+ base.borderC = palette.border;
763
+ }
764
+ return { ...element, base };
765
+ }));
766
+ };
767
+ const loadNocturneShowcase = () => {
768
+ if (elements.some((element) => !componentAssetIds.has(element.id)) && !window.confirm("This replaces the current page canvas with the Nocturne showcase. Continue?"))
769
+ return;
770
+ const showcase = createNocturneShowcase();
771
+ beginTransaction();
772
+ setElements((current) => [...current.filter((element) => componentAssetIds.has(element.id)), ...showcase.elements]);
773
+ setRootStyle(showcase.rootStyle);
774
+ setSelectedIds([]);
775
+ setBreakpoint("desktop");
776
+ setLeftTab("Layers");
777
+ endTransaction();
778
+ window.setTimeout(() => zoomToFit(), 80);
779
+ };
780
+ const select = useCallback((id, additive) => {
781
+ setSelectedIds((current) => {
782
+ if (!additive)
783
+ return [id];
784
+ return current.includes(id)
785
+ ? current.filter((x) => x !== id)
786
+ : [...current, id];
787
+ });
788
+ }, []);
789
+ /* ------------------------------------------------------ drag and resize */
790
+ const gesturing = dragInfo !== null || resizeInfo !== null;
791
+ useEffect(() => {
792
+ if (!gesturing)
793
+ return;
794
+ const handleMouseMove = (event) => {
795
+ const gestureBreakpoint = dragInfo?.breakpoint ?? resizeInfo?.breakpoint ?? breakpoint;
796
+ if (dragInfo) {
797
+ const moving = byId.get(dragInfo.id);
798
+ if (!moving)
799
+ return;
800
+ const dx = (event.clientX - dragInfo.startX) / scale;
801
+ const dy = (event.clientY - dragInfo.startY) / scale;
802
+ // Which container would receive the element if dropped here.
803
+ const rect = canvasRef.current?.getBoundingClientRect();
804
+ if (rect) {
805
+ const point = {
806
+ x: (event.clientX - rect.left) / scale,
807
+ y: (event.clientY - rect.top) / scale,
808
+ };
809
+ setDropTargetId(containerAt(visibleEditorElements, byId, point, gestureBreakpoint, dragInfo.id));
810
+ }
811
+ if (dragInfo.mode === "reflow") {
812
+ // Position is owned by the parent's layout, so only show a
813
+ // ghost offset while the pointer looks for a new home.
814
+ setGhost({ dx, dy });
815
+ return;
816
+ }
817
+ const style = resolveStyle(moving, gestureBreakpoint, cascade);
818
+ const proposed = {
819
+ x: dragInfo.initialX + dx,
820
+ y: dragInfo.initialY + dy,
821
+ w: style.w,
822
+ h: style.h,
823
+ };
824
+ const parent = moving.parentId ? byId.get(moving.parentId) : undefined;
825
+ const parentStyle = parent ? resolveStyle(parent, gestureBreakpoint, cascade) : undefined;
826
+ const gestureFrameWidth = breakpointDefs.find((definition) => definition.id === gestureBreakpoint)?.width ?? frameWidth;
827
+ const frame = parentStyle
828
+ ? { w: parentStyle.w, h: parentStyle.h }
829
+ : { w: gestureFrameWidth, h: canvasHeight };
830
+ // Alt suspends snapping for pixel-exact placement.
831
+ const snapped = event.altKey
832
+ ? { ...proposed, guides: [] }
833
+ : snapPosition(elements, dragInfo.id, proposed, frame, gestureBreakpoint, 6 / scale);
834
+ setGuides({
835
+ origin: parent
836
+ ? absolutePosition(byId, parent, gestureBreakpoint, cascade)
837
+ : { x: 0, y: 0 },
838
+ lines: snapped.guides,
839
+ });
840
+ setElements((els) => els.map((el) => el.id === dragInfo.id
841
+ ? applyStyleIsolated(el, gestureBreakpoint, { x: snapped.x, y: snapped.y }, breakpointDefs.map((definition) => definition.id), cascade)
842
+ : el));
843
+ return;
844
+ }
845
+ if (!resizeInfo)
846
+ return;
847
+ const dx = (event.clientX - resizeInfo.startX) / scale;
848
+ const dy = (event.clientY - resizeInfo.startY) / scale;
849
+ const { initialX, initialY, initialW, initialH, handle } = resizeInfo;
850
+ let x = initialX;
851
+ let y = initialY;
852
+ let w = initialW;
853
+ let h = initialH;
854
+ const resizing = byId.get(resizeInfo.id);
855
+ const minSize = resizing?.componentRole === "master" ? 1 : MIN_SIZE;
856
+ if (handle.includes("e"))
857
+ w = Math.max(minSize, initialW + dx);
858
+ if (handle.includes("s"))
859
+ h = Math.max(minSize, initialH + dy);
860
+ if (handle.includes("w") && initialW - dx >= minSize) {
861
+ w = initialW - dx;
862
+ x = initialX + dx;
863
+ }
864
+ if (handle.includes("n") && initialH - dy >= minSize) {
865
+ h = initialH - dy;
866
+ y = initialY + dy;
867
+ }
868
+ setElements((els) => els.map((el) => el.id === resizeInfo.id
869
+ ? applyStyleIsolated(el, gestureBreakpoint, { x, y, w, h }, breakpointDefs.map((definition) => definition.id), cascade)
870
+ : el));
871
+ };
872
+ const handleMouseUp = () => {
873
+ if (dragInfo) {
874
+ const moving = byId.get(dragInfo.id);
875
+ const currentParent = moving?.parentId;
876
+ // `undefined` means the page root, which is a valid destination,
877
+ // so only skip when nothing was hovered at all.
878
+ if (dragInfo.mode === "reflow" &&
879
+ dragInfo.breakpoint === cascade.baseId &&
880
+ dropTargetId !== undefined &&
881
+ dropTargetId !== currentParent) {
882
+ doReparent(dragInfo.id, dropTargetId ?? undefined);
883
+ }
884
+ }
885
+ setDragInfo(null);
886
+ setPressedEffectId(null);
887
+ setResizeInfo(null);
888
+ setGhost(null);
889
+ setDropTargetId(undefined);
890
+ setGuides({ origin: { x: 0, y: 0 }, lines: [] });
891
+ // The whole gesture lands in the undo stack as a single step.
892
+ endTransaction();
893
+ };
894
+ window.addEventListener("mousemove", handleMouseMove);
895
+ window.addEventListener("mouseup", handleMouseUp);
896
+ return () => {
897
+ window.removeEventListener("mousemove", handleMouseMove);
898
+ window.removeEventListener("mouseup", handleMouseUp);
899
+ };
900
+ }, [
901
+ breakpoint,
902
+ breakpointDefs,
903
+ byId,
904
+ cascade,
905
+ canvasHeight,
906
+ doReparent,
907
+ dragInfo,
908
+ dropTargetId,
909
+ elements,
910
+ endTransaction,
911
+ frameWidth,
912
+ gesturing,
913
+ resizeInfo,
914
+ scale,
915
+ setElements,
916
+ visibleEditorElements,
917
+ ]);
918
+ const handleElementMouseDown = (event, el, bp = breakpoint) => {
919
+ // Space-drag and middle-drag pan the canvas instead of moving elements.
920
+ if (tryBeginPan(event))
921
+ return;
922
+ event.stopPropagation();
923
+ // Editing in a frame switches the toolbar to that frame's breakpoint,
924
+ // so the inspector and the drag write to the same layer.
925
+ if (bp !== breakpoint)
926
+ setBreakpoint(bp);
927
+ if (effectsPreview) {
928
+ if (el.press)
929
+ setPressedEffectId(el.id);
930
+ const interaction = el.interaction;
931
+ if (interaction && ["toggle-layer", "show-layer", "hide-layer"].includes(interaction.action)) {
932
+ const target = byId.get(interaction.value);
933
+ if (target)
934
+ setPreviewVisibility((current) => {
935
+ const currentlyVisible = current[target.id] ?? !resolveStyle(target, bp, cascade).hidden;
936
+ const visible = interaction.action === "show-layer" ? true : interaction.action === "hide-layer" ? false : !currentlyVisible;
937
+ return { ...current, [target.id]: visible };
938
+ });
939
+ }
940
+ return;
941
+ }
942
+ select(el.id, event.shiftKey || event.metaKey);
943
+ if (editingId !== el.id)
944
+ setEditingId(null);
945
+ if (el.locked || editingId === el.id)
946
+ return;
947
+ const style = resolveStyle(el, bp, cascade);
948
+ beginTransaction();
949
+ setDragInfo({
950
+ id: el.id,
951
+ breakpoint: bp,
952
+ mode: contextFor(el).parentLayout === "absolute" ? "free" : "reflow",
953
+ startX: event.clientX,
954
+ startY: event.clientY,
955
+ initialX: style.x,
956
+ initialY: style.y,
957
+ });
958
+ };
959
+ const handleResizeMouseDown = (event, handle, el, bp) => {
960
+ event.stopPropagation();
961
+ if (bp !== breakpoint)
962
+ setBreakpoint(bp);
963
+ setSelectedIds([el.id]);
964
+ const style = resolveStyle(el, bp, cascade);
965
+ beginTransaction();
966
+ setResizeInfo({
967
+ id: el.id,
968
+ breakpoint: bp,
969
+ handle,
970
+ startX: event.clientX,
971
+ startY: event.clientY,
972
+ initialX: style.x,
973
+ initialY: style.y,
974
+ initialW: style.w,
975
+ initialH: style.h,
976
+ });
977
+ };
978
+ const beginMarquee = (event, bp) => {
979
+ if (event.button !== 0 || spaceHeld || tryBeginPan(event))
980
+ return;
981
+ event.preventDefault();
982
+ if (bp !== breakpoint)
983
+ setBreakpoint(bp);
984
+ marqueePageRef.current = (event.currentTarget.closest("[data-canvas-page]") ?? event.currentTarget);
985
+ marqueeBaseRef.current = event.shiftKey || event.metaKey ? selectedIds : [];
986
+ if (!marqueeBaseRef.current.length)
987
+ setSelectedIds([]);
988
+ setEditingId(null);
989
+ setMarquee({ startX: event.clientX, startY: event.clientY, x: event.clientX, y: event.clientY });
990
+ };
991
+ useEffect(() => {
992
+ if (!marquee)
993
+ return;
994
+ const move = (event) => {
995
+ const left = Math.min(marquee.startX, event.clientX);
996
+ const top = Math.min(marquee.startY, event.clientY);
997
+ const right = Math.max(marquee.startX, event.clientX);
998
+ const bottom = Math.max(marquee.startY, event.clientY);
999
+ const hits = Array.from(marqueePageRef.current?.querySelectorAll("[data-canvas-element]") ?? []).flatMap((node) => {
1000
+ const rect = node.getBoundingClientRect();
1001
+ const intersects = rect.right >= left && rect.left <= right && rect.bottom >= top && rect.top <= bottom;
1002
+ const id = node.dataset.canvasElement;
1003
+ return intersects && id ? [id] : [];
1004
+ });
1005
+ setSelectedIds(Array.from(new Set([...marqueeBaseRef.current, ...hits])));
1006
+ setMarquee((current) => current ? { ...current, x: event.clientX, y: event.clientY } : null);
1007
+ };
1008
+ const up = () => {
1009
+ setMarquee(null);
1010
+ marqueePageRef.current = null;
1011
+ };
1012
+ window.addEventListener("mousemove", move);
1013
+ window.addEventListener("mouseup", up, { once: true });
1014
+ return () => {
1015
+ window.removeEventListener("mousemove", move);
1016
+ window.removeEventListener("mouseup", up);
1017
+ };
1018
+ }, [marquee?.startX, marquee?.startY]);
1019
+ /* ------------------------------------------------------------- dropping */
1020
+ const handleDrop = (event, parentId) => {
1021
+ event.preventDefault();
1022
+ event.stopPropagation();
1023
+ setDropTargetId(undefined);
1024
+ // An existing layer dragged out of the Layers panel.
1025
+ const movedId = event.dataTransfer.getData(MOVE_MIME);
1026
+ if (movedId) {
1027
+ doReparent(movedId, parentId ?? undefined);
1028
+ setSelectedIds([movedId]);
1029
+ return;
1030
+ }
1031
+ const componentId = event.dataTransfer.getData(COMPONENT_MIME);
1032
+ if (componentId) {
1033
+ const master = elements.find((element) => element.id === componentId && element.componentRole === "master");
1034
+ if (!master)
1035
+ return;
1036
+ const rect = event.currentTarget.getBoundingClientRect();
1037
+ const style = resolveStyle(master, cascade.baseId, cascade);
1038
+ const x = Math.max(0, (event.clientX - rect.left) / scale - style.w / 2);
1039
+ const y = Math.max(0, (event.clientY - rect.top) / scale - style.h / 2);
1040
+ const clone = cloneSubtree(elements, master.id, { x: x - style.x, y: y - style.y });
1041
+ if (!clone)
1042
+ return;
1043
+ const copies = clone.elements.map((element) => element.id === clone.rootId ? { ...element, parentId: parentId ?? undefined, componentRole: "instance", componentId: master.componentId, variant: master.variant, z: nextZ(elements, parentId ?? undefined) } : { ...element, componentRole: undefined });
1044
+ setElements((current) => [...current, ...copies]);
1045
+ setSelectedIds([clone.rootId]);
1046
+ return;
1047
+ }
1048
+ const type = event.dataTransfer.getData(DRAG_MIME);
1049
+ if (!type)
1050
+ return;
1051
+ // The drop target's rect is already scaled by the zoom transform, so
1052
+ // dividing brings the pointer back into canvas coordinates.
1053
+ const rect = event.currentTarget.getBoundingClientRect();
1054
+ const x = (event.clientX - rect.left) / scale;
1055
+ const y = (event.clientY - rect.top) / scale;
1056
+ const element = createElement(type, {
1057
+ x: 0,
1058
+ y: 0,
1059
+ z: nextZ(elements, parentId ?? undefined),
1060
+ parentId: parentId ?? undefined,
1061
+ });
1062
+ element.base.x = Math.max(0, x - element.base.w / 2);
1063
+ element.base.y = Math.max(0, y - element.base.h / 2);
1064
+ setElements((els) => [...els, element]);
1065
+ setSelectedIds([element.id]);
1066
+ };
1067
+ /* ------------------------------------------------------------ shortcuts */
1068
+ useEffect(() => {
1069
+ const handler = (event) => {
1070
+ const target = event.target;
1071
+ const typing = !!target &&
1072
+ (target.isContentEditable ||
1073
+ ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
1074
+ if (event.key === "Escape") {
1075
+ if (editingId)
1076
+ setEditingId(null);
1077
+ else
1078
+ setSelectedIds([]);
1079
+ setContextMenu(null);
1080
+ return;
1081
+ }
1082
+ const mod = event.metaKey || event.ctrlKey;
1083
+ const key = event.key.toLowerCase();
1084
+ // These stay live even while a field has focus.
1085
+ if (mod && key === "s") {
1086
+ event.preventDefault();
1087
+ saveNow();
1088
+ return;
1089
+ }
1090
+ if (mod && key === "z") {
1091
+ event.preventDefault();
1092
+ if (event.shiftKey)
1093
+ redo();
1094
+ else
1095
+ undo();
1096
+ return;
1097
+ }
1098
+ if (mod && key === "y") {
1099
+ event.preventDefault();
1100
+ redo();
1101
+ return;
1102
+ }
1103
+ if (mod && (event.key === "=" || event.key === "+")) {
1104
+ event.preventDefault();
1105
+ stepZoom(1);
1106
+ return;
1107
+ }
1108
+ if (mod && event.key === "-") {
1109
+ event.preventDefault();
1110
+ stepZoom(-1);
1111
+ return;
1112
+ }
1113
+ if (mod && event.key === "0") {
1114
+ event.preventDefault();
1115
+ zoomTo(100);
1116
+ return;
1117
+ }
1118
+ // Everything below would fight with normal text entry.
1119
+ if (typing)
1120
+ return;
1121
+ if (mod && key === "a") {
1122
+ event.preventDefault();
1123
+ setSelectedIds(elements.map((el) => el.id));
1124
+ return;
1125
+ }
1126
+ if (mod && key === "c" && selectedIds.length) {
1127
+ copyElements(selectedIds);
1128
+ return;
1129
+ }
1130
+ if (mod && key === "x" && selectedIds.length) {
1131
+ copyElements(selectedIds);
1132
+ deleteElements(selectedIds);
1133
+ return;
1134
+ }
1135
+ if (mod && key === "v") {
1136
+ event.preventDefault();
1137
+ pasteClipboard();
1138
+ return;
1139
+ }
1140
+ if (mod && key === "d" && selectedIds.length) {
1141
+ event.preventDefault();
1142
+ duplicateElements(selectedIds);
1143
+ return;
1144
+ }
1145
+ if ((event.key === "Delete" || event.key === "Backspace") && selectedIds.length) {
1146
+ event.preventDefault();
1147
+ deleteElements(selectedIds);
1148
+ return;
1149
+ }
1150
+ const step = event.shiftKey ? 10 : 1;
1151
+ const arrows = {
1152
+ ArrowLeft: [-step, 0],
1153
+ ArrowRight: [step, 0],
1154
+ ArrowUp: [0, -step],
1155
+ ArrowDown: [0, step],
1156
+ };
1157
+ const delta = arrows[event.key];
1158
+ if (delta) {
1159
+ event.preventDefault();
1160
+ nudge(delta[0], delta[1]);
1161
+ }
1162
+ };
1163
+ window.addEventListener("keydown", handler);
1164
+ return () => window.removeEventListener("keydown", handler);
1165
+ }, [
1166
+ copyElements,
1167
+ deleteElements,
1168
+ duplicateElements,
1169
+ editingId,
1170
+ elements,
1171
+ nudge,
1172
+ pasteClipboard,
1173
+ redo,
1174
+ saveNow,
1175
+ selectedIds,
1176
+ stepZoom,
1177
+ undo,
1178
+ zoomTo,
1179
+ ]);
1180
+ useEffect(() => {
1181
+ if (!contextMenu)
1182
+ return;
1183
+ const close = () => setContextMenu(null);
1184
+ window.addEventListener("click", close);
1185
+ return () => window.removeEventListener("click", close);
1186
+ }, [contextMenu]);
1187
+ /* -------------------------------------------------------- page actions */
1188
+ const runPageAction = useCallback((action, navigate = false) => {
1189
+ setPageError(null);
1190
+ startTransition(async () => {
1191
+ const result = await action();
1192
+ if (result.status === "error") {
1193
+ setPageError(result.message);
1194
+ return;
1195
+ }
1196
+ if (navigate && result.pageId)
1197
+ adapters?.navigate?.(result.pageId, { replace: navigate === "replace" });
1198
+ else
1199
+ adapters?.refresh?.();
1200
+ });
1201
+ }, [adapters]);
1202
+ const installSiteTemplate = useCallback((templateId) => {
1203
+ setPageError(null);
1204
+ startTransition(async () => {
1205
+ const result = await (adapters?.installTemplate ?? unavailable)(templateId);
1206
+ if (result.status === "error") {
1207
+ setPageError(result.message);
1208
+ return;
1209
+ }
1210
+ // A template can replace the currently open page document. A hard
1211
+ // navigation guarantees stale editor state cannot autosave over it.
1212
+ if (result.pageId)
1213
+ adapters?.navigate?.(result.pageId, { replace: true });
1214
+ });
1215
+ }, [adapters]);
1216
+ const publish = useCallback(() => {
1217
+ // Flush the draft first; Publish snapshots whatever the server holds.
1218
+ saveNow();
1219
+ setPageError(null);
1220
+ startTransition(async () => {
1221
+ const result = await (adapters?.publishPage ?? unavailable)(page.id);
1222
+ if (result.status === "error")
1223
+ setPageError(result.message);
1224
+ else
1225
+ adapters?.refresh?.();
1226
+ });
1227
+ }, [adapters, page.id, saveNow]);
1228
+ /* --------------------------------------------------------------- render */
1229
+ const enclosingDataBlock = useMemo(() => {
1230
+ if (!selectedElement)
1231
+ return undefined;
1232
+ let cursor = selectedElement.parentId
1233
+ ? byId.get(selectedElement.parentId)
1234
+ : undefined;
1235
+ const guard = new Set([selectedElement.id]);
1236
+ while (cursor && !guard.has(cursor.id)) {
1237
+ guard.add(cursor.id);
1238
+ if (cursor.type === "Repeat" || cursor.type === "Request")
1239
+ return cursor;
1240
+ cursor = cursor.parentId ? byId.get(cursor.parentId) : undefined;
1241
+ }
1242
+ return undefined;
1243
+ }, [byId, selectedElement]);
1244
+ const bindingSourceId = enclosingDataBlock?.sourceId ?? selectedElement?.sourceId;
1245
+ const bindingKeys = bindingSourceId
1246
+ ? (samples[bindingSourceId]?.keys ?? [])
1247
+ : [];
1248
+ const canvasData = useMemo(() => Object.fromEntries(Object.entries(samples).map(([id, sample]) => [id, sample.rows])), [samples]);
1249
+ const renderNode = (raw, frame, row, keyPrefix = "") => {
1250
+ const directRow = row ?? (raw.sourceId ? canvasData[raw.sourceId]?.[0] : undefined);
1251
+ const el = bindElement(raw, directRow);
1252
+ const resolvedStyle = resolveStyle(el, frame.bp, cascade);
1253
+ const isActiveFrame = frame.bp === breakpoint;
1254
+ const previewVisible = previewVisibility[el.id];
1255
+ if (previewVisible !== undefined)
1256
+ resolvedStyle.hidden = !previewVisible;
1257
+ const style = {
1258
+ ...resolvedStyle,
1259
+ ...(isActiveFrame && hoveredEffectId === el.id ? el.hover : undefined),
1260
+ ...(isActiveFrame && pressedEffectId === el.id ? el.press : undefined),
1261
+ };
1262
+ if (style.hidden)
1263
+ return null;
1264
+ const children = childrenOf(elements, el.id);
1265
+ const container = isContainer(el.type);
1266
+ const isSelected = isActiveFrame && selectedIds.includes(el.id);
1267
+ const isEditing = isActiveFrame && editingId === el.id;
1268
+ const isDropTarget = isActiveFrame && dropTargetId === el.id;
1269
+ const isGhosting = ghost !== null && dragInfo?.id === el.id && dragInfo.breakpoint === frame.bp;
1270
+ const note = isNote(el, byId, frame.bp, frame.width, rootStyle.layout, cascade);
1271
+ const band = isBand(el.type, style, rootStyle);
1272
+ const css = styleToCss(style, contextFor(el, frame.bp), el);
1273
+ if (el.hover || el.press)
1274
+ css.transition = "transform .42s cubic-bezier(.16,1,.3,1), scale .42s cubic-bezier(.16,1,.3,1), rotate .42s cubic-bezier(.16,1,.3,1), background-color .32s ease, color .32s ease, border-color .32s ease, box-shadow .42s cubic-bezier(.16,1,.3,1), opacity .32s ease, filter .42s ease";
1275
+ css.zIndex = el.z;
1276
+ if (el.loop)
1277
+ css.animation = `pg-loop-${el.loop.type} ${el.loop.duration}ms ease-in-out infinite`;
1278
+ css.cursor = el.locked ? "default" : isEditing ? "text" : "move";
1279
+ css.outline = isSelected
1280
+ ? "1.5px solid var(--ed-accent)"
1281
+ : isDropTarget
1282
+ ? "1.5px solid #22c55e"
1283
+ : note
1284
+ ? "1.5px dashed var(--ed-accent)"
1285
+ : undefined;
1286
+ if (note)
1287
+ css.opacity = 0.55;
1288
+ css.outlineOffset = isSelected || isDropTarget ? "-1px" : undefined;
1289
+ // A band paints edge to edge while its content sits in a centred inner
1290
+ // box — the same split the published stylesheet emits.
1291
+ const split = band ? splitBand(css, rootStyle.maxWidth) : null;
1292
+ if (isGhosting) {
1293
+ css.transform = `${css.transform ?? ""} translate(${ghost.dx}px, ${ghost.dy}px)`;
1294
+ css.opacity = 0.6;
1295
+ css.pointerEvents = "none";
1296
+ }
1297
+ // Repeat iterates sampled rows. Request renders once and passes the
1298
+ // sampled object to every descendant as its binding context.
1299
+ const renderChildren = () => el.type === "Repeat"
1300
+ ? rowsFor(el, canvasData, true).flatMap((dataRow, index) => children.map((child) => renderNode(child, frame, dataRow, `${keyPrefix}${index}:`)))
1301
+ : children.map((child) => renderNode(child, frame, el.type === "Request" ? canvasData[el.sourceId ?? ""]?.[0] : row, keyPrefix));
1302
+ return (
1303
+ // A canvas node is manipulated by pointer; the Layers panel is its keyboard equivalent.
1304
+ // biome-ignore lint/a11y/noStaticElementInteractions: pointer-driven canvas node
1305
+ _jsxs("div", { "data-canvas-element": el.id, style: split ? split.shell : css, onMouseDown: (event) => handleElementMouseDown(event, el, frame.bp), onMouseEnter: () => effectsPreview && el.hover && setHoveredEffectId(el.id), onMouseUp: () => effectsPreview && setPressedEffectId((id) => id === el.id ? null : id), onMouseLeave: () => { setHoveredEffectId((id) => id === el.id ? null : id); setPressedEffectId((id) => id === el.id ? null : id); }, onDoubleClick: (event) => {
1306
+ event.stopPropagation();
1307
+ if (isTextual(el.type) && !el.locked)
1308
+ setEditingId(el.id);
1309
+ }, onContextMenu: (event) => {
1310
+ event.preventDefault();
1311
+ event.stopPropagation();
1312
+ if (!selectedIds.includes(el.id))
1313
+ setSelectedIds([el.id]);
1314
+ setContextMenu({
1315
+ x: event.clientX,
1316
+ y: event.clientY,
1317
+ elementId: el.id,
1318
+ });
1319
+ }, onDragOver: container
1320
+ ? (event) => {
1321
+ event.preventDefault();
1322
+ event.stopPropagation();
1323
+ event.dataTransfer.dropEffect = "copy";
1324
+ setDropTargetId(el.id);
1325
+ }
1326
+ : undefined, onDragLeave: container
1327
+ ? () => setDropTargetId((c) => (c === el.id ? undefined : c))
1328
+ : undefined, onDrop: container ? (event) => handleDrop(event, el.id) : undefined, children: [isEditing ? (_jsx("textarea", {
1329
+ // Focus follows the double-click that opened the editor.
1330
+ ref: (node) => node?.focus(), value: el.content ?? "", onChange: (event) => patchProps(el.id, { content: event.target.value }), onMouseDown: (event) => event.stopPropagation(), onClick: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), onBlur: () => setEditingId(null), className: "min-h-[1em] w-full flex-1 resize-none overflow-hidden whitespace-pre-wrap bg-transparent text-inherit outline-none", style: {
1331
+ font: "inherit",
1332
+ color: "inherit",
1333
+ textAlign: style.textAlign,
1334
+ letterSpacing: "inherit",
1335
+ lineHeight: "inherit",
1336
+ } })) : (_jsx(ElementBody, { element: el })), split ? (_jsx("div", { style: split.inner, children: renderChildren() })) : (renderChildren()), isSelected && !note && (_jsxs("span", { className: "pointer-events-none absolute -bottom-1 left-1/2 z-[70] translate-y-full whitespace-nowrap rounded px-1.5 py-0.5 font-mono text-[10px] font-medium text-white shadow-sm", style: {
1337
+ background: "var(--ed-accent)",
1338
+ // Undo the canvas zoom so the label keeps one size.
1339
+ transform: `translate(-50%, 100%) scale(${1 / scale})`,
1340
+ transformOrigin: "top center",
1341
+ }, children: [Math.round(style.widthMode === "fixed" ? style.w : 0) || "auto", " \u00D7", " ", Math.round(style.heightMode === "fixed" ? style.h : 0) || "auto"] })), note && (_jsx("span", { className: "pointer-events-none absolute -top-5 left-0 whitespace-nowrap rounded px-1.5 py-0.5 text-[10px] font-medium", style: {
1342
+ background: "var(--ed-accent-soft)",
1343
+ color: "var(--ed-accent)",
1344
+ }, children: "Note \u00B7 not published" })), isSelected && !el.locked && selectedIds.length === 1 && (_jsx(ResizeHandles, { element: el, style: style, onMouseDown: (event, handle, element) => handleResizeMouseDown(event, handle, element, frame.bp) }))] }, `${keyPrefix}${el.id}`));
1345
+ };
1346
+ const rootCss = rootStyleToCss(rootStyle);
1347
+ const customFontCss = (rootStyle.customFonts ?? []).map((font) => `@font-face{font-family:"${font.name.replace(/["'{};]/g, "")}";src:url("${font.url.replace(/["'()\\]/g, "")}");font-weight:${font.weight};font-style:${font.style};font-display:swap}`).join("\n");
1348
+ const arrangeable = useMemo(() => {
1349
+ if (selectedIds.length < 2)
1350
+ return false;
1351
+ const parents = new Set(selectedIds.map((id) => byId.get(id)?.parentId ?? "__root__"));
1352
+ if (parents.size !== 1)
1353
+ return false;
1354
+ const first = byId.get(selectedIds[0]);
1355
+ return first ? contextFor(first).parentLayout === "absolute" : false;
1356
+ }, [byId, contextFor, selectedIds]);
1357
+ const menuElementId = contextMenu?.elementId;
1358
+ return (_jsxs("div", { className: "pg-editor flex h-screen w-full flex-col overflow-hidden bg-ed-surface font-sans text-xs text-ed-text selection:bg-blue-500/30", "data-ed-theme": chromeTheme === "light" ? "light" : undefined, children: [_jsxs("header", { className: "relative z-30 grid h-14 shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-b border-ed-border bg-ed-surface/90 px-3 backdrop-blur-2xl", children: [_jsx("button", { type: "button", onClick: () => setIsLeftCollapsed((current) => !current), title: "Toggle sidebar", className: "w-fit text-[13px] font-semibold tracking-tight text-ed-text", children: "Pagiera" }), _jsxs("div", { className: "pg-chrome-card flex items-center gap-2 rounded-2xl border border-ed-border bg-ed-subtle/90 p-1.5 backdrop-blur-xl", children: [_jsxs("div", { className: "flex items-center gap-2 text-[11px] font-medium text-ed-muted", children: [componentMode && _jsxs(_Fragment, { children: [_jsxs(Select, { value: activeComponentMaster?.id, onValueChange: (id) => { setActiveComponentMasterId(id); setSelectedIds([id]); }, children: [_jsx(SelectTrigger, { "aria-label": "Variant", children: _jsx(SelectValue, { placeholder: "Select variant" }) }), _jsx(SelectContent, { children: activeComponentVariants.map((master) => _jsx(SelectItem, { value: master.id, children: master.variant ?? "Default" }, master.id)) })] }), _jsx("button", { type: "button", onClick: createComponentVariant, disabled: selectedElement?.componentRole !== "master", title: "Add variant", className: "flex size-7 items-center justify-center rounded-lg bg-ed-field text-ed-muted hover:bg-ed-field-hover hover:text-ed-text disabled:opacity-30", children: _jsx(IconPlus, { size: 13 }) })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx("button", { type: "button", title: "Zoom out (Ctrl -)", onClick: () => stepZoom(-1), className: "rounded p-1 transition-colors hover:bg-ed-field hover:text-ed-text", children: _jsx(IconMinus, { size: 12 }) }), _jsxs("button", { type: "button", title: "Reset zoom (Ctrl 0)", onClick: () => zoomTo(100), className: "w-10 select-none text-center tabular-nums transition-colors hover:text-ed-text", children: [zoom, "%"] }), _jsx("button", { type: "button", title: "Zoom in (Ctrl +)", onClick: () => stepZoom(1), className: "rounded p-1 transition-colors hover:bg-ed-field hover:text-ed-text", children: _jsx(IconPlus, { size: 12 }) })] })] }), _jsxs("div", { className: "flex items-center gap-2 border-l border-ed-border pl-2 text-ed-muted", children: [_jsx("button", { type: "button", title: "Undo (Ctrl Z)", onClick: undo, disabled: !canUndo, className: "transition-colors hover:text-ed-text disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ed-muted", children: _jsx(IconArrowBackUp, { size: 16 }) }), _jsx("button", { type: "button", title: "Redo (Ctrl Shift Z)", onClick: redo, disabled: !canRedo, className: "transition-colors hover:text-ed-text disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ed-muted", children: _jsx(IconArrowForwardUp, { size: 16 }) })] })] }), _jsxs("div", { className: "flex min-w-0 items-center justify-end gap-2", children: [page.publishedAt && (_jsxs("a", { href: (adapters?.publishedHref ?? defaultPublishedHref)(page.slug), target: "_blank", rel: "noopener noreferrer", className: "hidden items-center gap-1.5 text-[11px] text-emerald-500 transition-colors hover:text-emerald-400 xl:flex", children: [_jsx(IconWorld, { size: 14 }), (adapters?.publishedHref ?? defaultPublishedHref)(page.slug), _jsx(IconExternalLink, { size: 11 })] })), _jsx("button", { type: "button", onClick: publish, disabled: isPending || isDirty, title: isDirty ? "Waiting for the draft to save…" : undefined, className: "rounded-lg bg-ed-accent px-3 py-1.5 text-[11px] font-semibold text-white shadow-sm transition-all hover:opacity-90 disabled:opacity-50", children: isPending ? "Working…" : page.publishedAt ? "Republish" : "Publish" }), page.publishedAt && (_jsx("button", { type: "button", onClick: () => runPageAction(() => (adapters?.unpublishPage ?? unavailable)(page.id, page.slug)), className: "text-[11px] text-ed-muted transition-colors hover:text-ed-text", children: "Unpublish" })), _jsx("button", { type: "button", title: chromeTheme === "dark" ? "Switch to light editor" : "Switch to dark editor", "aria-label": "Toggle editor theme", onClick: toggleChromeTheme, className: "rounded-md p-1.5 text-ed-muted transition-colors hover:bg-ed-field hover:text-ed-text", children: chromeTheme === "dark" ? _jsx(IconSun, { size: 16 }) : _jsx(IconMoon, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => setIsRightCollapsed((c) => !c), className: "text-ed-faint transition-colors hover:text-ed-text", children: isRightCollapsed ? (_jsx(IconLayoutSidebarLeftCollapse, { size: 18 })) : (_jsx(IconLayoutSidebarRightCollapse, { size: 18 })) })] })] }), _jsxs("div", { className: "flex flex-1 overflow-hidden bg-ed-canvas", children: [_jsxs("aside", { className: "z-20 flex w-12 shrink-0 flex-col items-center border-r border-ed-border bg-ed-surface/95 py-2 backdrop-blur-xl", children: [_jsx("button", { type: "button", title: "Select (V)", className: "mb-1 flex size-8 items-center justify-center rounded-xl bg-ed-accent/15 text-ed-accent ring-1 ring-inset ring-ed-accent/20 transition-all hover:scale-105 hover:bg-ed-accent/20", children: _jsx(IconPointer, { size: 17, stroke: 1.7 }) }), _jsx("button", { type: "button", title: "Insert (A)", className: "flex size-8 items-center justify-center rounded-lg text-ed-muted transition-colors hover:bg-ed-field hover:text-ed-text", onClick: () => {
1359
+ setLeftTab("Elements");
1360
+ setIsLeftCollapsed(false);
1361
+ }, children: _jsx(IconPlus, { size: 18, stroke: 1.8 }) }), _jsx("div", { className: "my-2 h-px w-5 bg-ed-border" }), _jsx("div", { className: "flex w-full flex-col items-center gap-1 px-1.5", children: ["Layers", componentMode ? "Components" : "Assets", "Library", "Icons", ...(componentMode ? [] : ["Templates"]), "Variables", "AI", "Data", ...(componentMode ? [] : ["Pages"])].map((tab) => {
1362
+ const Icon = tab === "Layers"
1363
+ ? IconLayersLinked
1364
+ : tab === "Elements"
1365
+ ? IconBox
1366
+ : tab === "Library"
1367
+ ? IconLibrary
1368
+ : tab === "Icons"
1369
+ ? IconIcons
1370
+ : tab === "Templates"
1371
+ ? IconTemplate
1372
+ : tab === "Variables"
1373
+ ? IconPalette
1374
+ : tab === "AI"
1375
+ ? IconSparkles
1376
+ : tab === "Components" || tab === "Assets"
1377
+ ? IconComponents
1378
+ : tab === "Data"
1379
+ ? IconDatabase
1380
+ : IconFile;
1381
+ return (_jsx("button", { type: "button", onClick: () => {
1382
+ setLeftTab(tab);
1383
+ if (isLeftCollapsed)
1384
+ setIsLeftCollapsed(false);
1385
+ }, className: `flex size-8 items-center justify-center rounded-xl transition-all hover:scale-105 ${leftTab === tab && !isLeftCollapsed
1386
+ ? "bg-ed-field text-ed-accent"
1387
+ : "text-ed-muted hover:bg-ed-subtle hover:text-ed-text"}`, title: tab, children: _jsx(Icon, { size: 17, stroke: 1.6 }) }, tab));
1388
+ }) })] }), _jsx(AnimatePresence, { initial: false, children: !isLeftCollapsed && (_jsxs(motion.aside, { initial: { width: 0, opacity: 0, x: -12 }, animate: { width: 292, opacity: 1, x: 0 }, exit: { width: 0, opacity: 0, x: -12 }, transition: { type: "spring", stiffness: 420, damping: 38 }, className: "relative z-10 flex w-[292px] shrink-0 flex-col overflow-hidden border-r border-ed-border bg-ed-surface/95 backdrop-blur-xl", children: [_jsxs("div", { className: "flex h-11 shrink-0 items-center justify-between border-b border-ed-border px-3.5", children: [_jsx("span", { className: "text-[11px] font-semibold text-ed-text", children: leftTab }), _jsx("button", { type: "button", onClick: () => setIsLeftCollapsed(true), className: "text-ed-faint hover:text-ed-muted transition-colors p-1 rounded-md hover:bg-ed-field", children: _jsx(IconX, { size: 16 }) })] }), leftTab !== "Pages" && leftTab !== "Components" && leftTab !== "Assets" && leftTab !== "Library" && leftTab !== "Templates" && leftTab !== "Variables" && leftTab !== "Data" && leftTab !== "AI" && (_jsx("div", { className: "border-b border-ed-border p-3 bg-ed-subtle", children: _jsxs("div", { className: "flex items-center gap-2 rounded-md border border-ed-border bg-ed-surface px-2.5 py-1.5 transition-all focus-within:border-ed-accent/50 focus-within:ring-1 focus-within:ring-blue-500/20", children: [_jsx(IconSearch, { size: 14, className: "text-ed-faint" }), _jsx("input", { type: "text", value: search, onChange: (event) => setSearch(event.target.value), placeholder: `Search ${leftTab.toLowerCase()}...`, className: "w-full bg-transparent text-xs text-ed-text outline-none placeholder:text-ed-faint" }), _jsx(IconCommand, { size: 12, className: "text-ed-faint" })] }) })), _jsx("div", { className: "custom-scrollbar flex-1 overflow-y-auto", children: leftTab === "Layers" ? (_jsx(LayersPanel, { elements: visibleEditorElements, breakpoint: breakpoint, search: search, selectedIds: selectedIds, onSelect: select, onToggleHidden: (id) => {
1389
+ const el = byId.get(id);
1390
+ if (el) {
1391
+ patchStyle([id], {
1392
+ hidden: !resolveStyle(el, breakpoint, cascade).hidden,
1393
+ });
1394
+ }
1395
+ }, onToggleLocked: (id) => {
1396
+ const el = byId.get(id);
1397
+ if (el)
1398
+ patchProps(id, { locked: !el.locked });
1399
+ }, onReorder: (id, direction) => setElements((els) => reorder(els, id, direction)), onDelete: (id) => deleteElements([id]), onReparent: doReparent })) : leftTab === "Elements" ? (_jsx(ElementsPanel, { search: search, onInsert: insertElement })) : leftTab === "Icons" ? (_jsx(IconsPanel, { search: search, onInsert: (iconName) => insertElement("Icon", { iconName }) })) : leftTab === "Data" ? (_jsx(DataPanel, { sources: dataSources, samples: samples, onChange: setDataSources, onSample: (id, sample) => setSamples((prev) => ({ ...prev, [id]: sample })), preview: adapters?.previewSource ?? (async () => ({ status: "error", message: "No data preview adapter configured." })) })) : leftTab === "Library" ? (_jsx(LibraryPanel, { pages: library, currentPageId: page.id, onInsert: insertFromLibrary })) : leftTab === "Templates" ? (_jsx(TemplatesPanel, { busy: isPending, onInstall: (id) => {
1400
+ if (!window.confirm("Replace this site with the selected template? All existing pages and their revisions will be deleted."))
1401
+ return;
1402
+ installSiteTemplate(id);
1403
+ } })) : leftTab === "Assets" ? (_jsxs("div", { className: "p-3", children: [_jsxs("div", { className: "mb-3 rounded-xl border border-ed-border bg-ed-subtle p-3", children: [_jsx("p", { className: "text-[11px] font-semibold text-ed-text", children: "Project assets" }), _jsx("p", { className: "mt-1 text-[9px] leading-relaxed text-ed-faint", children: "Open a component to edit its own canvas, or drag a variant directly onto the page." })] }), _jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("span", { className: "text-[10px] font-semibold text-ed-muted", children: "Components" }), _jsxs("span", { className: "flex gap-1", children: [_jsxs("button", { type: "button", onClick: createBlankComponent, className: "flex items-center gap-1 rounded-md bg-ed-field px-2 py-1 text-[9px] text-ed-muted hover:text-ed-text", children: [_jsx(IconPlus, { size: 10 }), " New"] }), _jsx("button", { type: "button", onClick: () => setCodeComposerOpen(true), className: "flex items-center gap-1 rounded-md bg-ed-field px-2 py-1 text-[9px] text-ed-muted hover:text-ed-text", children: "Code" })] })] }), _jsxs("div", { className: "space-y-2", children: [componentMasters.map((master) => _jsxs("button", { type: "button", draggable: true, onDragStart: (event) => { event.dataTransfer.setData(COMPONENT_MIME, master.id); event.dataTransfer.effectAllowed = "copy"; }, onClick: () => { setRootStyle({ ...rootStyle, documentMode: "component" }); setActiveComponentMasterId(master.id); setSelectedIds([master.id]); setBreakpoint("desktop"); setLeftTab("Components"); }, className: "group flex w-full cursor-grab items-center gap-3 rounded-xl border border-ed-border bg-ed-subtle p-2.5 text-left hover:border-ed-accent/50 hover:bg-ed-field active:cursor-grabbing", children: [_jsx("span", { className: "flex size-9 items-center justify-center rounded-lg bg-ed-field text-ed-accent", children: _jsx(IconComponents, { size: 16 }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-[10px] font-semibold text-ed-text", children: master.name ?? "Component" }), _jsxs("span", { className: "block text-[9px] text-ed-faint", children: [master.variant ?? "Default", " \u00B7 drag to insert"] })] })] }, master.id)), componentMasters.length === 0 && _jsx("p", { className: "rounded-xl border border-dashed border-ed-border p-5 text-center text-[10px] text-ed-faint", children: "Create a component or add one from code." })] })] })) : leftTab === "Variables" ? (_jsx(VariablesPanel, { rootStyle: rootStyle, selectedElement: selectedElement, setRootStyle: setRootStyle, setElements: setElements })) : leftTab === "AI" ? (_jsx(AiPanel, { pageId: page.id, elements: elements, rootStyle: rootStyle, breakpoint: breakpoint, onApply: applyAiPlan, generate: adapters?.generate })) : leftTab === "Components" ? (_jsxs("div", { className: "p-3", children: [_jsxs("div", { className: "mb-3 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-[11px] font-semibold text-ed-text", children: "Component canvas" }), _jsx("p", { className: "mt-1 text-[9px] text-ed-faint", children: "Every variant opens in its own resizable canvas." })] }), _jsx("button", { type: "button", onClick: () => { setRootStyle({ ...rootStyle, documentMode: "page" }); setLeftTab("Layers"); }, className: "rounded-lg bg-ed-field px-2 py-1.5 text-[9px] text-ed-muted hover:text-ed-text", children: "Page mode" })] }), selectedElement?.componentRole === "master" && _jsxs("div", { className: "mb-3 space-y-2 rounded-xl border border-ed-border bg-ed-subtle p-2.5", children: [_jsxs("label", { className: "flex items-center gap-2 text-[9px] text-ed-faint", children: [_jsx("span", { className: "w-16", children: "Name" }), _jsx("input", { value: selectedElement.name ?? "", placeholder: "Component name", onChange: (event) => patchProps(selectedElement.id, { name: event.target.value }), className: "h-8 min-w-0 flex-1 rounded-lg bg-ed-field px-2.5 text-[10px] text-ed-text outline-none focus:ring-1 focus:ring-ed-accent" })] }), _jsxs("label", { className: "flex items-center gap-2 text-[9px] text-ed-faint", children: [_jsx("span", { className: "w-16", children: "Variant" }), _jsx("input", { value: selectedElement.variant ?? "Default", onChange: (event) => patchProps(selectedElement.id, { variant: event.target.value }), className: "h-8 min-w-0 flex-1 rounded-lg bg-ed-field px-2.5 text-[10px] text-ed-text outline-none focus:ring-1 focus:ring-ed-accent" })] })] }), _jsxs("div", { className: "space-y-2", children: [componentMasters.map((master) => _jsxs("button", { type: "button", onClick: () => { setActiveComponentMasterId(master.id); setSelectedIds([master.id]); }, className: `flex w-full items-center gap-3 rounded-xl border p-2.5 text-left ${activeComponentMaster?.id === master.id ? "border-ed-accent bg-ed-accent/10" : "border-ed-border bg-ed-subtle hover:bg-ed-field"}`, children: [_jsx("span", { className: "flex size-8 items-center justify-center rounded-lg bg-ed-field text-ed-accent", children: _jsx(IconComponents, { size: 15 }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-[10px] font-semibold text-ed-text", children: master.name ?? "Component" }), _jsx("span", { className: "block text-[9px] text-ed-faint", children: master.variant ?? "Default" })] }), _jsxs("span", { className: "font-mono text-[9px] text-ed-faint", children: [Math.round(master.base.w), "\u00D7", Math.round(master.base.h)] })] }, master.id)), componentMasters.length === 0 && _jsx("p", { className: "rounded-xl border border-dashed border-ed-border p-5 text-center text-[10px] text-ed-faint", children: "Create a new component canvas." })] }), _jsxs("button", { type: "button", onClick: createComponentVariant, disabled: selectedElement?.componentRole !== "master", className: "mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg bg-ed-accent px-3 py-2 text-[10px] font-semibold text-white disabled:opacity-30", children: [_jsx(IconPlus, { size: 12 }), " Add variant"] })] })) : (_jsx(PagesPanel, { pages: pages, currentId: page.id, busy: isPending, error: pageError, onCreate: (name) => runPageAction(() => (adapters?.createPage ?? unavailable)(name, name), "push"), onRename: (id, name, slug) => runPageAction(() => (adapters?.renamePage ?? unavailable)(id, name, slug)), onDuplicate: (id, name) => runPageAction(() => (adapters?.duplicatePage ?? unavailable)(id, name, name), "push"), onDelete: (id) => runPageAction(() => (adapters?.deletePage ?? unavailable)(id), id === page.id ? "replace" : false), onNavigate: (id) => (adapters?.navigate ?? ((pageId) => { window.location.href = defaultEditorHref(pageId); }))(id), publishedHref: adapters?.publishedHref ?? defaultPublishedHref })) }), _jsxs("div", { className: "flex items-center justify-between border-t border-ed-border p-2 px-3 text-ed-muted", children: [_jsxs("span", { className: "text-[10px] tabular-nums", children: [elements.length, " element", elements.length === 1 ? "" : "s"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", title: "Duplicate (Ctrl D)", disabled: selectedIds.length === 0, onClick: () => duplicateElements(selectedIds), className: "rounded p-1.5 transition-colors hover:bg-ed-field hover:text-ed-text disabled:pointer-events-none disabled:opacity-30", children: _jsx(IconCopy, { size: 16 }) }), _jsx("button", { type: "button", title: "Delete (Del)", disabled: selectedIds.length === 0, onClick: () => deleteElements(selectedIds), className: "rounded p-1.5 transition-colors hover:bg-ed-field hover:text-ed-text disabled:pointer-events-none disabled:opacity-30", children: _jsx(IconTrash, { size: 16 }) })] })] })] })) }), _jsxs("main", { className: "relative flex flex-1 flex-col overflow-hidden bg-ed-canvas", style: {
1404
+ // The grid is drawn from the border colour, so it reads
1405
+ // on the dark well and the light one alike.
1406
+ backgroundImage: "linear-gradient(to right, var(--ed-grid) 1px, transparent 1px), linear-gradient(to bottom, var(--ed-grid) 1px, transparent 1px)",
1407
+ backgroundSize: "28px 28px",
1408
+ backgroundPosition: "-1px -1px",
1409
+ }, onMouseDown: (event) => {
1410
+ if (event.target === event.currentTarget)
1411
+ setSelectedIds([]);
1412
+ }, children: [_jsx("div", { ref: viewportRef, className: "canvas-scrollbar absolute inset-0 overflow-auto overscroll-none", style: { cursor: isPanning ? "grabbing" : spaceHeld ? "grab" : undefined }, onMouseDown: (event) => {
1413
+ if (tryBeginPan(event))
1414
+ return;
1415
+ setContextMenu(null);
1416
+ if (event.target === event.currentTarget)
1417
+ beginMarquee(event, breakpoint);
1418
+ }, onContextMenu: (event) => {
1419
+ if (event.target.closest("[data-canvas-page]"))
1420
+ return;
1421
+ event.preventDefault();
1422
+ const rect = frameRef.current?.getBoundingClientRect();
1423
+ setContextMenu({
1424
+ x: event.clientX,
1425
+ y: event.clientY,
1426
+ canvasX: rect ? Math.round((event.clientX - rect.left) / scale) : 0,
1427
+ canvasY: rect ? Math.round((event.clientY - rect.top) / scale) : 0,
1428
+ });
1429
+ }, children: _jsx("div", { className: "flex min-w-max items-start justify-center gap-10 px-[900px] py-[560px]", children: frames.map((frame) => {
1430
+ const primary = frame.bp === breakpoint;
1431
+ const definition = breakpointDefs.find((item) => item.id === frame.bp);
1432
+ const isBaseFrame = !componentMode && frame.bp === cascade.baseId;
1433
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { draggable: !componentMode, onDragStart: (event) => { if (componentMode)
1434
+ return; setDraggedBreakpointId(frame.bp); event.dataTransfer.effectAllowed = "move"; }, onDragEnd: () => setDraggedBreakpointId(null), onDragOver: (event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, onDrop: (event) => { event.preventDefault(); if (draggedBreakpointId)
1435
+ moveBreakpoint(draggedBreakpointId, frame.bp); setDraggedBreakpointId(null); }, className: `flex h-8 cursor-grab items-center gap-2 rounded-lg border px-2 active:cursor-grabbing ${draggedBreakpointId === frame.bp ? "border-ed-accent bg-ed-accent/10 opacity-60" : "border-ed-border bg-ed-subtle"}`, style: { width: frame.width * scale }, children: [editingBreakpointId === frame.bp && !componentMode ? (_jsx("input", { ref: (node) => node?.select(), value: definition?.name ?? frame.bp, onChange: (event) => renameBreakpoint(frame.bp, event.target.value), onBlur: (event) => { renameBreakpoint(frame.bp, event.target.value); commitBreakpointName(frame.bp); setEditingBreakpointId(null); }, onKeyDown: (event) => {
1436
+ if (event.key === "Enter")
1437
+ event.currentTarget.blur();
1438
+ if (event.key === "Escape")
1439
+ setEditingBreakpointId(null);
1440
+ }, className: "w-24 rounded bg-ed-field px-1 text-[11px] font-medium text-ed-text outline-none ring-1 ring-ed-accent" })) : (_jsx("button", { type: "button", onClick: () => setBreakpoint(frame.bp), onDoubleClick: () => { if (!componentMode)
1441
+ setEditingBreakpointId(frame.bp); }, title: componentMode ? undefined : "Double-click to rename", className: `text-[11px] font-medium capitalize transition-colors ${primary
1442
+ ? "text-ed-text"
1443
+ : "text-ed-faint hover:text-ed-muted"}`, children: componentMode ? activeComponentMaster?.name ?? "Component" : definition?.name ?? frame.bp })), componentMode ? (_jsx("span", { className: "font-mono text-[10px] text-ed-faint", children: `${frame.width} × ${canvasHeight}` })) : (_jsx("input", { type: "number", value: frame.width, onChange: (event) => resizeBreakpoint(frame.bp, Number(event.target.value)), onFocus: (event) => event.target.select(), title: `Governs ${breakpointRange(frame.bp)}px`, className: "w-12 bg-transparent font-mono text-[10px] text-ed-faint outline-none hover:text-ed-muted focus:text-ed-text [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" })), !componentMode && (_jsx("span", { className: "font-mono text-[10px] text-ed-faint/60", children: breakpointRange(frame.bp) })), _jsxs("div", { className: "ml-auto flex items-center gap-1", children: [!componentMode && (_jsx("button", { type: "button", onClick: (event) => { event.stopPropagation(); setBaseBreakpoint(frame.bp); }, disabled: isBaseFrame, title: isBaseFrame ? "Main breakpoint — its edits are shared" : "Make main breakpoint", className: `flex size-5 items-center justify-center rounded-md ${isBaseFrame ? "bg-ed-accent/15 text-ed-accent" : "bg-ed-field text-ed-muted hover:bg-ed-field-hover hover:text-ed-text"}`, children: isBaseFrame ? _jsx(IconPinFilled, { size: 11 }) : _jsx(IconPin, { size: 11 }) })), !componentMode && breakpointDefs.length > 1 && (_jsx("button", { type: "button", onClick: (event) => { event.stopPropagation(); removeBreakpoint(frame.bp); }, title: "Remove breakpoint", className: "flex size-5 items-center justify-center rounded-md bg-ed-field text-ed-muted hover:bg-red-500/15 hover:text-red-400", children: _jsx(IconTrash, { size: 11 }) })), _jsx("button", { type: "button", onClick: (event) => { event.stopPropagation(); if (componentMode)
1444
+ createComponentVariant();
1445
+ else
1446
+ addBreakpoint(); }, disabled: componentMode && selectedElement?.componentRole !== "master", className: "flex size-5 items-center justify-center rounded-md bg-ed-field text-ed-muted hover:bg-ed-field-hover hover:text-ed-text disabled:opacity-30", title: componentMode ? "Add variant" : "Add breakpoint", children: _jsx(IconPlus, { size: 12 }) })] })] }), _jsxs("div", { className: "group/frame relative", style: {
1447
+ width: frame.width * scale,
1448
+ height: displayCanvasHeight * scale,
1449
+ flexShrink: 0,
1450
+ }, children: [_jsx("div", { ref: primary ? frameRef : undefined, "data-canvas-page": true, className: "shadow-2xl", style: {
1451
+ width: frame.width,
1452
+ minHeight: canvasHeight,
1453
+ background: rootStyle.bg,
1454
+ transform: `scale(${scale})`,
1455
+ transformOrigin: "top left",
1456
+ outline: primary && frames.length > 1
1457
+ ? "2px solid var(--ed-accent)"
1458
+ : undefined,
1459
+ outlineOffset: 3,
1460
+ }, onMouseDown: (event) => {
1461
+ if (event.target === event.currentTarget)
1462
+ beginMarquee(event, frame.bp);
1463
+ }, children: _jsxs("div", { ref: primary ? canvasRef : undefined, style: {
1464
+ ...rootCss,
1465
+ // Match the published page: the
1466
+ // app font must not leak in.
1467
+ fontFamily: resolveFont(rootStyle.fontFamily),
1468
+ // The shell spans the frame; the
1469
+ // content width comes from bands.
1470
+ maxWidth: "none",
1471
+ minHeight: canvasHeight,
1472
+ }, onDragOver: (event) => {
1473
+ event.preventDefault();
1474
+ event.dataTransfer.dropEffect = "copy";
1475
+ setDropTargetId(null);
1476
+ }, onDrop: (event) => handleDrop(event, null), onMouseDown: (event) => {
1477
+ if (event.target === event.currentTarget)
1478
+ beginMarquee(event, frame.bp);
1479
+ }, children: [visibleEditorElements.length === 0 && (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center text-sm font-medium text-ed-muted", children: "Drag an element here to start" })), childrenOf(visibleEditorElements, undefined).map((el) => renderNode(el, frame, undefined, `${frame.bp}:`)), primary &&
1480
+ guides.lines.map((guide) => (_jsx("div", { className: "pointer-events-none absolute z-[9999] bg-fuchsia-500", style: guide.axis === "x"
1481
+ ? {
1482
+ left: guides.origin.x +
1483
+ guide.at,
1484
+ top: guides.origin.y +
1485
+ guide.from,
1486
+ width: 1,
1487
+ height: guide.to -
1488
+ guide.from,
1489
+ }
1490
+ : {
1491
+ top: guides.origin.y +
1492
+ guide.at,
1493
+ left: guides.origin.x +
1494
+ guide.from,
1495
+ height: 1,
1496
+ width: guide.to -
1497
+ guide.from,
1498
+ } }, `${guide.axis}-${guide.at}-${guide.from}`)))] }) }), componentMode && _jsx("button", { type: "button", "aria-label": "Resize component width", onMouseDown: beginComponentWidthResize, className: "absolute -right-1.5 inset-y-0 z-30 w-3 cursor-ew-resize opacity-0 transition-opacity group-hover/frame:opacity-100", children: _jsx("span", { className: "absolute inset-y-1/2 left-1/2 h-12 w-1 -translate-x-1/2 -translate-y-1/2 rounded-full bg-ed-accent shadow-[0_0_0_3px_var(--ed-accent-soft)]" }) }), primary && _jsx("button", { type: "button", "aria-label": `Resize ${componentMode ? "component" : "page"} canvas height`, onMouseDown: beginCanvasResize, className: "absolute -bottom-1.5 inset-x-0 z-30 h-3 cursor-ns-resize opacity-0 transition-opacity group-hover/frame:opacity-100", children: _jsx("span", { className: "absolute left-1/2 top-1/2 h-1 w-14 -translate-x-1/2 -translate-y-1/2 rounded-full bg-ed-accent shadow-[0_0_0_3px_var(--ed-accent-soft)]" }) })] })] }, frame.bp));
1499
+ }) }) }), _jsx("div", { className: "absolute bottom-0 left-0 z-20 flex h-8 w-full items-center gap-2 border-t border-ed-border bg-ed-surface px-4 text-[11px] text-ed-muted shadow-sm", children: _jsx(Breadcrumbs, { byId: byId, selectedId: selectedId, onSelect: (id) => setSelectedIds([id]) }) }), _jsxs("div", { className: "absolute bottom-12 right-6 z-20 flex items-center gap-0.5 rounded-lg border border-ed-border bg-ed-surface/90 p-1 shadow-md backdrop-blur", children: [_jsx("button", { type: "button", title: effectsPreview ? "Stop interaction preview" : "Preview hover, press and layer actions", onClick: () => { setEffectsPreview((value) => !value); setHoveredEffectId(null); setPressedEffectId(null); setPreviewVisibility({}); }, className: `rounded-md p-1.5 transition-colors ${effectsPreview ? "bg-ed-accent text-white" : "text-ed-faint hover:bg-ed-field hover:text-ed-text"}`, children: _jsx(IconPlayerPlay, { size: 16, stroke: 1.5 }) }), _jsx("button", { type: "button", title: "Select", className: "rounded-md bg-[var(--ed-accent-soft)] p-1.5 text-ed-accent", children: _jsx(IconPointer, { size: 16, stroke: 1.5 }) }), _jsx("button", { type: "button", title: "Recentre the canvas", onClick: recenter, className: "rounded-md p-1.5 text-ed-faint transition-colors hover:bg-ed-field hover:text-ed-text", children: _jsx(IconFocusCentered, { size: 16, stroke: 1.5 }) }), _jsx("button", { type: "button", title: "Zoom to 100% (Ctrl 0)", onClick: () => zoomTo(100), className: "rounded-md px-1.5 py-1.5 font-mono text-[11px] text-ed-faint transition-colors hover:bg-ed-field hover:text-ed-text", children: "1:1" }), _jsx("button", { type: "button", title: "Fit to width", onClick: () => zoomToFit(), className: "rounded-md p-1.5 text-ed-faint transition-colors hover:bg-ed-field hover:text-ed-text", children: _jsx(IconArrowsMaximize, { size: 16, stroke: 1.5 }) })] })] }), _jsx(AnimatePresence, { initial: false, children: !isRightCollapsed && (_jsxs(motion.aside, { initial: { width: 0, opacity: 0, x: 14 }, animate: { width: 312, opacity: 1, x: 0 }, exit: { width: 0, opacity: 0, x: 14 }, transition: { type: "spring", stiffness: 420, damping: 38 }, className: "z-10 flex w-[312px] shrink-0 flex-col overflow-hidden border-l border-ed-border bg-ed-surface/95 backdrop-blur-xl", children: [_jsx("div", { className: "flex h-11 shrink-0 items-end gap-0.5 border-b border-ed-border px-2", children: ["Design", "Content", "Hover", "Interact"].map((tab) => (_jsxs("button", { type: "button", onClick: () => setRightTab(tab), disabled: !selectedElement, className: `relative flex-1 px-2 pb-3 pt-2 text-[11px] font-medium transition-colors disabled:opacity-30 ${rightTab === tab
1500
+ ? "text-ed-text"
1501
+ : "text-ed-muted hover:text-ed-text"}`, children: [tab === "Hover" ? "Effects" : tab === "Interact" ? "Actions" : tab, rightTab === tab && selectedElement && (_jsx(motion.span, { layoutId: "inspector-tab", className: "absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-ed-accent", transition: { type: "spring", stiffness: 500, damping: 40 } }))] }, tab))) }), selectedElement && breakpoint !== "desktop" && (_jsxs("p", { className: "border-b border-ed-border bg-amber-500/10 px-5 py-2.5 text-[11px] leading-relaxed text-amber-500", children: ["Editing the ", _jsx("b", { children: breakpoint }), " breakpoint. Changes here override desktop and only apply at this size."] })), _jsx("div", { className: "custom-scrollbar flex flex-1 flex-col overflow-y-auto px-4 py-3", children: selectedElement ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mb-1 flex items-baseline justify-between", children: [_jsx("span", { className: "text-[15px] font-semibold tracking-tight text-ed-text", children: selectedElement.type }), _jsx("span", { className: "font-mono text-[10px] text-ed-faint", children: selectedElement.id.slice(0, 6) })] }), _jsxs("div", { className: "mb-3 flex items-center gap-1.5", children: [!componentMode && rootStyle.layout === "absolute" && selectedElement.parentId && _jsx("button", { type: "button", onClick: () => doReparent(selectedElement.id, undefined), className: "flex-1 rounded-lg border border-ed-border bg-ed-subtle px-2.5 py-2 text-[10px] font-medium text-ed-muted hover:border-ed-accent/50 hover:bg-ed-field hover:text-ed-text", children: "Detach to canvas" }), selectedElement.componentRole === "master" && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "rounded-md bg-ed-accent/15 px-2 py-1 text-[9px] font-semibold uppercase text-ed-accent", children: ["Master \u00B7 ", selectedElement.variant] }), _jsx("button", { type: "button", onClick: createComponentInstance, className: "rounded-lg bg-ed-field px-2 py-1.5 text-[9px] text-ed-text hover:bg-ed-field-hover", children: "Instance" }), _jsx("button", { type: "button", onClick: createComponentVariant, className: "rounded-lg bg-ed-field px-2 py-1.5 text-[9px] text-ed-text hover:bg-ed-field-hover", children: "+ Variant" })] })), selectedElement.componentRole === "instance" && (_jsxs(Select, { value: selectedElement.variant ?? "Default", onValueChange: switchInstanceVariant, children: [_jsx(SelectTrigger, { className: "h-8 min-w-0 flex-1", "aria-label": "Component variant", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: elements
1502
+ .filter((element) => element.componentRole === "master" &&
1503
+ element.componentId ===
1504
+ selectedElement.componentId)
1505
+ .map((element) => (_jsx(SelectItem, { value: element.variant ?? "Default", children: element.variant ?? "Default" }, element.id))) })] }))] }), _jsx(motion.div, { initial: { opacity: 0, y: 7 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] }, children: _jsx(Inspector, { tab: rightTab, element: selectedElement, elements: visibleEditorElements, style: resolveStyle(selectedElement, breakpoint, cascade), breakpoint: breakpoint, parentLayout: contextFor(selectedElement).parentLayout, onStyle: (patch) => patchStyle([selectedElement.id], patch), onReset: (keys) => resetOverrides([selectedElement.id], keys), onProps: (patch) => patchProps(selectedElement.id, patch), onCommitStart: beginTransaction, onCommitEnd: endTransaction, sources: dataSources, bindingKeys: bindingKeys, insideRepeat: enclosingDataBlock !== undefined }) }, rightTab)] })) : selectedIds.length > 1 ? (_jsx(MultiSelectPanel, { count: selectedIds.length, canArrange: arrangeable, onAlign: (action) => setElements((els) => alignElements(els, selectedIds, action, breakpoint)), onDistribute: (action) => setElements((els) => distributeElements(els, selectedIds, action, breakpoint)) })) : (_jsx(PageInspector, { rootStyle: rootStyle, onChange: (patch) => patch.layout ? switchPageLayout(patch.layout) : setRootStyle(patch), onApplyStyleKit: applyStyleKit, onLoadShowcase: loadNocturneShowcase })) })] })) })] }), codeComposerOpen && _jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/65 p-6 backdrop-blur-sm", onMouseDown: () => setCodeComposerOpen(false), children: _jsxs("div", { className: "flex h-[min(720px,85vh)] w-[min(820px,92vw)] flex-col overflow-hidden rounded-2xl border border-ed-border bg-ed-surface shadow-2xl", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "flex h-12 items-center justify-between border-b border-ed-border px-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs font-semibold text-ed-text", children: "New code component" }), _jsx("p", { className: "text-[9px] text-ed-faint", children: "Sandboxed HTML and CSS \u2014 scripts stay disabled." })] }), _jsx("button", { type: "button", onClick: () => setCodeComposerOpen(false), className: "rounded-md p-1.5 text-ed-muted hover:bg-ed-field hover:text-ed-text", children: _jsx(IconX, { size: 15 }) })] }), _jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-2", children: [_jsxs("div", { className: "flex min-h-0 flex-col gap-3 border-r border-ed-border p-4", children: [_jsxs("label", { className: "text-[10px] font-medium text-ed-muted", children: ["Name", _jsx("input", { value: codeComponentName, onChange: (event) => setCodeComponentName(event.target.value), className: "mt-1.5 h-9 w-full rounded-lg border border-ed-border bg-ed-field px-3 text-[11px] text-ed-text outline-none focus:border-ed-accent" })] }), _jsxs("label", { className: "flex min-h-0 flex-1 flex-col text-[10px] font-medium text-ed-muted", children: ["HTML / CSS", _jsx("textarea", { value: codeComponentSource, onChange: (event) => setCodeComponentSource(event.target.value), spellCheck: false, className: "mt-1.5 min-h-0 flex-1 resize-none rounded-xl border border-ed-border bg-[#101114] p-3 font-mono text-[11px] leading-relaxed text-zinc-300 outline-none focus:border-ed-accent" })] })] }), _jsxs("div", { className: "flex min-h-0 flex-col bg-ed-canvas p-4", children: [_jsx("span", { className: "mb-2 text-[10px] font-medium text-ed-muted", children: "Preview" }), _jsx("iframe", { title: "Code component preview", srcDoc: codeComponentSource, sandbox: "", className: "min-h-0 flex-1 rounded-xl border border-ed-border bg-white" })] })] }), _jsxs("div", { className: "flex h-14 items-center justify-end gap-2 border-t border-ed-border px-4", children: [_jsx("button", { type: "button", onClick: () => setCodeComposerOpen(false), className: "rounded-lg px-3 py-2 text-[10px] text-ed-muted hover:bg-ed-field", children: "Cancel" }), _jsx("button", { type: "button", onClick: createCodeComponent, disabled: !codeComponentSource.trim(), className: "rounded-lg bg-ed-accent px-3 py-2 text-[10px] font-semibold text-white disabled:opacity-30", children: "Create component" })] })] }) }), breakpointPanel && !componentMode && (_jsxs("div", { className: "fixed left-1/2 top-16 z-[80] w-[420px] -translate-x-1/2 rounded-2xl border border-ed-border bg-ed-surface p-4 shadow-2xl", children: [_jsxs("div", { className: "mb-4 flex items-start justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-ed-text", children: "Breakpoints" }), _jsx("p", { className: "mt-1 text-[11px] text-ed-muted", children: "Rename, resize and reorder the viewports. Main values are shared by every other size." })] }), _jsx("button", { type: "button", onClick: () => setBreakpointPanel(false), className: "rounded p-1 text-ed-muted hover:bg-ed-field", children: _jsx(IconX, { size: 15 }) })] }), _jsx("div", { className: "flex max-h-[360px] flex-col gap-2 overflow-y-auto scrollbar-none", children: breakpointDefs.map((item, index) => (_jsxs("div", { className: "grid grid-cols-[1fr_86px_auto] items-center gap-2 rounded-xl border border-ed-border bg-ed-subtle p-2", children: [_jsx("input", { "aria-label": "Breakpoint name", value: item.name, onChange: (event) => renameBreakpoint(item.id, event.target.value), onBlur: () => commitBreakpointName(item.id), className: "min-w-0 rounded-lg border border-ed-border bg-ed-field px-2.5 py-2 text-xs text-ed-text outline-none focus:border-ed-accent" }), _jsxs("label", { className: "flex items-center rounded-lg border border-ed-border bg-ed-field px-2 py-2 text-xs text-ed-muted", children: [_jsx("input", { "aria-label": "Breakpoint width", type: "number", min: 240, max: 4000, value: item.width, onChange: (event) => resizeBreakpoint(item.id, Number(event.target.value) || 240), className: "w-full bg-transparent text-right tabular-nums text-ed-text outline-none" }), "px"] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [item.id === cascade.baseId ? (_jsx("span", { className: "rounded-md bg-ed-accent/15 px-2 py-1.5 text-[10px] font-semibold text-ed-accent", children: "MAIN" })) : (_jsx("button", { type: "button", title: "Make main", onClick: () => setBaseBreakpoint(item.id), className: "rounded-md px-2 py-1.5 text-[10px] font-semibold text-ed-muted hover:bg-ed-field hover:text-ed-text", children: "MAIN" })), _jsx("button", { type: "button", disabled: index === 0, title: "Move left", onClick: () => { const next = [...breakpointDefs]; [next[index - 1], next[index]] = [next[index], next[index - 1]]; updateBreakpoints(next); }, className: "rounded p-1 text-ed-faint hover:bg-ed-field disabled:opacity-20", children: _jsx(IconChevronLeft, { size: 13 }) }), _jsx("button", { type: "button", disabled: index === breakpointDefs.length - 1, title: "Move right", onClick: () => { const next = [...breakpointDefs]; [next[index], next[index + 1]] = [next[index + 1], next[index]]; updateBreakpoints(next); }, className: "rounded p-1 text-ed-faint hover:bg-ed-field disabled:opacity-20", children: _jsx(IconChevronRight, { size: 13 }) }), item.id !== cascade.baseId && breakpointDefs.length > 1 && (_jsx("button", { type: "button", title: "Delete", onClick: () => removeBreakpoint(item.id), className: "rounded p-1 text-ed-faint hover:bg-red-500/10 hover:text-red-400", children: _jsx(IconTrash, { size: 13 }) }))] })] }, item.id))) })] })), contextMenu && menuElementId && (_jsx(ContextMenu, { x: contextMenu.x, y: contextMenu.y, onBringForward: () => {
1506
+ setElements((els) => reorder(els, menuElementId, "up"));
1507
+ setContextMenu(null);
1508
+ }, onSendBackward: () => {
1509
+ setElements((els) => reorder(els, menuElementId, "down"));
1510
+ setContextMenu(null);
1511
+ }, onWrap: () => {
1512
+ const wrapped = wrapInContainer(elements, menuElementId, breakpoint);
1513
+ if (wrapped) {
1514
+ setElements(wrapped.elements);
1515
+ setSelectedIds([wrapped.wrapperId]);
1516
+ }
1517
+ setContextMenu(null);
1518
+ }, onUnwrap: () => {
1519
+ const el = byId.get(menuElementId);
1520
+ const grandparent = el?.parentId
1521
+ ? byId.get(el.parentId)?.parentId
1522
+ : undefined;
1523
+ doReparent(menuElementId, grandparent);
1524
+ setContextMenu(null);
1525
+ }, onDuplicate: () => duplicateElements([menuElementId]), onCopy: () => {
1526
+ copyElements([menuElementId]);
1527
+ setContextMenu(null);
1528
+ }, onDelete: () => deleteElements([menuElementId]) })), contextMenu && !contextMenu.elementId && (_jsxs("div", { className: "fixed z-[90] w-48 overflow-hidden rounded-xl border border-ed-border bg-ed-surface p-1.5 shadow-2xl", style: { left: contextMenu.x, top: contextMenu.y }, children: [["Text", "Heading", "Button", "Container"].map((type) => (_jsxs("button", { type: "button", onClick: () => {
1529
+ const created = createElement(type, { x: contextMenu.canvasX ?? 0, y: contextMenu.canvasY ?? 0, z: nextZ(elements) });
1530
+ if (type === "Text")
1531
+ created.content = "Canvas note";
1532
+ setElements((current) => [...current, created]);
1533
+ setSelectedIds([created.id]);
1534
+ setContextMenu(null);
1535
+ }, className: "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-xs text-ed-muted hover:bg-ed-field hover:text-ed-text", children: [_jsx(IconPlus, { size: 13 }), " Add ", type] }, type))), _jsx("div", { className: "my-1 h-px bg-ed-border" }), componentMode ? _jsxs("button", { type: "button", onClick: () => { createComponentVariant(); setContextMenu(null); }, disabled: selectedElement?.componentRole !== "master", className: "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-xs font-medium text-ed-muted hover:bg-ed-field hover:text-ed-text disabled:opacity-30", children: [_jsx(IconComponents, { size: 13 }), " Add variant"] }) : _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => {
1536
+ addBreakpoint();
1537
+ setBreakpointPanel(true);
1538
+ setContextMenu(null);
1539
+ }, className: "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-xs font-medium text-ed-muted hover:bg-ed-field hover:text-ed-text", children: [_jsx(IconLayoutColumns, { size: 13 }), " Add breakpoint"] }), _jsxs("button", { type: "button", onClick: () => {
1540
+ setBreakpointPanel(true);
1541
+ setContextMenu(null);
1542
+ }, className: "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-xs text-ed-muted hover:bg-ed-field hover:text-ed-text", children: [_jsx(IconBox, { size: 13 }), " Breakpoint settings"] })] })] })), marquee && _jsx("div", { className: "pointer-events-none fixed z-[110] border border-blue-400 bg-blue-500/20 shadow-[0_0_0_1px_rgba(59,130,246,.15)]", style: { left: Math.min(marquee.startX, marquee.x), top: Math.min(marquee.startY, marquee.y), width: Math.abs(marquee.x - marquee.startX), height: Math.abs(marquee.y - marquee.startY) } }), _jsx("style", {
1543
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: webkit scrollbar pseudo-elements have no Tailwind utility
1544
+ dangerouslySetInnerHTML: {
1545
+ __html: `${customFontCss}
1546
+ .canvas-scrollbar { scrollbar-width: none; -ms-overflow-style: none; }
1547
+ .canvas-scrollbar::-webkit-scrollbar { display: none; width: 0; height: 0; }
1548
+ `,
1549
+ } })] }));
1550
+ }
1551
+ //# sourceMappingURL=editor.js.map