@oh-just-another/editor 0.61.1 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Diagram.d.ts CHANGED
@@ -87,18 +87,32 @@ export interface DiagramProps {
87
87
  */
88
88
  readonly hideDrawingPanel?: boolean;
89
89
  /**
90
- * Show the built-in minimap: a scene overview + viewport rect, docked
90
+ * Show the built-in minimap at startup: a scene overview + viewport rect, docked
91
91
  * bottom-right above the zoom controls. Click / drag it to pan. Hidden in
92
92
  * zen mode along with the rest of the chrome. Off by default.
93
93
  */
94
94
  readonly minimap?: boolean;
95
+ /**
96
+ * Brand cell at the start of the top bar. Defaults to the built-in
97
+ * `BrandLogo` (light / dark artwork from `assets/logo*.svg`); pass a node
98
+ * to replace it, or `null` to drop the cell.
99
+ */
100
+ readonly logo?: ReactNode;
95
101
  readonly renderTopBarLeft?: () => ReactNode;
96
102
  readonly renderTopBarCenter?: () => ReactNode;
97
103
  readonly renderTopBarRight?: () => ReactNode;
98
104
  readonly renderBottomBarLeft?: () => ReactNode;
99
105
  readonly renderBottomBarCenter?: () => ReactNode;
100
106
  readonly renderBottomBarRight?: () => ReactNode;
107
+ /** Extra rows appended to the main menu's top level (after a separator). */
101
108
  readonly renderMainMenuExtras?: () => ReactNode;
109
+ /** Extra rows inside the main menu's Board › submenu, right after Export (e.g. import / export formats). */
110
+ readonly renderBoardMenuExtras?: () => ReactNode;
111
+ /**
112
+ * Extra rows inside Board › Export, after the built-in PNG / SVG entries —
113
+ * for host export formats (JSON, Mermaid, …). Render `MainMenu.Item`s.
114
+ */
115
+ readonly renderExportMenuExtras?: () => ReactNode;
102
116
  /** Called when user clicks the "Import" button in the Library panel. */
103
117
  readonly onImportTemplates?: () => void;
104
118
  /**
@@ -128,6 +142,13 @@ export interface DiagramProps {
128
142
  * on reload).
129
143
  */
130
144
  readonly persistTheme?: boolean | string;
145
+ /**
146
+ * Persist the per-user editor preferences (`EditorPreferences`: object
147
+ * snapping, size readouts, wheel mode — the canvas menu's check rows) in
148
+ * `localStorage`. Pass `true` for the default key `"diagram-preferences"`,
149
+ * or a custom key string. Omit to keep them in memory for the session.
150
+ */
151
+ readonly persistPreferences?: boolean | string;
131
152
  /**
132
153
  * URL for the "GitHub" link in the Help menu. Omit to use the
133
154
  * project's repository; pass your own, or `null` to hide the link
@@ -1 +1 @@
1
- {"version":3,"file":"Diagram.d.ts","sourceRoot":"","sources":["../src/Diagram.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Diagram.d.ts","sourceRoot":"","sources":["../src/Diagram.tsx"],"names":[],"mappings":"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"}
package/dist/Diagram.js CHANGED
@@ -1,23 +1,21 @@
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
- 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, 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";
3
+ import { Ban, Clipboard, Command, Copy, Delete, Download, Expand, Eye, FileDown, FileUp, Grid3x3, Grip, ImageDown, Keyboard, LayoutDashboard, Magnet, Map as MapIcon, Maximize, Minus, Monitor, Moon, Mouse, MousePointer, Pencil, Plus, Redo2, RotateCcw, Ruler, Scissors, Search, Shrink, SlidersHorizontal, Sun, Undo2, ZoomIn, } from "lucide-react";
4
+ import { BottomBar, BottomSheet, ButtonGroup, ContextMenu, DEFAULT_CONTEXT_MENU, DEFAULT_VERTICAL_TOOLBAR, DiagramRoot, CommandPalette, DiagramSurface, HelpButton, HelpDialog, IconButton, DrawingPanel, LibraryPanel, MainMenu, Minimap, ResetToContentButton, LinkHoverPopup, LinkBadges, StickyReactions, LinkDropShapeMenu, LinkCaptionEditor, SelectionFloatingPanel, SearchOverlay, StatsPanel, TextEditorOverlay, FrameNameEditorOverlay, PortalContainerProvider, ToastHost, Toolbar, TooltipProvider, TopBar, UILayer, ZenModeProvider, useDiagramOptional, useHelpDialogHotkey, useMobileLayout, usePalettePlacement, useEditorSelector, useFullscreen, useZenMode, CONTROL_ICON, ROW_ICON, FileDropOverlay, } from "@oh-just-another/react-ui";
5
+ import { BrandLogo } from "./brand/brand-logo.js";
6
+ import { diagramFileDropHandler } from "@oh-just-another/importers";
5
7
  /**
6
8
  * Lucide icon sizing — `MENU_ICON_SIZE` is for in-row icons of
7
- * `MainMenu.Item`, `TOGGLE_ICON_SIZE` is for the segmented Theme /
8
- * Grid toggles, `BUTTON_ICON_SIZE` is for `IconButton` slot
9
+ * `MainMenu.Item`, `BUTTON_ICON_SIZE` is for `IconButton` slot
9
10
  * children (library, zoom, fit). All share `BUTTON_ICON_STROKE`
10
11
  * for visual consistency with the toolbar.
11
12
  */
12
- const MENU_ICON_SIZE = 14;
13
- const TOGGLE_ICON_SIZE = 14;
14
- const BUTTON_ICON_SIZE = 16;
15
- const BUTTON_ICON_STROKE = 1.75;
16
- const menuIcon = { size: MENU_ICON_SIZE, strokeWidth: BUTTON_ICON_STROKE };
17
- const toggleIcon = { size: TOGGLE_ICON_SIZE, strokeWidth: BUTTON_ICON_STROKE };
18
- const buttonIcon = { size: BUTTON_ICON_SIZE, strokeWidth: BUTTON_ICON_STROKE };
13
+ const menuIcon = ROW_ICON;
14
+ const buttonIcon = CONTROL_ICON;
19
15
  /** Default target for the Help-menu "GitHub" link (overridable / hideable via the `repositoryUrl` prop). */
20
16
  const DEFAULT_REPOSITORY_URL = "https://github.com/oh-just-another/diagram";
17
+ import { defaultActionRegistry } from "@oh-just-another/state";
18
+ import { DEFAULT_PREFERENCES_STORAGE_KEY, bindPreferencesPersistence, } from "./preferences-storage.js";
21
19
  import { formatHotkey } from "@oh-just-another/state";
22
20
  import { hydrateScene, isText, } from "@oh-just-another/scene";
23
21
  import { WasmTextShaper } from "@oh-just-another/text-wasm";
@@ -33,7 +31,7 @@ import { useThemedPortalContainer } from "./themed-portal-container.js";
33
31
  import { downloadScene, downloadSvg, downloadPng, openSceneFile, copySceneAsImage, registerFileActions, setFileActionNotifier, } from "./file-actions.js";
34
32
  import { isEditableTarget } from "./dom-focus";
35
33
  export const Diagram = forwardRef(function Diagram(props, ref) {
36
- const { initialScene, initialTool = "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;
34
+ const { initialScene, initialTool = "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, logo = _jsx(BrandLogo, {}), renderTopBarLeft, renderTopBarCenter, renderTopBarRight, renderBottomBarLeft, renderBottomBarCenter, renderBottomBarRight, renderMainMenuExtras, renderBoardMenuExtras, renderExportMenuExtras, onImportTemplates, theme: themeProp, defaultTheme = "system", onThemeChange, persistTheme, persistPreferences, repositoryUrl, onConfirm, onNotify, className, style, } = props;
37
35
  // Theme: controlled when `themeProp` is provided, otherwise
38
36
  // self-managed via `internalTheme`. The lazy `useState` initializer
39
37
  // reads from `localStorage` once when `persistTheme` is on.
@@ -173,6 +171,9 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
173
171
  // --- Editor wiring ---
174
172
  const [editor, setEditor] = useState(null);
175
173
  const handleReady = useCallback((e) => {
174
+ // Built-in diagram import (native JSON, Excalidraw, Mermaid, …) first,
175
+ // then the host's handlers.
176
+ e.registerFileDropHandler(diagramFileDropHandler);
176
177
  if (fileDropHandlers) {
177
178
  for (const handler of fileDropHandlers)
178
179
  e.registerFileDropHandler(handler);
@@ -180,6 +181,30 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
180
181
  setEditor(e);
181
182
  onReady?.(e);
182
183
  }, [fileDropHandlers, onReady]);
184
+ // Minimap visibility: the `minimap` prop seeds it; the zoom menu's
185
+ // "Hide / Show minimap" row and the `M` key toggle it at runtime.
186
+ const [minimapVisible, setMinimapVisible] = useState(minimap === true);
187
+ useEffect(() => {
188
+ setMinimapVisible(minimap === true);
189
+ }, [minimap]);
190
+ const toggleMinimap = useCallback(() => {
191
+ setMinimapVisible((v) => !v);
192
+ }, []);
193
+ // Fullscreen target: the editor root (chrome + canvas), see the zoom menu.
194
+ const rootRef = useRef(null);
195
+ // Per-user preferences: load once the editor exists, then mirror changes.
196
+ const preferencesKey = useMemo(() => {
197
+ if (persistPreferences === true)
198
+ return DEFAULT_PREFERENCES_STORAGE_KEY;
199
+ if (typeof persistPreferences === "string")
200
+ return persistPreferences;
201
+ return null;
202
+ }, [persistPreferences]);
203
+ useEffect(() => {
204
+ if (!editor || !preferencesKey)
205
+ return undefined;
206
+ return bindPreferencesPersistence(editor, preferencesKey);
207
+ }, [editor, preferencesKey]);
183
208
  useEffect(() => {
184
209
  if (!editor)
185
210
  return undefined;
@@ -249,6 +274,11 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
249
274
  ed.cancelFlowchart();
250
275
  e.preventDefault();
251
276
  }
277
+ // `M` — toggle the minimap (zoom menu parity).
278
+ if (e.key.toLowerCase() === "m" && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
279
+ toggleMinimap();
280
+ e.preventDefault();
281
+ }
252
282
  };
253
283
  const onKeyUp = (e) => {
254
284
  sync(e);
@@ -275,7 +305,7 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
275
305
  ed.setSnapSuppressed(false);
276
306
  ed.setTransformModifiers({ alt: false, shift: false });
277
307
  };
278
- }, [editor]);
308
+ }, [editor, toggleMinimap]);
279
309
  useImperativeHandle(ref, () => ({
280
310
  editor,
281
311
  capabilities: profile,
@@ -309,7 +339,7 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
309
339
  if (profile.renderer === "webgl2" && profile.wasmText && !wasmTextSettled && sceneHasText) {
310
340
  return _jsx("div", { className: className, style: style });
311
341
  }
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: {
342
+ return (_jsx(PortalContainerProvider, { container: portalContainer, children: _jsx(ToastHost, { children: _jsx(TooltipProvider, { children: _jsx("div", { ref: rootRef, className: className, "data-diagram-root": true, ...(theme === "system" ? {} : { "data-theme": theme }), style: {
313
343
  position: "relative",
314
344
  width: "100%",
315
345
  height: "100%",
@@ -317,7 +347,7 @@ export const Diagram = forwardRef(function Diagram(props, ref) {
317
347
  ...style,
318
348
  }, children: _jsx(DiagramRoot, { initialScene: seed, initialTool: initialTool, onReady: handleReady, renderer: profile.renderer, ...(profile.renderer === "offscreen"
319
349
  ? { 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 }) }) }) }) }) }) }));
350
+ : {}), ...(wasmShaper ? { textShaper: wasmShaper } : {}), ...(wasmRaster ? { rasterizer: wasmRaster } : {}), children: _jsx(ZenModeProvider, { children: _jsx(EditorShell, { hideTopBar: hideTopBar, hideBottomBar: hideBottomBar, hideToolbar: hideToolbar, hideLibraryButton: hideLibraryButton, hideMainMenu: hideMainMenu, logo: logo, hideZoomControls: hideZoomControls, hideResetToContent: hideResetToContent, hideHelpButton: hideHelpButton, hideContextMenu: hideContextMenu, hideSelectionPanel: hideSelectionPanel, hideDrawingPanel: hideDrawingPanel, minimapVisible: minimapVisible, onToggleMinimap: toggleMinimap, rootRef: rootRef, renderTopBarLeft: renderTopBarLeft, renderTopBarCenter: renderTopBarCenter, renderTopBarRight: renderTopBarRight, renderBottomBarLeft: renderBottomBarLeft, renderBottomBarCenter: renderBottomBarCenter, renderBottomBarRight: renderBottomBarRight, renderMainMenuExtras: renderMainMenuExtras, renderBoardMenuExtras: renderBoardMenuExtras, renderExportMenuExtras: renderExportMenuExtras, onImportTemplates: onImportTemplates, repositoryUrl: repositoryUrl, onConfirm: onConfirm, onNotify: onNotify, theme: theme, changeTheme: changeTheme }) }) }) }) }) }) }));
321
351
  });
