@matfsw/docusaurus-plantuml-plugin 1.0.0 → 1.0.1

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 CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.1]
11
+
12
+ ### Fixed
13
+
14
+ - Maximizing a diagram no longer uses the Fullscreen API. It took the whole browser window
15
+ fullscreen in Firefox instead of presenting the diagram, and the `::backdrop` sits outside
16
+ the fullscreened element, so the page showed through behind a diagram with a transparent
17
+ background. The diagram now expands into an opaque in-page overlay that fills the browser
18
+ viewport, closed with the same button or <kbd>Escape</kbd>.
19
+ - The maximize control is now available in every browser. It previously required
20
+ `Element.requestFullscreen()` and was hidden where that is missing, notably iOS Safari.
21
+
22
+ ### Added
23
+
24
+ - `data-plantuml-maximized` on the figure while a diagram fills the viewport.
25
+
26
+ ### Changed
27
+
28
+ - The fourth toolbar button is labelled `Maximize diagram` (was `Toggle fullscreen`) and
29
+ exposes `aria-pressed`. End-to-end tests selecting it by its accessible name need updating.
30
+
10
31
  ## [1.0.0]
11
32
 
12
33
  First stable release. The public surface — plugin options, fence metadata flags, the `data-*`
@@ -65,6 +86,7 @@ Initial release.
65
86
  opt-out.
66
87
  - Plugin option validation that rejects unknown keys and out-of-range values at build time.
67
88
 
68
- [Unreleased]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.0.0...HEAD
89
+ [Unreleased]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.0.1...HEAD
90
+ [1.0.1]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.0.0...v1.0.1
69
91
  [1.0.0]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v0.1.0...v1.0.0
70
92
  [0.1.0]: https://github.com/matf/docusaurus-plantuml-plugin/releases/tag/v0.1.0
package/README.md CHANGED
@@ -257,7 +257,7 @@ pannable by default, with a small control toolbar in the top-right corner.
257
257
  | Drag | Pans, once zoomed in |
258
258
  | One finger on a touchscreen | Scrolls the page |
259
259
  | Two-finger pinch on a touchscreen | The browser's own page zoom |
260
- | Toolbar buttons | Zoom out, zoom in, reset, fullscreen |
260
+ | Toolbar buttons | Zoom out, zoom in, reset, maximize |
261
261
 
262
262
  `Cmd` + wheel is deliberately **not** intercepted: on macOS that is the browser's own page
263
263
  zoom, and taking it over would fight the platform.
@@ -280,6 +280,18 @@ working, and `Tab` always moves on — the diagram is never a keyboard trap.
280
280
  Scale is limited to 0.25×–8×, and the view resets to 100% whenever the diagram source or the
281
281
  site colour mode changes.
282
282
 
283
+ ### Maximizing
284
+
285
+ The fourth toolbar button expands the diagram to fill the browser viewport, fitted to the
286
+ available space, over an opaque background. <kbd>Escape</kbd> or the same button restores it,
287
+ along with whatever zoom level you had before.
288
+
289
+ This is an in-page overlay rather than the Fullscreen API. `requestFullscreen()` takes the
290
+ entire browser window fullscreen in Firefox instead of presenting the element, and its
291
+ `::backdrop` sits outside the element so the page shows through behind the diagram. An overlay
292
+ has neither problem, behaves identically in every browser, and needs no capability detection —
293
+ so the control also works on iOS Safari, which has no element fullscreen at all.
294
+
283
295
  ### Turning it off
284
296
 
285
297
  Globally:
@@ -528,8 +540,6 @@ The runtime requires:
528
540
  - Pointer Events — required for drag-to-pan when `zoom` is enabled
529
541
  - `ResizeObserver` — optional; without it zoom still works, but the view is not re-clamped when
530
542
  the column is resized
531
- - `Element.requestFullscreen()` — optional; the fullscreen button is simply not rendered where
532
- it is unavailable, which includes iOS Safari
533
543
 
