@barzhsieh/nuxt-content-mermaid 2.2.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +190 -90
  2. package/README.zh-TW.md +183 -99
  3. package/dist/module.d.mts +39 -53
  4. package/dist/module.json +3 -3
  5. package/dist/module.mjs +600 -155
  6. package/dist/runtime/built-in-renderer/BuiltInRenderer.d.vue.ts +0 -0
  7. package/dist/runtime/built-in-renderer/BuiltInRenderer.vue +597 -0
  8. package/dist/runtime/built-in-renderer/BuiltInRenderer.vue.d.ts +0 -0
  9. package/dist/runtime/component-configuration-error.d.ts +3 -0
  10. package/dist/runtime/component-configuration-error.js +7 -0
  11. package/dist/runtime/component-configuration.d.ts +23 -0
  12. package/dist/runtime/component-configuration.js +78 -0
  13. package/dist/runtime/components/ContentMermaidTransport.d.vue.ts +10 -0
  14. package/dist/runtime/components/ContentMermaidTransport.vue +21 -0
  15. package/dist/runtime/components/ContentMermaidTransport.vue.d.ts +10 -0
  16. package/dist/runtime/components/Mermaid.d.vue.ts +0 -31
  17. package/dist/runtime/components/Mermaid.vue +136 -743
  18. package/dist/runtime/components/Mermaid.vue.d.ts +0 -31
  19. package/dist/runtime/components/MermaidExpandOverlay.d.vue.ts +13 -16
  20. package/dist/runtime/components/MermaidExpandOverlay.vue +61 -53
  21. package/dist/runtime/components/MermaidExpandOverlay.vue.d.ts +13 -16
  22. package/dist/runtime/components/MermaidFullscreenPresentation.d.vue.ts +18 -0
  23. package/dist/runtime/components/MermaidFullscreenPresentation.vue +56 -0
  24. package/dist/runtime/components/MermaidFullscreenPresentation.vue.d.ts +18 -0
  25. package/dist/runtime/composables/useMermaidExpand.d.ts +16 -32
  26. package/dist/runtime/composables/useMermaidExpand.js +232 -75
  27. package/dist/runtime/composables/useMermaidFullscreen.d.ts +18 -0
  28. package/dist/runtime/composables/useMermaidFullscreen.js +242 -0
  29. package/dist/runtime/composables/useMermaidZoom.d.ts +5 -1
  30. package/dist/runtime/composables/useMermaidZoom.js +54 -28
  31. package/dist/runtime/configuration/core.d.ts +27 -0
  32. package/dist/runtime/configuration/core.js +340 -0
  33. package/dist/runtime/configuration/module.d.ts +17 -0
  34. package/dist/runtime/configuration/module.js +233 -0
  35. package/dist/runtime/configuration/runtime-options.d.ts +4 -0
  36. package/dist/runtime/configuration/runtime-options.js +57 -0
  37. package/dist/runtime/constants.d.ts +24 -0
  38. package/dist/runtime/constants.js +60 -2
  39. package/dist/runtime/direct-mermaid-config.d.ts +5 -0
  40. package/dist/runtime/direct-mermaid-config.js +216 -0
  41. package/dist/runtime/mermaid-config.d.ts +9 -5
  42. package/dist/runtime/mermaid-config.js +30 -19
  43. package/dist/runtime/mermaid-rendering.d.ts +28 -0
  44. package/dist/runtime/mermaid-rendering.js +140 -0
  45. package/dist/runtime/plugins/content-mermaid-transport.d.ts +2 -0
  46. package/dist/runtime/plugins/content-mermaid-transport.js +5 -0
  47. package/dist/runtime/plugins/mermaid.client.d.ts +0 -3
  48. package/dist/runtime/plugins/mermaid.client.js +14 -32
  49. package/dist/runtime/plugins/runtime-config.d.ts +2 -0
  50. package/dist/runtime/plugins/runtime-config.js +9 -0
  51. package/dist/runtime/rendererSelection.d.ts +43 -0
  52. package/dist/runtime/rendererSelection.js +36 -0
  53. package/dist/runtime/rendererSelectionOrchestration.d.ts +12 -0
  54. package/dist/runtime/rendererSelectionOrchestration.js +13 -0
  55. package/dist/runtime/runtime-snapshot.d.ts +3 -0
  56. package/dist/runtime/runtime-snapshot.js +26 -0
  57. package/dist/runtime/utils/index.d.ts +0 -4
  58. package/dist/runtime/utils/index.js +0 -37
  59. package/dist/types.d.mts +2 -2
  60. package/package.json +36 -32
  61. package/dist/runtime/utils/mermaid-transform.d.ts +0 -18
  62. package/dist/runtime/utils/mermaid-transform.js +0 -345
