@oh-just-another/editor 0.58.2 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -0
- package/README.md +40 -6
- package/dist/.tsbuildinfo +1 -1
- package/dist/Diagram.d.ts +19 -1
- package/dist/Diagram.d.ts.map +1 -1
- package/dist/Diagram.js +173 -180
- package/dist/Diagram.js.map +1 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +1 -8
- package/dist/capabilities.js.map +1 -1
- package/dist/constants.d.ts +17 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/define-shape.d.ts +71 -0
- package/dist/define-shape.d.ts.map +1 -0
- package/dist/define-shape.js +41 -0
- package/dist/define-shape.js.map +1 -0
- package/dist/dom-focus.d.ts +7 -4
- package/dist/dom-focus.d.ts.map +1 -1
- package/dist/dom-focus.js +7 -9
- package/dist/dom-focus.js.map +1 -1
- package/dist/file-actions.d.ts +52 -0
- package/dist/file-actions.d.ts.map +1 -0
- package/dist/file-actions.js +201 -0
- package/dist/file-actions.js.map +1 -0
- package/dist/gif-animation.d.ts +7 -0
- package/dist/gif-animation.d.ts.map +1 -0
- package/dist/gif-animation.js +99 -0
- package/dist/gif-animation.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/png-export.d.ts +3 -9
- package/dist/png-export.d.ts.map +1 -1
- package/dist/png-export.js +5 -13
- package/dist/png-export.js.map +1 -1
- package/dist/themed-portal-container.d.ts +25 -0
- package/dist/themed-portal-container.d.ts.map +1 -0
- package/dist/themed-portal-container.js +47 -0
- package/dist/themed-portal-container.js.map +1 -0
- package/package.json +20 -18
package/dist/Diagram.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { Clipboard, Copy, Delete, Download, FileDown, FileUp, Grid3x3, Grip, HelpCircle, ImageDown, Library as LibraryIcon, Magnet, Maximize, Minus, Monitor, Moon, MousePointer, Plus, Redo2, RotateCcw, Scissors, Sun, Undo2, ZoomIn, ZoomOut, } from "lucide-react";
|
|
4
|
-
import { BottomBar, BottomSheet, ButtonGroup, ContextMenu, DEFAULT_CONTEXT_MENU, DEFAULT_VERTICAL_TOOLBAR, DiagramRoot, CommandPalette, DiagramSurface, HelpButton, HelpDialog, IconButton, LibraryPanel, MainMenu, ResetToContentButton, LinkHoverPopup, LinkDropShapeMenu, LinkCaptionEditor, SelectionFloatingPanel, TextEditorOverlay, FrameNameEditorOverlay, ToastHost, Toolbar, Tooltip, TooltipProvider, TopBar, UILayer, useDiagramOptional, useHelpDialogHotkey, useMobileLayout, usePalettePlacement, useScene, } from "@oh-just-another/react-ui";
|
|
4
|
+
import { BottomBar, BottomSheet, ButtonGroup, ContextMenu, DEFAULT_CONTEXT_MENU, DEFAULT_VERTICAL_TOOLBAR, DiagramRoot, CommandPalette, DiagramSurface, HelpButton, HelpDialog, IconButton, DrawingPanel, LibraryPanel, MainMenu, Minimap, ResetToContentButton, LinkHoverPopup, LinkDropShapeMenu, LinkCaptionEditor, SelectionFloatingPanel, SearchOverlay, StatsPanel, TextEditorOverlay, FrameNameEditorOverlay, PortalContainerProvider, ToastHost, Toolbar, Tooltip, TooltipProvider, TopBar, UILayer, ZenModeProvider, useDiagramOptional, useHelpDialogHotkey, useMobileLayout, usePalettePlacement, useScene, useZenMode, } from "@oh-just-another/react-ui";
|
|
5
5
|
/**
|
|
6
6
|
* Lucide icon sizing — `MENU_ICON_SIZE` is for in-row icons of
|
|
7
7
|
* `MainMenu.Item`, `TOGGLE_ICON_SIZE` is for the segmented Theme /
|
|
@@ -19,20 +19,21 @@ const buttonIcon = { size: BUTTON_ICON_SIZE, strokeWidth: BUTTON_ICON_STROKE };
|
|
|
19
19
|
/** Default target for the Help-menu "GitHub" link (overridable / hideable via the `repositoryUrl` prop). */
|
|
20
20
|
const DEFAULT_REPOSITORY_URL = "https://github.com/oh-just-another/diagram";
|
|
21
21
|
import { formatHotkey } from "@oh-just-another/state";
|
|
22
|
-
import {
|
|
23
|
-
import { parseScene, stringifyScene } from "@oh-just-another/serialization";
|
|
24
|
-
import { renderSceneToSvg } from "@oh-just-another/renderer-svg";
|
|
22
|
+
import { hydrateScene, isText, } from "@oh-just-another/scene";
|
|
25
23
|
import { WasmTextShaper } from "@oh-just-another/text-wasm";
|
|
26
24
|
import { WasmRasterizer } from "@oh-just-another/raster-wasm";
|
|
25
|
+
import { registerBundledFonts } from "@oh-just-another/fonts";
|
|
27
26
|
import { createRenderWorker } from "@oh-just-another/renderer-canvas";
|
|
28
27
|
import { registerAnimationAdapter, setActiveRasterizer, setActiveTextShaper, } from "@oh-just-another/renderer-core";
|
|
29
28
|
import { registerLayoutKind } from "@oh-just-another/scene";
|
|
30
29
|
import { defaultRegistry } from "@oh-just-another/templates";
|
|
31
30
|
import { detectCapabilities, logCapabilities, } from "./capabilities";
|
|
32
|
-
import {
|
|
31
|
+
import { installGifAnimationAdapter } from "./gif-animation.js";
|
|
32
|
+
import { useThemedPortalContainer } from "./themed-portal-container.js";
|
|
33
|
+
import { downloadScene, downloadSvg, downloadPng, openSceneFile, copySceneAsImage, registerFileActions, setFileActionNotifier, } from "./file-actions.js";
|
|
33
34
|
import { isEditableTarget } from "./dom-focus";
|
|
34
35
|
export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
35
|
-
const { initialScene, initialMode = "select", templates, fileDropHandlers, layoutKinds, animationAdapters, onReady, onSceneChange, onSelectionChange, capabilities: capabilityOverrides, workerFactory, hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton, hideMainMenu, hideZoomControls, hideResetToContent, hideHelpButton, hideContextMenu, hideSelectionPanel, renderTopBarLeft, renderTopBarCenter, renderTopBarRight, renderBottomBarLeft, renderBottomBarCenter, renderBottomBarRight, renderMainMenuExtras, onImportTemplates, theme: themeProp, defaultTheme = "system", onThemeChange, persistTheme, repositoryUrl, onConfirm, onNotify, className, style, } = props;
|
|
36
|
+
const { initialScene, initialMode = "select", grid, snap, templates, fileDropHandlers, layoutKinds, animationAdapters, onReady, onSceneChange, onSelectionChange, capabilities: capabilityOverrides, workerFactory, hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton, hideMainMenu, hideZoomControls, hideResetToContent, hideHelpButton, hideContextMenu, hideSelectionPanel, hideDrawingPanel, minimap, renderTopBarLeft, renderTopBarCenter, renderTopBarRight, renderBottomBarLeft, renderBottomBarCenter, renderBottomBarRight, renderMainMenuExtras, onImportTemplates, theme: themeProp, defaultTheme = "system", onThemeChange, persistTheme, repositoryUrl, onConfirm, onNotify, className, style, } = props;
|
|
36
37
|
// Theme: controlled when `themeProp` is provided, otherwise
|
|
37
38
|
// self-managed via `internalTheme`. The lazy `useState` initializer
|
|
38
39
|
// reads from `localStorage` once when `persistTheme` is on.
|
|
@@ -52,6 +53,9 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
52
53
|
return defaultTheme;
|
|
53
54
|
});
|
|
54
55
|
const theme = themeProp ?? internalTheme;
|
|
56
|
+
// Floating UI portals here (a body-level wrapper that mirrors `theme`) so it
|
|
57
|
+
// inherits the app theme instead of the OS `prefers-color-scheme` fallback.
|
|
58
|
+
const portalContainer = useThemedPortalContainer(theme);
|
|
55
59
|
const changeTheme = useCallback((next) => {
|
|
56
60
|
if (themeProp === undefined)
|
|
57
61
|
setInternalTheme(next);
|
|
@@ -60,17 +64,30 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
60
64
|
}
|
|
61
65
|
onThemeChange?.(next);
|
|
62
66
|
}, [themeProp, onThemeChange, storageKey]);
|
|
63
|
-
|
|
67
|
+
// Seed scene: host grid/snap props are merged over the defaults; a persisted
|
|
68
|
+
// `initialScene` (user data) wins over them. Depend on primitives so an
|
|
69
|
+
// inline `grid` object prop doesn't re-seed the editor every render.
|
|
70
|
+
const gridEnabled = grid?.enabled;
|
|
71
|
+
const gridStyle = grid?.style;
|
|
72
|
+
const seed = useMemo(() => hydrateScene({
|
|
73
|
+
...(initialScene ? { saved: initialScene } : {}),
|
|
74
|
+
hostSettings: buildHostSettings(gridEnabled, gridStyle, snap),
|
|
75
|
+
}), [initialScene, gridEnabled, gridStyle, snap]);
|
|
64
76
|
// Does the initial scene contain any text? Drives whether first paint
|
|
65
77
|
// waits for the MSDF shaper (see the mount gate below).
|
|
66
78
|
const sceneHasText = useMemo(() => {
|
|
67
79
|
for (const s of seed.elements.values())
|
|
68
|
-
if (s
|
|
80
|
+
if (isText(s))
|
|
69
81
|
return true;
|
|
70
82
|
return false;
|
|
71
83
|
}, [seed]);
|
|
72
84
|
// --- Plugin registration ---
|
|
73
85
|
useEffect(() => {
|
|
86
|
+
// Built-in GIF decoder, registered by default so dropped / pasted GIFs play
|
|
87
|
+
// out of the box. Idempotent + lazy (gifuct-js loads on first decode). A
|
|
88
|
+
// host `animationAdapters` entry with kind "gif" overrides it (those are
|
|
89
|
+
// registered after).
|
|
90
|
+
installGifAnimationAdapter();
|
|
74
91
|
if (templates)
|
|
75
92
|
for (const t of templates)
|
|
76
93
|
defaultRegistry.register(t);
|
|
@@ -90,6 +107,9 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
90
107
|
// font is ready, so text doesn't render in a fallback font and then
|
|
91
108
|
// snap to the WASM font ("jump" on load — a FOUT).
|
|
92
109
|
const [wasmTextSettled, setWasmTextSettled] = useState(false);
|
|
110
|
+
// Flipped once the bundled web fonts finish loading, so the canvas can
|
|
111
|
+
// redraw text in them (the browser doesn't auto-repaint canvas text).
|
|
112
|
+
const [fontsReady, setFontsReady] = useState(false);
|
|
93
113
|
const detectionRef = useRef(null);
|
|
94
114
|
const loggedRef = useRef(false);
|
|
95
115
|
useEffect(() => {
|
|
@@ -106,6 +126,18 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
106
126
|
}
|
|
107
127
|
setProfile(detected);
|
|
108
128
|
const loads = [];
|
|
129
|
+
// Load the bundled fonts so every backend draws the same faces; redraw
|
|
130
|
+
// once they settle.
|
|
131
|
+
loads.push(registerBundledFonts(document).then(() => {
|
|
132
|
+
if (!cancelled)
|
|
133
|
+
setFontsReady(true);
|
|
134
|
+
}, (err) => {
|
|
135
|
+
// Settle even on failure so a text scene still mounts (in the
|
|
136
|
+
// fallback font) instead of hanging on the first-paint gate.
|
|
137
|
+
if (!cancelled)
|
|
138
|
+
setFontsReady(true);
|
|
139
|
+
console.warn("[diagram] bundled fonts load failed", err);
|
|
140
|
+
}));
|
|
109
141
|
if (detected.wasmText) {
|
|
110
142
|
loads.push(WasmTextShaper.loadBundled().then((shaper) => {
|
|
111
143
|
if (cancelled)
|
|
@@ -156,19 +188,24 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
156
188
|
editor.setMode(editor.mode);
|
|
157
189
|
return undefined;
|
|
158
190
|
}, [editor, wasmShaper, wasmRaster]);
|
|
159
|
-
// Animation adapters (GIF decoder) are registered in the plugin
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// `
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// decoding and stays blank.
|
|
191
|
+
// Animation adapters (GIF decoder) are registered in the plugin effect above,
|
|
192
|
+
// which runs AFTER the editor's first paint (child effects fire before parent
|
|
193
|
+
// ones). Force one render once the editor is ready so each animated shape's
|
|
194
|
+
// first `getFrameAt` runs — that kicks off the async decode, after which the
|
|
195
|
+
// decode→re-render nudge (`onAnimationContentReady`) paints the frame. The
|
|
196
|
+
// built-in GIF adapter is always registered, so this nudge is unconditional
|
|
197
|
+
// (also re-runs if a host swaps `animationAdapters`). Without it, a paused GIF
|
|
198
|
+
// restored from storage never even starts decoding and stays blank.
|
|
167
199
|
useEffect(() => {
|
|
168
|
-
if (editor
|
|
200
|
+
if (editor)
|
|
169
201
|
editor.forceRender();
|
|
170
|
-
}
|
|
171
202
|
}, [editor, animationAdapters]);
|
|
203
|
+
// Redraw once the bundled fonts load so canvas text switches from the
|
|
204
|
+
// fallback face to the bundled one.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (editor && fontsReady)
|
|
207
|
+
editor.forceRender();
|
|
208
|
+
}, [editor, fontsReady]);
|
|
172
209
|
useEffect(() => {
|
|
173
210
|
if (!editor || (!onSceneChange && !onSelectionChange))
|
|
174
211
|
return undefined;
|
|
@@ -185,30 +222,58 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
185
222
|
}
|
|
186
223
|
});
|
|
187
224
|
}, [editor, onSceneChange, onSelectionChange]);
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
225
|
+
// Track transform modifiers off every key event (and reset on blur) so a
|
|
226
|
+
// missed keyup can't leave a flag stuck: Cmd/Ctrl pulls a shape off the grid
|
|
227
|
+
// for one drag; Alt resizes about the centre; Shift locks the resize aspect
|
|
228
|
+
// ratio or constrains a move to one axis.
|
|
191
229
|
useEffect(() => {
|
|
192
230
|
if (!editor)
|
|
193
231
|
return undefined;
|
|
232
|
+
const ed = editor;
|
|
194
233
|
const sync = (e) => {
|
|
195
|
-
// Don't track
|
|
196
|
-
//
|
|
234
|
+
// Don't track modifiers (or touch snap state) while typing in a field —
|
|
235
|
+
// keep the editor's transform flags inert there.
|
|
236
|
+
if (isEditableTarget(e.target))
|
|
237
|
+
return;
|
|
238
|
+
ed.setSnapSuppressed(e.metaKey || e.ctrlKey);
|
|
239
|
+
ed.setTransformModifiers({ alt: e.altKey, shift: e.shiftKey });
|
|
240
|
+
};
|
|
241
|
+
// Flowchart CREATE lifecycle: Cmd/Ctrl+Arrow grows a pending preview; the
|
|
242
|
+
// session commits when Cmd/Ctrl is released and cancels on Escape. Keydown
|
|
243
|
+
// handles the cancel; keyup the commit.
|
|
244
|
+
const onKeyDown = (e) => {
|
|
245
|
+
sync(e);
|
|
197
246
|
if (isEditableTarget(e.target))
|
|
198
247
|
return;
|
|
199
|
-
|
|
248
|
+
if (e.key === "Escape" && ed.flowchartPreview !== null) {
|
|
249
|
+
ed.cancelFlowchart();
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const onKeyUp = (e) => {
|
|
254
|
+
sync(e);
|
|
255
|
+
// Cmd/Ctrl released while a session is open → commit the preview.
|
|
256
|
+
if (!e.metaKey && !e.ctrlKey && ed.flowchartPreview !== null) {
|
|
257
|
+
ed.commitFlowchart();
|
|
258
|
+
}
|
|
200
259
|
};
|
|
201
260
|
const reset = () => {
|
|
202
|
-
|
|
261
|
+
ed.setSnapSuppressed(false);
|
|
262
|
+
ed.setTransformModifiers({ alt: false, shift: false });
|
|
263
|
+
// A window blur can swallow the Cmd/Ctrl keyup — commit any live session
|
|
264
|
+
// so a missed keyup can't strand the preview.
|
|
265
|
+
if (ed.flowchartPreview !== null)
|
|
266
|
+
ed.commitFlowchart();
|
|
203
267
|
};
|
|
204
|
-
window.addEventListener("keydown",
|
|
205
|
-
window.addEventListener("keyup",
|
|
268
|
+
window.addEventListener("keydown", onKeyDown);
|
|
269
|
+
window.addEventListener("keyup", onKeyUp);
|
|
206
270
|
window.addEventListener("blur", reset);
|
|
207
271
|
return () => {
|
|
208
|
-
window.removeEventListener("keydown",
|
|
209
|
-
window.removeEventListener("keyup",
|
|
272
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
273
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
210
274
|
window.removeEventListener("blur", reset);
|
|
211
|
-
|
|
275
|
+
ed.setSnapSuppressed(false);
|
|
276
|
+
ed.setTransformModifiers({ alt: false, shift: false });
|
|
212
277
|
};
|
|
213
278
|
}, [editor]);
|
|
214
279
|
useImperativeHandle(ref, () => ({
|
|
@@ -233,24 +298,26 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
|
|
|
233
298
|
if (!profile) {
|
|
234
299
|
return _jsx("div", { className: className, style: style });
|
|
235
300
|
}
|
|
236
|
-
// Hold the first paint of a text-bearing scene until
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
301
|
+
// Hold the first paint of a text-bearing scene until its font is ready, so
|
|
302
|
+
// text renders in its final face from frame one (no fallback-font jump).
|
|
303
|
+
// Every backend now draws the bundled fonts, so all wait on `fontsReady`;
|
|
304
|
+
// the WebGL2 MSDF path also waits on the shaper. Text-free scenes mount
|
|
305
|
+
// immediately and don't pay the load latency.
|
|
306
|
+
if (sceneHasText && !fontsReady) {
|
|
307
|
+
return _jsx("div", { className: className, style: style });
|
|
308
|
+
}
|
|
242
309
|
if (profile.renderer === "webgl2" && profile.wasmText && !wasmTextSettled && sceneHasText) {
|
|
243
310
|
return _jsx("div", { className: className, style: style });
|
|
244
311
|
}
|
|
245
|
-
return (_jsx(ToastHost, { children: _jsx(TooltipProvider, { children: _jsx("div", { className: className, "data-diagram-root": true, ...(theme === "system" ? {} : { "data-theme": theme }), style: {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
312
|
+
return (_jsx(PortalContainerProvider, { container: portalContainer, children: _jsx(ToastHost, { children: _jsx(TooltipProvider, { children: _jsx("div", { className: className, "data-diagram-root": true, ...(theme === "system" ? {} : { "data-theme": theme }), style: {
|
|
313
|
+
position: "relative",
|
|
314
|
+
width: "100%",
|
|
315
|
+
height: "100%",
|
|
316
|
+
background: "var(--du-canvas-bg)",
|
|
317
|
+
...style,
|
|
318
|
+
}, children: _jsx(DiagramRoot, { initialScene: seed, initialMode: initialMode, onReady: handleReady, renderer: profile.renderer, ...(profile.renderer === "offscreen"
|
|
319
|
+
? { workerFactory: workerFactory ?? createRenderWorker }
|
|
320
|
+
: {}), ...(wasmShaper ? { textShaper: wasmShaper } : {}), ...(wasmRaster ? { rasterizer: wasmRaster } : {}), children: _jsx(ZenModeProvider, { children: _jsx(EditorShell, { hideTopBar: hideTopBar, hideBottomBar: hideBottomBar, hideToolbar: hideToolbar, hideLibraryButton: hideLibraryButton, hideMainMenu: hideMainMenu, hideZoomControls: hideZoomControls, hideResetToContent: hideResetToContent, hideHelpButton: hideHelpButton, hideContextMenu: hideContextMenu, hideSelectionPanel: hideSelectionPanel, hideDrawingPanel: hideDrawingPanel, minimap: minimap, renderTopBarLeft: renderTopBarLeft, renderTopBarCenter: renderTopBarCenter, renderTopBarRight: renderTopBarRight, renderBottomBarLeft: renderBottomBarLeft, renderBottomBarCenter: renderBottomBarCenter, renderBottomBarRight: renderBottomBarRight, renderMainMenuExtras: renderMainMenuExtras, onImportTemplates: onImportTemplates, repositoryUrl: repositoryUrl, onConfirm: onConfirm, onNotify: onNotify, theme: theme, changeTheme: changeTheme }) }) }) }) }) }) }));
|
|
254
321
|
});
|
|
255
322
|
// Match the primary public name (`<Editor>`) in DevTools / stack traces,
|
|
256
323
|
// even though the internal forwardRef function is named `Diagram`.
|
|
@@ -261,8 +328,11 @@ Diagram.displayName = "Editor";
|
|
|
261
328
|
* `useHelpDialogHotkey`) resolve correctly. Composes the
|
|
262
329
|
* canvas-surface + ui-layer overlay + side panels into one tree.
|
|
263
330
|
*/
|
|
264
|
-
const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton, hideMainMenu, hideZoomControls, hideResetToContent, hideHelpButton, hideContextMenu, hideSelectionPanel, renderTopBarLeft, renderTopBarCenter, renderTopBarRight, renderBottomBarLeft, renderBottomBarCenter, renderBottomBarRight, renderMainMenuExtras, onImportTemplates, repositoryUrl, onConfirm, onNotify, theme, changeTheme, }) => {
|
|
331
|
+
const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton, hideMainMenu, hideZoomControls, hideResetToContent, hideHelpButton, hideContextMenu, hideSelectionPanel, hideDrawingPanel, minimap, renderTopBarLeft, renderTopBarCenter, renderTopBarRight, renderBottomBarLeft, renderBottomBarCenter, renderBottomBarRight, renderMainMenuExtras, onImportTemplates, repositoryUrl, onConfirm, onNotify, theme, changeTheme, }) => {
|
|
265
332
|
const editor = useDiagramOptional();
|
|
333
|
+
// Zen mode (⌥Z): hide every chrome surface for focused work, leaving the
|
|
334
|
+
// canvas + the observational overlays (search, stats, command palette).
|
|
335
|
+
const { zen } = useZenMode();
|
|
266
336
|
// Omitted → project repo; explicit string → that URL; null → no link.
|
|
267
337
|
const repositoryHref = repositoryUrl === undefined ? DEFAULT_REPOSITORY_URL : repositoryUrl;
|
|
268
338
|
// Native dialogs by default; hosts can route through their own UI.
|
|
@@ -271,8 +341,16 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
|
|
|
271
341
|
((message) => {
|
|
272
342
|
window.alert(message);
|
|
273
343
|
});
|
|
344
|
+
// Register the file-ops actions (Save / Open / Export / Copy-as-image)
|
|
345
|
+
// on the shared registry so hosts binding hotkeys / the command palette
|
|
346
|
+
// pick them up, and route their error messages through this shell's
|
|
347
|
+
// notifier (host toast or the alert fallback above).
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
registerFileActions();
|
|
350
|
+
setFileActionNotifier(notify);
|
|
351
|
+
}, [notify]);
|
|
274
352
|
// Subscribe to scene changes so the Grid toggle in MainMenu reads
|
|
275
|
-
// the latest viewport.
|
|
353
|
+
// the latest viewport.gridEnabled / gridStyle. `useScene` is a thin
|
|
276
354
|
// selector hook — re-renders only on scene identity flips.
|
|
277
355
|
void useScene();
|
|
278
356
|
const paletteDropHandlers = usePalettePlacement();
|
|
@@ -319,7 +397,7 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
|
|
|
319
397
|
bottom: 0,
|
|
320
398
|
left: 0,
|
|
321
399
|
right: 0,
|
|
322
|
-
}, children: [_jsx(DiagramSurface, { style: { position: "absolute", inset: 0 } }), _jsx(TextEditorOverlay, {}), _jsx(FrameNameEditorOverlay, {}), _jsx(LinkHoverPopup, {}), _jsx(LinkDropShapeMenu, {}), _jsx(LinkCaptionEditor, {}), !hideContextMenu && _jsx(ContextMenu, { items: DEFAULT_CONTEXT_MENU })] }), !hideToolbar ? (_jsx(Toolbar, { orientation: "vertical", items: toolbarItems, style: {
|
|
400
|
+
}, children: [_jsx(DiagramSurface, { style: { position: "absolute", inset: 0 } }), _jsx(TextEditorOverlay, {}), _jsx(FrameNameEditorOverlay, {}), _jsx(LinkHoverPopup, {}), _jsx(LinkDropShapeMenu, {}), _jsx(LinkCaptionEditor, {}), !hideContextMenu && _jsx(ContextMenu, { items: DEFAULT_CONTEXT_MENU })] }), !hideToolbar && !zen ? (_jsx(Toolbar, { orientation: "vertical", items: toolbarItems, style: {
|
|
323
401
|
position: "absolute",
|
|
324
402
|
// Vertically centred on the left; floats just to the right of
|
|
325
403
|
// the library when it's open (else flush near the edge).
|
|
@@ -329,18 +407,22 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
|
|
|
329
407
|
left: `calc(env(safe-area-inset-left, 0px) + ${toolbarLeft}px)`,
|
|
330
408
|
transform: "translateY(-50%)",
|
|
331
409
|
zIndex: 60,
|
|
332
|
-
} })) : null, _jsxs(UILayer, { children: [!hideTopBar && (_jsx(TopBar, { left: _jsxs(ButtonGroup, { ariaLabel: "Logo and main menu", children: [_jsx("span", { "aria-label": "Diagram", title: "Diagram", className: "du-icon-button", style: {
|
|
410
|
+
} })) : null, _jsxs(UILayer, { children: [!hideTopBar && !zen && (_jsx(TopBar, { left: _jsxs(ButtonGroup, { ariaLabel: "Logo and main menu", children: [_jsx("span", { "aria-label": "Diagram", title: "Diagram", className: "du-icon-button", style: {
|
|
333
411
|
minWidth: 36,
|
|
334
412
|
padding: "0 10px",
|
|
335
413
|
cursor: "default",
|
|
336
414
|
fontWeight: 600,
|
|
337
415
|
letterSpacing: 0.5,
|
|
338
416
|
}, children: "\u2317" }), !hideMainMenu && (_jsxs(MainMenu, { children: [_jsxs(MainMenu.Group, { title: "File", children: [_jsx(MainMenu.Item, { icon: _jsx(FileUp, { ...menuIcon }), onClick: () => {
|
|
339
|
-
|
|
340
|
-
|
|
417
|
+
if (editor)
|
|
418
|
+
openSceneFile(editor);
|
|
419
|
+
}, shortcut: formatHotkey({ key: "O", meta: true }), children: "Open\u2026" }), _jsx(MainMenu.Item, { icon: _jsx(FileDown, { ...menuIcon }), onClick: () => {
|
|
341
420
|
if (editor)
|
|
342
421
|
downloadScene(editor.scene);
|
|
343
|
-
}, disabled: !editor,
|
|
422
|
+
}, disabled: !editor, shortcut: formatHotkey({ key: "S", meta: true }), children: "Save as JSON" }), _jsx(MainMenu.Item, { icon: _jsx(Copy, { ...menuIcon }), onClick: () => {
|
|
423
|
+
if (editor)
|
|
424
|
+
void copySceneAsImage(editor);
|
|
425
|
+
}, disabled: !editor, shortcut: formatHotkey({ key: "C", shift: true, alt: true }), children: "Copy as image" }), _jsxs(MainMenu.Submenu, { icon: _jsx(Download, { ...menuIcon }), label: "Export\u2026", disabled: !editor, children: [_jsx(MainMenu.Item, { icon: _jsx(ImageDown, { ...menuIcon }), onClick: () => editor && void downloadPng(editor, "transparent"), disabled: !editor, children: "PNG (transparent)" }), _jsx(MainMenu.Item, { icon: _jsx(ImageDown, { ...menuIcon }), onClick: () => editor && void downloadPng(editor, "color"), disabled: !editor, shortcut: formatHotkey({ key: "E", meta: true, shift: true }), children: "PNG (with background)" }), _jsx(MainMenu.Item, { icon: _jsx(ImageDown, { ...menuIcon }), onClick: () => editor && void downloadPng(editor, "color-and-grid"), disabled: !editor, children: "PNG (with background + grid)" }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(Download, { ...menuIcon }), onClick: () => {
|
|
344
426
|
if (editor)
|
|
345
427
|
downloadSvg(editor.scene);
|
|
346
428
|
}, disabled: !editor, children: "SVG" })] }), _jsx(MainMenu.Item, { icon: _jsx(RotateCcw, { ...menuIcon }), onClick: () => {
|
|
@@ -348,10 +430,10 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
|
|
|
348
430
|
return;
|
|
349
431
|
if (confirmDialog("Reset canvas? This clears all shapes.")) {
|
|
350
432
|
// editor.clear() keeps viewport (zoom /
|
|
351
|
-
// pan /
|
|
433
|
+
// pan / gridEnabled) and layers — only
|
|
352
434
|
// shapes / edges go. loadScene(emptyScene())
|
|
353
|
-
// would also
|
|
354
|
-
// DEFAULT_VIEWPORT has
|
|
435
|
+
// would also reset the grid because
|
|
436
|
+
// DEFAULT_VIEWPORT has it disabled.
|
|
355
437
|
editor.clear();
|
|
356
438
|
}
|
|
357
439
|
}, disabled: !editor, children: "Reset canvas" })] }), _jsx(MainMenu.Separator, {}), _jsxs(MainMenu.Group, { title: "Edit", children: [_jsx(MainMenu.Item, { icon: _jsx(Undo2, { ...menuIcon }), shortcut: "\u2318Z", onClick: () => editor?.undo(), disabled: !editor, children: "Undo" }), _jsx(MainMenu.Item, { icon: _jsx(Redo2, { ...menuIcon }), shortcut: "\u21E7\u2318Z", onClick: () => editor?.redo(), disabled: !editor, children: "Redo" }), _jsx(MainMenu.Item, { icon: _jsx(Scissors, { ...menuIcon }), shortcut: "\u2318X", onClick: () => editor?.cutSelected(), disabled: !editor, children: "Cut" }), _jsx(MainMenu.Item, { icon: _jsx(Copy, { ...menuIcon }), shortcut: "\u2318C", onClick: () => editor?.copySelected(), disabled: !editor, children: "Copy" }), _jsx(MainMenu.Item, { icon: _jsx(Clipboard, { ...menuIcon }), shortcut: "\u2318V", onClick: () => editor?.paste(), disabled: !editor, children: "Paste" }), _jsx(MainMenu.Item, { icon: _jsx(MousePointer, { ...menuIcon }), shortcut: "\u2318A", onClick: () => editor?.selectAll(), disabled: !editor, children: "Select all" }), _jsx(MainMenu.Item, { icon: _jsx(Delete, { ...menuIcon }), shortcut: "\u232B", onClick: () => editor?.deleteSelected(), disabled: !editor, children: "Delete selected" })] }), _jsx(MainMenu.Separator, {}), _jsxs(MainMenu.Group, { title: "View", children: [_jsx(MainMenu.Item, { icon: _jsx(Maximize, { ...menuIcon }), shortcut: "\u21E7F", onClick: () => editor?.zoomToFit(), disabled: !editor, children: "Fit to screen" }), _jsx(MainMenu.Item, { icon: _jsx(ZoomIn, { ...menuIcon }), shortcut: "\u2318+", onClick: () => editor?.zoomIn(), disabled: !editor, children: "Zoom in" }), _jsx(MainMenu.Item, { icon: _jsx(ZoomOut, { ...menuIcon }), shortcut: "\u2318\u2212", onClick: () => editor?.zoomOut(), disabled: !editor, children: "Zoom out" })] }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Group, { title: "Theme", children: _jsx(MainMenu.Toggle, { value: theme, onChange: changeTheme, options: [
|
|
@@ -371,24 +453,39 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
|
|
|
371
453
|
{ value: "off", label: "Off", icon: _jsx(Minus, { ...toggleIcon }) },
|
|
372
454
|
] }) }), _jsx(MainMenu.Separator, {}), _jsxs(MainMenu.Group, { title: "Help", children: [_jsx(MainMenu.Item, { icon: _jsx(HelpCircle, { ...menuIcon }), shortcut: "?", onClick: () => {
|
|
373
455
|
setHelpOpen(true);
|
|
374
|
-
}, children: "Hotkeys" }), repositoryHref ? (_jsx(MainMenu.ItemLink, { href: repositoryHref, external: true, children: "GitHub" })) : null] }), renderMainMenuExtras ? (_jsxs(_Fragment, { children: [_jsx(MainMenu.Separator, {}), renderMainMenuExtras()] })) : null] })), renderTopBarLeft ? renderTopBarLeft() : null] }), center: renderTopBarCenter?.(), right: _jsx(ButtonGroup, { ariaLabel: "Top bar actions", children: renderTopBarRight ? renderTopBarRight() : null }) })), !hideBottomBar && (_jsx(BottomBar, { left: renderBottomBarLeft ? renderBottomBarLeft() : null, center: renderBottomBarCenter ? (renderBottomBarCenter()) : !hideResetToContent ? (_jsx(ResetToContentButton, {})) : null, right: renderBottomBarRight ? (renderBottomBarRight()) : !hideZoomControls ? (
|
|
456
|
+
}, children: "Hotkeys" }), repositoryHref ? (_jsx(MainMenu.ItemLink, { href: repositoryHref, external: true, children: "GitHub" })) : null] }), renderMainMenuExtras ? (_jsxs(_Fragment, { children: [_jsx(MainMenu.Separator, {}), renderMainMenuExtras()] })) : null] })), renderTopBarLeft ? renderTopBarLeft() : null] }), center: renderTopBarCenter?.(), right: _jsx(ButtonGroup, { ariaLabel: "Top bar actions", children: renderTopBarRight ? renderTopBarRight() : null }) })), !hideBottomBar && !zen && (_jsx(BottomBar, { left: renderBottomBarLeft ? renderBottomBarLeft() : null, center: renderBottomBarCenter ? (renderBottomBarCenter()) : !hideResetToContent ? (_jsx(ResetToContentButton, {})) : null, right: renderBottomBarRight ? (renderBottomBarRight()) : !hideZoomControls ? (
|
|
375
457
|
// Help sits inside the zoom pill group, right next to it.
|
|
376
|
-
_jsx(ZoomControls, { trailing: !hideHelpButton ? _jsx(HelpButton, {}) : undefined })) : !hideHelpButton ? (_jsx(HelpButton, {})) : null })), mobile ? (libraryOpen ? (_jsx(BottomSheet, { snapPoints: [0, 60, 92], defaultValue: 60, style: { pointerEvents: "auto" }, onChange: (vh) => {
|
|
458
|
+
_jsx(ZoomControls, { trailing: !hideHelpButton ? _jsx(HelpButton, {}) : undefined })) : !hideHelpButton ? (_jsx(HelpButton, {})) : null })), zen ? null : mobile ? (libraryOpen ? (_jsx(BottomSheet, { snapPoints: [0, 60, 92], defaultValue: 60, style: { pointerEvents: "auto" }, onChange: (vh) => {
|
|
377
459
|
if (vh <= 0)
|
|
378
460
|
setLibraryOpen(false);
|
|
379
461
|
}, children: _jsx(LibraryPanel, { open: true, sheet: true, onClose: () => {
|
|
380
462
|
setLibraryOpen(false);
|
|
381
463
|
}, ...(onImportTemplates ? { onImport: onImportTemplates } : {}) }) })) : null) : (_jsx(LibraryPanel, { open: libraryOpen, side: "left", style: { left: 0 }, onClose: () => {
|
|
382
464
|
setLibraryOpen(false);
|
|
383
|
-
}, ...(onImportTemplates ? { onImport: onImportTemplates } : {}) }))] }), !
|
|
465
|
+
}, ...(onImportTemplates ? { onImport: onImportTemplates } : {}) }))] }), minimap && !zen && (_jsx("div", { style: {
|
|
466
|
+
position: "absolute",
|
|
467
|
+
right: "var(--du-bar-inset, 12px)",
|
|
468
|
+
bottom: "calc(var(--du-bar-inset, 12px) + 52px)",
|
|
469
|
+
border: "1px solid var(--du-border, #d0d0d0)",
|
|
470
|
+
borderRadius: 6,
|
|
471
|
+
background: "var(--du-surface, #fff)",
|
|
472
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
|
|
473
|
+
overflow: "hidden",
|
|
474
|
+
zIndex: 40,
|
|
475
|
+
}, children: _jsx(Minimap, {}) })), !hideDrawingPanel && !zen && (_jsx("div", { style: {
|
|
476
|
+
position: "absolute",
|
|
477
|
+
top: "calc(var(--du-bar-inset, 12px) + 52px)",
|
|
478
|
+
right: "var(--du-bar-inset, 12px)",
|
|
479
|
+
zIndex: 40,
|
|
480
|
+
}, children: _jsx(DrawingPanel, {}) })), !hideSelectionPanel && !zen && _jsx(SelectionFloatingPanel, {}), _jsx(HelpDialog, { open: helpOpen, onClose: () => {
|
|
384
481
|
setHelpOpen(false);
|
|
385
|
-
} }), _jsx(CommandPalette, {}), void editor] }));
|
|
482
|
+
} }), _jsx(CommandPalette, {}), _jsx(SearchOverlay, {}), _jsx(StatsPanel, {}), void editor] }));
|
|
386
483
|
};
|
|
387
484
|
/**
|
|
388
485
|
* Platform-correct hotkey labels for the zoom-control tooltips — ⌘
|
|
389
486
|
* glyphs on macOS, "Ctrl+…" elsewhere, mirroring the top toolbar's
|
|
390
|
-
* tool tooltips.
|
|
391
|
-
*
|
|
487
|
+
* tool tooltips. Mirror the bound zoom hotkeys (display uses the
|
|
488
|
+
* minus/plus glyphs).
|
|
392
489
|
*/
|
|
393
490
|
const ZOOM_OUT_HOTKEY = formatHotkey({ meta: true, key: "−" });
|
|
394
491
|
const ZOOM_IN_HOTKEY = formatHotkey({ meta: true, key: "+" });
|
|
@@ -431,122 +528,25 @@ const ZoomControls = ({ trailing }) => {
|
|
|
431
528
|
editor.zoomToFit();
|
|
432
529
|
}, children: _jsx(Maximize, { ...buttonIcon }) }), trailing] }));
|
|
433
530
|
};
|
|
434
|
-
// --- MainMenu File-group helpers --------------------------------------------
|
|
435
|
-
/**
|
|
436
|
-
* Trigger a browser download of arbitrary bytes. Used by the
|
|
437
|
-
* Save / Export menu items. Creates a temporary `<a>`, clicks it,
|
|
438
|
-
* cleans up the object URL on the next animation frame so the
|
|
439
|
-
* browser has time to start the download.
|
|
440
|
-
*/
|
|
441
|
-
const downloadBlob = (blob, filename) => {
|
|
442
|
-
const url = URL.createObjectURL(blob);
|
|
443
|
-
const a = document.createElement("a");
|
|
444
|
-
a.href = url;
|
|
445
|
-
a.download = filename;
|
|
446
|
-
document.body.appendChild(a);
|
|
447
|
-
a.click();
|
|
448
|
-
document.body.removeChild(a);
|
|
449
|
-
requestAnimationFrame(() => {
|
|
450
|
-
URL.revokeObjectURL(url);
|
|
451
|
-
});
|
|
452
|
-
};
|
|
453
|
-
/** "Save as JSON" — serialises the scene through @serialization. */
|
|
454
|
-
const downloadScene = (scene) => {
|
|
455
|
-
const json = stringifyScene(scene, 2);
|
|
456
|
-
downloadBlob(new Blob([json], { type: "application/json" }), "scene.diagram.json");
|
|
457
|
-
};
|
|
458
|
-
/**
|
|
459
|
-
* "Open…" — file picker that accepts `.diagram.json`, parses it,
|
|
460
|
-
* and replaces the editor's scene. Resets history (matches the
|
|
461
|
-
* default `loadScene` behaviour). User cancellation = no-op.
|
|
462
|
-
*/
|
|
463
|
-
const openSceneFile = (editor, notify) => {
|
|
464
|
-
if (!editor)
|
|
465
|
-
return;
|
|
466
|
-
const input = document.createElement("input");
|
|
467
|
-
input.type = "file";
|
|
468
|
-
input.accept = "application/json,.json";
|
|
469
|
-
input.onchange = () => {
|
|
470
|
-
const file = input.files?.[0];
|
|
471
|
-
if (!file)
|
|
472
|
-
return;
|
|
473
|
-
void file.text().then((text) => {
|
|
474
|
-
try {
|
|
475
|
-
const scene = parseScene(text);
|
|
476
|
-
editor.loadScene(scene);
|
|
477
|
-
}
|
|
478
|
-
catch (err) {
|
|
479
|
-
console.error("[diagram] failed to parse scene file:", err);
|
|
480
|
-
notify("Failed to parse the file — make sure it was saved through this app's Save action.");
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
};
|
|
484
|
-
input.click();
|
|
485
|
-
};
|
|
486
|
-
/**
|
|
487
|
-
* "Export as PNG" — renders the **full scene** (not just the visible
|
|
488
|
-
* viewport) into an OffscreenCanvas via the standard `renderScene` +
|
|
489
|
-
* `renderLinks` pipeline and downloads the result. Three variants
|
|
490
|
-
* exposed in the menu:
|
|
491
|
-
*
|
|
492
|
-
* • transparent — PNG with alpha channel preserved
|
|
493
|
-
* • color — solid background fill (host canvas colour)
|
|
494
|
-
* • color-and-grid — solid fill + same grid the user sees
|
|
495
|
-
*
|
|
496
|
-
* Scale fixed at 2× for retina-quality output (matches the standard /
|
|
497
|
-
* standard default). The full-scene contract makes this symmetric with
|
|
498
|
-
* SVG export, which always emits the whole scene.
|
|
499
|
-
*
|
|
500
|
-
* Implementation lives in `./png-export.ts` so this file stays
|
|
501
|
-
* focused on UI wiring.
|
|
502
|
-
*/
|
|
503
|
-
const PNG_EXPORT_SCALE = 2;
|
|
504
|
-
const downloadPng = async (editor, background, notify) => {
|
|
505
|
-
const backgroundColor = readCanvasBackgroundColor();
|
|
506
|
-
const blob = await exportSceneToPng(editor.scene, {
|
|
507
|
-
background,
|
|
508
|
-
scale: PNG_EXPORT_SCALE,
|
|
509
|
-
backgroundColor,
|
|
510
|
-
});
|
|
511
|
-
if (!blob) {
|
|
512
|
-
// Empty scene — convertToBlob unavailable or no shapes to export.
|
|
513
|
-
notify("Nothing to export — the canvas is empty.");
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
downloadBlob(blob, "scene.png");
|
|
517
|
-
};
|
|
518
|
-
/**
|
|
519
|
-
* Read the host's current `--du-canvas-bg` CSS variable. Falls back
|
|
520
|
-
* to white if the variable isn't set (e.g. host hasn't loaded the
|
|
521
|
-
* react-ui stylesheet). Matches what the user sees behind the
|
|
522
|
-
* shapes on the live canvas.
|
|
523
|
-
*/
|
|
524
|
-
const readCanvasBackgroundColor = () => {
|
|
525
|
-
const probe = document.querySelector('canvas[data-layer="main"]') ?? document.body;
|
|
526
|
-
const value = getComputedStyle(probe).getPropertyValue("--du-canvas-bg").trim();
|
|
527
|
-
return value || "#ffffff";
|
|
528
|
-
};
|
|
529
|
-
/**
|
|
530
|
-
* "Export as SVG" — uses `@renderer-svg.renderSceneToSvg` so the
|
|
531
|
-
* output is identical to the headless render path (vector, no
|
|
532
|
-
* bitmap fall-back, works in any browser). One file per scene.
|
|
533
|
-
*/
|
|
534
|
-
const downloadSvg = (scene) => {
|
|
535
|
-
const svg = renderSceneToSvg(scene);
|
|
536
|
-
downloadBlob(new Blob([svg], { type: "image/svg+xml" }), "scene.svg");
|
|
537
|
-
};
|
|
538
531
|
// --- Grid toggle helpers ----------------------------------------------------
|
|
539
|
-
|
|
532
|
+
/** Translate the `grid` / `snap` props into a partial settings override. */
|
|
533
|
+
const buildHostSettings = (gridEnabled, gridStyle, snap) => ({
|
|
534
|
+
viewport: {
|
|
535
|
+
...(gridEnabled !== undefined ? { gridEnabled } : {}),
|
|
536
|
+
...(gridStyle !== undefined ? { gridStyle } : {}),
|
|
537
|
+
...(snap !== undefined ? { snapToGrid: snap } : {}),
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
540
|
/**
|
|
541
|
-
* Map the current viewport state to the segmented Grid toggle's
|
|
542
|
-
*
|
|
543
|
-
*
|
|
541
|
+
* Map the current viewport state to the segmented Grid toggle's value.
|
|
542
|
+
* `"off"` when the grid is disabled; otherwise the stored gridStyle
|
|
543
|
+
* (default `"lines"`).
|
|
544
544
|
*/
|
|
545
545
|
const gridSelection = (editor) => {
|
|
546
546
|
if (!editor)
|
|
547
547
|
return "lines";
|
|
548
548
|
const vp = editor.scene.viewport;
|
|
549
|
-
if (!vp.
|
|
549
|
+
if (!vp.gridEnabled)
|
|
550
550
|
return "off";
|
|
551
551
|
return vp.gridStyle ?? "lines";
|
|
552
552
|
};
|
|
@@ -556,21 +556,14 @@ const gridSelection = (editor) => {
|
|
|
556
556
|
* visibility).
|
|
557
557
|
*/
|
|
558
558
|
const snapSelection = (editor) => (editor?.snapToGridEnabled ?? true) ? "on" : "off";
|
|
559
|
-
/**
|
|
560
|
-
* Inverse — translate the toggle's value back into a `setGrid`
|
|
561
|
-
* call. Switching from "off" to lines/dots restores the default
|
|
562
|
-
* grid size so the user doesn't have to also re-enter it
|
|
563
|
-
* separately.
|
|
564
|
-
*/
|
|
559
|
+
/** Inverse — translate the toggle's value back into a `setGrid` call. */
|
|
565
560
|
const applyGridSelection = (editor, next) => {
|
|
566
561
|
if (!editor)
|
|
567
562
|
return;
|
|
568
563
|
if (next === "off") {
|
|
569
|
-
editor.setGrid({
|
|
564
|
+
editor.setGrid({ enabled: false });
|
|
570
565
|
return;
|
|
571
566
|
}
|
|
572
|
-
|
|
573
|
-
const size = vp.gridSize && vp.gridSize > 0 ? vp.gridSize : DEFAULT_GRID_SIZE;
|
|
574
|
-
editor.setGrid({ size, style: next });
|
|
567
|
+
editor.setGrid({ enabled: true, style: next });
|
|
575
568
|
};
|
|
576
569
|
//# sourceMappingURL=Diagram.js.map
|