534
544
  That is modern evergreen Chrome, Edge, Firefox and Safari. Internet Explorer is not
535
545
  supported and will not be.
@@ -774,8 +784,6 @@ package.
774
784
  the browser reports it as Ctrl + wheel.
775
785
  - **Zoom state is not persisted.** Navigating away and back, or switching colour mode, returns
776
786
  the diagram to 100%.
777
- - **No fullscreen on iOS Safari.** It does not implement `Element.requestFullscreen()`, so the
778
- button is not shown there.
779
787
 
780
788
  ## Troubleshooting
781
789
 
@@ -26,4 +26,6 @@ export declare const DATA_ATTR: {
26
26
  * the imperative zoom writes can never race React's attribute diffing.
27
27
  */
28
28
  readonly zoom: "data-plantuml-zoom";
29
+ /** `"true"` on the figure while the diagram fills the browser viewport. */
30
+ readonly maximized: "data-plantuml-maximized";
29
31
  };
package/dist/constants.js CHANGED
@@ -28,4 +28,6 @@ export const DATA_ATTR = {
28
28
  * the imperative zoom writes can never race React's attribute diffing.
29
29
  */
30
30
  zoom: 'data-plantuml-zoom',
31
+ /** `"true"` on the figure while the diagram fills the browser viewport. */
32
+ maximized: 'data-plantuml-maximized',
31
33
  };
