Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions src/components/docs/Mermaid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
// Renders a Mermaid diagram. Source is passed via the `code` prop:
//
// <Mermaid code={`flowchart LR
// A --> B
// `} />
//
// Mermaid is loaded once per page via a hoisted, deduplicated <script>.
// We re-run on theme changes so dark/light switches re-render the diagram.

const { code } = Astro.props as { code: string };
---

<div class="mermaid-wrapper" data-mermaid>
<pre class="mermaid">{code}</pre>
</div>

<script>
// Lazy-load mermaid from CDN the first time a diagram appears on the page.
// Subsequent pages with diagrams reuse the cached module thanks to the CDN
// and the browser HTTP cache; this script tag is hoisted+deduplicated by
// Astro so it only runs once per page even if the component renders many times.

declare global {
interface Window {
__fagiMermaidPromise?: Promise<typeof import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs')>;
}
}

async function loadMermaid() {
if (!window.__fagiMermaidPromise) {
window.__fagiMermaidPromise = import(
/* @vite-ignore */
'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'
);
}
const mod = await window.__fagiMermaidPromise;
return (mod as any).default;
}

function isDarkTheme(): boolean {
if (typeof document === 'undefined') return true;
const root = document.documentElement;
if (root.dataset.theme) return root.dataset.theme === 'dark';
if (root.classList.contains('dark')) return true;
if (root.classList.contains('light')) return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

function themeConfig() {
const dark = isDarkTheme();
return {
startOnLoad: false,
theme: 'base' as const,
securityLevel: 'loose' as const,
flowchart: { htmlLabels: true, curve: 'basis' },
themeVariables: dark
? {
background: '#0a0a0a',
primaryColor: '#1f1f23',
primaryTextColor: '#e5e7eb',
primaryBorderColor: '#3f3f46',
lineColor: '#8b5cf6',
secondaryColor: '#18181b',
tertiaryColor: '#111113',
textColor: '#e5e7eb',
mainBkg: '#1f1f23',
secondBkg: '#27272a',
tertiaryBkg: '#111113',
nodeBorder: '#3f3f46',
clusterBkg: '#111113',
clusterBorder: '#27272a',
labelTextColor: '#e5e7eb',
edgeLabelBackground: '#18181b',
actorBkg: '#1f1f23',
actorBorder: '#3f3f46',
actorTextColor: '#e5e7eb',
actorLineColor: '#6b7280',
signalColor: '#a78bfa',
signalTextColor: '#e5e7eb',
sequenceNumberColor: '#0a0a0a',
noteBkgColor: '#312e81',
noteTextColor: '#e5e7eb',
noteBorderColor: '#4338ca',
}
: {
background: '#ffffff',
primaryColor: '#f3f4f6',
primaryTextColor: '#111827',
primaryBorderColor: '#d1d5db',
lineColor: '#6d28d9',
textColor: '#111827',
mainBkg: '#f9fafb',
edgeLabelBackground: '#ffffff',
},
};
}

async function render() {
const wrappers = document.querySelectorAll<HTMLElement>('[data-mermaid]');
if (wrappers.length === 0) return;
const mermaid = await loadMermaid();
mermaid.initialize(themeConfig());

// Reset any previously-rendered diagrams before re-running.
for (const wrap of wrappers) {
const pre = wrap.querySelector<HTMLElement>('pre.mermaid');
if (!pre) continue;
if (wrap.dataset.mermaidSource) {
pre.textContent = wrap.dataset.mermaidSource;
} else {
wrap.dataset.mermaidSource = pre.textContent ?? '';
}
pre.removeAttribute('data-processed');
}

await mermaid.run({ querySelector: '[data-mermaid] pre.mermaid' });
}

function init() {
render().catch((err) => console.error('Mermaid render failed', err));
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

// Re-render on theme toggle (the site uses data-theme or .dark on <html>).
const themeObserver = new MutationObserver(() => render().catch(() => {}));
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme', 'class'],
});

// Re-render after Astro view transitions, if the site uses them.
document.addEventListener('astro:after-swap', init);
</script>

<style>
.mermaid-wrapper {
margin: 1.25rem 0;
padding: 1.25rem;
background: var(--mermaid-bg, rgba(255, 255, 255, 0.02));
border: 1px solid var(--color-border-default, #27272a);
border-radius: 0.75rem;
overflow-x: auto;
}
.mermaid-wrapper pre.mermaid {
background: transparent !important;
border: 0 !important;
padding: 0 !important;
margin: 0 !important;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.85rem;
color: var(--color-text-muted, #9ca3af);
text-align: center;
}
.mermaid-wrapper svg {
max-width: 100%;
height: auto;
display: inline-block;
}
</style>
Loading
Loading