@@ -0,0 +1,597 @@
1
+ <script setup>
2
+ import { useNuxtApp } from "#app";
3
+ import {
4
+ ref,
5
+ onMounted,
6
+ onUnmounted,
7
+ nextTick,
8
+ computed,
9
+ watch,
10
+ shallowRef,
11
+ useTemplateRef
12
+ } from "vue";
13
+ import { defu } from "defu";
14
+ import { materializeMermaidConfigForInvocation, resolveMermaidTheme } from "../mermaid-config";
15
+ import { createMermaidRenderer } from "../mermaid-rendering";
16
+ import { getRuntimeMermaidSnapshot } from "../runtime-snapshot";
17
+ import { parseSizeToPx, isRecord } from "../utils";
18
+ import { useMermaidTheme } from "../composables/useMermaidTheme";
19
+ import { useMermaidCursors } from "../composables/useMermaidCursors";
20
+ import IconClipboard from "../components/icons/IconClipboard.vue";
21
+ import IconClipboardHover from "../components/icons/IconClipboardHover.vue";
22
+ import IconClipboardSuccess from "../components/icons/IconClipboardSuccess.vue";
23
+ import IconFullScreen from "../components/icons/iconFullScreen.vue";
24
+ import IconFullScreenExit from "../components/icons/iconFullScreenExit.vue";
25
+ import MermaidExpandOverlay from "../components/MermaidExpandOverlay.vue";
26
+ import IconExpand from "../components/icons/IconExpand.vue";
27
+ import IconCollapse from "../components/icons/IconCollapse.vue";
28
+ import MermaidFullscreenPresentation from "../components/MermaidFullscreenPresentation.vue";
29
+ const props = defineProps({
30
+ toolbar: { type: Object, required: false },
31
+ code: { type: String, required: false },
32
+ pageConfig: { type: null, required: false },
33
+ config: { type: null, required: false },
34
+ componentSource: { type: Object, required: true },
35
+ spinnerComponent: { type: null, required: true }
36
+ });
37
+ const componentSource = computed(() => props.componentSource);
38
+ const nuxtApp = useNuxtApp();
39
+ const contentMermaidOptions = getRuntimeMermaidSnapshot(nuxtApp);
40
+ const isEnabled = true;
41
+ const debug = contentMermaidOptions.debug || false;
42
+ const loaderOptions = contentMermaidOptions.loader || {};
43
+ const themeOptions = contentMermaidOptions.theme || {};
44
+ const componentOptions = contentMermaidOptions.components || {};
45
+ const expandOptions = contentMermaidOptions.expand;
46
+ const expandEnabled = expandOptions.enabled !== false;
47
+ const expandOpenOptions = expandOptions.invokeOpenOn || {};
48
+ const allowOpenDiagramClick = expandOpenOptions.diagramClick !== false;
49
+ const lazyOption = loaderOptions.lazy;
50
+ const isLazy = lazyOption !== false;
51
+ const lazyThreshold = typeof lazyOption === "object" && typeof lazyOption.threshold === "number" ? lazyOption.threshold : 0.1;
52
+ const baseMermaidInit = loaderOptions.init || {};
53
+ const lightTheme = themeOptions.light;
54
+ const darkTheme = themeOptions.dark;
55
+ const colorMode = nuxtApp.$colorMode;
56
+ const { $mermaid } = nuxtApp;
57
+ const { currentTheme: manualThemeMode } = useMermaidTheme();
58
+ const mermaidBlock = useTemplateRef("mermaidBlock");
59
+ const mermaidWrapper = useTemplateRef("mermaidWrapper");
60
+ const mermaidContainer = useTemplateRef("mermaidContainer");
61
+ const fullscreenPresentation = useTemplateRef("fullscreenPresentation");
62
+ const isFullscreenSupported = ref(false);
63
+ const isFullscreen = ref(false);
64
+ const handleFullscreenSupportedChange = (supported) => {
65
+ isFullscreenSupported.value = supported;
66
+ };
67
+ const handleFullscreenActiveChange = (active) => {
68
+ isFullscreen.value = active;
69
+ };
70
+ const toggleFullscreen = () => {
71
+ expandOverlay.value?.endForDiagramReplacement();
72
+ return fullscreenPresentation.value?.toggle();
73
+ };
74
+ const hasRenderStarted = ref(false);
75
+ const hasRenderedOnce = ref(false);
76
+ const isLoading = ref(false);
77
+ const hasError = ref(false);
78
+ const errorContent = shallowRef(null);
79
+ const decodedCode = computed(() => props.code ? decodeURIComponent(props.code) : "");
80
+ const mermaidDefinition = ref(decodedCode.value);
81
+ let observer = null;
82
+ const runtimeToolbarDefaults = contentMermaidOptions.toolbar;
83
+ const defaultToolbarTitle = runtimeToolbarDefaults?.title ?? "mermaid";
84
+ const defaultToolbarFontSize = runtimeToolbarDefaults?.fontSize ?? "14px";
85
+ const defaultFullscreenToolbarScale = runtimeToolbarDefaults?.fullscreenToolbarScale ?? 1.25;
86
+ const resolvedToolbar = computed(() => {
87
+ return defu({}, props.toolbar ?? {}, runtimeToolbarDefaults ?? {});
88
+ });
89
+ const toolbarButtons = computed(() => {
90
+ const buttons = resolvedToolbar.value.buttons;
91
+ return isRecord(buttons) ? buttons : {};
92
+ });
93
+ const toolbarTitle = computed(() => resolvedToolbar.value.title ?? defaultToolbarTitle);
94
+ const toolbarFontSize = computed(() => {
95
+ const value = resolvedToolbar.value.fontSize ?? defaultToolbarFontSize;
96
+ if (typeof value === "number")
97
+ return `${value}px`;
98
+ if (typeof value === "string") {
99
+ const trimmed = value.trim();
100
+ if (/^\d+(?:\.\d+)?$/.test(trimmed))
101
+ return `${trimmed}px`;
102
+ return trimmed;
103
+ }
104
+ return defaultToolbarFontSize;
105
+ });
106
+ const showCopyButton = computed(() => toolbarButtons.value.copy !== false);
107
+ const copySource = computed(() => decodedCode.value || mermaidDefinition.value || "");
108
+ const hasCopySource = computed(() => !!copySource.value);
109
+ const copyState = ref("idle");
110
+ const copyLabel = computed(() => {
111
+ if (copyState.value === "copied") return "Copied";
112
+ if (copyState.value === "error") return "Copy failed";
113
+ return "Copy";
114
+ });
115
+ let copyResetTimer = null;
116
+ const isCopyHovered = ref(false);
117
+ const copyIcon = computed(() => {
118
+ if (copyState.value === "copied") return IconClipboardSuccess;
119
+ if (isCopyHovered.value && hasCopySource.value) return IconClipboardHover;
120
+ return IconClipboard;
121
+ });
122
+ const showFullscreenButton = computed(() => toolbarButtons.value.fullscreen !== false && isFullscreenSupported.value);
123
+ const fullscreenLabel = computed(() => isFullscreen.value ? "Exit fullscreen" : "Enter fullscreen");
124
+ const fullscreenToolbarScale = computed(() => {
125
+ const raw = resolvedToolbar.value.fullscreenToolbarScale ?? defaultFullscreenToolbarScale;
126
+ if (typeof raw === "number" && Number.isFinite(raw))
127
+ return raw;
128
+ if (typeof raw === "string") {
129
+ const parsed = Number.parseFloat(raw);
130
+ if (Number.isFinite(parsed))
131
+ return parsed;
132
+ }
133
+ return defaultFullscreenToolbarScale;
134
+ });
135
+ const toolbarFontSizePx = computed(() => {
136
+ return parseSizeToPx(resolvedToolbar.value.fontSize ?? defaultToolbarFontSize, 14);
137
+ });
138
+ const fullscreenToolbarFontSize = computed(() => {
139
+ const scaled = toolbarFontSizePx.value * fullscreenToolbarScale.value;
140
+ return `${Math.round(scaled * 100) / 100}px`;
141
+ });
142
+ const effectiveToolbarFontSize = computed(() => {
143
+ return isFullscreen.value ? fullscreenToolbarFontSize.value : toolbarFontSize.value;
144
+ });
145
+ const effectiveToolbarFontSizePx = computed(() => {
146
+ const baseSize = toolbarFontSizePx.value;
147
+ return isFullscreen.value ? baseSize * fullscreenToolbarScale.value : baseSize;
148
+ });
149
+ const diagramSourceConfig = computed(() => {
150
+ const source = componentSource.value;
151
+ return source.kind === "runtime-only" || source.kind === "conflict" ? void 0 : source.config;
152
+ });
153
+ const mermaidTheme = computed(() => {
154
+ return resolveMermaidTheme({
155
+ colorModeValue: colorMode?.value,
156
+ manualThemeMode: manualThemeMode.value,
157
+ frontmatterTheme: diagramSourceConfig.value?.theme,
158
+ baseTheme: baseMermaidInit.theme,
159
+ lightTheme,
160
+ darkTheme
161
+ });
162
+ });
163
+ function materializeEffectiveMermaidInit() {
164
+ const source = componentSource.value;
165
+ if (source.kind === "conflict") throw source.error;
166
+ return materializeMermaidConfigForInvocation({
167
+ runtimeConfig: baseMermaidInit,
168
+ source,
169
+ theme: mermaidTheme.value
170
+ });
171
+ }
172
+ const configuredErrorName = computed(() => componentOptions.error?.trim() || "");
173
+ const errorComponent = shallowRef(null);
174
+ const spinnerComponent = computed(() => props.spinnerComponent);
175
+ const iconSize = computed(() => {
176
+ return Math.round(effectiveToolbarFontSizePx.value * 1.2);
177
+ });
178
+ const isExpandBlocked = computed(() => !isEnabled || !expandEnabled || isLoading.value || hasError.value || isFullscreen.value);
179
+ const expandOverlay = useTemplateRef("expandOverlay");
180
+ const isExpandActive = ref(false);
181
+ const handleExpandActiveChange = (active) => {
182
+ isExpandActive.value = active;
183
+ };
184
+ const toggleExpand = (event) => expandOverlay.value?.toggle(event);
185
+ const handleMermaidClick = (event) => expandOverlay.value?.openFromDiagram(event);
186
+ let requestBuiltInRender;
187
+ function getBuiltInRenderRequest() {
188
+ requestBuiltInRender ??= createMermaidRenderer({
189
+ loadMermaid: $mermaid,
190
+ readRenderData: () => {
191
+ if (componentSource.value.kind === "conflict") {
192
+ return {
193
+ source: null,
194
+ config: {},
195
+ target: mermaidContainer.value
196
+ };
197
+ }
198
+ return {
199
+ source: mermaidDefinition.value,
200
+ config: materializeEffectiveMermaidInit(),
201
+ target: mermaidContainer.value
202
+ };
203
+ },
204
+ beforeCommit: () => {
205
+ if (import.meta.client) {
206
+ expandOverlay.value?.endForDiagramReplacement();
207
+ void fullscreenPresentation.value?.endForDiagramReplacement();
208
+ }
209
+ hasError.value = false;
210
+ errorContent.value = null;
211
+ },
212
+ debug
213
+ });
214
+ return requestBuiltInRender;
215
+ }
216
+ const showExpandToolbarButton = computed(() => {
217
+ if (!expandEnabled) return false;
218
+ if (toolbarButtons.value.expand === false) return false;
219
+ if (isExpandActive.value) return false;
220
+ if (isFullscreen.value) return false;
221
+ return true;
222
+ });
223
+ function normalizeIdentifier(value) {
224
+ return value.replace(/\.vue$/i, "").split(/[\\/]/).pop()?.replace(/[\s_-]+/g, "").toLowerCase() || "";
225
+ }
226
+ async function loadAppComponent(name, appComponents) {
227
+ const target = normalizeIdentifier(name);
228
+ const matchEntry = Object.entries(appComponents).find(([path]) => {
229
+ const base = path.split(/[\\/]/).pop() || "";
230
+ const normalized = normalizeIdentifier(base);
231
+ return normalized === target;
232
+ });
233
+ if (!matchEntry) return null;
234
+ const [, componentLoader] = matchEntry;
235
+ const mod = await componentLoader();
236
+ return mod.default || mod;
237
+ }
238
+ async function resolveErrorAppComponent(name, appComponents) {
239
+ try {
240
+ const component = await loadAppComponent(name, appComponents);
241
+ if (!component) {
242
+ console.warn(
243
+ "[nuxt-content-mermaid] Cannot find error component:",
244
+ name
245
+ );
246
+ }
247
+ return component;
248
+ } catch (error) {
249
+ console.error(
250
+ "[nuxt-content-mermaid] Failed to load error component:",
251
+ error
252
+ );
253
+ return null;
254
+ }
255
+ }
256
+ if (import.meta.client && isEnabled) {
257
+ const appComponents = import.meta.glob("~/components/**/*.{vue,js,ts}");
258
+ watch(
259
+ configuredErrorName,
260
+ async (name) => {
261
+ if (!name) {
262
+ errorComponent.value = null;
263
+ return;
264
+ }
265
+ errorComponent.value = await resolveErrorAppComponent(name, appComponents);
266
+ },
267
+ { immediate: true }
268
+ );
269
+ }
270
+ function serializeMermaidFromNode(root) {
271
+ const TEXT_NODE = 3;
272
+ const ELEMENT_NODE = 1;
273
+ if (root.nodeType === TEXT_NODE) {
274
+ return root.data;
275
+ }
276
+ if (root.nodeType === ELEMENT_NODE) {
277
+ const el = root;
278
+ const tag = el.tagName?.toUpperCase();
279
+ if (tag === "BR") return "\n";
280
+ const blockTags = [
281
+ "DIV",
282
+ "P",
283
+ "LI",
284
+ "TR",
285
+ "H1",
286
+ "H2",
287
+ "H3",
288
+ "H4",
289
+ "H5",
290
+ "H6",
291
+ "BLOCKQUOTE",
292
+ "PRE",
293
+ "UL",
294
+ "OL",
295
+ "DL",
296
+ "DT",
297
+ "DD",
298
+ "TABLE",
299
+ "THEAD",
300
+ "TBODY",
301
+ "TFOOT",
302
+ "SECTION",
303
+ "ARTICLE",
304
+ "ASIDE",
305
+ "HEADER",
306
+ "FOOTER",
307
+ "MAIN"
308
+ ];
309
+ const isBlock = blockTags.includes(tag) || el.classList.contains("line") || el.classList.contains("code-line");
310
+ let out = "";
311
+ el.childNodes.forEach((child) => {
312
+ out += serializeMermaidFromNode(child);
313
+ });
314
+ if (isBlock) return "\n" + out + "\n";
315
+ return out;
316
+ }
317
+ return "";
318
+ }
319
+ function extractMermaidDefinition(container) {
320
+ const codeNodes = Array.from(container.querySelectorAll("code"));
321
+ if (codeNodes.length > 0) {
322
+ return codeNodes.map((node) => serializeMermaidFromNode(node)).join("\n").trim();
323
+ }
324
+ const pre = container.querySelector("pre");
325
+ if (pre) return serializeMermaidFromNode(pre).trim();
326
+ return serializeMermaidFromNode(container).trim();
327
+ }
328
+ function getMermaidSvg() {
329
+ if (!mermaidContainer.value) return null;
330
+ const svg = mermaidContainer.value.querySelector("svg");
331
+ return svg instanceof SVGSVGElement ? svg : null;
332
+ }
333
+ function getMermaidViewport() {
334
+ return mermaidWrapper.value;
335
+ }
336
+ function stopLazyObservation() {
337
+ const activeObserver = observer;
338
+ observer = null;
339
+ activeObserver?.takeRecords();
340
+ activeObserver?.disconnect();
341
+ }
342
+ async function renderMermaid() {
343
+ if (componentSource.value.kind === "conflict") return;
344
+ const request = getBuiltInRenderRequest();
345
+ hasRenderStarted.value = true;
346
+ stopLazyObservation();
347
+ isLoading.value = true;
348
+ const outcome = await request();
349
+ if (outcome.status === "stale")
350
+ return;
351
+ if (outcome.status === "success") {
352
+ hasRenderedOnce.value = true;
353
+ } else if (outcome.status === "failure") {
354
+ console.error("[nuxt-content-mermaid]", outcome.error);
355
+ hasError.value = true;
356
+ errorContent.value = outcome.error;
357
+ }
358
+ isLoading.value = false;
359
+ }
360
+ function setupMermaidContainer() {
361
+ const container = mermaidContainer.value;
362
+ if (!container) return;
363
+ if (!isLazy) {
364
+ renderMermaid();
365
+ return;
366
+ }
367
+ const nextObserver = new IntersectionObserver(
368
+ (entries) => {
369
+ if (observer !== nextObserver) return;
370
+ if (entries[0]?.isIntersecting && !hasRenderedOnce.value)
371
+ renderMermaid();
372
+ },
373
+ { threshold: lazyThreshold }
374
+ );
375
+ observer = nextObserver;
376
+ nextObserver.observe(container);
377
+ }
378
+ async function copyMermaidSource() {
379
+ if (!copySource.value) return;
380
+ const copied = await copyToClipboard(copySource.value);
381
+ copyState.value = copied ? "copied" : "error";
382
+ if (copyResetTimer)
383
+ clearTimeout(copyResetTimer);
384
+ copyResetTimer = setTimeout(() => {
385
+ copyState.value = "idle";
386
+ }, 2e3);
387
+ }
388
+ async function copyToClipboard(text) {
389
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
390
+ try {
391
+ await navigator.clipboard.writeText(text);
392
+ return true;
393
+ } catch {
394
+ }
395
+ }
396
+ if (typeof document === "undefined")
397
+ return false;
398
+ const textarea = document.createElement("textarea");
399
+ textarea.value = text;
400
+ textarea.setAttribute("readonly", "");
401
+ textarea.style.position = "fixed";
402
+ textarea.style.top = "-9999px";
403
+ textarea.style.opacity = "0";
404
+ document.body.appendChild(textarea);
405
+ textarea.select();
406
+ let copied = false;
407
+ try {
408
+ copied = document.execCommand("copy");
409
+ } catch {
410
+ copied = false;
411
+ } finally {
412
+ document.body.removeChild(textarea);
413
+ }
414
+ return copied;
415
+ }
416
+ onMounted(() => {
417
+ if (!isEnabled) return;
418
+ if (!mermaidDefinition.value && mermaidContainer.value)
419
+ mermaidDefinition.value = extractMermaidDefinition(mermaidContainer.value);
420
+ nextTick(() => setupMermaidContainer());
421
+ });
422
+ onUnmounted(() => {
423
+ requestBuiltInRender?.invalidate();
424
+ stopLazyObservation();
425
+ if (copyResetTimer) clearTimeout(copyResetTimer);
426
+ });
427
+ watch(
428
+ () => ({
429
+ componentSource: componentSource.value,
430
+ code: decodedCode.value,
431
+ colorMode: colorMode?.value,
432
+ manualThemeMode: manualThemeMode.value
433
+ }),
434
+ ({ code, componentSource: source }, previousInputs) => {
435
+ if (code !== previousInputs.code)
436
+ mermaidDefinition.value = code;
437
+ const wasConflict = previousInputs.componentSource.kind === "conflict";
438
+ if (source.kind === "conflict") {
439
+ if (wasConflict) return;
440
+ requestBuiltInRender?.invalidate();
441
+ isLoading.value = false;
442
+ return;
443
+ }
444
+ if (!isEnabled) return;
445
+ if (wasConflict || hasRenderStarted.value) renderMermaid();
446
+ },
447
+ { flush: "post" }
448
+ );
449
+ const { cursorVariables } = useMermaidCursors(iconSize, expandEnabled);
450
+ </script>
451
+
452
+ <template>
453
+ <div
454
+ ref="mermaidBlock"
455
+ class="mermaid-block"
456
+ :style="{ '--ncm-icon-size': `${iconSize}px` }"
457
+ >
458
+ <!-- Toolbar: Title and Actions -->
459
+ <div class="mermaid-toolbar">
460
+ <div
461
+ class="mermaid-title"
462
+ :style="{ fontSize: effectiveToolbarFontSize }"
463
+ >
464
+ <span v-if="toolbarTitle">{{ toolbarTitle }}</span>
465
+ </div>
466
+ <div class="mermaid-actions">
467
+ <button
468
+ v-if="showCopyButton"
469
+ type="button"
470
+ class="mermaid-btn"
471
+ :title="copyLabel"
472
+ :aria-label="copyLabel"
473
+ :disabled="!hasCopySource"
474
+ @click="copyMermaidSource"
475
+ @mouseenter="isCopyHovered = true"
476
+ @mouseleave="isCopyHovered = false"
477
+ >
478
+ <component
479
+ :is="copyIcon"
480
+ :size="iconSize"
481
+ />
482
+ </button>
483
+ <button
484
+ v-if="showExpandToolbarButton"
485
+ type="button"
486
+ class="mermaid-btn"
487
+ :title="isExpandActive ? 'Collapse' : 'Expand'"
488
+ :aria-label="isExpandActive ? 'Collapse diagram' : 'Expand diagram'"
489
+ @click="toggleExpand"
490
+ >
491
+ <IconExpand
492
+ v-if="!isExpandActive"
493
+ :size="iconSize"
494
+ />
495
+ <IconCollapse
496
+ v-else
497
+ :size="iconSize"
498
+ />
499
+ </button>
500
+ <button
501
+ v-if="showFullscreenButton"
502
+ type="button"
503
+ class="mermaid-btn"
504
+ :title="fullscreenLabel"
505
+ :aria-label="fullscreenLabel"
506
+ @click="toggleFullscreen"
507
+ >
508
+ <IconFullScreen
509
+ v-if="!isFullscreen"
510
+ :size="iconSize"
511
+ />
512
+ <IconFullScreenExit
513
+ v-else
514
+ :size="iconSize"
515
+ />
516
+ </button>
517
+ </div>
518
+ </div>
519
+
520
+ <MermaidFullscreenPresentation
521
+ ref="fullscreenPresentation"
522
+ :fullscreen-target="mermaidBlock"
523
+ :viewport-target="mermaidWrapper"
524
+ :render-target="mermaidContainer"
525
+ :icon-size="iconSize"
526
+ @active-change="handleFullscreenActiveChange"
527
+ @supported-change="handleFullscreenSupportedChange"
528
+ />
529
+
530
+ <!-- Diagram Container -->
531
+ <div
532
+ ref="mermaidWrapper"
533
+ class="mermaid-wrapper"
534
+ :class="{
535
+ 'ncm-is-loading': isLoading && !hasRenderedOnce,
536
+ 'ncm-expand-hidden': isExpandActive,
537
+ 'ncm-expand-clickable': expandEnabled && allowOpenDiagramClick && !isFullscreen,
538
+ 'ncm-fullscreen-zoom': isFullscreen
539
+ }"
540
+ :style="cursorVariables"
541
+ @click="handleMermaidClick"
542
+ >
543
+ <div
544
+ ref="mermaidContainer"
545
+ class="mermaid"
546
+ >
547
+ <!-- Initially show the slot's <pre><code> for SSR; client-side rendering will replace it with the mermaid SVG -->
548
+ <slot>
549
+ <pre v-if="decodedCode"><code>{{ decodedCode }}</code></pre>
550
+ </slot>
551
+ </div>
552
+
553
+ <template v-if="isLoading && !hasRenderedOnce">
554
+ <slot name="loading">
555
+ <component :is="spinnerComponent" />
556
+ </slot>
557
+ </template>
558
+ <template v-else-if="hasError">
559
+ <slot
560
+ name="error"
561
+ :error="errorContent"
562
+ :source="mermaidDefinition"
563
+ >
564
+ <component
565
+ :is="errorComponent"
566
+ v-if="errorComponent"
567
+ :error="errorContent"
568
+ :source="mermaidDefinition"
569
+ />
570
+
571
+ <div
572
+ v-else
573
+ class="mermaid-error-default"
574
+ >
575
+ ⚠️ Mermaid Diagram Error
576
+ </div>
577
+ </slot>
578
+ </template>
579
+ </div>
580
+
581
+ <MermaidExpandOverlay
582
+ ref="expandOverlay"
583
+ :options="expandOptions"
584
+ :blocked="isExpandBlocked"
585
+ :get-expand-target="getMermaidSvg"
586
+ :get-expand-viewport="getMermaidViewport"
587
+ :overlay-style="cursorVariables"
588
+ :content-style="cursorVariables"
589
+ :icon-size="iconSize"
590
+ @active-change="handleExpandActiveChange"
591
+ />
592
+ </div>
593
+ </template>
594
+
595
+ <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){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
+ </style>
@@ -0,0 +1,3 @@
1
+ export declare function createMermaidComponentConfigurationError(message: string): Error & {
2
+ readonly code: string;
3
+ };
@@ -0,0 +1,7 @@
1
+ class MermaidComponentConfigurationError extends Error {
2
+ code = "CONTENT_MERMAID_COMPONENT_CONFIGURATION_ERROR";
3
+ name = "MermaidComponentConfigurationError";
4
+ }
5
+ export function createMermaidComponentConfigurationError(message) {
6
+ return new MermaidComponentConfigurationError(message);
7
+ }
@@ -0,0 +1,23 @@
1
+ import type { MermaidConfig } from 'mermaid';
2
+ import type { PageMermaidConfig } from '../types/config.js';
3
+ interface MermaidComponentSourceInput {
4
+ readonly pageConfig?: unknown;
5
+ readonly config?: unknown;
6
+ }
7
+ export type MermaidComponentSource = {
8
+ readonly kind: 'runtime-only';
9
+ } | {
10
+ readonly kind: 'page';
11
+ readonly config: PageMermaidConfig;
12
+ } | {
13
+ readonly kind: 'direct';
14
+ readonly config: MermaidConfig;
15
+ } | {
16
+ readonly kind: 'conflict';
17
+ readonly error: Error & {
18
+ readonly code: string;
19
+ };
20
+ };
21
+ export declare function resolveMermaidComponentSource(input: MermaidComponentSourceInput): MermaidComponentSource;
22
+ export declare function collectMermaidComponentSourceDependencies(input: MermaidComponentSourceInput): readonly unknown[];
23
+ export {};