1- import mermaid from "mermaid" ;
2-
31const diagrams = document . querySelectorAll < HTMLPreElement > ( "pre.mermaid" ) ;
42
5- let init = false ;
3+ // Skip all setup if no mermaid diagrams on this page
4+ if ( diagrams . length === 0 ) {
5+ // No-op — avoid creating dialog, listeners, or loading the mermaid bundle
6+ } else {
7+ let init = false ;
68
7- // Get computed font family from CSS variable
8- function getFontFamily ( ) : string {
9- const computedStyle = getComputedStyle ( document . documentElement ) ;
10- const slFont = computedStyle . getPropertyValue ( "--__sl-font" ) . trim ( ) ;
11- return slFont || "system-ui, -apple-system, sans-serif" ;
12- }
9+ // Full-screen expand dialog (lazy — only created because diagrams exist)
10+ let dialog : HTMLDialogElement | null = null ;
11+
12+ function getDialog ( ) : HTMLDialogElement {
13+ if ( dialog ) return dialog ;
14+
15+ dialog = document . createElement ( "dialog" ) ;
16+ dialog . className = "mermaid-dialog" ;
17+ dialog . innerHTML = `
18+ <div class="mermaid-dialog-body"></div>
19+ <button class="mermaid-dialog-close" aria-label="Close">
20+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21+ <line x1="18" y1="6" x2="6" y2="18"></line>
22+ <line x1="6" y1="6" x2="18" y2="18"></line>
23+ </svg>
24+ </button>
25+ ` ;
26+ document . body . appendChild ( dialog ) ;
27+
28+ function closeWithAnimation ( ) {
29+ if ( ! dialog || ! dialog . open ) return ;
30+ dialog . classList . add ( "closing" ) ;
31+ dialog . addEventListener (
32+ "animationend" ,
33+ ( ) => {
34+ dialog ! . classList . remove ( "closing" ) ;
35+ dialog ! . close ( ) ;
36+ document . documentElement . style . overflow = "" ;
37+ } ,
38+ { once : true } ,
39+ ) ;
40+ }
41+
42+ dialog . addEventListener ( "click" , ( e ) => {
43+ if ( e . target === dialog ) closeWithAnimation ( ) ;
44+ } ) ;
45+ dialog
46+ . querySelector ( ".mermaid-dialog-close" )
47+ ?. addEventListener ( "click" , ( ) => {
48+ closeWithAnimation ( ) ;
49+ } ) ;
50+ // Handle Escape key — native dialog closes immediately,
51+ // so we intercept cancel to animate first
52+ dialog . addEventListener ( "cancel" , ( e ) => {
53+ e . preventDefault ( ) ;
54+ closeWithAnimation ( ) ;
55+ } ) ;
1356
14- // Create wrapper container with annotation
15- function wrapDiagram ( diagram : HTMLPreElement , title : string | null ) {
16- // Skip if already wrapped
17- if ( diagram . parentElement ?. classList . contains ( "mermaid-container" ) ) {
18- return ;
57+ return dialog ;
1958 }
2059
21- // Create container
22- const container = document . createElement ( "div" ) ;
23- container . className = "mermaid- container" ;
60+ function openDiagram ( container : HTMLElement ) {
61+ const d = getDialog ( ) ;
62+ const clone = container . cloneNode ( true ) as HTMLElement ;
2463
25- // Wrap the diagram
26- diagram . parentNode ?. insertBefore ( container , diagram ) ;
27- container . appendChild ( diagram ) ;
64+ // Remove the expand button from the clone
65+ clone . querySelector ( ".mermaid-expand" ) ?. remove ( ) ;
2866
29- // Add annotation footer if title exists
30- if ( title ) {
31- const footer = document . createElement ( "div" ) ;
32- footer . className = "mermaid-annotation" ;
67+ // Let the SVG scale freely in the expanded view
68+ const svg = clone . querySelector ( "svg" ) ;
69+ if ( svg ) {
70+ svg . removeAttribute ( "style" ) ;
71+ svg . setAttribute ( "width" , "100%" ) ;
72+ svg . setAttribute ( "height" , "auto" ) ;
73+ }
3374
34- const titleSpan = document . createElement ( "span ") ;
35- titleSpan . className = "mermaid-annotation-title" ;
36- titleSpan . textContent = title ;
75+ const body = d . querySelector ( ".mermaid-dialog-body ") ;
76+ if ( ! body ) return ;
77+ body . replaceChildren ( clone ) ;
3778
38- const logo = document . createElement ( "img" ) ;
39- logo . src = "/logo.svg" ;
40- logo . alt = "Cloudflare" ;
41- logo . className = "mermaid-annotation-logo" ;
79+ // Close dialog when clicking Mermaid `click` links inside the expanded view.
80+ clone . addEventListener ( "click" , ( e ) => {
81+ const target = e . target as Element ;
82+ const anchor = target . closest ( "a" ) ;
83+ const clickable = target . closest ( ".clickable" ) ;
84+ if ( anchor || clickable ) {
85+ // Skip animation for link clicks — navigate immediately
86+ d . close ( ) ;
87+ document . documentElement . style . overflow = "" ;
88+ }
89+ } ) ;
4290
43- footer . appendChild ( titleSpan ) ;
44- footer . appendChild ( logo ) ;
45- container . appendChild ( footer ) ;
91+ document . documentElement . style . overflow = "hidden" ;
92+ d . showModal ( ) ;
4693 }
47- }
4894
49- async function render ( ) {
50- const isLight =
51- document . documentElement . getAttribute ( "data-theme" ) === "light" ;
52- const fontFamily = getFontFamily ( ) ;
53-
54- // Custom theme variables for Cloudflare branding
55- const lightThemeVars = {
56- fontFamily,
57- primaryColor : "#fef1e6" , // cl1-orange-9 (very light orange for node backgrounds)
58- primaryBorderColor : "#f6821f" , // cl1-brand-orange
59- primaryTextColor : "#1d1d1d" , // cl1-gray-0
60- secondaryColor : "#f2f2f2" , // cl1-gray-9
61- secondaryBorderColor : "#999999" , // cl1-gray-6
62- secondaryTextColor : "#1d1d1d" , // cl1-gray-0
63- tertiaryColor : "#f2f2f2" , // cl1-gray-9
64- tertiaryBorderColor : "#999999" , // cl1-gray-6
65- tertiaryTextColor : "#1d1d1d" , // cl1-gray-0
66- lineColor : "#f6821f" , // cl1-brand-orange for arrows
67- textColor : "#1d1d1d" , // cl1-gray-0
68- mainBkg : "#fef1e6" , // cl1-orange-9
69- errorBkgColor : "#ffefee" , // cl1-red-9
70- errorTextColor : "#3c0501" , // cl1-red-0
71- edgeLabelBackground : "#ffffff" , // white background for edge labels in light mode
72- labelBackground : "#ffffff" , // white background for labels in light mode
73- } ;
74-
75- const darkThemeVars = {
76- fontFamily,
77- primaryColor : "#482303" , // cl1-orange-1 (dark orange for node backgrounds)
78- primaryBorderColor : "#f6821f" , // cl1-brand-orange
79- primaryTextColor : "#f2f2f2" , // cl1-gray-9
80- secondaryColor : "#313131" , // cl1-gray-1
81- secondaryBorderColor : "#797979" , // cl1-gray-5
82- secondaryTextColor : "#f2f2f2" , // cl1-gray-9
83- tertiaryColor : "#313131" , // cl1-gray-1
84- tertiaryBorderColor : "#797979" , // cl1-gray-5
85- tertiaryTextColor : "#f2f2f2" , // cl1-gray-9
86- lineColor : "#f6821f" , // cl1-brand-orange for arrows
87- textColor : "#f2f2f2" , // cl1-gray-9
88- mainBkg : "#482303" , // cl1-orange-1
89- background : "#1d1d1d" , // cl1-gray-0
90- errorBkgColor : "#3c0501" , // cl1-red-0
91- errorTextColor : "#ffefee" , // cl1-red-9
92- edgeLabelBackground : "#1d1d1d" , // dark background for edge labels
93- labelBackground : "#1d1d1d" , // dark background for labels
94- } ;
95-
96- const themeVariables = isLight ? lightThemeVars : darkThemeVars ;
97-
98- for ( const diagram of diagrams ) {
99- if ( ! init ) {
100- diagram . setAttribute ( "data-diagram" , diagram . textContent as string ) ;
95+ // Get computed font family from CSS variable
96+ function getFontFamily ( ) : string {
97+ const computedStyle = getComputedStyle ( document . documentElement ) ;
98+ const slFont = computedStyle . getPropertyValue ( "--__sl-font" ) . trim ( ) ;
99+ return slFont || "system-ui, -apple-system, sans-serif" ;
100+ }
101+
102+ // Read the actual page background color from CSS
103+ function getPageBackground ( ) : string {
104+ const style = getComputedStyle ( document . documentElement ) ;
105+ const bg = style . getPropertyValue ( "--sl-color-bg" ) . trim ( ) ;
106+ return (
107+ bg ||
108+ ( document . documentElement . getAttribute ( "data-theme" ) === "light"
109+ ? "#ffffff"
110+ : "#1d1d1d" )
111+ ) ;
112+ }
113+
114+ // Create wrapper container with annotation
115+ function wrapDiagram ( diagram : HTMLPreElement , title : string | null ) {
116+ // Skip if already wrapped
117+ if ( diagram . parentElement ?. classList . contains ( "mermaid-container" ) ) {
118+ return ;
119+ }
120+
121+ // Create container
122+ const container = document . createElement ( "div" ) ;
123+ container . className = "mermaid-container" ;
124+
125+ // Wrap the diagram
126+ diagram . parentNode ?. insertBefore ( container , diagram ) ;
127+ container . appendChild ( diagram ) ;
128+
129+ // Add expand button
130+ const expandBtn = document . createElement ( "button" ) ;
131+ expandBtn . className = "mermaid-expand" ;
132+ expandBtn . setAttribute ( "aria-label" , "Expand diagram" ) ;
133+ expandBtn . innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
134+ <polyline points="15 3 21 3 21 9"></polyline>
135+ <polyline points="9 21 3 21 3 15"></polyline>
136+ <line x1="21" y1="3" x2="14" y2="10"></line>
137+ <line x1="3" y1="21" x2="10" y2="14"></line>
138+ </svg>` ;
139+ expandBtn . addEventListener ( "click" , ( ) => openDiagram ( container ) ) ;
140+ container . appendChild ( expandBtn ) ;
141+
142+ // Add annotation footer if title exists
143+ if ( title ) {
144+ const footer = document . createElement ( "div" ) ;
145+ footer . className = "mermaid-annotation" ;
146+
147+ const titleSpan = document . createElement ( "span" ) ;
148+ titleSpan . className = "mermaid-annotation-title" ;
149+ titleSpan . textContent = title ;
150+
151+ const logo = document . createElement ( "img" ) ;
152+ logo . src = "/logo.svg" ;
153+ logo . alt = "Cloudflare" ;
154+ logo . className = "mermaid-annotation-logo" ;
155+
156+ footer . appendChild ( titleSpan ) ;
157+ footer . appendChild ( logo ) ;
158+ container . appendChild ( footer ) ;
101159 }
160+ }
161+
162+ async function render ( ) {
163+ // Dynamically import mermaid — the ~2.5 MB bundle is only fetched
164+ // on the ~2% of pages that actually contain diagrams.
165+ const { default : mermaid } = await import ( "mermaid" ) ;
166+
167+ const isLight =
168+ document . documentElement . getAttribute ( "data-theme" ) === "light" ;
169+ const fontFamily = getFontFamily ( ) ;
170+ const pageBg = getPageBackground ( ) ;
171+
172+ // Custom theme variables for Cloudflare branding
173+ const lightThemeVars = {
174+ fontFamily,
175+ primaryColor : "#fef1e6" , // cl1-orange-9 (very light orange for node backgrounds)
176+ primaryBorderColor : "#f6821f" , // cl1-brand-orange
177+ primaryTextColor : "#1d1d1d" , // cl1-gray-0
178+ secondaryColor : "#f2f2f2" , // cl1-gray-9
179+ secondaryBorderColor : "#999999" , // cl1-gray-6
180+ secondaryTextColor : "#1d1d1d" , // cl1-gray-0
181+ tertiaryColor : "#f2f2f2" , // cl1-gray-9
182+ tertiaryBorderColor : "#999999" , // cl1-gray-6
183+ tertiaryTextColor : "#1d1d1d" , // cl1-gray-0
184+ lineColor : "#f6821f" , // cl1-brand-orange for arrows
185+ textColor : "#1d1d1d" , // cl1-gray-0
186+ mainBkg : "#fef1e6" , // cl1-orange-9
187+ errorBkgColor : "#ffefee" , // cl1-red-9
188+ errorTextColor : "#3c0501" , // cl1-red-0
189+ edgeLabelBackground : pageBg , // match page background to occlude arrows
190+ labelBackground : pageBg ,
191+ } ;
102192
103- const def = diagram . getAttribute ( "data-diagram" ) as string ;
193+ const darkThemeVars = {
194+ fontFamily,
195+ primaryColor : "#482303" , // cl1-orange-1 (dark orange for node backgrounds)
196+ primaryBorderColor : "#f6821f" , // cl1-brand-orange
197+ primaryTextColor : "#f2f2f2" , // cl1-gray-9
198+ secondaryColor : "#313131" , // cl1-gray-1
199+ secondaryBorderColor : "#797979" , // cl1-gray-5
200+ secondaryTextColor : "#f2f2f2" , // cl1-gray-9
201+ tertiaryColor : "#313131" , // cl1-gray-1
202+ tertiaryBorderColor : "#797979" , // cl1-gray-5
203+ tertiaryTextColor : "#f2f2f2" , // cl1-gray-9
204+ lineColor : "#f6821f" , // cl1-brand-orange for arrows
205+ textColor : "#f2f2f2" , // cl1-gray-9
206+ mainBkg : "#482303" , // cl1-orange-1
207+ background : "#1d1d1d" , // cl1-gray-0
208+ errorBkgColor : "#3c0501" , // cl1-red-0
209+ errorTextColor : "#ffefee" , // cl1-red-9
210+ edgeLabelBackground : pageBg , // match page background to occlude arrows
211+ labelBackground : pageBg ,
212+ } ;
104213
105- // Initialize with base theme and custom variables
214+ const themeVariables = isLight ? lightThemeVars : darkThemeVars ;
215+
216+ // Initialize once before the loop — config is identical for all diagrams
106217 mermaid . initialize ( {
107218 startOnLoad : false ,
108219 theme : "base" ,
109220 themeVariables,
110221 flowchart : {
111222 htmlLabels : true ,
112223 useMaxWidth : true ,
224+ curve : "linear" ,
113225 } ,
114226 } ) ;
115227
116- await mermaid
117- . render ( `mermaid-${ crypto . randomUUID ( ) } ` , def )
118- . then ( ( { svg } ) => {
228+ for ( const diagram of diagrams ) {
229+ try {
230+ if ( ! init ) {
231+ diagram . setAttribute ( "data-diagram" , diagram . textContent as string ) ;
232+ }
233+
234+ const def = diagram . getAttribute ( "data-diagram" ) as string ;
235+
236+ const { svg } = await mermaid . render (
237+ `mermaid-${ crypto . randomUUID ( ) } ` ,
238+ def ,
239+ ) ;
119240 diagram . innerHTML = svg ;
120241
121242 // Extract title from SVG for annotation
@@ -125,19 +246,22 @@ async function render() {
125246
126247 // Wrap diagram with container and annotation
127248 wrapDiagram ( diagram , title ) ;
128- } ) ;
249+ } catch ( e ) {
250+ console . error ( "Mermaid render failed:" , e ) ;
251+ }
129252
130- diagram . setAttribute ( "data-processed" , "true" ) ;
131- }
253+ diagram . setAttribute ( "data-processed" , "true" ) ;
254+ }
132255
133- init = true ;
134- }
256+ init = true ;
257+ }
135258
136- const obs = new MutationObserver ( ( ) => render ( ) ) ;
259+ const obs = new MutationObserver ( ( ) => render ( ) ) ;
137260
138- obs . observe ( document . documentElement , {
139- attributes : true ,
140- attributeFilter : [ "data-theme" ] ,
141- } ) ;
261+ obs . observe ( document . documentElement , {
262+ attributes : true ,
263+ attributeFilter : [ "data-theme" ] ,
264+ } ) ;
142265
143- render ( ) ;
266+ render ( ) ;
267+ }
0 commit comments