@barzhsieh/nuxt-content-mermaid 3.0.2 → 3.0.3

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.
Files changed (29) hide show
  1. package/dist/module.d.mts +18 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +20 -1
  4. package/dist/runtime/built-in-renderer/BuiltInRenderer.vue +188 -7
  5. package/dist/runtime/components/MermaidExpandOverlay.d.vue.ts +4 -0
  6. package/dist/runtime/components/MermaidExpandOverlay.vue +17 -4
  7. package/dist/runtime/components/MermaidExpandOverlay.vue.d.ts +4 -0
  8. package/dist/runtime/components/MermaidFullscreenPresentation.d.vue.ts +3 -0
  9. package/dist/runtime/components/MermaidFullscreenPresentation.vue +6 -2
  10. package/dist/runtime/components/MermaidFullscreenPresentation.vue.d.ts +3 -0
  11. package/dist/runtime/components/MermaidZoomToolbar.d.vue.ts +2 -2
  12. package/dist/runtime/components/MermaidZoomToolbar.vue +10 -6
  13. package/dist/runtime/components/MermaidZoomToolbar.vue.d.ts +2 -2
  14. package/dist/runtime/components/icons/IconDownload.d.vue.ts +8 -0
  15. package/dist/runtime/components/icons/IconDownload.vue +28 -0
  16. package/dist/runtime/components/icons/IconDownload.vue.d.ts +8 -0
  17. package/dist/runtime/composables/useMermaidExpand.d.ts +2 -0
  18. package/dist/runtime/composables/useMermaidExpand.js +68 -1
  19. package/dist/runtime/composables/useMermaidFullscreen.d.ts +1 -0
  20. package/dist/runtime/composables/useMermaidFullscreen.js +27 -1
  21. package/dist/runtime/configuration/module.js +7 -2
  22. package/dist/runtime/constants.d.ts +17 -0
  23. package/dist/runtime/constants.js +34 -1
  24. package/dist/runtime/mermaid-rendering.js +11 -9
  25. package/dist/runtime/png-rasterizer.d.ts +7 -0
  26. package/dist/runtime/png-rasterizer.js +145 -0
  27. package/dist/runtime/svg-download.d.ts +18 -0
  28. package/dist/runtime/svg-download.js +136 -0
  29. package/package.json +7 -3
package/dist/module.d.mts CHANGED
@@ -9,11 +9,29 @@ type MermaidToolbarButtons = {
9
9
  expand?: boolean
10
10
  }
11
11
 
12
+ type MermaidToolbarLabels = {
13
+ copy?: string
14
+ copied?: string
15
+ copyFailed?: string
16
+ expand?: string
17
+ collapse?: string
18
+ minimize?: string
19
+ enterFullscreen?: string
20
+ exitFullscreen?: string
21
+ zoomIn?: string
22
+ zoomOut?: string
23
+ resetZoom?: string
24
+ download?: string
25
+ downloadSvg?: string
26
+ downloadPng?: string
27
+ }
28
+
12
29
  type MermaidToolbarOptions = {
13
30
  title?: string
14
31
  fontSize?: string | number
15
32
  fullscreenToolbarScale?: number
16
33
  buttons?: MermaidToolbarButtons
34
+ labels?: MermaidToolbarLabels
17
35
  }
18
36
 
19
37
  declare module '#app' {
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^4.1.0"
6
6
  },
7
- "version": "3.0.2",
7
+ "version": "3.0.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,9 +1,11 @@
1
1
  import { defineNuxtModule, createResolver, addVitePlugin, addPlugin, addComponent, addImports, addTypeTemplate } from '@nuxt/kit';
2
+ import { relative } from 'node:path';
2
3
  import { cloneOwnedData, mergeByPresence, assertStrictData, ContentMermaidConfigurationError } from '../dist/runtime/configuration/core.js';
3
4
  import { resolveModuleConfiguration } from '../dist/runtime/configuration/module.js';
4
5
  import destr from 'destr';
5
6
  import { parse, stringify } from 'yaml';
6
7
  import { isRecord, isNonEmptyRecord } from '../dist/runtime/utils/is.js';
8
+ import { TOOLBAR_LABEL_KEYS } from '../dist/runtime/constants.js';
7
9
 
8
10
  const UNSAFE_INLINE_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
9
11
  function inspectMermaidFrontmatter(rawCode, newline) {
@@ -305,7 +307,7 @@ const MARKDOWN_METADATA_PHASE = {
305
307
  };
306
308
  const UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
307
309
  const INLINE_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set(["title", "config", "toolbar"]);
308
- const TOOLBAR_KEYS = /* @__PURE__ */ new Set(["title", "fontSize", "fullscreenToolbarScale", "buttons"]);
310
+ const TOOLBAR_KEYS = /* @__PURE__ */ new Set(["title", "fontSize", "fullscreenToolbarScale", "buttons", "labels"]);
309
311
  const TOOLBAR_BUTTON_KEYS = /* @__PURE__ */ new Set(["copy", "fullscreen", "expand"]);
310
312
  function hasOwn(value, key) {
311
313
  return Object.prototype.hasOwnProperty.call(value, key);
@@ -363,6 +365,14 @@ function normalizeToolbar(value) {
363
365
  if (button !== void 0 && typeof button !== "boolean") return null;
364
366
  }
365
367
  }
368
+ const labels = valueAt(toolbar, "labels");
369
+ if (labels !== void 0) {
370
+ if (!isPlainRecord(labels) || !hasOnlyKeys(labels, new Set(TOOLBAR_LABEL_KEYS))) return null;
371
+ for (const key of TOOLBAR_LABEL_KEYS) {
372
+ const label = valueAt(labels, key);
373
+ if (label !== void 0 && typeof label !== "string") return null;
374
+ }
375
+ }
366
376
  return toolbar;
367
377
  }
