@barzhsieh/nuxt-content-mermaid 3.0.2 → 3.0.4

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 (32) 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 +190 -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/direct-mermaid-config.js +1 -1
  25. package/dist/runtime/mermaid-rendering.js +11 -9
  26. package/dist/runtime/png-font-embedding.d.ts +7 -0
  27. package/dist/runtime/png-font-embedding.js +305 -0
  28. package/dist/runtime/png-rasterizer.d.ts +7 -0
  29. package/dist/runtime/png-rasterizer.js +151 -0
  30. package/dist/runtime/svg-download.d.ts +18 -0
  31. package/dist/runtime/svg-download.js +136 -0
  32. package/package.json +10 -6
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.4",
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,12 @@ 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 generatedDownloadDisclosureId = `ncm-download-${useId()}`;
102
+ const downloadDisclosureId = ref();
103
+ const isDownloadDisclosureOpen = ref(false);
104
+ const isPngDownloadPending = ref(false);
79
105
  const decodedCode = computed(() => props.code ? decodeURIComponent(props.code) : "");
80
106
  const mermaidDefinition = ref(decodedCode.value);
81
107
  let observer = null;
@@ -86,6 +112,9 @@ const defaultFullscreenToolbarScale = runtimeToolbarDefaults?.fullscreenToolbarS
86
112
  const resolvedToolbar = computed(() => {
87
113
  return defu({}, props.toolbar ?? {}, runtimeToolbarDefaults ?? {});
88
114
  });
