blog infra

This commit is contained in:
Emergent Reasons
2025-10-16 06:23:59 +00:00
parent 4740d39105
commit 0841917e09
16 changed files with 2703 additions and 190 deletions
+233 -184
View File
@@ -2,6 +2,10 @@ import { defineConfig } from 'vitepress';
import { withMermaid } from 'vitepress-plugin-mermaid';
import typedocSidebar from '../api/typedoc-sidebar.json';
// Node imports for auto blog sidebar (works at docs/src/.vitepress)
import fs from 'node:fs';
import path from 'node:path';
// Special cases map for acronyms and exceptions
const SPECIAL_CASES = {
cryptocompare: 'CryptoCompare',
@@ -66,202 +70,247 @@ function convertNavToPascalCase(nav: Array<any>) {
});
}
// https://vitepress.dev/reference/site-config
export default withMermaid({
outDir: '../dist',
title: 'XO Stack',
description:
'XO Stack: Building smart applications on the Bitcoin Cash Network',
head: [
// Favicon.
['link', { rel: 'icon', href: '/favicon.png' }],
// Basic OpenGraph tags
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:title', content: 'XO Stack' }],
[
'meta',
{
property: 'og:description',
content:
'XO Stack: Building smart applications on the Bitcoin Cash Network',
},
],
[
'meta',
{
property: 'og:image',
content: 'https://stack.xo.cash/stack-opengraph.jpg',
},
],
['meta', { property: 'og:url', content: 'https://stack.xo.cash/' }],
function blogSidebar() {
const blogDir = path.resolve(__dirname, '../blog');
// Optional additional tags
['meta', { property: 'og:site_name', content: 'XO Stack' }],
if (!fs.existsSync(blogDir)) {
return [{ text: 'XO Blog', items: [] }];
}
// Twitter Card tags (optional but recommended)
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:site', content: '@yourtwitterhandle' }],
],
themeConfig: {
// Set the logo
logo: {
light: '/logo-light.svg',
dark: '/logo-dark.svg',
const files = fs
.readdirSync(blogDir)
.filter((f) => f.endsWith('.md') && f !== 'index.md')
.sort()
.reverse();
return [
{
text: 'XO Blog',
items: files.map((filePath) => {
const slug = path.parse(filePath).name;
// Try to extract YYYY-MM-DD from the filename
const match = slug.match(/^(\d{4}-\d{2}-\d{2})-(.*)$/);
let date = '';
let titleSlug = slug;
if (match) {
date = match[1]; // "2025-09-19"
titleSlug = match[2]; // "first-post"
}
const title = titleSlug
.replace(/-/g, ' ')
.replace(/^\w/, (c) => c.toUpperCase());
return {
// Use HTML for linebreak
text: date ? `${date}<br/>${title}` : title,
link: `/blog/${slug}`,
};
}),
},
];
}
// Enable search functionality.
// TODO: Re-enable once we release documentation.
/*
// https://vitepress.dev/reference/site-config
export default withMermaid(
defineConfig({
outDir: '../dist',
title: 'XO Stack',
description:
'XO Stack: Building smart applications on the Bitcoin Cash Network',
head: [
// Favicon.
['link', { rel: 'icon', href: '/favicon.png' }],
// Basic OpenGraph tags
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:title', content: 'XO Stack' }],
[
'meta',
{
property: 'og:description',
content:
'XO Stack: Building smart applications on the Bitcoin Cash Network',
},
],
[
'meta',
{
property: 'og:image',
content: 'https://stack.xo.cash/stack-opengraph.jpg',
},
],
['meta', { property: 'og:url', content: 'https://stack.xo.cash/' }],
// Optional additional tags
['meta', { property: 'og:site_name', content: 'XO Stack' }],
// Twitter Card tags (optional but recommended)
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:site', content: '@yourtwitterhandle' }],
],
themeConfig: {
// Set the logo
logo: {
light: '/logo-light.svg',
dark: '/logo-dark.svg',
},
// Enable search functionality.
// TODO: Re-enable once we release documentation.
/*
search: {
provider: 'local',
},
*/
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
// TODO: Re-enable once we release documentation
// { text: 'Documentation', link: '/quick-start' },
{ text: 'Tools', link: '/tools/primitives' },
{ text: 'Playground', link: '/playground' },
],
sidebar: {
// Default Side Menu.
'/': [
{
text: 'Guide',
items: [
{ text: 'Quick Start', link: '/quick-start' },
{ text: 'Architecture', link: '/architecture' },
{ text: 'Best Practices', link: '/api-examples' },
{ text: 'Contributing', link: '/contributing' },
],
},
{
text: 'Frameworks',
items: [
{ text: 'Vue', link: '/frameworks/vue' },
{ text: 'React', link: '/frameworks/react' },
{ text: 'Svelte', link: '/frameworks/svelte' },
],
},
{
text: 'API',
items: convertNavToPascalCase(typedocSidebar),
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
// TODO: Re-enable once we release documentation
// { text: 'Documentation', link: '/quick-start' },
{ text: 'Blog', link: '/blog' },
{ text: 'Tools', link: '/tools/primitives' },
{ text: 'Playground', link: '/playground' },
],
// Side-menu for all routes under /tools/
'/tools/': [
{
text: 'XO Tools',
items: [
{
text: 'Primitives',
link: '/tools/primitives',
collapsed: false,
items: [
{ text: 'Address', link: '/tools/primitives#address' },
{
text: 'Block Header',
link: '/tools/primitives#block-header',
},
{ text: 'Bytes', link: '/tools/primitives#bytes' },
{
text: 'Encrypter',
link: '/tools/primitives#encrypter',
},
{
text: 'Hasher',
link: '/tools/primitives#hasher',
},
{
text: 'HD Private Node',
link: '/tools/primitives#hd-private-node',
},
{
text: 'HD Public Node',
link: '/tools/primitives#hd-public-node',
},
{
text: 'Mnemonic',
link: '/tools/primitives#mnemonic',
},
{
text: 'Private Key',
link: '/tools/primitives#private-key',
},
{
text: 'Public Key',
link: '/tools/primitives#public-key',
},
{
text: 'Satoshis',
link: '/tools/primitives#satoshis',
},
{
text: 'Script',
link: '/tools/primitives#script',
},
{
text: 'Transaction',
link: '/tools/primitives#transaction',
},
],
},
{
text: 'Stack',
collapsed: false,
items: [
{
text: 'Blockchain',
link: '/tools/stack/blockchain',
items: [
{
text: 'Electrum',
link: '/tools/stack/blockchain#electrum',
},
],
},
],
},
],
},
sidebar: {
// Default Side Menu.
'/': [
{
text: 'Guide',
items: [
{ text: 'Quick Start', link: '/quick-start' },
{ text: 'Architecture', link: '/architecture' },
{ text: 'Best Practices', link: '/api-examples' },
{ text: 'Contributing', link: '/contributing' },
],
},
{
text: 'Frameworks',
items: [
{ text: 'Vue', link: '/frameworks/vue' },
{ text: 'React', link: '/frameworks/react' },
{ text: 'Svelte', link: '/frameworks/svelte' },
],
},
{
text: 'API',
items: convertNavToPascalCase(typedocSidebar),
},
],
// Side-menu for all routes under /tools/
'/tools/': [
{
text: 'XO Tools',
items: [
{
text: 'Primitives',
link: '/tools/primitives',
collapsed: false,
items: [
{ text: 'Address', link: '/tools/primitives#address' },
{
text: 'Block Header',
link: '/tools/primitives#block-header',
},
{ text: 'Bytes', link: '/tools/primitives#bytes' },
{
text: 'Encrypter',
link: '/tools/primitives#encrypter',
},
{
text: 'Hasher',
link: '/tools/primitives#hasher',
},
{
text: 'HD Private Node',
link: '/tools/primitives#hd-private-node',
},
{
text: 'HD Public Node',
link: '/tools/primitives#hd-public-node',
},
{
text: 'Mnemonic',
link: '/tools/primitives#mnemonic',
},
{
text: 'Private Key',
link: '/tools/primitives#private-key',
},
{
text: 'Public Key',
link: '/tools/primitives#public-key',
},
{
text: 'Satoshis',
link: '/tools/primitives#satoshis',
},
{
text: 'Script',
link: '/tools/primitives#script',
},
{
text: 'Transaction',
link: '/tools/primitives#transaction',
},
],
},
{
text: 'Stack',
collapsed: false,
items: [
{
text: 'Blockchain',
link: '/tools/stack/blockchain',
items: [
{
text: 'Electrum',
link: '/tools/stack/blockchain#electrum',
},
],
},
],
},
],
},
],
// Blog sidebar (auto-generated)
'/blog/': blogSidebar(),
},
socialLinks: [
// { icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' },
{ icon: 'x', link: 'https://x.com/xo_bitcoincash' },
],
},
/*
*/
socialLinks: [
// { icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' },
{ icon: 'x', link: 'https://x.com/xo_bitcoincash' },
],
// This is the outline on the right-hand side of the page.
outline: {
level: [2, 4],
label: 'On this page',
},
},
// Set the dev and build targets to "esnext"
// NOTE: We do this so that we can support top-level-await.
vite: {
esbuild: {
target: 'esnext',
},
build: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
// This is the outline on the right-hand side of the page.
outline: {
level: [2, 4],
label: 'On this page',
},
},
resolve: {
// Configure to use browser version of @xocash/stack
conditions: ['browser', 'import', 'module', 'default'],
// Set the dev and build targets to "esnext"
// NOTE: We do this so that we can support top-level-await.
vite: {
esbuild: {
target: 'esnext',
},
build: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
},
},
resolve: {
// Configure to use browser version of @xocash/stack
conditions: ['browser', 'import', 'module', 'default'],
},
},
},
});
}),
);
@@ -0,0 +1,204 @@
<template>
<div v-if="!posts.length" class="empty">
<p>No posts yet. Add Markdown files to <code>docs/src/blog/</code>.</p>
</div>
<div v-else class="grid">
<article v-for="p in posts" :key="p.url" class="card">
<div class="body">
<!-- date -->
<time v-if="p.dateText" class="date">{{ p.dateText }}</time>
<!-- title -->
<h2 class="title">
<a :href="p.url">{{ p.title }}</a>
</h2>
<!-- description -->
<p v-if="p.description" class="desc">{{ p.description }}</p>
<!-- author -->
<div v-if="p.author" class="author">
{{ p.author }}
</div>
<div class="spacer"></div>
<!-- tags at bottom (styled globally) -->
<div v-if="p.tags && p.tags.length" class="tags">
<span v-for="t in p.tags" :key="t" class="tag-badge VPButton brand">{{ t }}</span>
</div>
</div>
</article>
</div>
</template>
<script setup>
// Use the vite custom import to get each markdown file as a module
// with additional metadata from the markdown's frontmatter (if any).
// The modules are in a lookup according to their relative path.
const allMDModules = import.meta.glob('../../../blog/**/*.md', { eager: true })
function formatDate(d) {
// instanceof checks that we have a Date object
// isNaN checks validity of the Date object
if (!(d instanceof Date) || isNaN(d)) {
throw new Error(`argument is not a valid date object: ${d}`)
}
// JS Date doesn't have a great way to do this timezone aware so this manual method is common
// Note: getMonth() alone is 0-indexed so it has a +1
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const posts = Object.entries(allMDModules)
// Ignore index.md in the md module lookup
.filter(([p]) => !/[/\\]index\.md$/.test(p))
// Convert each path + md module into a well formed post object
.map(([p, mdModule]) => {
// Extract the frontmatter, which can be in different places depending on the vite setup
const frontmatter =
(mdModule && mdModule.frontmatter) ||
(mdModule && mdModule.__pageData && mdModule.__pageData.frontmatter) ||
{}
const normalizedPath = p.replace(/\\/g, '/')
const relativePath = normalizedPath.split('/blog/')[1] || ''
const slug = relativePath.replace(/\.md$/, '')
// We assume that the posts have a 'yyyy-mm-dd-' prefix and remove it.
const namePart = slug.replace(/^\d{4}-\d{2}-\d{2}-/, '')
// We use global regex replace to replace all hyphens, and regex to capitalize the first letter
const fallbackTitle = namePart.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase())
// Extract the post author from the frontmatter
const postAuthor = frontmatter.author || null
// Extract the post date from either the frontmatter or the filename, in that order
const postDate = frontmatter.date ? new Date(frontmatter.date) : null
if ((postDate === null) || (isNaN(postDate))) {
throw new Error(`Blog posts are required to have a date in their frontmatter but did not find one for ${slug}`);
}
return {
url: '/blog/' + slug,
title: frontmatter.title || fallbackTitle,
dateText: postDate ? formatDate(postDate) : '',
description: frontmatter.description || '',
author: postAuthor,
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
sort: postDate ? postDate.getTime() : 0
}
})
// Sort the blogs with most recent first
.sort((a, b) => b.sort - a.sort)
</script>
<style scoped>
/* responsive grid */
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 20px;
margin-top: 12px;
}
.card { grid-column: span 4; }
@media (max-width: 1100px) { .card { grid-column: span 6; } }
@media (max-width: 700px) { .card { grid-column: span 12; } }
/* compact, uniform-height cards */
.card {
display: flex;
flex-direction: column;
height: 240px; /* all cards same height */
border-radius: 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
overflow: hidden;
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0,0,0,.06);
border-color: var(--vp-c-brand);
}
.body {
display: flex;
flex-direction: column;
padding: 12px 14px 12px;
flex: 1;
}
/* date */
.date {
font-size: 12px;
color: var(--vp-c-text-2);
margin-bottom: 4px;
}
.author {
font-size: 12px;
color: var(--vp-c-brand-1);
margin: 4px 0px;
font-style: italic;
}
/* title (clamped to 2 lines) */
.title {
margin: 0;
font-size: 16px;
line-height: 1.3;
font-weight: 600;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.title a { color: var(--vp-c-text-1); text-decoration: none; }
.title a:hover { text-decoration: underline; }
.vp-doc h2 {
padding-top: 8px !important; /* override default 24px */
}
/* description (clamped to 3 lines) */
.desc {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* spacer pushes tags to bottom */
.spacer { flex: 1; }
/* tags row (visual style comes from your global CSS) */
/* tags row (visual style comes from your global CSS) */
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
text-transform: uppercase;
}
/* individual tag badge */
.tag-badge {
background: var(--vp-button-brand-bg);
color: #fff;
padding: 3px 8px;
border-radius: 6px; /* adds rounded corners */
font-size: 11px;
line-height: 1.2;
}
</style>
@@ -222,3 +222,14 @@ details summary {
display: flex;
justify-content: center;
}
.tag-badge {
font-size: 12px;
font-weight: 600;
color: #fff;
padding: 4px 10px;
border-radius: 5px;
background: linear-gradient(120deg, var(--tag-grad-start), var(--tag-grad-end));
line-height: 1;
display: inline-block;
}
@@ -5,6 +5,8 @@ import Layout from './Layout.vue';
import './style.css';
import './custom.css';
import './tools.css';
import './mermaid-overrides.css';
import './mermaid-lightbox.client';
export default {
extends: DefaultTheme,
@@ -0,0 +1,171 @@
// mermaid-lightbox.client.ts
// Click any .mermaid diagram to open a zoomable modal (works beautifully on desktop + mobile)
// Includes a theme-aware "Click diagram to expand" note that hides instantly when lightbox opens
if (typeof window !== 'undefined') {
const OVERLAY_ATTR = 'data-mermaid-lightbox';
const NO_LIGHTBOX_ATTR = 'data-no-lightbox';
const MEMO_CLASS = 'mermaid-expand-memo';
const hideAllMemos = () =>
document
.querySelectorAll(`.${MEMO_CLASS}`)
.forEach((m) => ((m as HTMLElement).style.opacity = '0'));
const showAllMemos = () =>
document
.querySelectorAll(`.${MEMO_CLASS}`)
.forEach((m) => ((m as HTMLElement).style.opacity = '0.55'));
// === Create overlay (only once) ===
const ensureOverlay = () => {
let overlay = document.querySelector<HTMLDivElement>(
`div[${OVERLAY_ATTR}]`,
);
if (overlay) return overlay;
overlay = document.createElement('div');
overlay.setAttribute(OVERLAY_ATTR, 'true');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.style.cssText = `
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,.6);
display: none; align-items: center; justify-content: center;
padding: 0;
touch-action: pan-x pan-y pinch-zoom;
`;
const box = document.createElement('div');
box.className = 'mermaid mermaid-lightbox-content';
box.style.cssText = `
background: var(--vp-c-bg);
border-radius: 8px;
width: 100%;
height: auto;
max-width: 100vw;
max-height: 100vh;
overflow: auto;
padding: 0.5rem;
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,.35);
`;
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.setAttribute('aria-label', 'Close');
closeBtn.innerHTML = '×';
closeBtn.style.cssText = `
position: absolute; top: 4px; right: 10px;
font-size: 32px; line-height: 1;
border: 0; background: transparent;
cursor: pointer; color: var(--vp-c-text-1);
z-index: 2;
`;
const hide = () => {
overlay!.style.display = 'none';
box.replaceChildren();
document.removeEventListener('keydown', onKey);
showAllMemos();
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') hide();
};
(overlay as any).showSvg = (svg: SVGSVGElement) => {
// Hide memos immediately
hideAllMemos();
// Clone ONLY the SVG element, not any parent containers
const clone = svg.cloneNode(true) as SVGSVGElement;
clone.removeAttribute('width');
clone.removeAttribute('height');
clone.style.display = 'block';
clone.style.width = '100%';
clone.style.height = 'auto';
clone.style.maxWidth = '100%';
clone.style.maxHeight = '90vh';
clone.setAttribute('preserveAspectRatio', 'xMidYMid meet');
// Clear the box completely before adding new content
box.innerHTML = '';
box.appendChild(closeBtn);
box.appendChild(clone);
overlay!.style.display = 'flex';
document.addEventListener('keydown', onKey);
};
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
hide();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) hide();
});
overlay.appendChild(box);
document.body.appendChild(overlay);
return overlay;
};
// === Add the small memo below each Mermaid ===
const addMemo = (container: HTMLElement) => {
if (container.nextElementSibling?.classList.contains(MEMO_CLASS)) return;
const memo = document.createElement('div');
memo.className = MEMO_CLASS;
memo.textContent = 'Click diagram to expand';
memo.style.cssText = `
text-align: center;
font-size: 0.78rem;
color: currentColor;
opacity: 0.55;
margin-top: 0.4rem;
font-family: system-ui, sans-serif;
user-select: none;
pointer-events: none;
transition: opacity 0.25s ease;
`;
container.insertAdjacentElement('afterend', memo);
};
// === Wire up click events ===
const wire = (root: ParentNode = document) => {
const svgs = root.querySelectorAll<SVGSVGElement>(
'.mermaid svg[id^="mermaid-"]',
);
svgs.forEach((svg) => {
const container = svg.closest('.mermaid') as HTMLElement | null;
if (!container || container.hasAttribute(NO_LIGHTBOX_ATTR)) return;
if ((svg as any)._mlb) return;
container.style.cursor = 'zoom-in';
container.setAttribute('role', 'button');
container.setAttribute('tabindex', '0');
addMemo(container);
container.addEventListener('click', (e) => {
if ((e.target as HTMLElement)?.closest('a')) return;
const overlay = ensureOverlay();
(overlay as any).showSvg(svg);
});
container.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const overlay = ensureOverlay();
(overlay as any).showSvg(svg);
}
});
(svg as any)._mlb = true;
});
};
// Initial run + observer for re-renders
wire();
const mo = new MutationObserver(() => wire());
mo.observe(document.documentElement, { childList: true, subtree: true });
}
@@ -0,0 +1,227 @@
/* ──────────────────────────────────────────────────────────────
Mermaid — Site CSS
- Mindmap: your custom palette (section-*)
- Flowchart: blue theme with white text
- Works in light & dark (via CSS vars)
- Responsive width + centering
────────────────────────────────────────────────────────────── */
/* ===== Brand palette (edit to taste) ===== */
:root {
/* === Mindmap Blue–Teal Theme === */
--mm-root-fill: #1e3a8a;
--mm-sec0-fill: #1e6091;
--mm-sec1-fill: #168aad;
--mm-sec2-fill: #34a0a4;
--mm-sec3-fill: #99d98c;
/* uniform thicker grey stroke for all nodes */
--mm-root-stroke: #7a7a7a;
--mm-sec0-stroke: #7a7a7a;
--mm-sec1-stroke: #7a7a7a;
--mm-sec2-stroke: #7a7a7a;
--mm-sec3-stroke: #7a7a7a;
/* optional text + fallback colors */
--mm-fallback-fill: #f8fafc;
--mm-fallback-stroke:#7a7a7a;
--mm-fallback-text: #0f172a;
/* edge color (grey-green neutral to fit palette) */
--mm-edge: #7a7a7a;
}
/* ===== Shared text overrides (foreignObject + tspans) ===== */
.mermaid svg[id^="mermaid-"] {
font-size: 110% !important; /* small global bump */
}
.mermaid svg[id^="mermaid-"] text,
.mermaid svg[id^="mermaid-"] tspan {
fill: #ffffff !important; /* default white text */
font-weight: 600;
}
.mermaid svg[id^="mermaid-"] foreignObject,
.mermaid svg[id^="mermaid-"] foreignObject * {
color: #ffffff !important;
}
/* ===== Mindmap palette (section-* classes) ===== */
/* Root node */
.mermaid svg[id^="mermaid-"] .section-root rect,
.mermaid svg[id^="mermaid-"] .section-root path,
.mermaid svg[id^="mermaid-"] .section-root circle,
.mermaid svg[id^="mermaid-"] .section-root polygon {
fill: var(--mm-root-fill) !important;
stroke: var(--mm-root-stroke) !important;
stroke-width: 4px !important;
}
.mermaid svg[id^="mermaid-"] .section-root text,
.mermaid svg[id^="mermaid-"] .section-root tspan {
fill: #ffffff !important;
font-weight: 700 !important;
}
/* Depth sections */
.mermaid svg[id^="mermaid-"] .section-0 rect,
.mermaid svg[id^="mermaid-"] .section-0 path,
.mermaid svg[id^="mermaid-"] .section-0 circle,
.mermaid svg[id^="mermaid-"] .section-0 polygon {
fill: var(--mm-sec0-fill) !important;
stroke: var(--mm-sec0-stroke) !important;
}
.mermaid svg[id^="mermaid-"] .section-0 text,
.mermaid svg[id^="mermaid-"] .section-0 tspan { fill: #ffffff !important; }
.mermaid svg[id^="mermaid-"] .section-1 rect,
.mermaid svg[id^="mermaid-"] .section-1 path,
.mermaid svg[id^="mermaid-"] .section-1 circle,
.mermaid svg[id^="mermaid-"] .section-1 polygon {
fill: var(--mm-sec1-fill) !important;
stroke: var(--mm-sec1-stroke) !important;
}
.mermaid svg[id^="mermaid-"] .section-1 text,
.mermaid svg[id^="mermaid-"] .section-1 tspan { fill: #ffffff !important; }
.mermaid svg[id^="mermaid-"] .section-2 rect,
.mermaid svg[id^="mermaid-"] .section-2 path,
.mermaid svg[id^="mermaid-"] .section-2 circle,
.mermaid svg[id^="mermaid-"] .section-2 polygon {
fill: var(--mm-sec2-fill) !important;
stroke: var(--mm-sec2-stroke) !important;
}
.mermaid svg[id^="mermaid-"] .section-2 text,
.mermaid svg[id^="mermaid-"] .section-2 tspan { fill: #ffffff !important; }
.mermaid svg[id^="mermaid-"] .section-3 rect,
.mermaid svg[id^="mermaid-"] .section-3 path,
.mermaid svg[id^="mermaid-"] .section-3 circle,
.mermaid svg[id^="mermaid-"] .section-3 polygon {
fill: var(--mm-sec3-fill) !important;
stroke: var(--mm-sec3-stroke) !important;
}
.mermaid svg[id^="mermaid-"] .section-3 text,
.mermaid svg[id^="mermaid-"] .section-3 tspan { fill: #ffffff !important; }
/* Fallback for other section-* */
.mermaid svg[id^="mermaid-"] [class^="section-"] rect,
.mermaid svg[id^="mermaid-"] [class^="section-"] path,
.mermaid svg[id^="mermaid-"] [class^="section-"] circle,
.mermaid svg[id^="mermaid-"] [class^="section-"] polygon {
fill: var(--mm-fallback-fill) !important;
stroke: var(--mm-fallback-stroke) !important;
}
.mermaid svg[id^="mermaid-"] [class^="section-"] text,
.mermaid svg[id^="mermaid-"] [class^="section-"] tspan {
fill: var(--mm-fallback-text) !important;
}
/* Mindmap branches/edges */
.mermaid svg[id^="mermaid-"] .edge,
.mermaid svg[id^="mermaid-"] .mindmap-edges path,
.mermaid svg[id^="mermaid-"] .mindmap-edges line,
.mermaid svg[id^="mermaid-"] [class^="section-edge-"] {
stroke: var(--mm-edge) !important;
stroke-width: 2px !important;
fill: none !important;
}
/* Optional: tiny underline lines Mermaid adds in some node styles */
.mermaid svg[id^="mermaid-"] .node-line-0,
.mermaid svg[id^="mermaid-"] .node-line-1,
.mermaid svg[id^="mermaid-"] .node-line-2 {
stroke: rgba(0,0,0,0.25) !important;
}
:root {
--mm-node-blue: #1570ef; /* inner node fill (light) */
--mm-surface: rgba(0,0,0,0.06); /* cluster/edge-label pill bg on light */
--mm-edge: color-mix(in srgb, currentColor 70%, transparent);
}
html.dark {
--mm-node-blue: #1363d1; /* inner node fill (dark) */
--mm-surface: rgba(255,255,255,0.08); /* cluster/edge-label pill bg on dark */
--mm-edge: color-mix(in srgb, currentColor 60%, transparent);
}
.mermaid svg[id^="mermaid-"] .label text,
.mermaid svg[id^="mermaid-"] text,
.mermaid svg[id^="mermaid-"] tspan,
.mermaid svg[id^="mermaid-"] foreignObject,
.mermaid svg[id^="mermaid-"] foreignObject * {
fill: currentColor !important;
color: currentColor !important;
}
.mermaid svg[id^="mermaid-"] .node rect,
.mermaid svg[id^="mermaid-"] .node circle,
.mermaid svg[id^="mermaid-"] .node ellipse,
.mermaid svg[id^="mermaid-"] .node polygon,
.mermaid svg[id^="mermaid-"] .node path {
fill: var(--mm-node-blue) !important;
stroke: none !important;
}
.mermaid svg[id^="mermaid-"] .node .label text,
.mermaid svg[id^="mermaid-"] .node .label tspan,
.mermaid svg[id^="mermaid-"] .node foreignObject,
.mermaid svg[id^="mermaid-"] .node foreignObject * {
fill: #ffffff !important;
color: #ffffff !important;
font-weight: 600;
}
.mermaid svg[id^="mermaid-"] .cluster rect {
fill: var(--mm-surface) !important;
stroke: none !important;
}
.mermaid svg[id^="mermaid-"] .cluster text,
.mermaid svg[id^="mermaid-"] .cluster span {
fill: currentColor !important;
color: currentColor !important;
font-weight: 600;
}
.mermaid svg[id^="mermaid-"] .edgePath .path,
.mermaid svg[id^="mermaid-"] .flowchart-link {
stroke: var(--mm-edge) !important;
stroke-width: 2px !important;
fill: none !important;
}
.mermaid svg[id^="mermaid-"] .marker,
.mermaid svg[id^="mermaid-"] .arrowheadPath {
fill: var(--mm-edge) !important;
stroke: var(--mm-edge) !important;
}
.mermaid svg[id^="mermaid-"] .edgeLabel rect {
fill: var(--mm-surface) !important;
stroke: none !important;
opacity: 1 !important;
}
.mermaid svg[id^="mermaid-"] .edgeLabel span,
.mermaid svg[id^="mermaid-"] .edgeLabel text {
fill: currentColor !important;
color: currentColor !important;
font-weight: 500;
}
.mermaid svg[id^="mermaid-"] rect.text { fill: none !important; }
/* ===== Layout: responsive width + centering in docs ===== */
.vp-doc .mermaid > svg {
width: 100% !important;
height: auto !important;
display: block;
}
.vp-doc .mermaid {
display: flex;
justify-content: center;
}