322
352
  // Match the primary public name (`<Editor>`) in DevTools / stack traces,
323
353
  // even though the internal forwardRef function is named `Diagram`.
@@ -328,11 +358,12 @@ Diagram.displayName = "Editor";
328
358
  * `useHelpDialogHotkey`) resolve correctly. Composes the
329
359
  * canvas-surface + ui-layer overlay + side panels into one tree.
330
360
  */
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, }) => {
361
+ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton, hideMainMenu, logo, hideZoomControls, hideResetToContent, hideHelpButton, hideContextMenu, hideSelectionPanel, hideDrawingPanel, minimapVisible, onToggleMinimap, rootRef, renderTopBarLeft, renderTopBarCenter, renderTopBarRight, renderBottomBarLeft, renderBottomBarCenter, renderBottomBarRight, renderMainMenuExtras, renderBoardMenuExtras, renderExportMenuExtras, onImportTemplates, repositoryUrl, onConfirm, onNotify, theme, changeTheme, }) => {
332
362
  const editor = useDiagramOptional();
333
363
  // Zen mode (⌥Z): hide every chrome surface for focused work, leaving the
334
364
  // canvas + the observational overlays (search, stats, command palette).
335
365
  const { zen } = useZenMode();
366
+ const fullscreen = useFullscreen(rootRef);
336
367
  // Omitted → project repo; explicit string → that URL; null → no link.
337
368
  const repositoryHref = repositoryUrl === undefined ? DEFAULT_REPOSITORY_URL : repositoryUrl;
338
369
  // Native dialogs by default; hosts can route through their own UI.
@@ -349,11 +380,20 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
349
380
  registerFileActions();
350
381
  setFileActionNotifier(notify);
351
382
  }, [notify]);
