astro-better-mermaid 0.1.1

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/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Better Static Sites
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # astro-better-mermaid
2
+
3
+ Astro integration that renders ` ```mermaid ` code blocks to inline SVG at build time. No client-side JavaScript is shipped to the browser.
4
+
5
+ Rendering happens via [svgdom](https://github.com/svgdotjs/svgdom), a headless DOM shim that convinces mermaid it is running in a browser. It is not running in a browser.
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ npm install astro-better-mermaid mermaid svgdom
11
+ ```
12
+
13
+ `mermaid` and `svgdom` are peer dependencies — install them alongside this package.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ // astro.config.ts
19
+ import { defineConfig } from 'astro/config';
20
+ import mermaidSSR from 'astro-better-mermaid';
21
+
22
+ export default defineConfig({
23
+ integrations: [mermaidSSR()],
24
+ });
25
+ ```
26
+
27
+ Mermaid code blocks in your Markdown and MDX files will be replaced with pre-rendered SVG wrapped in a `<div class="mermaid">` at build time. Blocks that fail to render are left as fenced code blocks.
28
+
29
+ ## Options
30
+
31
+ ```ts
32
+ mermaidSSR({
33
+ theme: 'default', // mermaid theme — 'default', 'dark', 'neutral', 'forest'
34
+ securityLevel: 'loose', // mermaid securityLevel
35
+ titleFix: true, // parse title="..." from code block meta as a caption
36
+ injectCSS: true, // inject the bundled stylesheet automatically
37
+ })
38
+ ```
39
+
40
+ ### Diagram titles
41
+
42
+ With `titleFix` enabled, you can add a caption to any code block via its meta string:
43
+
44
+ ````
45
+ ```mermaid title="Authentication flow"
46
+ ...
47
+ ```
48
+ ````
49
+
50
+ Captions on mermaid blocks appear below the diagram; captions on other code blocks appear above.
51
+
52
+ ## Styles
53
+
54
+ The integration injects a default stylesheet that covers light mode and dark mode. Dark mode is driven by a `.dark` class on an ancestor element (the Tailwind `darkMode: 'class'` convention).
55
+
56
+ To use your own styles instead, set `injectCSS: false` and import the bundled CSS as a starting point:
57
+
58
+ ```css
59
+ @import 'astro-better-mermaid/styles.css';
60
+ ```
61
+
62
+ ### Custom cluster fill colors
63
+
64
+ Mermaid's `style X fill:#color` directive injects `!important` inline styles that block CSS overrides. This integration strips the `!important` from cluster background rects and stores the original color in a `data-fill-color` attribute. The default stylesheet handles `#999` and `#944`. For other colors, add rules of the form:
65
+
66
+ ```css
67
+ .cluster > rect[data-fill-color="#abc"] { fill: your-color !important; }
68
+ .dark .cluster > rect[data-fill-color="#abc"] { fill: your-dark-color !important; }
69
+ ```
70
+
71
+ ## Using the remark plugins directly
72
+
73
+ If you prefer to wire up the remark plugins yourself:
74
+
75
+ ```ts
76
+ import { remarkMermaidSSR, mermaidTitleFix } from 'astro-better-mermaid';
77
+
78
+ export default defineConfig({
79
+ markdown: {
80
+ remarkPlugins: [
81
+ mermaidTitleFix,
82
+ [remarkMermaidSSR, { theme: 'default' }],
83
+ ],
84
+ },
85
+ vite: {
86
+ ssr: { external: ['svgdom', 'mermaid'] },
87
+ },
88
+ });
89
+ ```
90
+
91
+ `svgdom` and `mermaid` must be listed in `vite.ssr.external` — they are Node-only packages and cannot be bundled into client output.
package/index.mjs ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * astro-mermaid-ssr
3
+ *
4
+ * Astro integration + remark plugins for server-side mermaid diagram rendering.
5
+ * No client-side JavaScript required — diagrams are pre-rendered to SVG at build time.
6
+ *
7
+ * Usage (astro.config.ts):
8
+ *
9
+ * import mermaidSSR from 'astro-mermaid-ssr';
10
+ * export default defineConfig({
11
+ * integrations: [mermaidSSR()],
12
+ * });
13
+ *
14
+ * Options:
15
+ * titleFix {boolean} — Include the title="..." remark plugin (default: true)
16
+ * injectCSS {boolean} — Auto-inject default styles (default: true)
17
+ * theme {string} — Mermaid theme, e.g. 'default', 'dark', 'neutral' (default: 'default')
18
+ * securityLevel {string} — Mermaid securityLevel (default: 'loose')
19
+ *
20
+ * If injectCSS is false, import the styles yourself:
21
+ * import 'astro-mermaid-ssr/styles.css';
22
+ *
23
+ * To use the remark plugins directly without the integration:
24
+ * import { remarkMermaidSSR, mermaidTitleFix } from 'astro-mermaid-ssr';
25
+ */
26
+
27
+ import { fileURLToPath } from 'node:url';
28
+ import { remarkMermaidSSR, mermaidTitleFix } from './src/remark.mjs';
29
+
30
+ export { remarkMermaidSSR, mermaidTitleFix };
31
+
32
+ const STYLES_PATH = fileURLToPath(new URL('./src/styles.css', import.meta.url));
33
+
34
+ export default function astroMermaidSSR(opts = {}) {
35
+ const {
36
+ titleFix = true,
37
+ injectCSS = true,
38
+ theme = 'default',
39
+ securityLevel = 'loose',
40
+ } = opts;
41
+
42
+ return {
43
+ name: 'astro-mermaid-ssr',
44
+ hooks: {
45
+ 'astro:config:setup': ({ updateConfig, injectScript }) => {
46
+ updateConfig({
47
+ markdown: {
48
+ remarkPlugins: [
49
+ ...(titleFix ? [mermaidTitleFix] : []),
50
+ [remarkMermaidSSR, { theme, securityLevel }],
51
+ ],
52
+ },
53
+ vite: {
54
+ ssr: {
55
+ // svgdom and mermaid are Node-only packages; they must not be bundled
56
+ // into the client-side Vite output.
57
+ external: ['svgdom', 'mermaid'],
58
+ },
59
+ },
60
+ });
61
+
62
+ if (injectCSS) {
63
+ injectScript('page-ssr', `import ${JSON.stringify(STYLES_PATH)}`);
64
+ }
65
+ },
66
+ },
67
+ };
68
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "astro-better-mermaid",
3
+ "version": "0.1.1",
4
+ "description": "Astro integration for server-side mermaid diagram rendering via svgdom — no client-side JavaScript required",
5
+ "keywords": [
6
+ "astro",
7
+ "astro-integration",
8
+ "mermaid",
9
+ "ssr",
10
+ "remark",
11
+ "diagram"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Nathan Contino <ncontino@lambdalatitudinarians.org>",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/better-static-sites/better-static-sites.github.io.git",
18
+ "directory": "astro-better-mermaid"
19
+ },
20
+ "homepage": "https://better-static-sites.github.io",
21
+ "bugs": {
22
+ "url": "https://github.com/better-static-sites/better-static-sites.github.io/issues"
23
+ },
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./index.mjs",
27
+ "./styles.css": "./src/styles.css"
28
+ },
29
+ "files": [
30
+ "index.mjs",
31
+ "src/"
32
+ ],
33
+ "scripts": {
34
+ "test": "node --test test/test.mjs"
35
+ },
36
+ "dependencies": {
37
+ "svgdom": "^0.1.28",
38
+ "unist-util-visit": "^5.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "astro": ">=4.0.0",
42
+ "mermaid": ">=10.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "mermaid": {
46
+ "optional": false
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "mermaid": "^11.0.0"
51
+ }
52
+ }
package/src/remark.mjs ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Remark plugins for mermaid SSR.
3
+ *
4
+ * remarkMermaidSSR — renders ```mermaid blocks to inline SVG at build time using
5
+ * svgdom as a headless browser shim. Diagrams that fail are
6
+ * left as fenced code blocks.
7
+ *
8
+ * mermaidTitleFix — reads `title="..."` from mermaid code block meta and emits
9
+ * a <p data-title-bottom> node below the diagram. Non-mermaid
10
+ * blocks are left alone; rehypeCodeBlocks handles their titles.
11
+ *
12
+ * IMPORTANT: mermaid must be imported at module-evaluation time — Vite's module
13
+ * runner closes before remark callbacks fire, making dynamic imports impossible
14
+ * inside the plugin body.
15
+ */
16
+
17
+ import { visit } from 'unist-util-visit';
18
+ import { createRequire } from 'node:module';
19
+ import { webcrypto } from 'node:crypto';
20
+
21
+ const _require = createRequire(import.meta.url);
22
+
23
+ function setupSvgdom() {
24
+ let createSVGWindow;
25
+ try { ({ createSVGWindow } = _require('svgdom')); } catch { return null; }
26
+
27
+ const win = createSVGWindow();
28
+ const doc = win.document;
29
+
30
+ // svgdom creates SVG documents with no <body>; mermaid's d3 needs select("body").
31
+ const body = doc.createElement('body');
32
+ doc.documentElement.appendChild(body);
33
+ Object.defineProperty(doc, 'body', { get: () => body, configurable: true });
34
+
35
+ class CSSStyleSheet {
36
+ constructor() { this.cssRules = []; }
37
+ insertRule(rule) { this.cssRules.push({ cssText: rule }); }
38
+ }
39
+
40
+ // svgdom only implements SVG element geometry; mermaid calls this on HTML elements
41
+ // to measure label text. Return a plausible box based on character count.
42
+ if (win.HTMLElement) {
43
+ win.HTMLElement.prototype.getBoundingClientRect = function () {
44
+ const w = Math.max(50, (this.textContent || '').length * 8);
45
+ return { width: w, height: 24, top: 0, left: 0, right: w, bottom: 24, x: 0, y: 0 };
46
+ };
47
+ }
48
+
49
+ win.location = new URL('http://localhost/');
50
+ win.crypto = webcrypto;
51
+ win.CSSStyleSheet = CSSStyleSheet;
52
+ win.addEventListener = () => {};
53
+ win.removeEventListener = () => {};
54
+ win.dispatchEvent = () => false;
55
+ win.getComputedStyle = () => new Proxy({}, { get: () => '' });
56
+ win.MutationObserver = class { observe(){} disconnect(){} takeRecords(){ return []; } };
57
+ win.ResizeObserver = class { observe(){} disconnect(){} };
58
+
59
+ globalThis.window = win;
60
+ globalThis.document = doc;
61
+ globalThis.CSSStyleSheet = CSSStyleSheet;
62
+ for (const k of ['SVGElement', 'HTMLElement', 'Element', 'Node', 'DocumentFragment', 'Event']) {
63
+ if (win[k]) globalThis[k] = win[k];
64
+ }
65
+
66
+ return { win, doc };
67
+ }
68
+
69
+ const domReady = setupSvgdom();
70
+
71
+ // Import mermaid at module-evaluation time — see file header.
72
+ const _mermaidImport = domReady
73
+ ? import('mermaid').then(m => m.default).catch(err => {
74
+ console.warn(`[astro-mermaid-ssr] Failed to import mermaid: ${err.message}`);
75
+ return null;
76
+ })
77
+ : Promise.resolve(null);
78
+
79
+ /**
80
+ * Remark plugin factory. Options:
81
+ * theme — mermaid theme (default: 'default')
82
+ * securityLevel — mermaid securityLevel (default: 'loose')
83
+ */
84
+ export function remarkMermaidSSR(options = {}) {
85
+ const {
86
+ theme = 'default',
87
+ securityLevel = 'loose',
88
+ } = options;
89
+
90
+ const mermaidReady = _mermaidImport.then(mermaid => {
91
+ if (!mermaid) return null;
92
+ mermaid.initialize({
93
+ startOnLoad: false,
94
+ theme,
95
+ securityLevel,
96
+ htmlLabels: false, // svgdom loses innerHTML inside <foreignObject>; use SVG text
97
+ });
98
+ return mermaid;
99
+ });
100
+
101
+ return async (tree) => {
102
+ const nodes = [];
103
+ visit(tree, 'code', (node) => {
104
+ if (node.lang === 'mermaid') nodes.push(node);
105
+ });
106
+ if (nodes.length === 0) return;
107
+
108
+ const mermaid = await mermaidReady;
109
+ if (!mermaid) return;
110
+
111
+ for (let i = 0; i < nodes.length; i++) {
112
+ const node = nodes[i];
113
+ try {
114
+ const id = `mermaid-ssr-${i}-${Math.random().toString(36).slice(2, 6)}`;
115
+ let { svg } = await mermaid.render(id, node.value);
116
+
117
+ // `style X fill:#color` directives inject `fill:color !important` on cluster
118
+ // background rects, blocking CSS theme overrides. Strip the !important and stash
119
+ // the color in data-fill-color so CSS can override per theme.
120
+ // Skip rects with a class attribute — those are node rects with intentional colors.
121
+ svg = svg.replace(
122
+ /(<rect\b[^>]*)\bstyle="fill:(#[0-9a-fA-F]{3,6})\s*!important([^"]*)"/g,
123
+ (match, prefix, color, rest) => {
124
+ if (/\bclass=/.test(prefix)) return match;
125
+ return `${prefix}data-fill-color="${color.toLowerCase()}" style="fill:${color}${rest}"`;
126
+ }
127
+ );
128
+
129
+ // Multi-word edge labels: the .label group has translate(dx, dy) but the outer
130
+ // tspan keeps x="0", placing text dx pixels left of its background rect.
131
+ // Shift the tspan's x by -dx to re-center it on the rect.
132
+ svg = svg.replace(
133
+ /(<g class="label"[^>]* transform="translate\()(-?[\d.]+)(,\s*-?[\d.]+\)"><g><rect[^>]*><\/rect><text[^>]*><tspan[^>]* )x="0"/g,
134
+ (_, pre, dxStr, mid) => {
135
+ const dx = parseFloat(dxStr);
136
+ if (Math.abs(dx) < 0.5) return pre + dxStr + mid + 'x="0"';
137
+ return `${pre}${dxStr}${mid}x="${-dx}"`;
138
+ }
139
+ );
140
+
141
+ const escapedSrc = node.value.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
142
+ node.type = 'html';
143
+ node.value = `<div class="mermaid" data-processed="true" data-mermaid-src="${escapedSrc}">\n${svg}\n</div>`;
144
+ delete node.lang;
145
+ delete node.meta;
146
+ } catch (err) {
147
+ console.warn(`[astro-mermaid-ssr] Diagram ${i} failed to render.\n ${err.message.slice(0, 120)}`);
148
+ }
149
+ }
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Remark plugin: reads `title="..."` from mermaid code block meta and emits a
155
+ * <p data-title-bottom> element AFTER (below) the diagram.
156
+ * Non-mermaid blocks are left alone -- rehypeCodeBlocks handles their titles.
157
+ */
158
+ export function mermaidTitleFix() {
159
+ return (tree) => {
160
+ visit(tree, 'code', (node, index, parent) => {
161
+ if (node.lang !== 'mermaid') return;
162
+ const meta = node.meta || '';
163
+ const titleMatch = meta.match(/title=["'](.*?)["']/);
164
+ if (!titleMatch) return;
165
+
166
+ const title = titleMatch[1];
167
+ const titleNode = {
168
+ type: 'paragraph',
169
+ data: {
170
+ hName: 'p',
171
+ hProperties: { 'data-title-bottom': title },
172
+ },
173
+ children: [{ type: 'text', value: title }],
174
+ };
175
+
176
+ parent.children.splice(index + 1, 0, titleNode);
177
+ return index + 2;
178
+ });
179
+ };
180
+ }
package/src/styles.css ADDED
@@ -0,0 +1,148 @@
1
+ /* astro-mermaid-ssr default styles
2
+ *
3
+ * Covers light mode ([id^="mermaid-"]) and dark mode (.dark [id^="mermaid-"]).
4
+ * Dark mode uses the .dark parent-class convention (e.g. Tailwind darkMode: 'class').
5
+ * If your project uses a different dark mode convention, import these rules as a
6
+ * starting point and adapt the selectors.
7
+ *
8
+ * Diagram titles (from title="..." meta) sit below mermaid blocks and above other
9
+ * code blocks. The .mermaid wrapper div comes from remarkMermaidSSR.
10
+ */
11
+
12
+ foreignObject { overflow: visible; }
13
+
14
+ /* Mermaid diagram title positioning */
15
+ .mermaid:has(+ p[data-title-bottom]) {
16
+ margin-bottom: 0 !important;
17
+ padding-bottom: 0 !important;
18
+ }
19
+ .mermaid + [data-title-bottom] {
20
+ display: block;
21
+ width: 100%;
22
+ margin: 0 !important;
23
+ padding: 0 1rem;
24
+ border-bottom-left-radius: 0.375rem;
25
+ border-bottom-right-radius: 0.375rem;
26
+ font-size: 0.7rem;
27
+ font-style: italic;
28
+ text-align: center;
29
+ letter-spacing: 0.025em;
30
+ }
31
+
32
+ /* --- Light mode --- */
33
+ [id^="mermaid-"] {
34
+ -webkit-font-smoothing: antialiased;
35
+ -moz-osx-font-smoothing: auto;
36
+ font-size: 16px !important;
37
+ width: 100%;
38
+
39
+ p { color: #000 !important; }
40
+ /* stroke on > * sets the default line/border color and is inherited by all descendants */
41
+ > * { stroke: #1e293b !important; }
42
+ line { stroke: #cbd5e1 !important; fill: #cbd5e1 !important; }
43
+ .cluster > rect { fill: none !important; stroke: #94a3b8 !important; }
44
+ .error-icon { fill: red !important; }
45
+ .error-text { fill: red !important; stroke: red !important; }
46
+
47
+ /* Lines & arrow markers */
48
+ .marker { fill: #1e293b !important; stroke: #1e293b !important; }
49
+ .marker.cross { stroke: red !important; }
50
+ .messageLine0, .messageLine1 { stroke: #64748b !important; }
51
+ .flowchart-link { stroke: #64748b !important; }
52
+ /* flowchart arrows (v2) use .arrowMarkerPath; sequence diagrams use marker IDs */
53
+ .arrowMarkerPath { fill: #64748b !important; stroke: none !important; }
54
+ [id$="-arrowhead"] path, [id$="-filled-head"] path,
55
+ [id$="-solidTopArrowHead"] path, [id$="-solidBottomArrowHead"] path { fill: #64748b !important; stroke: none !important; }
56
+ [id$="-crosshead"] path,
57
+ [id$="-stickTopArrowHead"] path, [id$="-stickBottomArrowHead"] path { fill: none !important; stroke: #64748b !important; }
58
+
59
+ /* Text */
60
+ .messageText, .labelText, .labelText > tspan, .noteText, .noteText > tspan { fill: #1e293b !important; stroke: none !important; }
61
+ .loopText, .loopText > tspan { fill: #f8fafc !important; stroke: none !important; }
62
+ .labelBox, .loopLine, .note { fill: #c7d2fe !important; stroke: #818cf8 !important; }
63
+ .nodeLabel p { color: #1e293b !important; }
64
+ .edgeLabel p { background-color: #e2e8f0 !important; color: #1e293b !important; }
65
+ /* stroke: none prevents the inherited stroke from outlining label text */
66
+ .node .label text, .node .label tspan,
67
+ .cluster-label text, .cluster-label tspan { fill: #1e293b !important; stroke: none !important; }
68
+ .edgeLabel text, .edgeLabel tspan { fill: #1e293b !important; stroke: none !important; }
69
+
70
+ /* Sequence diagram actors & activations */
71
+ .sequenceNumber { stroke: #eee !important; }
72
+ #sequencenumber circle { r: 9px; }
73
+ .actor { stroke: #1e293b !important; fill: #e2e8f0 !important; }
74
+ text.actor > tspan { fill: #1e293b !important; stroke: none !important; }
75
+ .actor-line { stroke: #1e293b !important; }
76
+ .actor-man line { fill: #e2e8f0 !important; stroke: #94a3b8 !important; }
77
+ .actor-man circle { fill: #e2e8f0 !important; stroke: #94a3b8 !important; }
78
+ .activation0 { fill: #e2e8f0 !important; stroke: #94a3b8 !important; }
79
+
80
+ /* Flowchart nodes */
81
+ .node :is(rect, circle, ellipse, polygon, path) { stroke: #1e293b !important; fill: #e2e8f0 !important; }
82
+
83
+ /* Edge label pill: SSR positions the background rect for y=-10.1 but tspan dy="1.1em"
84
+ renders ~16px lower. translateY corrects the vertical offset; rx/ry round the corners. */
85
+ .edgeLabel rect {
86
+ transform: translateY(16px) !important;
87
+ fill: rgba(226, 232, 240, 0.8) !important;
88
+ stroke: none !important;
89
+ rx: 4px; ry: 4px;
90
+ }
91
+
92
+ /* Cluster fills from `style X fill:#color` directives — the remark plugin strips
93
+ !important and adds data-fill-color so CSS can override per theme. Add rules here
94
+ for any fill colors used in your diagrams. */
95
+ .cluster > rect[data-fill-color="#999"] { fill: rgba(148, 163, 184, 0.15) !important; }
96
+ .cluster > rect[data-fill-color="#944"] { fill: rgba(239, 68, 68, 0.1) !important; }
97
+ }
98
+
99
+ /* --- Dark mode (requires .dark on an ancestor element) --- */
100
+ .dark [id^="mermaid-"] {
101
+ > * { stroke: #94a3b8 !important; }
102
+ line { stroke: #64748b !important; fill: #64748b !important; }
103
+ .cluster > rect { fill: none !important; stroke: #475569 !important; }
104
+
105
+ /* Lines & arrow markers */
106
+ .marker { fill: #e2e8f0 !important; stroke: #e2e8f0 !important; }
107
+ .messageLine0, .messageLine1 { stroke: #e2e8f0 !important; }
108
+ .flowchart-link { stroke: #e2e8f0 !important; }
109
+ .arrowMarkerPath { fill: #e2e8f0 !important; stroke: none !important; }
110
+ [id$="-arrowhead"] path, [id$="-filled-head"] path,
111
+ [id$="-solidTopArrowHead"] path, [id$="-solidBottomArrowHead"] path { fill: #e2e8f0 !important; stroke: none !important; }
112
+ [id$="-crosshead"] path,
113
+ [id$="-stickTopArrowHead"] path, [id$="-stickBottomArrowHead"] path { fill: none !important; stroke: #e2e8f0 !important; }
114
+
115
+ /* Text */
116
+ .messageText, .labelText, .labelText > tspan { fill: #f8fafc !important; stroke: none !important; }
117
+ .noteText, .noteText > tspan { fill: #f1f5f9 !important; stroke: none !important; }
118
+ .labelBox, .loopLine, .note { fill: #3730a3 !important; stroke: #818cf8 !important; }
119
+ .nodeLabel p { color: #FFF !important; }
120
+ .edgeLabel p { background-color: #1e293b !important; color: #FFF !important; }
121
+ .node .label text, .node .label tspan,
122
+ .cluster-label text, .cluster-label tspan { fill: #f8fafc !important; stroke: none !important; }
123
+ .edgeLabel text, .edgeLabel tspan { fill: #f8fafc !important; stroke: none !important; }
124
+
125
+ /* Sequence diagram actors & activations */
126
+ .sequenceNumber { fill: #334155 !important; }
127
+ .actor { stroke: #94a3b8 !important; fill: #1e293b !important; }
128
+ text.actor > tspan { fill: #f8fafc !important; stroke: none !important; }
129
+ .actor-line { stroke: #94a3b8 !important; }
130
+ .actor-man circle { fill: #1e293b !important; stroke: #94a3b8 !important; }
131
+ .activation0 { fill: #1e293b !important; stroke: #94a3b8 !important; }
132
+
133
+ /* Flowchart nodes */
134
+ .node :is(rect, circle, ellipse, polygon, path) { stroke: #94a3b8 !important; fill: #1e293b !important; }
135
+
136
+ /* Edge label pill */
137
+ .edgeLabel rect {
138
+ transform: translateY(16px) !important;
139
+ fill: rgba(30, 41, 59, 0.85) !important;
140
+ stroke: none !important;
141
+ opacity: 1 !important;
142
+ rx: 4px; ry: 4px;
143
+ }
144
+
145
+ /* Cluster fills — purple tint for neutral groups, red for error/external groups */
146
+ .cluster > rect[data-fill-color="#999"] { fill: rgba(139, 92, 246, 0.18) !important; }
147
+ .cluster > rect[data-fill-color="#944"] { fill: rgba(239, 68, 68, 0.2) !important; }
148
+ }