368
378
  function resolveDiagramMermaidConfig(value) {
@@ -634,7 +644,16 @@ const module$1 = defineNuxtModule({
634
644
  publicRuntimeConfig.contentMermaid = resolved.runtimeOptions;
635
645
  const resolver = createResolver(import.meta.url);
636
646
  const runtimeDir = resolver.resolve("./runtime");
647
+ const pngRasterizerPath = resolver.resolve(runtimeDir, "png-rasterizer");
637
648
  const baseMermaidComponentName = "Mermaid";
649
+ nuxt.hook("build:manifest", async (manifest) => {
650
+ const resolvedPngRasterizerPath = await resolver.resolvePath(pngRasterizerPath);
651
+ const manifestKey = relative(nuxt.options.rootDir, resolvedPngRasterizerPath).replaceAll("\\", "/");
652
+ const pngRasterizerEntry = manifest[manifestKey];
653
+ if (pngRasterizerEntry?.src === manifestKey && pngRasterizerEntry.isDynamicEntry) {
654
+ pngRasterizerEntry.prefetch = false;
655
+ }
656
+ });
638
657
  nuxt.options.css ||= [];
639
658
  nuxt.options.css.push(resolver.resolve("./runtime/styles.css"));
640
659
  addVitePlugin(() => ({
@@ -8,12 +8,18 @@ import {
8
8
  computed,
9
9
  watch,
10
10
  shallowRef,
11
+ useId,
11
12
  useTemplateRef
12
13
  } from "vue";
13
14
  import { defu } from "defu";
14
15
  import { materializeMermaidConfigForInvocation, resolveMermaidTheme } from "../mermaid-config";
15
16
  import { createMermaidRenderer } from "../mermaid-rendering";
16
17
  import { getRuntimeMermaidSnapshot } from "../runtime-snapshot";
18
+ import {
19
+ createSafeStandaloneSvgClone,
20
+ downloadBlob,
21
+ downloadStandaloneSvg
22
+ } from "../svg-download";
17
23
  import { parseSizeToPx, isRecord } from "../utils";
18
24
  import { useMermaidTheme } from "../composables/useMermaidTheme";
19
25
  import { useMermaidCursors } from "../composables/useMermaidCursors";
@@ -25,7 +31,13 @@ import IconFullScreenExit from "../components/icons/iconFullScreenExit.vue";
25
31
  import MermaidExpandOverlay from "../components/MermaidExpandOverlay.vue";
26
32
  import IconExpand from "../components/icons/IconExpand.vue";
27
33
  import IconCollapse from "../components/icons/IconCollapse.vue";
34
+ import IconDownload from "../components/icons/IconDownload.vue";
28
35
  import MermaidFullscreenPresentation from "../components/MermaidFullscreenPresentation.vue";
36
+ import { DEFAULT_TOOLBAR_LABELS } from "../constants";
37
+ let pngRasterizerModulePromise;
38
+ function loadPngRasterizer() {
39
+ return pngRasterizerModulePromise ??= import("../png-rasterizer");
40
+ }
29
41
  const props = defineProps({
30
42
  toolbar: { type: Object, required: false },
31
43
  code: { type: String, required: false },
@@ -58,6 +70,14 @@ const { currentTheme: manualThemeMode } = useMermaidTheme();
58
70
  const mermaidBlock = useTemplateRef("mermaidBlock");
59
71
  const mermaidWrapper = useTemplateRef("mermaidWrapper");
60
72
  const mermaidContainer = useTemplateRef("mermaidContainer");
73
+ const downloadRoot = useTemplateRef("downloadRoot");
74
+ const downloadTrigger = useTemplateRef("downloadTrigger");
75
+ const downloadDisclosure = useTemplateRef("downloadDisclosure");
76
+ const downloadSvgButton = useTemplateRef("downloadSvgButton");
77
+ const expandButton = useTemplateRef("expandButton");
78
+ const fullscreenButton = useTemplateRef("fullscreenButton");
79
+ const getExpandFocusTarget = () => expandButton.value;
80
+ const getFullscreenFocusTarget = () => fullscreenButton.value;
61
81
  const fullscreenPresentation = useTemplateRef("fullscreenPresentation");
62
82
  const isFullscreenSupported = ref(false);
63
83
  const isFullscreen = ref(false);
@@ -76,6 +96,11 @@ const hasRenderedOnce = ref(false);
76
96
  const isLoading = ref(false);
77
97
  const hasError = ref(false);
78
98
  const errorContent = shallowRef(null);
99
+ const committedExportSnapshot = shallowRef(null);
100
+ const isCommittedOutputSvg = ref(false);
101
+ const downloadDisclosureId = `ncm-download-${useId()}`;
102
+ const isDownloadDisclosureOpen = ref(false);
103
+ const isPngDownloadPending = ref(false);
79
104
  const decodedCode = computed(() => props.code ? decodeURIComponent(props.code) : "");
80
105
  const mermaidDefinition = ref(decodedCode.value);
81
106
  let observer = null;
@@ -86,6 +111,9 @@ const defaultFullscreenToolbarScale = runtimeToolbarDefaults?.fullscreenToolbarS
86
111
  const resolvedToolbar = computed(() => {
87
112
  return defu({}, props.toolbar ?? {}, runtimeToolbarDefaults ?? {});
88
113
  });
114
+ const toolbarLabels = computed(() => {
115
+ return defu({}, resolvedToolbar.value.labels ?? {}, DEFAULT_TOOLBAR_LABELS);
116
+ });
89
117
  const toolbarButtons = computed(() => {
90
118
  const buttons = resolvedToolbar.value.buttons;
91
119
  return isRecord(buttons) ? buttons : {};
@@ -104,13 +132,16 @@ const toolbarFontSize = computed(() => {
104
132
  return defaultToolbarFontSize;
105
133
  });
106
134
  const showCopyButton = computed(() => toolbarButtons.value.copy !== false);
135
+ const canDownloadSvg = computed(() => {
136
+ return isCommittedOutputSvg.value && committedExportSnapshot.value !== null && componentSource.value.kind !== "conflict";
137
+ });
107
138
  const copySource = computed(() => decodedCode.value || mermaidDefinition.value || "");
108
139
  const hasCopySource = computed(() => !!copySource.value);
109
140
  const copyState = ref("idle");
110
141
  const copyLabel = computed(() => {
111
- if (copyState.value === "copied") return "Copied";
112
- if (copyState.value === "error") return "Copy failed";
113
- return "Copy";
142
+ if (copyState.value === "copied") return toolbarLabels.value.copied;
143
+ if (copyState.value === "error") return toolbarLabels.value.copyFailed;
144
+ return toolbarLabels.value.copy;
114
145
  });
115
146
  let copyResetTimer = null;
116
147
  const isCopyHovered = ref(false);
@@ -120,7 +151,7 @@ const copyIcon = computed(() => {
120
151
  return IconClipboard;
121
152
  });
122
153
  const showFullscreenButton = computed(() => toolbarButtons.value.fullscreen !== false && isFullscreenSupported.value);
123
- const fullscreenLabel = computed(() => isFullscreen.value ? "Exit fullscreen" : "Enter fullscreen");
154
+ const fullscreenLabel = computed(() => isFullscreen.value ? toolbarLabels.value.exitFullscreen : toolbarLabels.value.enterFullscreen);
124
155
  const fullscreenToolbarScale = computed(() => {
125
156
  const raw = resolvedToolbar.value.fullscreenToolbarScale ?? defaultFullscreenToolbarScale;
126
157
  if (typeof raw === "number" && Number.isFinite(raw))
@@ -178,6 +209,7 @@ const iconSize = computed(() => {
178
209
  const isExpandBlocked = computed(() => !isEnabled || !expandEnabled || isLoading.value || hasError.value || isFullscreen.value);
179
210
  const expandOverlay = useTemplateRef("expandOverlay");
180
211
  const isExpandActive = ref(false);
212
+ const expandToolbarLabel = computed(() => isExpandActive.value ? toolbarLabels.value.collapse : toolbarLabels.value.expand);
181
213
  const handleExpandActiveChange = (active) => {
182
214
  isExpandActive.value = active;
183
215
  };
@@ -330,6 +362,78 @@ function getMermaidSvg() {
330
362
  const svg = mermaidContainer.value.querySelector("svg");
331
363
  return svg instanceof SVGSVGElement ? svg : null;
332
364
  }
365
+ function closeDownloadDisclosure(focusTrigger = false) {
366
+ isDownloadDisclosureOpen.value = false;
367
+ if (!focusTrigger) return;
368
+ void nextTick(() => {
369
+ if (downloadTrigger.value?.isConnected)
370
+ downloadTrigger.value.focus();
371
+ });
372
+ }
373
+ async function toggleDownloadDisclosure(event) {
374
+ if (!canDownloadSvg.value || isPngDownloadPending.value) return;
375
+ if (isDownloadDisclosureOpen.value) {
376
+ closeDownloadDisclosure();
377
+ return;
378
+ }
379
+ isDownloadDisclosureOpen.value = true;
380
+ if (event.detail === 0) {
381
+ await nextTick();
382
+ downloadSvgButton.value?.focus();
383
+ }
384
+ }
385
+ function handleDownloadFocusout(event) {
386
+ if (isPngDownloadPending.value) return;
387
+ const nextTarget = event.relatedTarget;
388
+ if (nextTarget instanceof Node && downloadDisclosure.value?.contains(nextTarget)) {
389
+ return;
390
+ }
391
+ closeDownloadDisclosure();
392
+ }
393
+ function handleDownloadEscape() {
394
+ closeDownloadDisclosure(true);
395
+ }
396
+ function handleDocumentPointerDown(event) {
397
+ const target = event.target;
398
+ if (isDownloadDisclosureOpen.value && !isPngDownloadPending.value && target instanceof Node && !downloadRoot.value?.contains(target)) {
399
+ closeDownloadDisclosure();
400
+ }
401
+ }
402
+ function downloadSvg() {
403
+ const snapshot = committedExportSnapshot.value;
404
+ if (!canDownloadSvg.value || !snapshot) return;
405
+ try {
406
+ downloadStandaloneSvg(snapshot.svg, {
407
+ filename: "mermaid-diagram.svg"
408
+ });
409
+ } finally {
410
+ closeDownloadDisclosure(true);
411
+ }
412
+ }
413
+ async function downloadPng() {
414
+ const snapshot = committedExportSnapshot.value;
415
+ if (!canDownloadSvg.value || !snapshot || isPngDownloadPending.value)
416
+ return;
417
+ isPngDownloadPending.value = true;
418
+ try {
419
+ const svg = createSafeStandaloneSvgClone(snapshot.svg);
420
+ const { rasterizePngSnapshot } = await loadPngRasterizer();
421
+ const blob = await rasterizePngSnapshot({
422
+ svg,
423
+ width: snapshot.width,
424
+ height: snapshot.height
425
+ });
426
+ downloadBlob(blob, "mermaid-diagram.png");
427
+ } catch (error) {
428
+ console.error(
429
+ "[nuxt-content-mermaid] Failed to download PNG:",
430
+ error
431
+ );
432
+ } finally {
433
+ isPngDownloadPending.value = false;
434
+ closeDownloadDisclosure(true);
435
+ }
436
+ }
333
437
  function getMermaidViewport() {
334
438
  return mermaidWrapper.value;
335
439
  }
@@ -350,6 +454,17 @@ async function renderMermaid() {
350
454
  return;
351
455
  if (outcome.status === "success") {
352
456
  hasRenderedOnce.value = true;
457
+ const committedSvg = getMermaidSvg();
458
+ const bounds = committedSvg?.getBoundingClientRect();
459
+ const isValidSvgSnapshot = committedSvg && bounds && Number.isFinite(bounds.width) && bounds.width > 0 && Number.isFinite(bounds.height) && bounds.height > 0;
460
+ isCommittedOutputSvg.value = Boolean(isValidSvgSnapshot);
461
+ if (isValidSvgSnapshot) {
462
+ committedExportSnapshot.value = {
463
+ svg: committedSvg.cloneNode(true),
464
+ width: bounds.width,
465
+ height: bounds.height
466
+ };
467
+ }
353
468
  } else if (outcome.status === "failure") {
354
469
  console.error("[nuxt-content-mermaid]", outcome.error);
355
470
  hasError.value = true;
@@ -415,6 +530,7 @@ async function copyToClipboard(text) {
415
530
  }
416
531
  onMounted(() => {
417
532
  if (!isEnabled) return;
533
+ document.addEventListener("pointerdown", handleDocumentPointerDown);
418
534
  if (!mermaidDefinition.value && mermaidContainer.value)
419
535
  mermaidDefinition.value = extractMermaidDefinition(mermaidContainer.value);
420
536
  nextTick(() => setupMermaidContainer());
@@ -423,6 +539,11 @@ onUnmounted(() => {
423
539
  requestBuiltInRender?.invalidate();
424
540
  stopLazyObservation();
425
541
  if (copyResetTimer) clearTimeout(copyResetTimer);
542
+ document.removeEventListener("pointerdown", handleDocumentPointerDown);
543
+ });
544
+ watch(canDownloadSvg, (isEligible) => {
545
+ if (!isEligible)
546
+ closeDownloadDisclosure();
426
547
  });
427
548
  watch(
428
549
  () => ({
@@ -480,12 +601,66 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
480
601
  :size="iconSize"
481
602
  />
482
603
  </button>
604
+ <div
605
+ ref="downloadRoot"
606
+ class="ncm-download"
607
+ >
608
+ <button
609
+ ref="downloadTrigger"
610
+ type="button"
611
+ class="mermaid-btn"
612
+ :title="toolbarLabels.download"
613
+ :aria-label="toolbarLabels.download"
614
+ :aria-expanded="isDownloadDisclosureOpen"
615
+ :aria-controls="downloadDisclosureId"
616
+ :disabled="!canDownloadSvg"
617
+ @click="toggleDownloadDisclosure"
618
+ >
619
+ <IconDownload :size="iconSize" />
620
+ </button>
621
+ <div
622
+ v-if="isDownloadDisclosureOpen"
623
+ :id="downloadDisclosureId"
624
+ ref="downloadDisclosure"
625
+ class="ncm-download-disclosure"
626
+ @focusout="handleDownloadFocusout"
627
+ @keydown.esc.stop.prevent="handleDownloadEscape"
628
+ >
629
+ <button
630
+ ref="downloadSvgButton"
631
+ type="button"
632
+ class="ncm-download-choice"
633
+ :title="toolbarLabels.downloadSvg"
634
+ :aria-label="toolbarLabels.downloadSvg"
635
+ @click="downloadSvg"
636
+ >
637
+ {{ toolbarLabels.downloadSvg }}
638
+ </button>
639
+ <button
640
+ type="button"
641
+ class="ncm-download-choice"
642
+ :title="toolbarLabels.downloadPng"
643
+ :aria-label="toolbarLabels.downloadPng"
644
+ :disabled="isPngDownloadPending"
645
+ :aria-busy="isPngDownloadPending || void 0"
646
+ @click="downloadPng"
647
+ >
648
+ <span
649
+ v-if="isPngDownloadPending"
650
+ class="ncm-download-spinner"
651
+ aria-hidden="true"
652
+ />
653
+ {{ toolbarLabels.downloadPng }}
654
+ </button>
655
+ </div>
656
+ </div>
483
657
  <button
484
658
  v-if="showExpandToolbarButton"
659
+ ref="expandButton"
485
660
  type="button"
486
661
  class="mermaid-btn"
487
- :title="isExpandActive ? 'Collapse' : 'Expand'"
488
- :aria-label="isExpandActive ? 'Collapse diagram' : 'Expand diagram'"
662
+ :title="expandToolbarLabel"
663
+ :aria-label="expandToolbarLabel"
489
664
  @click="toggleExpand"
490
665
  >
491
666
  <IconExpand
@@ -499,6 +674,7 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
499
674
  </button>
500
675
  <button
501
676
  v-if="showFullscreenButton"
677
+ ref="fullscreenButton"
502
678
  type="button"
503
679
  class="mermaid-btn"
504
680
  :title="fullscreenLabel"
@@ -523,6 +699,8 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
523
699
  :viewport-target="mermaidWrapper"
524
700
  :render-target="mermaidContainer"
525
701
  :icon-size="iconSize"
702
+ :labels="toolbarLabels"
703
+ :get-focus-target="getFullscreenFocusTarget"
526
704
  @active-change="handleFullscreenActiveChange"
527
705
  @supported-change="handleFullscreenSupportedChange"
528
706
  />
@@ -584,14 +762,17 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
584
762
  :blocked="isExpandBlocked"
585
763
  :get-expand-target="getMermaidSvg"
586
764
  :get-expand-viewport="getMermaidViewport"
765
+ :get-return-focus-target="getExpandFocusTarget"
587
766
  :overlay-style="cursorVariables"
588
767
  :content-style="cursorVariables"
589
768
  :icon-size="iconSize"
769
+ :labels="toolbarLabels"
770
+ :dialog-label="toolbarTitle || toolbarLabels.expand"
590
771
  @active-change="handleExpandActiveChange"
591
772
  />
592
773
  </div>
593
774
  </template>
594
775
 
595
776
  <style scoped>
596
- .mermaid-block{border:var(--ncm-border);border-radius:4px;flex-direction:column;margin:1rem 0;overflow:hidden;--ncm-toolbar-height:calc(var(--ncm-icon-size, 17px) + 20px)}.mermaid-block,.mermaid-toolbar{background-color:var(--ncm-code-bg);display:flex}.mermaid-toolbar{align-items:center;border-bottom:var(--ncm-border-bottom);border-top-left-radius:4px;border-top-right-radius:4px;justify-content:space-between;padding:.375rem .375rem .375rem .75rem;z-index:20}.mermaid-title{color:var(--ncm-text-muted);font-weight:500;letter-spacing:0;line-height:1.25;max-width:80%;overflow:hidden;padding:4px 0;text-overflow:ellipsis;white-space:nowrap}:fullscreen .mermaid-title{max-width:80%}.mermaid-actions{display:flex;gap:0;justify-self:flex-end}.mermaid-btn{align-items:center;background:var(--ncm-code-bg);border:none;border-radius:4px;color:var(--ncm-text-xmuted);cursor:pointer;display:flex;justify-content:center;line-height:1.25;padding:4px;transition:color .2s,background-color .2s}.mermaid-btn:hover{background-color:var(--ncm-code-bg-hover);color:var(--ncm-text)}.mermaid-btn:disabled{color:var(--ncm-text-xmuted);cursor:not-allowed;opacity:.6}.mermaid-wrapper{align-items:center;display:flex;justify-content:center;overflow:auto;padding:1rem;position:relative;--sb-thumb:rgba(0,0,0,.2);--sb-thumb-hover:rgba(0,0,0,.35)}.mermaid-wrapper>div{height:100%;width:100%}.mermaid-wrapper>div>:deep(svg){display:block;margin:0 auto}.mermaid-wrapper :deep(code),.mermaid-wrapper :deep(pre){height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;clip-path:inset(50%);white-space:nowrap}.mermaid:not([data-processed]){color:transparent;min-height:100px}:fullscreen .mermaid-wrapper{flex:1;overflow:hidden;touch-action:none}:fullscreen .mermaid-wrapper::-webkit-scrollbar{display:none}:fullscreen .mermaid-wrapper :deep(svg){height:100%!important;max-height:none!important;max-width:none!important;width:100%!important}:global(.dark .mermaid-wrapper){--sb-thumb:hsla(0,0%,100%,.2);--sb-thumb-hover:hsla(0,0%,100%,.35)}.mermaid-wrapper::-webkit-scrollbar{width:14px}.mermaid-wrapper::-webkit-scrollbar-track{background:hsla(0,0%,94%,.08)}:global(.dark .mermaid-wrapper::-webkit-scrollbar-track){background:hsla(0,0%,9%,.08)}.mermaid-wrapper::-webkit-scrollbar-corner{background:transparent}.mermaid-wrapper::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--sb-thumb);border:3px solid transparent;border-radius:10px}.mermaid-wrapper::-webkit-scrollbar-thumb:hover{background-color:var(--sb-thumb-hover)}.mermaid-wrapper.ncm-is-loading{align-items:center;display:flex;justify-content:center;min-height:150px}.mermaid-wrapper.ncm-expand-hidden{opacity:0;pointer-events:none}.mermaid-wrapper.ncm-expand-clickable :deep(svg){cursor:var(--ncm-cursor-expand)}.mermaid-error-default{color:#f55;font-family:monospace;padding:1rem;white-space:pre-wrap}
777
+ .mermaid-block{border:var(--ncm-border);border-radius:4px;flex-direction:column;margin:1rem 0;overflow:hidden;--ncm-toolbar-height:calc(var(--ncm-icon-size, 17px) + 20px)}.mermaid-block,.mermaid-toolbar{background-color:var(--ncm-code-bg);display:flex}.mermaid-toolbar{align-items:center;border-bottom:var(--ncm-border-bottom);border-top-left-radius:4px;border-top-right-radius:4px;justify-content:space-between;padding:.375rem .375rem .375rem .75rem;z-index:20}.mermaid-title{color:var(--ncm-text-muted);font-weight:500;letter-spacing:0;line-height:1.25;max-width:80%;overflow:hidden;padding:4px 0;text-overflow:ellipsis;white-space:nowrap}:fullscreen .mermaid-title{max-width:80%}.mermaid-actions{display:flex;gap:0;justify-self:flex-end}.ncm-download{display:flex;position:relative}.ncm-download-disclosure{background:var(--ncm-code-bg);border:var(--ncm-border);border-radius:4px;box-shadow:0 4px 12px rgba(0,0,0,.15);display:flex;flex-direction:column;min-width:-moz-max-content;min-width:max-content;padding:4px;position:absolute;right:0;top:calc(100% + 4px);z-index:10}.ncm-download-choice{align-items:center;background:transparent;border:none;border-radius:3px;color:var(--ncm-text);cursor:pointer;display:flex;font:inherit;gap:6px;padding:6px 8px;white-space:nowrap}.ncm-download-choice:focus-visible,.ncm-download-choice:hover{background:var(--ncm-code-bg-hover)}.ncm-download-choice:disabled{cursor:not-allowed;opacity:.6}.ncm-download-spinner{animation:ncm-download-spin .8s linear infinite;border:2px solid;border-radius:50%;border-right:2px solid transparent;height:12px;width:12px}@keyframes ncm-download-spin{to{transform:rotate(1turn)}}.mermaid-btn{align-items:center;background:var(--ncm-code-bg);border:none;border-radius:4px;color:var(--ncm-text-xmuted);cursor:pointer;display:flex;justify-content:center;line-height:1.25;padding:4px;transition:color .2s,background-color .2s}.mermaid-btn:hover{background-color:var(--ncm-code-bg-hover);color:var(--ncm-text)}.mermaid-btn:disabled{color:var(--ncm-text-xmuted);cursor:not-allowed;opacity:.6}.mermaid-wrapper{align-items:center;display:flex;justify-content:center;overflow:auto;padding:1rem;position:relative;--sb-thumb:rgba(0,0,0,.2);--sb-thumb-hover:rgba(0,0,0,.35)}.mermaid-wrapper>div{height:100%;width:100%}.mermaid-wrapper>div>:deep(svg){display:block;margin:0 auto}.mermaid-wrapper :deep(code),.mermaid-wrapper :deep(pre){height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;clip-path:inset(50%);white-space:nowrap}.mermaid:not([data-processed]){color:transparent;min-height:100px}:fullscreen .mermaid-wrapper{flex:1;overflow:hidden;touch-action:none}:fullscreen .mermaid-wrapper::-webkit-scrollbar{display:none}:fullscreen .mermaid-wrapper :deep(svg){height:100%!important;max-height:none!important;max-width:none!important;width:100%!important}:global(.dark .mermaid-wrapper){--sb-thumb:hsla(0,0%,100%,.2);--sb-thumb-hover:hsla(0,0%,100%,.35)}.mermaid-wrapper::-webkit-scrollbar{width:14px}.mermaid-wrapper::-webkit-scrollbar-track{background:hsla(0,0%,94%,.08)}:global(.dark .mermaid-wrapper::-webkit-scrollbar-track){background:hsla(0,0%,9%,.08)}.mermaid-wrapper::-webkit-scrollbar-corner{background:transparent}.mermaid-wrapper::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--sb-thumb);border:3px solid transparent;border-radius:10px}.mermaid-wrapper::-webkit-scrollbar-thumb:hover{background-color:var(--sb-thumb-hover)}.mermaid-wrapper.ncm-is-loading{align-items:center;display:flex;justify-content:center;min-height:150px}.mermaid-wrapper.ncm-expand-hidden{opacity:0;pointer-events:none}.mermaid-wrapper.ncm-expand-clickable :deep(svg){cursor:var(--ncm-cursor-expand)}.mermaid-error-default{color:#f55;font-family:monospace;padding:1rem;white-space:pre-wrap}
597
778
  </style>
@@ -1,13 +1,17 @@
1
1
  import type { CSSProperties } from 'vue';
2
2
  import type { ExpandOptions } from '../types/expand.js';
3
+ import type { MermaidToolbarLabels } from '../../types/mermaid.js';
3
4
  type __VLS_Props = {
4
5
  options: ExpandOptions;
5
6
  blocked: boolean;
6
7
  getExpandTarget: () => SVGElement | null;
7
8
  getExpandViewport: () => HTMLElement | null;
9
+ getReturnFocusTarget: () => HTMLElement | null;
8
10
  overlayStyle?: CSSProperties;
9
11
  contentStyle?: CSSProperties;
10
12
  iconSize?: number;
13
+ labels: Required<MermaidToolbarLabels>;
14
+ dialogLabel: string;
11
15
  };
12
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
13
17
  toggle: (event?: Event) => void;
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, watch } from "vue";
2
+ import { computed, useTemplateRef, watch } from "vue";
3
3
  import { useMermaidExpand } from "../composables/useMermaidExpand";
4
4
  import MermaidZoomToolbar from "./MermaidZoomToolbar.vue";
5
5
  const props = defineProps({
@@ -7,15 +7,19 @@ const props = defineProps({
7
7
  blocked: { type: Boolean, required: true },
8
8
  getExpandTarget: { type: Function, required: true },
9
9
  getExpandViewport: { type: Function, required: true },
10
+ getReturnFocusTarget: { type: Function, required: true },
10
11
  overlayStyle: { type: Object, required: false },
11
12
  contentStyle: { type: Object, required: false },
12
- iconSize: { type: Number, required: false }
13
+ iconSize: { type: Number, required: false },
14
+ labels: { type: Object, required: true },
15
+ dialogLabel: { type: String, required: true }
13
16
  });
14
17
  const isMac = import.meta.client ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent) : false;
15
18
  const emit = defineEmits(["active-change"]);
16
19
  const blocked = computed(() => props.blocked);
17
20
  const allowCloseButton = computed(() => props.options.invokeCloseOn?.closeButtonClick !== false);
18
21
  const targetHasMargin = computed(() => typeof props.options.margin === "number" && props.options.margin > 0);
22
+ const minimizeButton = useTemplateRef("minimizeButton");
19
23
  const {
20
24
  setExpandModal,
21
25
  setExpandTargetWrap,
@@ -39,7 +43,9 @@ const {
39
43
  getExpandTarget: props.getExpandTarget,
40
44
  getExpandViewport: props.getExpandViewport,
41
45
  expandOptions: props.options,
42
- isBlocked: blocked
46
+ isBlocked: blocked,
47
+ getInitialFocusTarget: () => minimizeButton.value,
48
+ getReturnFocusTarget: props.getReturnFocusTarget
43
49
  });
44
50
  watch(isExpandActive, (active) => emit("active-change", active), { flush: "sync", immediate: true });
45
51
  defineExpose({ toggle, openFromDiagram, endForDiagramReplacement });
@@ -51,6 +57,10 @@ defineExpose({ toggle, openFromDiagram, endForDiagramReplacement });
51
57
  v-if="isExpandActive"
52
58
  :ref="setExpandModal"
53
59
  class="ncm-expand-modal"
60
+ role="dialog"
61
+ aria-modal="true"
62
+ :aria-label="dialogLabel"
63
+ tabindex="-1"
54
64
  :style="{
55
65
  cursor
56
66
  }"
@@ -86,6 +96,7 @@ defineExpose({ toggle, openFromDiagram, endForDiagramReplacement });
86
96
  <MermaidZoomToolbar
87
97
  :scale="scale"
88
98
  :icon-size="iconSize"
99
+ :labels="labels"
89
100
  :icon-scale="0.75"
90
101
  @zoom-out="zoomOut"
91
102
  @zoom-in="zoomIn"
@@ -104,9 +115,11 @@ defineExpose({ toggle, openFromDiagram, endForDiagramReplacement });
104
115
 
105
116
  <button
106
117
  v-if="allowCloseButton"
118
+ ref="minimizeButton"
107
119
  type="button"
108
120
  class="ncm-expand-btn"
109
- aria-label="Minimize diagram"
121
+ :title="labels.minimize"
122
+ :aria-label="labels.minimize"
110
123
  @click="closeFromButton"
111
124
  @mousedown.stop
112
125
  @touchstart.stop
@@ -1,13 +1,17 @@
1
1
  import type { CSSProperties } from 'vue';
2
2
  import type { ExpandOptions } from '../types/expand.js';
3
+ import type { MermaidToolbarLabels } from '../../types/mermaid.js';
3
4
  type __VLS_Props = {
4
5
  options: ExpandOptions;
5
6
  blocked: boolean;
6
7
  getExpandTarget: () => SVGElement | null;
7
8
  getExpandViewport: () => HTMLElement | null;
9
+ getReturnFocusTarget: () => HTMLElement | null;
8
10
  overlayStyle?: CSSProperties;
9
11
  contentStyle?: CSSProperties;
10
12
  iconSize?: number;
13
+ labels: Required<MermaidToolbarLabels>;
14
+ dialogLabel: string;
11
15
  };
12
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
13
17
  toggle: (event?: Event) => void;
@@ -1,8 +1,11 @@
1
+ import type { MermaidToolbarLabels } from '../../types/mermaid.js';
1
2
  type __VLS_Props = {
2
3
  fullscreenTarget: HTMLElement | null;
3
4
  viewportTarget: HTMLElement | null;
4
5
  renderTarget: HTMLElement | null;
5
6
  iconSize?: number;
7
+ labels: Required<MermaidToolbarLabels>;
8
+ getFocusTarget: () => HTMLElement | null;
6
9
  };
7
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
8
11
  toggle: () => Promise<void>;
@@ -6,7 +6,9 @@ const props = defineProps({
6
6
  fullscreenTarget: { type: null, required: true },
7
7
  viewportTarget: { type: null, required: true },
8
8
  renderTarget: { type: null, required: true },
9
- iconSize: { type: Number, required: false }
9
+ iconSize: { type: Number, required: false },
10
+ labels: { type: Object, required: true },
11
+ getFocusTarget: { type: Function, required: true }
10
12
  });
11
13
  const emit = defineEmits(["active-change", "supported-change"]);
12
14
  const isMac = import.meta.client ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent) : false;
@@ -23,7 +25,8 @@ const {
23
25
  } = useMermaidFullscreen({
24
26
  getFullscreenTarget: () => props.fullscreenTarget,
25
27
  getViewportTarget: () => props.viewportTarget,
26
- getRenderTarget: () => props.renderTarget
28
+ getRenderTarget: () => props.renderTarget,
29
+ getFocusTarget: props.getFocusTarget
27
30
  });
28
31
  watch(isActive, (active) => emit("active-change", active), { flush: "sync", immediate: true });
29
32
  watch(isSupported, (supported) => emit("supported-change", supported), { flush: "sync", immediate: true });
@@ -36,6 +39,7 @@ defineExpose({ toggle, endForDiagramReplacement });
36
39
  variant="fullscreen"
37
40
  :scale="scale"
38
41
  :icon-size="iconSize"
42
+ :labels="labels"
39
43
  @zoom-out="zoomOut"
40
44
  @zoom-in="zoomIn"
41
45
  @reset="resetZoom"
@@ -1,8 +1,11 @@
1
+ import type { MermaidToolbarLabels } from '../../types/mermaid.js';
1
2
  type __VLS_Props = {
2
3
  fullscreenTarget: HTMLElement | null;
3
4
  viewportTarget: HTMLElement | null;
4
5
  renderTarget: HTMLElement | null;
5
6
  iconSize?: number;
7
+ labels: Required<MermaidToolbarLabels>;
8
+ getFocusTarget: () => HTMLElement | null;
6
9
  };
7
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
8
11
  toggle: () => Promise<void>;
@@ -1,9 +1,10 @@
1
+ import type { MermaidToolbarLabels } from '../../types/mermaid.js';
1
2
  type __VLS_Props = {
2
3
  scale: number;
3
4
  iconSize?: number;
4
5
  iconScale?: number;
5
6
  variant?: 'fullscreen' | 'overlay';
6
- resetLabel?: string;
7
+ labels: Required<MermaidToolbarLabels>;
7
8
  };
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
10
  reset: () => void;
@@ -17,7 +18,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
17
18
  iconSize: number;
18
19
  iconScale: number;
19
20
  variant: "fullscreen" | "overlay";
20
- resetLabel: string;
21
21
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const _default: typeof __VLS_export;
23
23
  export default _default;
@@ -7,7 +7,7 @@ const props = defineProps({
7
7
  iconSize: { type: Number, required: false, default: 18 },
8
8
  iconScale: { type: Number, required: false, default: 1 },
9
9
  variant: { type: String, required: false, default: "overlay" },
10
- resetLabel: { type: String, required: false, default: "Reset" }
10
+ labels: { type: Object, required: true }
11
11
  });
12
12
  const emit = defineEmits(["zoom-in", "zoom-out", "reset"]);
13
13
  const zoomIconSize = computed(() => {
@@ -18,7 +18,8 @@ const zoomPercent = computed(() => Math.round(props.scale * 100));
18
18
  const isFullscreen = computed(() => props.variant === "fullscreen");
19
19
  const handleAction = (event, action) => {
20
20
  emit(action);
21
- event.currentTarget?.blur();
21
+ if (event.detail > 0)
22
+ event.currentTarget?.blur();
22
23
  };
23
24
  </script>
24
25
 
@@ -35,7 +36,8 @@ const handleAction = (event, action) => {
35
36
  <button
36
37
  type="button"
37
38
  class="ncm-zoom-btn"
38
- aria-label="Zoom Out"
39
+ :title="labels.zoomOut"
40
+ :aria-label="labels.zoomOut"
39
41
  @click.stop="handleAction($event, 'zoom-out')"
40
42
  >
41
43
  <IconMinus :size="zoomIconSize" />
@@ -44,7 +46,8 @@ const handleAction = (event, action) => {
44
46
  <button
45
47
  type="button"
46
48
  class="ncm-zoom-btn"
47
- aria-label="Zoom In"
49
+ :title="labels.zoomIn"
50
+ :aria-label="labels.zoomIn"
48
51
  @click.stop="handleAction($event, 'zoom-in')"
49
52
  >
50
53
  <IconPlus :size="zoomIconSize" />
@@ -53,10 +56,11 @@ const handleAction = (event, action) => {
53
56
  <button
54
57
  type="button"
55
58
  class="ncm-zoom-btn ncm-zoom-reset"
56
- aria-label="Reset Zoom"
59
+ :title="labels.resetZoom"
60
+ :aria-label="labels.resetZoom"
57
61
  @click.stop="handleAction($event, 'reset')"
58
62
  >
59
- {{ resetLabel }}
63
+ {{ labels.resetZoom }}
60
64
  </button>
61
65
  </div>
62
66
  </template>