352
- // Subscribe to scene changes so the Grid toggle in MainMenu reads
353
- // the latest viewport.gridEnabled / gridStyle. `useScene` is a thin
354
- // selector hook — re-renders only on scene identity flips.
355
- void useScene();
356
- const paletteDropHandlers = usePalettePlacement();
383
+ // Subscribe ONLY to the Grid / Snap toggle VALUES, not the scene
384
+ // identity: the scene reference flips on every frame of a drag, and a
385
+ // whole-shell re-render (menus, toolbars, HelpDialog) per frame makes
386
+ // moving elements visibly sluggish. The selectors return primitives,
387
+ // so `Object.is` skips re-renders until a toggle actually changes.
388
+ useEditorSelector((e) => gridSelection(e), "lines", "scene");
389
+ useEditorSelector((e) => snapSelection(e), "on");
390
+ // Preference switches in the View / Preferences submenus.
391
+ const showObjectSize = useEditorSelector((e) => e.preferences.showObjectSize, true);
392
+ const snapObjects = useEditorSelector((e) => e.preferences.snapObjects, true);
393
+ const suggestObjectSize = useEditorSelector((e) => e.preferences.suggestObjectSize, true);
394
+ const wheelMode = useEditorSelector((e) => e.preferences.wheelMode, "auto");
395
+ const [fileDragging, setFileDragging] = useState(false);
396
+ const paletteDropHandlers = usePalettePlacement({ onFileDrag: setFileDragging });
357
397
  // Touch / narrow screens: the library opens as a bottom sheet instead of
