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;
+}