c97bb07371
The regex literal in #105 got its backslashes consumed by the surrounding template literal, producing "//index.html$|/$/" in the deployed output. The "//" was parsed as a line comment, breaking the entire inline <script type="module"> block with a syntax error and disabling all client-side interactions (Editor link, Random Theme, theme pills, mega-menu). Replaced with endsWith/slice equivalent. Added a comment explaining the template-literal interpolation pitfall to prevent the same mistake on future inline scripts. Co-Authored-By: Craft Agent <agents-noreply@craft.do>
1882 lines
69 KiB
TypeScript
1882 lines
69 KiB
TypeScript
/**
|
|
* Generates index.html showcasing all beautiful-mermaid rendering capabilities.
|
|
*
|
|
* Usage: bun run index.ts
|
|
*
|
|
* This file doubles as a **visual test suite** — every supported feature,
|
|
* shape, edge type, block construct, and theme variant is exercised by at
|
|
* least one sample. If a rendering change causes regressions, it will be
|
|
* visible in the generated HTML.
|
|
*
|
|
* The generated HTML is **dynamic** — it includes a bundled copy of the
|
|
* mermaid renderer and renders all diagrams client-side in real time,
|
|
* showing progressive loading and per-diagram render timing.
|
|
*
|
|
* Sample definitions live in samples-data.ts (shared with bench.ts).
|
|
*/
|
|
|
|
import { samples } from './samples-data.ts'
|
|
import { THEMES } from './src/theme.ts'
|
|
import { createHighlighter } from 'shiki'
|
|
|
|
// ============================================================================
|
|
// HTML generation — dynamic version
|
|
//
|
|
// Instead of pre-rendering SVGs at build time, we:
|
|
// 1. Bundle the mermaid renderer for the browser via Bun.build()
|
|
// 2. Embed sample definitions as inline JSON
|
|
// 3. Emit client-side JS that renders each diagram on page load
|
|
// ============================================================================
|
|
|
|
function escapeHtml(text: string): string {
|
|
return text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
}
|
|
|
|
/** Convert markdown-style backtick spans to <code> tags in description text. */
|
|
function formatDescription(text: string): string {
|
|
return text.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
}
|
|
|
|
/** Human-readable labels for theme keys */
|
|
const THEME_LABELS: Record<string, string> = {
|
|
'zinc-dark': 'Zinc Dark',
|
|
'tokyo-night': 'Tokyo Night',
|
|
'tokyo-night-storm': 'Tokyo Storm',
|
|
'tokyo-night-light': 'Tokyo Light',
|
|
'catppuccin-mocha': 'Catppuccin',
|
|
'catppuccin-latte': 'Latte',
|
|
'nord': 'Nord',
|
|
'nord-light': 'Nord Light',
|
|
'dracula': 'Dracula',
|
|
'github-light': 'GitHub',
|
|
'github-dark': 'GitHub Dark',
|
|
'solarized-light': 'Solarized',
|
|
'solarized-dark': 'Solar Dark',
|
|
'one-dark': 'One Dark',
|
|
}
|
|
|
|
async function generateHtml(): Promise<string> {
|
|
// Step 0: Create Shiki highlighter for mermaid syntax highlighting in source panels.
|
|
// We use 'github-light' as the base theme — its hex colors get overridden by CSS
|
|
// color-mix() rules derived from --t-fg / --t-bg so tokens adapt to any theme.
|
|
const highlighter = await createHighlighter({
|
|
langs: ['mermaid'],
|
|
themes: ['github-light'],
|
|
})
|
|
|
|
// Step 1: Bundle the mermaid renderer for the browser
|
|
const buildResult = await Bun.build({
|
|
entrypoints: [new URL('./src/browser.ts', import.meta.url).pathname],
|
|
target: 'browser',
|
|
format: 'esm',
|
|
minify: true,
|
|
})
|
|
if (!buildResult.success) {
|
|
console.error('Bundle build failed:', buildResult.logs)
|
|
process.exit(1)
|
|
}
|
|
const bundleJs = await buildResult.outputs[0]!.text()
|
|
console.log(`Browser bundle: ${(bundleJs.length / 1024).toFixed(1)} KB`)
|
|
|
|
// Step 2: Build sample JSON (only serializable fields needed by client)
|
|
const samplesJson = JSON.stringify(samples.map(s => ({
|
|
title: s.title,
|
|
description: s.description,
|
|
source: s.source,
|
|
category: s.category ?? 'Other',
|
|
options: s.options ?? {},
|
|
})))
|
|
|
|
// Step 3: Group samples by category for TOC (done at build time since it's static)
|
|
const categories = new Map<string, number[]>()
|
|
samples.forEach((sample, i) => {
|
|
const cat = sample.category ?? 'Other'
|
|
if (!categories.has(cat)) categories.set(cat, [])
|
|
categories.get(cat)!.push(i)
|
|
})
|
|
|
|
const categoryBadgeColors: Record<string, string> = {
|
|
Flowchart: '#3b82f6',
|
|
State: '#8b5cf6',
|
|
Sequence: '#10b981',
|
|
Class: '#f59e0b',
|
|
ER: '#ef4444',
|
|
'XY Chart': '#f97316',
|
|
'Theme Showcase': '#06b6d4',
|
|
}
|
|
|
|
// Map category names to the title prefixes they use, so we can strip duplicates in the ToC
|
|
const categoryPrefixes: Record<string, string> = {
|
|
'State': 'State: ',
|
|
'Sequence': 'Sequence: ',
|
|
'Class': 'Class: ',
|
|
'ER': 'ER: ',
|
|
'XY Chart': 'XY: ',
|
|
'Theme Showcase': 'Theme: ',
|
|
}
|
|
|
|
// Build mapping from original index to display number (excluding Hero samples)
|
|
const heroCount = samples.filter(s => s.category === 'Hero').length
|
|
const displayNum = (i: number) => i + 1 - heroCount
|
|
|
|
const tocSections = [...categories.entries()]
|
|
.filter(([cat]) => cat !== 'Hero') // Skip Hero from TOC
|
|
.map(([cat, indices]) => {
|
|
const badgeColor = categoryBadgeColors[cat] ?? '#71717a'
|
|
const prefix = categoryPrefixes[cat]
|
|
const items = indices.map(i => {
|
|
let title = samples[i]!.title
|
|
// Strip the category prefix from the title since it's already under the category heading
|
|
if (prefix && title.startsWith(prefix)) title = title.slice(prefix.length)
|
|
return `<li><a href="#sample-${i}"><span class="toc-num">${displayNum(i)}.</span> ${escapeHtml(title)}</a></li>`
|
|
}).join('\n ')
|
|
return `
|
|
<div class="toc-category">
|
|
<h3>${escapeHtml(cat)} (${indices.length} samples)</h3>
|
|
<ol start="${displayNum(indices[0]!)}">
|
|
${items}
|
|
</ol>
|
|
</div>`
|
|
}).join('\n')
|
|
|
|
// Step 3b: Build theme selector pills (build-time so we include swatches)
|
|
// Only show Default, Dracula, and Solarized inline; rest go in "More" dropdown
|
|
const VISIBLE_THEMES = new Set(['dracula', 'solarized-light'])
|
|
|
|
function buildThemePill(key: string, colors: { bg: string; fg: string }, active = false): string {
|
|
const isDark = parseInt(colors.bg.replace('#', '').slice(0, 2), 16) < 0x80
|
|
const shadow = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.1)'
|
|
const label = key === '' ? 'Default' : (THEME_LABELS[key] ?? key)
|
|
const activeClass = active ? ' active' : ''
|
|
return `<button class="theme-pill shadow-minimal${activeClass}" data-theme="${key}"><span class="theme-swatch" style="background:${colors.bg};box-shadow:inset 0 0 0 1px ${shadow}"></span>${escapeHtml(label)}</button>`
|
|
}
|
|
|
|
const themeEntries = Object.entries(THEMES)
|
|
// Visible inline pills: Default + Dracula + Solarized
|
|
const visiblePills = [
|
|
'<button class="theme-pill shadow-minimal active" data-theme=""><span class="theme-swatch" style="background:#FFFFFF;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.1)"></span>Default</button>',
|
|
...themeEntries
|
|
.filter(([key]) => VISIBLE_THEMES.has(key))
|
|
.map(([key, colors]) => buildThemePill(key, colors)),
|
|
]
|
|
// All themes go in the dropdown (including Default, Dracula, Solarized)
|
|
const allDropdownPills = [
|
|
buildThemePill('', { bg: '#FFFFFF', fg: '#27272A' }, true),
|
|
...themeEntries.map(([key, colors]) => buildThemePill(key, colors)),
|
|
]
|
|
const totalThemes = allDropdownPills.length
|
|
|
|
const themePillsHtml = `
|
|
<div class="theme-pills-inline">
|
|
${visiblePills.join('\n ')}
|
|
</div>
|
|
<div class="theme-more-wrapper">
|
|
<button class="theme-pill shadow-minimal" id="theme-more-btn">${totalThemes} Themes</button>
|
|
<div class="theme-more-dropdown shadow-modal-small" id="theme-more-dropdown">
|
|
${allDropdownPills.join('\n ')}
|
|
</div>
|
|
</div>`
|
|
|
|
// Step 4: Pre-highlight all sample sources with Shiki (build-time only, zero runtime cost).
|
|
// The mermaid TextMate grammar requires a fenced code block prefix to tokenize properly
|
|
// (see https://github.com/shikijs/shiki/issues/973), so we wrap each source with
|
|
// ```mermaid ... ``` and then strip those fence lines from the output HTML.
|
|
// Source panels always use github-dark — Shiki's inline colors are used directly.
|
|
const highlightedSources = samples.map(sample => {
|
|
const fenced = '```mermaid\n' + sample.source.trim() + '\n```'
|
|
const html = highlighter.codeToHtml(fenced, {
|
|
lang: 'mermaid',
|
|
theme: 'github-light',
|
|
})
|
|
// Strip the first line (```mermaid) and last line (```) from the output
|
|
return html.replace(
|
|
/(<code>)<span class="line">.*?<\/span>\n/, // first line
|
|
'$1'
|
|
).replace(
|
|
/\n<span class="line">.*?<\/span>(<\/code>)/, // last line
|
|
'$1'
|
|
)
|
|
})
|
|
|
|
// Step 5: Build sample card HTML shells (SVG + ASCII are empty, filled client-side)
|
|
// data-sample-bg stores the per-sample background for "Default" mode restoration.
|
|
// Hero samples get special full-width SVG-only treatment and are placed before "Samples" heading.
|
|
const heroCards: string[] = []
|
|
const regularCards: string[] = []
|
|
|
|
samples.forEach((sample, i) => {
|
|
const bg = sample.options?.bg ?? ''
|
|
const isHero = sample.category === 'Hero'
|
|
|
|
if (isHero) {
|
|
// Hero sample: full-width SVG only, no header/source/ASCII panels
|
|
heroCards.push(`
|
|
<section class="sample sample-hero" id="sample-${i}">
|
|
<div class="hero-diagram-panel" id="svg-panel-${i}" data-sample-bg="${bg}">
|
|
<div class="svg-container" id="svg-${i}">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
</div>
|
|
</section>`)
|
|
} else {
|
|
regularCards.push(`
|
|
<section class="sample" id="sample-${i}">
|
|
<div class="sample-header">
|
|
<h2>${escapeHtml(sample.title)}</h2>
|
|
<p class="description">${formatDescription(sample.description)}</p>
|
|
</div>
|
|
<div class="sample-content">
|
|
<div class="source-panel" id="source-panel-${i}">
|
|
${highlightedSources[i]}
|
|
${sample.options ? `<div class="options"><strong>Options:</strong> <code>${escapeHtml(JSON.stringify(sample.options))}</code></div>` : ''}
|
|
<button class="edit-btn" data-sample="${i}">Edit</button>
|
|
</div>
|
|
<div class="svg-panel" id="svg-panel-${i}" data-sample-bg="${bg}">
|
|
<div class="svg-container" id="svg-${i}">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
</div>
|
|
<div class="ascii-panel" id="ascii-panel-${i}">
|
|
<pre class="ascii-output"><code id="ascii-${i}">Rendering\u2026</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>`)
|
|
}
|
|
})
|
|
|
|
const heroCardsHtml = heroCards.join('\n')
|
|
const regularCardsHtml = regularCards.join('\n')
|
|
|
|
// ============================================================================
|
|
// Step 5: Assemble full HTML
|
|
// ============================================================================
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="theme-color" id="theme-color-meta" content="#f9f9fa" />
|
|
<title>Beautiful Mermaid — Mermaid Rendering, Made Beautiful</title>
|
|
<meta name="description" content="Open source diagram rendering library built for the AI era. Ultra-fast, fully themeable, outputs to SVG and ASCII. Supports Flowchart, State, Sequence, Class, and ER diagrams." />
|
|
<link rel="icon" type="image/svg+xml" href="/mermaid/favicon.svg" />
|
|
<link rel="icon" type="image/x-icon" href="/mermaid/favicon.ico" />
|
|
<link rel="apple-touch-icon" href="/mermaid/apple-touch-icon.png" />
|
|
<meta property="og:title" content="Beautiful Mermaid" />
|
|
<meta property="og:description" content="Open source diagram rendering library built for the AI era. Ultra-fast, fully themeable, outputs to SVG and ASCII." />
|
|
<meta property="og:image" content="https://agents.craft.do/mermaid/og-image.png" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content="https://agents.craft.do/mermaid" />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:title" content="Beautiful Mermaid" />
|
|
<meta name="twitter:description" content="Mermaid rendering, made beautiful. Ultra-fast, fully themeable, outputs to SVG and ASCII." />
|
|
<meta name="twitter:image" content="https://agents.craft.do/mermaid/og-image.png" />
|
|
<!-- Plausible Analytics -->
|
|
<script defer data-domain="agents.craft.do/mermaid" src="https://plausible.io/js/script.js"></script>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
<style>
|
|
/* -- Reset & base -- */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
/* -----------------------------------------------------------------
|
|
* CSS custom property theming
|
|
*
|
|
* --t-bg and --t-fg drive the entire page color scheme.
|
|
* All other colors are derived via color-mix(). When a theme is
|
|
* selected from the pill bar, JS updates these two variables on
|
|
* <body> — and the whole page adapts instantly.
|
|
* ----------------------------------------------------------------- */
|
|
body {
|
|
--t-bg: #FFFFFF;
|
|
--t-fg: #27272A;
|
|
--t-accent: #3b82f6;
|
|
--foreground-rgb: 39, 39, 42;
|
|
--accent-rgb: 59, 130, 246;
|
|
--shadow-border-opacity: 0.08;
|
|
--shadow-blur-opacity: 0.06;
|
|
--theme-bar-bg: #f9f9fa; /* Mixed bg for theme bar and top gradient — updated by JS on theme change */
|
|
|
|
font-family: 'Geist', system-ui, -apple-system, sans-serif;
|
|
background: color-mix(in srgb, var(--t-fg) 4%, var(--t-bg));
|
|
color: var(--t-fg);
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
transition: background 0.2s, color 0.2s;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
.content-wrapper {
|
|
max-width: 1440px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
padding-top: 0;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.content-wrapper {
|
|
padding: 1rem;
|
|
padding-top: 0;
|
|
}
|
|
}
|
|
@media (min-width: 1000px) {
|
|
.content-wrapper {
|
|
padding: 3rem;
|
|
padding-top: 0;
|
|
}
|
|
}
|
|
|
|
/* -- Scroll fade gradients (GPU accelerated) -- */
|
|
body::before,
|
|
body::after {
|
|
content: '';
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
height: 64px;
|
|
pointer-events: none;
|
|
z-index: 1000;
|
|
will-change: transform;
|
|
}
|
|
body::before {
|
|
top: 0;
|
|
background: linear-gradient(to bottom, var(--theme-bar-bg) 0%, transparent 100%);
|
|
}
|
|
body::after {
|
|
bottom: 0;
|
|
background: linear-gradient(to top, var(--theme-bar-bg) 0%, transparent 100%);
|
|
}
|
|
|
|
/* -- Theme selector bar (full-width, sits outside .content-wrapper) -- */
|
|
.theme-bar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1001;
|
|
background: transparent;
|
|
padding: 0.5rem 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
overflow: visible;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.theme-bar {
|
|
padding: 0.5rem 1rem;
|
|
}
|
|
}
|
|
.theme-label {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
|
|
white-space: nowrap;
|
|
}
|
|
.theme-pills {
|
|
display: flex;
|
|
gap: 0.3rem;
|
|
overflow: visible;
|
|
padding: 4px;
|
|
margin: -4px;
|
|
margin-left: auto;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
.theme-pills-inline {
|
|
display: flex;
|
|
gap: 0.3rem;
|
|
}
|
|
/* Hide inline theme pills on smaller screens, show only "15 Themes" dropdown */
|
|
@media (max-width: 1024px) {
|
|
.theme-pills-inline {
|
|
display: none;
|
|
}
|
|
}
|
|
.theme-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 30px;
|
|
gap: 8px;
|
|
padding: 0 14px 0 12px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--t-bg) 97%, var(--t-fg));
|
|
color: color-mix(in srgb, var(--t-fg) 80%, var(--t-bg));
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: color 0.15s, background 0.15s, box-shadow 0.2s, transform 0.1s;
|
|
}
|
|
.theme-pill:hover {
|
|
color: var(--t-fg);
|
|
background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
|
|
}
|
|
.theme-pill.active {
|
|
color: var(--t-fg);
|
|
background: var(--t-bg);
|
|
font-weight: 600;
|
|
}
|
|
.theme-pill:active {
|
|
transform: translateY(0.5px);
|
|
}
|
|
.theme-swatch {
|
|
display: inline-block;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* -- "More" dropdown for overflow themes -- */
|
|
.theme-more-wrapper {
|
|
position: relative;
|
|
}
|
|
.theme-more-dropdown {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 6px);
|
|
right: 0;
|
|
background: var(--t-bg);
|
|
border-radius: 12px;
|
|
padding: 6px;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 160px;
|
|
z-index: 1002;
|
|
}
|
|
.theme-more-dropdown.open {
|
|
display: flex;
|
|
}
|
|
.theme-more-dropdown .theme-pill {
|
|
width: 100%;
|
|
justify-content: flex-start;
|
|
background: transparent;
|
|
box-shadow: none;
|
|
}
|
|
.theme-more-dropdown .theme-pill:hover {
|
|
background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
|
|
}
|
|
/* Active pill in dropdown gets bg + shadow-minimal (same as inline pills) */
|
|
.theme-more-dropdown .theme-pill.active,
|
|
.theme-more-dropdown .theme-pill.shadow-tinted {
|
|
background: var(--t-bg);
|
|
box-shadow:
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
|
|
rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 1px 1px -0.5px,
|
|
rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 3px 3px -1.5px;
|
|
}
|
|
|
|
/* -- Brand badge (left-aligned in theme bar) -- */
|
|
.brand-badge-wrapper {
|
|
position: relative;
|
|
}
|
|
.brand-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 30px;
|
|
gap: 6px;
|
|
padding: 0 12px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--t-bg) 97%, var(--t-fg));
|
|
color: color-mix(in srgb, var(--t-fg) 80%, var(--t-bg));
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
font-family: inherit;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
transition: color 0.15s, background 0.15s, box-shadow 0.2s, transform 0.1s;
|
|
}
|
|
.brand-badge:hover {
|
|
color: var(--t-fg);
|
|
background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
|
|
}
|
|
.brand-badge.active {
|
|
color: var(--t-fg);
|
|
background: var(--t-bg);
|
|
}
|
|
.brand-badge:active {
|
|
transform: translateY(0.5px);
|
|
}
|
|
.brand-logo {
|
|
width: 14px;
|
|
height: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* -- Brand dropdown -- */
|
|
.brand-dropdown {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 6px);
|
|
left: 0;
|
|
background: var(--t-bg);
|
|
border-radius: 12px;
|
|
padding: 6px;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
width: max-content;
|
|
z-index: 1002;
|
|
}
|
|
.brand-dropdown.open {
|
|
display: flex;
|
|
}
|
|
.brand-dropdown-item {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 34px;
|
|
gap: 8px;
|
|
padding: 0 12px;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: var(--t-fg);
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
font-weight: 400;
|
|
transition: background 0.15s;
|
|
}
|
|
.brand-dropdown-item:hover {
|
|
background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
|
|
}
|
|
.brand-dropdown-logo {
|
|
flex-shrink: 0;
|
|
}
|
|
.brand-dropdown-item .tagline {
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
font-weight: 300;
|
|
margin-left: 0.25rem;
|
|
}
|
|
.brand-dropdown-item .tagline::before {
|
|
content: '·';
|
|
margin-right: 0.25rem;
|
|
}
|
|
|
|
/* -- Contents button (screen-centered via absolute positioning) -- */
|
|
.contents-btn {
|
|
position: absolute;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: center;
|
|
height: 30px;
|
|
gap: 6px;
|
|
padding: 0 12px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--t-bg) 97%, var(--t-fg));
|
|
color: color-mix(in srgb, var(--t-fg) 80%, var(--t-bg));
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: color 0.15s, background 0.15s, box-shadow 0.2s, transform 0.1s;
|
|
}
|
|
.contents-btn:hover {
|
|
color: var(--t-fg);
|
|
background: color-mix(in srgb, var(--t-bg) 92%, var(--t-fg));
|
|
}
|
|
.contents-btn.active {
|
|
color: var(--t-fg);
|
|
background: var(--t-bg);
|
|
}
|
|
.contents-btn:active {
|
|
transform: translateX(-50%) translateY(0.5px);
|
|
}
|
|
.contents-btn svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
/* Hide contents button on smaller screens */
|
|
@media (max-width: 1024px) {
|
|
.contents-btn,
|
|
.mega-menu {
|
|
display: none !important;
|
|
}
|
|
}
|
|
/* -- Craft shadow + radius utilities -- */
|
|
.rounded-6px { border-radius: 6px; }
|
|
.shadow-minimal {
|
|
box-shadow:
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
|
|
rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 1px 1px -0.5px,
|
|
rgba(0, 0, 0, var(--shadow-blur-opacity)) 0px 3px 3px -1.5px;
|
|
}
|
|
.shadow-modal-small {
|
|
box-shadow:
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
|
|
rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.67)) 0px 1px 1px -0.5px,
|
|
rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.67)) 0px 3px 3px 0px,
|
|
rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.33)) 0px 6px 6px 0px,
|
|
rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.33)) 0px 12px 12px 0px,
|
|
rgba(0, 0, 0, calc(var(--shadow-blur-opacity) * 0.33)) 0px 24px 24px 0px;
|
|
}
|
|
.shadow-tinted {
|
|
--shadow-color: 0, 0, 0;
|
|
box-shadow:
|
|
rgba(var(--shadow-color), 0) 0px 0px 0px 0px,
|
|
rgba(var(--shadow-color), 0) 0px 0px 0px 0px,
|
|
rgba(var(--shadow-color), calc(var(--shadow-border-opacity) * 1.5)) 0px 0px 0px 1px,
|
|
rgba(var(--shadow-color), var(--shadow-border-opacity)) 0px 1px 1px -0.5px,
|
|
rgba(var(--shadow-color), var(--shadow-blur-opacity)) 0px 3px 3px -1.5px,
|
|
rgba(var(--shadow-color), calc(var(--shadow-blur-opacity) * 0.67)) 0px 6px 6px -3px;
|
|
}
|
|
|
|
/* -- Mega menu dropdown (x-centered with Contents button) -- */
|
|
.mega-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 6px);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
max-width: min(1180px, calc(100vw - 2rem));
|
|
width: max-content;
|
|
background: var(--t-bg);
|
|
border-radius: 12px;
|
|
padding: 1.5rem 2rem;
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
z-index: 998;
|
|
}
|
|
.mega-menu.open {
|
|
display: block;
|
|
}
|
|
.toc-grid {
|
|
columns: 4;
|
|
column-gap: 2rem;
|
|
}
|
|
@media (max-width: 1200px) {
|
|
.toc-grid {
|
|
columns: 3;
|
|
}
|
|
}
|
|
.toc-category {
|
|
display: inline-block;
|
|
width: 100%;
|
|
margin: 0;
|
|
padding-bottom: 1rem;
|
|
}
|
|
.toc-category h3 {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
margin: 0 0 0.5rem 0;
|
|
color: var(--t-fg);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.toc-category ol {
|
|
padding: 0;
|
|
margin: 0;
|
|
list-style: none;
|
|
font-size: 0.8rem;
|
|
}
|
|
.toc-category li {
|
|
margin-bottom: 0.15rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.toc-category a { color: var(--t-fg); text-decoration: none; }
|
|
.toc-category a:hover { text-decoration: underline; }
|
|
.toc-num { color: color-mix(in srgb, var(--t-fg) 30%, var(--t-bg)); }
|
|
|
|
/* -- Sample card -- */
|
|
.sample {
|
|
background: var(--t-bg);
|
|
margin-bottom: 2rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* -- Hero sample (full-width SVG showcase, above Samples heading) -- */
|
|
.sample-hero {
|
|
margin-bottom: 0;
|
|
background: transparent;
|
|
}
|
|
.hero-diagram-panel {
|
|
padding: 1rem 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background: transparent;
|
|
}
|
|
.hero-diagram-panel .svg-container {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
.hero-diagram-panel .svg-container svg {
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.sample-header {
|
|
padding: 1.25rem 1.5rem;
|
|
max-width: 48rem;
|
|
border-bottom: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg));
|
|
}
|
|
.sample-header h2 {
|
|
font-size: 1.5rem;
|
|
font-weight: 500;
|
|
color: var(--t-fg);
|
|
}
|
|
.description {
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
font-size: 1rem;
|
|
font-weight: 400;
|
|
margin-top: 0.1rem;
|
|
}
|
|
.description code {
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
font-size: 0.875em;
|
|
color: color-mix(in srgb, var(--t-fg) 85%, var(--t-bg));
|
|
background: color-mix(in srgb, var(--t-fg) 6%, var(--t-bg));
|
|
padding: 0.15rem 0.4rem;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.sample-content {
|
|
display: grid;
|
|
grid-template-columns:
|
|
minmax(200px, 1fr)
|
|
minmax(250px, 2fr)
|
|
minmax(250px, 2fr);
|
|
min-height: 420px;
|
|
}
|
|
@media (max-width: 900px) {
|
|
.sample-content { grid-template-columns: 1fr; }
|
|
.ascii-panel { border-left: none !important; border-top: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg)) !important; }
|
|
}
|
|
|
|
/* -- Source panel -- */
|
|
.source-panel {
|
|
position: relative;
|
|
padding: 0.75rem 1.5rem;
|
|
border-right: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg));
|
|
min-width: 0; /* grid child: allow shrinking below content width */
|
|
overflow-y: auto;
|
|
}
|
|
.source-panel h3 {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.source-panel pre {
|
|
padding: 1rem;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.source-panel code {
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
}
|
|
|
|
/* -- Shiki syntax highlighting overrides --
|
|
* Shiki outputs inline style="color:#hex" per token. We override these with
|
|
* color-mix() rules derived from --t-fg / --t-bg so tokens adapt to any theme.
|
|
* The hex values below are from the github-light Shiki theme used at build time. */
|
|
.source-panel {
|
|
background: color-mix(in srgb, var(--t-fg) 1.5%, var(--t-bg));
|
|
}
|
|
.source-panel .shiki {
|
|
background: transparent !important;
|
|
padding: 0.5rem 0;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
margin: 0;
|
|
}
|
|
.source-panel .shiki code {
|
|
background: transparent;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
}
|
|
/* Default text */
|
|
.source-panel .shiki,
|
|
.source-panel .shiki span[style*="#24292e"],
|
|
.source-panel .shiki span[style*="#24292E"] {
|
|
color: color-mix(in srgb, var(--t-fg) 70%, var(--t-bg)) !important;
|
|
}
|
|
/* Keywords: graph, subgraph, end, participant, -->, classDef, brackets */
|
|
.source-panel .shiki span[style*="#D73A49"],
|
|
.source-panel .shiki span[style*="#d73a49"] {
|
|
color: color-mix(in srgb, var(--t-fg) 90%, var(--t-bg)) !important;
|
|
font-weight: 500;
|
|
}
|
|
/* Direction labels, subgraph names */
|
|
.source-panel .shiki span[style*="#6F42C1"],
|
|
.source-panel .shiki span[style*="#6f42c1"] {
|
|
color: color-mix(in srgb, var(--t-fg) 65%, var(--t-bg)) !important;
|
|
}
|
|
/* Node IDs */
|
|
.source-panel .shiki span[style*="#E36209"],
|
|
.source-panel .shiki span[style*="#e36209"] {
|
|
color: color-mix(in srgb, var(--t-fg) 75%, var(--t-bg)) !important;
|
|
}
|
|
/* Strings, labels, message text */
|
|
.source-panel .shiki span[style*="#032F62"],
|
|
.source-panel .shiki span[style*="#032f62"] {
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg)) !important;
|
|
}
|
|
.options {
|
|
margin-top: 0.75rem;
|
|
font-size: 0.8rem;
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
}
|
|
.options code {
|
|
background: color-mix(in srgb, var(--t-fg) 6%, var(--t-bg));
|
|
padding: 0.15rem 0.4rem;
|
|
border-radius: 3px;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
/* -- Edit button (subtle text link, bottom-left of source panel) -- */
|
|
.edit-btn {
|
|
position: absolute;
|
|
bottom: 0.75rem;
|
|
left: 1.5rem;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
font-size: 0.75rem;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: color 0.15s;
|
|
}
|
|
.edit-btn:hover {
|
|
color: var(--t-fg);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* -- Edit dialog overlay -- */
|
|
.edit-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 2000;
|
|
background: rgba(0, 0, 0, 0.4);
|
|
backdrop-filter: blur(4px);
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.edit-overlay.open { display: flex; }
|
|
.edit-dialog {
|
|
background: var(--t-bg);
|
|
border-radius: 16px;
|
|
width: min(680px, calc(100vw - 3rem));
|
|
max-height: calc(100vh - 4rem);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
.edit-dialog-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem 1.25rem;
|
|
border-bottom: 1px solid color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
|
|
}
|
|
.edit-dialog-title {
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
color: var(--t-fg);
|
|
}
|
|
.edit-dialog-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.25rem;
|
|
color: color-mix(in srgb, var(--t-fg) 40%, var(--t-bg));
|
|
cursor: pointer;
|
|
padding: 0 0.25rem;
|
|
line-height: 1;
|
|
}
|
|
.edit-dialog-close:hover { color: var(--t-fg); }
|
|
.edit-dialog-textarea {
|
|
flex: 1;
|
|
min-height: 300px;
|
|
max-height: 60vh;
|
|
margin: 0;
|
|
padding: 1rem 1.25rem;
|
|
border: none;
|
|
outline: none;
|
|
resize: none;
|
|
background: color-mix(in srgb, var(--t-fg) 2%, var(--t-bg));
|
|
color: var(--t-fg);
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
white-space: pre;
|
|
tab-size: 2;
|
|
}
|
|
.edit-dialog-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.25rem;
|
|
border-top: 1px solid color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
|
|
}
|
|
.edit-dialog-btn {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 8px;
|
|
border: none;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: opacity 0.15s;
|
|
}
|
|
.edit-dialog-btn:hover { opacity: 0.85; }
|
|
.edit-dialog-cancel {
|
|
background: color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
|
|
color: var(--t-fg);
|
|
}
|
|
.edit-dialog-save {
|
|
background: var(--t-fg);
|
|
color: var(--t-bg);
|
|
}
|
|
|
|
/* -- SVG panel -- */
|
|
.svg-panel {
|
|
padding: 1.25rem 1.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0; /* grid child: allow shrinking below content width */
|
|
/* Background set dynamically: matches the SVG --bg in default mode,
|
|
or the global theme bg when a theme is active. */
|
|
}
|
|
.svg-panel h3 {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.svg-container {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 0; /* flex child: allow shrinking to fit */
|
|
}
|
|
.svg-container svg {
|
|
max-width: 100%;
|
|
max-height: 100%; /* scale down to fit both axes */
|
|
height: auto;
|
|
}
|
|
|
|
/* -- ASCII panel -- */
|
|
.ascii-panel {
|
|
padding: 1.25rem 1.5rem;
|
|
border-left: 1px solid color-mix(in srgb, var(--t-fg) 5%, var(--t-bg));
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 0; /* grid child: allow shrinking below content width */
|
|
}
|
|
.ascii-panel h3 {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.ascii-output {
|
|
padding: 1rem;
|
|
font-size: 0.7rem;
|
|
line-height: 1.3;
|
|
overflow-x: auto; /* horizontal scroll only */
|
|
overflow-y: hidden; /* scale to height, no vertical scroll */
|
|
white-space: pre;
|
|
flex: 1;
|
|
max-width: 100%;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
}
|
|
|
|
/* -- Loading spinner -- */
|
|
.loading-spinner {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 2px solid color-mix(in srgb, var(--t-fg) 12%, var(--t-bg));
|
|
border-top-color: color-mix(in srgb, var(--t-fg) 35%, var(--t-bg));
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* -- Timing badge -- */
|
|
.timing {
|
|
font-size: 0.7rem;
|
|
font-weight: 400;
|
|
color: color-mix(in srgb, var(--t-fg) 30%, var(--t-bg));
|
|
margin-left: 0.5rem;
|
|
text-transform: none;
|
|
letter-spacing: normal;
|
|
}
|
|
|
|
/* -- Error state -- */
|
|
.render-error {
|
|
color: #ef4444;
|
|
font-size: 0.85rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* -- Hero header section -- */
|
|
.hero-header {
|
|
max-width: 1440px;
|
|
margin: 0 auto;
|
|
padding: 6rem 2rem 2rem;
|
|
text-align: left;
|
|
}
|
|
@media (min-width: 1000px) {
|
|
.hero-header {
|
|
padding: 6rem 3rem 2rem;
|
|
}
|
|
}
|
|
.hero-title {
|
|
font-size: 2.25rem;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
margin: 0 0 0.25rem;
|
|
color: var(--t-fg);
|
|
}
|
|
.hero-tagline {
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
margin: 0 0 1rem;
|
|
}
|
|
.hero-description {
|
|
font-size: 0.95rem;
|
|
line-height: 1.6;
|
|
color: color-mix(in srgb, var(--t-fg) 70%, var(--t-bg));
|
|
margin: 0 0 1.5rem;
|
|
max-width: 680px;
|
|
}
|
|
.hero-description a {
|
|
color: var(--t-fg);
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.hero-description a:hover {
|
|
color: var(--t-accent);
|
|
}
|
|
.hero-buttons {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.hero-buttons {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
.hero-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
line-height: 1.25;
|
|
border-radius: 12px;
|
|
text-decoration: none;
|
|
transition: opacity 0.15s, transform 0.1s;
|
|
cursor: pointer;
|
|
border: none;
|
|
margin: 0;
|
|
box-sizing: border-box;
|
|
font-family: inherit;
|
|
}
|
|
.hero-btn:hover {
|
|
opacity: 0.9;
|
|
}
|
|
.hero-btn:active {
|
|
transform: translateY(0.5px);
|
|
}
|
|
.hero-btn-primary {
|
|
background: var(--t-fg);
|
|
color: var(--t-bg);
|
|
box-shadow:
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
|
rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
|
|
}
|
|
.hero-btn-secondary {
|
|
background: var(--t-bg);
|
|
color: var(--t-fg);
|
|
box-shadow:
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
|
rgba(var(--foreground-rgb), 0.06) 0px 0px 0px 1px,
|
|
rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
|
rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
|
|
}
|
|
.hero-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
.hero-description code {
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 0.85em;
|
|
background: color-mix(in srgb, var(--t-fg) 8%, var(--t-bg));
|
|
padding: 0.15em 0.4em;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* -- Hero meta (below buttons) -- */
|
|
.hero-meta {
|
|
margin-top: 1.25rem;
|
|
}
|
|
.hero-meta .meta {
|
|
font-size: 0.85rem;
|
|
color: color-mix(in srgb, var(--t-fg) 40%, var(--t-bg));
|
|
margin: 0.15rem 0;
|
|
}
|
|
|
|
.hero-meta .meta a {
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.hero-meta .meta a:hover {
|
|
color: var(--t-fg);
|
|
}
|
|
|
|
/* -- Section title -- */
|
|
.section-title {
|
|
font-size: 1.875rem;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
margin: 0;
|
|
padding: 2.5rem 0 1.5rem;
|
|
color: var(--t-fg);
|
|
}
|
|
|
|
/* -- Footer -- */
|
|
.site-footer {
|
|
position: relative;
|
|
z-index: 10;
|
|
padding: 1.5rem 2rem 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
max-width: 1440px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
font-size: 12px;
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
}
|
|
@media (min-width: 1000px) {
|
|
.site-footer {
|
|
padding: 1.5rem 3rem 2rem;
|
|
}
|
|
}
|
|
.footer-links {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
.footer-links a {
|
|
color: color-mix(in srgb, var(--t-fg) 50%, var(--t-bg));
|
|
text-decoration: none;
|
|
transition: color 0.15s;
|
|
}
|
|
.footer-links a:hover {
|
|
color: var(--t-fg);
|
|
}
|
|
.footer-links svg {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Safari 26+ reads title bar color from the topmost fixed element's background.
|
|
This invisible 1px div provides a real DOM element for Safari to detect. -->
|
|
<div id="safari-theme-color" style="position:fixed;top:0;left:0;right:0;height:1px;background:var(--theme-bar-bg);z-index:9999;pointer-events:none;"></div>
|
|
|
|
<!-- Navigation + theme bar -->
|
|
<div class="theme-bar" id="theme-bar">
|
|
<div class="brand-badge-wrapper">
|
|
<button class="brand-badge shadow-minimal" id="brand-badge-btn"><svg class="brand-logo" viewBox="0 0 299 300" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M137.879,300.001 L137.875,300.001 C62.3239,300.001 0.966154,239.232 0.0117188,163.908 L2.56478e-10,162.126 L137.879,162.126 L137.879,300.001 Z" fill="#06367A"/><path d="M137.879,0 L137.875,0 C61.729,0 0,61.729 0,137.875 L0,137.878 L137.879,137.878 L137.879,0 Z" fill="#FF51FF"/><path d="M160.558,137.883 L160.561,137.883 C236.707,137.883 298.436,76.1537 298.436,0.00758561 L298.436,0.00562043 L160.558,0.00562043 L160.558,137.883 Z" fill="#007CFF"/><path d="M160.558,162.123 L160.561,162.123 C236.112,162.123 297.471,222.891 298.426,298.216 L298.436,299.998 L160.558,299.998 L160.558,162.123 Z" fill="#0A377B"/></svg><span><strong>Beautiful Mermaid</strong> by Craft</span></button>
|
|
<div class="brand-dropdown shadow-modal-small" id="brand-dropdown">
|
|
<a href="https://agents.craft.do" class="brand-dropdown-item" target="_blank" rel="noopener">
|
|
<svg width="18" height="18" class="brand-dropdown-logo" style="margin-left: -4px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g transform="translate(3.4502, 3)" fill="#9570BE"><path d="M3.17890888,3.6 L3.17890888,0 L16,0 L16,3.6 L3.17890888,3.6 Z M9.642,7.2 L9.64218223,10.8 L0,10.8 L0,3.6 L16,3.6 L16,7.2 L9.642,7.2 Z M3.17890888,18 L3.178,14.4 L0,14.4 L0,10.8 L16,10.8 L16,18 L3.17890888,18 Z" fill-rule="nonzero"></path></g></svg>
|
|
<span style="margin-left: -2px;">Craft Agents<span class="tagline">Simply mind-blowing</span></span>
|
|
</a>
|
|
<a href="https://craft.do" class="brand-dropdown-item" target="_blank" rel="noopener">
|
|
<svg width="12" height="12" class="brand-dropdown-logo" viewBox="0 0 299 300" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M137.879 300L137.875 300.001C62.3239 300.001 0.966154 239.232 0.0117188 163.908L2.56478e-10 162.126H137.879V300Z" fill="currentColor"/><path d="M137.879 0.000976562L137.875 0C61.729 6.6569e-06 0.000194275 61.729 0 137.875L2.56478e-10 137.878L137.879 137.878L137.879 0.000976562Z" fill="currentColor"/><path d="M160.558 137.882L160.561 137.883C236.707 137.882 298.436 76.1537 298.436 0.00758561V0.00563248L160.558 0.00562043L160.558 137.882Z" fill="currentColor"/><path d="M160.558 162.124L160.561 162.123C236.112 162.123 297.471 222.891 298.426 298.216L298.436 299.998H160.558V162.124Z" fill="currentColor"/></svg>
|
|
<span>Craft Docs<span class="tagline">Amazing Notes & Docs</span></span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<button class="contents-btn shadow-minimal" id="contents-btn"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="4" x2="13" y2="4"/><line x1="3" y1="8" x2="13" y2="8"/><line x1="3" y1="12" x2="10" y2="12"/></svg>Contents</button>
|
|
<div class="theme-pills" id="theme-pills">
|
|
${themePillsHtml}
|
|
</div>
|
|
<div class="mega-menu shadow-modal-small" id="mega-menu">
|
|
<div class="toc-grid">
|
|
${tocSections}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hero header section -->
|
|
<header class="hero-header">
|
|
<h1 class="hero-title">Beautiful Mermaid</h1>
|
|
<p class="hero-tagline">Mermaid Rendering, made beautiful.</p>
|
|
<p class="hero-description">
|
|
An open source library for rendering diagrams, designed for the age of AI: <a href="https://www.npmjs.com/package/beautiful-mermaid" target="_blank" rel="noopener"><code>beautiful-mermaid</code></a>.
|
|
Ultra-fast, fully themeable, and outputs to both SVG and ASCII.<br>
|
|
Built by the team at <a href="https://craft.do" target="_blank" rel="noopener">Craft</a> — because diagrams deserve great design too.
|
|
</p>
|
|
<div class="hero-buttons">
|
|
<a href="https://agents.craft.do" target="_blank" rel="noopener" class="hero-btn hero-btn-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g transform="translate(3.4502, 3)" fill="currentColor"><path d="M3.17890888,3.6 L3.17890888,0 L16,0 L16,3.6 L3.17890888,3.6 Z M9.642,7.2 L9.64218223,10.8 L0,10.8 L0,3.6 L16,3.6 L16,7.2 L9.642,7.2 Z M3.17890888,18 L3.178,14.4 L0,14.4 L0,10.8 L16,10.8 L16,18 L3.17890888,18 Z" fill-rule="nonzero"></path></g></svg>
|
|
Use in Craft Agents
|
|
</a>
|
|
<a href="editor" id="editor-link" class="hero-btn hero-btn-secondary">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
|
Editor
|
|
</a>
|
|
<a href="https://github.com/lukilabs/beautiful-mermaid" target="_blank" rel="noopener" class="hero-btn hero-btn-secondary">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
GitHub
|
|
</a>
|
|
<button type="button" class="hero-btn hero-btn-secondary" id="random-theme-btn">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
|
|
Random Theme
|
|
</button>
|
|
</div>
|
|
<div class="hero-meta">
|
|
<p class="meta" id="total-timing">Rendering ${samples.length * 2} samples\u2026</p>
|
|
<div class="meta">ASCII rendering based on <a href="https://github.com/AlexanderGrooff/mermaid-ascii" target="_blank" rel="noopener">Mermaid-ASCII</a></div>
|
|
<div class="meta">Early preview — actively evolving</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content-wrapper">
|
|
|
|
${heroCardsHtml}
|
|
|
|
<h2 class="section-title">Samples</h2>
|
|
|
|
${regularCardsHtml}
|
|
|
|
<!-- Bundled mermaid renderer — exposes window.__mermaid -->
|
|
<script type="module">
|
|
${bundleJs}
|
|
|
|
// ============================================================================
|
|
// Client-side rendering + theme switching
|
|
// ============================================================================
|
|
|
|
var samples = ${samplesJson};
|
|
var THEMES = window.__mermaid.THEMES;
|
|
var renderMermaid = window.__mermaid.renderMermaidSVGAsync;
|
|
var renderMermaidAscii = window.__mermaid.renderMermaidASCII;
|
|
var diagramColorsToAsciiTheme = window.__mermaid.diagramColorsToAsciiTheme;
|
|
var getSeriesColor = window.__mermaid.getSeriesColor;
|
|
var CHART_ACCENT_FALLBACK = window.__mermaid.CHART_ACCENT_FALLBACK;
|
|
|
|
var totalTimingEl = document.getElementById('total-timing');
|
|
|
|
// -- Theme state --
|
|
// Stores each SVG element's original inline style attribute (from initial render)
|
|
// so we can restore per-sample colors when switching back to "Default".
|
|
var originalSvgStyles = [];
|
|
|
|
function hexToRgb(hex) {
|
|
if (!hex || typeof hex !== 'string') return null;
|
|
var value = hex.trim();
|
|
if (value[0] === '#') value = value.slice(1);
|
|
if (value.length === 3) {
|
|
value = value[0] + value[0] + value[1] + value[1] + value[2] + value[2];
|
|
}
|
|
if (value.length !== 6) return null;
|
|
var intValue = parseInt(value, 16);
|
|
if (Number.isNaN(intValue)) return null;
|
|
return {
|
|
r: (intValue >> 16) & 255,
|
|
g: (intValue >> 8) & 255,
|
|
b: intValue & 255,
|
|
};
|
|
}
|
|
|
|
function setShadowVars(theme) {
|
|
var body = document.body;
|
|
var fg = theme ? theme.fg : '#27272A';
|
|
var bg = theme ? theme.bg : '#FFFFFF';
|
|
var accent = theme ? (theme.accent || '#3b82f6') : '#3b82f6';
|
|
var fgRgb = hexToRgb(fg) || { r: 39, g: 39, b: 42 };
|
|
var bgRgb = hexToRgb(bg) || { r: 255, g: 255, b: 255 };
|
|
var accentRgb = hexToRgb(accent) || { r: 59, g: 130, b: 246 };
|
|
var brightness = (bgRgb.r * 299 + bgRgb.g * 587 + bgRgb.b * 114) / 1000;
|
|
var darkMode = brightness < 140;
|
|
|
|
body.style.setProperty('--foreground-rgb', fgRgb.r + ', ' + fgRgb.g + ', ' + fgRgb.b);
|
|
body.style.setProperty('--accent-rgb', accentRgb.r + ', ' + accentRgb.g + ', ' + accentRgb.b);
|
|
body.style.setProperty('--shadow-border-opacity', darkMode ? '0.15' : '0.08');
|
|
body.style.setProperty('--shadow-blur-opacity', darkMode ? '0.12' : '0.06');
|
|
}
|
|
|
|
// Update <meta name="theme-color"> so Safari 26+ title bar matches the page.
|
|
// Computes color-mix(in srgb, fg 4%, bg) in JS since browsers may not
|
|
// reliably re-evaluate CSS color-mix() for the meta tag.
|
|
function updateThemeColor(fg, bg) {
|
|
var fgRgb = hexToRgb(fg) || { r: 39, g: 39, b: 42 };
|
|
var bgRgb = hexToRgb(bg) || { r: 255, g: 255, b: 255 };
|
|
// Mix: 4% foreground, 96% background (matches body CSS)
|
|
var r = Math.round(bgRgb.r * 0.96 + fgRgb.r * 0.04);
|
|
var g = Math.round(bgRgb.g * 0.96 + fgRgb.g * 0.04);
|
|
var b = Math.round(bgRgb.b * 0.96 + fgRgb.b * 0.04);
|
|
var hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
document.getElementById('theme-color-meta').setAttribute('content', hex);
|
|
// Update --theme-bar-bg on body so gradients update instantly
|
|
document.body.style.setProperty('--theme-bar-bg', hex);
|
|
// Force Safari 26+ to re-read title bar color by updating the invisible fixed div
|
|
// and triggering a reflow (display toggle + offsetHeight read)
|
|
var safariDiv = document.getElementById('safari-theme-color');
|
|
safariDiv.style.background = hex;
|
|
safariDiv.style.display = 'none';
|
|
void safariDiv.offsetHeight;
|
|
safariDiv.style.display = '';
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Apply a named theme (or '' for Default) to the entire page.
|
|
//
|
|
// This is instant — no re-rendering needed. SVGs use CSS custom
|
|
// properties internally, so updating --bg/--fg on the <svg> tag
|
|
// re-paints all nodes, edges, text, and backgrounds via color-mix().
|
|
// ----------------------------------------------------------------
|
|
function applyTheme(themeKey) {
|
|
var theme = themeKey ? THEMES[themeKey] : null;
|
|
var body = document.body;
|
|
|
|
// 1. Update body CSS variables — the entire page derives from these
|
|
if (theme) {
|
|
body.style.setProperty('--t-bg', theme.bg);
|
|
body.style.setProperty('--t-fg', theme.fg);
|
|
body.style.setProperty('--t-accent', theme.accent || '#3b82f6');
|
|
} else {
|
|
body.style.setProperty('--t-bg', '#FFFFFF');
|
|
body.style.setProperty('--t-fg', '#27272A');
|
|
body.style.setProperty('--t-accent', '#3b82f6');
|
|
}
|
|
setShadowVars(theme);
|
|
updateThemeColor(theme ? theme.fg : '#27272A', theme ? theme.bg : '#FFFFFF');
|
|
|
|
// 2. Update all rendered SVG elements' CSS variables
|
|
var svgs = document.querySelectorAll('.svg-container svg');
|
|
for (var j = 0; j < svgs.length; j++) {
|
|
var svgEl = svgs[j];
|
|
if (theme) {
|
|
// Override with the global theme colors
|
|
svgEl.style.setProperty('--bg', theme.bg);
|
|
svgEl.style.setProperty('--fg', theme.fg);
|
|
// Set enrichment variables if provided, else remove so SVG
|
|
// internal color-mix() fallbacks activate
|
|
var enrichment = ['line', 'accent', 'muted', 'surface', 'border'];
|
|
for (var k = 0; k < enrichment.length; k++) {
|
|
var prop = enrichment[k];
|
|
if (theme[prop]) svgEl.style.setProperty('--' + prop, theme[prop]);
|
|
else svgEl.style.removeProperty('--' + prop);
|
|
}
|
|
// Recompute xychart series color vars from the new accent
|
|
var maxColor = parseInt(svgEl.getAttribute('data-xychart-colors') || '-1', 10);
|
|
if (maxColor >= 0) {
|
|
var accent = theme.accent || CHART_ACCENT_FALLBACK;
|
|
svgEl.style.setProperty('--xychart-color-0', accent);
|
|
for (var ci = 1; ci <= maxColor; ci++) {
|
|
svgEl.style.setProperty('--xychart-color-' + ci, getSeriesColor(ci, accent, theme.bg));
|
|
}
|
|
}
|
|
} else {
|
|
// Restore original inline style from initial render
|
|
if (originalSvgStyles[j] !== undefined) {
|
|
svgEl.setAttribute('style', originalSvgStyles[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Update SVG panel backgrounds to match (skip hero panels - keep transparent)
|
|
for (var j = 0; j < samples.length; j++) {
|
|
var panel = document.getElementById('svg-panel-' + j);
|
|
if (!panel) continue;
|
|
// Skip hero panels - they stay transparent
|
|
if (panel.classList.contains('hero-diagram-panel')) continue;
|
|
if (theme) {
|
|
panel.style.background = theme.bg;
|
|
} else {
|
|
// Default mode: use the per-sample bg (or clear for page default)
|
|
var sampleBg = panel.getAttribute('data-sample-bg');
|
|
panel.style.background = sampleBg || '';
|
|
}
|
|
}
|
|
|
|
// 4. Re-render ASCII panels with new theme colors
|
|
var asciiTheme = theme ? diagramColorsToAsciiTheme(theme) : null;
|
|
for (var j = 0; j < samples.length; j++) {
|
|
var asciiEl = document.getElementById('ascii-' + j);
|
|
if (!asciiEl) continue;
|
|
try {
|
|
asciiEl.innerHTML = renderMermaidAscii(
|
|
samples[j].source,
|
|
asciiTheme ? { theme: asciiTheme } : {}
|
|
);
|
|
} catch (e) { /* keep existing content */ }
|
|
}
|
|
|
|
// 5. Update active pill
|
|
var pills = document.querySelectorAll('.theme-pill');
|
|
for (var j = 0; j < pills.length; j++) {
|
|
var isActive = pills[j].getAttribute('data-theme') === themeKey;
|
|
pills[j].classList.toggle('active', isActive);
|
|
pills[j].classList.toggle('shadow-tinted', isActive);
|
|
}
|
|
|
|
// 6. Persist selection
|
|
if (themeKey) {
|
|
localStorage.setItem('mermaid-theme', themeKey);
|
|
} else {
|
|
localStorage.removeItem('mermaid-theme');
|
|
}
|
|
}
|
|
|
|
// -- Set up theme pill click handlers --
|
|
document.getElementById('theme-pills').addEventListener('click', function(e) {
|
|
var pill = e.target.closest('.theme-pill');
|
|
if (!pill || pill.id === 'theme-more-btn') return;
|
|
applyTheme(pill.getAttribute('data-theme') || '');
|
|
// Close "More" dropdown if a theme was picked from it
|
|
var dd = document.getElementById('theme-more-dropdown');
|
|
if (dd && dd.classList.contains('open')) dd.classList.remove('open');
|
|
});
|
|
|
|
// -- "More" themes dropdown (direct listener, same pattern as Contents) --
|
|
var moreBtn = document.getElementById('theme-more-btn');
|
|
var moreDropdown = document.getElementById('theme-more-dropdown');
|
|
|
|
if (moreBtn && moreDropdown) {
|
|
moreBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
moreDropdown.classList.toggle('open');
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', function(e) {
|
|
if (!moreDropdown.classList.contains('open')) return;
|
|
if (!e.target.closest('.theme-more-wrapper')) {
|
|
moreDropdown.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && moreDropdown.classList.contains('open')) {
|
|
moreDropdown.classList.remove('open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// -- Random theme button --
|
|
// Resolve the Editor link against the current path so it works under both
|
|
// / (direct Pages URL) and /mermaid (Worker proxy, with or without trailing slash).
|
|
// Note: this script is embedded inside a TypeScript template literal, so regex
|
|
// backslashes get consumed during string interpolation. We use endsWith/slice
|
|
// here instead of a regex literal to avoid escaping pitfalls.
|
|
var editorLink = document.getElementById('editor-link');
|
|
if (editorLink) {
|
|
var basePath = location.pathname;
|
|
if (basePath.indexOf('/index.html', basePath.length - 11) !== -1) {
|
|
basePath = basePath.slice(0, basePath.length - 11);
|
|
}
|
|
if (basePath.charAt(basePath.length - 1) === '/') {
|
|
basePath = basePath.slice(0, basePath.length - 1);
|
|
}
|
|
editorLink.href = basePath + '/editor';
|
|
}
|
|
|
|
var randomThemeBtn = document.getElementById('random-theme-btn');
|
|
var themeKeys = Object.keys(THEMES);
|
|
var currentThemeKey = localStorage.getItem('mermaid-theme') || '';
|
|
|
|
if (randomThemeBtn) {
|
|
randomThemeBtn.addEventListener('click', function() {
|
|
// Filter out the current theme so we never pick the same one
|
|
var availableKeys = themeKeys.filter(function(k) { return k !== currentThemeKey; });
|
|
// Also include default ('') if not currently selected
|
|
if (currentThemeKey !== '') availableKeys.push('');
|
|
// Pick a random theme
|
|
var randomIndex = Math.floor(Math.random() * availableKeys.length);
|
|
var newThemeKey = availableKeys[randomIndex];
|
|
currentThemeKey = newThemeKey;
|
|
applyTheme(newThemeKey);
|
|
});
|
|
}
|
|
|
|
// -- Brand dropdown --
|
|
var brandBtn = document.getElementById('brand-badge-btn');
|
|
var brandDropdown = document.getElementById('brand-dropdown');
|
|
|
|
if (brandBtn && brandDropdown) {
|
|
brandBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
var isOpen = brandDropdown.classList.toggle('open');
|
|
brandBtn.classList.toggle('active', isOpen);
|
|
brandBtn.classList.toggle('shadow-tinted', isOpen);
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', function(e) {
|
|
if (!brandDropdown.classList.contains('open')) return;
|
|
if (!e.target.closest('.brand-badge-wrapper')) {
|
|
brandDropdown.classList.remove('open');
|
|
brandBtn.classList.remove('active');
|
|
brandBtn.classList.remove('shadow-tinted');
|
|
}
|
|
});
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && brandDropdown.classList.contains('open')) {
|
|
brandDropdown.classList.remove('open');
|
|
brandBtn.classList.remove('active');
|
|
brandBtn.classList.remove('shadow-tinted');
|
|
}
|
|
});
|
|
}
|
|
|
|
// -- Mega menu (Contents dropdown) --
|
|
var contentsBtn = document.getElementById('contents-btn');
|
|
var megaMenu = document.getElementById('mega-menu');
|
|
|
|
contentsBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
var isOpen = megaMenu.classList.toggle('open');
|
|
contentsBtn.classList.toggle('active', isOpen);
|
|
contentsBtn.classList.toggle('shadow-tinted', isOpen);
|
|
});
|
|
|
|
// Close on clicking a ToC link (smooth scroll to target)
|
|
megaMenu.addEventListener('click', function(e) {
|
|
var link = e.target.closest('a');
|
|
if (!link) return;
|
|
e.preventDefault();
|
|
megaMenu.classList.remove('open');
|
|
contentsBtn.classList.remove('active');
|
|
contentsBtn.classList.remove('shadow-tinted');
|
|
var target = document.querySelector(link.getAttribute('href'));
|
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', function(e) {
|
|
if (!megaMenu.classList.contains('open')) return;
|
|
if (!e.target.closest('.mega-menu') && !e.target.closest('.contents-btn')) {
|
|
megaMenu.classList.remove('open');
|
|
contentsBtn.classList.remove('active');
|
|
contentsBtn.classList.remove('shadow-tinted');
|
|
}
|
|
});
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && megaMenu.classList.contains('open')) {
|
|
megaMenu.classList.remove('open');
|
|
contentsBtn.classList.remove('active');
|
|
contentsBtn.classList.remove('shadow-tinted');
|
|
}
|
|
});
|
|
|
|
// -- Restore saved theme immediately (before rendering begins) --
|
|
var savedTheme = localStorage.getItem('mermaid-theme');
|
|
if (savedTheme && THEMES[savedTheme]) {
|
|
// Apply page-level CSS variables right away to avoid flash
|
|
document.body.style.setProperty('--t-bg', THEMES[savedTheme].bg);
|
|
document.body.style.setProperty('--t-fg', THEMES[savedTheme].fg);
|
|
document.body.style.setProperty('--t-accent', THEMES[savedTheme].accent || '#3b82f6');
|
|
setShadowVars(THEMES[savedTheme]);
|
|
updateThemeColor(THEMES[savedTheme].fg, THEMES[savedTheme].bg);
|
|
// Mark the correct pill as active
|
|
var pills = document.querySelectorAll('.theme-pill');
|
|
for (var j = 0; j < pills.length; j++) {
|
|
var isActive = pills[j].getAttribute('data-theme') === savedTheme;
|
|
pills[j].classList.toggle('active', isActive);
|
|
pills[j].classList.toggle('shadow-tinted', isActive);
|
|
}
|
|
} else {
|
|
setShadowVars(null);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Progressive rendering — render each diagram sequentially
|
|
// ============================================================================
|
|
|
|
var totalStart = performance.now();
|
|
|
|
for (var i = 0; i < samples.length; i++) {
|
|
var sample = samples[i];
|
|
var svgContainer = document.getElementById('svg-' + i);
|
|
var asciiContainer = document.getElementById('ascii-' + i);
|
|
var svgPanel = document.getElementById('svg-panel-' + i);
|
|
|
|
// Render SVG — wrapped in a timeout guard so a stalled layout
|
|
// doesn't block all remaining diagrams from rendering.
|
|
try {
|
|
var svg = await renderMermaid(sample.source, sample.options);
|
|
svgContainer.innerHTML = svg;
|
|
|
|
// Store the SVG's original inline style for Default mode restoration
|
|
var svgEl = svgContainer.querySelector('svg');
|
|
if (svgEl) {
|
|
originalSvgStyles.push(svgEl.getAttribute('style') || '');
|
|
|
|
// If a global theme is active, immediately override the SVG's variables
|
|
if (savedTheme && THEMES[savedTheme]) {
|
|
var th = THEMES[savedTheme];
|
|
svgEl.style.setProperty('--bg', th.bg);
|
|
svgEl.style.setProperty('--fg', th.fg);
|
|
var enrichment = ['line', 'accent', 'muted', 'surface', 'border'];
|
|
for (var k = 0; k < enrichment.length; k++) {
|
|
if (th[enrichment[k]]) svgEl.style.setProperty('--' + enrichment[k], th[enrichment[k]]);
|
|
else svgEl.style.removeProperty('--' + enrichment[k]);
|
|
}
|
|
// Recompute xychart series color vars from the saved theme's accent
|
|
var maxColor = parseInt(svgEl.getAttribute('data-xychart-colors') || '-1', 10);
|
|
if (maxColor >= 0) {
|
|
var accent = th.accent || CHART_ACCENT_FALLBACK;
|
|
svgEl.style.setProperty('--xychart-color-0', accent);
|
|
for (var ci = 1; ci <= maxColor; ci++) {
|
|
svgEl.style.setProperty('--xychart-color-' + ci, getSeriesColor(ci, accent, th.bg));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
originalSvgStyles.push('');
|
|
}
|
|
|
|
// Set panel background to match the SVG (skip for hero panels - keep transparent)
|
|
var isHeroPanel = svgPanel.classList.contains('hero-diagram-panel');
|
|
if (!isHeroPanel) {
|
|
if (savedTheme && THEMES[savedTheme]) {
|
|
svgPanel.style.background = THEMES[savedTheme].bg;
|
|
} else {
|
|
var sampleBg = svgPanel.getAttribute('data-sample-bg');
|
|
if (sampleBg) svgPanel.style.background = sampleBg;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
svgContainer.innerHTML = '<div class="render-error">SVG Error: ' + escapeHtml(String(err)) + '</div>';
|
|
originalSvgStyles.push('');
|
|
}
|
|
|
|
// Hero samples don't have ASCII panels
|
|
if (asciiContainer) {
|
|
try {
|
|
var asciiOpts = savedTheme && THEMES[savedTheme]
|
|
? { theme: diagramColorsToAsciiTheme(THEMES[savedTheme]) }
|
|
: {};
|
|
asciiContainer.innerHTML = renderMermaidAscii(sample.source, asciiOpts);
|
|
} catch (e) {
|
|
asciiContainer.textContent = '(ASCII not supported for this diagram type)';
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Done — show total time
|
|
var totalMs = (performance.now() - totalStart).toFixed(0);
|
|
totalTimingEl.textContent = (samples.length * 2) + ' samples (SVG+ASCII) rendered in ' + totalMs + ' ms';
|
|
|
|
function escapeHtml(text) {
|
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edit dialog — open, close, save & re-render
|
|
// ============================================================================
|
|
|
|
var editOverlay = document.getElementById('edit-overlay');
|
|
var editTextarea = document.getElementById('edit-dialog-textarea');
|
|
var editSaveBtn = document.getElementById('edit-dialog-save');
|
|
var editCancelBtn = document.getElementById('edit-dialog-cancel');
|
|
var editCloseBtn = document.getElementById('edit-dialog-close');
|
|
var editingSampleIndex = -1;
|
|
|
|
function openEditDialog(index) {
|
|
editingSampleIndex = index;
|
|
editTextarea.value = samples[index].source;
|
|
editOverlay.classList.add('open');
|
|
editTextarea.focus();
|
|
}
|
|
|
|
function closeEditDialog() {
|
|
editOverlay.classList.remove('open');
|
|
editingSampleIndex = -1;
|
|
}
|
|
|
|
async function saveAndRender() {
|
|
var index = editingSampleIndex;
|
|
if (index < 0) return;
|
|
var source = editTextarea.value;
|
|
samples[index].source = source;
|
|
|
|
// Close dialog immediately so user sees results rendering
|
|
closeEditDialog();
|
|
|
|
// Update source panel with plain text (Shiki not available at runtime)
|
|
var sourcePanel = document.getElementById('source-panel-' + index);
|
|
if (sourcePanel) {
|
|
var shikiEl = sourcePanel.querySelector('.shiki');
|
|
if (shikiEl) {
|
|
shikiEl.innerHTML = '<code>' + escapeHtml(source) + '</code>';
|
|
}
|
|
}
|
|
|
|
// Re-render SVG (async — renderMermaid returns a Promise)
|
|
var svgContainer = document.getElementById('svg-' + index);
|
|
try {
|
|
var svg = await renderMermaid(source, samples[index].options);
|
|
svgContainer.innerHTML = svg;
|
|
var svgEl = svgContainer.querySelector('svg');
|
|
if (svgEl) {
|
|
originalSvgStyles[index] = svgEl.getAttribute('style') || '';
|
|
var activeTheme = localStorage.getItem('mermaid-theme');
|
|
if (activeTheme && THEMES[activeTheme]) {
|
|
var th = THEMES[activeTheme];
|
|
svgEl.style.setProperty('--bg', th.bg);
|
|
svgEl.style.setProperty('--fg', th.fg);
|
|
var enrichment = ['line', 'accent', 'muted', 'surface', 'border'];
|
|
for (var k = 0; k < enrichment.length; k++) {
|
|
if (th[enrichment[k]]) svgEl.style.setProperty('--' + enrichment[k], th[enrichment[k]]);
|
|
else svgEl.style.removeProperty('--' + enrichment[k]);
|
|
}
|
|
// Recompute xychart series color vars
|
|
var maxColor = parseInt(svgEl.getAttribute('data-xychart-colors') || '-1', 10);
|
|
if (maxColor >= 0) {
|
|
var accent = th.accent || CHART_ACCENT_FALLBACK;
|
|
svgEl.style.setProperty('--xychart-color-0', accent);
|
|
for (var ci = 1; ci <= maxColor; ci++) {
|
|
svgEl.style.setProperty('--xychart-color-' + ci, getSeriesColor(ci, accent, th.bg));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
svgContainer.innerHTML = '<div class="render-error">' + escapeHtml(String(err)) + '</div>';
|
|
}
|
|
|
|
// Re-render ASCII
|
|
var asciiContainer = document.getElementById('ascii-' + index);
|
|
if (asciiContainer) {
|
|
try {
|
|
var activeThemeKey = localStorage.getItem('mermaid-theme');
|
|
var editAsciiOpts = activeThemeKey && THEMES[activeThemeKey]
|
|
? { theme: diagramColorsToAsciiTheme(THEMES[activeThemeKey]) }
|
|
: {};
|
|
asciiContainer.innerHTML = renderMermaidAscii(source, editAsciiOpts);
|
|
} catch (e) {
|
|
asciiContainer.textContent = '(ASCII error: ' + e.message + ')';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
document.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.edit-btn');
|
|
if (btn) openEditDialog(parseInt(btn.dataset.sample, 10));
|
|
});
|
|
editSaveBtn.addEventListener('click', saveAndRender);
|
|
editCancelBtn.addEventListener('click', closeEditDialog);
|
|
editCloseBtn.addEventListener('click', closeEditDialog);
|
|
editOverlay.addEventListener('click', function(e) {
|
|
if (e.target === editOverlay) closeEditDialog();
|
|
});
|
|
document.addEventListener('keydown', function(e) {
|
|
if (!editOverlay.classList.contains('open')) return;
|
|
if (e.key === 'Escape') closeEditDialog();
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') saveAndRender();
|
|
});
|
|
|
|
</script>
|
|
|
|
<!-- Edit dialog (shared single instance) -->
|
|
<div class="edit-overlay" id="edit-overlay">
|
|
<div class="edit-dialog shadow-modal-small">
|
|
<div class="edit-dialog-header">
|
|
<span class="edit-dialog-title">Edit Diagram</span>
|
|
<button class="edit-dialog-close" id="edit-dialog-close">×</button>
|
|
</div>
|
|
<textarea class="edit-dialog-textarea" id="edit-dialog-textarea"
|
|
spellcheck="false" autocomplete="off" autocorrect="off"></textarea>
|
|
<div class="edit-dialog-footer">
|
|
<button class="edit-dialog-btn edit-dialog-cancel" id="edit-dialog-cancel">Cancel</button>
|
|
<button class="edit-dialog-btn edit-dialog-save" id="edit-dialog-save">Save & Render</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- .content-wrapper -->
|
|
|
|
<footer class="site-footer">
|
|
<span>© 2026 Craft Docs Limited, Inc. All rights reserved.</span>
|
|
<div class="footer-links">
|
|
<a href="mailto:agents@craft.do">Contact</a>
|
|
<a href="https://github.com/lukilabs/beautiful-mermaid" target="_blank" rel="noopener noreferrer">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
</svg>
|
|
</a>
|
|
<a href="https://x.com/craftdocs" target="_blank" rel="noopener noreferrer">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main
|
|
// ============================================================================
|
|
|
|
const html = await generateHtml()
|
|
const outPath = new URL('./index.html', import.meta.url).pathname
|
|
await Bun.write(outPath, html)
|
|
console.log(`Written to ${outPath} (${(html.length / 1024).toFixed(1)} KB)`)
|