115
+ const toolbarLabels = computed(() => {
116
+ return defu({}, resolvedToolbar.value.labels ?? {}, DEFAULT_TOOLBAR_LABELS);
117
+ });
89
118
  const toolbarButtons = computed(() => {
90
119
  const buttons = resolvedToolbar.value.buttons;
91
120
  return isRecord(buttons) ? buttons : {};
@@ -104,13 +133,16 @@ const toolbarFontSize = computed(() => {
104
133
  return defaultToolbarFontSize;
105
134
  });
106
135
  const showCopyButton = computed(() => toolbarButtons.value.copy !== false);
136
+ const canDownloadSvg = computed(() => {
137
+ return isCommittedOutputSvg.value && committedExportSnapshot.value !== null && componentSource.value.kind !== "conflict";
138
+ });
107
139
  const copySource = computed(() => decodedCode.value || mermaidDefinition.value || "");
108
140
  const hasCopySource = computed(() => !!copySource.value);
109
141
  const copyState = ref("idle");
110
142
  const copyLabel = computed(() => {
111
- if (copyState.value === "copied") return "Copied";
112
- if (copyState.value === "error") return "Copy failed";
113
- return "Copy";
143
+ if (copyState.value === "copied") return toolbarLabels.value.copied;
144
+ if (copyState.value === "error") return toolbarLabels.value.copyFailed;
145
+ return toolbarLabels.value.copy;
114
146
  });
115
147
  let copyResetTimer = null;
116
148
  const isCopyHovered = ref(false);
@@ -120,7 +152,7 @@ const copyIcon = computed(() => {
120
152
  return IconClipboard;
121
153
  });
122
154
  const showFullscreenButton = computed(() => toolbarButtons.value.fullscreen !== false && isFullscreenSupported.value);
123
- const fullscreenLabel = computed(() => isFullscreen.value ? "Exit fullscreen" : "Enter fullscreen");
155
+ const fullscreenLabel = computed(() => isFullscreen.value ? toolbarLabels.value.exitFullscreen : toolbarLabels.value.enterFullscreen);
124
156
  const fullscreenToolbarScale = computed(() => {
125
157
  const raw = resolvedToolbar.value.fullscreenToolbarScale ?? defaultFullscreenToolbarScale;
126
158
  if (typeof raw === "number" && Number.isFinite(raw))
@@ -178,6 +210,7 @@ const iconSize = computed(() => {
178
210
  const isExpandBlocked = computed(() => !isEnabled || !expandEnabled || isLoading.value || hasError.value || isFullscreen.value);
179
211
  const expandOverlay = useTemplateRef("expandOverlay");
180
212
  const isExpandActive = ref(false);
213
+ const expandToolbarLabel = computed(() => isExpandActive.value ? toolbarLabels.value.collapse : toolbarLabels.value.expand);
181
214
  const handleExpandActiveChange = (active) => {
182
215
  isExpandActive.value = active;
183
216
  };
@@ -330,6 +363,78 @@ function getMermaidSvg() {
330
363
  const svg = mermaidContainer.value.querySelector("svg");
331
364
  return svg instanceof SVGSVGElement ? svg : null;
332
365
  }
366
+ function closeDownloadDisclosure(focusTrigger = false) {
367
+ isDownloadDisclosureOpen.value = false;
368
+ if (!focusTrigger) return;
369
+ void nextTick(() => {
370
+ if (downloadTrigger.value?.isConnected)
371
+ downloadTrigger.value.focus();
372
+ });
373
+ }
374
+ async function toggleDownloadDisclosure(event) {
375
+ if (!canDownloadSvg.value || isPngDownloadPending.value) return;
376
+ if (isDownloadDisclosureOpen.value) {
377
+ closeDownloadDisclosure();
378
+ return;
379
+ }
380
+ isDownloadDisclosureOpen.value = true;
381
+ if (event.detail === 0) {
382
+ await nextTick();
383
+ downloadSvgButton.value?.focus();
384
+ }
385
+ }
386
+ function handleDownloadFocusout(event) {
387
+ if (isPngDownloadPending.value) return;
388
+ const nextTarget = event.relatedTarget;
389
+ if (nextTarget instanceof Node && downloadDisclosure.value?.contains(nextTarget)) {
390
+ return;
391
+ }
392
+ closeDownloadDisclosure();
393
+ }
394
+ function handleDownloadEscape() {
395
+ closeDownloadDisclosure(true);
396
+ }
397
+ function handleDocumentPointerDown(event) {
398
+ const target = event.target;
399
+ if (isDownloadDisclosureOpen.value && !isPngDownloadPending.value && target instanceof Node && !downloadRoot.value?.contains(target)) {
400
+ closeDownloadDisclosure();
401
+ }
402
+ }
403
+ function downloadSvg() {
404
+ const snapshot = committedExportSnapshot.value;
405
+ if (!canDownloadSvg.value || !snapshot) return;
406
+ try {
407
+ downloadStandaloneSvg(snapshot.svg, {
408
+ filename: "mermaid-diagram.svg"
409
+ });
410
+ } finally {
411
+ closeDownloadDisclosure(true);
412
+ }
413
+ }
414
+ async function downloadPng() {
415
+ const snapshot = committedExportSnapshot.value;
416
+ if (!canDownloadSvg.value || !snapshot || isPngDownloadPending.value)
417
+ return;
418
+ isPngDownloadPending.value = true;
419
+ try {
420
+ const svg = createSafeStandaloneSvgClone(snapshot.svg);
421
+ const { rasterizePngSnapshot } = await loadPngRasterizer();
422
+ const blob = await rasterizePngSnapshot({
423
+ svg,
424
+ width: snapshot.width,
425
+ height: snapshot.height
426
+ });
427
+ downloadBlob(blob, "mermaid-diagram.png");
428
+ } catch (error) {
429
+ console.error(
430
+ "[nuxt-content-mermaid] Failed to download PNG:",
431
+ error
432
+ );
433
+ } finally {
434
+ isPngDownloadPending.value = false;
435
+ closeDownloadDisclosure(true);
436
+ }
437
+ }
333
438
  function getMermaidViewport() {
334
439
  return mermaidWrapper.value;
335
440
  }
@@ -350,6 +455,17 @@ async function renderMermaid() {
350
455
  return;
351
456
  if (outcome.status === "success") {
352
457
  hasRenderedOnce.value = true;
458
+ const committedSvg = getMermaidSvg();
459
+ const bounds = committedSvg?.getBoundingClientRect();
460
+ const isValidSvgSnapshot = committedSvg && bounds && Number.isFinite(bounds.width) && bounds.width > 0 && Number.isFinite(bounds.height) && bounds.height > 0;
461
+ isCommittedOutputSvg.value = Boolean(isValidSvgSnapshot);
462
+ if (isValidSvgSnapshot) {
463
+ committedExportSnapshot.value = {
464
+ svg: committedSvg.cloneNode(true),
465
+ width: bounds.width,
466
+ height: bounds.height
467
+ };
468
+ }
353
469
  } else if (outcome.status === "failure") {
354
470
  console.error("[nuxt-content-mermaid]", outcome.error);
355
471
  hasError.value = true;
@@ -414,7 +530,9 @@ async function copyToClipboard(text) {
414
530
  return copied;
415
531
  }
416
532
  onMounted(() => {
533
+ downloadDisclosureId.value = generatedDownloadDisclosureId;
417
534
  if (!isEnabled) return;
535
+ document.addEventListener("pointerdown", handleDocumentPointerDown);
418
536
  if (!mermaidDefinition.value && mermaidContainer.value)
419
537
  mermaidDefinition.value = extractMermaidDefinition(mermaidContainer.value);
420
538
  nextTick(() => setupMermaidContainer());
@@ -423,6 +541,11 @@ onUnmounted(() => {
423
541
  requestBuiltInRender?.invalidate();
424
542
  stopLazyObservation();
425
543
  if (copyResetTimer) clearTimeout(copyResetTimer);
544
+ document.removeEventListener("pointerdown", handleDocumentPointerDown);
545
+ });
546
+ watch(canDownloadSvg, (isEligible) => {
547
+ if (!isEligible)
548
+ closeDownloadDisclosure();
426
549
  });
427
550
  watch(
428
551
  () => ({
@@ -480,12 +603,66 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
480
603
  :size="iconSize"
481
604
  />
482
605
  </button>
606
+ <div
607
+ ref="downloadRoot"
608
+ class="ncm-download"
609
+ >
610
+ <button
611
+ ref="downloadTrigger"
612
+ type="button"
613
+ class="mermaid-btn"
614
+ :title="toolbarLabels.download"
615
+ :aria-label="toolbarLabels.download"
616
+ :aria-expanded="isDownloadDisclosureOpen"
617
+ :aria-controls="downloadDisclosureId"
618
+ :disabled="!canDownloadSvg"
619
+ @click="toggleDownloadDisclosure"
620
+ >
621
+ <IconDownload :size="iconSize" />
622
+ </button>
623
+ <div
624
+ v-show="isDownloadDisclosureOpen"
625
+ :id="downloadDisclosureId"
626
+ ref="downloadDisclosure"
627
+ class="ncm-download-disclosure"
628
+ @focusout="handleDownloadFocusout"
629
+ @keydown.esc.stop.prevent="handleDownloadEscape"
630
+ >
631
+ <button
632
+ ref="downloadSvgButton"
633
+ type="button"
634
+ class="ncm-download-choice"
635
+ :title="toolbarLabels.downloadSvg"
636
+ :aria-label="toolbarLabels.downloadSvg"
637
+ @click="downloadSvg"
638
+ >
639
+ {{ toolbarLabels.downloadSvg }}
640
+ </button>
641
+ <button
642
+ type="button"
643
+ class="ncm-download-choice"
644
+ :title="toolbarLabels.downloadPng"
645
+ :aria-label="toolbarLabels.downloadPng"
646
+ :disabled="isPngDownloadPending"
647
+ :aria-busy="isPngDownloadPending || void 0"
648
+ @click="downloadPng"
649
+ >
650
+ <span
651
+ v-if="isPngDownloadPending"
652
+ class="ncm-download-spinner"
653
+ aria-hidden="true"
654
+ />
655
+ {{ toolbarLabels.downloadPng }}
656
+ </button>
657
+ </div>
658
+ </div>
483
659
  <button
484
660
  v-if="showExpandToolbarButton"
661
+ ref="expandButton"
485
662
  type="button"
486
663
  class="mermaid-btn"
487
- :title="isExpandActive ? 'Collapse' : 'Expand'"
488
- :aria-label="isExpandActive ? 'Collapse diagram' : 'Expand diagram'"
664
+ :title="expandToolbarLabel"
665
+ :aria-label="expandToolbarLabel"
489
666
  @click="toggleExpand"
490
667
  >
491
668
  <IconExpand
@@ -499,6 +676,7 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
499
676
  </button>
500
677
  <button
501
678
  v-if="showFullscreenButton"
679
+ ref="fullscreenButton"
502
680
  type="button"
503
681
  class="mermaid-btn"
504
682
  :title="fullscreenLabel"
@@ -523,6 +701,8 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
523
701
  :viewport-target="mermaidWrapper"
524
702
  :render-target="mermaidContainer"
525
703
  :icon-size="iconSize"
704
+ :labels="toolbarLabels"
705
+ :get-focus-target="getFullscreenFocusTarget"
526
706
  @active-change="handleFullscreenActiveChange"
527
707
  @supported-change="handleFullscreenSupportedChange"
528
708
  />
@@ -584,14 +764,17 @@ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
584
764
  :blocked="isExpandBlocked"
585
765
  :get-expand-target="getMermaidSvg"
586
766
  :get-expand-viewport="getMermaidViewport"
767
+ :get-return-focus-target="getExpandFocusTarget"
587
768
  :overlay-style="cursorVariables"
588
769
  :content-style="cursorVariables"
589
770
  :icon-size="iconSize"
771
+ :labels="toolbarLabels"
772
+ :dialog-label="toolbarTitle || toolbarLabels.expand"
590
773
  @active-change="handleExpandActiveChange"
591
774
  />
592
775
  </div>
593
776
  </template>
594
777
 
595
778
  <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}
779
+ .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
780
  </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>