Repository navigation
Expand file tree
/
Copy pathexplainer.html
More file actions
352 lines (334 loc) · 23 KB
/
Copy pathexplainer.html
File metadata and controls
352 lines (334 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>kane-qe v2, Explained</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700&family=Lexend:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* A short story read top to bottom: one idea per section, each with a picture next to it.
Same palette as the band: purple is kane, yellow is assurance only, orange an untested
change, mint/coral/cyan passed/failed/running. */
:root {
--bg: #f7f6fa;
--surface: #ffffff;
--ink: #1c1a26;
--muted: #5c5870;
--rule: #e0dcea;
--accent: #6a3fd1;
--accent-soft: #ece6fd;
--assure: #9a7200;
--warn: #b85400;
--run: #1f6fa8;
--pass: #17845a;
--fail: #c8323f;
--code-bg: #efedf5;
--term: #15141c;
--display: "Bricolage Grotesque", "Avenir Next", "Segoe UI", sans-serif;
--body: "Lexend", "Helvetica Neue", Arial, sans-serif;
--mono: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #131219; --surface: #1c1a25; --ink: #ecebf3; --muted: #a7a2ba; --rule: #312d40;
--accent: #b388ff; --accent-soft: #2c2350; --assure: #ffd66b; --warn: #ffa45c; --run: #9fdcff; --pass: #7fe3b5; --fail: #ff8b94;
--code-bg: #25222f; color-scheme: dark;
}
}
:root[data-theme="dark"] {
--bg: #131219; --surface: #1c1a25; --ink: #ecebf3; --muted: #a7a2ba; --rule: #312d40;
--accent: #b388ff; --accent-soft: #2c2350; --assure: #ffd66b; --warn: #ffa45c; --run: #9fdcff; --pass: #7fe3b5; --fail: #ff8b94;
--code-bg: #25222f; color-scheme: dark;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--ink); font: 300 17px/1.7 var(--body); padding-inline: 16px; padding-block: 0 72px; margin: 0; }
main { max-width: 980px; margin-inline: auto; display: grid; gap: 64px; }
h1, h2, h3 { font-family: var(--display); text-wrap: balance; line-height: 1.15; margin: 0; }
h1 { font-size: clamp(2.3rem, 5.4vw, 3.6rem); font-weight: 700; letter-spacing: -0.01em; }
h2 { font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 700; }
h3 { font-size: 1.1rem; font-weight: 700; }
p { margin: 0; max-width: 64ch; }
strong { font-weight: 600; }
a { color: var(--accent); }
code { font-family: var(--mono); font-size: 0.85em; background: var(--code-bg); padding: 0.1em 0.35em; border-radius: 4px; }
.eyebrow { font: 500 0.78rem/1 var(--body); letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); }
section { display: grid; gap: 18px; min-width: 0; }
.nav { padding-block: 22px 0; display: flex; gap: 18px; flex-wrap: wrap; font-size: 0.92rem; }
.nav a { text-decoration: none; }
.nav a[aria-current="page"] { color: var(--ink); font-weight: 500; }
header.top { padding-block: 36px 0; display: grid; gap: 20px; }
.lede { font-size: 1.22rem; color: var(--muted); }
.breath { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; }
.breath > div { background: var(--surface); border: 1px solid var(--rule); border-radius: 12px; padding: 18px; display: grid; gap: 8px; align-content: start; min-width: 0; }
.breath .who { font: 500 0.78rem/1 var(--mono); color: var(--accent); }
.breath p { color: var(--muted); font-size: 0.97rem; }
.pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 28px; align-items: start; }
@media (max-width: 820px) { .pair { grid-template-columns: 1fr; } }
.words { display: grid; gap: 14px; }
.figure { background: var(--surface); border: 1px solid var(--rule); border-radius: 12px; padding: 18px; min-width: 0; display: grid; gap: 10px; }
.cap { font-size: 0.86rem; color: var(--muted); }
pre.mermaid { margin: 0; overflow-x: auto; background: transparent; text-align: center; font-family: var(--body); white-space: normal; }
/* the band, in text */
.band { background: var(--term); color: #e9e7f2; border-radius: 10px; padding: 14px 16px; font: 500 0.86rem/1.6 var(--mono); overflow-x: auto; white-space: pre; }
.band .chip { background: #2c2350; color: #b388ff; font-weight: 600; } .band .idle { background: #1c1a2b; color: #8480a3; }
.band .btn { color: #9fc6ff; font-weight: 600; } .band .a { color: #ffd66b; } .band .d { color: #8480a3; } .band .p { color: #7fe3b5; }
.band .f { color: #ff8b94; } .band .r { color: #9fdcff; } .band .w { color: #ffa45c; } .band .m { color: #8d7af6; }
.rows { display: grid; gap: 10px; }
.rows > div { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 12px; align-items: start; }
.rows b { font: 600 0.85rem/1.6 var(--mono); color: var(--accent); }
.rows p { color: var(--muted); font-size: 0.97rem; }
.modes { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
.mode { background: var(--surface); border: 1px solid var(--rule); border-radius: 12px; padding: 16px; display: grid; gap: 8px; align-content: start; }
.mode .k { font: 600 0.78rem/1 var(--mono); color: var(--warn); }
.mode p { font-size: 0.95rem; color: var(--muted); }
.chart { width: 100%; height: auto; display: block; }
.chart text { fill: var(--muted); font: 400 12px var(--body); }
.chart .lbl { fill: var(--ink); }
.chart .val { fill: var(--ink); font: 500 12px var(--mono); }
.chart .grid { stroke: var(--rule); stroke-width: 1; }
.legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 0.85rem; color: var(--muted); }
.legend span::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; background: var(--c); vertical-align: -1px; }
.meters { display: grid; gap: 14px; }
.meter { display: grid; gap: 6px; }
.meter .bar { height: 12px; border-radius: 6px; background: var(--code-bg); overflow: hidden; }
.meter .bar > i { display: block; height: 100%; background: var(--assure); }
.meter .row { display: flex; justify-content: space-between; gap: 10px; font-size: 0.92rem; }
.verdict { background: var(--surface); border: 1px solid var(--rule); border-left: 3px solid var(--fail); border-radius: 12px; padding: 16px 18px; display: grid; gap: 8px; }
.verdict dl { display: grid; grid-template-columns: 60px minmax(0, 1fr); gap: 6px 12px; margin: 0; font-size: 0.95rem; }
.verdict dt { font: 500 0.85rem/1.6 var(--mono); color: var(--muted); }
.verdict dd { margin: 0; }
dl.gloss { display: grid; grid-template-columns: minmax(130px, 200px) minmax(0, 1fr); gap: 0; margin: 0; border-top: 1px solid var(--rule); }
dl.gloss dt, dl.gloss dd { margin: 0; padding: 12px 0; border-bottom: 1px solid var(--rule); }
dl.gloss dt { font-weight: 600; padding-right: 16px; }
dl.gloss dd { color: var(--muted); }
@media (max-width: 560px) { dl.gloss { grid-template-columns: 1fr; } dl.gloss dt { border-bottom: 0; padding-bottom: 0; } }
.steps { display: grid; gap: 10px; counter-reset: s; padding: 0; margin: 0; list-style: none; }
.steps li { counter-increment: s; display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 12px; align-items: start; }
.steps li::before { content: counter(s); font: 700 1rem/34px var(--display); text-align: center; width: 34px; height: 34px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); }
footer { color: var(--muted); font-size: 0.88rem; border-top: 1px solid var(--rule); padding-top: 18px; }
</style>
<main>
<nav class="nav" aria-label="Pages">
<a href="index.html">Field Guide</a>
<a href="explainer.html" aria-current="page">Explained simply</a>
<a href="https://github.com/LambdaTest/kane-claude-plugin">GitHub</a>
</nav>
<header class="top">
<div class="eyebrow">kane-qe v2, explained simply</div>
<h1>A dashboard light for your robot tester</h1>
<p class="lede">An AI tester clicks through a website for you, and a test takes a minute or two. kane-qe v2 is a small strip above where you type that tells you, without asking, what the tester is doing right now, what just broke and why, and whether the code you just changed has been tested at all.</p>
</header>
<section aria-labelledby="who-h">
<div class="eyebrow">The cast</div>
<h2 id="who-h">Who does what</h2>
<div class="breath">
<div><span class="who">you</span><h3>The tester</h3><p>You say what to check: “add an iPod to the cart and make sure it shows 1 item”.</p></div>
<div><span class="who">Claude Code</span><h3>Your assistant</h3><p>The AI you chat with. It writes code, and it starts tests when you ask.</p></div>
<div><span class="who">kane-cli</span><h3>The robot tester</h3><p>Opens a real browser, clicks like a person, checks what you asked, and keeps a diary of every step on disk.</p></div>
<div><span class="who">kane-qe v2</span><h3>The dashboard light</h3><p>Reads that diary once a second and shows it. It never starts a test itself, and never makes anything up.</p></div>
</div>
<div class="figure">
<pre class="mermaid">
sequenceDiagram
participant You
participant Claude
participant Kane as kane-cli
participant Disk as its diary on disk
participant Band as the band
You->>Claude: test adding to the cart
Claude->>Kane: kane-cli run "…"
Kane->>Disk: I'm running (a note with my process id)
loop every step
Kane->>Disk: step done: Clicking Add to cart
Band->>Disk: anything new? (every second)
Disk-->>Band: one more line
Band-->>You: shows the step
end
Kane->>Disk: finished: passed or failed, and why
Band-->>You: ✓ 1 passed, or ✗ and the step it failed on
</pre>
<p class="cap">The band only reads. If kane-cli did not write it down, the band does not show it.</p>
</div>
</section>
<section aria-labelledby="band-h">
<div class="eyebrow">The band</div>
<h2 id="band-h">Three rows, always there</h2>
<div class="pair">
<div class="words">
<p>The band sits just above where you type, in every session. The little helmet on the left is kane's mascot: its eyes move while a test runs and stay still when nothing does. So a glance tells you if anything is happening.</p>
<div class="rows">
<div><b>row 1</b><p>What kane-cli is doing: <em>idle</em>, <em>running</em> one test with its time, several runs as coloured squares, or a suite as <em>5 of 12 tests done</em>.</p></div>
<div><b>row 2</b><p>The detail: the step a test is on, or the test that just failed and where, or a warning that code changed and nobody tested it.</p></div>
<div><b>row 3</b><p>How much of what the product should do has been proven by a real test. Always yellow, always last.</p></div>
</div>
</div>
<div class="figure">
<div class="band"><span class="m">▄▀▄▄▀▄ </span> <span class="chip"> suite </span> <span class="p">■■■■</span><span class="f">■</span><span class="r">■■■</span><span class="d">□□□□</span> <b>5 of 12 tests done</b> <span class="d">· 3 running · 4 left</span> <span class="btn">[ Open ]</span>
<span class="m">▀▀▀▀▀▀▀▀</span> <span class="f">✗ checkout_guest</span> <span class="d">failed on</span> Fill the shipping address
<span class="m">▀▀▀▀▀▀▀▀</span> <span class="a">assurance ━━━━━━━━</span><span class="d">────────</span> <span class="a"><b>47%</b></span> <span class="d">proven · 4 use cases</span></div>
<div class="legend"><span style="--c:var(--pass)">passed</span><span style="--c:var(--fail)">failed</span><span style="--c:var(--run)">running</span><span style="--c:var(--muted)">waiting</span></div>
<p class="cap">One square per test. A failure takes row 2 the moment it happens, while the rest of the suite keeps going.</p>
</div>
</div>
</section>
<section aria-labelledby="runs-h">
<div class="eyebrow">Real runs</div>
<h2 id="runs-h">What the band saw while the video was recorded</h2>
<div class="words">
<div class="words">
<p>Every bar is one real kane-cli run from the walkthrough video, placed where it started and stopped. Some ran alone, some at the same time, some as part of a suite. The band followed all of them by itself: nobody told it a run had started.</p>
<p>When two or more bars overlap, row 1 turns into squares, one per run. When they all end, it goes back to <em>idle</em> with how many passed and failed.</p>
</div>
<div class="figure">
<svg class="chart" id="runs-chart" viewBox="0 0 720 300" role="img" aria-label="Real kane-cli runs over the recording, as bars from start to end, coloured by result"></svg>
<div class="legend"><span style="--c:var(--pass)">passed</span><span style="--c:var(--fail)">failed</span></div>
<p class="cap" id="runs-cap">Minutes from the start of the recording, before waiting stretches were sped up. 7 runs: 4 passed, 3 failed. The failures are kane's own reports, shown as they came: one run started in the wrong page, and two saved tests stopped for want of a start URL (the suite ran without <code>--url</code>, and those tests name none).</p>
</div>
</div>
</section>
<section aria-labelledby="why-h">
<div class="eyebrow">When a test fails</div>
<h2 id="why-h">It tells you what kane found, and nothing it didn't</h2>
<div class="pair">
<div class="words">
<p>When kane-cli's test fails, kane investigates and writes down a verdict: the reason, what kind of problem it is, how serious, and how sure it is. kane-qe shows exactly that, with the step it failed on.</p>
<p>It never guesses. If kane-cli did not give a reason, the pane says so. In this real case the button kane was asked to press is on the page but hidden, so the verdict is worth reading carefully: kane calls it a product defect; a person would call it a test that asked for a button users cannot see.</p>
</div>
<div class="verdict" aria-label="A real verdict">
<h3>Click Enable Notification, then choose Safari so the wizard reaches step 3.</h3>
<dl>
<dt>why</dt><dd>The previous-step control accepted the click but the guided wizard stayed on step 3 instead of moving back to step 2.</dd>
<dt>kind</dt><dd>functional defect · major · 90% sure</dd>
<dt>where</dt><dd>press the Tab key to focus the wizard's previous-step navigation button</dd>
</dl>
<p class="cap">What the pane shows for a real run on kaneai-playground.lambdatest.io. <em>where</em> is the last step kane took before it gave up.</p>
</div>
</div>
</section>
<section aria-labelledby="change-h">
<div class="eyebrow">After a change</div>
<h2 id="change-h">Did anyone test what Claude just changed?</h2>
<p>Claude can change code in seconds. It is easy to finish without checking the result in a browser. kane-qe notices which files Claude changed (not docs, not tool folders), and when Claude finishes, it does one of three things. You choose which.</p>
<div class="modes">
<div class="mode"><span class="k">offer · the default</span><h3>A reminder with a button</h3><p>The band says <em>⚠ 1 file changed, untested</em>. Press <em>Test this change</em>: you get the saved tests that already cover those files, with how they did last time, or Claude writes a new test from the change.</p></div>
<div class="mode"><span class="k">auto</span><h3>Claude tests before it stops</h3><p>When Claude tries to finish, it is asked once to test the change first. In a real try it did: it ran kane-cli and reported back. If it still finishes without a test, the band goes back to the button.</p></div>
<div class="mode"><span class="k">off</span><h3>Quiet</h3><p>Nothing. The band never mentions changes.</p></div>
</div>
<div class="figure">
<pre class="mermaid">
flowchart LR
A["Claude edits<br/>addToCart.ts"] --> B["Claude finishes"]
B --> C["⚠ 1 file changed, untested"]
C --> D["Test this change"]
D --> E["cart_add_test.md<br/>covers it: Run it"]
E --> F["Claude runs kane-cli"]
F --> G["The run ends:<br/>the warning goes away"]
</pre>
<p class="cap">Any kane-cli run that starts after the last change clears the warning, whether it passes or not: the change has been looked at.</p>
</div>
</section>
<section aria-labelledby="assure-h">
<div class="eyebrow">Assurance</div>
<h2 id="assure-h">Proven, not just planned</h2>
<div class="pair">
<div class="words">
<p>A requirement says what the product should do. kane-cli can turn it into <strong>use cases</strong>, design tests for each, and record which ones a real run has <strong>proven</strong>. Row 3 of the band shows that number all the time.</p>
<p>These are the real numbers of the demo project. Every acceptance criterion is designed and proven, so the project reads 100%. But the second use case has no test designed at all. That is why the Assurance tab shows a card per use case: a high total can still hide a gap.</p>
</div>
<div class="figure">
<div class="meters">
<div class="meter"><div class="row"><span>Acceptance criteria designed</span><b>5 of 5 · 100%</b></div><div class="bar"><i style="width:100%"></i></div></div>
<div class="meter"><div class="row"><span>Acceptance criteria proven</span><b>5 of 5 · 100%</b></div><div class="bar"><i style="width:100%"></i></div></div>
<div class="meter"><div class="row"><span>Search the product catalog</span><b>designed 100% · proven 100%</b></div><div class="bar"><i style="width:100%"></i></div></div>
<div class="meter"><div class="row"><span>View a product from search results</span><b>designed 0% · 1 to design</b></div><div class="bar"><i style="width:0%"></i></div></div>
</div>
<p class="cap">From <code>kane-cli cover gaps --json</code> in the demo project. Reading it costs no credits.</p>
</div>
</div>
</section>
<section aria-labelledby="gloss-h">
<div class="eyebrow">Words</div>
<h2 id="gloss-h">A tiny glossary</h2>
<dl class="gloss">
<dt>run</dt><dd>One test kane-cli carries out: an objective in plain words, or a saved test.</dd>
<dt>saved test</dt><dd>A test kept as a file (<code>…_test.md</code>) so it can be run again the same way.</dd>
<dt>suite</dt><dd>Several saved tests run together, a few at a time.</dd>
<dt>step</dt><dd>One thing the tester did: opened a page, typed, clicked, checked.</dd>
<dt>verdict</dt><dd>kane-cli's own explanation of a failure: why, what kind, how serious, how sure.</dd>
<dt>evidence</dt><dd>The pack kane-cli keeps for every run: screenshots, network, console, each step.</dd>
<dt>assurance</dt><dd>How much of a requirement a real test has proven.</dd>
<dt>use case</dt><dd>One thing a user does with the product, taken from the requirement.</dd>
<dt>band · pane</dt><dd>The three rows above the prompt, and the side panel that opens with <code>/kane</code>.</dd>
</dl>
</section>
<section aria-labelledby="try-h">
<div class="eyebrow">Try it</div>
<h2 id="try-h">Three steps</h2>
<ol class="steps">
<li><span>Install kane-cli and sign in: <code>npm install -g @testmuai/kane-cli</code>, then <code>! kane-cli login --oauth</code> in Claude Code.</span></li>
<li><span>Install the plugin: <code>/plugin marketplace add LambdaTest/kane-claude-plugin</code>, then <code>/plugin install kane-qe@lambdatest</code>. The band appears.</span></li>
<li><span>Ask Claude: “test that searching for iPod lists at least 4 products on ecommerce-playground.lambdatest.io”. Watch the band.</span></li>
</ol>
</section>
<footer>kane-qe v2 · every number on this page is from a real kane-cli run · <a href="index.html">the Field Guide</a> has the full reference.</footer>
</main>
<script>
// Real kane-cli runs from the walkthrough recording: start and end in minutes, and the result.
const RUNS = /*RUNS*/[{"label": "Open the Laptops category", "start": 0.43, "end": 1.74, "ok": true}, {"label": "Search for iPod Shuffle", "start": 2.12, "end": 3.79, "ok": true}, {"label": "Enable Notification, Safari, go back", "start": 2.12, "end": 3.33, "ok": false}, {"label": "suite: login_page_test.md", "start": 4.11, "end": 4.26, "ok": false}, {"label": "suite: cart_add_test.md", "start": 4.11, "end": 4.27, "ok": false}, {"label": "suite: search_ipod_test.md", "start": 4.19, "end": 4.48, "ok": true}, {"label": "search_ipod_test.md (the change)", "start": 5.31, "end": 5.79, "ok": true}];
const svg = document.getElementById('runs-chart');
const NS = 'http://www.w3.org/2000/svg';
const el = (name, attrs, text) => {
const n = document.createElementNS(NS, name);
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
if (text !== undefined) n.textContent = text;
svg.appendChild(n);
return n;
};
if (RUNS.length) {
const W = 720, rowH = 30, padTop = 10, left = 250, right = 20;
const end = Math.ceil(Math.max(...RUNS.map(r => r.end)));
const x = m => left + ((W - left - right) * m) / end;
const height = padTop + RUNS.length * rowH + 30;
svg.setAttribute('viewBox', `0 0 ${W} ${height}`);
for (let m = 0; m <= end; m += end > 12 ? 4 : 2) {
el('line', { x1: x(m), x2: x(m), y1: padTop - 4, y2: padTop + RUNS.length * rowH, class: 'grid' });
el('text', { x: x(m), y: padTop + RUNS.length * rowH + 18, 'text-anchor': 'middle' }, `${m}`);
}
RUNS.forEach((r, i) => {
const y = padTop + i * rowH;
el('text', { x: left - 10, y: y + rowH / 2 + 4, 'text-anchor': 'end', class: 'lbl' }, r.label);
const bar = el('rect', { x: x(r.start), y: y + 7, width: Math.max(3, x(r.end) - x(r.start)), height: rowH - 14, rx: 4 });
bar.style.fill = r.ok ? 'var(--pass)' : 'var(--fail)';
});
}
</script>
<script>
window.addEventListener('load', () => setTimeout(() => {
const pending = [...document.querySelectorAll('pre.mermaid')].filter(p => !p.querySelector('svg') && !p.dataset.processed);
if (!pending.length) return;
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js';
s.onload = async () => {
// Measure labels in the page's own font: drawn before it loads, the boxes come out too narrow.
await document.fonts.ready
const dark = matchMedia('(prefers-color-scheme: dark)').matches && document.documentElement.dataset.theme !== 'light';
window.mermaid.initialize({ startOnLoad: false, theme: dark ? 'dark' : 'neutral', fontFamily: 'Lexend, sans-serif', flowchart: { padding: 14, wrappingWidth: 220 } });
;(async () => {
let i = 0
for (const node of pending) {
const copy = node.cloneNode(true)
copy.querySelectorAll('br').forEach(br => br.replaceWith('<br/>'))
try {
const { svg } = await window.mermaid.render(`kq-diagram-${i++}`, copy.textContent)
node.innerHTML = svg
node.dataset.processed = 'true'
} catch (error) {
console.warn('diagram did not render', error)
}
}
})()
};
document.head.appendChild(s);
}, 400));
</script>