stuntdouble 0.1.0 → 0.1.2
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.
package/dist/ui.html
CHANGED
|
@@ -1,265 +1,1359 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
2
|
+
<html lang="en-GB">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Stunt Double Local</title>
|
|
7
7
|
<style>
|
|
8
|
+
/* Continuity, ported from Stunt Double's @stuntdouble/ui (globals.css and
|
|
9
|
+
motion.css): the semantic tokens, radii, type stacks and motion tiers
|
|
10
|
+
this page uses, under the same names. Untitled Sans is a licensed face
|
|
11
|
+
that cannot ship in an open source package, and this page loads
|
|
12
|
+
nothing from the network, so the sans is the token's own system
|
|
13
|
+
fallback. Geist Mono and Space Grotesk are used when installed. */
|
|
8
14
|
:root {
|
|
9
15
|
color-scheme: light dark;
|
|
10
16
|
--background: oklch(0.985 0.005 250);
|
|
11
17
|
--foreground: oklch(0.18 0.016 268);
|
|
12
18
|
--card: oklch(1 0 0);
|
|
19
|
+
--secondary: oklch(0.945 0.009 255);
|
|
20
|
+
--secondary-foreground: oklch(0.22 0.016 268);
|
|
13
21
|
--muted: oklch(0.955 0.007 255);
|
|
14
22
|
--muted-foreground: oklch(0.46 0.017 268);
|
|
15
|
-
--
|
|
23
|
+
--accent: oklch(0.945 0.02 255);
|
|
24
|
+
--accent-foreground: oklch(0.22 0.016 268);
|
|
16
25
|
--primary: oklch(0.52 0.22 268);
|
|
17
26
|
--primary-foreground: oklch(0.985 0.005 250);
|
|
27
|
+
--destructive: oklch(0.53 0.215 22);
|
|
28
|
+
--border: oklch(0.88 0.01 255);
|
|
29
|
+
--input: oklch(0.86 0.01 255);
|
|
30
|
+
--ring: oklch(0.52 0.22 268);
|
|
18
31
|
--success: oklch(0.6 0.13 160);
|
|
32
|
+
--success-ink: oklch(0.52 0.12 160);
|
|
19
33
|
--warning: oklch(0.75 0.15 75);
|
|
20
|
-
--
|
|
21
|
-
|
|
34
|
+
--warning-ink: oklch(0.52 0.13 70);
|
|
35
|
+
|
|
36
|
+
--radius-md: 0.25rem;
|
|
37
|
+
--radius-lg: 0.375rem;
|
|
38
|
+
|
|
22
39
|
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
40
|
+
--font-mono:
|
|
41
|
+
'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
23
42
|
--font-display: 'Space Grotesk', var(--font-sans);
|
|
24
|
-
|
|
43
|
+
|
|
44
|
+
--ease-snap: cubic-bezier(0.23, 1, 0.32, 1);
|
|
45
|
+
--ease-glide: cubic-bezier(0.77, 0, 0.175, 1);
|
|
46
|
+
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
47
|
+
--duration-press: 140ms;
|
|
48
|
+
--duration-quick: 180ms;
|
|
49
|
+
--duration-base: 250ms;
|
|
50
|
+
--duration-slow: 360ms;
|
|
51
|
+
|
|
52
|
+
/* The primary button's light (.button-arc). */
|
|
53
|
+
--button-fill-lit: color-mix(in oklab, var(--primary) 92%, var(--foreground));
|
|
54
|
+
--button-lamp: oklch(1 0 0 / 0.24);
|
|
55
|
+
--button-specular: oklch(1 0 0 / 0.28);
|
|
56
|
+
--button-specular-lit: oklch(1 0 0 / 0.46);
|
|
57
|
+
--button-shade: oklch(0.255 0.11 268 / 0.38);
|
|
58
|
+
--button-rim: oklch(0.32 0.15 268 / 0.5);
|
|
59
|
+
--button-shadow:
|
|
60
|
+
0 1px 1px oklch(0.25 0.03 265 / 0.14), 0 2px 5px -2px oklch(0.25 0.03 265 / 0.22);
|
|
61
|
+
--button-shadow-lit:
|
|
62
|
+
0 1px 1px oklch(0.25 0.03 265 / 0.16), 0 5px 12px -5px oklch(0.25 0.03 265 / 0.34);
|
|
25
63
|
}
|
|
26
64
|
@media (prefers-color-scheme: dark) {
|
|
27
65
|
:root {
|
|
28
66
|
--background: oklch(0.14 0.014 265);
|
|
29
67
|
--foreground: oklch(0.95 0.006 250);
|
|
30
68
|
--card: oklch(0.17 0.014 265);
|
|
69
|
+
--secondary: oklch(0.22 0.014 265);
|
|
70
|
+
--secondary-foreground: oklch(0.95 0.006 250);
|
|
31
71
|
--muted: oklch(0.22 0.014 265);
|
|
32
72
|
--muted-foreground: oklch(0.68 0.014 250);
|
|
33
|
-
--
|
|
73
|
+
--accent: oklch(0.24 0.018 265);
|
|
74
|
+
--accent-foreground: oklch(0.95 0.006 250);
|
|
34
75
|
--primary: oklch(0.68 0.165 276);
|
|
35
76
|
--primary-foreground: oklch(0.14 0.014 265);
|
|
77
|
+
--destructive: oklch(0.64 0.2 22);
|
|
78
|
+
--border: oklch(0.95 0.006 250 / 0.1);
|
|
79
|
+
--input: oklch(0.95 0.006 250 / 0.12);
|
|
80
|
+
--ring: oklch(0.68 0.165 276);
|
|
36
81
|
--success: oklch(0.68 0.14 160);
|
|
82
|
+
--success-ink: oklch(0.72 0.14 160);
|
|
37
83
|
--warning: oklch(0.82 0.15 75);
|
|
38
|
-
--
|
|
84
|
+
--warning-ink: oklch(0.82 0.15 75);
|
|
85
|
+
|
|
86
|
+
--button-lamp: oklch(1 0 0 / 0.3);
|
|
87
|
+
--button-specular: oklch(1 0 0 / 0.4);
|
|
88
|
+
--button-specular-lit: oklch(1 0 0 / 0.6);
|
|
89
|
+
--button-shade: oklch(0.2 0.08 272 / 0.3);
|
|
90
|
+
--button-rim: oklch(1 0 0 / 0.1);
|
|
91
|
+
--button-shadow: 0 1px 1px oklch(0 0 0 / 0.5), 0 2px 6px -2px oklch(0 0 0 / 0.5);
|
|
92
|
+
--button-shadow-lit: 0 1px 1px oklch(0 0 0 / 0.5), 0 5px 14px -5px oklch(0 0 0 / 0.65);
|
|
39
93
|
}
|
|
40
94
|
}
|
|
95
|
+
|
|
96
|
+
/* ── Base ─────────────────────────────────────────────── */
|
|
41
97
|
* {
|
|
42
98
|
box-sizing: border-box;
|
|
43
99
|
}
|
|
100
|
+
[hidden] {
|
|
101
|
+
display: none !important;
|
|
102
|
+
}
|
|
103
|
+
html {
|
|
104
|
+
-webkit-text-size-adjust: 100%;
|
|
105
|
+
}
|
|
44
106
|
body {
|
|
45
107
|
margin: 0;
|
|
108
|
+
min-height: 100vh;
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
46
111
|
background: var(--background);
|
|
47
112
|
color: var(--foreground);
|
|
48
|
-
font: 14px/1.5 var(--font-sans);
|
|
113
|
+
font: 400 14px/1.5 var(--font-sans);
|
|
114
|
+
-webkit-font-smoothing: antialiased;
|
|
115
|
+
-moz-osx-font-smoothing: grayscale;
|
|
116
|
+
}
|
|
117
|
+
h1,
|
|
118
|
+
h2,
|
|
119
|
+
h3 {
|
|
120
|
+
margin: 0;
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
text-wrap: balance;
|
|
123
|
+
}
|
|
124
|
+
p,
|
|
125
|
+
li {
|
|
126
|
+
text-wrap: pretty;
|
|
127
|
+
}
|
|
128
|
+
p {
|
|
129
|
+
margin: 0;
|
|
49
130
|
}
|
|
50
131
|
a {
|
|
51
132
|
color: inherit;
|
|
52
133
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
ul,
|
|
135
|
+
ol {
|
|
136
|
+
list-style: none;
|
|
137
|
+
margin: 0;
|
|
138
|
+
padding: 0;
|
|
139
|
+
}
|
|
140
|
+
:focus-visible {
|
|
141
|
+
outline: 2px solid var(--ring);
|
|
142
|
+
outline-offset: 2px;
|
|
143
|
+
}
|
|
144
|
+
::selection {
|
|
145
|
+
background-color: var(--primary);
|
|
146
|
+
color: var(--primary-foreground);
|
|
147
|
+
}
|
|
148
|
+
/* A full-bleed row inside a clipped group draws its ring inside. */
|
|
149
|
+
.group :focus-visible {
|
|
150
|
+
outline-offset: -2px;
|
|
151
|
+
}
|
|
152
|
+
code,
|
|
153
|
+
.mono {
|
|
154
|
+
font-family: var(--font-mono);
|
|
155
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
156
|
+
}
|
|
157
|
+
svg.icon {
|
|
158
|
+
width: 1rem;
|
|
159
|
+
height: 1rem;
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
}
|
|
162
|
+
.muted {
|
|
163
|
+
color: var(--muted-foreground);
|
|
164
|
+
}
|
|
165
|
+
.sr-only {
|
|
166
|
+
position: absolute;
|
|
167
|
+
width: 1px;
|
|
168
|
+
height: 1px;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
clip: rect(0 0 0 0);
|
|
171
|
+
white-space: nowrap;
|
|
172
|
+
}
|
|
173
|
+
.truncate {
|
|
174
|
+
min-width: 0;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ── Chrome ───────────────────────────────────────────── */
|
|
181
|
+
.container {
|
|
182
|
+
width: 100%;
|
|
56
183
|
max-width: 72rem;
|
|
57
184
|
margin: 0 auto;
|
|
58
|
-
padding:
|
|
185
|
+
padding-inline: 1rem;
|
|
59
186
|
}
|
|
60
|
-
|
|
187
|
+
/* The narrow tier, kept on the chrome's left edge. */
|
|
188
|
+
.container.narrow > * {
|
|
189
|
+
max-width: 48rem;
|
|
190
|
+
}
|
|
191
|
+
@media (min-width: 40rem) {
|
|
192
|
+
.container {
|
|
193
|
+
padding-inline: 1.5rem;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
@media (min-width: 64rem) {
|
|
197
|
+
.container {
|
|
198
|
+
padding-inline: 2rem;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
.bar {
|
|
202
|
+
border-bottom: 1px solid var(--border);
|
|
203
|
+
background: var(--background);
|
|
204
|
+
}
|
|
205
|
+
.bar .container {
|
|
61
206
|
display: flex;
|
|
62
207
|
flex-wrap: wrap;
|
|
63
208
|
align-items: center;
|
|
64
|
-
gap: 0.5rem
|
|
209
|
+
gap: 0.5rem 2rem;
|
|
65
210
|
min-height: 3.5rem;
|
|
66
211
|
padding-block: 0.5rem;
|
|
67
|
-
border-bottom: 1px solid var(--border);
|
|
68
212
|
}
|
|
69
|
-
|
|
70
|
-
font-
|
|
213
|
+
.product {
|
|
214
|
+
font-weight: 500;
|
|
71
215
|
letter-spacing: -0.012em;
|
|
72
216
|
white-space: nowrap;
|
|
73
217
|
}
|
|
74
|
-
|
|
75
|
-
|
|
218
|
+
.bar nav {
|
|
219
|
+
display: flex;
|
|
220
|
+
gap: 0.25rem;
|
|
221
|
+
margin-inline: -0.5rem;
|
|
222
|
+
}
|
|
223
|
+
.bar nav a {
|
|
224
|
+
padding: 0.25rem 0.5rem;
|
|
225
|
+
border-radius: var(--radius-md);
|
|
76
226
|
color: var(--muted-foreground);
|
|
77
227
|
text-decoration: none;
|
|
78
228
|
white-space: nowrap;
|
|
229
|
+
transition: color var(--duration-quick) ease;
|
|
79
230
|
}
|
|
80
|
-
|
|
231
|
+
.bar nav a:hover {
|
|
81
232
|
color: var(--foreground);
|
|
82
233
|
}
|
|
234
|
+
.bar nav a[aria-current='page'] {
|
|
235
|
+
color: var(--foreground);
|
|
236
|
+
background: var(--accent);
|
|
237
|
+
}
|
|
83
238
|
main {
|
|
84
|
-
|
|
85
|
-
padding-
|
|
239
|
+
flex: 1;
|
|
240
|
+
padding-block: 2rem 4rem;
|
|
241
|
+
}
|
|
242
|
+
@media (min-width: 40rem) {
|
|
243
|
+
main {
|
|
244
|
+
padding-block: 2.5rem 5rem;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
footer {
|
|
248
|
+
border-top: 1px solid var(--border);
|
|
249
|
+
padding-block: 1.5rem 2rem;
|
|
250
|
+
font-size: 0.75rem;
|
|
251
|
+
color: var(--muted-foreground);
|
|
252
|
+
}
|
|
253
|
+
footer p {
|
|
254
|
+
max-width: 42rem;
|
|
255
|
+
}
|
|
256
|
+
footer a,
|
|
257
|
+
.link {
|
|
258
|
+
color: var(--foreground);
|
|
259
|
+
text-underline-offset: 3px;
|
|
260
|
+
text-decoration-color: var(--border);
|
|
261
|
+
transition: text-decoration-color var(--duration-quick) ease;
|
|
262
|
+
}
|
|
263
|
+
footer a:hover,
|
|
264
|
+
.link:hover {
|
|
265
|
+
text-decoration-color: currentColor;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* ── Page header (PageHeader, PageTitle) ──────────────── */
|
|
269
|
+
.page-header {
|
|
270
|
+
margin-bottom: 2rem;
|
|
271
|
+
}
|
|
272
|
+
@media (min-width: 40rem) {
|
|
273
|
+
.page-header {
|
|
274
|
+
margin-bottom: 2.5rem;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
.back {
|
|
278
|
+
display: flex;
|
|
279
|
+
min-width: 0;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 0.5rem;
|
|
282
|
+
margin-bottom: 1rem;
|
|
283
|
+
font-size: 0.875rem;
|
|
284
|
+
color: var(--muted-foreground);
|
|
285
|
+
}
|
|
286
|
+
.back a {
|
|
287
|
+
display: inline-flex;
|
|
288
|
+
flex-shrink: 0;
|
|
289
|
+
align-items: center;
|
|
290
|
+
gap: 0.375rem;
|
|
291
|
+
text-decoration: none;
|
|
292
|
+
transition: color var(--duration-quick) ease;
|
|
293
|
+
}
|
|
294
|
+
.back a:hover {
|
|
295
|
+
color: var(--foreground);
|
|
296
|
+
}
|
|
297
|
+
.back .slash {
|
|
298
|
+
color: var(--border);
|
|
299
|
+
}
|
|
300
|
+
.header-grid {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-wrap: wrap;
|
|
303
|
+
align-items: flex-start;
|
|
304
|
+
justify-content: space-between;
|
|
305
|
+
gap: 1rem 1.5rem;
|
|
306
|
+
}
|
|
307
|
+
.header-main {
|
|
308
|
+
min-width: 0;
|
|
309
|
+
display: grid;
|
|
310
|
+
gap: 0.5rem;
|
|
311
|
+
}
|
|
312
|
+
.title-row {
|
|
313
|
+
display: flex;
|
|
314
|
+
min-width: 0;
|
|
315
|
+
flex-wrap: wrap;
|
|
316
|
+
align-items: center;
|
|
317
|
+
gap: 0.375rem 0.75rem;
|
|
318
|
+
}
|
|
319
|
+
.page-title {
|
|
320
|
+
font-family: var(--font-display);
|
|
321
|
+
font-size: 1.875rem;
|
|
322
|
+
font-weight: 500;
|
|
323
|
+
letter-spacing: -0.035em;
|
|
324
|
+
line-height: 1.1;
|
|
325
|
+
overflow-wrap: anywhere;
|
|
326
|
+
}
|
|
327
|
+
@media (min-width: 40rem) {
|
|
328
|
+
.page-title {
|
|
329
|
+
font-size: 2.25rem;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
.description {
|
|
333
|
+
max-width: 42rem;
|
|
334
|
+
color: var(--muted-foreground);
|
|
335
|
+
}
|
|
336
|
+
@media (min-width: 40rem) {
|
|
337
|
+
.description {
|
|
338
|
+
font-size: 1rem;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
.actions {
|
|
342
|
+
display: flex;
|
|
343
|
+
max-width: 100%;
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
flex-wrap: wrap;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 0.5rem;
|
|
348
|
+
}
|
|
349
|
+
.chips {
|
|
350
|
+
display: flex;
|
|
351
|
+
max-width: 42rem;
|
|
352
|
+
flex-wrap: wrap;
|
|
353
|
+
align-items: center;
|
|
354
|
+
gap: 0.375rem;
|
|
355
|
+
padding-top: 0.25rem;
|
|
356
|
+
}
|
|
357
|
+
.chips > li {
|
|
358
|
+
min-width: 0;
|
|
359
|
+
max-width: 100%;
|
|
360
|
+
}
|
|
361
|
+
/* The outline badge at its large size, in the quiet ink: a fact, not a state. */
|
|
362
|
+
.chip {
|
|
363
|
+
display: inline-flex;
|
|
364
|
+
max-width: 100%;
|
|
365
|
+
height: 1.5rem;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 0.25rem;
|
|
368
|
+
padding: 0.125rem 0.625rem;
|
|
369
|
+
border: 1px solid var(--border);
|
|
370
|
+
border-radius: 9999px;
|
|
371
|
+
font-size: 0.6875rem;
|
|
372
|
+
color: var(--muted-foreground);
|
|
373
|
+
text-decoration: none;
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
transition:
|
|
376
|
+
color var(--duration-quick) var(--ease-snap),
|
|
377
|
+
border-color var(--duration-quick) var(--ease-snap);
|
|
378
|
+
}
|
|
379
|
+
.chip svg.icon {
|
|
380
|
+
width: 0.75rem;
|
|
381
|
+
height: 0.75rem;
|
|
382
|
+
}
|
|
383
|
+
a.chip:hover {
|
|
384
|
+
color: var(--foreground);
|
|
385
|
+
border-color: color-mix(in oklch, var(--foreground) 25%, transparent);
|
|
386
|
+
}
|
|
387
|
+
@media (min-width: 40rem) {
|
|
388
|
+
.chips {
|
|
389
|
+
gap: 0.5rem;
|
|
390
|
+
}
|
|
391
|
+
.chip {
|
|
392
|
+
height: 1.75rem;
|
|
393
|
+
gap: 0.375rem;
|
|
394
|
+
padding: 0.25rem 0.75rem;
|
|
395
|
+
font-size: 0.75rem;
|
|
396
|
+
}
|
|
397
|
+
.chip svg.icon {
|
|
398
|
+
width: 0.875rem;
|
|
399
|
+
height: 0.875rem;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* ── Badge ────────────────────────────────────────────── */
|
|
404
|
+
.badge {
|
|
405
|
+
display: inline-flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 0.25rem;
|
|
408
|
+
padding: 0.125rem 0.625rem;
|
|
409
|
+
border: 1px solid transparent;
|
|
410
|
+
border-radius: 9999px;
|
|
411
|
+
font-size: 0.6875rem;
|
|
412
|
+
font-weight: 500;
|
|
413
|
+
line-height: 1.4;
|
|
414
|
+
white-space: nowrap;
|
|
415
|
+
}
|
|
416
|
+
.badge svg.icon {
|
|
417
|
+
width: 0.75rem;
|
|
418
|
+
height: 0.75rem;
|
|
419
|
+
}
|
|
420
|
+
.badge.lg {
|
|
421
|
+
gap: 0.375rem;
|
|
422
|
+
padding: 0.25rem 0.75rem;
|
|
423
|
+
font-size: 0.75rem;
|
|
424
|
+
}
|
|
425
|
+
.badge.lg svg.icon {
|
|
426
|
+
width: 0.875rem;
|
|
427
|
+
height: 0.875rem;
|
|
428
|
+
}
|
|
429
|
+
.badge.sm {
|
|
430
|
+
padding: 1px 0.5rem;
|
|
431
|
+
font-size: 0.625rem;
|
|
432
|
+
}
|
|
433
|
+
.badge.outline {
|
|
434
|
+
border-color: var(--border);
|
|
435
|
+
color: var(--foreground);
|
|
436
|
+
}
|
|
437
|
+
.badge.secondary {
|
|
438
|
+
background: var(--secondary);
|
|
439
|
+
color: var(--secondary-foreground);
|
|
440
|
+
}
|
|
441
|
+
/* Skipped, waiting and stopped: the product's muted override. */
|
|
442
|
+
.badge.quiet {
|
|
443
|
+
background: var(--muted);
|
|
444
|
+
color: var(--muted-foreground);
|
|
445
|
+
}
|
|
446
|
+
.badge.success {
|
|
447
|
+
border-color: color-mix(in oklch, var(--success) 35%, transparent);
|
|
448
|
+
background: color-mix(in oklch, var(--success) 8%, transparent);
|
|
449
|
+
color: var(--success-ink);
|
|
450
|
+
}
|
|
451
|
+
.badge.warning {
|
|
452
|
+
border-color: color-mix(in oklch, var(--warning) 40%, transparent);
|
|
453
|
+
background: color-mix(in oklch, var(--warning) 10%, transparent);
|
|
454
|
+
color: var(--warning-ink);
|
|
455
|
+
}
|
|
456
|
+
.badge.destructive {
|
|
457
|
+
border-color: color-mix(in oklch, var(--destructive) 30%, transparent);
|
|
458
|
+
background: color-mix(in oklch, var(--destructive) 8%, transparent);
|
|
459
|
+
color: var(--destructive);
|
|
460
|
+
}
|
|
461
|
+
.badge.arc {
|
|
462
|
+
border-color: color-mix(in oklch, var(--primary) 25%, transparent);
|
|
463
|
+
background: color-mix(in oklch, var(--primary) 8%, transparent);
|
|
464
|
+
color: var(--primary);
|
|
465
|
+
}
|
|
466
|
+
/* The `.stamp` motif pressed off square, in the colour it is given. */
|
|
467
|
+
.badge.stamp {
|
|
468
|
+
padding: 0.25rem 0.75rem;
|
|
469
|
+
border: 1.25px solid currentColor;
|
|
470
|
+
background: transparent;
|
|
471
|
+
font-family: var(--font-mono);
|
|
472
|
+
font-size: 0.75rem;
|
|
473
|
+
font-weight: 400;
|
|
474
|
+
letter-spacing: 0.04em;
|
|
475
|
+
text-transform: uppercase;
|
|
476
|
+
transform: rotate(-1.5deg);
|
|
477
|
+
opacity: 0.9;
|
|
478
|
+
}
|
|
479
|
+
.bulb {
|
|
480
|
+
display: inline-block;
|
|
481
|
+
width: 0.375rem;
|
|
482
|
+
height: 0.375rem;
|
|
483
|
+
flex-shrink: 0;
|
|
484
|
+
border-radius: 9999px;
|
|
485
|
+
background: currentColor;
|
|
486
|
+
}
|
|
487
|
+
/* Ambient status loops (this and .chase) run past the 360ms ceiling on
|
|
488
|
+
purpose: they are motion.css's --animate-tally and .marquee-chase, not
|
|
489
|
+
transitions, and they stop under prefers-reduced-motion. */
|
|
490
|
+
.bulb.live {
|
|
491
|
+
animation: marquee-dot 1.6s ease-in-out infinite;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ── Button ───────────────────────────────────────────── */
|
|
495
|
+
.btn {
|
|
496
|
+
position: relative;
|
|
497
|
+
display: inline-flex;
|
|
498
|
+
height: 2rem;
|
|
499
|
+
flex-shrink: 0;
|
|
500
|
+
align-items: center;
|
|
501
|
+
justify-content: center;
|
|
502
|
+
gap: 0.375rem;
|
|
503
|
+
padding: 0 0.75rem;
|
|
504
|
+
border: 1px solid transparent;
|
|
505
|
+
border-radius: var(--radius-md);
|
|
506
|
+
font: 500 0.8125rem/1 var(--font-sans);
|
|
507
|
+
letter-spacing: -0.003em;
|
|
508
|
+
text-decoration: none;
|
|
509
|
+
white-space: nowrap;
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
transition:
|
|
512
|
+
transform var(--duration-press) var(--ease-snap),
|
|
513
|
+
background-color var(--duration-quick) ease,
|
|
514
|
+
border-color var(--duration-quick) ease,
|
|
515
|
+
color var(--duration-quick) ease,
|
|
516
|
+
box-shadow var(--duration-quick) ease;
|
|
517
|
+
}
|
|
518
|
+
.btn:has(> svg) {
|
|
519
|
+
padding-inline: 0.625rem 0.75rem;
|
|
520
|
+
}
|
|
521
|
+
/* The shared press: 1px into the page, never for a keyboard press. */
|
|
522
|
+
.btn:active:not(:focus-visible) {
|
|
523
|
+
transform: translateY(1px) scale(0.985);
|
|
524
|
+
}
|
|
525
|
+
.btn:disabled {
|
|
526
|
+
pointer-events: none;
|
|
527
|
+
cursor: default;
|
|
528
|
+
opacity: 0.45;
|
|
529
|
+
}
|
|
530
|
+
/* Pending stays readable: its label is the only word that Stop took. */
|
|
531
|
+
.btn[data-pending] {
|
|
532
|
+
cursor: default;
|
|
533
|
+
box-shadow: none;
|
|
534
|
+
color: var(--muted-foreground);
|
|
535
|
+
}
|
|
536
|
+
.btn.outline {
|
|
537
|
+
border-color: var(--border);
|
|
538
|
+
background: var(--background);
|
|
539
|
+
color: var(--foreground);
|
|
540
|
+
}
|
|
541
|
+
.btn.outline:hover {
|
|
542
|
+
border-color: color-mix(in oklch, var(--foreground) 30%, transparent);
|
|
543
|
+
background: var(--accent);
|
|
544
|
+
color: var(--accent-foreground);
|
|
545
|
+
}
|
|
546
|
+
@media (prefers-color-scheme: dark) {
|
|
547
|
+
.btn.outline {
|
|
548
|
+
background: color-mix(in oklab, var(--input) 15%, transparent);
|
|
549
|
+
}
|
|
550
|
+
.btn.outline:hover {
|
|
551
|
+
background: color-mix(in oklab, var(--input) 30%, transparent);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/* Arc: the one light. A fill lit from the top left, a specular edge,
|
|
555
|
+
a shaded foot and a short cast. Hover brings the lamp up. */
|
|
556
|
+
.btn.primary {
|
|
557
|
+
isolation: isolate;
|
|
558
|
+
overflow: hidden;
|
|
559
|
+
background-color: var(--primary);
|
|
560
|
+
color: var(--primary-foreground);
|
|
561
|
+
box-shadow: var(--button-shadow);
|
|
86
562
|
}
|
|
87
|
-
|
|
88
|
-
|
|
563
|
+
.btn.primary::before {
|
|
564
|
+
content: '';
|
|
565
|
+
position: absolute;
|
|
566
|
+
inset: 0;
|
|
567
|
+
z-index: -1;
|
|
568
|
+
border-radius: inherit;
|
|
569
|
+
pointer-events: none;
|
|
570
|
+
background-image: radial-gradient(
|
|
571
|
+
130% 80% at 0% 0%,
|
|
572
|
+
var(--button-lamp) 0%,
|
|
573
|
+
color-mix(in oklab, var(--button-lamp) 88%, transparent) 10%,
|
|
574
|
+
color-mix(in oklab, var(--button-lamp) 56%, transparent) 25%,
|
|
575
|
+
color-mix(in oklab, var(--button-lamp) 22%, transparent) 50%,
|
|
576
|
+
color-mix(in oklab, var(--button-lamp) 6%, transparent) 65%,
|
|
577
|
+
transparent 80%
|
|
578
|
+
);
|
|
579
|
+
opacity: 0.4;
|
|
580
|
+
transition: opacity var(--duration-quick) ease;
|
|
581
|
+
}
|
|
582
|
+
.btn.primary::after {
|
|
583
|
+
content: '';
|
|
584
|
+
position: absolute;
|
|
585
|
+
inset: 0;
|
|
586
|
+
border-radius: inherit;
|
|
587
|
+
pointer-events: none;
|
|
588
|
+
box-shadow:
|
|
589
|
+
inset 1px 1px 0 var(--button-specular),
|
|
590
|
+
inset -1px -1px 0 var(--button-shade),
|
|
591
|
+
inset 0 0 0 1px var(--button-rim);
|
|
592
|
+
transition: box-shadow var(--duration-quick) ease;
|
|
593
|
+
}
|
|
594
|
+
@media (hover: hover) {
|
|
595
|
+
.btn.primary:hover {
|
|
596
|
+
background-color: var(--button-fill-lit);
|
|
597
|
+
box-shadow: var(--button-shadow-lit);
|
|
598
|
+
}
|
|
599
|
+
.btn.primary:hover::before {
|
|
600
|
+
opacity: 1;
|
|
601
|
+
transition: opacity var(--duration-quick) var(--ease-snap);
|
|
602
|
+
}
|
|
603
|
+
.btn.primary:hover::after {
|
|
604
|
+
box-shadow:
|
|
605
|
+
inset 1px 1px 0 var(--button-specular-lit),
|
|
606
|
+
inset -1px -1px 0 var(--button-shade),
|
|
607
|
+
inset 0 0 0 1px var(--button-rim);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* ── Sections, groups and rows ────────────────────────── */
|
|
612
|
+
.stack > * + * {
|
|
613
|
+
margin-top: 2.5rem;
|
|
614
|
+
}
|
|
615
|
+
@media (min-width: 40rem) {
|
|
616
|
+
.stack > * + * {
|
|
617
|
+
margin-top: 3rem;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
.section > * + * {
|
|
621
|
+
margin-top: 0.75rem;
|
|
622
|
+
}
|
|
623
|
+
.section-title {
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
gap: 0.5rem;
|
|
627
|
+
font-size: 0.875rem;
|
|
89
628
|
letter-spacing: -0.025em;
|
|
90
|
-
margin: 0 0 0.25rem;
|
|
91
629
|
}
|
|
92
|
-
|
|
630
|
+
.section-title.large {
|
|
93
631
|
font-size: 1rem;
|
|
94
|
-
|
|
632
|
+
letter-spacing: -0.012em;
|
|
95
633
|
}
|
|
96
|
-
|
|
634
|
+
@media (min-width: 40rem) {
|
|
635
|
+
.section-title.large {
|
|
636
|
+
font-size: 1.125rem;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
.count {
|
|
640
|
+
font-family: var(--font-mono);
|
|
641
|
+
font-size: 0.75rem;
|
|
642
|
+
font-weight: 400;
|
|
643
|
+
letter-spacing: 0;
|
|
644
|
+
font-variant-numeric: tabular-nums;
|
|
97
645
|
color: var(--muted-foreground);
|
|
98
646
|
}
|
|
99
|
-
.
|
|
647
|
+
.section-note {
|
|
648
|
+
font-size: 0.875rem;
|
|
649
|
+
color: var(--muted-foreground);
|
|
650
|
+
}
|
|
651
|
+
/* One group, rows split by hairlines at 60%: never a card per row. */
|
|
652
|
+
.group {
|
|
653
|
+
overflow: hidden;
|
|
654
|
+
border: 1px solid var(--border);
|
|
655
|
+
border-radius: var(--radius-lg);
|
|
656
|
+
}
|
|
657
|
+
.group.card {
|
|
100
658
|
background: var(--card);
|
|
659
|
+
}
|
|
660
|
+
.group > li + li,
|
|
661
|
+
.group > a + a,
|
|
662
|
+
.group > .row + .row {
|
|
663
|
+
border-top: 1px solid color-mix(in oklch, var(--border) 60%, transparent);
|
|
664
|
+
}
|
|
665
|
+
.card {
|
|
101
666
|
border: 1px solid var(--border);
|
|
102
|
-
border-radius: var(--radius);
|
|
667
|
+
border-radius: var(--radius-lg);
|
|
668
|
+
background: var(--card);
|
|
103
669
|
}
|
|
104
|
-
.
|
|
105
|
-
|
|
670
|
+
.overline {
|
|
671
|
+
font-size: 0.6875rem;
|
|
672
|
+
font-weight: 500;
|
|
673
|
+
line-height: 1;
|
|
674
|
+
letter-spacing: 0.12em;
|
|
675
|
+
text-transform: uppercase;
|
|
676
|
+
color: var(--muted-foreground);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* Runs: a table at width, stacked on a phone. */
|
|
680
|
+
.runs-head,
|
|
681
|
+
.run-row {
|
|
106
682
|
display: grid;
|
|
107
|
-
grid-template-columns: 1fr auto;
|
|
683
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
108
684
|
gap: 0.25rem 1rem;
|
|
685
|
+
align-items: center;
|
|
109
686
|
padding: 0.75rem 1rem;
|
|
110
|
-
|
|
687
|
+
}
|
|
688
|
+
.runs-head {
|
|
689
|
+
display: none;
|
|
690
|
+
}
|
|
691
|
+
.run-row {
|
|
111
692
|
text-decoration: none;
|
|
693
|
+
transition: background-color var(--duration-quick) ease;
|
|
112
694
|
}
|
|
113
|
-
.
|
|
114
|
-
|
|
695
|
+
.run-row:hover {
|
|
696
|
+
background: color-mix(in oklch, var(--muted) 60%, transparent);
|
|
115
697
|
}
|
|
116
|
-
.
|
|
117
|
-
|
|
698
|
+
.run-name {
|
|
699
|
+
display: flex;
|
|
700
|
+
min-width: 0;
|
|
701
|
+
align-items: center;
|
|
702
|
+
gap: 0.5rem;
|
|
703
|
+
}
|
|
704
|
+
.run-name strong {
|
|
705
|
+
font-weight: 500;
|
|
706
|
+
}
|
|
707
|
+
.run-name > svg {
|
|
708
|
+
color: var(--muted-foreground);
|
|
709
|
+
}
|
|
710
|
+
.run-name > div {
|
|
711
|
+
min-width: 0;
|
|
118
712
|
}
|
|
119
|
-
.
|
|
713
|
+
.run-id {
|
|
714
|
+
font-size: 0.75rem;
|
|
715
|
+
color: var(--muted-foreground);
|
|
716
|
+
}
|
|
717
|
+
.result {
|
|
120
718
|
display: flex;
|
|
121
719
|
flex-wrap: wrap;
|
|
122
|
-
gap: 0.5rem;
|
|
123
720
|
align-items: center;
|
|
124
|
-
|
|
125
|
-
|
|
721
|
+
justify-content: flex-end;
|
|
722
|
+
gap: 0.25rem 0.5rem;
|
|
126
723
|
}
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
font-size: 12px;
|
|
132
|
-
line-height: 1.5rem;
|
|
133
|
-
border: 1px solid var(--border);
|
|
724
|
+
.tally {
|
|
725
|
+
font-family: var(--font-mono);
|
|
726
|
+
font-size: 0.75rem;
|
|
727
|
+
font-variant-numeric: tabular-nums;
|
|
134
728
|
color: var(--muted-foreground);
|
|
135
729
|
white-space: nowrap;
|
|
136
730
|
}
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
731
|
+
.facts {
|
|
732
|
+
grid-column: 1 / -1;
|
|
733
|
+
display: flex;
|
|
734
|
+
flex-wrap: wrap;
|
|
735
|
+
align-items: center;
|
|
736
|
+
gap: 0.25rem 1rem;
|
|
737
|
+
padding-left: 1.5rem;
|
|
738
|
+
font-size: 0.75rem;
|
|
739
|
+
color: var(--muted-foreground);
|
|
140
740
|
}
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
741
|
+
.fact {
|
|
742
|
+
display: inline-flex;
|
|
743
|
+
min-width: 0;
|
|
744
|
+
align-items: center;
|
|
745
|
+
gap: 0.375rem;
|
|
144
746
|
}
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
747
|
+
.fact svg.icon {
|
|
748
|
+
width: 0.875rem;
|
|
749
|
+
height: 0.875rem;
|
|
148
750
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
751
|
+
@media (min-width: 48rem) {
|
|
752
|
+
.runs-head,
|
|
753
|
+
.run-row {
|
|
754
|
+
grid-template-columns: minmax(0, 1fr) 12rem 9rem 10rem 7.5rem;
|
|
755
|
+
}
|
|
756
|
+
.runs-head {
|
|
757
|
+
display: grid;
|
|
758
|
+
height: 2.5rem;
|
|
759
|
+
padding-block: 0;
|
|
760
|
+
border-bottom: 1px solid var(--border);
|
|
761
|
+
font-size: 0.6875rem;
|
|
762
|
+
font-weight: 600;
|
|
763
|
+
letter-spacing: 0.14em;
|
|
764
|
+
text-transform: uppercase;
|
|
765
|
+
color: var(--muted-foreground);
|
|
766
|
+
}
|
|
767
|
+
.group > .runs-head + .run-row {
|
|
768
|
+
border-top: 0;
|
|
769
|
+
}
|
|
770
|
+
.result {
|
|
771
|
+
justify-content: flex-start;
|
|
772
|
+
}
|
|
773
|
+
.facts {
|
|
774
|
+
display: contents;
|
|
775
|
+
}
|
|
152
776
|
}
|
|
153
|
-
|
|
777
|
+
|
|
778
|
+
/* A check, as the run report draws it. */
|
|
779
|
+
.check {
|
|
780
|
+
padding: 0.75rem 1rem;
|
|
781
|
+
}
|
|
782
|
+
.check-head {
|
|
783
|
+
display: flex;
|
|
784
|
+
width: 100%;
|
|
785
|
+
align-items: flex-start;
|
|
786
|
+
gap: 0.75rem;
|
|
787
|
+
padding: 0;
|
|
788
|
+
border: 0;
|
|
789
|
+
background: none;
|
|
790
|
+
color: inherit;
|
|
154
791
|
font: inherit;
|
|
155
|
-
|
|
156
|
-
|
|
792
|
+
text-align: left;
|
|
793
|
+
}
|
|
794
|
+
button.check-head {
|
|
795
|
+
cursor: pointer;
|
|
796
|
+
}
|
|
797
|
+
.check-head > svg.icon {
|
|
798
|
+
margin-top: 0.125rem;
|
|
799
|
+
}
|
|
800
|
+
.check-body {
|
|
801
|
+
display: block;
|
|
802
|
+
min-width: 0;
|
|
803
|
+
flex: 1;
|
|
804
|
+
}
|
|
805
|
+
.check-desc {
|
|
806
|
+
display: block;
|
|
807
|
+
font-weight: 500;
|
|
808
|
+
line-height: 1.375;
|
|
809
|
+
}
|
|
810
|
+
.check.waiting .check-desc {
|
|
811
|
+
font-weight: 400;
|
|
812
|
+
color: var(--muted-foreground);
|
|
813
|
+
}
|
|
814
|
+
.check-meta {
|
|
815
|
+
display: flex;
|
|
816
|
+
flex-wrap: wrap;
|
|
817
|
+
align-items: center;
|
|
818
|
+
gap: 0.375rem;
|
|
819
|
+
margin-top: 0.375rem;
|
|
820
|
+
}
|
|
821
|
+
.check-meta code {
|
|
822
|
+
font-size: 0.6875rem;
|
|
823
|
+
color: var(--muted-foreground);
|
|
824
|
+
}
|
|
825
|
+
.check-meta .badge {
|
|
826
|
+
font-size: 0.625rem;
|
|
827
|
+
}
|
|
828
|
+
.check-side {
|
|
829
|
+
display: flex;
|
|
830
|
+
flex-shrink: 0;
|
|
831
|
+
align-items: center;
|
|
832
|
+
gap: 0.5rem;
|
|
833
|
+
}
|
|
834
|
+
.thumb {
|
|
835
|
+
display: none;
|
|
836
|
+
width: 3rem;
|
|
837
|
+
height: 2rem;
|
|
157
838
|
border: 1px solid var(--border);
|
|
158
|
-
|
|
839
|
+
border-radius: var(--radius-md);
|
|
840
|
+
object-fit: cover;
|
|
841
|
+
object-position: top;
|
|
842
|
+
background: var(--muted);
|
|
843
|
+
}
|
|
844
|
+
@media (min-width: 40rem) {
|
|
845
|
+
.thumb {
|
|
846
|
+
display: block;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
.chevron {
|
|
850
|
+
color: var(--muted-foreground);
|
|
851
|
+
}
|
|
852
|
+
.open .chevron {
|
|
853
|
+
transform: rotate(180deg);
|
|
854
|
+
}
|
|
855
|
+
.check-detail {
|
|
856
|
+
display: grid;
|
|
857
|
+
gap: 0.75rem;
|
|
858
|
+
margin-top: 0.5rem;
|
|
859
|
+
padding-left: 1.75rem;
|
|
860
|
+
}
|
|
861
|
+
.evidence {
|
|
862
|
+
color: var(--muted-foreground);
|
|
863
|
+
}
|
|
864
|
+
.evidence.observed {
|
|
159
865
|
color: var(--foreground);
|
|
160
|
-
cursor: pointer;
|
|
161
866
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
867
|
+
.cited {
|
|
868
|
+
font-size: 0.75rem;
|
|
869
|
+
color: var(--muted-foreground);
|
|
870
|
+
overflow-wrap: anywhere;
|
|
871
|
+
}
|
|
872
|
+
.shot {
|
|
873
|
+
display: block;
|
|
874
|
+
width: fit-content;
|
|
875
|
+
max-width: min(100%, 36rem);
|
|
876
|
+
overflow: hidden;
|
|
877
|
+
border: 1px solid var(--border);
|
|
878
|
+
border-radius: var(--radius-md);
|
|
879
|
+
background: var(--muted);
|
|
880
|
+
}
|
|
881
|
+
.shot img {
|
|
882
|
+
display: block;
|
|
883
|
+
max-width: 100%;
|
|
884
|
+
max-height: 20rem;
|
|
885
|
+
width: auto;
|
|
886
|
+
object-fit: contain;
|
|
887
|
+
}
|
|
888
|
+
.tone-passed {
|
|
889
|
+
color: var(--success-ink);
|
|
890
|
+
}
|
|
891
|
+
.tone-failed {
|
|
892
|
+
color: var(--destructive);
|
|
893
|
+
}
|
|
894
|
+
.tone-error {
|
|
895
|
+
color: var(--warning-ink);
|
|
896
|
+
}
|
|
897
|
+
.tone-informational {
|
|
898
|
+
color: var(--primary);
|
|
166
899
|
}
|
|
167
|
-
|
|
900
|
+
.tone-skipped,
|
|
901
|
+
.tone-waiting {
|
|
902
|
+
color: var(--muted-foreground);
|
|
903
|
+
}
|
|
904
|
+
.tone-waiting {
|
|
168
905
|
opacity: 0.6;
|
|
169
|
-
cursor: default;
|
|
170
906
|
}
|
|
171
|
-
|
|
907
|
+
|
|
908
|
+
/* The run's score, as RunSummary draws it. */
|
|
909
|
+
.summary {
|
|
910
|
+
padding: 1rem;
|
|
911
|
+
}
|
|
912
|
+
@media (min-width: 40rem) {
|
|
913
|
+
.summary {
|
|
914
|
+
padding: 1.5rem;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
.summary-top {
|
|
918
|
+
display: flex;
|
|
919
|
+
flex-wrap: wrap;
|
|
920
|
+
align-items: flex-end;
|
|
921
|
+
justify-content: space-between;
|
|
922
|
+
gap: 0.75rem 1.5rem;
|
|
923
|
+
}
|
|
924
|
+
.figure {
|
|
925
|
+
margin-top: 0.25rem;
|
|
926
|
+
font-family: var(--font-mono);
|
|
927
|
+
font-size: 2.25rem;
|
|
928
|
+
line-height: 1;
|
|
929
|
+
letter-spacing: -0.012em;
|
|
930
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
931
|
+
}
|
|
932
|
+
@media (min-width: 40rem) {
|
|
933
|
+
.figure {
|
|
934
|
+
font-size: 3rem;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
.figure .of {
|
|
938
|
+
color: var(--muted-foreground);
|
|
939
|
+
}
|
|
940
|
+
.summary-line {
|
|
941
|
+
margin-top: 0.5rem;
|
|
942
|
+
font-size: 0.875rem;
|
|
943
|
+
color: var(--muted-foreground);
|
|
944
|
+
}
|
|
945
|
+
.verdict {
|
|
172
946
|
display: flex;
|
|
947
|
+
flex-wrap: wrap;
|
|
948
|
+
align-items: center;
|
|
949
|
+
gap: 0.5rem;
|
|
950
|
+
}
|
|
951
|
+
.meter {
|
|
952
|
+
display: flex;
|
|
953
|
+
height: 0.5rem;
|
|
954
|
+
margin-top: 1.25rem;
|
|
955
|
+
overflow: hidden;
|
|
956
|
+
border-radius: 9999px;
|
|
957
|
+
background: var(--muted);
|
|
958
|
+
}
|
|
959
|
+
.meter .passed {
|
|
960
|
+
background: var(--success);
|
|
961
|
+
}
|
|
962
|
+
.meter .failed {
|
|
963
|
+
background: var(--destructive);
|
|
964
|
+
}
|
|
965
|
+
.meter .error {
|
|
966
|
+
background: var(--warning);
|
|
967
|
+
}
|
|
968
|
+
.counts {
|
|
969
|
+
display: flex;
|
|
970
|
+
flex-wrap: wrap;
|
|
173
971
|
gap: 0.5rem;
|
|
174
972
|
margin-top: 1rem;
|
|
175
973
|
}
|
|
176
|
-
|
|
974
|
+
|
|
975
|
+
/* Actor speech, verbatim: the sans, upright, behind a rule. */
|
|
976
|
+
.quote {
|
|
977
|
+
padding-left: 0.75rem;
|
|
978
|
+
border-left: 2px solid var(--border);
|
|
979
|
+
overflow-wrap: anywhere;
|
|
980
|
+
}
|
|
981
|
+
.notice {
|
|
982
|
+
display: flex;
|
|
983
|
+
align-items: flex-start;
|
|
984
|
+
gap: 0.5rem;
|
|
985
|
+
padding: 0.75rem;
|
|
986
|
+
border: 1px solid var(--border);
|
|
987
|
+
border-radius: var(--radius-lg);
|
|
988
|
+
font-size: 0.875rem;
|
|
989
|
+
overflow-wrap: anywhere;
|
|
990
|
+
}
|
|
991
|
+
.notice > svg.icon {
|
|
992
|
+
margin-top: 0.125rem;
|
|
993
|
+
}
|
|
994
|
+
.notice.destructive {
|
|
995
|
+
border-color: color-mix(in oklch, var(--destructive) 25%, transparent);
|
|
996
|
+
background: color-mix(in oklch, var(--destructive) 5%, transparent);
|
|
997
|
+
color: var(--destructive);
|
|
998
|
+
}
|
|
999
|
+
.notice.warning {
|
|
1000
|
+
border-color: color-mix(in oklch, var(--warning) 40%, transparent);
|
|
1001
|
+
background: color-mix(in oklch, var(--warning) 10%, transparent);
|
|
1002
|
+
color: var(--warning-ink);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/* A run in progress. */
|
|
1006
|
+
.live-card {
|
|
177
1007
|
display: grid;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
align-items: start;
|
|
1008
|
+
gap: 0.75rem;
|
|
1009
|
+
padding: 1rem;
|
|
181
1010
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
1011
|
+
.live-head {
|
|
1012
|
+
display: flex;
|
|
1013
|
+
flex-wrap: wrap;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
justify-content: space-between;
|
|
1016
|
+
gap: 0.5rem 0.75rem;
|
|
1017
|
+
}
|
|
1018
|
+
.live-step {
|
|
1019
|
+
display: flex;
|
|
1020
|
+
min-width: 0;
|
|
1021
|
+
flex: 1 1 12rem;
|
|
1022
|
+
align-items: center;
|
|
1023
|
+
gap: 0.5rem;
|
|
1024
|
+
font-size: 0.875rem;
|
|
1025
|
+
color: var(--muted-foreground);
|
|
1026
|
+
}
|
|
1027
|
+
.live-side {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
flex-shrink: 0;
|
|
1030
|
+
flex-wrap: wrap;
|
|
1031
|
+
align-items: center;
|
|
1032
|
+
justify-content: flex-end;
|
|
1033
|
+
gap: 0.5rem 0.75rem;
|
|
1034
|
+
}
|
|
1035
|
+
.stop-error {
|
|
1036
|
+
flex-basis: 100%;
|
|
1037
|
+
font-size: 0.75rem;
|
|
1038
|
+
color: var(--destructive);
|
|
1039
|
+
text-align: right;
|
|
1040
|
+
}
|
|
1041
|
+
.chase {
|
|
1042
|
+
display: inline-flex;
|
|
1043
|
+
flex-shrink: 0;
|
|
1044
|
+
align-items: center;
|
|
1045
|
+
gap: 0.3em;
|
|
1046
|
+
}
|
|
1047
|
+
.chase > span {
|
|
1048
|
+
display: block;
|
|
1049
|
+
width: 0.4em;
|
|
1050
|
+
height: 0.4em;
|
|
1051
|
+
border-radius: 9999px;
|
|
1052
|
+
background: currentColor;
|
|
1053
|
+
animation: sd-chase 1.1s var(--ease-glide) infinite both;
|
|
1054
|
+
animation-delay: calc(var(--bulb, 0) * 110ms);
|
|
1055
|
+
}
|
|
1056
|
+
.chase > span:nth-child(2) {
|
|
1057
|
+
--bulb: 1;
|
|
1058
|
+
}
|
|
1059
|
+
.chase > span:nth-child(3) {
|
|
1060
|
+
--bulb: 2;
|
|
1061
|
+
}
|
|
1062
|
+
.chase > span:nth-child(4) {
|
|
1063
|
+
--bulb: 3;
|
|
1064
|
+
}
|
|
1065
|
+
.chase > span:nth-child(5) {
|
|
1066
|
+
--bulb: 4;
|
|
1067
|
+
}
|
|
1068
|
+
.progress {
|
|
1069
|
+
height: 2px;
|
|
1070
|
+
overflow: hidden;
|
|
1071
|
+
border-radius: 9999px;
|
|
1072
|
+
background: var(--muted);
|
|
1073
|
+
}
|
|
1074
|
+
.progress > span {
|
|
1075
|
+
display: block;
|
|
1076
|
+
height: 100%;
|
|
1077
|
+
background: var(--primary);
|
|
186
1078
|
}
|
|
187
1079
|
.frame {
|
|
188
|
-
width: 100%;
|
|
189
1080
|
display: block;
|
|
190
|
-
|
|
1081
|
+
width: 100%;
|
|
1082
|
+
max-height: 32rem;
|
|
1083
|
+
object-fit: contain;
|
|
1084
|
+
object-position: top;
|
|
191
1085
|
border: 1px solid var(--border);
|
|
1086
|
+
border-radius: var(--radius-md);
|
|
192
1087
|
background: var(--muted);
|
|
193
1088
|
}
|
|
194
|
-
|
|
1089
|
+
|
|
1090
|
+
/* Activity: the tool calls, in order. */
|
|
1091
|
+
.activity li {
|
|
1092
|
+
display: grid;
|
|
1093
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1094
|
+
gap: 0.125rem 0.75rem;
|
|
1095
|
+
padding: 0.5rem 1rem;
|
|
1096
|
+
font-size: 0.8125rem;
|
|
1097
|
+
}
|
|
1098
|
+
.activity time {
|
|
1099
|
+
font-family: var(--font-mono);
|
|
1100
|
+
font-size: 0.75rem;
|
|
1101
|
+
font-variant-numeric: tabular-nums;
|
|
1102
|
+
color: var(--muted-foreground);
|
|
1103
|
+
white-space: nowrap;
|
|
1104
|
+
}
|
|
1105
|
+
.activity code {
|
|
1106
|
+
font-size: 0.75rem;
|
|
1107
|
+
}
|
|
1108
|
+
.activity .text {
|
|
1109
|
+
grid-column: 1 / -1;
|
|
1110
|
+
white-space: pre-wrap;
|
|
1111
|
+
overflow-wrap: anywhere;
|
|
1112
|
+
color: var(--muted-foreground);
|
|
1113
|
+
}
|
|
1114
|
+
@media (min-width: 40rem) {
|
|
1115
|
+
.activity li {
|
|
1116
|
+
grid-template-columns: 5.5rem 8rem minmax(0, 1fr);
|
|
1117
|
+
}
|
|
1118
|
+
.activity .text {
|
|
1119
|
+
grid-column: auto;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/* Definitions. */
|
|
1124
|
+
.def {
|
|
1125
|
+
display: grid;
|
|
1126
|
+
gap: 0.5rem;
|
|
195
1127
|
padding: 1rem;
|
|
196
|
-
border-top: 1px solid var(--border);
|
|
197
1128
|
}
|
|
198
|
-
.
|
|
199
|
-
|
|
1129
|
+
.def-head {
|
|
1130
|
+
display: flex;
|
|
1131
|
+
flex-wrap: wrap;
|
|
1132
|
+
align-items: center;
|
|
1133
|
+
justify-content: space-between;
|
|
1134
|
+
gap: 0.5rem;
|
|
200
1135
|
}
|
|
201
|
-
.
|
|
202
|
-
all: unset;
|
|
1136
|
+
.def-name {
|
|
203
1137
|
display: flex;
|
|
1138
|
+
min-width: 0;
|
|
1139
|
+
align-items: center;
|
|
204
1140
|
gap: 0.5rem;
|
|
1141
|
+
font-weight: 500;
|
|
1142
|
+
}
|
|
1143
|
+
.def-name > svg {
|
|
1144
|
+
color: var(--muted-foreground);
|
|
1145
|
+
}
|
|
1146
|
+
.def-facts {
|
|
1147
|
+
display: flex;
|
|
1148
|
+
flex-wrap: wrap;
|
|
1149
|
+
align-items: center;
|
|
1150
|
+
gap: 0.25rem 1rem;
|
|
1151
|
+
padding-left: 1.5rem;
|
|
1152
|
+
font-size: 0.75rem;
|
|
1153
|
+
color: var(--muted-foreground);
|
|
1154
|
+
overflow-wrap: anywhere;
|
|
1155
|
+
}
|
|
1156
|
+
.def-body {
|
|
1157
|
+
padding-left: 1.5rem;
|
|
1158
|
+
color: var(--muted-foreground);
|
|
1159
|
+
}
|
|
1160
|
+
.def-checks {
|
|
1161
|
+
margin-left: 1.5rem;
|
|
1162
|
+
margin-top: 0.25rem;
|
|
1163
|
+
border-top: 1px solid color-mix(in oklch, var(--border) 60%, transparent);
|
|
1164
|
+
}
|
|
1165
|
+
.def-checks li {
|
|
1166
|
+
display: flex;
|
|
1167
|
+
flex-wrap: wrap;
|
|
205
1168
|
align-items: baseline;
|
|
206
|
-
|
|
1169
|
+
gap: 0.25rem 0.5rem;
|
|
1170
|
+
padding-block: 0.5rem;
|
|
1171
|
+
border-bottom: 1px solid color-mix(in oklch, var(--border) 60%, transparent);
|
|
207
1172
|
}
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
max-height: 20rem;
|
|
212
|
-
border-radius: calc(var(--radius) - 2px);
|
|
213
|
-
border: 1px solid var(--border);
|
|
1173
|
+
.def-checks li:last-child {
|
|
1174
|
+
border-bottom: 0;
|
|
1175
|
+
padding-bottom: 0;
|
|
214
1176
|
}
|
|
215
|
-
code
|
|
216
|
-
|
|
1177
|
+
.def-checks code {
|
|
1178
|
+
font-size: 0.6875rem;
|
|
1179
|
+
color: var(--muted-foreground);
|
|
1180
|
+
}
|
|
1181
|
+
.error-text {
|
|
217
1182
|
font-family: var(--font-mono);
|
|
218
|
-
font-size:
|
|
1183
|
+
font-size: 0.75rem;
|
|
1184
|
+
color: var(--muted-foreground);
|
|
1185
|
+
white-space: pre-wrap;
|
|
1186
|
+
overflow-wrap: anywhere;
|
|
219
1187
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1188
|
+
|
|
1189
|
+
/* EmptyState: a framed medallion, a display title, one action. */
|
|
1190
|
+
.empty {
|
|
1191
|
+
display: flex;
|
|
1192
|
+
min-height: 52vh;
|
|
1193
|
+
align-items: center;
|
|
1194
|
+
justify-content: center;
|
|
1195
|
+
padding-inline: 1.5rem;
|
|
1196
|
+
text-align: center;
|
|
225
1197
|
}
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
1198
|
+
.frame-marks {
|
|
1199
|
+
position: relative;
|
|
1200
|
+
width: 100%;
|
|
1201
|
+
max-width: 28rem;
|
|
1202
|
+
padding: 2.5rem 1.5rem;
|
|
1203
|
+
}
|
|
1204
|
+
.frame-marks::after {
|
|
1205
|
+
content: '';
|
|
1206
|
+
position: absolute;
|
|
1207
|
+
inset: 0.75rem;
|
|
1208
|
+
pointer-events: none;
|
|
1209
|
+
background:
|
|
1210
|
+
linear-gradient(currentColor, currentColor) top left / 1.25rem 1.25px no-repeat,
|
|
1211
|
+
linear-gradient(currentColor, currentColor) top left / 1.25px 1.25rem no-repeat,
|
|
1212
|
+
linear-gradient(currentColor, currentColor) bottom right / 1.25rem 1.25px no-repeat,
|
|
1213
|
+
linear-gradient(currentColor, currentColor) bottom right / 1.25px 1.25rem no-repeat;
|
|
1214
|
+
color: color-mix(in oklch, var(--muted-foreground) 45%, transparent);
|
|
1215
|
+
}
|
|
1216
|
+
.medallion {
|
|
1217
|
+
display: flex;
|
|
1218
|
+
width: 3.5rem;
|
|
1219
|
+
height: 3.5rem;
|
|
1220
|
+
align-items: center;
|
|
1221
|
+
justify-content: center;
|
|
1222
|
+
margin: 0 auto 1.25rem;
|
|
1223
|
+
border: 1px solid var(--border);
|
|
1224
|
+
border-radius: 9999px;
|
|
1225
|
+
background: color-mix(in oklch, var(--muted) 60%, transparent);
|
|
1226
|
+
color: var(--muted-foreground);
|
|
1227
|
+
}
|
|
1228
|
+
.medallion svg.icon {
|
|
1229
|
+
width: 1.5rem;
|
|
1230
|
+
height: 1.5rem;
|
|
1231
|
+
}
|
|
1232
|
+
.empty h2 {
|
|
1233
|
+
font-family: var(--font-display);
|
|
1234
|
+
font-size: 1.125rem;
|
|
1235
|
+
letter-spacing: -0.035em;
|
|
1236
|
+
line-height: 1.1;
|
|
1237
|
+
}
|
|
1238
|
+
.empty p {
|
|
1239
|
+
max-width: 24rem;
|
|
1240
|
+
margin: 0.5rem auto 0;
|
|
1241
|
+
font-size: 0.875rem;
|
|
1242
|
+
color: var(--muted-foreground);
|
|
1243
|
+
}
|
|
1244
|
+
.empty code {
|
|
1245
|
+
font-size: 0.8125rem;
|
|
1246
|
+
color: var(--foreground);
|
|
1247
|
+
}
|
|
1248
|
+
.inline-empty {
|
|
1249
|
+
display: flex;
|
|
1250
|
+
flex-direction: column;
|
|
1251
|
+
align-items: center;
|
|
229
1252
|
gap: 0.75rem;
|
|
230
|
-
padding:
|
|
231
|
-
|
|
1253
|
+
padding: 3rem 1rem;
|
|
1254
|
+
text-align: center;
|
|
1255
|
+
font-size: 0.875rem;
|
|
1256
|
+
color: var(--muted-foreground);
|
|
1257
|
+
}
|
|
1258
|
+
.inline-empty svg.icon {
|
|
1259
|
+
width: 2.5rem;
|
|
1260
|
+
height: 2.5rem;
|
|
1261
|
+
opacity: 0.5;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/* ── Motion (motion.css tokens and keyframes) ─────────── */
|
|
1265
|
+
.animate-page-in {
|
|
1266
|
+
animation: sd-rise var(--duration-slow) var(--ease-drawer) both;
|
|
1267
|
+
}
|
|
1268
|
+
.animate-rise {
|
|
1269
|
+
animation: sd-rise var(--duration-slow) var(--ease-snap) backwards;
|
|
1270
|
+
}
|
|
1271
|
+
.animate-stamp {
|
|
1272
|
+
animation: sd-stamp var(--duration-slow) var(--ease-snap) backwards;
|
|
1273
|
+
}
|
|
1274
|
+
@keyframes sd-rise {
|
|
1275
|
+
from {
|
|
1276
|
+
opacity: 0;
|
|
1277
|
+
transform: translateY(0.5rem);
|
|
1278
|
+
}
|
|
1279
|
+
to {
|
|
1280
|
+
opacity: 1;
|
|
1281
|
+
transform: none;
|
|
1282
|
+
}
|
|
232
1283
|
}
|
|
233
|
-
|
|
234
|
-
|
|
1284
|
+
@keyframes sd-stamp {
|
|
1285
|
+
0% {
|
|
1286
|
+
opacity: 0;
|
|
1287
|
+
transform: rotate(-6deg) scale(1.35);
|
|
1288
|
+
}
|
|
1289
|
+
60% {
|
|
1290
|
+
opacity: 0.9;
|
|
1291
|
+
transform: rotate(-1.5deg) scale(0.97);
|
|
1292
|
+
}
|
|
1293
|
+
100% {
|
|
1294
|
+
opacity: 0.9;
|
|
1295
|
+
transform: rotate(-1.5deg) scale(1);
|
|
1296
|
+
}
|
|
235
1297
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
1298
|
+
@keyframes sd-chase {
|
|
1299
|
+
0%,
|
|
1300
|
+
100% {
|
|
1301
|
+
opacity: 0.18;
|
|
1302
|
+
transform: scale(0.8);
|
|
1303
|
+
}
|
|
1304
|
+
20% {
|
|
1305
|
+
opacity: 1;
|
|
1306
|
+
transform: none;
|
|
1307
|
+
}
|
|
239
1308
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
1309
|
+
@keyframes marquee-dot {
|
|
1310
|
+
0%,
|
|
1311
|
+
100% {
|
|
1312
|
+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 22%, transparent);
|
|
1313
|
+
opacity: 1;
|
|
1314
|
+
}
|
|
1315
|
+
50% {
|
|
1316
|
+
box-shadow: 0 0 0 6px color-mix(in oklch, var(--primary) 0%, transparent);
|
|
1317
|
+
opacity: 0.7;
|
|
1318
|
+
}
|
|
243
1319
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
1320
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1321
|
+
*,
|
|
1322
|
+
*::before,
|
|
1323
|
+
*::after {
|
|
1324
|
+
animation-duration: 0.01ms !important;
|
|
1325
|
+
animation-iteration-count: 1 !important;
|
|
1326
|
+
animation-delay: 0s !important;
|
|
1327
|
+
transition-duration: 0.01ms !important;
|
|
1328
|
+
}
|
|
1329
|
+
.chase > span {
|
|
1330
|
+
opacity: 0.6;
|
|
1331
|
+
}
|
|
247
1332
|
}
|
|
248
1333
|
</style>
|
|
249
1334
|
</head>
|
|
250
1335
|
<body>
|
|
251
|
-
<header>
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
1336
|
+
<header class="bar">
|
|
1337
|
+
<div class="container">
|
|
1338
|
+
<span class="product">Stunt Double Local</span>
|
|
1339
|
+
<nav aria-label="Viewer">
|
|
1340
|
+
<a href="/" data-nav>Runs</a>
|
|
1341
|
+
<a href="/definitions" data-nav>Checklists and actors</a>
|
|
1342
|
+
</nav>
|
|
1343
|
+
</div>
|
|
257
1344
|
</header>
|
|
258
|
-
<main
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
1345
|
+
<main>
|
|
1346
|
+
<div class="container" id="app"></div>
|
|
1347
|
+
<p class="sr-only" id="status" role="status"></p>
|
|
1348
|
+
</main>
|
|
1349
|
+
<footer>
|
|
1350
|
+
<div class="container">
|
|
1351
|
+
<p>
|
|
1352
|
+
Local runs use your own Claude plan and a browser on this machine, one at a time while
|
|
1353
|
+
your session is open. The cloud runs on schedules and in parallel, records every run and
|
|
1354
|
+
shares reports with your team. <a href="https://stuntdouble.io/pricing">See plans</a>
|
|
1355
|
+
</p>
|
|
1356
|
+
</div>
|
|
263
1357
|
</footer>
|
|
264
1358
|
<script>
|
|
265
1359
|
// Every value from a file is set as text, never as markup: verdicts and
|
|
@@ -278,42 +1372,192 @@
|
|
|
278
1372
|
return el;
|
|
279
1373
|
}
|
|
280
1374
|
|
|
1375
|
+
// Continuity icons from StuntKit (@stunt-double/icons, svg/*.svg): the
|
|
1376
|
+
// drawing inside each 24 unit frame. Constant markup written here, never data.
|
|
1377
|
+
const ICONS = {
|
|
1378
|
+
actor:
|
|
1379
|
+
'<path d="M3 8V3h5"/><path d="M16 3h5v5"/><path d="M21 16v5h-5"/><path d="M8 21H3v-5"/><circle cx="12" cy="9.5" r="2.5"/><path d="M7.5 17.5a4.5 4.5 0 0 1 9 0"/>',
|
|
1380
|
+
'alert-triangle':
|
|
1381
|
+
'<path d="M12 3.5l9.5 16.5h-19z"/><path d="M12 9.5v4.5"/><rect x="10.75" y="16" width="2.5" height="2.5" fill="currentColor" stroke="none"/>',
|
|
1382
|
+
'arrow-left': '<path d="M20 12H4"/><path d="M10 6l-6 6 6 6"/>',
|
|
1383
|
+
calendar:
|
|
1384
|
+
'<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18"/><path d="M8 3v4"/><path d="M16 3v4"/>',
|
|
1385
|
+
'check-circle': '<circle cx="12" cy="12" r="9"/><path d="M8 12l3 3 5-5"/>',
|
|
1386
|
+
checklist:
|
|
1387
|
+
'<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8.5l1.5 1.5 3-3"/><path d="M14.5 9h2"/><rect x="8" y="13.5" width="3" height="3"/><path d="M14.5 15h2"/>',
|
|
1388
|
+
'chevron-down': '<path d="M5 9.5l7 7 7-7"/>',
|
|
1389
|
+
'circle-dashed':
|
|
1390
|
+
'<path d="M13.1 3.07A9 9 0 0 1 17.54 4.91"/><path d="M19.09 6.46A9 9 0 0 1 20.93 10.9"/><path d="M20.93 13.1A9 9 0 0 1 19.09 17.54"/><path d="M17.54 19.09A9 9 0 0 1 13.1 20.93"/><path d="M10.9 20.93A9 9 0 0 1 6.46 19.09"/><path d="M4.91 17.54A9 9 0 0 1 3.07 13.1"/><path d="M3.07 10.9A9 9 0 0 1 4.91 6.46"/><path d="M6.46 4.91A9 9 0 0 1 10.9 3.07"/>',
|
|
1391
|
+
copy: '<rect x="8" y="8" width="13" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/>',
|
|
1392
|
+
'external-link':
|
|
1393
|
+
'<path d="M10 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4"/><path d="M14 4h6v6"/><path d="M20 4l-9 9"/>',
|
|
1394
|
+
'git-branch':
|
|
1395
|
+
'<path d="M6 3v12.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="6" r="2.5"/><path d="M18 8.5A9.5 9.5 0 0 1 8.5 18"/>',
|
|
1396
|
+
globe:
|
|
1397
|
+
'<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z"/>',
|
|
1398
|
+
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5.5"/><rect x="10.75" y="6.75" width="2.5" height="2.5" fill="currentColor" stroke="none"/>',
|
|
1399
|
+
knowledge:
|
|
1400
|
+
'<rect x="6" y="3" width="14" height="18" rx="2"/><path d="M3.5 7.5h5"/><path d="M3.5 12h5"/><path d="M3.5 16.5h5"/><path d="M12 8h5"/><path d="M12 12h5"/>',
|
|
1401
|
+
'minus-circle': '<circle cx="12" cy="12" r="9"/><path d="M8 12h8"/>',
|
|
1402
|
+
monitor:
|
|
1403
|
+
'<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M12 16v4"/><path d="M8 20h8"/>',
|
|
1404
|
+
'octagon-x':
|
|
1405
|
+
'<path d="M8.25 3h7.5L21 8.25v7.5L15.75 21h-7.5L3 15.75v-7.5z"/><path d="M9 9l6 6"/><path d="M15 9l-6 6"/>',
|
|
1406
|
+
pending:
|
|
1407
|
+
'<rect x="3.75" y="10.75" width="2.5" height="2.5" fill="currentColor" stroke="none"/><rect x="10.75" y="10.75" width="2.5" height="2.5" fill="currentColor" stroke="none"/><rect x="17.75" y="10.75" width="2.5" height="2.5" fill="currentColor" stroke="none" opacity="0.35"/>',
|
|
1408
|
+
run: '<path d="M9 4.5l12 7.5-12 7.5z"/><path d="M3 9h3"/><path d="M3 15h3"/>',
|
|
1409
|
+
scale:
|
|
1410
|
+
'<path d="M12 4v17"/><path d="M8 21h8"/><path d="M5 7h14"/><path d="M5 7l-2.5 6h5z"/><path d="M19 7l-2.5 6h5z"/><path d="M2.5 13a2.5 2.5 0 0 0 5 0"/><path d="M16.5 13a2.5 2.5 0 0 0 5 0"/>',
|
|
1411
|
+
server:
|
|
1412
|
+
'<rect x="3" y="3" width="18" height="8" rx="2"/><rect x="3" y="13" width="18" height="8" rx="2"/><rect x="5.75" y="5.75" width="2.5" height="2.5" fill="currentColor" stroke="none"/><rect x="5.75" y="15.75" width="2.5" height="2.5" fill="currentColor" stroke="none"/>',
|
|
1413
|
+
stop: '<rect x="5" y="5" width="14" height="14"/>',
|
|
1414
|
+
timer:
|
|
1415
|
+
'<circle cx="12" cy="13.5" r="7.5"/><path d="M12 13.5l3-3"/><path d="M10 3h4"/><path d="M12 3v3"/>',
|
|
1416
|
+
'x-circle': '<circle cx="12" cy="12" r="9"/><path d="M9 9l6 6"/><path d="M15 9l-6 6"/>',
|
|
1417
|
+
};
|
|
1418
|
+
function icon(name, className) {
|
|
1419
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1420
|
+
for (const [key, value] of Object.entries({
|
|
1421
|
+
viewBox: '0 0 24 24',
|
|
1422
|
+
fill: 'none',
|
|
1423
|
+
stroke: 'currentColor',
|
|
1424
|
+
'stroke-width': '1.5',
|
|
1425
|
+
'stroke-linecap': 'square',
|
|
1426
|
+
'stroke-linejoin': 'miter',
|
|
1427
|
+
'aria-hidden': 'true',
|
|
1428
|
+
class: 'icon' + (className ? ' ' + className : ''),
|
|
1429
|
+
})) {
|
|
1430
|
+
svg.setAttribute(key, value);
|
|
1431
|
+
}
|
|
1432
|
+
svg.innerHTML = ICONS[name];
|
|
1433
|
+
return svg;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
281
1436
|
const app = document.getElementById('app');
|
|
282
|
-
const when = (iso) => (iso ? new Date(iso).toLocaleString() : '');
|
|
283
1437
|
const fileUrl = (path) => '/files/' + path.split('/').map(encodeURIComponent).join('/');
|
|
284
1438
|
// A committed run.json is someone else's file: only web addresses become links.
|
|
285
1439
|
const webUrl = (url) => (/^https?:\/\//i.test(url || '') ? url : null);
|
|
1440
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
1441
|
+
|
|
1442
|
+
// Times in the reader's locale and zone, the way the product's
|
|
1443
|
+
// TimeDisplay and RelativeTime render them.
|
|
1444
|
+
const dateTime = new Intl.DateTimeFormat(undefined, {
|
|
1445
|
+
dateStyle: 'medium',
|
|
1446
|
+
timeStyle: 'short',
|
|
1447
|
+
});
|
|
1448
|
+
const clock = new Intl.DateTimeFormat(undefined, {
|
|
1449
|
+
hour: '2-digit',
|
|
1450
|
+
minute: '2-digit',
|
|
1451
|
+
second: '2-digit',
|
|
1452
|
+
});
|
|
1453
|
+
const relative = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
|
1454
|
+
const DIVISIONS = [
|
|
1455
|
+
[60, 'seconds'],
|
|
1456
|
+
[60, 'minutes'],
|
|
1457
|
+
[24, 'hours'],
|
|
1458
|
+
[7, 'days'],
|
|
1459
|
+
[4.34524, 'weeks'],
|
|
1460
|
+
[12, 'months'],
|
|
1461
|
+
[Infinity, 'years'],
|
|
1462
|
+
];
|
|
1463
|
+
function ago(date) {
|
|
1464
|
+
let amount = (date.getTime() - Date.now()) / 1000;
|
|
1465
|
+
if (Math.abs(amount) < 45) return 'just now';
|
|
1466
|
+
for (const [size, unit] of DIVISIONS) {
|
|
1467
|
+
if (Math.abs(amount) < size) return relative.format(Math.round(amount), unit);
|
|
1468
|
+
amount /= size;
|
|
1469
|
+
}
|
|
1470
|
+
return '';
|
|
1471
|
+
}
|
|
1472
|
+
function time(iso, style) {
|
|
1473
|
+
const date = new Date(iso);
|
|
1474
|
+
if (!iso || Number.isNaN(date.getTime())) return null;
|
|
1475
|
+
const exact = dateTime.format(date);
|
|
1476
|
+
if (style === 'relative') return h('time', { datetime: iso, title: exact }, ago(date));
|
|
1477
|
+
return h('time', { datetime: iso }, style === 'clock' ? clock.format(date) : exact);
|
|
1478
|
+
}
|
|
1479
|
+
function took(start, end) {
|
|
1480
|
+
const seconds = Math.round((new Date(end) - new Date(start)) / 1000);
|
|
1481
|
+
if (!(seconds >= 0)) return null;
|
|
1482
|
+
if (seconds < 60) return seconds + 's';
|
|
1483
|
+
const minutes = Math.floor(seconds / 60);
|
|
1484
|
+
if (minutes < 60) return minutes + 'm ' + pad(seconds % 60) + 's';
|
|
1485
|
+
return Math.floor(minutes / 60) + 'h ' + pad(minutes % 60) + 'm';
|
|
1486
|
+
}
|
|
286
1487
|
|
|
287
|
-
|
|
288
|
-
|
|
1488
|
+
// The run's one headline badge: its verdict once it has one, otherwise
|
|
1489
|
+
// where the run got to.
|
|
1490
|
+
function runState(run) {
|
|
1491
|
+
if (run.status === 'running') return { label: 'Running', tone: 'arc', live: true };
|
|
289
1492
|
if (run.status === 'interrupted')
|
|
290
|
-
return
|
|
291
|
-
if (run.status === 'failed')
|
|
1493
|
+
return { label: 'Interrupted', tone: 'warning', icon: 'alert-triangle' };
|
|
1494
|
+
if (run.status === 'failed')
|
|
1495
|
+
return { label: 'Run failed', tone: 'destructive', icon: 'octagon-x' };
|
|
292
1496
|
const outcome = run.score && run.score.outcome;
|
|
293
1497
|
if (!outcome)
|
|
294
|
-
return
|
|
295
|
-
|
|
296
|
-
|
|
1498
|
+
return {
|
|
1499
|
+
label: run.status === 'stopped' ? 'Stopped' : run.status,
|
|
1500
|
+
tone: 'quiet',
|
|
1501
|
+
icon: run.status === 'stopped' ? 'stop' : null,
|
|
1502
|
+
};
|
|
1503
|
+
const [label, tone, mark] = {
|
|
1504
|
+
passed: ['Passed', 'success', 'check-circle'],
|
|
1505
|
+
failed: ['Failed', 'destructive', 'x-circle'],
|
|
1506
|
+
inconclusive: ['Inconclusive', 'warning', 'alert-triangle'],
|
|
1507
|
+
}[outcome];
|
|
1508
|
+
return {
|
|
1509
|
+
label: run.status === 'stopped' ? label + ', stopped' : label,
|
|
1510
|
+
tone,
|
|
1511
|
+
icon: mark,
|
|
1512
|
+
outcome,
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function stateBadge(run, size) {
|
|
1516
|
+
const state = runState(run);
|
|
297
1517
|
return h(
|
|
298
1518
|
'span',
|
|
299
|
-
{ class: '
|
|
300
|
-
|
|
1519
|
+
{ class: 'badge ' + state.tone + (size ? ' ' + size : '') },
|
|
1520
|
+
state.live && h('span', { class: 'bulb live', 'aria-hidden': 'true' }),
|
|
1521
|
+
state.icon && icon(state.icon),
|
|
1522
|
+
state.label
|
|
301
1523
|
);
|
|
302
1524
|
}
|
|
303
1525
|
|
|
304
1526
|
const inUnit = (value, unit) => (unit == null ? value + '%' : value + ' ' + unit);
|
|
305
|
-
|
|
306
|
-
|
|
1527
|
+
// A checklist with no weights has no score, so the run reads as "3 of 4
|
|
1528
|
+
// passed", the same count the finish_run reply gives. Informational and
|
|
1529
|
+
// skipped checks are left out, as the scorer leaves them out.
|
|
1530
|
+
const tally = (run) => {
|
|
1531
|
+
const checks = (run.checklist_snapshot && run.checklist_snapshot.checks) || [];
|
|
1532
|
+
const status = Object.fromEntries((run.results || []).map((r) => [r.check, r.status]));
|
|
1533
|
+
const scored = checks.filter(
|
|
1534
|
+
(c) => c.type !== 'informational' && status[c.key] !== 'skipped'
|
|
1535
|
+
);
|
|
1536
|
+
return {
|
|
1537
|
+
passed: scored.filter((c) => status[c.key] === 'passed').length,
|
|
1538
|
+
total: scored.length,
|
|
1539
|
+
};
|
|
1540
|
+
};
|
|
1541
|
+
const scoreText = (score, run) => {
|
|
1542
|
+
if (score && score.score != null) return inUnit(score.score, score.unit);
|
|
1543
|
+
if (!score || !run || !run.checklist_snapshot) return '';
|
|
1544
|
+
const t = tally(run);
|
|
1545
|
+
return t.total ? t.passed + ' of ' + t.total + ' passed' : '';
|
|
1546
|
+
};
|
|
307
1547
|
|
|
308
|
-
const
|
|
309
|
-
|
|
1548
|
+
const runner = (run) =>
|
|
1549
|
+
run.runner.kind === 'cloud'
|
|
1550
|
+
? { icon: 'server', label: 'Cloud' }
|
|
1551
|
+
: { icon: 'monitor', label: 'Local, self-judged' };
|
|
1552
|
+
const checklistName = (run) =>
|
|
1553
|
+
(run.checklist_snapshot && run.checklist_snapshot.name) || run.checklist;
|
|
310
1554
|
|
|
311
1555
|
const RESULT = {
|
|
312
|
-
passed: ['Passed', '
|
|
313
|
-
failed: ['Failed', '
|
|
314
|
-
informational: ['Observed', ''],
|
|
315
|
-
error: ['Not judged', '
|
|
316
|
-
skipped: ['Skipped', ''],
|
|
1556
|
+
passed: ['Passed', 'success', 'check-circle'],
|
|
1557
|
+
failed: ['Failed', 'destructive', 'x-circle'],
|
|
1558
|
+
informational: ['Observed', 'arc', 'info'],
|
|
1559
|
+
error: ['Not judged', 'warning', 'alert-triangle'],
|
|
1560
|
+
skipped: ['Skipped', 'quiet', 'minus-circle'],
|
|
317
1561
|
};
|
|
318
1562
|
|
|
319
1563
|
async function getJson(url) {
|
|
@@ -322,59 +1566,196 @@
|
|
|
322
1566
|
return res.json();
|
|
323
1567
|
}
|
|
324
1568
|
|
|
1569
|
+
// A live region outside #app, so a redraw never re-announces the page.
|
|
1570
|
+
const status = document.getElementById('status');
|
|
1571
|
+
const announce = (text) => {
|
|
1572
|
+
if (status.textContent !== text) status.textContent = text;
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
// The label stays short so the header does not shift; the command is
|
|
1576
|
+
// the button's title.
|
|
325
1577
|
async function copy(text, button) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1578
|
+
const label = button.lastChild;
|
|
1579
|
+
label.dataset.label ??= label.textContent;
|
|
1580
|
+
let done = 'Copied';
|
|
1581
|
+
try {
|
|
1582
|
+
await navigator.clipboard.writeText(text);
|
|
1583
|
+
} catch {
|
|
1584
|
+
done = 'Copy failed';
|
|
1585
|
+
}
|
|
1586
|
+
label.textContent = done;
|
|
1587
|
+
announce(done === 'Copied' ? 'Copied ' + text : done);
|
|
1588
|
+
clearTimeout(button.resetLabel);
|
|
1589
|
+
button.resetLabel = setTimeout(() => (label.textContent = label.dataset.label), 1600);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Set when a Stop is refused, shown beside the button on the next draw.
|
|
1593
|
+
let stopError = null;
|
|
1594
|
+
|
|
1595
|
+
function pageHeader({ back, title, badge, description, chips, actions }) {
|
|
1596
|
+
return h(
|
|
1597
|
+
'header',
|
|
1598
|
+
{ class: 'page-header' },
|
|
1599
|
+
back &&
|
|
1600
|
+
h(
|
|
1601
|
+
'div',
|
|
1602
|
+
{ class: 'back' },
|
|
1603
|
+
h('a', { href: back.href, 'data-nav': true }, icon('arrow-left'), back.label),
|
|
1604
|
+
back.trail && [
|
|
1605
|
+
h('span', { class: 'slash', 'aria-hidden': 'true' }, '/'),
|
|
1606
|
+
h('span', { class: 'truncate mono' }, back.trail),
|
|
1607
|
+
]
|
|
1608
|
+
),
|
|
1609
|
+
h(
|
|
1610
|
+
'div',
|
|
1611
|
+
{ class: 'header-grid' },
|
|
1612
|
+
h(
|
|
1613
|
+
'div',
|
|
1614
|
+
{ class: 'header-main' },
|
|
1615
|
+
h('div', { class: 'title-row' }, h('h1', { class: 'page-title' }, title), badge),
|
|
1616
|
+
description && h('p', { class: 'description' }, description),
|
|
1617
|
+
chips &&
|
|
1618
|
+
chips.length > 0 &&
|
|
1619
|
+
h(
|
|
1620
|
+
'ul',
|
|
1621
|
+
{ class: 'chips' },
|
|
1622
|
+
chips.map((chip) =>
|
|
1623
|
+
h(
|
|
1624
|
+
'li',
|
|
1625
|
+
{},
|
|
1626
|
+
h(
|
|
1627
|
+
chip.href ? 'a' : 'span',
|
|
1628
|
+
{
|
|
1629
|
+
class: 'chip',
|
|
1630
|
+
title: chip.title,
|
|
1631
|
+
href: chip.href,
|
|
1632
|
+
target: chip.href && '_blank',
|
|
1633
|
+
rel: chip.href && 'noopener noreferrer',
|
|
1634
|
+
},
|
|
1635
|
+
icon(chip.icon),
|
|
1636
|
+
h('span', { class: 'truncate' + (chip.mono ? ' mono' : '') }, chip.label)
|
|
1637
|
+
)
|
|
1638
|
+
)
|
|
1639
|
+
)
|
|
1640
|
+
)
|
|
1641
|
+
),
|
|
1642
|
+
actions && h('div', { class: 'actions' }, actions)
|
|
1643
|
+
)
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function emptyState(mark, title, ...description) {
|
|
1648
|
+
return h(
|
|
1649
|
+
'div',
|
|
1650
|
+
{ class: 'empty' },
|
|
1651
|
+
h(
|
|
1652
|
+
'div',
|
|
1653
|
+
{ class: 'frame-marks' },
|
|
1654
|
+
h('div', { class: 'medallion' }, icon(mark)),
|
|
1655
|
+
h('h2', {}, title),
|
|
1656
|
+
h('p', {}, description)
|
|
1657
|
+
)
|
|
1658
|
+
);
|
|
330
1659
|
}
|
|
331
1660
|
|
|
332
1661
|
async function renderRuns() {
|
|
333
1662
|
const runs = await getJson('/api/runs');
|
|
334
1663
|
if (runs.length === 0) {
|
|
335
1664
|
return [
|
|
336
|
-
h('h1', {}, 'Runs'),
|
|
337
|
-
|
|
338
|
-
'
|
|
339
|
-
|
|
340
|
-
'
|
|
1665
|
+
h('h1', { class: 'sr-only' }, 'Runs'),
|
|
1666
|
+
emptyState(
|
|
1667
|
+
'checklist',
|
|
1668
|
+
'No runs yet',
|
|
1669
|
+
'Type ',
|
|
341
1670
|
h('code', {}, '/stuntdouble run <checklist>'),
|
|
342
1671
|
' in Claude Code to start one.'
|
|
343
1672
|
),
|
|
344
1673
|
];
|
|
345
1674
|
}
|
|
346
1675
|
return [
|
|
347
|
-
|
|
348
|
-
|
|
1676
|
+
pageHeader({
|
|
1677
|
+
title: 'Runs',
|
|
1678
|
+
description: 'Local and cloud runs of this project, newest first.',
|
|
1679
|
+
}),
|
|
349
1680
|
h(
|
|
350
1681
|
'div',
|
|
351
|
-
{ class: 'card
|
|
1682
|
+
{ class: 'group card' },
|
|
1683
|
+
h(
|
|
1684
|
+
'div',
|
|
1685
|
+
{ class: 'runs-head', 'aria-hidden': 'true' },
|
|
1686
|
+
h('span', {}, 'Checklist'),
|
|
1687
|
+
h('span', {}, 'Result'),
|
|
1688
|
+
h('span', {}, 'Actor'),
|
|
1689
|
+
h('span', {}, 'Runner'),
|
|
1690
|
+
h('span', {}, 'Started')
|
|
1691
|
+
),
|
|
352
1692
|
runs.map((entry) =>
|
|
353
1693
|
'error' in entry
|
|
354
1694
|
? h(
|
|
355
1695
|
'div',
|
|
356
|
-
{},
|
|
357
|
-
h(
|
|
358
|
-
|
|
359
|
-
|
|
1696
|
+
{ class: 'run-row' },
|
|
1697
|
+
h(
|
|
1698
|
+
'span',
|
|
1699
|
+
{ class: 'run-name' },
|
|
1700
|
+
icon('alert-triangle', 'tone-error'),
|
|
1701
|
+
h('span', { class: 'truncate mono' }, entry.id)
|
|
1702
|
+
),
|
|
1703
|
+
h(
|
|
1704
|
+
'span',
|
|
1705
|
+
{ class: 'result' },
|
|
1706
|
+
h('span', { class: 'badge warning' }, 'Unreadable')
|
|
1707
|
+
),
|
|
1708
|
+
h('span', { class: 'facts error-text' }, entry.error)
|
|
360
1709
|
)
|
|
361
1710
|
: h(
|
|
362
1711
|
'a',
|
|
363
|
-
{
|
|
364
|
-
|
|
1712
|
+
{
|
|
1713
|
+
class: 'run-row',
|
|
1714
|
+
href: '/runs/' + encodeURIComponent(entry.id),
|
|
1715
|
+
'data-nav': true,
|
|
1716
|
+
},
|
|
365
1717
|
h(
|
|
366
1718
|
'span',
|
|
367
|
-
{},
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1719
|
+
{ class: 'run-name' },
|
|
1720
|
+
icon('checklist'),
|
|
1721
|
+
h(
|
|
1722
|
+
'div',
|
|
1723
|
+
{},
|
|
1724
|
+
h(
|
|
1725
|
+
'strong',
|
|
1726
|
+
{ class: 'truncate', style: 'display:block' },
|
|
1727
|
+
checklistName(entry)
|
|
1728
|
+
),
|
|
1729
|
+
h(
|
|
1730
|
+
'span',
|
|
1731
|
+
{ class: 'run-id mono truncate', style: 'display:block' },
|
|
1732
|
+
entry.id
|
|
1733
|
+
)
|
|
1734
|
+
)
|
|
1735
|
+
),
|
|
1736
|
+
h(
|
|
1737
|
+
'span',
|
|
1738
|
+
{ class: 'result' },
|
|
1739
|
+
stateBadge(entry),
|
|
1740
|
+
scoreText(entry.score, entry) &&
|
|
1741
|
+
h('span', { class: 'tally' }, scoreText(entry.score, entry))
|
|
371
1742
|
),
|
|
372
1743
|
h(
|
|
373
1744
|
'span',
|
|
374
|
-
{ class: '
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1745
|
+
{ class: 'facts' },
|
|
1746
|
+
h(
|
|
1747
|
+
'span',
|
|
1748
|
+
{ class: 'fact' },
|
|
1749
|
+
icon('actor'),
|
|
1750
|
+
h('span', { class: 'truncate' }, entry.actor)
|
|
1751
|
+
),
|
|
1752
|
+
h(
|
|
1753
|
+
'span',
|
|
1754
|
+
{ class: 'fact' },
|
|
1755
|
+
icon(runner(entry).icon),
|
|
1756
|
+
h('span', { class: 'truncate' }, runner(entry).label)
|
|
1757
|
+
),
|
|
1758
|
+
h('span', { class: 'fact' }, time(entry.started_at, 'relative'))
|
|
378
1759
|
)
|
|
379
1760
|
)
|
|
380
1761
|
)
|
|
@@ -382,166 +1763,522 @@
|
|
|
382
1763
|
];
|
|
383
1764
|
}
|
|
384
1765
|
|
|
385
|
-
|
|
386
|
-
|
|
1766
|
+
// How a check is judged, beside its key. Silent for a plain pass/fail
|
|
1767
|
+
// check, an unconditional one and one that lets the run continue: those
|
|
1768
|
+
// are the defaults, and a badge on every row would bury the rest.
|
|
1769
|
+
function checkMeta(check) {
|
|
1770
|
+
const weight = check.weight ?? 1;
|
|
387
1771
|
return h(
|
|
1772
|
+
'span',
|
|
1773
|
+
{ class: 'check-meta' },
|
|
1774
|
+
h('code', {}, check.key),
|
|
1775
|
+
check.type === 'informational' &&
|
|
1776
|
+
h('span', { class: 'badge outline sm' }, icon('info'), 'Observation'),
|
|
1777
|
+
check.type === 'weighted' &&
|
|
1778
|
+
h(
|
|
1779
|
+
'span',
|
|
1780
|
+
{ class: 'badge outline sm' },
|
|
1781
|
+
icon(weight < 0 ? 'minus-circle' : 'scale'),
|
|
1782
|
+
h('span', { class: 'mono' }, weight < 0 ? weight + ' penalty' : weight + 'x')
|
|
1783
|
+
),
|
|
1784
|
+
check.run_condition &&
|
|
1785
|
+
check.run_condition !== 'always' &&
|
|
1786
|
+
h(
|
|
1787
|
+
'span',
|
|
1788
|
+
{ class: 'badge outline sm' },
|
|
1789
|
+
icon('git-branch'),
|
|
1790
|
+
check.run_condition.replaceAll('_', ' ')
|
|
1791
|
+
),
|
|
1792
|
+
check.on_failure === 'stop' &&
|
|
1793
|
+
h('span', { class: 'badge outline sm' }, icon('octagon-x'), 'Stops the run if it fails')
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Per page view: which rows a reader opened or closed, and what this
|
|
1798
|
+
// tab had already seen of each run, so a redraw keeps the rows as they
|
|
1799
|
+
// were and only something that just landed moves.
|
|
1800
|
+
const toggled = new Map();
|
|
1801
|
+
const seen = new Map();
|
|
1802
|
+
|
|
1803
|
+
function checkRow(run, check, result, opts) {
|
|
1804
|
+
if (!result) {
|
|
1805
|
+
return h(
|
|
1806
|
+
'li',
|
|
1807
|
+
{ class: 'check waiting' },
|
|
1808
|
+
h(
|
|
1809
|
+
'div',
|
|
1810
|
+
{ class: 'check-head' },
|
|
1811
|
+
icon(opts.running ? 'pending' : 'circle-dashed', 'tone-waiting'),
|
|
1812
|
+
h(
|
|
1813
|
+
'span',
|
|
1814
|
+
{ class: 'check-body' },
|
|
1815
|
+
h('span', { class: 'check-desc' }, check.description),
|
|
1816
|
+
checkMeta(check)
|
|
1817
|
+
),
|
|
1818
|
+
h('span', { class: 'badge quiet sm' }, opts.running ? 'Waiting' : 'Not reached')
|
|
1819
|
+
)
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
const [label, tone, mark] = RESULT[result.status];
|
|
1823
|
+
const shot = result.screenshot && fileUrl('runs/' + run.id + '/' + result.screenshot);
|
|
1824
|
+
const key = run.id + ':' + check.key;
|
|
1825
|
+
const open = toggled.has(key) ? toggled.get(key) : opts.openByDefault;
|
|
1826
|
+
const detail = h(
|
|
388
1827
|
'div',
|
|
389
|
-
{ class: 'check' },
|
|
1828
|
+
{ class: 'check-detail', id: 'detail-' + check.key, hidden: !open },
|
|
390
1829
|
h(
|
|
391
|
-
'
|
|
392
|
-
{},
|
|
393
|
-
|
|
394
|
-
h('span', { class: 'pill ' + tone }, label)
|
|
1830
|
+
'p',
|
|
1831
|
+
{ class: 'evidence' + (result.status === 'informational' ? ' observed' : '') },
|
|
1832
|
+
result.evidence
|
|
395
1833
|
),
|
|
396
|
-
result &&
|
|
397
|
-
result &&
|
|
398
|
-
result.locator &&
|
|
1834
|
+
result.locator &&
|
|
399
1835
|
h(
|
|
400
|
-
'
|
|
401
|
-
{ class: '
|
|
1836
|
+
'p',
|
|
1837
|
+
{ class: 'cited mono' },
|
|
402
1838
|
'Cited: ' + (result.locator.text || result.locator.selector)
|
|
403
1839
|
),
|
|
404
|
-
|
|
405
|
-
result.screenshot &&
|
|
1840
|
+
shot &&
|
|
406
1841
|
h(
|
|
407
1842
|
'a',
|
|
408
|
-
{
|
|
1843
|
+
{
|
|
1844
|
+
class: 'shot',
|
|
1845
|
+
href: shot,
|
|
1846
|
+
target: '_blank',
|
|
1847
|
+
title: 'Open the full-size screenshot',
|
|
1848
|
+
},
|
|
409
1849
|
h('img', {
|
|
410
|
-
src:
|
|
1850
|
+
src: shot,
|
|
411
1851
|
alt: 'The page when ' + check.key + ' was judged',
|
|
412
1852
|
loading: 'lazy',
|
|
413
1853
|
})
|
|
414
1854
|
)
|
|
415
1855
|
);
|
|
1856
|
+
const thumb =
|
|
1857
|
+
shot && h('img', { class: 'thumb', src: shot, alt: '', loading: 'lazy', hidden: open });
|
|
1858
|
+
const row = h(
|
|
1859
|
+
'li',
|
|
1860
|
+
{ class: 'check' + (open ? ' open' : '') + (opts.arrived ? ' animate-rise' : '') },
|
|
1861
|
+
h(
|
|
1862
|
+
'button',
|
|
1863
|
+
{
|
|
1864
|
+
type: 'button',
|
|
1865
|
+
class: 'check-head',
|
|
1866
|
+
'aria-expanded': String(open),
|
|
1867
|
+
'aria-controls': 'detail-' + check.key,
|
|
1868
|
+
'aria-labelledby': 'desc-' + check.key + ' label-' + check.key,
|
|
1869
|
+
'data-focus-key': 'check:' + check.key,
|
|
1870
|
+
onclick: () => {
|
|
1871
|
+
const now = detail.hidden;
|
|
1872
|
+
toggled.set(key, now);
|
|
1873
|
+
detail.hidden = !now;
|
|
1874
|
+
if (thumb) thumb.hidden = now;
|
|
1875
|
+
row.classList.toggle('open', now);
|
|
1876
|
+
row.firstChild.setAttribute('aria-expanded', String(now));
|
|
1877
|
+
},
|
|
1878
|
+
},
|
|
1879
|
+
icon(mark, 'tone-' + result.status),
|
|
1880
|
+
h(
|
|
1881
|
+
'span',
|
|
1882
|
+
{ class: 'check-body' },
|
|
1883
|
+
h('span', { class: 'check-desc', id: 'desc-' + check.key }, check.description),
|
|
1884
|
+
checkMeta(check)
|
|
1885
|
+
),
|
|
1886
|
+
h(
|
|
1887
|
+
'span',
|
|
1888
|
+
{ class: 'check-side' },
|
|
1889
|
+
thumb,
|
|
1890
|
+
h('span', { class: 'badge sm ' + tone, id: 'label-' + check.key }, label),
|
|
1891
|
+
icon('chevron-down', 'chevron')
|
|
1892
|
+
)
|
|
1893
|
+
),
|
|
1894
|
+
detail
|
|
1895
|
+
);
|
|
1896
|
+
return row;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function summaryCard(run, fresh) {
|
|
1900
|
+
const score = run.score;
|
|
1901
|
+
const state = runState(run);
|
|
1902
|
+
if (!score || !state.outcome) return null;
|
|
1903
|
+
const weighted = score.score != null;
|
|
1904
|
+
const t = tally(run);
|
|
1905
|
+
const counts = { passed: 0, failed: 0, error: 0, informational: 0, skipped: 0 };
|
|
1906
|
+
for (const r of run.results) counts[r.status]++;
|
|
1907
|
+
const judged = counts.passed + counts.failed + counts.error;
|
|
1908
|
+
const share = (n) => (judged ? (n / judged) * 100 : 0) + '%';
|
|
1909
|
+
return h(
|
|
1910
|
+
'section',
|
|
1911
|
+
{ class: 'card summary', 'aria-label': 'Score' },
|
|
1912
|
+
h(
|
|
1913
|
+
'div',
|
|
1914
|
+
{ class: 'summary-top' },
|
|
1915
|
+
h(
|
|
1916
|
+
'div',
|
|
1917
|
+
{},
|
|
1918
|
+
h('p', { class: 'overline' }, weighted ? 'Weighted score' : 'Checks passed'),
|
|
1919
|
+
h(
|
|
1920
|
+
'p',
|
|
1921
|
+
{
|
|
1922
|
+
class:
|
|
1923
|
+
'figure tone-' + (state.outcome === 'inconclusive' ? 'error' : state.outcome),
|
|
1924
|
+
},
|
|
1925
|
+
weighted
|
|
1926
|
+
? inUnit(score.score, score.unit)
|
|
1927
|
+
: [String(t.passed), h('span', { class: 'of' }, '/' + t.total)]
|
|
1928
|
+
),
|
|
1929
|
+
// Unweighted, the figure is the tally: no line repeats it.
|
|
1930
|
+
weighted &&
|
|
1931
|
+
h(
|
|
1932
|
+
'p',
|
|
1933
|
+
{ class: 'summary-line' },
|
|
1934
|
+
score.pass_threshold == null
|
|
1935
|
+
? 'No pass threshold set'
|
|
1936
|
+
: 'Passes at ' + inUnit(score.pass_threshold, score.unit)
|
|
1937
|
+
)
|
|
1938
|
+
),
|
|
1939
|
+
h(
|
|
1940
|
+
'div',
|
|
1941
|
+
{ class: 'verdict' },
|
|
1942
|
+
score.grade && h('span', { class: 'badge outline lg' }, score.grade),
|
|
1943
|
+
h(
|
|
1944
|
+
'span',
|
|
1945
|
+
{
|
|
1946
|
+
class:
|
|
1947
|
+
'badge stamp tone-' +
|
|
1948
|
+
(state.outcome === 'inconclusive' ? 'error' : state.outcome) +
|
|
1949
|
+
(fresh ? ' animate-stamp' : ''),
|
|
1950
|
+
},
|
|
1951
|
+
state.label
|
|
1952
|
+
)
|
|
1953
|
+
)
|
|
1954
|
+
),
|
|
1955
|
+
h(
|
|
1956
|
+
'div',
|
|
1957
|
+
{ class: 'meter', 'aria-hidden': 'true' },
|
|
1958
|
+
['passed', 'failed', 'error'].map(
|
|
1959
|
+
(s) => counts[s] > 0 && h('span', { class: s, style: 'width:' + share(counts[s]) })
|
|
1960
|
+
)
|
|
1961
|
+
),
|
|
1962
|
+
h(
|
|
1963
|
+
'ul',
|
|
1964
|
+
{ class: 'counts' },
|
|
1965
|
+
Object.entries(counts)
|
|
1966
|
+
.filter(([, n]) => n > 0)
|
|
1967
|
+
.map(([s, n]) =>
|
|
1968
|
+
h(
|
|
1969
|
+
'li',
|
|
1970
|
+
{},
|
|
1971
|
+
h(
|
|
1972
|
+
'span',
|
|
1973
|
+
{ class: 'badge lg ' + RESULT[s][1] },
|
|
1974
|
+
icon(RESULT[s][2]),
|
|
1975
|
+
h('span', { class: 'mono' }, n),
|
|
1976
|
+
RESULT[s][0]
|
|
1977
|
+
)
|
|
1978
|
+
)
|
|
1979
|
+
)
|
|
1980
|
+
)
|
|
1981
|
+
);
|
|
416
1982
|
}
|
|
417
1983
|
|
|
418
1984
|
async function renderRun(id) {
|
|
419
1985
|
const data = await getJson('/api/runs/' + encodeURIComponent(id));
|
|
420
|
-
if (data.error)
|
|
1986
|
+
if (data.error) {
|
|
1987
|
+
return [
|
|
1988
|
+
pageHeader({ back: { href: '/', label: 'Runs' }, title: id }),
|
|
1989
|
+
h('div', { class: 'notice warning' }, icon('alert-triangle'), h('p', {}, data.error)),
|
|
1990
|
+
];
|
|
1991
|
+
}
|
|
421
1992
|
const run = data.run;
|
|
422
1993
|
const results = new Map(run.results.map((r) => [r.check, r]));
|
|
423
1994
|
const checks = run.checklist_snapshot.checks;
|
|
424
1995
|
const running = run.status === 'running';
|
|
425
1996
|
const resumable = run.status === 'interrupted' || run.status === 'stopped';
|
|
426
1997
|
|
|
1998
|
+
const before = seen.get(run.id);
|
|
1999
|
+
seen.set(run.id, { status: run.status, results: new Set(results.keys()) });
|
|
2000
|
+
const fresh = before && before.status === 'running' && !running;
|
|
2001
|
+
const arrived = (key) => before && !before.results.has(key);
|
|
2002
|
+
|
|
2003
|
+
// Stop sits in the live card, beside the tally, as CancelRunButton does.
|
|
2004
|
+
const pending = data.stop_requested;
|
|
2005
|
+
const stopButton =
|
|
2006
|
+
running &&
|
|
2007
|
+
h(
|
|
2008
|
+
'button',
|
|
2009
|
+
{
|
|
2010
|
+
type: 'button',
|
|
2011
|
+
class: 'btn outline',
|
|
2012
|
+
'aria-disabled': pending && 'true',
|
|
2013
|
+
'data-pending': pending,
|
|
2014
|
+
'data-focus-key': 'stop',
|
|
2015
|
+
onclick: async (event) => {
|
|
2016
|
+
const button = event.currentTarget;
|
|
2017
|
+
if (button.hasAttribute('data-pending')) return;
|
|
2018
|
+
button.setAttribute('aria-disabled', 'true');
|
|
2019
|
+
button.setAttribute('data-pending', '');
|
|
2020
|
+
button.lastChild.textContent = 'Stopping at the next step';
|
|
2021
|
+
stopError = null;
|
|
2022
|
+
let res = null;
|
|
2023
|
+
try {
|
|
2024
|
+
res = await fetch('/api/stop', {
|
|
2025
|
+
method: 'POST',
|
|
2026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2027
|
+
body: JSON.stringify({ run: run.id }),
|
|
2028
|
+
});
|
|
2029
|
+
} catch {
|
|
2030
|
+
res = null;
|
|
2031
|
+
}
|
|
2032
|
+
if (res && (res.ok || res.status === 409)) return; // 409: it already finished
|
|
2033
|
+
stopError =
|
|
2034
|
+
res && res.status === 403
|
|
2035
|
+
? 'Stop was refused. Open the viewer from the link Claude Code gave you.'
|
|
2036
|
+
: 'The run could not be stopped. Try again.';
|
|
2037
|
+
announce(stopError);
|
|
2038
|
+
render(false);
|
|
2039
|
+
},
|
|
2040
|
+
},
|
|
2041
|
+
icon('stop'),
|
|
2042
|
+
h('span', {}, pending ? 'Stopping at the next step' : 'Stop')
|
|
2043
|
+
);
|
|
2044
|
+
|
|
2045
|
+
const rerun = '/stuntdouble run ' + run.checklist + (resumable ? ' resume' : '');
|
|
2046
|
+
const cloud = '/stuntdouble cloud ' + run.checklist;
|
|
427
2047
|
const actions = running
|
|
428
|
-
?
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
method: 'POST',
|
|
440
|
-
headers: { 'Content-Type': 'application/json' },
|
|
441
|
-
body: JSON.stringify({ run: run.id }),
|
|
442
|
-
});
|
|
2048
|
+
? null
|
|
2049
|
+
: run.runner.kind === 'local'
|
|
2050
|
+
? [
|
|
2051
|
+
h(
|
|
2052
|
+
'button',
|
|
2053
|
+
{
|
|
2054
|
+
type: 'button',
|
|
2055
|
+
class: 'btn outline',
|
|
2056
|
+
title: rerun,
|
|
2057
|
+
'data-focus-key': 'rerun',
|
|
2058
|
+
onclick: (event) => copy(rerun, event.currentTarget),
|
|
443
2059
|
},
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
2060
|
+
icon('copy'),
|
|
2061
|
+
h('span', {}, resumable ? 'Copy resume command' : 'Copy re-run command')
|
|
2062
|
+
),
|
|
2063
|
+
h(
|
|
2064
|
+
'button',
|
|
2065
|
+
{
|
|
2066
|
+
type: 'button',
|
|
2067
|
+
class: 'btn outline',
|
|
2068
|
+
title: cloud,
|
|
2069
|
+
'data-focus-key': 'cloud',
|
|
2070
|
+
onclick: (event) => copy(cloud, event.currentTarget),
|
|
2071
|
+
},
|
|
2072
|
+
icon('copy'),
|
|
2073
|
+
h('span', {}, 'Run in the cloud')
|
|
2074
|
+
),
|
|
2075
|
+
]
|
|
2076
|
+
: webUrl(run.runner.url)
|
|
2077
|
+
? [
|
|
2078
|
+
h(
|
|
2079
|
+
'a',
|
|
2080
|
+
{
|
|
2081
|
+
class: 'btn outline',
|
|
2082
|
+
href: run.runner.url,
|
|
2083
|
+
target: '_blank',
|
|
2084
|
+
rel: 'noopener noreferrer',
|
|
2085
|
+
},
|
|
2086
|
+
icon('external-link'),
|
|
2087
|
+
h('span', {}, 'Open this run in Stunt Double')
|
|
2088
|
+
),
|
|
2089
|
+
]
|
|
2090
|
+
: null;
|
|
2091
|
+
|
|
2092
|
+
const duration = run.finished_at && took(run.started_at, run.finished_at);
|
|
2093
|
+
const header = pageHeader({
|
|
2094
|
+
back: { href: '/', label: 'Runs', trail: run.id },
|
|
2095
|
+
title: checklistName(run),
|
|
2096
|
+
badge: stateBadge(run, 'lg'),
|
|
2097
|
+
chips: [
|
|
2098
|
+
{ icon: runner(run).icon, label: runner(run).label },
|
|
2099
|
+
{ icon: 'actor', label: run.actor, title: 'Actor' },
|
|
2100
|
+
{ icon: 'globe', label: run.url, href: webUrl(run.url), title: 'Target' },
|
|
2101
|
+
{ icon: 'calendar', label: time(run.started_at), title: 'Started' },
|
|
2102
|
+
duration && {
|
|
2103
|
+
icon: 'timer',
|
|
2104
|
+
label: duration,
|
|
2105
|
+
mono: true,
|
|
2106
|
+
title: 'How long the run took',
|
|
2107
|
+
},
|
|
2108
|
+
].filter(Boolean),
|
|
2109
|
+
actions,
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
const judged = run.results.length;
|
|
2113
|
+
const checked = pad(judged) + ' of ' + pad(checks.length) + ' checked';
|
|
2114
|
+
if (running) announce(stopError || judged + ' of ' + checks.length + ' checked');
|
|
2115
|
+
else if (fresh) announce(runState(run).label);
|
|
2116
|
+
if (!running) stopError = null;
|
|
2117
|
+
const live =
|
|
2118
|
+
running &&
|
|
2119
|
+
h(
|
|
2120
|
+
'section',
|
|
2121
|
+
{ class: 'card live-card', 'aria-label': 'Live' },
|
|
449
2122
|
h(
|
|
450
2123
|
'div',
|
|
451
|
-
{ class: '
|
|
2124
|
+
{ class: 'live-head' },
|
|
452
2125
|
h(
|
|
453
|
-
'
|
|
454
|
-
{
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
2126
|
+
'p',
|
|
2127
|
+
{ class: 'live-step' },
|
|
2128
|
+
h('span', { class: 'chase', role: 'img', 'aria-label': 'Run in progress' }, [
|
|
2129
|
+
h('span'),
|
|
2130
|
+
h('span'),
|
|
2131
|
+
h('span'),
|
|
2132
|
+
h('span'),
|
|
2133
|
+
h('span'),
|
|
2134
|
+
]),
|
|
2135
|
+
h(
|
|
2136
|
+
'span',
|
|
2137
|
+
{ class: 'truncate' },
|
|
2138
|
+
data.activity.length > 0
|
|
2139
|
+
? data.activity[data.activity.length - 1].text.split('\n')[0]
|
|
2140
|
+
: 'Running'
|
|
2141
|
+
)
|
|
463
2142
|
),
|
|
464
2143
|
h(
|
|
465
|
-
'
|
|
466
|
-
{
|
|
467
|
-
|
|
2144
|
+
'span',
|
|
2145
|
+
{ class: 'live-side' },
|
|
2146
|
+
h(
|
|
2147
|
+
'span',
|
|
2148
|
+
{ class: 'badge arc' },
|
|
2149
|
+
h('span', { class: 'bulb live', 'aria-hidden': 'true' }),
|
|
2150
|
+
'Live'
|
|
2151
|
+
),
|
|
2152
|
+
h('span', { class: 'tally' }, checked),
|
|
2153
|
+
stopButton,
|
|
2154
|
+
stopError && h('span', { class: 'stop-error' }, stopError)
|
|
468
2155
|
)
|
|
469
|
-
)
|
|
2156
|
+
),
|
|
2157
|
+
h(
|
|
2158
|
+
'div',
|
|
2159
|
+
{
|
|
2160
|
+
class: 'progress',
|
|
2161
|
+
role: 'progressbar',
|
|
2162
|
+
'aria-label': 'Run progress',
|
|
2163
|
+
'aria-valuemin': 0,
|
|
2164
|
+
'aria-valuemax': checks.length,
|
|
2165
|
+
'aria-valuenow': judged,
|
|
2166
|
+
},
|
|
2167
|
+
h('span', {
|
|
2168
|
+
style: 'width:' + (checks.length ? (judged / checks.length) * 100 : 0) + '%',
|
|
2169
|
+
})
|
|
2170
|
+
),
|
|
2171
|
+
h('img', {
|
|
2172
|
+
class: 'frame',
|
|
2173
|
+
id: 'frame',
|
|
2174
|
+
src: fileUrl('.live/latest.png') + '?v=' + Date.now(),
|
|
2175
|
+
alt: 'The browser now',
|
|
2176
|
+
})
|
|
2177
|
+
);
|
|
470
2178
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
2179
|
+
// What went wrong first, since that is what somebody opens a report to
|
|
2180
|
+
// find, then what held, then what was only read off the page, then what
|
|
2181
|
+
// never ran.
|
|
2182
|
+
const groups = [
|
|
2183
|
+
['Needs attention', ['failed', 'error'], true],
|
|
2184
|
+
['Passed', ['passed'], false],
|
|
2185
|
+
['Observations', ['informational'], true],
|
|
2186
|
+
['Skipped', ['skipped'], false],
|
|
2187
|
+
]
|
|
2188
|
+
.map(([title, statuses, openByDefault]) => ({
|
|
2189
|
+
title,
|
|
2190
|
+
openByDefault,
|
|
2191
|
+
checks: checks.filter(
|
|
2192
|
+
(c) => results.has(c.key) && statuses.includes(results.get(c.key).status)
|
|
2193
|
+
),
|
|
2194
|
+
}))
|
|
2195
|
+
.concat({
|
|
2196
|
+
title: running ? 'Still to check' : 'Not reached',
|
|
2197
|
+
checks: checks.filter((c) => !results.has(c.key)),
|
|
2198
|
+
})
|
|
2199
|
+
.filter((g) => g.checks.length > 0);
|
|
2200
|
+
|
|
2201
|
+
const notice =
|
|
2202
|
+
run.summary &&
|
|
2203
|
+
(run.status === 'failed'
|
|
2204
|
+
? h('div', { class: 'notice destructive' }, icon('octagon-x'), h('p', {}, run.summary))
|
|
2205
|
+
: h('p', { class: 'quote' }, run.summary));
|
|
476
2206
|
|
|
477
2207
|
return [
|
|
478
|
-
|
|
479
|
-
h('h1', {}, run.checklist),
|
|
2208
|
+
header,
|
|
480
2209
|
h(
|
|
481
2210
|
'div',
|
|
482
|
-
{ class: '
|
|
483
|
-
|
|
484
|
-
|
|
2211
|
+
{ class: 'stack' },
|
|
2212
|
+
live,
|
|
2213
|
+
notice,
|
|
2214
|
+
summaryCard(run, fresh),
|
|
2215
|
+
groups.map((group) =>
|
|
485
2216
|
h(
|
|
486
|
-
'
|
|
487
|
-
{},
|
|
488
|
-
scoreText(run.score) +
|
|
489
|
-
(run.score.pass_threshold == null
|
|
490
|
-
? ''
|
|
491
|
-
: ', passes at ' + inUnit(run.score.pass_threshold, run.score.unit))
|
|
492
|
-
),
|
|
493
|
-
runnerPill(run),
|
|
494
|
-
h('span', {}, run.actor),
|
|
495
|
-
webUrl(run.url) ? h('a', { href: run.url }, run.url) : h('span', {}, run.url),
|
|
496
|
-
h('span', {}, when(run.started_at))
|
|
497
|
-
),
|
|
498
|
-
run.summary && h('p', {}, run.summary),
|
|
499
|
-
webUrl(run.runner.url) &&
|
|
500
|
-
h('p', {}, h('a', { href: run.runner.url }, 'Open this run in Stunt Double')),
|
|
501
|
-
actions,
|
|
502
|
-
running
|
|
503
|
-
? h(
|
|
504
|
-
'div',
|
|
505
|
-
{ class: 'split' },
|
|
2217
|
+
'section',
|
|
2218
|
+
{ class: 'section' },
|
|
506
2219
|
h(
|
|
507
|
-
'
|
|
508
|
-
{},
|
|
509
|
-
|
|
510
|
-
h('
|
|
511
|
-
class: 'frame',
|
|
512
|
-
id: 'frame',
|
|
513
|
-
src: fileUrl('.live/latest.png') + '?v=' + Date.now(),
|
|
514
|
-
alt: 'The browser now',
|
|
515
|
-
})
|
|
2220
|
+
'h2',
|
|
2221
|
+
{ class: 'section-title' },
|
|
2222
|
+
group.title,
|
|
2223
|
+
h('span', { class: 'count' }, pad(group.checks.length))
|
|
516
2224
|
),
|
|
517
|
-
h('div', {}, h('h2', {}, 'Checks'), verdicts)
|
|
518
|
-
)
|
|
519
|
-
: [h('h2', {}, 'Checks'), verdicts],
|
|
520
|
-
data.activity.length > 0 && [
|
|
521
|
-
h('h2', {}, 'Activity'),
|
|
522
|
-
h(
|
|
523
|
-
'ol',
|
|
524
|
-
{ class: 'card timeline' },
|
|
525
|
-
data.activity.map((line) =>
|
|
526
2225
|
h(
|
|
527
|
-
'
|
|
528
|
-
{},
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
2226
|
+
'ul',
|
|
2227
|
+
{ class: 'group' },
|
|
2228
|
+
group.checks.map((check) =>
|
|
2229
|
+
checkRow(run, check, results.get(check.key), {
|
|
2230
|
+
running,
|
|
2231
|
+
openByDefault: group.openByDefault,
|
|
2232
|
+
arrived: results.has(check.key) && arrived(check.key),
|
|
2233
|
+
})
|
|
2234
|
+
)
|
|
532
2235
|
)
|
|
533
2236
|
)
|
|
534
2237
|
),
|
|
535
|
-
|
|
2238
|
+
data.activity.length > 0 &&
|
|
2239
|
+
h(
|
|
2240
|
+
'section',
|
|
2241
|
+
{ class: 'section' },
|
|
2242
|
+
h(
|
|
2243
|
+
'h2',
|
|
2244
|
+
{ class: 'section-title' },
|
|
2245
|
+
'Activity',
|
|
2246
|
+
h('span', { class: 'count' }, pad(data.activity.length))
|
|
2247
|
+
),
|
|
2248
|
+
h(
|
|
2249
|
+
'ol',
|
|
2250
|
+
{ class: 'group activity' },
|
|
2251
|
+
data.activity.map((line) =>
|
|
2252
|
+
h(
|
|
2253
|
+
'li',
|
|
2254
|
+
{},
|
|
2255
|
+
time(line.at, 'clock'),
|
|
2256
|
+
h('code', {}, line.tool),
|
|
2257
|
+
h('span', { class: 'text' }, line.text)
|
|
2258
|
+
)
|
|
2259
|
+
)
|
|
2260
|
+
)
|
|
2261
|
+
)
|
|
2262
|
+
),
|
|
536
2263
|
];
|
|
537
2264
|
}
|
|
538
2265
|
|
|
539
2266
|
function definitionError(entry) {
|
|
540
2267
|
return h(
|
|
541
|
-
'
|
|
542
|
-
{},
|
|
543
|
-
h(
|
|
544
|
-
|
|
2268
|
+
'li',
|
|
2269
|
+
{ class: 'def' },
|
|
2270
|
+
h(
|
|
2271
|
+
'div',
|
|
2272
|
+
{ class: 'def-head' },
|
|
2273
|
+
h(
|
|
2274
|
+
'span',
|
|
2275
|
+
{ class: 'def-name' },
|
|
2276
|
+
icon('alert-triangle', 'tone-error'),
|
|
2277
|
+
h('code', {}, entry.slug)
|
|
2278
|
+
),
|
|
2279
|
+
h('span', { class: 'badge warning' }, 'Unreadable')
|
|
2280
|
+
),
|
|
2281
|
+
h('p', { class: 'def-body error-text' }, entry.error)
|
|
545
2282
|
);
|
|
546
2283
|
}
|
|
547
2284
|
|
|
@@ -549,107 +2286,107 @@
|
|
|
549
2286
|
const { checklists, actors } = await getJson('/api/definitions');
|
|
550
2287
|
const cloudIds = (cloud) =>
|
|
551
2288
|
cloud && Object.values(cloud).some((v) => v && typeof v !== 'object')
|
|
552
|
-
? h('span', { class: '
|
|
553
|
-
: h('span', { class: '
|
|
554
|
-
|
|
555
|
-
h('h1', {}, 'Checklists and actors'),
|
|
2289
|
+
? h('span', { class: 'badge arc' }, 'Linked to the cloud')
|
|
2290
|
+
: h('span', { class: 'badge secondary' }, 'Local only');
|
|
2291
|
+
const section = (title, entries, mark, empty, row) =>
|
|
556
2292
|
h(
|
|
557
|
-
'
|
|
558
|
-
{ class: '
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
'div',
|
|
572
|
-
{ class: 'check' },
|
|
573
|
-
h(
|
|
574
|
-
'header',
|
|
575
|
-
{},
|
|
576
|
-
h('strong', {}, entry.data.name),
|
|
577
|
-
cloudIds(entry.data.cloud)
|
|
578
|
-
),
|
|
579
|
-
h(
|
|
580
|
-
'div',
|
|
581
|
-
{ class: 'meta' },
|
|
582
|
-
h('code', {}, entry.slug),
|
|
583
|
-
h('span', {}, 'run by ' + entry.data.actor),
|
|
584
|
-
h('span', {}, entry.data.url)
|
|
585
|
-
),
|
|
586
|
-
h('p', {}, entry.data.instructions),
|
|
587
|
-
h(
|
|
588
|
-
'ul',
|
|
589
|
-
{},
|
|
590
|
-
entry.data.checks.map((c) =>
|
|
591
|
-
h(
|
|
592
|
-
'li',
|
|
593
|
-
{},
|
|
594
|
-
h('code', {}, c.key),
|
|
595
|
-
' ',
|
|
596
|
-
c.description,
|
|
597
|
-
' ',
|
|
598
|
-
h(
|
|
599
|
-
'span',
|
|
600
|
-
{ class: 'muted' },
|
|
601
|
-
c.type === 'weighted'
|
|
602
|
-
? 'weighted x' + (c.weight ?? 1)
|
|
603
|
-
: c.type.replace('_', '/')
|
|
604
|
-
)
|
|
605
|
-
)
|
|
606
|
-
)
|
|
607
|
-
)
|
|
608
|
-
)
|
|
2293
|
+
'section',
|
|
2294
|
+
{ class: 'section' },
|
|
2295
|
+
h(
|
|
2296
|
+
'h2',
|
|
2297
|
+
{ class: 'section-title large' },
|
|
2298
|
+
title,
|
|
2299
|
+
h('span', { class: 'count' }, pad(entries.length))
|
|
2300
|
+
),
|
|
2301
|
+
entries.length === 0
|
|
2302
|
+
? h('div', { class: 'card inline-empty' }, icon(mark), h('p', {}, empty))
|
|
2303
|
+
: h(
|
|
2304
|
+
'ul',
|
|
2305
|
+
{ class: 'group card' },
|
|
2306
|
+
entries.map((entry) => (entry.error ? definitionError(entry) : row(entry)))
|
|
609
2307
|
)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
2308
|
+
);
|
|
2309
|
+
return [
|
|
2310
|
+
pageHeader({
|
|
2311
|
+
title: 'Checklists and actors',
|
|
2312
|
+
description:
|
|
2313
|
+
'Read only. Edit them in Claude Code: they are the JSON files in .stuntdouble/.',
|
|
2314
|
+
}),
|
|
2315
|
+
h(
|
|
2316
|
+
'div',
|
|
2317
|
+
{ class: 'stack' },
|
|
2318
|
+
section('Checklists', checklists, 'checklist', 'No checklists yet.', (entry) =>
|
|
2319
|
+
h(
|
|
2320
|
+
'li',
|
|
2321
|
+
{ class: 'def' },
|
|
2322
|
+
h(
|
|
2323
|
+
'div',
|
|
2324
|
+
{ class: 'def-head' },
|
|
2325
|
+
h(
|
|
2326
|
+
'span',
|
|
2327
|
+
{ class: 'def-name' },
|
|
2328
|
+
icon('checklist'),
|
|
2329
|
+
h('span', { class: 'truncate' }, entry.data.name)
|
|
2330
|
+
),
|
|
2331
|
+
cloudIds(entry.data.cloud)
|
|
2332
|
+
),
|
|
2333
|
+
h(
|
|
2334
|
+
'div',
|
|
2335
|
+
{ class: 'def-facts' },
|
|
2336
|
+
h('code', {}, entry.slug),
|
|
2337
|
+
h('span', { class: 'fact' }, icon('actor'), 'Run by ' + entry.data.actor),
|
|
2338
|
+
h('span', { class: 'fact' }, icon('globe'), entry.data.url)
|
|
2339
|
+
),
|
|
2340
|
+
h('p', { class: 'def-body' }, entry.data.instructions),
|
|
2341
|
+
h(
|
|
2342
|
+
'ul',
|
|
2343
|
+
{ class: 'def-checks' },
|
|
2344
|
+
entry.data.checks.map((c) =>
|
|
2345
|
+
h('li', {}, h('span', {}, c.description), checkMeta(c))
|
|
2346
|
+
)
|
|
643
2347
|
)
|
|
644
|
-
)
|
|
2348
|
+
)
|
|
2349
|
+
),
|
|
2350
|
+
section('Actors', actors, 'actor', 'No actors yet.', (entry) =>
|
|
2351
|
+
h(
|
|
2352
|
+
'li',
|
|
2353
|
+
{ class: 'def' },
|
|
2354
|
+
h(
|
|
2355
|
+
'div',
|
|
2356
|
+
{ class: 'def-head' },
|
|
2357
|
+
h(
|
|
2358
|
+
'span',
|
|
2359
|
+
{ class: 'def-name' },
|
|
2360
|
+
icon('actor'),
|
|
2361
|
+
h('span', { class: 'truncate' }, entry.data.name)
|
|
2362
|
+
),
|
|
2363
|
+
cloudIds(entry.data.cloud)
|
|
2364
|
+
),
|
|
2365
|
+
h(
|
|
2366
|
+
'div',
|
|
2367
|
+
{ class: 'def-facts' },
|
|
2368
|
+
h('code', {}, entry.slug),
|
|
2369
|
+
entry.data.persist_login && h('span', {}, 'Keeps its logins')
|
|
2370
|
+
),
|
|
2371
|
+
entry.data.description && h('p', { class: 'def-body' }, entry.data.description),
|
|
2372
|
+
entry.data.knowledge.length > 0 &&
|
|
2373
|
+
h(
|
|
2374
|
+
'p',
|
|
2375
|
+
{ class: 'def-body fact' },
|
|
2376
|
+
icon('knowledge'),
|
|
2377
|
+
'Knows: ' + entry.data.knowledge.map((k) => k.title).join(', ')
|
|
2378
|
+
)
|
|
2379
|
+
)
|
|
2380
|
+
)
|
|
2381
|
+
),
|
|
645
2382
|
];
|
|
646
2383
|
}
|
|
647
2384
|
|
|
648
2385
|
let rendering = Promise.resolve();
|
|
649
|
-
function render() {
|
|
2386
|
+
function render(enter) {
|
|
650
2387
|
rendering = rendering.then(async () => {
|
|
651
2388
|
const path = location.pathname;
|
|
652
|
-
for (const link of document.querySelectorAll('
|
|
2389
|
+
for (const link of document.querySelectorAll('.bar nav a')) {
|
|
653
2390
|
const current = link.getAttribute('href') === (path.startsWith('/runs/') ? '/' : path);
|
|
654
2391
|
if (current) link.setAttribute('aria-current', 'page');
|
|
655
2392
|
else link.removeAttribute('aria-current');
|
|
@@ -662,9 +2399,27 @@
|
|
|
662
2399
|
? await renderDefinitions()
|
|
663
2400
|
: await renderRuns();
|
|
664
2401
|
} catch (err) {
|
|
665
|
-
view = [
|
|
2402
|
+
view = [
|
|
2403
|
+
h('div', { class: 'notice destructive' }, icon('octagon-x'), h('p', {}, err.message)),
|
|
2404
|
+
];
|
|
666
2405
|
}
|
|
2406
|
+
app.classList.toggle('narrow', path === '/definitions');
|
|
2407
|
+
// A live redraw replaces the view: keep keyboard focus where it was.
|
|
2408
|
+
const focused = document.activeElement?.closest?.('[data-focus-key]');
|
|
2409
|
+
const focusKey = focused && app.contains(focused) && focused.dataset.focusKey;
|
|
667
2410
|
app.replaceChildren(...[view].flat(2).filter(Boolean));
|
|
2411
|
+
if (focusKey) {
|
|
2412
|
+
app
|
|
2413
|
+
.querySelector('[data-focus-key="' + CSS.escape(focusKey) + '"]')
|
|
2414
|
+
?.focus({ preventScroll: true });
|
|
2415
|
+
}
|
|
2416
|
+
// A view the reader went to rises in once. Redraws for live updates,
|
|
2417
|
+
// and anything reached from the keyboard, cut.
|
|
2418
|
+
if (enter) {
|
|
2419
|
+
app.classList.remove('animate-page-in');
|
|
2420
|
+
void app.offsetWidth;
|
|
2421
|
+
app.classList.add('animate-page-in');
|
|
2422
|
+
}
|
|
668
2423
|
});
|
|
669
2424
|
}
|
|
670
2425
|
|
|
@@ -673,9 +2428,10 @@
|
|
|
673
2428
|
if (!link || event.metaKey || event.ctrlKey) return;
|
|
674
2429
|
event.preventDefault();
|
|
675
2430
|
history.pushState(null, '', link.getAttribute('href'));
|
|
676
|
-
|
|
2431
|
+
window.scrollTo(0, 0);
|
|
2432
|
+
render(event.detail > 0);
|
|
677
2433
|
});
|
|
678
|
-
window.addEventListener('popstate', render);
|
|
2434
|
+
window.addEventListener('popstate', () => render(false));
|
|
679
2435
|
|
|
680
2436
|
// Live updates: a new frame swaps the picture, anything else redraws the page.
|
|
681
2437
|
let queued = false;
|
|
@@ -689,10 +2445,12 @@
|
|
|
689
2445
|
queued = true;
|
|
690
2446
|
setTimeout(() => {
|
|
691
2447
|
queued = false;
|
|
692
|
-
render();
|
|
2448
|
+
render(false);
|
|
693
2449
|
}, 300);
|
|
694
2450
|
};
|
|
695
|
-
|
|
2451
|
+
// Relative times on the runs list stay current.
|
|
2452
|
+
setInterval(() => location.pathname === '/' && render(false), 60_000);
|
|
2453
|
+
render(false);
|
|
696
2454
|
</script>
|
|
697
2455
|
</body>
|
|
698
2456
|
</html>
|