diff --git a/src/scripts/MERMAID.md b/src/scripts/MERMAID.md new file mode 100644 index 000000000000000..96cb6a43333937c --- /dev/null +++ b/src/scripts/MERMAID.md @@ -0,0 +1,54 @@ +# Mermaid Diagram Rendering + +Client-side rendering of Mermaid diagrams with Cloudflare branding (orange `#f6821f`), light/dark theme support, and annotation footers. + +## Usage + +### Basic Diagram + +````markdown +```mermaid +flowchart LR +A[Client] --> B[Server] +B --> C[Database] +``` +```` + +### With Title and Accessibility + +````markdown +```mermaid +flowchart LR +accTitle: Workers for Platforms - Main Flow +accDescr: Shows how requests are routed through the platform + +A[Browser Request] --> B[Router Worker] +B -->|Service Binding| C[User Worker A] +B -->|Service Binding| D[User Worker B] +``` +```` + +The `accTitle` appears in the annotation footer. Always include `accDescr` for screen readers. + +## How It Works + +1. Rehype plugin (`src/plugins/rehype/mermaid.ts`) transforms markdown code blocks to `
` +2. Client script (`src/scripts/mermaid.ts`) renders diagrams as SVG with custom theme variables +3. Theme changes trigger automatic re-rendering via `MutationObserver` + +## Customization + +- **Theme variables**: Edit `mermaid.ts` +- **Container styles**: Edit `src/styles/mermaid.css` +- See [Mermaid theming docs](https://mermaid.js.org/config/theming.html) + +## Troubleshooting + +- **Not rendering**: Check browser console, validate syntax at [mermaid.live](https://mermaid.live/) +- **No annotation**: Ensure `accTitle` is included in diagram definition + +## Related Files + +- `src/scripts/mermaid.ts` - Rendering script +- `src/styles/mermaid.css` - Styles +- `src/plugins/rehype/mermaid.ts` - Markdown transformer diff --git a/src/scripts/mermaid.ts b/src/scripts/mermaid.ts index 7698f76482a681f..04ae1b826a64287 100644 --- a/src/scripts/mermaid.ts +++ b/src/scripts/mermaid.ts @@ -4,11 +4,96 @@ const diagrams = document.querySelectorAll("pre.mermaid"); let init = false; +// Get computed font family from CSS variable +function getFontFamily(): string { + const computedStyle = getComputedStyle(document.documentElement); + const slFont = computedStyle.getPropertyValue("--__sl-font").trim(); + return slFont || "system-ui, -apple-system, sans-serif"; +} + +// Create wrapper container with annotation +function wrapDiagram(diagram: HTMLPreElement, title: string | null) { + // Skip if already wrapped + if (diagram.parentElement?.classList.contains("mermaid-container")) { + return; + } + + // Create container + const container = document.createElement("div"); + container.className = "mermaid-container"; + + // Wrap the diagram + diagram.parentNode?.insertBefore(container, diagram); + container.appendChild(diagram); + + // Add annotation footer if title exists + if (title) { + const footer = document.createElement("div"); + footer.className = "mermaid-annotation"; + + const titleSpan = document.createElement("span"); + titleSpan.className = "mermaid-annotation-title"; + titleSpan.textContent = title; + + const logo = document.createElement("img"); + logo.src = "/logo.svg"; + logo.alt = "Cloudflare"; + logo.className = "mermaid-annotation-logo"; + + footer.appendChild(titleSpan); + footer.appendChild(logo); + container.appendChild(footer); + } +} + async function render() { - const theme = - document.documentElement.getAttribute("data-theme") === "light" - ? "neutral" - : "dark"; + const isLight = + document.documentElement.getAttribute("data-theme") === "light"; + const fontFamily = getFontFamily(); + + // Custom theme variables for Cloudflare branding + const lightThemeVars = { + fontFamily, + primaryColor: "#fef1e6", // cl1-orange-9 (very light orange for node backgrounds) + primaryBorderColor: "#f6821f", // cl1-brand-orange + primaryTextColor: "#1d1d1d", // cl1-gray-0 + secondaryColor: "#f2f2f2", // cl1-gray-9 + secondaryBorderColor: "#999999", // cl1-gray-6 + secondaryTextColor: "#1d1d1d", // cl1-gray-0 + tertiaryColor: "#f2f2f2", // cl1-gray-9 + tertiaryBorderColor: "#999999", // cl1-gray-6 + tertiaryTextColor: "#1d1d1d", // cl1-gray-0 + lineColor: "#f6821f", // cl1-brand-orange for arrows + textColor: "#1d1d1d", // cl1-gray-0 + mainBkg: "#fef1e6", // cl1-orange-9 + errorBkgColor: "#ffefee", // cl1-red-9 + errorTextColor: "#3c0501", // cl1-red-0 + edgeLabelBackground: "#ffffff", // white background for edge labels in light mode + labelBackground: "#ffffff", // white background for labels in light mode + }; + + const darkThemeVars = { + fontFamily, + primaryColor: "#482303", // cl1-orange-1 (dark orange for node backgrounds) + primaryBorderColor: "#f6821f", // cl1-brand-orange + primaryTextColor: "#f2f2f2", // cl1-gray-9 + secondaryColor: "#313131", // cl1-gray-1 + secondaryBorderColor: "#797979", // cl1-gray-5 + secondaryTextColor: "#f2f2f2", // cl1-gray-9 + tertiaryColor: "#313131", // cl1-gray-1 + tertiaryBorderColor: "#797979", // cl1-gray-5 + tertiaryTextColor: "#f2f2f2", // cl1-gray-9 + lineColor: "#f6821f", // cl1-brand-orange for arrows + textColor: "#f2f2f2", // cl1-gray-9 + mainBkg: "#482303", // cl1-orange-1 + background: "#1d1d1d", // cl1-gray-0 + errorBkgColor: "#3c0501", // cl1-red-0 + errorTextColor: "#ffefee", // cl1-red-9 + edgeLabelBackground: "#1d1d1d", // dark background for edge labels + labelBackground: "#1d1d1d", // dark background for labels + }; + + const themeVariables = isLight ? lightThemeVars : darkThemeVars; for (const diagram of diagrams) { if (!init) { @@ -17,10 +102,30 @@ async function render() { const def = diagram.getAttribute("data-diagram") as string; - mermaid.initialize({ startOnLoad: false, theme }); + // Initialize with base theme and custom variables + mermaid.initialize({ + startOnLoad: false, + theme: "base", + themeVariables, + flowchart: { + htmlLabels: true, + useMaxWidth: true, + }, + }); + await mermaid .render(`mermaid-${crypto.randomUUID()}`, def) - .then(({ svg }) => (diagram.innerHTML = svg)); + .then(({ svg }) => { + diagram.innerHTML = svg; + + // Extract title from SVG for annotation + const svgElement = diagram.querySelector("svg"); + const titleElement = svgElement?.querySelector("title"); + const title = titleElement?.textContent?.trim() || null; + + // Wrap diagram with container and annotation + wrapDiagram(diagram, title); + }); diagram.setAttribute("data-processed", "true"); } @@ -34,3 +139,5 @@ obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"], }); + +render(); diff --git a/src/styles/mermaid.css b/src/styles/mermaid.css index 84b443be558081d..920d8652a32d844 100644 --- a/src/styles/mermaid.css +++ b/src/styles/mermaid.css @@ -1,3 +1,75 @@ +/* Hide unprocessed diagrams to prevent flash of unstyled content */ pre.mermaid:not([data-processed]) { visibility: hidden; } + +/* Container wrapper for diagram + annotation */ +.mermaid-container { + background: transparent; + overflow: hidden; + margin: 1rem 0; + border-radius: 0; + box-shadow: none; +} + +/* The diagram itself */ +pre.mermaid[data-processed] { + padding: 1.5rem; + margin: 0; + background: transparent; + border-radius: 0; + box-shadow: none; + display: block; + line-height: 0; + border-color: var(--sl-color-hairline) !important; +} + +/* Ensure SVG fills the width nicely */ +pre.mermaid[data-processed] svg { + max-width: 100%; + height: auto; + display: block; + margin: 0; + padding: 0; + vertical-align: top; +} + +/* Remove any box shadow from SVG elements */ +pre.mermaid[data-processed] svg * { + box-shadow: none !important; +} + +/* Annotation footer */ +.mermaid-annotation { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--sl-color-gray-3); + background: rgba(0, 0, 0, 0.02); + margin: 0; +} + +.mermaid-annotation-title { + flex: 1; +} + +.mermaid-annotation-logo { + height: 16px; + width: auto; + opacity: 0.7; +} + +/* Dark mode adjustments */ +:root[data-theme="dark"] .mermaid-annotation { + background: rgba(255, 255, 255, 0.03); + color: var(--sl-color-gray-2); +} + +:root[data-theme="dark"] .mermaid-annotation-logo { + opacity: 0.8; +}