358
398
  // a left overlay (which would cover the whole small canvas).
359
399
  const mobile = useMobileLayout();
@@ -361,6 +401,13 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
361
401
  // toggle and closed via its ✕. Starts closed; no dock / pin.
362
402
  const [libraryOpen, setLibraryOpen] = useState(false);
363
403
  const [helpOpen, setHelpOpen] = useState(false);
404
+ // Export content switches (sticky reactions / tags / author) — session
405
+ // state seeded by EXPORT_CONTENT_DEFAULTS inside the export helpers;
406
+ // the Export… submenu checkboxes flip them per run.
407
+ const [exportContent, setExportContent] = useState({});
408
+ const toggleExportContent = (key) => {
409
+ setExportContent((cur) => ({ ...cur, [key]: !(cur?.[key] ?? true) }));
410
+ };
364
411
  useHelpDialogHotkey(() => {
365
412
  setHelpOpen((v) => !v);
366
413
  });
@@ -369,51 +416,37 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
369
416
  // The library overlays the canvas (no reflow); the toolbar floats just
370
417
  // to its right when open, else near the edge. On mobile the library is a
371
418
  // bottom sheet, so the toolbar never shifts.
372
- const LIBRARY_PANEL_WIDTH = 240;
373
- const BAR_INSET = 12;
374
- const toolbarLeft = !mobile && libraryOpen ? LIBRARY_PANEL_WIDTH + BAR_INSET : BAR_INSET;
419
+ // Dock inset from the edge, or past the open library (its inset + width)
420
+ // with a flyout gap — all CSS tokens so hosts retune them in one place.
421
+ const toolbarLeft = !mobile && libraryOpen
422
+ ? "var(--du-bar-inset) + var(--du-side-panel-w) + var(--du-flyout-gap)"
423
+ : "var(--du-dock-inset)";
375
424
  // Items for the vertical creation dock: an optional templates-library
376
425
  // toggle on top (hidden with `hideLibraryButton`), then the standard
377
426
  // creation tools.
