blog infra
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user