jekyll-theme-zer0 1.28.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1143 -13
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +507 -2
  7. data/_data/consumers.yml +157 -9
  8. data/_data/features.yml +303 -20
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/i18n/fr.yml +12 -7
  11. data/_data/i18n/manifest.yml +39 -12
  12. data/_data/ingredient_densities.yml +122 -0
  13. data/_data/landing.yml +5 -2
  14. data/_data/navigation/main.yml +16 -0
  15. data/_data/navigation/quickstart.yml +4 -0
  16. data/_data/recipe_courses.yml +64 -0
  17. data/_data/site_builder.yml +874 -0
  18. data/_data/theme-manifest.yml +160 -124
  19. data/_data/ui-text.yml +26 -0
  20. data/_includes/README.md +26 -2
  21. data/_includes/analytics/posthog.html +2 -2
  22. data/_includes/components/admin-links.html +2 -2
  23. data/_includes/components/admin-tabs.html +2 -2
  24. data/_includes/components/ai-chat.html +14 -11
  25. data/_includes/components/analytics-dashboard.html +8 -8
  26. data/_includes/components/author-bio.html +1 -1
  27. data/_includes/components/author-card.html +10 -2
  28. data/_includes/components/author-eeat.html +4 -4
  29. data/_includes/components/background-customizer.html +10 -10
  30. data/_includes/components/background-image.html +114 -0
  31. data/_includes/components/background-settings.html +28 -15
  32. data/_includes/components/collection-manager.html +5 -5
  33. data/_includes/components/component-showcase.html +13 -13
  34. data/_includes/components/config-editor.html +12 -12
  35. data/_includes/components/config-viewer.html +8 -8
  36. data/_includes/components/cookie-consent.html +15 -15
  37. data/_includes/components/cta-button.html +7 -2
  38. data/_includes/components/dev-shortcuts.html +7 -7
  39. data/_includes/components/env-dashboard.html +8 -8
  40. data/_includes/components/env-switcher.html +9 -9
  41. data/_includes/components/feature-card.html +2 -2
  42. data/_includes/components/halfmoon.html +2 -2
  43. data/_includes/components/info-section.html +42 -37
  44. data/_includes/components/js-cdn.html +15 -15
  45. data/_includes/components/language-toggle.html +168 -21
  46. data/_includes/components/mermaid.html +72 -435
  47. data/_includes/components/nanobar.html +5 -5
  48. data/_includes/components/nav-editor.html +2 -2
  49. data/_includes/components/nav-export.html +2 -2
  50. data/_includes/components/nav-overview.html +2 -2
  51. data/_includes/components/page-feedback.html +45 -30
  52. data/_includes/components/page-views-init.html +55 -0
  53. data/_includes/components/page-views.html +33 -0
  54. data/_includes/components/post-card.html +22 -22
  55. data/_includes/components/post-type-badge.html +2 -2
  56. data/_includes/components/powered-by.html +2 -2
  57. data/_includes/components/preview-image.html +6 -0
  58. data/_includes/components/quick-index.html +2 -2
  59. data/_includes/components/recipe-card.html +67 -0
  60. data/_includes/components/recipe-duration.html +50 -0
  61. data/_includes/components/recipe-grams.html +58 -0
  62. data/_includes/components/recipe-index.html +96 -0
  63. data/_includes/components/recipe-ingredients.html +90 -0
  64. data/_includes/components/recipe-meta.html +96 -0
  65. data/_includes/components/recipe-nutrition.html +57 -0
  66. data/_includes/components/recipe-qty.html +73 -0
  67. data/_includes/components/recipe-ratio.html +151 -0
  68. data/_includes/components/recipe-scaler.html +73 -0
  69. data/_includes/components/recipe-steps.html +86 -0
  70. data/_includes/components/recipe-temp.html +45 -0
  71. data/_includes/components/search-modal.html +29 -4
  72. data/_includes/components/searchbar.html +2 -2
  73. data/_includes/components/shortcuts-modal.html +3 -0
  74. data/_includes/components/svg-background.html +2 -2
  75. data/_includes/components/theme-customizer.html +2 -2
  76. data/_includes/components/theme-info.html +14 -7
  77. data/_includes/components/theme-preview-gallery.html +22 -22
  78. data/_includes/content/giscus.html +2 -2
  79. data/_includes/content/intro.html +8 -8
  80. data/_includes/content/jsonld-faq.html +2 -2
  81. data/_includes/content/jsonld-software.html +24 -5
  82. data/_includes/content/seo.html +4 -4
  83. data/_includes/content/sitemap.html +27 -27
  84. data/_includes/content/toc.html +183 -183
  85. data/_includes/core/branding.html +6 -6
  86. data/_includes/core/console-capture.html +32 -74
  87. data/_includes/core/favicon.html +49 -7
  88. data/_includes/core/footer-fabs.html +17 -3
  89. data/_includes/core/footer.html +49 -34
  90. data/_includes/core/head.html +110 -86
  91. data/_includes/core/header.html +76 -54
  92. data/_includes/docs/bootstrap-docs.html +8 -8
  93. data/_includes/landing/landing-install-cards.html +2 -2
  94. data/_includes/landing/landing-quick-links.html +1 -1
  95. data/_includes/navigation/admin-nav.html +2 -2
  96. data/_includes/navigation/nav-tree.html +8 -8
  97. data/_includes/navigation/navbar.html +12 -12
  98. data/_includes/navigation/section-sidebar.html +109 -27
  99. data/_includes/navigation/sidebar-config.html +36 -2
  100. data/_includes/navigation/sidebar-left.html +17 -16
  101. data/_includes/navigation/sidebar-right.html +8 -7
  102. data/_includes/obsidian/full-graph.html +2 -2
  103. data/_includes/setup/claude-session.html +72 -0
  104. data/_includes/setup/prereq-checklist.html +90 -0
  105. data/_includes/setup/wizard.html +924 -222
  106. data/_includes/stats/stats-categories.html +8 -8
  107. data/_includes/stats/stats-header.html +14 -14
  108. data/_includes/stats/stats-metrics.html +14 -14
  109. data/_includes/stats/stats-no-data.html +12 -12
  110. data/_includes/stats/stats-overview.html +6 -6
  111. data/_includes/stats/stats-tags.html +8 -8
  112. data/_layouts/404.html +38 -24
  113. data/_layouts/README.md +2 -0
  114. data/_layouts/admin.html +24 -24
  115. data/_layouts/article.html +43 -33
  116. data/_layouts/author.html +20 -20
  117. data/_layouts/authors.html +2 -2
  118. data/_layouts/book-abc.html +12 -12
  119. data/_layouts/book-story.html +15 -15
  120. data/_layouts/book.html +12 -12
  121. data/_layouts/collection.html +33 -33
  122. data/_layouts/cookbook.html +88 -0
  123. data/_layouts/default.html +31 -28
  124. data/_layouts/home.html +23 -23
  125. data/_layouts/index.html +10 -10
  126. data/_layouts/landing.html +17 -17
  127. data/_layouts/news.html +44 -44
  128. data/_layouts/note.html +38 -38
  129. data/_layouts/notebook.html +34 -34
  130. data/_layouts/recipe.html +274 -0
  131. data/_layouts/root.html +92 -55
  132. data/_layouts/section.html +62 -33
  133. data/_layouts/setup.html +3 -3
  134. data/_layouts/sitemap-collection.html +49 -49
  135. data/_layouts/stats.html +40 -40
  136. data/_layouts/tag.html +12 -12
  137. data/_layouts/welcome.html +21 -21
  138. data/_sass/components/_callout.scss +1 -1
  139. data/_sass/components/_footer.scss +37 -1
  140. data/_sass/components/_mermaid.scss +375 -0
  141. data/_sass/components/_page-views.scss +36 -0
  142. data/_sass/components/_recipe.scss +506 -0
  143. data/_sass/components/_setup-wizard.scss +764 -0
  144. data/_sass/components/_ui-enhancements.scss +6 -6
  145. data/_sass/core/_navbar.scss +261 -46
  146. data/_sass/layouts/_landing.scss +2 -2
  147. data/_sass/layouts/_navbar-extras.scss +14 -4
  148. data/_sass/tokens/_color.scss +6 -0
  149. data/_sass/tokens/_index.scss +2 -0
  150. data/_sass/tokens/_radius.scss +21 -0
  151. data/_sass/tokens/_typography.scss +4 -0
  152. data/_sass/utilities/_focus.scss +14 -0
  153. data/assets/css/main.scss +4 -0
  154. data/assets/js/ai-chat.js +47 -5
  155. data/assets/js/fleet-feedback-capture.js +124 -0
  156. data/assets/js/fleet-feedback.js +853 -0
  157. data/assets/js/mermaid-diagrams.js +1267 -0
  158. data/assets/js/modules/navigation/config.js +9 -6
  159. data/assets/js/modules/navigation/navbar.js +55 -0
  160. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  161. data/assets/js/modules/theme/appearance.js +8 -2
  162. data/assets/js/obsidian-wiki-links.js +8 -3
  163. data/assets/js/page-feedback.js +125 -192
  164. data/assets/js/page-views.js +372 -0
  165. data/assets/js/recipe-scaler.js +501 -0
  166. data/assets/js/search-modal.js +36 -0
  167. data/assets/js/setup-wizard.js +2279 -226
  168. data/assets/js/site-builder.js +1834 -0
  169. data/assets/js/ui-enhancements.js +11 -3
  170. data/scripts/README.md +44 -0
  171. data/scripts/ai/README.md +38 -0
  172. data/scripts/ai/api_call.rb +124 -0
  173. data/scripts/ai/usage.rb +314 -0
  174. data/scripts/ai/usage_report.rb +225 -0
  175. data/scripts/bin/audit-consumer +39 -7
  176. data/scripts/bin/giscus-discussions +213 -14
  177. data/scripts/bin/manifest +35 -12
  178. data/scripts/ci/agent_review_result.py +164 -0
  179. data/scripts/ci/test_agent_review_result.py +172 -0
  180. data/scripts/ci/test_visual_evidence_autogen.py +341 -0
  181. data/scripts/ci/visual_evidence_autogen.py +1060 -0
  182. data/scripts/content-review.rb +20 -1
  183. data/scripts/design-system-check.rb +170 -0
  184. data/scripts/lib/audit.sh +42 -2
  185. data/scripts/lint-liquid-raw.rb +137 -0
  186. data/scripts/test/integration/mermaid +22 -8
  187. data/scripts/test/lib/run_tests.sh +3 -1
  188. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  189. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  190. data/scripts/translate.rb +94 -16
  191. metadata +48 -2