@@ -138,7 +138,8 @@ export default function PlantUmlDiagram({ source, title, language, zoom: zoomPro
138
138
  return (_jsxs("figure", { ref: containerRef, className: styles.figure, "aria-busy": busy || undefined, [DATA_ATTR.diagram]: language ?? 'plantuml',
139
139
  [DATA_ATTR.status]: state.status,
140
140
  [DATA_ATTR.theme]: dark ? 'dark' : 'light',
141
- ...(interactive ? { [DATA_ATTR.interactive]: 'true' } : {}), children: [state.status === 'ready' && state.svg !== null && !interactive && canvas, state.status === 'ready' && state.svg !== null && interactive && (_jsxs("div", { ref: zoom.stageRef, className: styles.stage, children: [_jsx("div", { ref: zoom.viewportRef, className: styles.viewport, tabIndex: 0, "aria-describedby": hintId, "aria-keyshortcuts": "Plus Minus 0 ArrowUp ArrowDown ArrowLeft ArrowRight", onKeyDown: zoom.onKeyDown, [DATA_ATTR.zoom]: '1', children: _jsx("div", { ref: zoom.layerRef, className: styles.layer, children: canvas }) }), _jsxs("div", { className: styles.toolbar, role: "group", "aria-label": `${label} zoom controls`, children: [_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom out", onClick: zoom.zoomOut, children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom in", onClick: zoom.zoomIn, children: _jsx("span", { "aria-hidden": "true", children: "+" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Reset zoom", onClick: zoom.reset, children: _jsx("span", { "aria-hidden": "true", children: "\u27F2" }) }), zoom.fullscreenSupported && (_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Toggle fullscreen", onClick: zoom.toggleFullscreen, children: _jsx("span", { "aria-hidden": "true", children: "\u26F6" }) })), _jsx("span", { ref: zoom.readoutRef, className: styles.readout, "aria-hidden": "true", children: "100%" })] })] })), state.status === 'ready' && state.svg !== null && interactive && (_jsx("p", { id: hintId, className: styles.visuallyHidden, children: "Zoomable diagram. Use the zoom controls, or press plus and minus to zoom, the arrow keys to pan, and zero to reset." })), state.status !== 'ready' && state.status !== 'error' && (
141
+ ...(interactive ? { [DATA_ATTR.interactive]: 'true' } : {}),
142
+ ...(zoom.maximized ? { [DATA_ATTR.maximized]: 'true' } : {}), children: [state.status === 'ready' && state.svg !== null && !interactive && canvas, state.status === 'ready' && state.svg !== null && interactive && (_jsxs("div", { ref: zoom.stageRef, className: zoom.maximized ? `${styles.stage} ${styles.maximized}` : styles.stage, children: [_jsx("div", { ref: zoom.viewportRef, className: styles.viewport, tabIndex: 0, "aria-describedby": hintId, "aria-keyshortcuts": "Plus Minus 0 ArrowUp ArrowDown ArrowLeft ArrowRight", onKeyDown: zoom.onKeyDown, [DATA_ATTR.zoom]: '1', children: _jsx("div", { ref: zoom.layerRef, className: styles.layer, children: canvas }) }), _jsxs("div", { className: styles.toolbar, role: "group", "aria-label": `${label} zoom controls`, children: [_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom out", onClick: zoom.zoomOut, children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom in", onClick: zoom.zoomIn, children: _jsx("span", { "aria-hidden": "true", children: "+" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Reset zoom", onClick: zoom.reset, children: _jsx("span", { "aria-hidden": "true", children: "\u27F2" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Maximize diagram", "aria-pressed": zoom.maximized, onClick: zoom.toggleMaximize, children: _jsx("span", { "aria-hidden": "true", children: zoom.maximized ? '✕' : '⛶' }) }), _jsx("span", { ref: zoom.readoutRef, className: styles.readout, "aria-hidden": "true", children: "100%" })] })] })), state.status === 'ready' && state.svg !== null && interactive && (_jsx("p", { id: hintId, className: styles.visuallyHidden, children: "Zoomable diagram. Use the zoom controls, or press plus and minus to zoom, the arrow keys to pan, and zero to reset." })), state.status !== 'ready' && state.status !== 'error' && (
142
143
  // `aria-busy` on the figure already conveys progress; announcing every phase change
143
144
  // would make a page with several diagrams unusable with a screen reader.
144
145
  _jsxs("div", { className: styles.placeholder, children: [_jsx("span", { className: styles.spinner, "aria-hidden": "true" }), _jsxs("span", { children: [state.status === 'loading' ? 'Loading PlantUML runtime…' : null, state.status === 'rendering' ? `Rendering ${label}…` : null, state.status === 'idle' ? `${label} (waiting to render)` : null] })] })), state.status === 'error' && (_jsxs("div", { className: styles.error, role: "alert", children: [_jsxs("p", { className: styles.errorHeading, children: [_jsx("span", { className: styles.errorIcon, "aria-hidden": "true", children: "\u26A0" }), _jsx("span", { children: "Error: PlantUML diagram could not be rendered" })] }), _jsx("pre", { className: styles.errorMessage, children: state.error }), config?.options.showSourceOnError !== false && (_jsxs("details", { className: styles.errorDetails, children: [_jsx("summary", { children: "Show diagram source" }), _jsx("pre", { className: styles.source, children: source })] }))] })), title !== undefined && _jsx("figcaption", { className: styles.caption, children: title }), _jsx("noscript", { dangerouslySetInnerHTML: {
@@ -108,24 +108,36 @@
108
108
  text-align: right;
109
109
  }
110
110
 
111
- .stage:fullscreen {
111
+ /*
112
+ * Maximized is an in-page overlay, not the Fullscreen API.
113
+ *
114
+ * `requestFullscreen()` took the whole browser window fullscreen in Firefox, and its
115
+ * `::backdrop` sits outside the element so the page showed through behind the diagram. A
116
+ * fixed overlay is fully opaque, behaves the same everywhere, and needs no capability check.
117
+ */
118
+ .maximized {
119
+ position: fixed;
120
+ inset: 0;
121
+ z-index: var(--ifm-z-index-overlay, 400);
112
122
  display: flex;
113
123
  align-items: center;
114
124
  justify-content: center;
115
- background: var(--ifm-background-color);
125
+ /*
126
+ * `--ifm-background-surface-color`, not `--ifm-background-color`: Infima defines the latter
127
+ * as `#0000` in light mode, so the overlay would be transparent and the page would show
128
+ * through — which is the defect this overlay exists to fix. The surface colour is opaque in
129
+ * both colour modes (white / #242526), and a `var()` fallback would not have saved us here,
130
+ * because the variable is defined, just transparent.
131
+ */
132
+ background: var(--ifm-background-surface-color, #ffffff);
116
133
  }
117
134
 
118
- .stage:fullscreen .viewport {
135
+ .maximized .viewport {
119
136
  width: 100vw;
120
137
  height: 100dvh;
121
138
  max-height: none;
122
139
  }
123
140
 
124
- /* The default backdrop is black, which makes a transparent light-mode diagram look broken. */
125
- .stage::backdrop {
126
- background: var(--ifm-background-color, #ffffff);
127
- }
128
-
129
141
  .visuallyHidden {
130
142
  position: absolute;
131
143
  width: 1px;
@@ -8,10 +8,10 @@ export interface ZoomPanHandle {
8
8
  zoomIn: () => void;
9
9
  zoomOut: () => void;
10
10
  reset: () => void;
11
- toggleFullscreen: () => void;
11
+ toggleMaximize: () => void;
12
12
  onKeyDown: (event: ReactKeyboardEvent<HTMLDivElement>) => void;
13
- /** Whether this browser can fullscreen an element, so the button can be hidden if not. */
14
- fullscreenSupported: boolean;
13
+ /** Whether the diagram currently fills the browser viewport. */
14
+ maximized: boolean;
15
15
  }
16
16
  export interface UseZoomPanParams {
17
17
  enabled: boolean;
@@ -1,38 +1,26 @@
1
- import { useCallback, useEffect, useRef, } from 'react';
1
+ import { useCallback, useEffect, useRef, useState, } from 'react';
2
2
  import { clampTransform, fitScale, formatPercent, formatZoom, IDENTITY, MAX_SCALE, MIN_SCALE, normalizeWheelDelta, panBy, PAN_STEP, SCALE_STEP, toCssTransform, wheelZoomFactor, zoomAbout, } from './zoomMath.js';
3
3
  /**
4
- * Zoom, pan and fullscreen for one diagram.
4
+ * Zoom, pan and maximize for one diagram.
5
5
  *
6
- * The hook holds no React state at all. The transform lives in a ref and is written straight
7
- * to the DOM, so panning never re-renders the component — and, more usefully, "no state update
8
- * after unmount" becomes structurally true rather than something a guard has to enforce.
6
+ * The transform lives in a ref and is written straight to the DOM, so panning and zooming
7
+ * never re-render the component. The single piece of React state is `maximized`, which changes
8
+ * only on an explicit click or Escape — it drives markup, so it belongs in React, and at one
9
+ * transition per user action it costs nothing.
9
10
  */
10
11
  /** Distance the pointer must travel before a press becomes a drag rather than a click. */
11
12
  const DRAG_THRESHOLD_PX = 3;
12
13
  /** Fraction of the viewport a Shift+arrow press pans. */
13
14
  const PAGE_PAN_RATIO = 0.9;
14
- function supportsFullscreen() {
15
- if (typeof document === 'undefined')
16
- return false;
17
- const doc = document;
18
- return Boolean(doc.fullscreenEnabled ||
19
- typeof document.documentElement.webkitRequestFullscreen ===
20
- 'function');
21
- }
22
- function currentFullscreenElement() {
23
- if (typeof document === 'undefined')
24
- return null;
25
- const doc = document;
26
- return doc.fullscreenElement ?? doc.webkitFullscreenElement ?? null;
27
- }
28
15
  export function useZoomPan({ enabled, resetKey }) {
29
16
  const stageRef = useRef(null);
30
17
  const viewportRef = useRef(null);
31
18
  const layerRef = useRef(null);
32
19
  const readoutRef = useRef(null);
20
+ const [maximized, setMaximized] = useState(false);
33
21
  const transformRef = useRef(IDENTITY);
34
- /** The view to restore when leaving fullscreen. */
35
- const beforeFullscreenRef = useRef(null);
22
+ /** The view to restore when the diagram is un-maximized. */
23
+ const beforeMaximizeRef = useRef(null);
36
24
  const frameRef = useRef(null);
37
25
  /**
38
26
  * Layout sizes, never `getBoundingClientRect()`: the layer's rect includes the transform we
@@ -75,17 +63,8 @@ export function useZoomPan({ enabled, resetKey }) {
75
63
  }, [apply]);
76
64
  const zoomIn = useCallback(() => zoomByStep(SCALE_STEP), [zoomByStep]);
77
65
  const zoomOut = useCallback(() => zoomByStep(1 / SCALE_STEP), [zoomByStep]);
78
- const toggleFullscreen = useCallback(() => {
79
- const stage = stageRef.current;
80
- if (!stage)
81
- return;
82
- const doc = document;
83
- if (currentFullscreenElement()) {
84
- void (doc.exitFullscreen?.() ?? doc.webkitExitFullscreen?.());
85
- return;
86
- }
87
- beforeFullscreenRef.current = transformRef.current;
88
- void (stage.requestFullscreen?.() ?? stage.webkitRequestFullscreen?.());
66
+ const toggleMaximize = useCallback(() => {
67
+ setMaximized((previous) => !previous);
89
68
  }, []);
90
69
  const onKeyDown = useCallback((event) => {
91
70
  if (!enabled)
@@ -229,19 +208,6 @@ export function useZoomPan({ enabled, resetKey }) {
229
208
  event.preventDefault();
230
209
  event.stopPropagation();
231
210
  };
232
- const onFullscreenChange = () => {
233
- const stage = stageRef.current;
234
- const isFullscreen = stage !== null && currentFullscreenElement() === stage;
235
- if (isFullscreen) {
236
- // Fill the screen: the viewport is much larger now, so refit rather than keep a
237
- // transform that was computed for a column-width box.
238
- apply({ ...IDENTITY, k: fitScale(measure()) });
239
- }
240
- else {
241
- apply(beforeFullscreenRef.current ?? IDENTITY);
242
- beforeFullscreenRef.current = null;
243
- }
244
- };
245
211
  viewport.addEventListener('wheel', onWheel, { passive: false });
246
212
  viewport.addEventListener('pointerdown', onPointerDown);
247
213
  viewport.addEventListener('pointermove', onPointerMove);
@@ -249,8 +215,6 @@ export function useZoomPan({ enabled, resetKey }) {
249
215
  viewport.addEventListener('pointercancel', endDrag);
250
216
  viewport.addEventListener('lostpointercapture', endDrag);
251
217
  viewport.addEventListener('click', onClickCapture, true);
252
- document.addEventListener('fullscreenchange', onFullscreenChange);
253
- document.addEventListener('webkitfullscreenchange', onFullscreenChange);
254
218
  // Re-clamp when the column width changes (window resize, sidebar collapse), so a panned
255
219
  // diagram cannot end up stranded outside a narrower viewport.
256
220
  const observer = typeof ResizeObserver === 'undefined'
@@ -265,8 +229,6 @@ export function useZoomPan({ enabled, resetKey }) {
265
229
  viewport.removeEventListener('pointercancel', endDrag);
266
230
  viewport.removeEventListener('lostpointercapture', endDrag);
267
231
  viewport.removeEventListener('click', onClickCapture, true);
268
- document.removeEventListener('fullscreenchange', onFullscreenChange);
269
- document.removeEventListener('webkitfullscreenchange', onFullscreenChange);
270
232
  observer?.disconnect();
271
233
  if (frameRef.current !== null) {
272
234
  cancelAnimationFrame(frameRef.current);
@@ -295,6 +257,46 @@ export function useZoomPan({ enabled, resetKey }) {
295
257
  return;
296
258
  write(IDENTITY);
297
259
  }, [enabled, resetKey, write]);
260
+ /**
261
+ * Maximizing is done in the page, not with the Fullscreen API.
262
+ *
263
+ * `requestFullscreen()` gave two problems it cannot solve: Firefox takes the whole browser
264
+ * window fullscreen rather than presenting the element, and the `::backdrop` is outside the
265
+ * element so the page behind it shows through. A fixed-position overlay we own has neither
266
+ * issue, works identically in every browser, and needs no capability detection — which also
267
+ * restores the control on iOS Safari, where element fullscreen does not exist.
268
+ */
269
+ useEffect(() => {
270
+ if (!enabled || !maximized)
271
+ return undefined;
272
+ beforeMaximizeRef.current = transformRef.current;
273
+ // The overlay covers the viewport; scrolling the page underneath it would be confusing.
274
+ const body = document.body;
275
+ const previousOverflow = body.style.overflow;
276
+ body.style.overflow = 'hidden';
277
+ // The layout is already committed by the time an effect runs, so the viewport now
278
+ // reports its maximized size and the diagram can be fitted to it.
279
+ apply({ ...IDENTITY, k: fitScale(measure()) });
280
+ viewportRef.current?.focus();
281
+ const onKeyDownDocument = (event) => {
282
+ if (event.key !== 'Escape')
283
+ return;
284
+ event.preventDefault();
285
+ setMaximized(false);
286
+ };
287
+ document.addEventListener('keydown', onKeyDownDocument);
288
+ return () => {
289
+ document.removeEventListener('keydown', onKeyDownDocument);
290
+ body.style.overflow = previousOverflow;
291
+ apply(beforeMaximizeRef.current ?? IDENTITY);
292
+ beforeMaximizeRef.current = null;
293
+ };
294
+ }, [apply, enabled, maximized, measure]);
295
+ // A diagram that is re-rendered or unmounted must not leave the page scroll-locked.
296
+ useEffect(() => {
297
+ if (!enabled)
298
+ setMaximized(false);
299
+ }, [enabled]);
298
300
  return {
299
301
  stageRef,
300
302
  viewportRef,
@@ -303,9 +305,9 @@ export function useZoomPan({ enabled, resetKey }) {
303
305
  zoomIn,
304
306
  zoomOut,
305
307
  reset,
306
- toggleFullscreen,
308
+ toggleMaximize,
307
309
  onKeyDown,
308
- fullscreenSupported: supportsFullscreen(),
310
+ maximized,
309
311
  };
310
312
  }
311
313
  export { MAX_SCALE, MIN_SCALE };
@@ -62,7 +62,7 @@ export declare function wheelZoomFactor(normalizedDelta: number): number;
62
62
  /**
63
63
  * Largest scale at which the whole diagram still fits inside the given viewport.
64
64
  *
65
- * Used when entering fullscreen. Small diagrams are magnified rather than left stranded in
65
+ * Used when maximizing. Small diagrams are magnified rather than left stranded in
66
66
  * the middle of a large screen — the output is vector, so it stays crisp — but never beyond
67
67
  * `max`, and never below {@link MIN_SCALE}.
68
68
  */
@@ -82,7 +82,7 @@ export function wheelZoomFactor(normalizedDelta) {
82
82
  /**
83
83
  * Largest scale at which the whole diagram still fits inside the given viewport.
84
84
  *
85
- * Used when entering fullscreen. Small diagrams are magnified rather than left stranded in
85
+ * Used when maximizing. Small diagrams are magnified rather than left stranded in
86
86
  * the middle of a large screen — the output is vector, so it stays crisp — but never beyond
87
87
  * `max`, and never below {@link MIN_SCALE}.
88
88
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matfsw/docusaurus-plantuml-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Render PlantUML diagrams in Docusaurus 3 entirely in the browser - no Java, no PlantUML server, no CDN.",
5
5
  "keywords": [
6
6
  "docusaurus",