378
- const toolbarItems = useMemo(() => hideLibraryButton
379
- ? [...DEFAULT_VERTICAL_TOOLBAR]
380
- : [
381
- {
382
- kind: "action",
383
- id: "toggle-library",
384
- label: _jsx(LibraryIcon, { ...buttonIcon }),
385
- title: "Templates library",
386
- active: libraryOpen,
387
- onClick: () => {
388
- setLibraryOpen((v) => !v);
389
- },
427
+ // The template library's only toolbar entry point is the "More shapes"
428
+ // row inside the Shapes and lines flyout (`hideLibraryButton` removes
429
+ // it). No standalone library toggle in the dock.
430
+ const toolbarItems = useMemo(() => DEFAULT_VERTICAL_TOOLBAR.map((item) => item.kind === "shapes-flyout" && !hideLibraryButton
431
+ ? {
432
+ ...item,
433
+ onMoreShapes: () => {
434
+ setLibraryOpen(true);
390
435
  },
391
- { kind: "divider" },
392
- ...DEFAULT_VERTICAL_TOOLBAR,
393
- ], [hideLibraryButton, libraryOpen]);
394
- return (_jsxs("div", { style: { position: "absolute", inset: 0 }, onDragEnter: paletteDropHandlers.onDragEnter, onDragOver: paletteDropHandlers.onDragOver, onDragLeave: paletteDropHandlers.onDragLeave, onDrop: paletteDropHandlers.onDrop, children: [_jsxs("div", { style: {
436
+ }
437
+ : item), [hideLibraryButton]);
438
+ return (_jsxs("div", { style: { position: "absolute", inset: 0 }, onDragEnter: paletteDropHandlers.onDragEnter, onDragOver: paletteDropHandlers.onDragOver, onDragLeave: paletteDropHandlers.onDragLeave, onDrop: paletteDropHandlers.onDrop, children: [_jsx(FileDropOverlay, { active: fileDragging }), _jsxs("div", { style: {
395
439
  position: "absolute",
396
440
  top: 0,
397
441
  bottom: 0,
398
442
  left: 0,
399
443
  right: 0,
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: {
401
- position: "absolute",
402
- // Vertically centred on the left; floats just to the right of
403
- // the library when it's open (else flush near the edge).
404
- // `env(safe-area-inset-left)` is 0 on desktop, clears the notch
405
- // in mobile landscape.
406
- top: "50%",
407
- left: `calc(env(safe-area-inset-left, 0px) + ${toolbarLeft}px)`,
408
- transform: "translateY(-50%)",
409
- zIndex: 60,
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: {
411
- minWidth: 36,
412
- padding: "0 10px",
413
- cursor: "default",
414
- fontWeight: 600,
415
- letterSpacing: 0.5,
416
- }, children: "\u2317" }), !hideMainMenu && (_jsxs(MainMenu, { children: [_jsxs(MainMenu.Group, { title: "File", children: [_jsx(MainMenu.Item, { icon: _jsx(FileUp, { ...menuIcon }), onClick: () => {
444
+ }, children: [_jsx(DiagramSurface, { style: { position: "absolute", inset: 0 } }), _jsx(TextEditorOverlay, {}), _jsx(FrameNameEditorOverlay, {}), _jsx(LinkHoverPopup, {}), _jsx(LinkBadges, {}), _jsx(StickyReactions, {}), _jsx(LinkDropShapeMenu, {}), _jsx(LinkCaptionEditor, {}), !hideContextMenu && _jsx(ContextMenu, { items: DEFAULT_CONTEXT_MENU })] }), !hideToolbar && !zen ? (_jsx("div", { className: "du-dock", style: {
445
+ // Floats just to the right of the library when it's open (else
446
+ // near the edge). `env(safe-area-inset-left)` is 0 on desktop,
447
+ // clears the notch in mobile landscape.
448
+ left: `calc(env(safe-area-inset-left, 0px) + ${toolbarLeft})`,
449
+ }, children: _jsx(Toolbar, { orientation: "vertical", items: toolbarItems }) })) : null, _jsxs(UILayer, { children: [!hideTopBar && !zen && (_jsx(TopBar, { left: _jsxs(ButtonGroup, { ariaLabel: "Logo and main menu", children: [logo !== null && _jsx("span", { className: "du-icon-button du-brand", children: logo }), !hideMainMenu && (_jsxs(MainMenu, { children: [_jsxs(MainMenu.Submenu, { icon: _jsx(LayoutDashboard, { ...menuIcon }), label: "Board", children: [_jsx(MainMenu.Item, { icon: _jsx(FileUp, { ...menuIcon }), onClick: () => {
417
450
  if (editor)
418
451
  openSceneFile(editor);
419
452
  }, shortcut: formatHotkey({ key: "O", meta: true }), children: "Open\u2026" }), _jsx(MainMenu.Item, { icon: _jsx(FileDown, { ...menuIcon }), onClick: () => {
@@ -422,10 +455,16 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
422
455
  }, disabled: !editor, shortcut: formatHotkey({ key: "S", meta: true }), children: "Save as JSON" }), _jsx(MainMenu.Item, { icon: _jsx(Copy, { ...menuIcon }), onClick: () => {
423
456
  if (editor)
424
457
  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: () => {
458
+ }, disabled: !editor, shortcut: formatHotkey({ key: "C", shift: true, alt: true }), children: "Copy as image" }), _jsxs(MainMenu.Submenu, { icon: _jsx(Download, { ...menuIcon }), label: "Export", disabled: !editor, children: [_jsx(MainMenu.Item, { icon: _jsx(ImageDown, { ...menuIcon }), onClick: () => editor && void downloadPng(editor, "transparent", exportContent), disabled: !editor, children: "PNG (transparent)" }), _jsx(MainMenu.Item, { icon: _jsx(ImageDown, { ...menuIcon }), onClick: () => editor && void downloadPng(editor, "color", exportContent), 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", exportContent), disabled: !editor, children: "PNG (with background + grid)" }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(Download, { ...menuIcon }), onClick: () => {
426
459
  if (editor)
427
- downloadSvg(editor.scene);
428
- }, disabled: !editor, children: "SVG" })] }), _jsx(MainMenu.Item, { icon: _jsx(RotateCcw, { ...menuIcon }), onClick: () => {
460
+ downloadSvg(editor.scene, exportContent);
461
+ }, disabled: !editor, children: "SVG" }), renderExportMenuExtras?.(), _jsx(MainMenu.Separator, {}), _jsxs(MainMenu.Group, { title: "Include in export", children: [_jsx(MainMenu.Item, { keepOpen: true, onClick: () => {
462
+ toggleExportContent("stickyReactions");
463
+ }, checked: exportContent?.stickyReactions !== false, children: "Sticky reactions" }), _jsx(MainMenu.Item, { keepOpen: true, onClick: () => {
464
+ toggleExportContent("stickyTags");
465
+ }, checked: exportContent?.stickyTags !== false, children: "Sticky tags" }), _jsx(MainMenu.Item, { keepOpen: true, onClick: () => {
466
+ toggleExportContent("stickyAuthor");
467
+ }, checked: exportContent?.stickyAuthor !== false, children: "Sticky author" })] })] }), renderBoardMenuExtras?.(), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(MapIcon, { ...menuIcon }), onClick: () => editor?.goToStartView(), disabled: (editor?.startView ?? null) === null, children: "Start view" }), _jsx(MainMenu.Item, { icon: _jsx(MapIcon, { ...menuIcon }), onClick: () => editor?.setCurrentViewAsStart(), disabled: !editor, children: "Set current view as start" }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(RotateCcw, { ...menuIcon }), onClick: () => {
429
468
  if (!editor)
430
469
  return;
431
470
  if (confirmDialog("Reset canvas? This clears all shapes.")) {
@@ -436,48 +475,29 @@ const EditorShell = ({ hideTopBar, hideBottomBar, hideToolbar, hideLibraryButton
436
475
  // DEFAULT_VIEWPORT has it disabled.
437
476
  editor.clear();
438
477
  }
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: [
440
- { value: "light", label: "Light", icon: _jsx(Sun, { ...toggleIcon }) },
441
- { value: "dark", label: "Dark", icon: _jsx(Moon, { ...toggleIcon }) },
442
- { value: "system", label: "System", icon: _jsx(Monitor, { ...toggleIcon }) },
443
- ] }) }), _jsx(MainMenu.Group, { title: "Grid", children: _jsx(MainMenu.Toggle, { value: gridSelection(editor), onChange: (next) => {
444
- applyGridSelection(editor, next);
445
- }, options: [
446
- { value: "lines", label: "Lines", icon: _jsx(Grid3x3, { ...toggleIcon }) },
447
- { value: "dots", label: "Dots", icon: _jsx(Grip, { ...toggleIcon }) },
448
- { value: "off", label: "Off", icon: _jsx(Minus, { ...toggleIcon }) },
449
- ] }) }), _jsx(MainMenu.Group, { title: "Snap to grid", children: _jsx(MainMenu.Toggle, { value: snapSelection(editor), onChange: (next) => {
450
- editor?.setSnapToGrid(next === "on");
451
- }, options: [
452
- { value: "on", label: "On", icon: _jsx(Magnet, { ...toggleIcon }) },
453
- { value: "off", label: "Off", icon: _jsx(Minus, { ...toggleIcon }) },
454
- ] }) }), _jsx(MainMenu.Separator, {}), _jsxs(MainMenu.Group, { title: "Help", children: [_jsx(MainMenu.Item, { icon: _jsx(HelpCircle, { ...menuIcon }), shortcut: "?", onClick: () => {
455
- setHelpOpen(true);
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 ? (
478
+ }, disabled: !editor, children: "Reset canvas" })] }), _jsxs(MainMenu.Submenu, { icon: _jsx(Pencil, { ...menuIcon }), label: "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.Separator, {}), _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.Separator, {}), _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, {}), _jsx(MainMenu.Item, { icon: _jsx(Command, { ...menuIcon }), shortcut: "\u2318K", onClick: () => {
479
+ if (editor) {
480
+ defaultActionRegistry.dispatch("open-command-palette", { editor });
481
+ }
482
+ }, disabled: !editor, children: "Commands" }), _jsx(MainMenu.Item, { icon: _jsx(Search, { ...menuIcon }), shortcut: "\u2318F", onClick: () => {
483
+ if (editor)
484
+ defaultActionRegistry.dispatch("open-search", { editor });
485
+ }, disabled: !editor, children: "Find" })] }), _jsxs(MainMenu.Submenu, { icon: _jsx(Eye, { ...menuIcon }), label: "View", children: [_jsxs(MainMenu.Submenu, { icon: _jsx(Grid3x3, { ...menuIcon }), label: "Grid", children: [GRID_OPTIONS.map((opt) => (_jsx(MainMenu.Item, { icon: opt.icon, active: gridSelection(editor) === opt.value, onClick: () => {
486
+ applyGridSelection(editor, opt.value);
487
+ }, children: opt.label }, opt.value))), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(Magnet, { ...menuIcon }), keepOpen: true, onClick: () => editor?.setSnapToGrid(!editor.snapToGridEnabled), checked: snapSelection(editor) === "on", children: "Snap to grid" })] }), _jsx(MainMenu.Item, { icon: _jsx(Ruler, { ...menuIcon }), keepOpen: true, onClick: () => editor?.setPreferences({ showObjectSize: !showObjectSize }), checked: showObjectSize, children: "Object dimensions" }), _jsx(MainMenu.Item, { icon: _jsx(MapIcon, { ...menuIcon }), keepOpen: true, onClick: onToggleMinimap, checked: minimapVisible, children: "Minimap" }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Submenu, { icon: _jsx(Sun, { ...menuIcon }), label: "Theme", children: THEME_OPTIONS.map((opt) => (_jsx(MainMenu.Item, { icon: opt.icon, active: theme === opt.value, onClick: () => {
488
+ changeTheme(opt.value);
489
+ }, children: opt.label }, opt.value))) }), fullscreen.supported ? (_jsxs(_Fragment, { children: [_jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: fullscreen.active ? (_jsx(Shrink, { ...menuIcon })) : (_jsx(Expand, { ...menuIcon })), onClick: fullscreen.toggle, children: fullscreen.active ? "Exit full screen" : "Enter full screen" })] })) : null] }), _jsxs(MainMenu.Submenu, { icon: _jsx(SlidersHorizontal, { ...menuIcon }), label: "Preferences", children: [_jsx(MainMenu.Submenu, { icon: _jsx(Mouse, { ...menuIcon }), label: "Mouse or trackpad", children: WHEEL_MODE_OPTIONS.map((opt) => (_jsx(MainMenu.Item, { active: wheelMode === opt.value, onClick: () => editor?.setPreferences({ wheelMode: opt.value }), children: opt.label }, opt.value))) }), _jsx(MainMenu.Item, { icon: _jsx(Magnet, { ...menuIcon }), keepOpen: true, onClick: () => editor?.setPreferences({ snapObjects: !snapObjects }), checked: snapObjects, children: "Snap objects" }), _jsx(MainMenu.Item, { icon: _jsx(Ruler, { ...menuIcon }), keepOpen: true, onClick: () => editor?.setPreferences({ suggestObjectSize: !suggestObjectSize }), checked: suggestObjectSize, children: "Suggest object size" })] }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(Keyboard, { ...menuIcon }), shortcut: "?", onClick: () => {
490
+ setHelpOpen(true);
491
+ }, 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 ? (
457
492
  // Help sits inside the zoom pill group, right next to it.
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) => {
493
+ _jsx(ZoomControls, { trailing: !hideHelpButton ? _jsx(HelpButton, {}) : undefined, fullscreen: fullscreen, minimapVisible: minimapVisible, onToggleMinimap: onToggleMinimap })) : !hideHelpButton ? (_jsx(HelpButton, {})) : null })), zen ? null : mobile ? (libraryOpen ? (_jsx(BottomSheet, { snapPoints: [0, 60, 92], defaultValue: 60, style: { pointerEvents: "auto" }, onChange: (vh) => {
459
494
  if (vh <= 0)
460
495
  setLibraryOpen(false);
461
496
  }, children: _jsx(LibraryPanel, { open: true, sheet: true, onClose: () => {
462
497
  setLibraryOpen(false);
463
- }, ...(onImportTemplates ? { onImport: onImportTemplates } : {}) }) })) : null) : (_jsx(LibraryPanel, { open: libraryOpen, side: "left", style: { left: 0 }, onClose: () => {
498
+ }, ...(onImportTemplates ? { onImport: onImportTemplates } : {}) }) })) : null) : (_jsx(LibraryPanel, { open: libraryOpen, side: "left", onClose: () => {
464
499
  setLibraryOpen(false);
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: () => {
500
+ }, ...(onImportTemplates ? { onImport: onImportTemplates } : {}) }))] }), minimapVisible && !zen && (_jsx("div", { className: "du-minimap-dock", children: _jsx(Minimap, {}) })), !hideDrawingPanel && !zen && (_jsx("div", { className: "du-tool-options-dock", children: _jsx(DrawingPanel, {}) })), !hideSelectionPanel && !zen && _jsx(SelectionFloatingPanel, {}), _jsx(HelpDialog, { open: helpOpen, onClose: () => {
481
501
  setHelpOpen(false);
482
502
  } }), _jsx(CommandPalette, {}), _jsx(SearchOverlay, {}), _jsx(StatsPanel, {}), void editor] }));
483
503
  };
@@ -500,34 +520,51 @@ const ZOOM_FIT_HOTKEY = formatHotkey({ alt: true, key: "1" });
500
520
  * `trailing` lets the host append extra controls inside the same pill
501
521
  * group (e.g. the Help button, so it sits right next to zoom).
502
522
  */
503
- const ZoomControls = ({ trailing }) => {
523
+ /** Zoom presets of the zoom menu (fractions of 100 %). */
524
+ const ZOOM_PRESETS = [0.5, 0.7, 1, 4, 12, 20];
525
+ const ZoomControls = ({ trailing, fullscreen, minimapVisible, onToggleMinimap, }) => {
504
526
  const editor = useDiagramOptional();
505
- // Force re-render on viewport change.
506
- const [, force] = useState(0);
507
- useEffect(() => {
508
- if (!editor)
509
- return undefined;
510
- return editor.subscribe(() => {
511
- force((n) => n + 1);
512
- });
513
- }, [editor]);
527
+ // Subscribe to the zoom VALUE only — a whole-editor subscription would
528
+ // re-render these buttons on every frame of an element drag.
529
+ const zoom = useEditorSelector((e) => e.scene.viewport.zoom, 1);
530
+ const showObjectSize = useEditorSelector((e) => e.preferences.showObjectSize, true);
514
531
  if (!editor)
515
532
  return null;
516
- const zoom = editor.scene.viewport.zoom;
533
+ const setShowObjectSize = (next) => {
534
+ editor.setPreferences({ showObjectSize: next });
535
+ };
517
536
  return (_jsxs(ButtonGroup, { ariaLabel: "Zoom", children: [_jsx(IconButton, { label: `Zoom out (${ZOOM_OUT_HOTKEY})`, onClick: () => {
518
537
  editor.zoomOut();
519
- }, children: _jsx(Minus, { ...buttonIcon }) }), _jsx(Tooltip, { content: `Reset zoom to 100% (${ZOOM_RESET_HOTKEY})`, children: _jsxs("button", { type: "button", className: "du-icon-button", "aria-label": "Reset zoom to 100%", onClick: () => {
520
- editor.resetZoom();
521
- }, style: {
522
- minWidth: 56,
523
- padding: "0 8px",
524
- borderRadius: 0,
525
- }, children: [Math.round(zoom * 100), "%"] }) }), _jsx(IconButton, { label: `Zoom in (${ZOOM_IN_HOTKEY})`, onClick: () => {
538
+ }, children: _jsx(Minus, { ...buttonIcon }) }), _jsxs(MainMenu, { ariaLabel: "Zoom menu", placement: "top-end", triggerClassName: "du-icon-button du-icon-button-flat du-zoom-trigger", trigger: _jsxs(_Fragment, { children: [Math.round(zoom * 100), "%"] }), children: [fullscreen.supported ? (_jsx(MainMenu.Item, { icon: fullscreen.active ? _jsx(Shrink, { ...menuIcon }) : _jsx(Expand, { ...menuIcon }), onClick: fullscreen.toggle, children: fullscreen.active ? "Exit full screen" : "Enter full screen" })) : null, _jsx(MainMenu.Item, { icon: _jsx(MapIcon, { ...menuIcon }), shortcut: "M", onClick: onToggleMinimap, children: minimapVisible ? "Hide minimap" : "Show minimap" }), _jsx(MainMenu.Submenu, { icon: _jsx(Grid3x3, { ...menuIcon }), label: "Grid", children: GRID_OPTIONS.map((opt) => (_jsx(MainMenu.Item, { icon: opt.icon, active: gridSelection(editor) === opt.value, onClick: () => {
539
+ applyGridSelection(editor, opt.value);
540
+ }, children: opt.label }, opt.value))) }), _jsx(MainMenu.Item, { icon: _jsx(Ruler, { ...menuIcon }), keepOpen: true, onClick: () => {
541
+ setShowObjectSize(!showObjectSize);
542
+ }, checked: showObjectSize, children: "Object dimensions" }), _jsx(MainMenu.Separator, {}), _jsx(MainMenu.Item, { icon: _jsx(Maximize, { ...menuIcon }), shortcut: ZOOM_FIT_HOTKEY, onClick: () => {
543
+ editor.zoomToFit();
544
+ }, children: "Fit to screen" }), ZOOM_PRESETS.map((level) => (_jsx(MainMenu.Item, { icon: _jsx(ZoomIn, { ...menuIcon }), ...(level === 1 ? { shortcut: ZOOM_RESET_HOTKEY } : {}), onClick: () => {
545
+ editor.setZoom(level);
546
+ }, children: `${String(Math.round(level * 100))}%` }, level)))] }), _jsx(IconButton, { label: `Zoom in (${ZOOM_IN_HOTKEY})`, onClick: () => {
526
547
  editor.zoomIn();
527
548
  }, children: _jsx(Plus, { ...buttonIcon }) }), _jsx(IconButton, { label: `Fit to screen (${ZOOM_FIT_HOTKEY})`, onClick: () => {
528
549
  editor.zoomToFit();
529
550
  }, children: _jsx(Maximize, { ...buttonIcon }) }), trailing] }));
530
551
  };
552
+ /** Grid submenu rows (reference labels), mapped onto the grid toggle values. */
553
+ const GRID_OPTIONS = [
554
+ { value: "off", label: "None", icon: _jsx(Ban, { ...menuIcon }) },
555
+ { value: "lines", label: "Line grid", icon: _jsx(Grid3x3, { ...menuIcon }) },
556
+ { value: "dots", label: "Dot grid", icon: _jsx(Grip, { ...menuIcon }) },
557
+ ];
558
+ const THEME_OPTIONS = [
559
+ { value: "light", label: "Light", icon: _jsx(Sun, { ...menuIcon }) },
560
+ { value: "dark", label: "Dark", icon: _jsx(Moon, { ...menuIcon }) },
561
+ { value: "system", label: "System", icon: _jsx(Monitor, { ...menuIcon }) },
562
+ ];
563
+ const WHEEL_MODE_OPTIONS = [
564
+ { value: "auto", label: "Auto-detect" },
565
+ { value: "mouse", label: "Mouse" },
566
+ { value: "trackpad", label: "Trackpad" },
567
+ ];
531
568
  // --- Grid toggle helpers ----------------------------------------------------
532
569
  /** Translate the `grid` / `snap` props into a partial settings override. */
533
570
  const buildHostSettings = (gridEnabled, gridStyle, snap) => ({