@nowline/preview-shell 0.0.0-dev.20260601071750.g04bdff9

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/mount.js ADDED
@@ -0,0 +1,1192 @@
1
+ // Framework-agnostic preview viewport mounted into a host element.
2
+ // Lifted from `packages/vscode-extension/src/preview/shell-html.ts`'s
3
+ // inline SCRIPT body and translated from a VS Code-coupled IIFE into
4
+ // a TypeScript module with an imperative API.
5
+ //
6
+ // The mount supplies its own DOM, CSS class scoping, zoom/pan logic,
7
+ // minimap, and diagnostic table. Consumers feed SVG strings + diagnostic
8
+ // rows in via the returned handle and receive user interactions (jump
9
+ // to source, save, copy, view-options changes) via callback options —
10
+ // no `acquireVsCodeApi()`, no `postMessage`, no host coupling.
11
+ import { buildViewport } from './markup.js';
12
+ import { PREVIEW_SHELL_CSS } from './styles.js';
13
+ let stylesheetMounted = false;
14
+ /**
15
+ * Last chrome position saved after a drag, shared across all shells in
16
+ * this JS session. Persists for the lifetime of the page.
17
+ */
18
+ let savedChromePosition = null;
19
+ function ensureStylesheet(doc) {
20
+ if (stylesheetMounted)
21
+ return;
22
+ // A CSP-restricted consumer (e.g. the VS Code webview, whose
23
+ // `style-src` is nonce-only with no `'unsafe-inline'`) pre-injects
24
+ // this stylesheet into the host HTML with a nonce, marked by the
25
+ // `data-nl-preview-shell` attribute. A second `<style>` created here
26
+ // would carry no nonce and be refused by the CSP, leaving the
27
+ // toolbar unstyled — so defer to the host-provided copy when present.
28
+ if (doc.querySelector('style[data-nl-preview-shell]')) {
29
+ stylesheetMounted = true;
30
+ return;
31
+ }
32
+ const style = doc.createElement('style');
33
+ style.setAttribute('data-nl-preview-shell', '');
34
+ style.textContent = PREVIEW_SHELL_CSS;
35
+ doc.head?.appendChild(style);
36
+ stylesheetMounted = true;
37
+ }
38
+ export function mountPreview(rootEl, options = {}) {
39
+ ensureStylesheet(rootEl.ownerDocument);
40
+ const els = buildViewport(rootEl);
41
+ const doc = rootEl.ownerDocument;
42
+ const win = doc.defaultView ?? globalThis;
43
+ const state = {
44
+ svgString: null,
45
+ scale: 1,
46
+ naturalWidth: 0,
47
+ naturalHeight: 0,
48
+ defaultFit: options.initialFit ?? 'fitPage',
49
+ activeFit: options.initialFit === 'fitWidth' ? 'fitWidth' : 'fitPage',
50
+ isDirty: false,
51
+ lastViewportWidth: 0,
52
+ lastViewportHeight: 0,
53
+ showMinimap: options.showMinimap !== false,
54
+ minimapDismissedThisSession: false,
55
+ firstRender: true,
56
+ mode: 'dark',
57
+ locale: options.locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en'),
58
+ export: { format: 'svg' },
59
+ view: {
60
+ theme: 'auto',
61
+ now: 'today',
62
+ showLinks: true,
63
+ overridden: { theme: false, now: false, showLinks: false },
64
+ },
65
+ };
66
+ const cleanups = [];
67
+ function on(target, type, handler, opts) {
68
+ target.addEventListener(type, handler, opts);
69
+ cleanups.push(() => target.removeEventListener(type, handler, opts));
70
+ }
71
+ // ===== Mode resolution =====
72
+ function resolveSystemMode() {
73
+ const body = doc.body;
74
+ if (body) {
75
+ if (body.classList.contains('vscode-dark') ||
76
+ body.classList.contains('vscode-high-contrast-dark'))
77
+ return 'dark';
78
+ if (body.classList.contains('vscode-light') ||
79
+ body.classList.contains('vscode-high-contrast-light') ||
80
+ body.classList.contains('vscode-high-contrast'))
81
+ return 'light';
82
+ }
83
+ if (typeof win.matchMedia === 'function') {
84
+ return win.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
85
+ }
86
+ return 'dark';
87
+ }
88
+ function applyMode(resolved) {
89
+ state.mode = resolved;
90
+ rootEl.setAttribute('data-nl-mode', resolved);
91
+ }
92
+ const rawMode = options.mode ?? 'system';
93
+ applyMode(rawMode === 'system' ? resolveSystemMode() : rawMode);
94
+ if (rawMode === 'system') {
95
+ if (typeof MutationObserver !== 'undefined' && doc.body) {
96
+ const bodyObs = new MutationObserver(() => applyMode(resolveSystemMode()));
97
+ bodyObs.observe(doc.body, { attributes: true, attributeFilter: ['class'] });
98
+ cleanups.push(() => bodyObs.disconnect());
99
+ }
100
+ const mq = typeof win.matchMedia === 'function'
101
+ ? win.matchMedia('(prefers-color-scheme: dark)')
102
+ : null;
103
+ if (mq) {
104
+ const onMqChange = () => {
105
+ const body = doc.body;
106
+ const hasVsCode = body &&
107
+ (body.classList.contains('vscode-dark') ||
108
+ body.classList.contains('vscode-light') ||
109
+ body.classList.contains('vscode-high-contrast') ||
110
+ body.classList.contains('vscode-high-contrast-dark') ||
111
+ body.classList.contains('vscode-high-contrast-light'));
112
+ if (!hasVsCode)
113
+ applyMode(mq.matches ? 'dark' : 'light');
114
+ };
115
+ mq.addEventListener('change', onMqChange);
116
+ cleanups.push(() => mq.removeEventListener('change', onMqChange));
117
+ }
118
+ }
119
+ // ===== Floating UI fade =====
120
+ let fadeTimer = null;
121
+ function showFloatingUI() {
122
+ els.chrome.classList.remove('faded');
123
+ els.minimap.classList.remove('faded');
124
+ if (fadeTimer)
125
+ clearTimeout(fadeTimer);
126
+ fadeTimer = setTimeout(() => {
127
+ els.chrome.classList.add('faded');
128
+ els.minimap.classList.add('faded');
129
+ }, 2000);
130
+ }
131
+ on(doc, 'mousemove', showFloatingUI);
132
+ showFloatingUI();
133
+ // ===== Collapse / restore =====
134
+ // « collapses the toolbar to a translucent puck (just the drag grip
135
+ // + a » restore arrow); » expands it back to the full toolbar.
136
+ on(els.collapseBtn, 'click', (e) => {
137
+ e.stopPropagation();
138
+ closeAllMenus();
139
+ els.chrome.classList.add('collapsed');
140
+ repositionChrome();
141
+ });
142
+ on(els.restoreBtn, 'click', (e) => {
143
+ e.stopPropagation();
144
+ els.chrome.classList.remove('collapsed');
145
+ repositionChrome();
146
+ });
147
+ // ===== Toolbar positioning + drag =====
148
+ // The chrome defaults to the upper-right corner and tracks it on
149
+ // resize (`chromePinnedRight`). Once dragged it becomes free-floating
150
+ // and is merely clamped back into view. Either way the position is
151
+ // always expressed as explicit `left`/`top` so a narrowing viewport
152
+ // shifts the toolbar left instead of squishing the row.
153
+ let chromePinnedRight = savedChromePosition === null;
154
+ let chromeDragStart = null;
155
+ function getGutter() {
156
+ const val = getComputedStyle(rootEl).getPropertyValue('--nl-preview-gutter').trim();
157
+ return parseInt(val, 10) || 8;
158
+ }
159
+ function repositionChrome() {
160
+ const g = getGutter();
161
+ const cw = els.chrome.offsetWidth;
162
+ const ch = els.chrome.offsetHeight;
163
+ const maxLeft = Math.max(g, rootEl.clientWidth - cw - g);
164
+ const maxTop = Math.max(g, rootEl.clientHeight - ch - g);
165
+ let left;
166
+ let top;
167
+ if (chromePinnedRight) {
168
+ left = rootEl.clientWidth - cw - g;
169
+ top = g;
170
+ }
171
+ else {
172
+ left = parseInt(els.chrome.style.left, 10);
173
+ top = parseInt(els.chrome.style.top, 10);
174
+ if (Number.isNaN(left))
175
+ left = savedChromePosition?.left ?? g;
176
+ if (Number.isNaN(top))
177
+ top = savedChromePosition?.top ?? g;
178
+ }
179
+ els.chrome.style.right = 'auto';
180
+ els.chrome.style.left = `${Math.max(g, Math.min(left, maxLeft))}px`;
181
+ els.chrome.style.top = `${Math.max(g, Math.min(top, maxTop))}px`;
182
+ }
183
+ function ensureChromeLeftTopAnchoring() {
184
+ if (els.chrome.style.left !== '')
185
+ return;
186
+ els.chrome.style.right = 'auto';
187
+ els.chrome.style.left = `${els.chrome.offsetLeft}px`;
188
+ els.chrome.style.top = `${els.chrome.offsetTop}px`;
189
+ }
190
+ on(els.toolbarHandle, 'pointerdown', (e) => {
191
+ e.preventDefault();
192
+ chromePinnedRight = false;
193
+ ensureChromeLeftTopAnchoring();
194
+ chromeDragStart = {
195
+ pointerX: e.clientX,
196
+ pointerY: e.clientY,
197
+ chromeLeft: parseInt(els.chrome.style.left, 10) || 0,
198
+ chromeTop: parseInt(els.chrome.style.top, 10) || 0,
199
+ };
200
+ els.chrome.classList.add('dragging');
201
+ els.toolbarHandle.setPointerCapture(e.pointerId);
202
+ });
203
+ on(els.toolbarHandle, 'pointermove', (e) => {
204
+ if (!chromeDragStart)
205
+ return;
206
+ const dx = e.clientX - chromeDragStart.pointerX;
207
+ const dy = e.clientY - chromeDragStart.pointerY;
208
+ const rawLeft = chromeDragStart.chromeLeft + dx;
209
+ const rawTop = chromeDragStart.chromeTop + dy;
210
+ const g = getGutter();
211
+ const maxLeft = Math.max(g, rootEl.clientWidth - els.chrome.offsetWidth - g);
212
+ const maxTop = Math.max(g, rootEl.clientHeight - els.chrome.offsetHeight - g);
213
+ els.chrome.style.left = `${Math.max(g, Math.min(rawLeft, maxLeft))}px`;
214
+ els.chrome.style.top = `${Math.max(g, Math.min(rawTop, maxTop))}px`;
215
+ });
216
+ function endChromeDrag() {
217
+ if (!chromeDragStart)
218
+ return;
219
+ chromeDragStart = null;
220
+ els.chrome.classList.remove('dragging');
221
+ savedChromePosition = {
222
+ left: parseInt(els.chrome.style.left, 10) || 0,
223
+ top: parseInt(els.chrome.style.top, 10) || 0,
224
+ };
225
+ }
226
+ on(els.toolbarHandle, 'pointerup', endChromeDrag);
227
+ on(els.toolbarHandle, 'pointercancel', () => {
228
+ chromeDragStart = null;
229
+ els.chrome.classList.remove('dragging');
230
+ });
231
+ // ===== Transform =====
232
+ function applyTransform() {
233
+ const w = Math.max(1, Math.round(state.naturalWidth * state.scale));
234
+ const h = Math.max(1, Math.round(state.naturalHeight * state.scale));
235
+ els.canvas.style.width = `${w}px`;
236
+ els.canvas.style.height = `${h}px`;
237
+ els.zoomReset.textContent = `${Math.round(state.scale * 100)}%`;
238
+ updateMinimapRect();
239
+ updateMinimapVisibility();
240
+ }
241
+ function clampScale(s) {
242
+ if (s < 0.05)
243
+ return 0.05;
244
+ if (s > 20)
245
+ return 20;
246
+ return s;
247
+ }
248
+ function setScale(newScale, anchorX, anchorY) {
249
+ const clamped = clampScale(newScale);
250
+ if (anchorX === undefined || anchorY === undefined || state.scale === 0) {
251
+ state.scale = clamped;
252
+ applyTransform();
253
+ return;
254
+ }
255
+ const ratio = clamped / state.scale;
256
+ const newScrollX = (els.viewport.scrollLeft + anchorX) * ratio - anchorX;
257
+ const newScrollY = (els.viewport.scrollTop + anchorY) * ratio - anchorY;
258
+ state.scale = clamped;
259
+ applyTransform();
260
+ els.viewport.scrollLeft = newScrollX;
261
+ els.viewport.scrollTop = newScrollY;
262
+ }
263
+ function fitPage() {
264
+ if (!state.naturalWidth || !state.naturalHeight)
265
+ return;
266
+ state.activeFit = 'fitPage';
267
+ state.isDirty = false;
268
+ const sx = els.viewport.clientWidth / state.naturalWidth;
269
+ const sy = els.viewport.clientHeight / state.naturalHeight;
270
+ setScale(Math.min(sx, sy));
271
+ els.viewport.scrollLeft = 0;
272
+ els.viewport.scrollTop = 0;
273
+ }
274
+ function fitWidth() {
275
+ if (!state.naturalWidth)
276
+ return;
277
+ state.activeFit = 'fitWidth';
278
+ state.isDirty = false;
279
+ setScale(els.viewport.clientWidth / state.naturalWidth);
280
+ els.viewport.scrollLeft = 0;
281
+ els.viewport.scrollTop = 0;
282
+ }
283
+ function actualSize() {
284
+ state.activeFit = 'manual';
285
+ state.isDirty = true;
286
+ setScale(1);
287
+ }
288
+ function applyDefaultFit() {
289
+ if (state.defaultFit === 'fitWidth')
290
+ fitWidth();
291
+ else if (state.defaultFit === 'actual')
292
+ actualSize();
293
+ else
294
+ fitPage();
295
+ }
296
+ function reapplyActiveFit() {
297
+ const newW = els.viewport.clientWidth;
298
+ const newH = els.viewport.clientHeight;
299
+ if (state.isDirty) {
300
+ const oldW = state.lastViewportWidth || newW;
301
+ const oldH = state.lastViewportHeight || newH;
302
+ if (state.scale > 0 && (oldW !== newW || oldH !== newH)) {
303
+ const cx = (els.viewport.scrollLeft + oldW / 2) / state.scale;
304
+ const cy = (els.viewport.scrollTop + oldH / 2) / state.scale;
305
+ els.viewport.scrollLeft = Math.max(0, cx * state.scale - newW / 2);
306
+ els.viewport.scrollTop = Math.max(0, cy * state.scale - newH / 2);
307
+ }
308
+ }
309
+ else if (state.activeFit === 'fitPage') {
310
+ fitPage();
311
+ }
312
+ else if (state.activeFit === 'fitWidth') {
313
+ fitWidth();
314
+ }
315
+ state.lastViewportWidth = newW;
316
+ state.lastViewportHeight = newH;
317
+ }
318
+ // ===== Render handling =====
319
+ function extractNaturalSize(svgEl) {
320
+ let w = 0;
321
+ let h = 0;
322
+ if (svgEl.viewBox?.baseVal?.width) {
323
+ w = svgEl.viewBox.baseVal.width;
324
+ h = svgEl.viewBox.baseVal.height;
325
+ }
326
+ if (!w && svgEl.width?.baseVal?.value) {
327
+ w = svgEl.width.baseVal.value;
328
+ }
329
+ if (!h && svgEl.height?.baseVal?.value) {
330
+ h = svgEl.height.baseVal.value;
331
+ }
332
+ svgEl.removeAttribute('width');
333
+ svgEl.removeAttribute('height');
334
+ if (!svgEl.getAttribute('viewBox') && w && h) {
335
+ svgEl.setAttribute('viewBox', `0 0 ${w} ${h}`);
336
+ }
337
+ svgEl.style.width = '100%';
338
+ svgEl.style.height = '100%';
339
+ state.naturalWidth = w || 800;
340
+ state.naturalHeight = h || 600;
341
+ }
342
+ function setSvg(svgString) {
343
+ state.svgString = svgString;
344
+ els.canvas.innerHTML = svgString;
345
+ const svgEl = els.canvas.querySelector('svg');
346
+ if (!svgEl)
347
+ return;
348
+ extractNaturalSize(svgEl);
349
+ rebuildMinimap(svgString);
350
+ if (state.firstRender) {
351
+ applyDefaultFit();
352
+ state.firstRender = false;
353
+ }
354
+ else {
355
+ applyTransform();
356
+ }
357
+ showSvgMode();
358
+ }
359
+ function showSvgMode() {
360
+ els.empty.classList.add('hidden');
361
+ els.canvas.classList.remove('dimmed');
362
+ els.diagnostics.classList.remove('show');
363
+ }
364
+ function showDiagnosticsMode(rows) {
365
+ els.empty.classList.add('hidden');
366
+ if (state.svgString) {
367
+ els.canvas.classList.add('dimmed');
368
+ }
369
+ populateDiagnostics(rows);
370
+ els.diagnostics.classList.add('show');
371
+ }
372
+ function showFatal(message) {
373
+ const fakeRow = {
374
+ severity: 'error',
375
+ code: 'preview-error',
376
+ message,
377
+ file: '',
378
+ line: 1,
379
+ column: 1,
380
+ };
381
+ showDiagnosticsMode([fakeRow]);
382
+ }
383
+ // ===== Diagnostics table =====
384
+ function populateDiagnostics(rows) {
385
+ let errors = 0;
386
+ let warnings = 0;
387
+ for (const r of rows) {
388
+ if (r.severity === 'warning')
389
+ warnings++;
390
+ else
391
+ errors++;
392
+ }
393
+ let summary = '';
394
+ if (errors)
395
+ summary += `${errors}${errors === 1 ? ' error' : ' errors'}`;
396
+ if (errors && warnings)
397
+ summary += ', ';
398
+ if (warnings)
399
+ summary += `${warnings}${warnings === 1 ? ' warning' : ' warnings'}`;
400
+ if (!summary)
401
+ summary = 'No problems';
402
+ els.diagSummary.textContent = summary;
403
+ els.diagSummary.className = 'diag-summary';
404
+ if (!errors && warnings)
405
+ els.diagSummary.classList.add('warn-only');
406
+ if (!errors && !warnings)
407
+ els.diagSummary.classList.add('clean');
408
+ // Hide Open Problems link when there are no issues
409
+ els.openProblems.hidden = errors + warnings === 0;
410
+ const tbody = els.diagTbody;
411
+ while (tbody.firstChild)
412
+ tbody.removeChild(tbody.firstChild);
413
+ for (const r of rows) {
414
+ const tr = doc.createElement('tr');
415
+ tr.dataset.file = r.file || '';
416
+ tr.dataset.line = String(r.line);
417
+ tr.dataset.column = String(r.column);
418
+ const sevTd = doc.createElement('td');
419
+ sevTd.className = `sev-cell ${r.severity === 'warning' ? 'sev-warning' : 'sev-error'}`;
420
+ sevTd.textContent = r.severity === 'warning' ? '\u26A0' : '!';
421
+ tr.appendChild(sevTd);
422
+ const locTd = doc.createElement('td');
423
+ locTd.className = 'loc-cell';
424
+ locTd.textContent = `Ln ${r.line}, ${r.column}`;
425
+ tr.appendChild(locTd);
426
+ const codeTd = doc.createElement('td');
427
+ codeTd.className = 'code-cell';
428
+ const pill = doc.createElement('span');
429
+ pill.className = 'code-pill';
430
+ pill.textContent = r.code;
431
+ codeTd.appendChild(pill);
432
+ tr.appendChild(codeTd);
433
+ const msgTd = doc.createElement('td');
434
+ msgTd.className = 'msg-cell';
435
+ msgTd.textContent = r.message;
436
+ if (r.suggestion) {
437
+ const sg = doc.createElement('span');
438
+ sg.className = 'suggestion';
439
+ sg.textContent = `Did you mean '${r.suggestion}'?`;
440
+ msgTd.appendChild(sg);
441
+ }
442
+ tr.appendChild(msgTd);
443
+ tbody.appendChild(tr);
444
+ }
445
+ }
446
+ on(els.diagTbody, 'click', (e) => {
447
+ const target = e.target;
448
+ const tr = target?.closest('tr');
449
+ if (!tr)
450
+ return;
451
+ options.onGoto?.({
452
+ file: tr.dataset.file ?? '',
453
+ line: Number.parseInt(tr.dataset.line ?? '1', 10) || 1,
454
+ column: Number.parseInt(tr.dataset.column ?? '1', 10) || 1,
455
+ });
456
+ });
457
+ on(els.openProblems, 'click', (e) => {
458
+ e.preventDefault();
459
+ options.onOpenProblems?.();
460
+ });
461
+ // ===== Minimap =====
462
+ function rebuildMinimap(svgString) {
463
+ if (!els.minimapCanvas)
464
+ return;
465
+ els.minimapCanvas.innerHTML = svgString;
466
+ const svgEl = els.minimapCanvas.querySelector('svg');
467
+ if (svgEl) {
468
+ svgEl.removeAttribute('width');
469
+ svgEl.removeAttribute('height');
470
+ if (state.naturalWidth && state.naturalHeight && !svgEl.getAttribute('viewBox')) {
471
+ svgEl.setAttribute('viewBox', `0 0 ${state.naturalWidth} ${state.naturalHeight}`);
472
+ }
473
+ svgEl.setAttribute('preserveAspectRatio', 'xMidYMid meet');
474
+ }
475
+ const maxW = 120;
476
+ const maxH = 90;
477
+ if (state.naturalWidth && state.naturalHeight) {
478
+ const ratio = state.naturalWidth / state.naturalHeight;
479
+ let w = maxW;
480
+ let h = maxW / ratio;
481
+ if (h > maxH) {
482
+ h = maxH;
483
+ w = maxH * ratio;
484
+ }
485
+ els.minimap.style.width = `${Math.round(w)}px`;
486
+ els.minimapCanvas.style.height = `${Math.round(h)}px`;
487
+ }
488
+ }
489
+ function updateMinimapRect() {
490
+ if (els.minimap.classList.contains('hidden'))
491
+ return;
492
+ const totalW = state.naturalWidth * state.scale;
493
+ const totalH = state.naturalHeight * state.scale;
494
+ if (!totalW || !totalH)
495
+ return;
496
+ const miniW = els.minimapCanvas.clientWidth;
497
+ const miniH = els.minimapCanvas.clientHeight;
498
+ const rectW = Math.min(1, els.viewport.clientWidth / totalW) * miniW;
499
+ const rectH = Math.min(1, els.viewport.clientHeight / totalH) * miniH;
500
+ const rectX = (els.viewport.scrollLeft / totalW) * miniW;
501
+ const rectY = (els.viewport.scrollTop / totalH) * miniH;
502
+ els.minimapRect.style.left = `${els.minimapCanvas.offsetLeft + rectX}px`;
503
+ els.minimapRect.style.top = `${els.minimapCanvas.offsetTop + rectY}px`;
504
+ els.minimapRect.style.width = `${rectW}px`;
505
+ els.minimapRect.style.height = `${rectH}px`;
506
+ }
507
+ function updateMinimapVisibility() {
508
+ if (!state.showMinimap || state.minimapDismissedThisSession) {
509
+ els.minimap.classList.add('hidden');
510
+ return;
511
+ }
512
+ const totalW = state.naturalWidth * state.scale;
513
+ const totalH = state.naturalHeight * state.scale;
514
+ const fits = totalW <= els.viewport.clientWidth + 1 && totalH <= els.viewport.clientHeight + 1;
515
+ if (fits) {
516
+ els.minimap.classList.add('hidden');
517
+ }
518
+ else {
519
+ els.minimap.classList.remove('hidden');
520
+ }
521
+ }
522
+ on(els.minimapClose, 'click', (e) => {
523
+ e.stopPropagation();
524
+ state.minimapDismissedThisSession = true;
525
+ els.minimap.classList.add('hidden');
526
+ });
527
+ function panToMinimapPoint(clientX, clientY) {
528
+ const rect = els.minimapCanvas.getBoundingClientRect();
529
+ const x = (clientX - rect.left) / rect.width;
530
+ const y = (clientY - rect.top) / rect.height;
531
+ const totalW = state.naturalWidth * state.scale;
532
+ const totalH = state.naturalHeight * state.scale;
533
+ els.viewport.scrollLeft = Math.max(0, x * totalW - els.viewport.clientWidth / 2);
534
+ els.viewport.scrollTop = Math.max(0, y * totalH - els.viewport.clientHeight / 2);
535
+ }
536
+ let miniDragging = false;
537
+ on(els.minimapCanvas, 'mousedown', (e) => {
538
+ miniDragging = true;
539
+ panToMinimapPoint(e.clientX, e.clientY);
540
+ e.preventDefault();
541
+ });
542
+ on(doc, 'mousemove', (e) => {
543
+ if (miniDragging)
544
+ panToMinimapPoint(e.clientX, e.clientY);
545
+ });
546
+ on(doc, 'mouseup', () => {
547
+ miniDragging = false;
548
+ });
549
+ on(els.viewport, 'scroll', updateMinimapRect);
550
+ let resizeTimer = null;
551
+ function handleResize() {
552
+ if (resizeTimer)
553
+ clearTimeout(resizeTimer);
554
+ resizeTimer = setTimeout(() => {
555
+ resizeTimer = null;
556
+ reapplyActiveFit();
557
+ updateMinimapRect();
558
+ updateMinimapVisibility();
559
+ repositionChrome();
560
+ }, 50);
561
+ }
562
+ on(win, 'resize', handleResize);
563
+ if (typeof ResizeObserver !== 'undefined') {
564
+ const ro = new ResizeObserver(handleResize);
565
+ ro.observe(els.viewport);
566
+ cleanups.push(() => ro.disconnect());
567
+ }
568
+ // ===== Wheel zoom =====
569
+ on(els.viewport, 'wheel', (e) => {
570
+ if (!(e.ctrlKey || e.metaKey))
571
+ return;
572
+ e.preventDefault();
573
+ state.activeFit = 'manual';
574
+ state.isDirty = true;
575
+ const rect = els.viewport.getBoundingClientRect();
576
+ const anchorX = e.clientX - rect.left;
577
+ const anchorY = e.clientY - rect.top;
578
+ const factor = Math.exp(-e.deltaY * 0.01);
579
+ setScale(state.scale * factor, anchorX, anchorY);
580
+ }, { passive: false });
581
+ // ===== Spacebar pan + keyboard fit presets =====
582
+ let spaceDown = false;
583
+ let dragOrigin = null;
584
+ function isInputFocus(target) {
585
+ if (!target)
586
+ return false;
587
+ const el = target;
588
+ if (!el.tagName)
589
+ return false;
590
+ const t = el.tagName.toUpperCase();
591
+ return t === 'INPUT' || t === 'TEXTAREA' || el.isContentEditable;
592
+ }
593
+ on(doc, 'keydown', (e) => {
594
+ if (isInputFocus(e.target))
595
+ return;
596
+ if (e.key === ' ' && !spaceDown) {
597
+ spaceDown = true;
598
+ els.viewport.style.cursor = 'grab';
599
+ e.preventDefault();
600
+ }
601
+ else if (e.key === '1') {
602
+ fitPage();
603
+ }
604
+ else if (e.key === '2') {
605
+ actualSize();
606
+ }
607
+ else if (e.key === '3') {
608
+ fitWidth();
609
+ }
610
+ else if (e.key === '0') {
611
+ fitPage();
612
+ }
613
+ });
614
+ on(doc, 'keyup', (e) => {
615
+ if (e.key === ' ') {
616
+ spaceDown = false;
617
+ els.viewport.style.cursor = '';
618
+ }
619
+ });
620
+ on(els.viewport, 'mousedown', (e) => {
621
+ if (!spaceDown)
622
+ return;
623
+ state.isDirty = true;
624
+ dragOrigin = {
625
+ x: e.clientX,
626
+ y: e.clientY,
627
+ sx: els.viewport.scrollLeft,
628
+ sy: els.viewport.scrollTop,
629
+ };
630
+ els.viewport.style.cursor = 'grabbing';
631
+ e.preventDefault();
632
+ });
633
+ on(doc, 'mousemove', (e) => {
634
+ if (!dragOrigin)
635
+ return;
636
+ els.viewport.scrollLeft = dragOrigin.sx - (e.clientX - dragOrigin.x);
637
+ els.viewport.scrollTop = dragOrigin.sy - (e.clientY - dragOrigin.y);
638
+ });
639
+ on(doc, 'mouseup', () => {
640
+ if (dragOrigin) {
641
+ dragOrigin = null;
642
+ els.viewport.style.cursor = spaceDown ? 'grab' : '';
643
+ }
644
+ });
645
+ // ===== Toolbar buttons =====
646
+ on(els.zoomOut, 'click', () => {
647
+ state.activeFit = 'manual';
648
+ state.isDirty = true;
649
+ setScale(state.scale / 1.1);
650
+ });
651
+ on(els.zoomIn, 'click', () => {
652
+ state.activeFit = 'manual';
653
+ state.isDirty = true;
654
+ setScale(state.scale * 1.1);
655
+ });
656
+ on(els.zoomReset, 'click', actualSize);
657
+ on(els.fitWidth, 'click', fitWidth);
658
+ on(els.fitPage, 'click', fitPage);
659
+ // ===== Menu state helpers =====
660
+ function closeSubMenus() {
661
+ els.formatMenu.hidden = true;
662
+ els.themeMenu.hidden = true;
663
+ els.nowPicker.hidden = true;
664
+ els.linksMenu.hidden = true;
665
+ }
666
+ function closeAllMenus() {
667
+ els.moreMenu.hidden = true;
668
+ closeSubMenus();
669
+ }
670
+ /**
671
+ * Keep a just-opened flyout (more-menu, a sub-dropdown, or the
672
+ * calendar) inside the preview root + gutters. `defaultSide` is the
673
+ * edge the panel is anchored to in CSS; if opening that way would
674
+ * spill past the opposite gutter the panel is flipped, then any
675
+ * residual horizontal overflow is nudged back with a transform and
676
+ * excess height is capped with an internal scroll.
677
+ *
678
+ * `clampVertical` is off for the more-menu so capping its height
679
+ * never clips the calendar that pops out of it.
680
+ */
681
+ function placeFlyout(panel, defaultSide, clampVertical = true) {
682
+ panel.classList.remove('flip');
683
+ panel.style.maxHeight = '';
684
+ panel.style.overflowY = '';
685
+ panel.style.transform = '';
686
+ const root = rootEl.getBoundingClientRect();
687
+ // Skip when the host hasn't laid out yet (e.g. happy-dom tests).
688
+ if (root.width === 0 && root.height === 0)
689
+ return;
690
+ const g = getGutter();
691
+ let box = panel.getBoundingClientRect();
692
+ const fitsWidth = box.width <= root.width - 2 * g;
693
+ if (fitsWidth && defaultSide === 'left' && box.right > root.right - g) {
694
+ panel.classList.add('flip');
695
+ }
696
+ else if (fitsWidth && defaultSide === 'right' && box.left < root.left + g) {
697
+ panel.classList.add('flip');
698
+ }
699
+ box = panel.getBoundingClientRect();
700
+ let dx = 0;
701
+ if (box.right > root.right - g)
702
+ dx = root.right - g - box.right;
703
+ if (box.left + dx < root.left + g)
704
+ dx = root.left + g - box.left;
705
+ if (dx !== 0)
706
+ panel.style.transform = `translateX(${Math.round(dx)}px)`;
707
+ if (clampVertical) {
708
+ box = panel.getBoundingClientRect();
709
+ const avail = root.bottom - g - box.top;
710
+ if (box.height > avail && avail > 80) {
711
+ panel.style.maxHeight = `${Math.round(avail)}px`;
712
+ panel.style.overflowY = 'auto';
713
+ }
714
+ }
715
+ }
716
+ on(doc, 'click', closeAllMenus);
717
+ on(els.moreMenu, 'click', (e) => e.stopPropagation());
718
+ // ===== More-menu toggle =====
719
+ on(els.moreToggle, 'click', (e) => {
720
+ e.stopPropagation();
721
+ const opening = els.moreMenu.hidden;
722
+ closeAllMenus();
723
+ if (opening) {
724
+ els.moreMenu.hidden = false;
725
+ placeFlyout(els.moreMenu, 'right', /* clampVertical */ false);
726
+ }
727
+ });
728
+ // ===== Format dropdown =====
729
+ on(els.formatToggle, 'click', (e) => {
730
+ e.stopPropagation();
731
+ const opening = els.formatMenu.hidden;
732
+ closeSubMenus();
733
+ if (opening) {
734
+ els.formatMenu.hidden = false;
735
+ placeFlyout(els.formatMenu, 'left');
736
+ }
737
+ });
738
+ on(els.formatMenu, 'click', (e) => {
739
+ e.stopPropagation();
740
+ const btn = e.target.closest('.format-opt');
741
+ if (!btn)
742
+ return;
743
+ const value = btn.getAttribute('data-value');
744
+ if (!value)
745
+ return;
746
+ state.export.format = value;
747
+ refreshFormatToggle();
748
+ els.formatMenu.hidden = true;
749
+ });
750
+ function refreshFormatToggle() {
751
+ els.formatToggle.textContent = `${state.export.format.toUpperCase()} \u25be`;
752
+ for (const btn of Array.from(els.formatMenu.querySelectorAll('.format-opt'))) {
753
+ btn.setAttribute('data-active', (btn.getAttribute('data-value') === state.export.format).toString());
754
+ }
755
+ }
756
+ // ===== Copy / export actions =====
757
+ on(els.copyAction, 'click', () => {
758
+ closeAllMenus();
759
+ void handleExportAction(`copy-${state.export.format}`);
760
+ });
761
+ on(els.exportAction, 'click', () => {
762
+ closeAllMenus();
763
+ void handleExportAction(`save-${state.export.format}`);
764
+ });
765
+ async function handleExportAction(action) {
766
+ if (!state.svgString)
767
+ return;
768
+ try {
769
+ if (action === 'save-svg') {
770
+ options.onSave?.({ format: 'svg', body: state.svgString });
771
+ }
772
+ else if (action === 'copy-svg') {
773
+ try {
774
+ await win.navigator.clipboard.writeText(state.svgString);
775
+ }
776
+ catch (err) {
777
+ options.onFatal?.(`Copy SVG failed: ${err.message}`);
778
+ }
779
+ }
780
+ else if (action === 'save-png') {
781
+ const blob = await rasterizePng();
782
+ const buf = await blob.arrayBuffer();
783
+ options.onSave?.({ format: 'png', body: new Uint8Array(buf) });
784
+ }
785
+ else if (action === 'copy-png') {
786
+ const blob = await rasterizePng();
787
+ const ClipboardItemCtor = win.ClipboardItem;
788
+ if (typeof ClipboardItemCtor === 'undefined' || !win.navigator.clipboard?.write) {
789
+ const buf = await blob.arrayBuffer();
790
+ options.onCopyPngFallback?.(new Uint8Array(buf));
791
+ return;
792
+ }
793
+ try {
794
+ await win.navigator.clipboard.write([
795
+ new ClipboardItemCtor({ 'image/png': blob }),
796
+ ]);
797
+ }
798
+ catch {
799
+ const buf = await blob.arrayBuffer();
800
+ options.onCopyPngFallback?.(new Uint8Array(buf));
801
+ }
802
+ }
803
+ }
804
+ catch (err) {
805
+ options.onFatal?.(`PNG render failed: ${err.message}`);
806
+ }
807
+ }
808
+ function rasterizePng() {
809
+ return new Promise((resolve, reject) => {
810
+ if (!state.svgString) {
811
+ reject(new Error('No diagram to rasterize.'));
812
+ return;
813
+ }
814
+ const blob = new Blob([state.svgString], { type: 'image/svg+xml;charset=utf-8' });
815
+ const url = win.URL.createObjectURL(blob);
816
+ const img = new win.Image();
817
+ img.onload = () => {
818
+ try {
819
+ const dpr = win.devicePixelRatio || 1;
820
+ const w = state.naturalWidth || img.naturalWidth || 800;
821
+ const h = state.naturalHeight || img.naturalHeight || 600;
822
+ const canvas = doc.createElement('canvas');
823
+ canvas.width = Math.max(1, Math.round(w * dpr));
824
+ canvas.height = Math.max(1, Math.round(h * dpr));
825
+ const ctx = canvas.getContext('2d');
826
+ if (!ctx)
827
+ throw new Error('2D canvas context unavailable.');
828
+ ctx.scale(dpr, dpr);
829
+ ctx.drawImage(img, 0, 0, w, h);
830
+ win.URL.revokeObjectURL(url);
831
+ canvas.toBlob((out) => {
832
+ if (out)
833
+ resolve(out);
834
+ else
835
+ reject(new Error('canvas.toBlob returned null.'));
836
+ }, 'image/png');
837
+ }
838
+ catch (err) {
839
+ win.URL.revokeObjectURL(url);
840
+ reject(err);
841
+ }
842
+ };
843
+ img.onerror = () => {
844
+ win.URL.revokeObjectURL(url);
845
+ reject(new Error('Failed to load SVG into <img> for rasterization.'));
846
+ };
847
+ img.src = url;
848
+ });
849
+ }
850
+ // ===== Theme dropdown =====
851
+ function buildThemeMenu(themes) {
852
+ els.themeMenu.innerHTML = '';
853
+ const autoLi = doc.createElement('li');
854
+ const autoBtn = doc.createElement('button');
855
+ autoBtn.className = 'btn theme-opt';
856
+ autoBtn.setAttribute('data-value', 'auto');
857
+ autoBtn.textContent = 'Auto';
858
+ autoLi.appendChild(autoBtn);
859
+ els.themeMenu.appendChild(autoLi);
860
+ for (const theme of themes) {
861
+ const li = doc.createElement('li');
862
+ const btn = doc.createElement('button');
863
+ btn.className = 'btn theme-opt';
864
+ btn.setAttribute('data-value', theme);
865
+ const chip = doc.createElement('code');
866
+ chip.className = 'code-chip';
867
+ chip.textContent = theme;
868
+ btn.appendChild(chip);
869
+ li.appendChild(btn);
870
+ els.themeMenu.appendChild(li);
871
+ }
872
+ refreshThemeToggle();
873
+ }
874
+ on(els.themeToggle, 'click', (e) => {
875
+ e.stopPropagation();
876
+ const opening = els.themeMenu.hidden;
877
+ closeSubMenus();
878
+ if (opening) {
879
+ els.themeMenu.hidden = false;
880
+ placeFlyout(els.themeMenu, 'left');
881
+ }
882
+ });
883
+ on(els.themeMenu, 'click', (e) => {
884
+ e.stopPropagation();
885
+ const btn = e.target.closest('.theme-opt');
886
+ if (!btn)
887
+ return;
888
+ const value = btn.getAttribute('data-value');
889
+ if (!value)
890
+ return;
891
+ state.view.theme = value;
892
+ state.view.overridden.theme = true;
893
+ refreshThemeToggle();
894
+ els.themeMenu.hidden = true;
895
+ postViewOverrides();
896
+ });
897
+ function refreshThemeToggle() {
898
+ if (state.view.theme === 'auto') {
899
+ els.themeToggle.textContent = `Auto \u25be`;
900
+ }
901
+ else {
902
+ els.themeToggle.innerHTML = '';
903
+ const chip = doc.createElement('code');
904
+ chip.className = 'code-chip';
905
+ chip.textContent = state.view.theme;
906
+ els.themeToggle.appendChild(chip);
907
+ els.themeToggle.appendChild(doc.createTextNode(` \u25be`));
908
+ }
909
+ for (const btn of Array.from(els.themeMenu.querySelectorAll('.theme-opt'))) {
910
+ btn.setAttribute('data-active', (btn.getAttribute('data-value') === state.view.theme).toString());
911
+ }
912
+ }
913
+ // ===== Now picker (calendar) =====
914
+ let calendarYear = new Date().getFullYear();
915
+ let calendarMonth = new Date().getMonth();
916
+ function buildCalendar() {
917
+ const picker = els.nowPicker;
918
+ picker.innerHTML = '';
919
+ const year = calendarYear;
920
+ const month = calendarMonth;
921
+ const nav = doc.createElement('div');
922
+ nav.className = 'cal-nav';
923
+ const prevBtn = doc.createElement('button');
924
+ prevBtn.className = 'btn cal-nav-btn';
925
+ prevBtn.textContent = '\u2039';
926
+ prevBtn.addEventListener('click', (e) => {
927
+ e.stopPropagation();
928
+ calendarMonth--;
929
+ if (calendarMonth < 0) {
930
+ calendarMonth = 11;
931
+ calendarYear--;
932
+ }
933
+ buildCalendar();
934
+ });
935
+ const heading = doc.createElement('span');
936
+ heading.className = 'cal-heading';
937
+ heading.textContent = new Intl.DateTimeFormat(state.locale, {
938
+ month: 'long',
939
+ year: 'numeric',
940
+ }).format(new Date(year, month, 1));
941
+ const nextBtn = doc.createElement('button');
942
+ nextBtn.className = 'btn cal-nav-btn';
943
+ nextBtn.textContent = '\u203a';
944
+ nextBtn.addEventListener('click', (e) => {
945
+ e.stopPropagation();
946
+ calendarMonth++;
947
+ if (calendarMonth > 11) {
948
+ calendarMonth = 0;
949
+ calendarYear++;
950
+ }
951
+ buildCalendar();
952
+ });
953
+ nav.appendChild(prevBtn);
954
+ nav.appendChild(heading);
955
+ nav.appendChild(nextBtn);
956
+ picker.appendChild(nav);
957
+ const grid = doc.createElement('div');
958
+ grid.className = 'cal-grid';
959
+ for (const d of ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']) {
960
+ const cell = doc.createElement('span');
961
+ cell.className = 'cal-dow';
962
+ cell.textContent = d;
963
+ grid.appendChild(cell);
964
+ }
965
+ const firstDow = new Date(year, month, 1).getDay();
966
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
967
+ for (let i = 0; i < firstDow; i++) {
968
+ const empty = doc.createElement('span');
969
+ empty.className = 'cal-empty';
970
+ grid.appendChild(empty);
971
+ }
972
+ const now = new Date();
973
+ const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
974
+ for (let d = 1; d <= daysInMonth; d++) {
975
+ const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
976
+ const btn = doc.createElement('button');
977
+ btn.className = 'btn cal-day';
978
+ btn.textContent = String(d);
979
+ if (dateStr === todayStr)
980
+ btn.classList.add('is-today');
981
+ if (state.view.now === dateStr)
982
+ btn.classList.add('is-selected');
983
+ btn.addEventListener('click', (e) => {
984
+ e.stopPropagation();
985
+ applyNow(dateStr);
986
+ });
987
+ grid.appendChild(btn);
988
+ }
989
+ picker.appendChild(grid);
990
+ const footer = doc.createElement('div');
991
+ footer.className = 'cal-footer';
992
+ const todayBtnEl = doc.createElement('button');
993
+ todayBtnEl.className = 'btn cal-footer-btn';
994
+ todayBtnEl.textContent = 'Today';
995
+ todayBtnEl.addEventListener('click', (e) => {
996
+ e.stopPropagation();
997
+ applyNow('today');
998
+ });
999
+ const noneBtnEl = doc.createElement('button');
1000
+ noneBtnEl.className = 'btn cal-footer-btn';
1001
+ noneBtnEl.textContent = 'None';
1002
+ noneBtnEl.addEventListener('click', (e) => {
1003
+ e.stopPropagation();
1004
+ applyNow('hide');
1005
+ });
1006
+ footer.appendChild(todayBtnEl);
1007
+ footer.appendChild(noneBtnEl);
1008
+ picker.appendChild(footer);
1009
+ }
1010
+ function applyNow(value) {
1011
+ state.view.now = value;
1012
+ state.view.overridden.now = true;
1013
+ refreshNowToggle();
1014
+ els.nowPicker.hidden = true;
1015
+ postViewOverrides();
1016
+ }
1017
+ on(els.nowToggle, 'click', (e) => {
1018
+ e.stopPropagation();
1019
+ const opening = els.nowPicker.hidden;
1020
+ closeSubMenus();
1021
+ if (opening) {
1022
+ buildCalendar();
1023
+ els.nowPicker.hidden = false;
1024
+ placeFlyout(els.nowPicker, 'left');
1025
+ }
1026
+ });
1027
+ function refreshNowToggle() {
1028
+ let label;
1029
+ if (state.view.now === 'today') {
1030
+ label = 'Today';
1031
+ }
1032
+ else if (state.view.now === 'hide') {
1033
+ label = 'None';
1034
+ }
1035
+ else {
1036
+ try {
1037
+ label = new Intl.DateTimeFormat(state.locale, { dateStyle: 'medium' }).format(new Date(`${state.view.now}T00:00:00`));
1038
+ }
1039
+ catch {
1040
+ label = state.view.now;
1041
+ }
1042
+ }
1043
+ els.nowLabel.textContent = label;
1044
+ }
1045
+ // ===== Show-links dropdown =====
1046
+ on(els.linksToggle, 'click', (e) => {
1047
+ e.stopPropagation();
1048
+ const opening = els.linksMenu.hidden;
1049
+ closeSubMenus();
1050
+ if (opening) {
1051
+ els.linksMenu.hidden = false;
1052
+ placeFlyout(els.linksMenu, 'left');
1053
+ }
1054
+ });
1055
+ on(els.linksMenu, 'click', (e) => {
1056
+ e.stopPropagation();
1057
+ const btn = e.target.closest('.links-opt');
1058
+ if (!btn)
1059
+ return;
1060
+ const value = btn.getAttribute('data-value');
1061
+ if (value === null)
1062
+ return;
1063
+ state.view.showLinks = value === 'true';
1064
+ state.view.overridden.showLinks = true;
1065
+ refreshLinksToggle();
1066
+ els.linksMenu.hidden = true;
1067
+ postViewOverrides();
1068
+ });
1069
+ function refreshLinksToggle() {
1070
+ els.linksToggle.textContent = `${state.view.showLinks ? 'Yes' : 'No'} \u25be`;
1071
+ for (const btn of Array.from(els.linksMenu.querySelectorAll('.links-opt'))) {
1072
+ btn.setAttribute('data-active', (btn.getAttribute('data-value') === (state.view.showLinks ? 'true' : 'false')).toString());
1073
+ }
1074
+ }
1075
+ // ===== View-options helpers =====
1076
+ function postViewOverrides() {
1077
+ const overrides = {};
1078
+ if (state.view.overridden.theme)
1079
+ overrides.theme = state.view.theme;
1080
+ if (state.view.overridden.now)
1081
+ overrides.now = state.view.now;
1082
+ if (state.view.overridden.showLinks)
1083
+ overrides.showLinks = state.view.showLinks;
1084
+ options.onViewOptions?.(overrides);
1085
+ }
1086
+ function refreshAll() {
1087
+ refreshThemeToggle();
1088
+ refreshNowToggle();
1089
+ refreshLinksToggle();
1090
+ refreshFormatToggle();
1091
+ }
1092
+ // ===== Theme control visibility =====
1093
+ if (options.themeControl === 'hide') {
1094
+ const themeRow = els.themeMenu.closest('.theme-control-row');
1095
+ if (themeRow)
1096
+ themeRow.style.display = 'none';
1097
+ }
1098
+ // ===== Apply initial theme menu + baselines =====
1099
+ buildThemeMenu(options.availableThemes ?? ['light', 'dark', 'grayscale']);
1100
+ if (options.viewBaseline) {
1101
+ applyBaseline(state, options.viewBaseline, true);
1102
+ }
1103
+ refreshAll();
1104
+ updateMinimapVisibility();
1105
+ // Anchor the toolbar explicitly (default: upper-right) so resize
1106
+ // shifts it left rather than the CSS `right` anchor squishing it.
1107
+ repositionChrome();
1108
+ // ===== Imperative API =====
1109
+ return {
1110
+ setSvg,
1111
+ setDiagnostics: showDiagnosticsMode,
1112
+ setFatal: showFatal,
1113
+ setViewBaseline(baseline, resetOverrides = false) {
1114
+ applyBaseline(state, baseline, resetOverrides);
1115
+ refreshAll();
1116
+ },
1117
+ setDefaultFit(fit) {
1118
+ state.defaultFit = fit;
1119
+ },
1120
+ setShowMinimap(show) {
1121
+ state.showMinimap = show;
1122
+ state.minimapDismissedThisSession = false;
1123
+ updateMinimapVisibility();
1124
+ },
1125
+ setMode(mode) {
1126
+ const resolved = mode === 'system' ? resolveSystemMode() : mode;
1127
+ applyMode(resolved);
1128
+ },
1129
+ setAvailableThemes(themes) {
1130
+ buildThemeMenu(themes);
1131
+ },
1132
+ setLocale(locale) {
1133
+ state.locale = locale;
1134
+ refreshNowToggle();
1135
+ },
1136
+ fitPage,
1137
+ fitWidth,
1138
+ actualSize,
1139
+ getZoom() {
1140
+ return state.scale;
1141
+ },
1142
+ setZoom(scale) {
1143
+ state.activeFit = 'manual';
1144
+ state.isDirty = true;
1145
+ setScale(scale);
1146
+ },
1147
+ dispose() {
1148
+ if (fadeTimer)
1149
+ clearTimeout(fadeTimer);
1150
+ if (resizeTimer)
1151
+ clearTimeout(resizeTimer);
1152
+ for (const c of cleanups)
1153
+ c();
1154
+ cleanups.length = 0;
1155
+ rootEl.classList.remove('nl-preview-root');
1156
+ rootEl.removeAttribute('data-nl-mode');
1157
+ rootEl.innerHTML = '';
1158
+ },
1159
+ };
1160
+ }
1161
+ function applyBaseline(state, baseline, resetOverrides) {
1162
+ if (baseline.theme && !state.view.overridden.theme) {
1163
+ state.view.theme = baseline.theme;
1164
+ }
1165
+ if (baseline.now !== undefined && !state.view.overridden.now) {
1166
+ if (baseline.now === 'none' || baseline.now === 'hide') {
1167
+ state.view.now = 'hide';
1168
+ }
1169
+ else if (typeof baseline.now === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(baseline.now)) {
1170
+ state.view.now = baseline.now;
1171
+ }
1172
+ else {
1173
+ state.view.now = 'today';
1174
+ }
1175
+ }
1176
+ if (baseline.showLinks !== undefined && !state.view.overridden.showLinks) {
1177
+ state.view.showLinks = baseline.showLinks !== false;
1178
+ }
1179
+ if (resetOverrides) {
1180
+ state.view.overridden = { theme: false, now: false, showLinks: false };
1181
+ }
1182
+ }
1183
+ /**
1184
+ * Test-only helper. Clears the module-level latch that prevents the
1185
+ * stylesheet from being mounted twice. Vitest reuses module instances
1186
+ * across tests, so without this every test after the first one would
1187
+ * skip writing the stylesheet into the test document.
1188
+ */
1189
+ export function __resetPreviewShellStylesheetForTests() {
1190
+ stylesheetMounted = false;
1191
+ }
1192
+ //# sourceMappingURL=mount.js.map