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.
- package/README.md +528 -0
- package/dist/adapters.d.ts +89 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +2 -0
- package/dist/adapters.js.map +1 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +33 -0
- package/dist/client.js.map +1 -0
- package/dist/data.js +210 -0
- package/dist/data.js.map +7 -0
- package/dist/document.d.ts +63 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +25 -0
- package/dist/document.js.map +1 -0
- package/dist/editor.d.ts +14 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +59 -0
- package/dist/editor.js.map +1 -0
- package/dist/full-editor.d.ts +53 -0
- package/dist/full-editor.js +11117 -0
- package/dist/full-editor.js.map +7 -0
- package/dist/full.css +3498 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/app/api/ai-design/route.d.ts +3 -0
- package/dist/internal/app/api/ai-design/route.d.ts.map +1 -0
- package/dist/internal/app/api/ai-design/route.js +165 -0
- package/dist/internal/app/api/ai-design/route.js.map +1 -0
- package/dist/internal/app/p/editor/ai-panel.d.ts +27 -0
- package/dist/internal/app/p/editor/ai-panel.d.ts.map +1 -0
- package/dist/internal/app/p/editor/ai-panel.js +77 -0
- package/dist/internal/app/p/editor/ai-panel.js.map +1 -0
- package/dist/internal/app/p/editor/data-modal.d.ts +26 -0
- package/dist/internal/app/p/editor/data-modal.d.ts.map +1 -0
- package/dist/internal/app/p/editor/data-modal.js +94 -0
- package/dist/internal/app/p/editor/data-modal.js.map +1 -0
- package/dist/internal/app/p/editor/data-panel.d.ts +18 -0
- package/dist/internal/app/p/editor/data-panel.d.ts.map +1 -0
- package/dist/internal/app/p/editor/data-panel.js +45 -0
- package/dist/internal/app/p/editor/data-panel.js.map +1 -0
- package/dist/internal/app/p/editor/editor.d.ts +51 -0
- package/dist/internal/app/p/editor/editor.d.ts.map +1 -0
- package/dist/internal/app/p/editor/editor.js +1551 -0
- package/dist/internal/app/p/editor/editor.js.map +1 -0
- package/dist/internal/app/p/editor/fields.d.ts +111 -0
- package/dist/internal/app/p/editor/fields.d.ts.map +1 -0
- package/dist/internal/app/p/editor/fields.js +141 -0
- package/dist/internal/app/p/editor/fields.js.map +1 -0
- package/dist/internal/app/p/editor/inspector.d.ts +38 -0
- package/dist/internal/app/p/editor/inspector.d.ts.map +1 -0
- package/dist/internal/app/p/editor/inspector.js +336 -0
- package/dist/internal/app/p/editor/inspector.js.map +1 -0
- package/dist/internal/app/p/editor/library.d.ts +18 -0
- package/dist/internal/app/p/editor/library.d.ts.map +1 -0
- package/dist/internal/app/p/editor/library.js +61 -0
- package/dist/internal/app/p/editor/library.js.map +1 -0
- package/dist/internal/app/p/editor/parts.d.ts +75 -0
- package/dist/internal/app/p/editor/parts.d.ts.map +1 -0
- package/dist/internal/app/p/editor/parts.js +261 -0
- package/dist/internal/app/p/editor/parts.js.map +1 -0
- package/dist/internal/app/p/editor/templates-panel.d.ts +6 -0
- package/dist/internal/app/p/editor/templates-panel.d.ts.map +1 -0
- package/dist/internal/app/p/editor/templates-panel.js +12 -0
- package/dist/internal/app/p/editor/templates-panel.js.map +1 -0
- package/dist/internal/app/p/editor/use-canvas-view.d.ts +24 -0
- package/dist/internal/app/p/editor/use-canvas-view.d.ts.map +1 -0
- package/dist/internal/app/p/editor/use-canvas-view.js +242 -0
- package/dist/internal/app/p/editor/use-canvas-view.js.map +1 -0
- package/dist/internal/app/p/editor/use-editor.d.ts +48 -0
- package/dist/internal/app/p/editor/use-editor.d.ts.map +1 -0
- package/dist/internal/app/p/editor/use-editor.js +224 -0
- package/dist/internal/app/p/editor/use-editor.js.map +1 -0
- package/dist/internal/app/p/editor/variables-panel.d.ts +8 -0
- package/dist/internal/app/p/editor/variables-panel.d.ts.map +1 -0
- package/dist/internal/app/p/editor/variables-panel.js +52 -0
- package/dist/internal/app/p/editor/variables-panel.js.map +1 -0
- package/dist/internal/components/ui/select.d.ts +12 -0
- package/dist/internal/components/ui/select.d.ts.map +1 -0
- package/dist/internal/components/ui/select.js +32 -0
- package/dist/internal/components/ui/select.js.map +1 -0
- package/dist/internal/drizzle/index.d.ts +6 -0
- package/dist/internal/drizzle/index.d.ts.map +1 -0
- package/dist/internal/drizzle/index.js +20 -0
- package/dist/internal/drizzle/index.js.map +1 -0
- package/dist/internal/drizzle/schema.d.ts +405 -0
- package/dist/internal/drizzle/schema.d.ts.map +1 -0
- package/dist/internal/drizzle/schema.js +70 -0
- package/dist/internal/drizzle/schema.js.map +1 -0
- package/dist/internal/lib/data/context.d.ts +8 -0
- package/dist/internal/lib/data/context.d.ts.map +1 -0
- package/dist/internal/lib/data/context.js +17 -0
- package/dist/internal/lib/data/context.js.map +1 -0
- package/dist/internal/lib/data/source.d.ts +54 -0
- package/dist/internal/lib/data/source.d.ts.map +1 -0
- package/dist/internal/lib/data/source.js +229 -0
- package/dist/internal/lib/data/source.js.map +1 -0
- package/dist/internal/lib/editor/ai-types.d.ts +44 -0
- package/dist/internal/lib/editor/ai-types.d.ts.map +1 -0
- package/dist/internal/lib/editor/ai-types.js +2 -0
- package/dist/internal/lib/editor/ai-types.js.map +1 -0
- package/dist/internal/lib/editor/arrange.d.ts +12 -0
- package/dist/internal/lib/editor/arrange.d.ts.map +1 -0
- package/dist/internal/lib/editor/arrange.js +72 -0
- package/dist/internal/lib/editor/arrange.js.map +1 -0
- package/dist/internal/lib/editor/cascade.d.ts +39 -0
- package/dist/internal/lib/editor/cascade.d.ts.map +1 -0
- package/dist/internal/lib/editor/cascade.js +86 -0
- package/dist/internal/lib/editor/cascade.js.map +1 -0
- package/dist/internal/lib/editor/icon.d.ts +6 -0
- package/dist/internal/lib/editor/icon.d.ts.map +1 -0
- package/dist/internal/lib/editor/icon.js +13 -0
- package/dist/internal/lib/editor/icon.js.map +1 -0
- package/dist/internal/lib/editor/presets.d.ts +10 -0
- package/dist/internal/lib/editor/presets.d.ts.map +1 -0
- package/dist/internal/lib/editor/presets.js +83 -0
- package/dist/internal/lib/editor/presets.js.map +1 -0
- package/dist/internal/lib/editor/showcase.d.ts +14 -0
- package/dist/internal/lib/editor/showcase.d.ts.map +1 -0
- package/dist/internal/lib/editor/showcase.js +219 -0
- package/dist/internal/lib/editor/showcase.js.map +1 -0
- package/dist/internal/lib/editor/site-templates.d.ts +4 -0
- package/dist/internal/lib/editor/site-templates.d.ts.map +1 -0
- package/dist/internal/lib/editor/site-templates.js +156 -0
- package/dist/internal/lib/editor/site-templates.js.map +1 -0
- package/dist/internal/lib/editor/snap.d.ts +34 -0
- package/dist/internal/lib/editor/snap.d.ts.map +1 -0
- package/dist/internal/lib/editor/snap.js +70 -0
- package/dist/internal/lib/editor/snap.js.map +1 -0
- package/dist/internal/lib/editor/style.d.ts +53 -0
- package/dist/internal/lib/editor/style.d.ts.map +1 -0
- package/dist/internal/lib/editor/style.js +313 -0
- package/dist/internal/lib/editor/style.js.map +1 -0
- package/dist/internal/lib/editor/tree.d.ts +72 -0
- package/dist/internal/lib/editor/tree.d.ts.map +1 -0
- package/dist/internal/lib/editor/tree.js +301 -0
- package/dist/internal/lib/editor/tree.js.map +1 -0
- package/dist/internal/lib/editor/types.d.ts +273 -0
- package/dist/internal/lib/editor/types.d.ts.map +1 -0
- package/dist/internal/lib/editor/types.js +411 -0
- package/dist/internal/lib/editor/types.js.map +1 -0
- package/dist/internal/lib/editor/validate.d.ts +20 -0
- package/dist/internal/lib/editor/validate.d.ts.map +1 -0
- package/dist/internal/lib/editor/validate.js +467 -0
- package/dist/internal/lib/editor/validate.js.map +1 -0
- package/dist/internal/lib/pages.d.ts +83 -0
- package/dist/internal/lib/pages.d.ts.map +1 -0
- package/dist/internal/lib/pages.js +294 -0
- package/dist/internal/lib/pages.js.map +1 -0
- package/dist/internal/lib/render/bind.d.ts +18 -0
- package/dist/internal/lib/render/bind.d.ts.map +1 -0
- package/dist/internal/lib/render/bind.js +28 -0
- package/dist/internal/lib/render/bind.js.map +1 -0
- package/dist/internal/lib/render/css.d.ts +31 -0
- package/dist/internal/lib/render/css.d.ts.map +1 -0
- package/dist/internal/lib/render/css.js +239 -0
- package/dist/internal/lib/render/css.js.map +1 -0
- package/dist/internal/lib/render/load-data.d.ts +15 -0
- package/dist/internal/lib/render/load-data.d.ts.map +1 -0
- package/dist/internal/lib/render/load-data.js +39 -0
- package/dist/internal/lib/render/load-data.js.map +1 -0
- package/dist/internal/lib/render/page-render.d.ts +17 -0
- package/dist/internal/lib/render/page-render.d.ts.map +1 -0
- package/dist/internal/lib/render/page-render.js +117 -0
- package/dist/internal/lib/render/page-render.js.map +1 -0
- package/dist/internal/lib/utils.d.ts +3 -0
- package/dist/internal/lib/utils.d.ts.map +1 -0
- package/dist/internal/lib/utils.js +6 -0
- package/dist/internal/lib/utils.js.map +1 -0
- package/dist/manrope-data.d.ts +3 -0
- package/dist/manrope-data.d.ts.map +1 -0
- package/dist/manrope-data.js +3 -0
- package/dist/manrope-data.js.map +1 -0
- package/dist/page.d.ts +9 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +148 -0
- package/dist/page.js.map +1 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +45 -0
- package/dist/provider.js.map +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +717 -0
- package/dist/runtime.js.map +7 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +2267 -0
- package/dist/server.js.map +7 -0
- package/dist/styles.css +300 -0
- package/package.json +94 -0
- 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
|