@@ -0,0 +1,1267 @@
1
+ // Feature: ZER0-013
2
+ /*
3
+ * ===================================================================
4
+ * mermaid-diagrams.js — Mermaid diagram figures (render + chrome)
5
+ * ===================================================================
6
+ *
7
+ * File: assets/js/mermaid-diagrams.js
8
+ * Purpose: Turn every ```mermaid fence (and legacy <div class="mermaid">)
9
+ * into an accessible <figure> with a rendered SVG, a toolbar
10
+ * (zoom, fullscreen, copy source, download SVG), a caption taken
11
+ * from the diagram's accTitle / title, and a graceful error state
12
+ * that keeps the diagram source on the page instead of losing it.
13
+ *
14
+ * Loaded by _includes/components/mermaid.html — only on pages that opt in
15
+ * with `mermaid: true` — after the vendored mermaid.min.js. Both scripts
16
+ * are `defer`, so this runs once the DOM is parsed but BEFORE
17
+ * DOMContentLoaded: fences are converted before code-copy.js decorates
18
+ * <pre> blocks, so a diagram never gets a copy button and a line gutter.
19
+ *
20
+ * Reads one JSON block injected by the include:
21
+ * #mermaidConfig — securityLevel, toolbar/fullscreen/download flags, labels
22
+ *
23
+ * Theming
24
+ * No colour is hard-coded. The palette is derived from the live design
25
+ * tokens (--zer0-color-*, --bs-*) every time a diagram is rendered, so
26
+ * diagrams follow the active colour mode (light / dark / wizard), the
27
+ * selected skin, and any `theme_color` or Appearance-panel override — and
28
+ * re-render when any of those change. "Dark mode" is decided by the page
29
+ * background's luminance, not by the name of an attribute value, so a
30
+ * dark skin on a light mode still gets legible ink.
31
+ *
32
+ * Re-rendering
33
+ * Mermaid marks a node `data-processed` and replaces its text with the
34
+ * SVG, so the source is gone after the first render. Every source is kept
35
+ * here (WeakMap keyed by figure) and each render goes through
36
+ * mermaid.render(), so a theme switch re-renders from the original text.
37
+ *
38
+ * Public API (also what the Playwright spec drives):
39
+ * window.zer0Mermaid.renderAll(root?) -> Promise<figure[]> convert + render fences under root
40
+ * window.zer0Mermaid.render(el, src?) -> Promise<figure> render one element (or replace it with src)
41
+ * window.zer0Mermaid.refresh() -> Promise<figure[]> re-derive the palette, re-render every figure
42
+ * window.zer0Mermaid.getSource(figure) -> string | null
43
+ * window.zer0Mermaid.palette() -> { dark, vars } the derived Mermaid themeVariables
44
+ * window.zer0Mermaid.figures() -> figure[]
45
+ * Events
46
+ * `zer0:diagram-rendered` on each figure (bubbles): { ok, type, error }
47
+ * `zer0:diagrams-ready` on document after a renderAll/refresh: { count, failed }
48
+ * ===================================================================
49
+ */
50
+ (function () {
51
+ "use strict";
52
+
53
+ if (typeof window === "undefined" || typeof document === "undefined") return;
54
+
55
+ // ---------------------------------------------------------------------
56
+ // Configuration
57
+ // ---------------------------------------------------------------------
58
+ function readJSON(id, fallback) {
59
+ var el = document.getElementById(id);
60
+ if (!el) return fallback;
61
+ try {
62
+ return JSON.parse(el.textContent);
63
+ } catch (err) {
64
+ return fallback;
65
+ }
66
+ }
67
+
68
+ var CONFIG = readJSON("mermaidConfig", null) || {};
69
+ var LABELS = Object.assign(
70
+ {
71
+ diagram: "Diagram",
72
+ tools: "Diagram tools",
73
+ zoomIn: "Zoom in",
74
+ zoomOut: "Zoom out",
75
+ zoomReset: "Reset zoom",
76
+ fullscreen: "View fullscreen",
77
+ close: "Close fullscreen view",
78
+ copy: "Copy diagram source",
79
+ copied: "Copied",
80
+ copyFailed: "Copy failed",
81
+ download: "Download as SVG",
82
+ errorTitle: "This diagram could not be rendered",
83
+ errorHint: "Check the syntax in the Mermaid Live Editor. The source is shown below so nothing is lost.",
84
+ showSource: "Diagram source",
85
+ loading: "Loading diagram",
86
+ },
87
+ CONFIG.labels || {}
88
+ );
89
+
90
+ var SHOW_TOOLBAR = CONFIG.toolbar !== false;
91
+ var ALLOW_FULLSCREEN = CONFIG.fullscreen !== false;
92
+ var ALLOW_DOWNLOAD = CONFIG.download !== false;
93
+ var SECURITY_LEVEL = CONFIG.securityLevel || "strict";
94
+ var SUPPORTS_DIALOG = typeof HTMLDialogElement !== "undefined" &&
95
+ typeof HTMLDialogElement.prototype.showModal === "function";
96
+
97
+ var ZOOM_MIN = 0.5;
98
+ var ZOOM_MAX = 4;
99
+ var ZOOM_STEP = 1.25;
100
+ var EPSILON = 0.001;
101
+
102
+ // ---------------------------------------------------------------------
103
+ // Diagram-type detection (for accessible names + the figure's data attr)
104
+ // ---------------------------------------------------------------------
105
+ var TYPES = [
106
+ [/^(graph|flowchart)\b/i, "flowchart", "Flowchart"],
107
+ [/^sequenceDiagram\b/i, "sequence", "Sequence diagram"],
108
+ [/^classDiagram\b/i, "class", "Class diagram"],
109
+ [/^stateDiagram\b/i, "state", "State diagram"],
110
+ [/^erDiagram\b/i, "er", "Entity relationship diagram"],
111
+ [/^gantt\b/i, "gantt", "Gantt chart"],
112
+ [/^pie\b/i, "pie", "Pie chart"],
113
+ [/^gitGraph\b/i, "git", "Git graph"],
114
+ [/^journey\b/i, "journey", "User journey"],
115
+ [/^mindmap\b/i, "mindmap", "Mind map"],
116
+ [/^timeline\b/i, "timeline", "Timeline"],
117
+ [/^quadrantChart\b/i, "quadrant", "Quadrant chart"],
118
+ [/^requirementDiagram\b/i, "requirement", "Requirement diagram"],
119
+ [/^C4(Context|Container|Component|Dynamic|Deployment)\b/i, "c4", "C4 diagram"],
120
+ [/^sankey(-beta)?\b/i, "sankey", "Sankey diagram"],
121
+ [/^xychart(-beta)?\b/i, "xychart", "XY chart"],
122
+ [/^block(-beta)?\b/i, "block", "Block diagram"],
123
+ [/^zenuml\b/i, "zenuml", "ZenUML sequence diagram"],
124
+ ];
125
+
126
+ function parseMeta(source) {
127
+ var text = String(source || "").replace(/\r\n?/g, "\n").trim();
128
+ var title = null;
129
+ var description = null;
130
+
131
+ // Optional YAML front matter (mermaid ≥ 10): `---\ntitle: X\n---`
132
+ var fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
133
+ if (fm) {
134
+ var t = fm[1].match(/^\s*title\s*:\s*(.+?)\s*$/m);
135
+ if (t) title = t[1].replace(/^["']|["']$/g, "");
136
+ }
137
+ // Accessibility directives win over the front-matter title.
138
+ var acc = text.match(/^\s*accTitle\s*:\s*(.+?)\s*$/m);
139
+ if (acc) title = acc[1];
140
+ var accDescr = text.match(/^\s*accDescr\s*:\s*(.+?)\s*$/m);
141
+ if (accDescr) description = accDescr[1];
142
+
143
+ var body = fm ? text.slice(fm[0].length) : text;
144
+ var lines = body.split("\n");
145
+ var first = "";
146
+ for (var i = 0; i < lines.length; i++) {
147
+ var line = lines[i].trim();
148
+ if (line && line.indexOf("%%") !== 0) { first = line; break; }
149
+ }
150
+ var type = "unknown";
151
+ var typeLabel = LABELS.diagram;
152
+ for (var j = 0; j < TYPES.length; j++) {
153
+ if (TYPES[j][0].test(first)) { type = TYPES[j][1]; typeLabel = TYPES[j][2]; break; }
154
+ }
155
+ return { source: text, title: title, description: description, type: type, typeLabel: typeLabel };
156
+ }
157
+
158
+ // ---------------------------------------------------------------------
159
+ // Colour helpers — tokens → concrete hex the way the browser resolves them
160
+ // ---------------------------------------------------------------------
161
+ var probeEl = null;
162
+ var canvasCtx = null;
163
+
164
+ function clamp01(v) { return Math.max(0, Math.min(1, v)); }
165
+
166
+ function hexToRgb(hex) {
167
+ var m = String(hex).trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
168
+ if (!m) return null;
169
+ var h = m[1];
170
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
171
+ return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
172
+ }
173
+
174
+ function rgbToHex(c) {
175
+ var to = function (v) { return ("0" + Math.round(Math.max(0, Math.min(255, v))).toString(16)).slice(-2); };
176
+ return "#" + to(c.r) + to(c.g) + to(c.b);
177
+ }
178
+
179
+ // Parse `rgb()` / `rgba()` with either comma or space syntax.
180
+ function parseRgbString(str) {
181
+ var m = String(str).match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)/i);
182
+ if (!m) return null;
183
+ var a = 1;
184
+ if (m[4] !== undefined) a = m[4].slice(-1) === "%" ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
185
+ return { r: +m[1], g: +m[2], b: +m[3], a: isNaN(a) ? 1 : a };
186
+ }
187
+
188
+ // Normalise any CSS colour string (hex, rgb, hsl, color-mix() output,
189
+ // color(srgb …)) to {r,g,b,a}. A 2D canvas is the one API every browser
190
+ // ships that turns any parsable colour into a canonical serialisation.
191
+ function toRgba(color) {
192
+ var direct = parseRgbString(color);
193
+ if (direct) return direct;
194
+ var hex = hexToRgb(color);
195
+ if (hex) { hex.a = 1; return hex; }
196
+ try {
197
+ if (!canvasCtx) canvasCtx = document.createElement("canvas").getContext("2d");
198
+ if (canvasCtx) {
199
+ canvasCtx.fillStyle = "#000000";
200
+ canvasCtx.fillStyle = color;
201
+ var out = canvasCtx.fillStyle;
202
+ var h2 = hexToRgb(out);
203
+ if (h2) { h2.a = 1; return h2; }
204
+ var p2 = parseRgbString(out);
205
+ if (p2) return p2;
206
+ }
207
+ } catch (err) { /* fall through */ }
208
+ return null;
209
+ }
210
+
211
+ // Composite a translucent colour over an opaque background.
212
+ function over(c, bg) {
213
+ if (!c) return null;
214
+ var a = c.a === undefined ? 1 : c.a;
215
+ if (a >= 1 || !bg) return { r: c.r, g: c.g, b: c.b };
216
+ return { r: c.r * a + bg.r * (1 - a), g: c.g * a + bg.g * (1 - a), b: c.b * a + bg.b * (1 - a) };
217
+ }
218
+
219
+ // Resolve `var(--token, fallback)` exactly as the cascade sees it right now.
220
+ function tokenColor(token, fallback, bgRgb) {
221
+ if (!probeEl) {
222
+ probeEl = document.createElement("span");
223
+ probeEl.setAttribute("aria-hidden", "true");
224
+ probeEl.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;visibility:hidden";
225
+ (document.body || document.documentElement).appendChild(probeEl);
226
+ }
227
+ probeEl.style.color = "";
228
+ probeEl.style.color = "var(" + token + ", " + fallback + ")";
229
+ var resolved = null;
230
+ try { resolved = toRgba(getComputedStyle(probeEl).color); } catch (err) { /* ignore */ }
231
+ if (!resolved) resolved = toRgba(fallback);
232
+ return rgbToHex(over(resolved, bgRgb));
233
+ }
234
+
235
+ function mix(hexA, hexB, weightA) {
236
+ var a = hexToRgb(hexA), b = hexToRgb(hexB);
237
+ if (!a || !b) return hexA;
238
+ var w = clamp01(weightA);
239
+ return rgbToHex({ r: a.r * w + b.r * (1 - w), g: a.g * w + b.g * (1 - w), b: a.b * w + b.b * (1 - w) });
240
+ }
241
+
242
+ function luminance(hex) {
243
+ var c = hexToRgb(hex);
244
+ if (!c) return 1;
245
+ var lin = function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
246
+ return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);
247
+ }
248
+
249
+ function rgbToHsl(c) {
250
+ var r = c.r / 255, g = c.g / 255, b = c.b / 255;
251
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
252
+ var h = 0, s = 0, l = (max + min) / 2;
253
+ if (max !== min) {
254
+ var d = max - min;
255
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
256
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
257
+ else if (max === g) h = (b - r) / d + 2;
258
+ else h = (r - g) / d + 4;
259
+ h *= 60;
260
+ }
261
+ return { h: h, s: s, l: l };
262
+ }
263
+
264
+ function hslToHex(h, s, l) {
265
+ h = ((h % 360) + 360) % 360;
266
+ var c = (1 - Math.abs(2 * l - 1)) * s;
267
+ var x = c * (1 - Math.abs(((h / 60) % 2) - 1));
268
+ var m = l - c / 2;
269
+ var r = 0, g = 0, b = 0;
270
+ if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; }
271
+ else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; }
272
+ return rgbToHex({ r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 });
273
+ }
274
+
275
+ // Text colour that reads on a given fill: dark ink above the luminance
276
+ // where black and white contrast equally (~0.18), white below it. A light
277
+ // brand such as the "air" skin's #6fa8dc (L≈0.37) needs dark text — white
278
+ // on it is only 2.5:1.
279
+ function inkOn(fillHex, ink) {
280
+ return luminance(fillHex) > 0.18 ? (luminance(ink) < 0.4 ? ink : "#1b1f22") : "#ffffff";
281
+ }
282
+
283
+ function contrastRatio(a, b) {
284
+ var la = luminance(a), lb = luminance(b);
285
+ var hi = Math.max(la, lb), lo = Math.min(la, lb);
286
+ return (hi + 0.05) / (lo + 0.05);
287
+ }
288
+
289
+ // Nudge `hex` toward `towards` until it reaches `min` contrast against `bg`.
290
+ // Guards palettes where the brand colour is (nearly) the page background —
291
+ // wizard mode paints the body in the brand blue, so blue nodes on a blue
292
+ // page would vanish.
293
+ function ensureContrast(hex, bg, min, towards) {
294
+ var out = hex;
295
+ for (var i = 0; i < 12 && contrastRatio(out, bg) < min; i++) out = mix(towards, out, 0.15);
296
+ return out;
297
+ }
298
+
299
+ // A 12-colour categorical series that starts from the brand hue and fans
300
+ // out around the wheel at a saturation/lightness that stays legible on the
301
+ // current background (pie slices, git branches, mindmap branches, x-y plots).
302
+ function buildSeries(primary, accent, dark) {
303
+ var base = rgbToHsl(hexToRgb(primary));
304
+ var acc = rgbToHsl(hexToRgb(accent));
305
+ var L = dark ? 0.62 : 0.44;
306
+ var S = Math.max(0.5, Math.min(0.8, base.s || 0.6));
307
+ var offsets = [0, 150, 60, 210, 300, 30, 180, 270, 90, 240, 330, 120];
308
+ var out = [];
309
+ for (var i = 0; i < offsets.length; i++) out.push(hslToHex(base.h + offsets[i], S, L));
310
+ // The accent hue is series #2 so brand pairs stay together.
311
+ if (acc.s > 0.2) out[1] = hslToHex(acc.h, Math.max(S, Math.min(0.85, acc.s)), L);
312
+ return out;
313
+ }
314
+
315
+ // ---------------------------------------------------------------------
316
+ // Palette → Mermaid `base` theme variables
317
+ // ---------------------------------------------------------------------
318
+ function palette() {
319
+ var bgHex = tokenColor("--bs-body-bg", "#ffffff", null);
320
+ var bgRgb = hexToRgb(bgHex);
321
+ var dark = luminance(bgHex) < 0.35;
322
+
323
+ var ink = tokenColor("--bs-body-color", dark ? "#dee2e6" : "#212529", bgRgb);
324
+ var inkMuted = tokenColor("--bs-secondary-color", dark ? "#adb5bd" : "#6c757d", bgRgb);
325
+ // The skin mixin writes both --bs-primary and --zer0-color-primary, but a
326
+ // site-level `theme_color.main` re-pins the latter at :root after main.css,
327
+ // so only --bs-primary reliably follows the active skin — it is what the
328
+ // buttons and links the reader sees are painted with.
329
+ var primary = tokenColor("--bs-primary", "var(--zer0-color-primary, #007bff)", bgRgb);
330
+ var accent = tokenColor("--zer0-color-accent", "#ffe484", bgRgb);
331
+ // Keep the brand usable as a border/line colour on this background.
332
+ primary = ensureContrast(primary, bgHex, 2.2, ink);
333
+ accent = ensureContrast(accent, bgHex, 1.6, ink);
334
+ var surface = tokenColor("--zer0-color-bg-elevated", dark ? "#2b3035" : "#f8f9fa", bgRgb);
335
+ var surface2 = tokenColor("--zer0-color-bg-muted", dark ? "#343a40" : "#e9ecef", bgRgb);
336
+ var border = tokenColor("--zer0-color-border", dark ? "#495057" : "#dee2e6", bgRgb);
337
+ var success = tokenColor("--zer0-color-success", "#198754", bgRgb);
338
+ var info = tokenColor("--zer0-color-info", "#0dcaf0", bgRgb);
339
+ var warning = tokenColor("--zer0-color-warning", "#ffc107", bgRgb);
340
+ var danger = tokenColor("--zer0-color-danger", "#dc3545", bgRgb);
341
+ var link = tokenColor("--zer0-color-link", primary, bgRgb);
342
+
343
+ var fontFamily = "";
344
+ try { fontFamily = getComputedStyle(document.body).fontFamily; } catch (err) { /* ignore */ }
345
+ if (!fontFamily) fontFamily = "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif";
346
+
347
+ // Tints: a colour laid over the page background. Dark mode needs less
348
+ // colour because the ink is light. A pale brand (a light-blue skin) needs
349
+ // MORE of itself before the tint reads as a surface, so the weight grows
350
+ // until the fill separates from the page — that keeps the hue instead of
351
+ // greying it. When the brand IS the page colour (wizard mode) no weight
352
+ // helps, so fall back to nudging toward the ink.
353
+ var tintTo = function (hex, minRatio) {
354
+ var w = dark ? 0.3 : 0.14;
355
+ var out = mix(hex, bgHex, w);
356
+ while (contrastRatio(out, bgHex) < minRatio && w < 0.6) {
357
+ w += 0.06;
358
+ out = mix(hex, bgHex, w);
359
+ }
360
+ return ensureContrast(out, bgHex, minRatio, ink);
361
+ };
362
+ var tint = function (hex) { return tintTo(hex, 1.12); };
363
+ var tintStrong = function (hex) { return ensureContrast(mix(hex, bgHex, dark ? 0.45 : 0.32), bgHex, 1.25, ink); };
364
+ var deepen = function (hex) { return mix(hex, dark ? "#ffffff" : "#000000", dark ? 0.85 : 0.8); };
365
+
366
+ var series = buildSeries(primary, accent, dark);
367
+ var accentDeep = deepen(accent);
368
+ var nodeFill = tintTo(primary, 1.15);
369
+ var nodeFill2 = tintTo(accent, 1.15);
370
+ var onPrimary = inkOn(primary, ink);
371
+ var edgeLabelBg = mix(surface, bgHex, 0.6);
372
+
373
+ var vars = {
374
+ darkMode: dark,
375
+ background: bgHex,
376
+ fontFamily: fontFamily,
377
+ fontSize: "14px",
378
+
379
+ // Core trio (most diagram types derive from these)
380
+ primaryColor: nodeFill,
381
+ primaryTextColor: ink,
382
+ primaryBorderColor: primary,
383
+ secondaryColor: nodeFill2,
384
+ secondaryTextColor: ink,
385
+ secondaryBorderColor: accentDeep,
386
+ tertiaryColor: surface,
387
+ tertiaryTextColor: ink,
388
+ tertiaryBorderColor: border,
389
+
390
+ lineColor: inkMuted,
391
+ textColor: ink,
392
+ mainBkg: nodeFill,
393
+ secondBkg: nodeFill2,
394
+ mainContrastColor: ink,
395
+ darkTextColor: ink,
396
+ border1: primary,
397
+ border2: border,
398
+ arrowheadColor: inkMuted,
399
+ labelBackground: edgeLabelBg,
400
+ labelColor: ink,
401
+ titleColor: ink,
402
+
403
+ // Flowchart
404
+ nodeBkg: nodeFill,
405
+ nodeBorder: primary,
406
+ nodeTextColor: ink,
407
+ clusterBkg: mix(surface, bgHex, 0.7),
408
+ clusterBorder: border,
409
+ defaultLinkColor: inkMuted,
410
+ edgeLabelBackground: edgeLabelBg,
411
+
412
+ // Notes
413
+ noteBkgColor: tintStrong(warning),
414
+ noteTextColor: ink,
415
+ noteBorderColor: deepen(warning),
416
+
417
+ // Sequence
418
+ actorBkg: nodeFill,
419
+ actorBorder: primary,
420
+ actorTextColor: ink,
421
+ actorLineColor: inkMuted,
422
+ signalColor: ink,
423
+ signalTextColor: ink,
424
+ labelBoxBkgColor: surface,
425
+ labelBoxBorderColor: border,
426
+ labelTextColor: ink,
427
+ loopTextColor: ink,
428
+ activationBkgColor: tintStrong(primary),
429
+ activationBorderColor: primary,
430
+ sequenceNumberColor: onPrimary,
431
+
432
+ // Gantt
433
+ sectionBkgColor: mix(primary, bgHex, dark ? 0.16 : 0.08),
434
+ altSectionBkgColor: bgHex,
435
+ sectionBkgColor2: mix(accent, bgHex, dark ? 0.16 : 0.12),
436
+ excludeBkgColor: surface2,
437
+ taskBorderColor: deepen(primary),
438
+ taskBkgColor: primary,
439
+ taskTextColor: onPrimary,
440
+ taskTextLightColor: onPrimary,
441
+ taskTextDarkColor: ink,
442
+ taskTextOutsideColor: ink,
443
+ taskTextClickableColor: link,
444
+ activeTaskBorderColor: accentDeep,
445
+ activeTaskBkgColor: tintStrong(accent),
446
+ gridColor: border,
447
+ doneTaskBkgColor: surface2,
448
+ doneTaskBorderColor: border,
449
+ critBorderColor: danger,
450
+ critBkgColor: tintStrong(danger),
451
+ todayLineColor: danger,
452
+
453
+ // Class / state / ER
454
+ classText: ink,
455
+ transitionColor: inkMuted,
456
+ transitionLabelColor: ink,
457
+ stateLabelColor: ink,
458
+ stateBkg: nodeFill,
459
+ labelBackgroundColor: bgHex,
460
+ compositeBackground: surface,
461
+ altBackground: surface2,
462
+ compositeTitleBackground: tintStrong(primary),
463
+ compositeBorder: border,
464
+ innerEndBackground: primary,
465
+ specialStateColor: ink,
466
+ attributeBackgroundColorOdd: bgHex,
467
+ attributeBackgroundColorEven: surface,
468
+
469
+ // Requirement / C4 / errors
470
+ requirementBackground: nodeFill,
471
+ requirementBorderColor: primary,
472
+ requirementTextColor: ink,
473
+ relationColor: inkMuted,
474
+ relationLabelBackground: bgHex,
475
+ relationLabelColor: ink,
476
+ personBorder: primary,
477
+ personBkg: nodeFill,
478
+ errorBkgColor: tintStrong(danger),
479
+ errorTextColor: ink,
480
+
481
+ // Pie
482
+ pieTitleTextColor: ink,
483
+ pieSectionTextColor: "#ffffff",
484
+ pieLegendTextColor: ink,
485
+ pieStrokeColor: bgHex,
486
+ pieOuterStrokeColor: border,
487
+ pieOpacity: 0.9,
488
+
489
+ // Git graph
490
+ commitLabelColor: ink,
491
+ commitLabelBackground: surface2,
492
+ tagLabelColor: ink,
493
+ tagLabelBackground: tintStrong(accent),
494
+ tagLabelBorder: accentDeep,
495
+
496
+ // Quadrant
497
+ quadrant1Fill: tint(series[0]),
498
+ quadrant2Fill: tint(series[1]),
499
+ quadrant3Fill: tint(series[2]),
500
+ quadrant4Fill: tint(series[3]),
501
+ quadrant1TextFill: ink,
502
+ quadrant2TextFill: ink,
503
+ quadrant3TextFill: ink,
504
+ quadrant4TextFill: ink,
505
+ quadrantPointFill: primary,
506
+ quadrantPointTextFill: ink,
507
+ quadrantXAxisTextFill: ink,
508
+ quadrantYAxisTextFill: ink,
509
+ quadrantInternalBorderStrokeFill: border,
510
+ quadrantExternalBorderStrokeFill: border,
511
+ quadrantTitleFill: ink,
512
+
513
+ // XY chart
514
+ xyChart: {
515
+ backgroundColor: bgHex,
516
+ titleColor: ink,
517
+ xAxisLabelColor: ink,
518
+ xAxisTitleColor: ink,
519
+ xAxisTickColor: inkMuted,
520
+ xAxisLineColor: border,
521
+ yAxisLabelColor: ink,
522
+ yAxisTitleColor: ink,
523
+ yAxisTickColor: inkMuted,
524
+ yAxisLineColor: border,
525
+ plotColorPalette: series.join(","),
526
+ },
527
+ };
528
+
529
+ // Indexed series: pie slices, git branches, journey/mindmap/timeline scales.
530
+ for (var i = 0; i < 12; i++) {
531
+ vars["pie" + (i + 1)] = series[i];
532
+ vars["cScale" + i] = tintStrong(series[i]);
533
+ vars["cScalePeer" + i] = series[i];
534
+ vars["cScaleLabel" + i] = ink;
535
+ vars["cScaleInv" + i] = series[i];
536
+ if (i < 8) {
537
+ vars["git" + i] = series[i];
538
+ vars["gitInv" + i] = inkOn(series[i], ink);
539
+ vars["gitBranchLabel" + i] = inkOn(series[i], ink);
540
+ vars["fillType" + i] = tintStrong(series[i]);
541
+ }
542
+ }
543
+
544
+ return { dark: dark, vars: vars, fontFamily: fontFamily, background: bgHex };
545
+ }
546
+
547
+ function buildMermaidConfig(p) {
548
+ return {
549
+ startOnLoad: false,
550
+ securityLevel: SECURITY_LEVEL,
551
+ theme: "base",
552
+ themeVariables: p.vars,
553
+ fontFamily: p.fontFamily,
554
+ // Parse/render failures are surfaced in the figure itself (see
555
+ // showError); Mermaid's own console logging would only duplicate them.
556
+ logLevel: "fatal",
557
+ flowchart: {
558
+ useMaxWidth: true,
559
+ // SVG labels avoid foreignObject clipping/position bugs with theme CSS
560
+ htmlLabels: false,
561
+ curve: "basis",
562
+ padding: 12,
563
+ nodeSpacing: 40,
564
+ rankSpacing: 48,
565
+ },
566
+ sequence: {
567
+ useMaxWidth: true,
568
+ mirrorActors: true,
569
+ diagramMarginX: 24,
570
+ diagramMarginY: 12,
571
+ actorMargin: 48,
572
+ messageMargin: 32,
573
+ boxMargin: 10,
574
+ noteMargin: 10,
575
+ },
576
+ gantt: {
577
+ useMaxWidth: true,
578
+ titleTopMargin: 24,
579
+ barHeight: 22,
580
+ barGap: 6,
581
+ topPadding: 48,
582
+ leftPadding: 96,
583
+ gridLineStartPadding: 36,
584
+ fontSize: 12,
585
+ numberSectionStyles: 4,
586
+ axisFormat: "%Y-%m-%d",
587
+ },
588
+ class: { useMaxWidth: true },
589
+ state: { useMaxWidth: true },
590
+ er: { useMaxWidth: true },
591
+ pie: { useMaxWidth: true },
592
+ journey: { useMaxWidth: true },
593
+ gitGraph: { useMaxWidth: true },
594
+ mindmap: { useMaxWidth: true },
595
+ timeline: { useMaxWidth: true },
596
+ requirement: { useMaxWidth: true },
597
+ quadrantChart: { useMaxWidth: true },
598
+ xyChart: { useMaxWidth: true },
599
+ };
600
+ }
601
+
602
+ var lastPaletteKey = null;
603
+ var lastPalette = null;
604
+
605
+ function initMermaid(force) {
606
+ var p = palette();
607
+ var key = JSON.stringify(p.vars) + "|" + p.fontFamily;
608
+ if (!force && key === lastPaletteKey) return false;
609
+ lastPaletteKey = key;
610
+ lastPalette = p;
611
+ window.mermaid.initialize(buildMermaidConfig(p));
612
+ return true;
613
+ }
614
+
615
+ // ---------------------------------------------------------------------
616
+ // DOM helpers
617
+ // ---------------------------------------------------------------------
618
+ function el(tag, className, attrs) {
619
+ var node = document.createElement(tag);
620
+ if (className) node.className = className;
621
+ if (attrs) Object.keys(attrs).forEach(function (k) { node.setAttribute(k, attrs[k]); });
622
+ return node;
623
+ }
624
+
625
+ function icon(name) {
626
+ var i = el("i", "bi " + name, { "aria-hidden": "true" });
627
+ return i;
628
+ }
629
+
630
+ function button(iconName, label, onClick) {
631
+ var b = el("button", "zer0-diagram__btn", { type: "button", "aria-label": label, title: label });
632
+ b.appendChild(icon(iconName));
633
+ b.addEventListener("click", function (e) {
634
+ e.preventDefault();
635
+ e.stopPropagation();
636
+ onClick(b, e);
637
+ });
638
+ return b;
639
+ }
640
+
641
+ function fire(target, name, detail) {
642
+ try {
643
+ target.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: detail || {} }));
644
+ } catch (err) { /* ignore */ }
645
+ }
646
+
647
+ function slugify(text) {
648
+ return String(text || "diagram").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "diagram";
649
+ }
650
+
651
+ // ---------------------------------------------------------------------
652
+ // Registry
653
+ // ---------------------------------------------------------------------
654
+ var registry = new WeakMap(); // figure -> { meta, scope }
655
+ var allFigures = [];
656
+ var counter = 0;
657
+
658
+ function nextId() {
659
+ counter += 1;
660
+ return "zer0-diagram-" + counter + "-" + Date.now().toString(36);
661
+ }
662
+
663
+ // ---------------------------------------------------------------------
664
+ // Zoom — implemented by sizing the SVG itself (real layout, so the
665
+ // scrollable viewport, centring and print all keep working; no transforms).
666
+ // ---------------------------------------------------------------------
667
+ function currentZoom(canvas) {
668
+ var z = parseFloat(canvas.getAttribute("data-zoom"));
669
+ return isNaN(z) ? 1 : z;
670
+ }
671
+
672
+ function fitWidth(svg) {
673
+ // Width the SVG occupies at zoom 1 (mermaid's max-width or the viewport).
674
+ var rect = svg.getBoundingClientRect();
675
+ return rect.width || parseFloat(svg.style.maxWidth) || (svg.viewBox && svg.viewBox.baseVal.width) || 0;
676
+ }
677
+
678
+ function setZoom(scope, zoom) {
679
+ var canvas = scope.canvas;
680
+ var svg = canvas.querySelector("svg");
681
+ zoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoom));
682
+ var isOne = Math.abs(zoom - 1) < EPSILON;
683
+
684
+ if (svg) {
685
+ if (isOne) {
686
+ svg.style.width = "";
687
+ svg.style.height = "";
688
+ if (svg.getAttribute("data-zer0-max-width")) svg.style.maxWidth = svg.getAttribute("data-zer0-max-width");
689
+ canvas.removeAttribute("data-zoom");
690
+ // Measure the fitted size only once we are back at 1:1.
691
+ svg.setAttribute("data-zer0-fit-width", String(fitWidth(svg)));
692
+ var h = svg.getBoundingClientRect().height;
693
+ if (h) svg.setAttribute("data-zer0-fit-height", String(h));
694
+ } else {
695
+ var base = parseFloat(svg.getAttribute("data-zer0-fit-width"));
696
+ if (!base) {
697
+ // First zoom without a 1:1 measurement: reset, measure, continue.
698
+ setZoom(scope, 1);
699
+ base = parseFloat(svg.getAttribute("data-zer0-fit-width")) || fitWidth(svg);
700
+ }
701
+ var baseH = parseFloat(svg.getAttribute("data-zer0-fit-height"));
702
+ svg.style.maxWidth = "none";
703
+ svg.style.width = Math.round(base * zoom) + "px";
704
+ if (svg.hasAttribute("height") && baseH) svg.style.height = Math.round(baseH * zoom) + "px";
705
+ canvas.setAttribute("data-zoom", zoom.toFixed(3));
706
+ }
707
+ }
708
+
709
+ scope.viewport.classList.toggle("is-zoomed", !isOne);
710
+ if (scope.level) {
711
+ scope.level.textContent = Math.round(zoom * 100) + "%";
712
+ scope.level.hidden = isOne;
713
+ }
714
+ if (scope.buttons) {
715
+ if (scope.buttons.zoomIn) scope.buttons.zoomIn.disabled = zoom >= ZOOM_MAX - EPSILON;
716
+ if (scope.buttons.zoomOut) scope.buttons.zoomOut.disabled = zoom <= ZOOM_MIN + EPSILON;
717
+ if (scope.buttons.reset) scope.buttons.reset.disabled = isOne;
718
+ }
719
+ return zoom;
720
+ }
721
+
722
+ function zoomBy(scope, factor) {
723
+ return setZoom(scope, currentZoom(scope.canvas) * factor);
724
+ }
725
+
726
+ // Drag-to-pan for mouse users once the diagram overflows its viewport.
727
+ function enablePan(viewport) {
728
+ var drag = null;
729
+ viewport.addEventListener("pointerdown", function (e) {
730
+ if (e.pointerType !== "mouse" || e.button !== 0) return;
731
+ if (e.target.closest("a, button")) return;
732
+ var canScroll = viewport.scrollWidth > viewport.clientWidth + 1 || viewport.scrollHeight > viewport.clientHeight + 1;
733
+ if (!canScroll) return;
734
+ drag = { x: e.clientX, y: e.clientY, left: viewport.scrollLeft, top: viewport.scrollTop, moved: false };
735
+ viewport.classList.add("is-panning");
736
+ try { viewport.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
737
+ });
738
+ viewport.addEventListener("pointermove", function (e) {
739
+ if (!drag) return;
740
+ var dx = e.clientX - drag.x, dy = e.clientY - drag.y;
741
+ if (Math.abs(dx) + Math.abs(dy) > 2) drag.moved = true;
742
+ viewport.scrollLeft = drag.left - dx;
743
+ viewport.scrollTop = drag.top - dy;
744
+ });
745
+ var end = function (e) {
746
+ if (!drag) return;
747
+ drag = null;
748
+ viewport.classList.remove("is-panning");
749
+ try { viewport.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
750
+ };
751
+ viewport.addEventListener("pointerup", end);
752
+ viewport.addEventListener("pointercancel", end);
753
+ }
754
+
755
+ function enableWheelZoom(viewport, scope) {
756
+ viewport.addEventListener("wheel", function (e) {
757
+ if (!(e.ctrlKey || e.metaKey)) return; // plain wheel keeps scrolling the page
758
+ e.preventDefault();
759
+ zoomBy(scope, e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP);
760
+ }, { passive: false });
761
+ }
762
+
763
+ function enableKeys(viewport, scope) {
764
+ viewport.addEventListener("keydown", function (e) {
765
+ if (e.altKey || e.ctrlKey || e.metaKey) return;
766
+ var handled = true;
767
+ switch (e.key) {
768
+ case "+": case "=": zoomBy(scope, ZOOM_STEP); break;
769
+ case "-": case "_": zoomBy(scope, 1 / ZOOM_STEP); break;
770
+ case "0": setZoom(scope, 1); break;
771
+ case "f": case "F":
772
+ if (scope.buttons && scope.buttons.fullscreen && !scope.buttons.fullscreen.disabled) scope.buttons.fullscreen.click();
773
+ else handled = false;
774
+ break;
775
+ default: handled = false;
776
+ }
777
+ if (handled) e.preventDefault();
778
+ });
779
+ }
780
+
781
+ // ---------------------------------------------------------------------
782
+ // Toolbar actions
783
+ // ---------------------------------------------------------------------
784
+ function announce(scope, text) {
785
+ if (!scope.status) return;
786
+ scope.status.textContent = "";
787
+ // Re-insert on the next tick so identical messages are announced again.
788
+ setTimeout(function () { scope.status.textContent = text; }, 30);
789
+ }
790
+
791
+ function copySource(btn, scope) {
792
+ var entry = registry.get(scope.figure);
793
+ var text = entry ? entry.meta.source : "";
794
+ var done = function (ok) {
795
+ var i = btn.querySelector("i");
796
+ if (i) i.className = "bi " + (ok ? "bi-check-lg" : "bi-x-lg");
797
+ btn.classList.toggle("is-success", ok);
798
+ btn.classList.toggle("is-failure", !ok);
799
+ announce(scope, ok ? LABELS.copied : LABELS.copyFailed);
800
+ setTimeout(function () {
801
+ if (i) i.className = "bi bi-clipboard";
802
+ btn.classList.remove("is-success", "is-failure");
803
+ }, 2000);
804
+ };
805
+ if (!navigator.clipboard || !navigator.clipboard.writeText) { done(false); return; }
806
+ navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); });
807
+ }
808
+
809
+ function downloadSvg(scope) {
810
+ var svg = scope.canvas.querySelector("svg");
811
+ var entry = registry.get(scope.figure);
812
+ if (!svg || !entry) return;
813
+ var clone = svg.cloneNode(true);
814
+ var vb = svg.viewBox && svg.viewBox.baseVal;
815
+ if (vb && vb.width && vb.height) {
816
+ clone.setAttribute("width", String(Math.round(vb.width)));
817
+ clone.setAttribute("height", String(Math.round(vb.height)));
818
+ }
819
+ clone.removeAttribute("style");
820
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
821
+ clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
822
+ // Diagrams render on a transparent canvas; bake the page surface in so a
823
+ // dark-mode export is not white-on-nothing when opened elsewhere.
824
+ if (lastPalette) clone.style.backgroundColor = lastPalette.background;
825
+ var xml = new XMLSerializer().serializeToString(clone);
826
+ var blob = new Blob(['<?xml version="1.0" encoding="UTF-8"?>\n' + xml], { type: "image/svg+xml;charset=utf-8" });
827
+ var url = URL.createObjectURL(blob);
828
+ var a = el("a", null, { href: url, download: slugify(entry.meta.title || entry.meta.typeLabel) + ".svg" });
829
+ a.style.display = "none";
830
+ document.body.appendChild(a);
831
+ a.click();
832
+ setTimeout(function () { a.remove(); URL.revokeObjectURL(url); }, 1000);
833
+ }
834
+
835
+ // Build a toolbar bound to a mutable scope ({ figure, canvas, viewport }).
836
+ // The lightbox reuses one toolbar for whichever figure it is showing.
837
+ function buildToolbar(scope, opts) {
838
+ var bar = el("div", "zer0-diagram__toolbar", { role: "toolbar", "aria-label": LABELS.tools });
839
+ var buttons = {};
840
+
841
+ buttons.zoomOut = button("bi-zoom-out", LABELS.zoomOut, function () { zoomBy(scope, 1 / ZOOM_STEP); });
842
+ var level = el("span", "zer0-diagram__zoom", { "aria-live": "polite", "aria-atomic": "true" });
843
+ level.hidden = true;
844
+ buttons.zoomIn = button("bi-zoom-in", LABELS.zoomIn, function () { zoomBy(scope, ZOOM_STEP); });
845
+ buttons.reset = button("bi-arrow-counterclockwise", LABELS.zoomReset, function () { setZoom(scope, 1); });
846
+ buttons.reset.disabled = true;
847
+
848
+ bar.appendChild(buttons.zoomOut);
849
+ bar.appendChild(level);
850
+ bar.appendChild(buttons.zoomIn);
851
+ bar.appendChild(buttons.reset);
852
+ bar.appendChild(el("span", "zer0-diagram__sep", { "aria-hidden": "true" }));
853
+
854
+ buttons.copy = button("bi-clipboard", LABELS.copy, function (b) { copySource(b, scope); });
855
+ bar.appendChild(buttons.copy);
856
+
857
+ if (ALLOW_DOWNLOAD) {
858
+ buttons.download = button("bi-download", LABELS.download, function () { downloadSvg(scope); });
859
+ bar.appendChild(buttons.download);
860
+ }
861
+
862
+ if (opts && opts.inLightbox) {
863
+ buttons.close = button("bi-x-lg", LABELS.close, function () { closeLightbox(); });
864
+ buttons.close.classList.add("zer0-diagram__btn--close");
865
+ bar.appendChild(buttons.close);
866
+ } else if (ALLOW_FULLSCREEN && SUPPORTS_DIALOG) {
867
+ buttons.fullscreen = button("bi-arrows-fullscreen", LABELS.fullscreen, function () { openLightbox(scope.figure); });
868
+ bar.appendChild(buttons.fullscreen);
869
+ }
870
+
871
+ var status = el("span", "visually-hidden", { "aria-live": "polite", "aria-atomic": "true" });
872
+ bar.appendChild(status);
873
+
874
+ scope.level = level;
875
+ scope.buttons = buttons;
876
+ scope.status = status;
877
+ return bar;
878
+ }
879
+
880
+ // state: "loading" | "rendered" | "error". Only a rendered SVG can be
881
+ // zoomed, exported or shown fullscreen; copying the source always works.
882
+ function setToolbarState(scope, state) {
883
+ var b = scope.buttons;
884
+ if (!b) return;
885
+ var rendered = state === "rendered";
886
+ ["zoomIn", "zoomOut", "download", "fullscreen"].forEach(function (k) {
887
+ if (b[k]) b[k].disabled = !rendered;
888
+ });
889
+ if (b.reset) b.reset.disabled = true;
890
+ }
891
+
892
+ // ---------------------------------------------------------------------
893
+ // Figure construction
894
+ // ---------------------------------------------------------------------
895
+ function makeFigure(meta) {
896
+ var figure = el("figure", "zer0-diagram is-loading", { "data-zer0-diagram": meta.type });
897
+ figure.setAttribute("aria-busy", "true");
898
+
899
+ var viewport = el("div", "zer0-diagram__viewport", { tabindex: "0", role: "region" });
900
+ viewport.setAttribute("aria-label", meta.title || meta.typeLabel);
901
+ var canvas = el("div", "zer0-diagram__canvas");
902
+ var skeleton = el("div", "zer0-diagram__skeleton skeleton", { "aria-hidden": "true" });
903
+ canvas.appendChild(skeleton);
904
+ var loading = el("span", "visually-hidden");
905
+ loading.textContent = LABELS.loading;
906
+ canvas.appendChild(loading);
907
+ viewport.appendChild(canvas);
908
+
909
+ var scope = { figure: figure, canvas: canvas, viewport: viewport };
910
+ if (SHOW_TOOLBAR) {
911
+ figure.appendChild(buildToolbar(scope));
912
+ enableWheelZoom(viewport, scope);
913
+ enableKeys(viewport, scope);
914
+ }
915
+ enablePan(viewport);
916
+ figure.appendChild(viewport);
917
+
918
+ var caption = el("figcaption", "zer0-diagram__caption");
919
+ if (meta.title) caption.textContent = meta.title; else caption.hidden = true;
920
+ figure.appendChild(caption);
921
+
922
+ registry.set(figure, { meta: meta, scope: scope });
923
+ allFigures.push(figure);
924
+ return figure;
925
+ }
926
+
927
+ // The element to swap out for a fence: the outermost wrapper Jekyll (or
928
+ // code-copy.js, if it somehow ran first) put around the <pre>.
929
+ function fenceWrapper(pre) {
930
+ return pre.closest(".highlighter-rouge") || pre.closest(".code-block-body") || pre;
931
+ }
932
+
933
+ function collect(root) {
934
+ root = root || document;
935
+ var found = [];
936
+
937
+ var codes = root.querySelectorAll("pre > code.language-mermaid");
938
+ Array.prototype.forEach.call(codes, function (code) {
939
+ var pre = code.parentNode;
940
+ if (!pre || !pre.isConnected) return;
941
+ var figure = makeFigure(parseMeta(code.textContent));
942
+ fenceWrapper(pre).replaceWith(figure);
943
+ found.push(figure);
944
+ });
945
+
946
+ var pres = root.querySelectorAll('pre[data-language="mermaid"]');
947
+ Array.prototype.forEach.call(pres, function (pre) {
948
+ if (!pre.isConnected) return;
949
+ var figure = makeFigure(parseMeta(pre.textContent));
950
+ fenceWrapper(pre).replaceWith(figure);
951
+ found.push(figure);
952
+ });
953
+
954
+ var divs = root.querySelectorAll("div.mermaid:not([data-processed])");
955
+ Array.prototype.forEach.call(divs, function (div) {
956
+ if (!div.isConnected || div.closest(".zer0-diagram")) return;
957
+ var figure = makeFigure(parseMeta(div.textContent));
958
+ div.replaceWith(figure);
959
+ found.push(figure);
960
+ });
961
+
962
+ return found;
963
+ }
964
+
965
+ // ---------------------------------------------------------------------
966
+ // Rendering
967
+ // ---------------------------------------------------------------------
968
+ function decorateSvg(svg, meta) {
969
+ svg.classList.add("zer0-diagram__svg");
970
+ if (!svg.hasAttribute("aria-labelledby") && !svg.hasAttribute("aria-label")) {
971
+ svg.setAttribute("aria-label", meta.title || meta.typeLabel);
972
+ }
973
+ if (!svg.hasAttribute("role")) svg.setAttribute("role", "img");
974
+ if (svg.style.maxWidth) svg.setAttribute("data-zer0-max-width", svg.style.maxWidth);
975
+ svg.removeAttribute("data-zer0-fit-width");
976
+ svg.removeAttribute("data-zer0-fit-height");
977
+ }
978
+
979
+ function showError(figure, entry, err) {
980
+ var scope = entry.scope;
981
+ var message = String((err && (err.message || err.str)) || err || "Unknown error").split("\n").slice(0, 4).join("\n");
982
+
983
+ scope.canvas.innerHTML = "";
984
+ var box = el("div", "zer0-diagram__error");
985
+ box.appendChild(icon("bi-exclamation-triangle-fill"));
986
+ var body = el("div", "zer0-diagram__error-body");
987
+ var title = el("p", "zer0-diagram__error-title");
988
+ title.textContent = LABELS.errorTitle;
989
+ body.appendChild(title);
990
+ // Plain <pre> (no <code> child) on purpose: code-copy.js decorates every
991
+ // `pre code` on DOMContentLoaded with a copy button and a line gutter, and
992
+ // an error card that renders before that pass would pick both up. The
993
+ // toolbar's copy control already covers the source.
994
+ var pre = el("pre", "zer0-diagram__error-message");
995
+ pre.textContent = message;
996
+ body.appendChild(pre);
997
+ var hint = el("p", "zer0-diagram__error-hint");
998
+ hint.textContent = LABELS.errorHint;
999
+ body.appendChild(hint);
1000
+ box.appendChild(body);
1001
+ scope.canvas.appendChild(box);
1002
+
1003
+ var details = el("details", "zer0-diagram__source");
1004
+ details.open = true;
1005
+ var summary = el("summary");
1006
+ summary.textContent = LABELS.showSource;
1007
+ details.appendChild(summary);
1008
+ var srcPre = el("pre", "zer0-diagram__source-text");
1009
+ srcPre.textContent = entry.meta.source;
1010
+ details.appendChild(srcPre);
1011
+ scope.canvas.appendChild(details);
1012
+
1013
+ figure.classList.add("has-error");
1014
+ figure.classList.remove("is-rendered");
1015
+ setToolbarState(scope, "error");
1016
+ }
1017
+
1018
+ function renderFigure(figure) {
1019
+ var entry = registry.get(figure);
1020
+ if (!entry || !window.mermaid) return Promise.resolve(figure);
1021
+ var scope = entry.scope;
1022
+ var meta = entry.meta;
1023
+
1024
+ figure.classList.add("is-loading");
1025
+ figure.classList.remove("has-error");
1026
+ figure.setAttribute("aria-busy", "true");
1027
+ setToolbarState(scope, "loading");
1028
+
1029
+ var id = nextId();
1030
+ return Promise.resolve()
1031
+ .then(function () { return window.mermaid.parse(meta.source); })
1032
+ .then(function () { return window.mermaid.render(id, meta.source, scope.canvas); })
1033
+ .then(function (result) {
1034
+ scope.canvas.innerHTML = result && result.svg ? result.svg : "";
1035
+ var svg = scope.canvas.querySelector("svg");
1036
+ if (!svg) throw new Error("Mermaid returned no SVG");
1037
+ decorateSvg(svg, meta);
1038
+ if (result.bindFunctions) {
1039
+ try { result.bindFunctions(scope.canvas); } catch (err) { /* interactive bindings are optional */ }
1040
+ }
1041
+ figure.classList.add("is-rendered");
1042
+ setToolbarState(scope, "rendered");
1043
+ setZoom(scope, 1);
1044
+ fire(figure, "zer0:diagram-rendered", { ok: true, type: meta.type });
1045
+ return figure;
1046
+ })
1047
+ .catch(function (err) {
1048
+ // Mermaid can leave its temporary render container behind on failure.
1049
+ ["d" + id, id].forEach(function (leftover) {
1050
+ var node = document.getElementById(leftover);
1051
+ if (node && !scope.canvas.contains(node)) node.remove();
1052
+ });
1053
+ showError(figure, entry, err);
1054
+ fire(figure, "zer0:diagram-rendered", { ok: false, type: meta.type, error: String((err && err.message) || err) });
1055
+ return figure;
1056
+ })
1057
+ .then(function () {
1058
+ figure.classList.remove("is-loading");
1059
+ figure.removeAttribute("aria-busy");
1060
+ return figure;
1061
+ });
1062
+ }
1063
+
1064
+ function renderMany(figures) {
1065
+ var failed = 0;
1066
+ return Promise.all(figures.map(function (f) {
1067
+ return renderFigure(f).then(function (fig) {
1068
+ if (fig.classList.contains("has-error")) failed += 1;
1069
+ return fig;
1070
+ });
1071
+ })).then(function (done) {
1072
+ fire(document, "zer0:diagrams-ready", { count: done.length, failed: failed });
1073
+ return done;
1074
+ });
1075
+ }
1076
+
1077
+ // ---------------------------------------------------------------------
1078
+ // Fullscreen lightbox (<dialog>) — the figure's canvas is MOVED into the
1079
+ // dialog and back, so ids, bound click handlers and zoom state stay unique.
1080
+ // ---------------------------------------------------------------------
1081
+ var lightbox = null;
1082
+
1083
+ function ensureLightbox() {
1084
+ if (lightbox) return lightbox;
1085
+ var dlg = el("dialog", "zer0-diagram-lightbox");
1086
+ var inner = el("div", "zer0-diagram-lightbox__inner");
1087
+ var bar = el("div", "zer0-diagram-lightbox__bar");
1088
+ var title = el("p", "zer0-diagram-lightbox__title", { id: "zer0-diagram-lightbox-title" });
1089
+ bar.appendChild(title);
1090
+ var viewport = el("div", "zer0-diagram__viewport zer0-diagram-lightbox__viewport", { tabindex: "0", role: "region" });
1091
+ var scope = { figure: null, canvas: null, viewport: viewport };
1092
+ var toolbar = buildToolbar(scope, { inLightbox: true });
1093
+ toolbar.classList.add("zer0-diagram__toolbar--static");
1094
+ bar.appendChild(toolbar);
1095
+ inner.appendChild(bar);
1096
+ inner.appendChild(viewport);
1097
+ dlg.appendChild(inner);
1098
+ dlg.setAttribute("aria-labelledby", "zer0-diagram-lightbox-title");
1099
+
1100
+ enablePan(viewport);
1101
+ enableWheelZoom(viewport, scope);
1102
+ enableKeys(viewport, scope);
1103
+
1104
+ dlg.addEventListener("click", function (e) {
1105
+ if (e.target === dlg) dlg.close(); // backdrop click
1106
+ });
1107
+ dlg.addEventListener("close", restoreFromLightbox);
1108
+ document.body.appendChild(dlg);
1109
+
1110
+ lightbox = { dialog: dlg, title: title, viewport: viewport, scope: scope, current: null };
1111
+ return lightbox;
1112
+ }
1113
+
1114
+ function openLightbox(figure) {
1115
+ if (!SUPPORTS_DIALOG) return;
1116
+ var entry = registry.get(figure);
1117
+ if (!entry || !figure.classList.contains("is-rendered")) return;
1118
+ var lb = ensureLightbox();
1119
+ if (lb.current) restoreFromLightbox();
1120
+
1121
+ var canvas = entry.scope.canvas;
1122
+ var placeholder = document.createComment("zer0-diagram canvas (shown fullscreen)");
1123
+ // Keep the figure's height so the page does not jump while the canvas is away.
1124
+ entry.scope.viewport.style.minHeight = entry.scope.viewport.getBoundingClientRect().height + "px";
1125
+ canvas.parentNode.insertBefore(placeholder, canvas);
1126
+
1127
+ lb.current = { figure: figure, canvas: canvas, placeholder: placeholder, opener: document.activeElement, zoom: currentZoom(canvas) };
1128
+ lb.scope.figure = figure;
1129
+ lb.scope.canvas = canvas;
1130
+ lb.title.textContent = entry.meta.title || entry.meta.typeLabel;
1131
+ lb.viewport.setAttribute("aria-label", entry.meta.title || entry.meta.typeLabel);
1132
+ lb.viewport.appendChild(canvas);
1133
+
1134
+ figure.classList.add("is-in-lightbox");
1135
+ document.documentElement.classList.add("zer0-diagram-lightbox-open");
1136
+ lb.dialog.showModal();
1137
+ setToolbarState(lb.scope, "rendered");
1138
+ setZoom(lb.scope, 1);
1139
+ fire(figure, "zer0:diagram-fullscreen", { open: true });
1140
+ }
1141
+
1142
+ function restoreFromLightbox() {
1143
+ var lb = lightbox;
1144
+ if (!lb || !lb.current) return;
1145
+ var cur = lb.current;
1146
+ lb.current = null;
1147
+ cur.placeholder.replaceWith(cur.canvas);
1148
+ cur.placeholder = null;
1149
+ var entry = registry.get(cur.figure);
1150
+ if (entry) {
1151
+ entry.scope.viewport.style.minHeight = "";
1152
+ setZoom(entry.scope, cur.zoom || 1);
1153
+ }
1154
+ cur.figure.classList.remove("is-in-lightbox");
1155
+ document.documentElement.classList.remove("zer0-diagram-lightbox-open");
1156
+ lb.scope.figure = null;
1157
+ lb.scope.canvas = null;
1158
+ if (cur.opener && typeof cur.opener.focus === "function" && cur.opener.isConnected) {
1159
+ try { cur.opener.focus({ preventScroll: true }); } catch (err) { /* ignore */ }
1160
+ }
1161
+ fire(cur.figure, "zer0:diagram-fullscreen", { open: false });
1162
+ }
1163
+
1164
+ function closeLightbox() {
1165
+ if (lightbox && lightbox.dialog.open) lightbox.dialog.close();
1166
+ }
1167
+
1168
+ // ---------------------------------------------------------------------
1169
+ // Theme / skin / token changes → re-render from source
1170
+ // ---------------------------------------------------------------------
1171
+ var refreshTimer = null;
1172
+
1173
+ function refresh(force) {
1174
+ if (!window.mermaid) return Promise.resolve([]);
1175
+ var changed = initMermaid(force);
1176
+ if (!changed && !force) return Promise.resolve(allFigures.slice());
1177
+ // A re-render replaces the canvas contents; leave the lightbox first so
1178
+ // its zoom/toolbar state cannot go stale.
1179
+ if (lightbox && lightbox.current) closeLightbox();
1180
+ return renderMany(allFigures.filter(function (f) { return f.isConnected; }));
1181
+ }
1182
+
1183
+ function scheduleRefresh() {
1184
+ clearTimeout(refreshTimer);
1185
+ refreshTimer = setTimeout(function () { refresh(false); }, 80);
1186
+ }
1187
+
1188
+ function watchTheme() {
1189
+ if (typeof MutationObserver === "function") {
1190
+ var observer = new MutationObserver(scheduleRefresh);
1191
+ // `style` catches the Appearance panel writing token overrides onto
1192
+ // <html>. `class` is deliberately NOT watched: the lightbox toggles a
1193
+ // class there, and the palette comparison in refresh() is what decides
1194
+ // whether anything actually changed.
1195
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-bs-theme", "data-theme-skin", "style"] });
1196
+ if (document.body) observer.observe(document.body, { attributes: true, attributeFilter: ["data-bs-theme"] });
1197
+ }
1198
+ document.addEventListener("zer0:skin-change", scheduleRefresh);
1199
+ if (window.matchMedia) {
1200
+ var mq = window.matchMedia("(prefers-color-scheme: dark)");
1201
+ if (mq.addEventListener) mq.addEventListener("change", scheduleRefresh);
1202
+ else if (mq.addListener) mq.addListener(scheduleRefresh);
1203
+ }
1204
+ }
1205
+
1206
+ // ---------------------------------------------------------------------
1207
+ // Public API + boot
1208
+ // ---------------------------------------------------------------------
1209
+ function renderAll(root) {
1210
+ if (!window.mermaid) return Promise.resolve([]);
1211
+ initMermaid(false);
1212
+ var figures = collect(root || document);
1213
+ return renderMany(figures);
1214
+ }
1215
+
1216
+ function render(target, source) {
1217
+ if (!window.mermaid) return Promise.resolve(null);
1218
+ initMermaid(false);
1219
+ var figure;
1220
+ if (target && registry.has(target)) {
1221
+ figure = target;
1222
+ if (typeof source === "string") {
1223
+ var entry = registry.get(figure);
1224
+ entry.meta = parseMeta(source);
1225
+ var caption = figure.querySelector(".zer0-diagram__caption");
1226
+ if (caption) { caption.textContent = entry.meta.title || ""; caption.hidden = !entry.meta.title; }
1227
+ figure.setAttribute("data-zer0-diagram", entry.meta.type);
1228
+ entry.scope.viewport.setAttribute("aria-label", entry.meta.title || entry.meta.typeLabel);
1229
+ }
1230
+ } else if (target && target.nodeType === 1) {
1231
+ figure = makeFigure(parseMeta(typeof source === "string" ? source : target.textContent));
1232
+ target.replaceWith(figure);
1233
+ } else {
1234
+ return Promise.resolve(null);
1235
+ }
1236
+ return renderFigure(figure);
1237
+ }
1238
+
1239
+ window.zer0Mermaid = {
1240
+ version: "3.0.0",
1241
+ config: CONFIG,
1242
+ labels: LABELS,
1243
+ renderAll: renderAll,
1244
+ render: render,
1245
+ refresh: function () { return refresh(true); },
1246
+ getSource: function (figure) { var e = registry.get(figure); return e ? e.meta.source : null; },
1247
+ palette: function () { return palette(); },
1248
+ figures: function () { return allFigures.filter(function (f) { return f.isConnected; }); },
1249
+ setZoom: function (figure, zoom) { var e = registry.get(figure); return e ? setZoom(e.scope, zoom) : null; },
1250
+ openFullscreen: openLightbox,
1251
+ closeFullscreen: closeLightbox,
1252
+ };
1253
+
1254
+ function boot() {
1255
+ if (!window.mermaid || typeof window.mermaid.render !== "function") {
1256
+ // Vendor bundle missing: leave the fences as readable code blocks.
1257
+ document.documentElement.classList.add("zer0-diagram-unavailable");
1258
+ if (window.console && console.warn) console.warn("[zer0-mermaid] mermaid.min.js did not load; diagrams left as source.");
1259
+ return;
1260
+ }
1261
+ renderAll(document);
1262
+ watchTheme();
1263
+ }
1264
+
1265
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
1266
+ else boot();
1267
+ })();