@cocoar/vue-mermaid 2.14.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.
- package/README.md +85 -0
- package/dist/CoarMermaidDiagram.vue.d.ts +15 -0
- package/dist/CoarMermaidDiagram.vue.d.ts.map +1 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +272 -0
- package/dist/internal/mermaid-loader.d.ts +38 -0
- package/dist/internal/mermaid-loader.d.ts.map +1 -0
- package/dist/internal/pan-zoom.d.ts +36 -0
- package/dist/internal/pan-zoom.d.ts.map +1 -0
- package/dist/internal/theme-bridge.d.ts +50 -0
- package/dist/internal/theme-bridge.d.ts.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @cocoar/vue-mermaid
|
|
2
|
+
|
|
3
|
+
A standalone [Mermaid](https://mermaid.js.org/) diagram component for Vue 3.
|
|
4
|
+
`<CoarMermaidDiagram :code>` renders a diagram from a Mermaid source string —
|
|
5
|
+
Cocoar-themed, lazy-loaded, with opt-in zoom/pan.
|
|
6
|
+
|
|
7
|
+
It knows **nothing about markdown** or any embedding layer. To render
|
|
8
|
+
` ```mermaid ` fenced code blocks inside [`@cocoar/vue-markdown`](../markdown),
|
|
9
|
+
use the thin adapter [`@cocoar/vue-markdown-mermaid`](../markdown-mermaid) — it
|
|
10
|
+
plugs this component in as a fence renderer.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @cocoar/vue-mermaid
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`vue` is the only peer dependency. Mermaid is a regular dependency, dynamically
|
|
19
|
+
imported on first render so it lands in its own lazy chunk. Import the stylesheet
|
|
20
|
+
once:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import '@cocoar/vue-mermaid/styles';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<template>
|
|
30
|
+
<CoarMermaidDiagram :code="code" zoomable />
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { CoarMermaidDiagram } from '@cocoar/vue-mermaid';
|
|
35
|
+
|
|
36
|
+
const code = `
|
|
37
|
+
flowchart LR
|
|
38
|
+
A[Start] --> B{Choice}
|
|
39
|
+
B -->|yes| C[Do it]
|
|
40
|
+
B -->|no| D[Skip]
|
|
41
|
+
`;
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Description |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `code` | `string` | The Mermaid diagram source. |
|
|
48
|
+
| `language` | `string` | Info-string label (default `'mermaid'`); handy when reused. |
|
|
49
|
+
| `zoomable` | `boolean` | Enable zoom/pan (default `false`). |
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
- **Rendering** is client-only (Mermaid needs a DOM) and lazy (Mermaid is
|
|
54
|
+
dynamically imported on first mount).
|
|
55
|
+
- **Theming** maps Cocoar design tokens onto Mermaid's `themeVariables` (colors
|
|
56
|
+
normalized to sRGB, since Cocoar tokens are `oklch(...)` which Mermaid can't
|
|
57
|
+
read) so diagrams match the app's fonts and palette.
|
|
58
|
+
- **Fonts**: rendering waits for `document.fonts.ready` so text isn't clipped by
|
|
59
|
+
boxes measured before the web font loaded.
|
|
60
|
+
- **Security**: Mermaid runs with `securityLevel: 'strict'` — author diagram text
|
|
61
|
+
is treated as untrusted (HTML in labels is sanitized).
|
|
62
|
+
- **Invalid source** degrades to an error box that still shows the raw source; it
|
|
63
|
+
never throws up to the app.
|
|
64
|
+
|
|
65
|
+
## Zoom & pan
|
|
66
|
+
|
|
67
|
+
With `zoomable`, the diagram sits in a fixed-height viewport with:
|
|
68
|
+
|
|
69
|
+
- **+ / − / ⤢ buttons** (top-right) — the primary, touch-friendly zoom;
|
|
70
|
+
- **Ctrl / ⌘ + wheel** — zoom toward the cursor;
|
|
71
|
+
- **drag** — pan (mouse / pen);
|
|
72
|
+
- **double-click** — reset.
|
|
73
|
+
|
|
74
|
+
Plain mouse-wheel scrolling is deliberately **not** captured, so a diagram never
|
|
75
|
+
traps the page scroll. On touch, one-finger scrolling still scrolls the page.
|
|
76
|
+
Set the viewport height with the `--coar-mermaid-height` CSS variable (default
|
|
77
|
+
`420px`).
|
|
78
|
+
|
|
79
|
+
## Exports
|
|
80
|
+
|
|
81
|
+
| Export | Description |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `CoarMermaidDiagram` | The renderer component (`{ code, language, zoomable }`). |
|
|
84
|
+
| `buildMermaidThemeVariables(getToken, resolveColor?)` | Pure Cocoar-token → Mermaid-theme mapping. |
|
|
85
|
+
| `makeCssColorResolver()` / `readCssTokens(el?)` | Browser-backed color/token resolvers for the bridge. |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
code: string;
|
|
3
|
+
language?: string;
|
|
4
|
+
zoomable?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
7
|
+
language: string;
|
|
8
|
+
zoomable: boolean;
|
|
9
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
10
|
+
container: HTMLDivElement;
|
|
11
|
+
viewport: HTMLDivElement;
|
|
12
|
+
content: HTMLDivElement;
|
|
13
|
+
}, HTMLDivElement>;
|
|
14
|
+
export default _default;
|
|
15
|
+
//# sourceMappingURL=CoarMermaidDiagram.vue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoarMermaidDiagram.vue.d.ts","sourceRoot":"","sources":["../src/CoarMermaidDiagram.vue"],"names":[],"mappings":"AAwSA,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;;cAA7B,MAAM;cAAa,OAAO;;;;;;AAyMxE,wBASG"}
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.coar-mermaid{margin:var(--coar-spacing-m,1rem) 0;position:relative}.coar-mermaid__viewport{justify-content:center;display:flex}.coar-mermaid__svg{max-width:100%}.coar-mermaid__svg svg{max-width:100%;height:auto}.coar-mermaid--zoomable .coar-mermaid__viewport{height:var(--coar-mermaid-height,420px);border:1px solid var(--coar-border-neutral-tertiary,#e2e2e2);border-radius:var(--coar-input-radius,8px);background:var(--coar-background-neutral-primary,#fff);cursor:grab;display:block;position:relative;overflow:hidden}.coar-mermaid--zoomable .coar-mermaid__viewport.is-panning{cursor:grabbing}.coar-mermaid--zoomable .coar-mermaid__svg{width:100%;max-width:none}.coar-mermaid--zoomable .coar-mermaid__viewport.is-panning .coar-mermaid__svg{will-change:transform}.coar-mermaid--zoomable .coar-mermaid__svg svg{max-width:none}.coar-mermaid__controls{top:var(--coar-spacing-xs,.25rem);right:var(--coar-spacing-xs,.25rem);border-radius:var(--coar-input-radius,8px);background:color-mix(in srgb, var(--coar-background-neutral-primary,#fff) 88%, transparent);border:1px solid var(--coar-border-neutral-tertiary,#e2e2e2);gap:2px;padding:2px;display:flex;position:absolute}.coar-mermaid__controls button{border-radius:calc(var(--coar-input-radius,8px) - 2px);width:26px;height:26px;color:var(--coar-text-neutral-secondary,#333);font-family:var(--coar-font-family-body,sans-serif);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:0;font-size:16px;line-height:1;display:inline-flex}.coar-mermaid__controls button:hover{background:var(--coar-background-neutral-secondary,#f5f5f5)}.coar-mermaid__loading{color:var(--coar-text-neutral-tertiary,#666);font-family:var(--coar-font-family-body,sans-serif);font-size:var(--coar-font-size-xs,14px);padding:var(--coar-spacing-m,1rem)}.coar-mermaid__error{border:1px solid var(--coar-border-semantic-error,#dc2626);border-radius:var(--coar-input-radius,8px);background:var(--coar-background-semantic-error-subtle,#fee2e2);width:100%;padding:var(--coar-spacing-m,1rem);font-family:var(--coar-font-family-body,sans-serif)}.coar-mermaid__error-title{margin:0 0 var(--coar-spacing-xs,.25rem);color:var(--coar-text-semantic-error-bold,#991b1b);font-weight:600}.coar-mermaid__error-message{margin:0 0 var(--coar-spacing-s,.5rem);color:var(--coar-text-semantic-error-subtle,#b91c1c);font-size:var(--coar-font-size-xs,14px)}.coar-mermaid__error-source{padding:var(--coar-spacing-s,.5rem);border-radius:var(--coar-input-radius,8px);background:var(--coar-background-neutral-secondary,#f8f9fa);color:var(--coar-text-neutral-secondary,#333);font-size:var(--coar-font-size-xs,14px);white-space:pre;margin:0;overflow-x:auto}
|
|
2
|
+
/*$vite$:1*/
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cocoar/vue-mermaid` — a standalone Mermaid diagram component for Vue 3.
|
|
3
|
+
*
|
|
4
|
+
* `<CoarMermaidDiagram :code>` takes a Mermaid diagram source string and renders
|
|
5
|
+
* it: lazy-loaded engine, client-only, `securityLevel: 'strict'`, Cocoar-themed,
|
|
6
|
+
* with opt-in zoom/pan. It has **no dependency on — and no knowledge of —
|
|
7
|
+
* markdown** or any embedding layer. To render ` ```mermaid ` fenced code blocks
|
|
8
|
+
* inside `@cocoar/vue-markdown`, use the thin adapter `@cocoar/vue-markdown-mermaid`.
|
|
9
|
+
*/
|
|
10
|
+
export { default as CoarMermaidDiagram } from './CoarMermaidDiagram.vue';
|
|
11
|
+
export { buildMermaidThemeVariables, makeCssColorResolver, readCssTokens, } from './internal/theme-bridge';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAIzE,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,aAAa,GACd,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createCommentVNode as e, createElementBlock as t, createElementVNode as n, defineComponent as r, nextTick as i, normalizeClass as a, onBeforeUnmount as o, onMounted as s, openBlock as c, ref as l, shallowRef as u, toDisplayString as d, watch as f } from "vue";
|
|
2
|
+
//#region src/internal/mermaid-loader.ts
|
|
3
|
+
var p = null;
|
|
4
|
+
function m(e) {
|
|
5
|
+
return p || (p = import("mermaid").then((t) => {
|
|
6
|
+
let n = t.default ?? t;
|
|
7
|
+
return n.initialize({
|
|
8
|
+
startOnLoad: !1,
|
|
9
|
+
securityLevel: "strict",
|
|
10
|
+
...e()
|
|
11
|
+
}), n;
|
|
12
|
+
}).catch((e) => {
|
|
13
|
+
throw p = null, e;
|
|
14
|
+
}), p);
|
|
15
|
+
}
|
|
16
|
+
var h = Promise.resolve(), g = 0;
|
|
17
|
+
function _(e, t) {
|
|
18
|
+
let n = async () => {
|
|
19
|
+
let n = await m(e);
|
|
20
|
+
if (typeof document < "u" && document.fonts?.ready) try {
|
|
21
|
+
await document.fonts.ready;
|
|
22
|
+
} catch {}
|
|
23
|
+
let r = `coar-mermaid-${g++}`;
|
|
24
|
+
try {
|
|
25
|
+
let { svg: e } = await n.render(r, t);
|
|
26
|
+
return e;
|
|
27
|
+
} finally {
|
|
28
|
+
typeof document < "u" && (document.getElementById(r)?.remove(), document.getElementById(`d${r}`)?.remove());
|
|
29
|
+
}
|
|
30
|
+
}, r = h.then(n, n);
|
|
31
|
+
return h = r.then(() => void 0, () => void 0), r;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/internal/theme-bridge.ts
|
|
35
|
+
var v = [
|
|
36
|
+
{
|
|
37
|
+
mermaidVar: "fontFamily",
|
|
38
|
+
token: "--coar-font-family-body",
|
|
39
|
+
kind: "font"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
mermaidVar: "primaryColor",
|
|
43
|
+
token: "--coar-background-accent-secondary",
|
|
44
|
+
kind: "color"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
mermaidVar: "mainBkg",
|
|
48
|
+
token: "--coar-background-accent-secondary",
|
|
49
|
+
kind: "color"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
mermaidVar: "primaryTextColor",
|
|
53
|
+
token: "--coar-text-neutral-primary",
|
|
54
|
+
kind: "color"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
mermaidVar: "primaryBorderColor",
|
|
58
|
+
token: "--coar-border-accent-primary",
|
|
59
|
+
kind: "color"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
mermaidVar: "nodeBorder",
|
|
63
|
+
token: "--coar-border-accent-primary",
|
|
64
|
+
kind: "color"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
mermaidVar: "secondaryColor",
|
|
68
|
+
token: "--coar-background-neutral-secondary",
|
|
69
|
+
kind: "color"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
mermaidVar: "tertiaryColor",
|
|
73
|
+
token: "--coar-background-neutral-tertiary",
|
|
74
|
+
kind: "color"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
mermaidVar: "clusterBkg",
|
|
78
|
+
token: "--coar-background-neutral-secondary",
|
|
79
|
+
kind: "color"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
mermaidVar: "clusterBorder",
|
|
83
|
+
token: "--coar-border-neutral-tertiary",
|
|
84
|
+
kind: "color"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
mermaidVar: "lineColor",
|
|
88
|
+
token: "--coar-border-neutral-secondary",
|
|
89
|
+
kind: "color"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
mermaidVar: "textColor",
|
|
93
|
+
token: "--coar-text-neutral-primary",
|
|
94
|
+
kind: "color"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
mermaidVar: "background",
|
|
98
|
+
token: "--coar-background-neutral-primary",
|
|
99
|
+
kind: "color"
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
function y(e, t = (e) => e) {
|
|
103
|
+
let n = {};
|
|
104
|
+
for (let { mermaidVar: r, token: i, kind: a } of v) {
|
|
105
|
+
let o = e(i).trim();
|
|
106
|
+
if (!o) continue;
|
|
107
|
+
let s = a === "color" ? t(o) : o;
|
|
108
|
+
s && (n[r] = s);
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
function b(e) {
|
|
113
|
+
let t = e ?? (typeof document < "u" ? document.documentElement : null);
|
|
114
|
+
if (!t || typeof getComputedStyle > "u") return () => "";
|
|
115
|
+
let n = getComputedStyle(t);
|
|
116
|
+
return (e) => n.getPropertyValue(e);
|
|
117
|
+
}
|
|
118
|
+
function x() {
|
|
119
|
+
if (typeof document > "u") return () => "";
|
|
120
|
+
let e = document.createElement("canvas");
|
|
121
|
+
e.width = 1, e.height = 1;
|
|
122
|
+
let t = e.getContext("2d", { willReadFrequently: !0 });
|
|
123
|
+
return t ? (e) => {
|
|
124
|
+
if (!e) return "";
|
|
125
|
+
t.fillStyle = "#000000", t.fillStyle = e;
|
|
126
|
+
let n = t.fillStyle;
|
|
127
|
+
if (t.fillStyle = "#ffffff", t.fillStyle = e, t.fillStyle !== n) return "";
|
|
128
|
+
t.clearRect(0, 0, 1, 1), t.fillRect(0, 0, 1, 1);
|
|
129
|
+
let [r, i, a, o] = t.getImageData(0, 0, 1, 1).data;
|
|
130
|
+
return o === 255 ? `rgb(${r}, ${i}, ${a})` : `rgba(${r}, ${i}, ${a}, ${(o / 255).toFixed(3)})`;
|
|
131
|
+
} : () => "";
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/internal/pan-zoom.ts
|
|
135
|
+
function S(e, t, n = {}) {
|
|
136
|
+
let r = n.minScale ?? .3, i = n.maxScale ?? 8, a = n.zoomSpeed ?? .0015, o = n.step ?? 1.25, s = 1, c = 0, l = 0, u = !1, d = -1, f = 0, p = 0, m = 0, h = 0;
|
|
137
|
+
t.style.transformOrigin = "0 0";
|
|
138
|
+
let g = () => {
|
|
139
|
+
t.style.transform = `translate(${c}px, ${l}px) scale(${s})`;
|
|
140
|
+
}, _ = (e, t, n) => {
|
|
141
|
+
let a = C(s * n, r, i), o = a / s;
|
|
142
|
+
c = e - o * (e - c), l = t - o * (t - l), s = a, g();
|
|
143
|
+
}, v = (t) => {
|
|
144
|
+
let n = e.getBoundingClientRect();
|
|
145
|
+
_(n.width / 2, n.height / 2, t);
|
|
146
|
+
}, y = (t) => {
|
|
147
|
+
if (!(t.ctrlKey || t.metaKey)) return;
|
|
148
|
+
t.preventDefault();
|
|
149
|
+
let n = e.getBoundingClientRect();
|
|
150
|
+
_(t.clientX - n.left, t.clientY - n.top, Math.exp(-t.deltaY * a));
|
|
151
|
+
}, b = (t) => {
|
|
152
|
+
t.button !== 0 || t.pointerType === "touch" || (u = !0, d = t.pointerId, f = t.clientX, p = t.clientY, m = c, h = l, e.setPointerCapture(d), e.classList.add("is-panning"));
|
|
153
|
+
}, x = (e) => {
|
|
154
|
+
u && (c = m + (e.clientX - f), l = h + (e.clientY - p), g());
|
|
155
|
+
}, S = () => {
|
|
156
|
+
if (u) {
|
|
157
|
+
u = !1;
|
|
158
|
+
try {
|
|
159
|
+
e.releasePointerCapture(d);
|
|
160
|
+
} catch {}
|
|
161
|
+
e.classList.remove("is-panning");
|
|
162
|
+
}
|
|
163
|
+
}, w = () => {
|
|
164
|
+
s = 1;
|
|
165
|
+
let n = t.style.transform;
|
|
166
|
+
t.style.transform = "none";
|
|
167
|
+
let r = e.getBoundingClientRect(), i = t.getBoundingClientRect();
|
|
168
|
+
t.style.transform = n, c = Math.max(0, (r.width - i.width) / 2), l = Math.max(0, (r.height - i.height) / 2), g();
|
|
169
|
+
};
|
|
170
|
+
return e.addEventListener("wheel", y, { passive: !1 }), e.addEventListener("pointerdown", b), e.addEventListener("pointermove", x), e.addEventListener("pointerup", S), e.addEventListener("pointercancel", S), e.addEventListener("dblclick", w), w(), {
|
|
171
|
+
zoomIn: () => v(o),
|
|
172
|
+
zoomOut: () => v(1 / o),
|
|
173
|
+
reset: w,
|
|
174
|
+
destroy() {
|
|
175
|
+
e.removeEventListener("wheel", y), e.removeEventListener("pointerdown", b), e.removeEventListener("pointermove", x), e.removeEventListener("pointerup", S), e.removeEventListener("pointercancel", S), e.removeEventListener("dblclick", w);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function C(e, t, n) {
|
|
180
|
+
return Math.min(n, Math.max(t, e));
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/CoarMermaidDiagram.vue?vue&type=script&setup=true&lang.ts
|
|
184
|
+
var w = ["data-status"], T = ["innerHTML"], E = {
|
|
185
|
+
key: 1,
|
|
186
|
+
class: "coar-mermaid__loading"
|
|
187
|
+
}, D = {
|
|
188
|
+
key: 2,
|
|
189
|
+
class: "coar-mermaid__error",
|
|
190
|
+
role: "alert"
|
|
191
|
+
}, O = { class: "coar-mermaid__error-message" }, k = { class: "coar-mermaid__error-source" }, A = {
|
|
192
|
+
key: 0,
|
|
193
|
+
class: "coar-mermaid__controls"
|
|
194
|
+
}, j = /* @__PURE__ */ r({
|
|
195
|
+
__name: "CoarMermaidDiagram",
|
|
196
|
+
props: {
|
|
197
|
+
code: {},
|
|
198
|
+
language: { default: "mermaid" },
|
|
199
|
+
zoomable: {
|
|
200
|
+
type: Boolean,
|
|
201
|
+
default: !1
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
setup(r) {
|
|
205
|
+
let p = r, m = l(null), h = l(null), g = l(null), v = l("idle"), C = l(""), j = l(""), M = 0, N = u(null);
|
|
206
|
+
function P() {
|
|
207
|
+
N.value?.destroy(), N.value = null;
|
|
208
|
+
}
|
|
209
|
+
async function F() {
|
|
210
|
+
if (typeof window > "u") return;
|
|
211
|
+
let e = p.code?.trim() ?? "";
|
|
212
|
+
if (P(), !e) {
|
|
213
|
+
v.value = "idle", C.value = "", j.value = "";
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let t = ++M;
|
|
217
|
+
v.value = "loading";
|
|
218
|
+
try {
|
|
219
|
+
let n = await _(() => ({
|
|
220
|
+
theme: "base",
|
|
221
|
+
themeVariables: y(b(m.value), x())
|
|
222
|
+
}), e);
|
|
223
|
+
if (t !== M) return;
|
|
224
|
+
C.value = n, j.value = "", v.value = "rendered", p.zoomable && (await i(), t === M && h.value && g.value && (N.value = S(h.value, g.value)));
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (t !== M) return;
|
|
227
|
+
C.value = "", j.value = e instanceof Error ? e.message : String(e), v.value = "error";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return s(F), f(() => p.code, F), o(P), (i, o) => (c(), t("div", {
|
|
231
|
+
ref_key: "container",
|
|
232
|
+
ref: m,
|
|
233
|
+
class: a(["coar-mermaid", { "coar-mermaid--zoomable": r.zoomable }]),
|
|
234
|
+
"data-status": v.value
|
|
235
|
+
}, [n("div", {
|
|
236
|
+
ref_key: "viewport",
|
|
237
|
+
ref: h,
|
|
238
|
+
class: "coar-mermaid__viewport"
|
|
239
|
+
}, [v.value === "rendered" ? (c(), t("div", {
|
|
240
|
+
key: 0,
|
|
241
|
+
ref_key: "content",
|
|
242
|
+
ref: g,
|
|
243
|
+
class: "coar-mermaid__svg",
|
|
244
|
+
innerHTML: C.value
|
|
245
|
+
}, null, 8, T)) : v.value === "loading" ? (c(), t("div", E, " Rendering diagram… ")) : v.value === "error" ? (c(), t("div", D, [
|
|
246
|
+
o[3] ||= n("p", { class: "coar-mermaid__error-title" }, "Diagram error", -1),
|
|
247
|
+
n("p", O, d(j.value), 1),
|
|
248
|
+
n("pre", k, d(r.code), 1)
|
|
249
|
+
])) : e("", !0)], 512), r.zoomable && v.value === "rendered" ? (c(), t("div", A, [
|
|
250
|
+
n("button", {
|
|
251
|
+
type: "button",
|
|
252
|
+
title: "Zoom out",
|
|
253
|
+
"aria-label": "Zoom out",
|
|
254
|
+
onClick: o[0] ||= (e) => N.value?.zoomOut()
|
|
255
|
+
}, " − "),
|
|
256
|
+
n("button", {
|
|
257
|
+
type: "button",
|
|
258
|
+
title: "Zoom in",
|
|
259
|
+
"aria-label": "Zoom in",
|
|
260
|
+
onClick: o[1] ||= (e) => N.value?.zoomIn()
|
|
261
|
+
}, " + "),
|
|
262
|
+
n("button", {
|
|
263
|
+
type: "button",
|
|
264
|
+
title: "Reset view (or double-click)",
|
|
265
|
+
"aria-label": "Reset view",
|
|
266
|
+
onClick: o[2] ||= (e) => N.value?.reset()
|
|
267
|
+
}, " ⤢ ")
|
|
268
|
+
])) : e("", !0)], 10, w));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
//#endregion
|
|
272
|
+
export { j as CoarMermaidDiagram, y as buildMermaidThemeVariables, x as makeCssColorResolver, b as readCssTokens };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, one-time loader for the Mermaid engine.
|
|
3
|
+
*
|
|
4
|
+
* Mermaid is heavy (it pulls in d3, dagre, …) and DOM-bound, so it is *never*
|
|
5
|
+
* imported eagerly: the first diagram that mounts triggers a dynamic
|
|
6
|
+
* `import('mermaid')`, which the consumer's bundler splits into its own chunk.
|
|
7
|
+
* Subsequent diagrams reuse the same resolved instance.
|
|
8
|
+
*
|
|
9
|
+
* Mermaid is initialized exactly once, with `startOnLoad: false` (we drive
|
|
10
|
+
* rendering imperatively) and `securityLevel: 'strict'` (author diagram text is
|
|
11
|
+
* untrusted — strict mode sanitizes HTML in labels and blocks click bindings).
|
|
12
|
+
* The theme config is captured at that first initialization; a reactive
|
|
13
|
+
* theme-swap at runtime is a deliberate follow-up, not part of this slice.
|
|
14
|
+
*/
|
|
15
|
+
/** The slice of Mermaid's API this package uses. */
|
|
16
|
+
export interface MermaidApi {
|
|
17
|
+
render(id: string, text: string): Promise<{
|
|
18
|
+
svg: string;
|
|
19
|
+
bindFunctions?: (element: Element) => void;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
/** Init config passed to Mermaid — kept loose to stay version-tolerant. */
|
|
23
|
+
export type MermaidInitConfig = Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the initialized Mermaid instance, importing + initializing it on the
|
|
26
|
+
* first call. `configFactory` runs once, on that first call, and must read any
|
|
27
|
+
* runtime theme tokens it needs then (it is not re-consulted afterward).
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadMermaid(configFactory: () => MermaidInitConfig): Promise<MermaidApi>;
|
|
30
|
+
/**
|
|
31
|
+
* Render one diagram to an SVG string, serialized against every other render.
|
|
32
|
+
* Loads + initializes Mermaid on first use (via {@link loadMermaid}); rejects if
|
|
33
|
+
* the source is invalid so the caller can show a fallback.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderMermaid(configFactory: () => MermaidInitConfig, code: string): Promise<string>;
|
|
36
|
+
/** Test-only hook: forget the cached instance so a fresh load can be asserted. */
|
|
37
|
+
export declare function resetMermaidForTests(): void;
|
|
38
|
+
//# sourceMappingURL=mermaid-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mermaid-loader.d.ts","sourceRoot":"","sources":["../../src/internal/mermaid-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,MAAM,CACJ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAC,CAAC;CACzE;AAED,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIxD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAuBvF;AAWD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,iBAAiB,EACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free pan + zoom for a rendered diagram.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately does NOT hijack the plain mouse wheel — that would trap page
|
|
5
|
+
* scrolling ("I scroll and get stuck on the diagram"). Instead:
|
|
6
|
+
* - plain wheel → the page scrolls normally (we don't touch it),
|
|
7
|
+
* - Ctrl/⌘ + wheel → zoom toward the cursor,
|
|
8
|
+
* - the +/−/reset controls (driven by {@link PanZoomHandle}) → zoom, and
|
|
9
|
+
* - mouse / pen drag → pan.
|
|
10
|
+
* Touch is intentionally left to the browser (one-finger touch scrolls the page),
|
|
11
|
+
* so a diagram never blocks scrolling on a tablet — zoom there is via the buttons.
|
|
12
|
+
*
|
|
13
|
+
* The caller owns the DOM: it passes the clipping `viewport` (overflow hidden)
|
|
14
|
+
* and the `content` wrapper (the element holding Mermaid's SVG). A CSS transform
|
|
15
|
+
* on `content` is all that's needed — no d3 / svg-pan-zoom.
|
|
16
|
+
*/
|
|
17
|
+
export interface PanZoomHandle {
|
|
18
|
+
/** Zoom in one step, toward the viewport center. */
|
|
19
|
+
zoomIn(): void;
|
|
20
|
+
/** Zoom out one step, from the viewport center. */
|
|
21
|
+
zoomOut(): void;
|
|
22
|
+
/** Recenter + reset zoom to 1. */
|
|
23
|
+
reset(): void;
|
|
24
|
+
/** Remove every listener. Idempotent. */
|
|
25
|
+
destroy(): void;
|
|
26
|
+
}
|
|
27
|
+
export interface PanZoomOptions {
|
|
28
|
+
minScale?: number;
|
|
29
|
+
maxScale?: number;
|
|
30
|
+
/** Ctrl/⌘+wheel sensitivity; larger = faster zoom. */
|
|
31
|
+
zoomSpeed?: number;
|
|
32
|
+
/** Multiplier applied by the +/− buttons per click. */
|
|
33
|
+
step?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function createPanZoom(viewport: HTMLElement, content: HTMLElement, options?: PanZoomOptions): PanZoomHandle;
|
|
36
|
+
//# sourceMappingURL=pan-zoom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pan-zoom.d.ts","sourceRoot":"","sources":["../../src/internal/pan-zoom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,MAAM,IAAI,IAAI,CAAC;IACf,mDAAmD;IACnD,OAAO,IAAI,IAAI,CAAC;IAChB,kCAAkC;IAClC,KAAK,IAAI,IAAI,CAAC;IACd,yCAAyC;IACzC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,WAAW,EACpB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAiHf"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges Cocoar design tokens → Mermaid `themeVariables`, so a rendered
|
|
3
|
+
* diagram picks up the app's fonts and palette instead of Mermaid's stock look.
|
|
4
|
+
*
|
|
5
|
+
* The mapping is expressed as a pure function over a *token getter* + a *color
|
|
6
|
+
* resolver* so it can be unit-tested without a DOM (feed it fakes).
|
|
7
|
+
* {@link readCssTokens} produces the real, browser-backed getter (reads CSS
|
|
8
|
+
* custom properties via `getComputedStyle`); {@link makeCssColorResolver}
|
|
9
|
+
* produces the real color resolver.
|
|
10
|
+
*
|
|
11
|
+
* Why a color resolver at all: Cocoar's color tokens resolve to CSS Color-4
|
|
12
|
+
* values — e.g. `oklch(from #1183CD 0.92 0.035 h)` — which Mermaid's color parser
|
|
13
|
+
* (khroma) can't read, so a raw hand-off makes every diagram error out. The
|
|
14
|
+
* resolver rasterizes each color to concrete sRGB `rgb()/rgba()` (a format
|
|
15
|
+
* Mermaid always understands) and rejects anything unparseable, so a bad token is
|
|
16
|
+
* dropped rather than poisoning the whole theme.
|
|
17
|
+
*
|
|
18
|
+
* Only tokens that resolve to a non-empty value are emitted — a missing (or
|
|
19
|
+
* unparseable) token is simply omitted, letting Mermaid's `base` theme fill the
|
|
20
|
+
* gap rather than being handed an empty string.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Build the `themeVariables` map from a token getter + a color resolver.
|
|
24
|
+
* `getToken(name)` returns the resolved CSS value for a custom property (or `''`
|
|
25
|
+
* when unset); `resolveColor(value)` normalizes a CSS color to a Mermaid-safe
|
|
26
|
+
* form and returns `''` for anything unparseable. Both default to identity /
|
|
27
|
+
* pass-through so the function stays usable in a plain (DOM-free) unit test.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildMermaidThemeVariables(getToken: (name: string) => string, resolveColor?: (value: string) => string): Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Produce a token getter backed by `getComputedStyle` on the given element
|
|
32
|
+
* (defaults to `document.documentElement`). Returns a getter that always yields
|
|
33
|
+
* `''` when there is no DOM (SSR) or no element, so callers stay side-effect and
|
|
34
|
+
* crash free on the server.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readCssTokens(el?: Element | null): (name: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* Produce a color resolver that normalizes any CSS color the browser can paint
|
|
39
|
+
* — including CSS Color-4 forms like `oklch(...)` — into a concrete sRGB
|
|
40
|
+
* `rgb()/rgba()` string that Mermaid understands. Unparseable input yields `''`.
|
|
41
|
+
*
|
|
42
|
+
* Mechanism: paint the color onto a 1×1 canvas and read the pixel back. Canvas
|
|
43
|
+
* rasterizes to 8-bit sRGB, so the read-back is always plain rgb regardless of
|
|
44
|
+
* the source color space. Validity is probed first by assigning the value over
|
|
45
|
+
* two distinct sentinels — a color the canvas rejects leaves them differing.
|
|
46
|
+
*
|
|
47
|
+
* Returns a getter yielding `''` when there is no DOM / no 2D context (SSR).
|
|
48
|
+
*/
|
|
49
|
+
export declare function makeCssColorResolver(): (value: string) => string;
|
|
50
|
+
//# sourceMappingURL=theme-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-bridge.d.ts","sourceRoot":"","sources":["../../src/internal/theme-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAyBH;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAClC,YAAY,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAyB,GACzD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAQ3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CA4BhE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cocoar/vue-mermaid",
|
|
3
|
+
"version": "2.14.0",
|
|
4
|
+
"description": "Standalone Mermaid diagram component for Vue 3: <CoarMermaidDiagram :code>. Cocoar-themed, lazy-loaded, with opt-in zoom/pan. Knows nothing about markdown — feed it a diagram source string.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cocoar-dev/cocoar-ui-vue.git",
|
|
9
|
+
"directory": "packages/mermaid"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.cocoar.dev/cocoar-ui-vue/",
|
|
12
|
+
"bugs": "https://github.com/cocoar-dev/cocoar-ui-vue/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vue",
|
|
15
|
+
"vue3",
|
|
16
|
+
"mermaid",
|
|
17
|
+
"diagram"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"*.css",
|
|
22
|
+
"*.vue"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./styles": "./dist/index.css"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "vite build",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"lint": "eslint src/",
|
|
41
|
+
"typecheck": "vue-tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"mermaid": "^11.4.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"vue": "^3.5.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"vue": "^3.5.32"
|
|
51
|
+
}
|
|
52
|
+
}
|