@kaizenreport/kensho-viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/app.js ADDED
@@ -0,0 +1,1396 @@
1
+ /* Auto-generated from app.jsx by packages/viewer/scripts/build.js. Edit the .jsx — DO NOT edit this file. */
2
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
3
+ /* global React, ReactDOM, lucide */
4
+ // =============================================================
5
+ // Kensho viewer — root App + router + keyboard shortcuts.
6
+ // Loaded LAST: depends on every other window.* component.
7
+ // =============================================================
8
+
9
+ const {
10
+ useState: useStateA,
11
+ useEffect: useEffectA
12
+ } = React;
13
+
14
+ // Stable null-context for the static-report path (mirrors the pattern in
15
+ // components.jsx / tree-detail.jsx). Always calling useContext keeps the
16
+ // hook order stable across the static and embedded mount paths.
17
+ const _kvAppNullCtx = React.createContext(null);
18
+ function fmtDuration(ms) {
19
+ return window._kenshoFmtDuration ? window._kenshoFmtDuration(ms) : ms + 'ms';
20
+ }
21
+ function relTime(iso) {
22
+ return window._kenshoRelTime ? window._kenshoRelTime(iso) : '';
23
+ }
24
+ const PAGE_NAMES = ['overview', 'suites', 'graphs', 'timeline', 'categories', 'flaky', 'behaviors', 'packages', 'history'];
25
+
26
+ // Parse `#/case/<id>` or `#/page/<name>` (defaults: page=overview, no case).
27
+ function parseHash() {
28
+ const h = (window.location.hash || '').replace(/^#\/?/, '');
29
+ if (!h) return {
30
+ page: 'overview',
31
+ caseId: null
32
+ };
33
+ const parts = h.split('/');
34
+ if (parts[0] === 'case' && parts[1]) return {
35
+ page: null,
36
+ caseId: decodeURIComponent(parts[1])
37
+ };
38
+ if (parts[0] === 'page' && parts[1] && PAGE_NAMES.includes(parts[1])) return {
39
+ page: parts[1],
40
+ caseId: null
41
+ };
42
+ // Bare page name in hash (e.g. #suites) — accept for niceness.
43
+ if (PAGE_NAMES.includes(parts[0])) return {
44
+ page: parts[0],
45
+ caseId: null
46
+ };
47
+ return {
48
+ page: 'overview',
49
+ caseId: null
50
+ };
51
+ }
52
+
53
+ // Build the long URL form so users can copy a permalink to a specific case.
54
+ function caseHashHref(id) {
55
+ return '#/case/' + encodeURIComponent(id);
56
+ }
57
+
58
+ // SummaryKpi — single tile inside the Summary hero's KPI band. Used as a
59
+ // 3×2 grid; we draw separators with `border` prop ("left" / "top" / "top-left")
60
+ // to avoid double-borders between adjacent tiles.
61
+ function SummaryKpi({
62
+ label,
63
+ value,
64
+ hint,
65
+ accent,
66
+ border,
67
+ onClick
68
+ }) {
69
+ const clickable = !!onClick;
70
+ return /*#__PURE__*/React.createElement("div", {
71
+ onClick: onClick || undefined,
72
+ style: {
73
+ position: 'relative',
74
+ padding: '14px 18px',
75
+ borderLeft: border === 'left' || border === 'top-left' ? '1px solid var(--line)' : 'none',
76
+ borderTop: border === 'top' || border === 'top-left' ? '1px solid var(--line)' : 'none',
77
+ cursor: clickable ? 'pointer' : 'default',
78
+ transition: 'background var(--dur-fast)',
79
+ minWidth: 0
80
+ },
81
+ onMouseEnter: clickable ? e => e.currentTarget.style.background = 'var(--bg-hover)' : undefined,
82
+ onMouseLeave: clickable ? e => e.currentTarget.style.background = 'transparent' : undefined
83
+ }, /*#__PURE__*/React.createElement("div", {
84
+ style: {
85
+ display: 'flex',
86
+ alignItems: 'center',
87
+ gap: 8,
88
+ marginBottom: 6
89
+ }
90
+ }, /*#__PURE__*/React.createElement("span", {
91
+ style: {
92
+ width: 6,
93
+ height: 6,
94
+ borderRadius: 999,
95
+ background: accent || 'var(--fg3)'
96
+ }
97
+ }), /*#__PURE__*/React.createElement("span", {
98
+ style: {
99
+ fontFamily: 'var(--font-mono)',
100
+ fontSize: 10.5,
101
+ color: 'var(--fg3)',
102
+ letterSpacing: '.12em',
103
+ textTransform: 'uppercase'
104
+ }
105
+ }, label)), /*#__PURE__*/React.createElement("div", {
106
+ style: {
107
+ fontFamily: 'var(--font-display)',
108
+ fontSize: 24,
109
+ fontWeight: 700,
110
+ letterSpacing: -0.4,
111
+ color: 'var(--fg1)',
112
+ lineHeight: 1,
113
+ fontVariantNumeric: 'tabular-nums',
114
+ marginBottom: hint ? 6 : 0
115
+ }
116
+ }, value), hint && /*#__PURE__*/React.createElement("div", {
117
+ style: {
118
+ fontFamily: 'var(--font-body)',
119
+ fontSize: 11.5,
120
+ color: 'var(--fg3)',
121
+ overflow: 'hidden',
122
+ textOverflow: 'ellipsis',
123
+ whiteSpace: 'nowrap'
124
+ },
125
+ title: hint
126
+ }, hint));
127
+ }
128
+
129
+ // =============================================================
130
+ // Toast host — minimal, top-right, auto-dismiss. Used by the
131
+ // "copy link" affordance and could be reused by future actions.
132
+ // =============================================================
133
+ function ToastHost() {
134
+ const [toasts, setToasts] = useStateA([]);
135
+ useEffectA(() => {
136
+ window.__kenshoToast = msg => {
137
+ const id = Math.random().toString(36).slice(2, 8);
138
+ setToasts(t => [...t, {
139
+ id,
140
+ msg
141
+ }]);
142
+ setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 2200);
143
+ };
144
+ return () => {
145
+ delete window.__kenshoToast;
146
+ };
147
+ }, []);
148
+ return /*#__PURE__*/React.createElement("div", {
149
+ style: {
150
+ position: 'fixed',
151
+ top: 20,
152
+ right: 20,
153
+ display: 'flex',
154
+ flexDirection: 'column',
155
+ gap: 8,
156
+ zIndex: 700,
157
+ pointerEvents: 'none'
158
+ }
159
+ }, toasts.map(t => /*#__PURE__*/React.createElement("div", {
160
+ key: t.id,
161
+ style: {
162
+ background: 'var(--fg1)',
163
+ color: 'var(--bg-elev)',
164
+ fontFamily: 'var(--font-body)',
165
+ fontSize: 13,
166
+ fontWeight: 500,
167
+ padding: '10px 14px',
168
+ borderRadius: 8,
169
+ boxShadow: '0 6px 20px rgba(0,0,0,0.15)',
170
+ display: 'flex',
171
+ alignItems: 'center',
172
+ gap: 8,
173
+ animation: 'kvToastIn 200ms ease-out'
174
+ }
175
+ }, /*#__PURE__*/React.createElement("span", {
176
+ style: {
177
+ color: 'var(--status-passed)',
178
+ fontWeight: 700
179
+ }
180
+ }, "\u2713"), t.msg)));
181
+ }
182
+
183
+ // =============================================================
184
+ // ShortcutsOverlay — opened by `?`. Plain modal, dismiss on Esc/click.
185
+ // =============================================================
186
+ function ShortcutsOverlay({
187
+ open,
188
+ onClose
189
+ }) {
190
+ if (!open) return null;
191
+ const ROWS = [['Navigation', [['/', 'Focus search on the current tree page'], ['j / k', 'Move selection down / up among visible tests'], ['Enter', 'Open the selected test in the detail pane'], ['Esc', 'Close detail pane or clear search']]], ['Go to page', [['g o', 'Overview'], ['g s', 'Suites'], ['g g', 'Graphs'], ['g f', 'Flaky'], ['g h', 'History']]], ['Help', [['?', 'Toggle this overlay']]]];
192
+ return /*#__PURE__*/React.createElement("div", {
193
+ onClick: onClose,
194
+ style: {
195
+ position: 'fixed',
196
+ inset: 0,
197
+ background: 'rgba(0,0,0,0.45)',
198
+ display: 'flex',
199
+ alignItems: 'center',
200
+ justifyContent: 'center',
201
+ zIndex: 600
202
+ }
203
+ }, /*#__PURE__*/React.createElement("div", {
204
+ onClick: e => e.stopPropagation(),
205
+ style: {
206
+ background: 'var(--bg-elev)',
207
+ border: '1px solid var(--line)',
208
+ borderRadius: 12,
209
+ width: 'min(540px, 92vw)',
210
+ padding: '24px 28px',
211
+ boxShadow: '0 20px 60px rgba(0,0,0,0.35)'
212
+ }
213
+ }, /*#__PURE__*/React.createElement("div", {
214
+ style: {
215
+ display: 'flex',
216
+ alignItems: 'baseline',
217
+ gap: 10,
218
+ marginBottom: 18
219
+ }
220
+ }, /*#__PURE__*/React.createElement("h2", {
221
+ style: {
222
+ margin: 0,
223
+ fontFamily: 'var(--font-display)',
224
+ fontSize: 20,
225
+ fontWeight: 700,
226
+ color: 'var(--fg1)'
227
+ }
228
+ }, "Keyboard shortcuts"), /*#__PURE__*/React.createElement("span", {
229
+ style: {
230
+ fontFamily: 'var(--font-mono)',
231
+ fontSize: 11,
232
+ color: 'var(--fg3)'
233
+ }
234
+ }, "press ", /*#__PURE__*/React.createElement("kbd", {
235
+ style: kbdStyle()
236
+ }, "Esc"), " to close")), ROWS.map(([group, items]) => /*#__PURE__*/React.createElement("div", {
237
+ key: group,
238
+ style: {
239
+ marginBottom: 16
240
+ }
241
+ }, /*#__PURE__*/React.createElement("div", {
242
+ className: "k-overline",
243
+ style: {
244
+ marginBottom: 8
245
+ }
246
+ }, group), /*#__PURE__*/React.createElement("div", {
247
+ style: {
248
+ display: 'grid',
249
+ gridTemplateColumns: '120px 1fr',
250
+ rowGap: 6,
251
+ columnGap: 14
252
+ }
253
+ }, items.map(([keys, desc]) => /*#__PURE__*/React.createElement(React.Fragment, {
254
+ key: keys
255
+ }, /*#__PURE__*/React.createElement("div", {
256
+ style: {
257
+ display: 'flex',
258
+ gap: 4,
259
+ alignItems: 'center'
260
+ }
261
+ }, keys.split(' ').map((k, i) => /*#__PURE__*/React.createElement(React.Fragment, {
262
+ key: i
263
+ }, i > 0 && /*#__PURE__*/React.createElement("span", {
264
+ style: {
265
+ fontFamily: 'var(--font-mono)',
266
+ fontSize: 10,
267
+ color: 'var(--fg3)'
268
+ }
269
+ }, "then"), /*#__PURE__*/React.createElement("kbd", {
270
+ style: kbdStyle()
271
+ }, k)))), /*#__PURE__*/React.createElement("div", {
272
+ style: {
273
+ fontFamily: 'var(--font-body)',
274
+ fontSize: 13,
275
+ color: 'var(--fg2)'
276
+ }
277
+ }, desc))))))));
278
+ }
279
+ function kbdStyle() {
280
+ return {
281
+ fontFamily: 'var(--font-mono)',
282
+ fontSize: 11,
283
+ fontWeight: 600,
284
+ padding: '2px 6px',
285
+ borderRadius: 4,
286
+ border: '1px solid var(--line)',
287
+ background: 'var(--bg-sunken)',
288
+ color: 'var(--fg1)',
289
+ minWidth: 18,
290
+ display: 'inline-flex',
291
+ justifyContent: 'center',
292
+ alignItems: 'center'
293
+ };
294
+ }
295
+
296
+ // =============================================================
297
+ // App — root component.
298
+ // =============================================================
299
+ function App() {
300
+ // Pull embed-mode extras / callbacks from the host's context (if any).
301
+ // Static-report path: ctx === null → behave as before (hash routing,
302
+ // keyboard shortcuts, no extras).
303
+ const ctx = React.useContext(window.__KenshoContext || _kvAppNullCtx);
304
+ const ownKeyboard = !!ctx?.ownKeyboard;
305
+ const extraSidebar = ctx?.extraSidebar || [];
306
+ const initial = parseHash();
307
+ const [page, setPage] = useStateA(ctx?.page ?? (initial.page || 'overview'));
308
+ const [selected, setSelected] = useStateA(null);
309
+ const [tab, setTab] = useStateA('steps');
310
+ const [shortcutsOpen, setShortcutsOpen] = useStateA(false);
311
+
312
+ // Icons render inline via the Icon component (see components.jsx); no
313
+ // global lucide.createIcons() pass — it would rewrite the host page's
314
+ // <i data-lucide=> elements when embedded inside another React app.
315
+
316
+ // When the host pushes a new page/case via context (controlled mode),
317
+ // sync local state. Only active when ownKeyboard is set.
318
+ useEffectA(() => {
319
+ if (!ownKeyboard) return;
320
+ if (ctx?.page && ctx.page !== page) setPage(ctx.page);
321
+ }, [ctx?.page, ownKeyboard]);
322
+
323
+ // Open a test by id, returning whether we found one.
324
+ const openTestById = React.useCallback(testId => {
325
+ const t = window.RICH_TESTS?.[testId];
326
+ if (!t) return false;
327
+ setSelected({
328
+ ns: '',
329
+ name: t.name,
330
+ status: t.status,
331
+ duration: t.dur,
332
+ retries: t.retries,
333
+ richId: t.id
334
+ });
335
+ setTab('steps');
336
+ return true;
337
+ }, []);
338
+
339
+ // ---- Hash router ----
340
+ // Treat the URL hash as the source of truth for "page" + "open case" so
341
+ // the back/forward buttons and copyable permalinks both work without
342
+ // pulling in a routing library.
343
+ // When the host owns navigation (`ownKeyboard: true`), skip hash routing
344
+ // — the host updates the URL itself and we react to its callbacks.
345
+ useEffectA(() => {
346
+ if (ownKeyboard) return;
347
+ const onHash = () => {
348
+ const {
349
+ page: p,
350
+ caseId
351
+ } = parseHash();
352
+ if (caseId) {
353
+ const ok = openTestById(caseId);
354
+ if (!ok) setSelected(null);
355
+ } else if (p) {
356
+ setSelected(null);
357
+ setPage(p);
358
+ }
359
+ };
360
+ window.addEventListener('hashchange', onHash);
361
+ onHash();
362
+ return () => window.removeEventListener('hashchange', onHash);
363
+ }, [openTestById, ownKeyboard]);
364
+
365
+ // ---- Global navigation hooks ----
366
+ // Mirror page/test changes back into the URL hash so refreshes preserve state.
367
+ // Embed-mode (ownKeyboard) skips the URL write and fires onCaseOpen /
368
+ // onPageChange instead.
369
+ useEffectA(() => {
370
+ const navTo = p => {
371
+ setSelected(null);
372
+ setPage(p);
373
+ if (ownKeyboard) {
374
+ ctx?.onPageChange?.(p);
375
+ } else {
376
+ const next = '#/page/' + p;
377
+ if (window.location.hash !== next) history.pushState(null, '', next);
378
+ }
379
+ };
380
+ const openTest = testId => {
381
+ if (!openTestById(testId)) return;
382
+ if (ownKeyboard) {
383
+ ctx?.onCaseOpen?.(testId);
384
+ } else {
385
+ const next = caseHashHref(testId);
386
+ if (window.location.hash !== next) history.pushState(null, '', next);
387
+ }
388
+ };
389
+ window.__navTo = navTo;
390
+ window.__openTest = openTest;
391
+ return () => {
392
+ delete window.__navTo;
393
+ delete window.__openTest;
394
+ };
395
+ }, [openTestById, ownKeyboard, ctx]);
396
+
397
+ // ---- Keyboard shortcuts ----
398
+ // Skipped entirely when the host owns the keyboard (embed mode) so we
399
+ // don't intercept their app-level chords.
400
+ useEffectA(() => {
401
+ if (ownKeyboard) return;
402
+ let pendingG = false;
403
+ let gTimer = null;
404
+ const isTextInput = el => {
405
+ if (!el) return false;
406
+ const tag = el.tagName;
407
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
408
+ if (el.isContentEditable) return true;
409
+ return false;
410
+ };
411
+ const focusTreeSearch = () => {
412
+ const el = document.querySelector('.kv-tree-search input, [data-kv-search] input');
413
+ if (el) {
414
+ el.focus();
415
+ el.select?.();
416
+ return true;
417
+ }
418
+ return false;
419
+ };
420
+ const moveSelection = delta => {
421
+ const ev = new CustomEvent('kensho:move-selection', {
422
+ detail: {
423
+ delta
424
+ }
425
+ });
426
+ window.dispatchEvent(ev);
427
+ };
428
+ const enterSelection = () => {
429
+ const ev = new CustomEvent('kensho:open-selection');
430
+ window.dispatchEvent(ev);
431
+ };
432
+ const escapeAction = () => {
433
+ // 1. close shortcuts overlay if open
434
+ // 2. close detail pane (back to tree placeholder)
435
+ // 3. otherwise dispatch clear-search to active tree page
436
+ if (shortcutsOpen) {
437
+ setShortcutsOpen(false);
438
+ return;
439
+ }
440
+ if (selected) {
441
+ setSelected(null);
442
+ history.pushState(null, '', '#/page/' + page);
443
+ return;
444
+ }
445
+ window.dispatchEvent(new CustomEvent('kensho:clear-search'));
446
+ };
447
+ const onKeyDown = e => {
448
+ if (isTextInput(e.target)) {
449
+ // Allow Esc in text input → blur + clear search via custom event.
450
+ if (e.key === 'Escape') {
451
+ e.target.blur();
452
+ window.dispatchEvent(new CustomEvent('kensho:clear-search'));
453
+ }
454
+ return;
455
+ }
456
+ // Modifier-laden combos belong to the browser/OS.
457
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
458
+
459
+ // `g` chord — wait up to 1.5s for the next key.
460
+ if (pendingG) {
461
+ const code = e.key.toLowerCase();
462
+ const map = {
463
+ o: 'overview',
464
+ s: 'suites',
465
+ g: 'graphs',
466
+ f: 'flaky',
467
+ h: 'history'
468
+ };
469
+ if (map[code]) {
470
+ e.preventDefault();
471
+ window.__navTo?.(map[code]);
472
+ }
473
+ pendingG = false;
474
+ clearTimeout(gTimer);
475
+ return;
476
+ }
477
+ switch (e.key) {
478
+ case '/':
479
+ {
480
+ e.preventDefault();
481
+ if (!focusTreeSearch()) {
482
+ // No tree search on the current page — jump to Suites and try again.
483
+ window.__navTo?.('suites');
484
+ setTimeout(focusTreeSearch, 50);
485
+ }
486
+ return;
487
+ }
488
+ case 'j':
489
+ e.preventDefault();
490
+ moveSelection(+1);
491
+ return;
492
+ case 'k':
493
+ e.preventDefault();
494
+ moveSelection(-1);
495
+ return;
496
+ case 'Enter':
497
+ e.preventDefault();
498
+ enterSelection();
499
+ return;
500
+ case 'Escape':
501
+ e.preventDefault();
502
+ escapeAction();
503
+ return;
504
+ case '?':
505
+ e.preventDefault();
506
+ setShortcutsOpen(o => !o);
507
+ return;
508
+ case 'g':
509
+ pendingG = true;
510
+ gTimer = setTimeout(() => {
511
+ pendingG = false;
512
+ }, 1500);
513
+ return;
514
+ default:
515
+ return;
516
+ }
517
+ };
518
+ window.addEventListener('keydown', onKeyDown);
519
+ return () => {
520
+ window.removeEventListener('keydown', onKeyDown);
521
+ clearTimeout(gTimer);
522
+ };
523
+ }, [page, selected, shortcutsOpen, ownKeyboard]);
524
+ const RUN = window.RUN;
525
+ const project = window.KENSHO_INDEX?.project || {
526
+ name: 'Kensho'
527
+ };
528
+ return /*#__PURE__*/React.createElement("div", {
529
+ className: "app"
530
+ }, /*#__PURE__*/React.createElement(Sidebar, {
531
+ active: page,
532
+ onNav: p => {
533
+ window.__navTo?.(p);
534
+ }
535
+ }), /*#__PURE__*/React.createElement("div", {
536
+ className: "right-col"
537
+ }, /*#__PURE__*/React.createElement(TopBar, {
538
+ crumbs: selected ? ['Run ' + RUN.id, 'Tests', selected.ns + selected.name] : ['Run ' + RUN.id, page === 'flaky' ? 'Flaky tests' : page[0].toUpperCase() + page.slice(1)],
539
+ onRerun: () => alert('Re-run hooks are configured by the integrating CI. Wire to your runner.'),
540
+ project: project
541
+ }), /*#__PURE__*/React.createElement("div", {
542
+ className: "main"
543
+ }, (() => {
544
+ if (selected) return /*#__PURE__*/React.createElement(Detail, {
545
+ test: selected,
546
+ onBack: () => {
547
+ setSelected(null);
548
+ if (!ownKeyboard) history.pushState(null, '', '#/page/' + page);
549
+ }
550
+ });
551
+ const ex = extraSidebar.find(x => x.id === page);
552
+ if (ex) return ex.render();
553
+ switch (page) {
554
+ case 'graphs':
555
+ return /*#__PURE__*/React.createElement(GraphsPage, null);
556
+ case 'timeline':
557
+ return /*#__PURE__*/React.createElement(TimelinePage, null);
558
+ case 'categories':
559
+ return /*#__PURE__*/React.createElement(CategoriesPage, null);
560
+ case 'flaky':
561
+ return /*#__PURE__*/React.createElement(FlakyPage, null);
562
+ case 'behaviors':
563
+ return /*#__PURE__*/React.createElement(BehaviorsPage, null);
564
+ case 'packages':
565
+ return /*#__PURE__*/React.createElement(PackagesPage, null);
566
+ case 'history':
567
+ return /*#__PURE__*/React.createElement(HistoryPage, null);
568
+ case 'suites':
569
+ return /*#__PURE__*/React.createElement(SuitesView, {
570
+ onOpen: t => window.__openTest?.(t.richId)
571
+ });
572
+ default:
573
+ return /*#__PURE__*/React.createElement(Overview, {
574
+ onOpen: t => window.__openTest?.(t.richId)
575
+ });
576
+ }
577
+ })())), /*#__PURE__*/React.createElement(ToastHost, null), /*#__PURE__*/React.createElement(ShortcutsOverlay, {
578
+ open: shortcutsOpen,
579
+ onClose: () => setShortcutsOpen(false)
580
+ }));
581
+ }
582
+
583
+ // =============================================================
584
+ // Overview — drag-to-reorder card grid.
585
+ // =============================================================
586
+ function SingleRunTrend() {
587
+ const RUN = window.RUN;
588
+ const c = RUN.counts;
589
+ const total = c.passed + c.failed + c.broken + c.skipped || 1;
590
+ const passRate = Math.round(c.passed / total * 100);
591
+ const SEGS = [['passed', c.passed, 'var(--status-passed)'], ['skipped', c.skipped, 'var(--status-skipped)'], ['broken', c.broken, 'var(--status-broken)'], ['failed', c.failed, 'var(--status-failed)']].filter(([, n]) => n > 0);
592
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
593
+ style: {
594
+ display: 'grid',
595
+ gridTemplateColumns: '1fr auto auto auto auto',
596
+ gap: 18,
597
+ alignItems: 'baseline',
598
+ padding: '4px 4px 14px',
599
+ borderBottom: '1px solid var(--line)',
600
+ marginBottom: 14
601
+ }
602
+ }, /*#__PURE__*/React.createElement("div", {
603
+ style: {
604
+ minWidth: 0
605
+ }
606
+ }, /*#__PURE__*/React.createElement("div", {
607
+ style: {
608
+ fontFamily: 'var(--font-mono)',
609
+ fontSize: 10.5,
610
+ color: 'var(--fg3)',
611
+ letterSpacing: 1.2,
612
+ textTransform: 'uppercase'
613
+ }
614
+ }, "Current run"), /*#__PURE__*/React.createElement("div", {
615
+ style: {
616
+ display: 'flex',
617
+ alignItems: 'baseline',
618
+ gap: 8,
619
+ marginTop: 4
620
+ }
621
+ }, /*#__PURE__*/React.createElement("span", {
622
+ style: {
623
+ fontFamily: 'var(--font-display)',
624
+ fontSize: 26,
625
+ fontWeight: 700,
626
+ color: 'var(--fg1)',
627
+ letterSpacing: -0.5,
628
+ lineHeight: 1,
629
+ fontVariantNumeric: 'tabular-nums'
630
+ }
631
+ }, passRate, /*#__PURE__*/React.createElement("span", {
632
+ style: {
633
+ fontSize: 14,
634
+ color: 'var(--fg3)',
635
+ marginLeft: 2
636
+ }
637
+ }, "%")), /*#__PURE__*/React.createElement("span", {
638
+ style: {
639
+ fontFamily: 'var(--font-mono)',
640
+ fontSize: 11,
641
+ color: 'var(--fg3)'
642
+ }
643
+ }, "pass rate"))), [['passed', c.passed, 'var(--status-passed)'], ['skipped', c.skipped, 'var(--status-skipped)'], ['broken', c.broken, 'var(--status-broken)'], ['failed', c.failed, 'var(--status-failed)']].map(([k, n, color]) => /*#__PURE__*/React.createElement("div", {
644
+ key: k,
645
+ style: {
646
+ textAlign: 'right',
647
+ minWidth: 60
648
+ }
649
+ }, /*#__PURE__*/React.createElement("div", {
650
+ style: {
651
+ display: 'flex',
652
+ alignItems: 'center',
653
+ gap: 5,
654
+ justifyContent: 'flex-end',
655
+ fontFamily: 'var(--font-mono)',
656
+ fontSize: 10,
657
+ color: 'var(--fg3)',
658
+ textTransform: 'uppercase',
659
+ letterSpacing: 0.5
660
+ }
661
+ }, /*#__PURE__*/React.createElement("span", {
662
+ style: {
663
+ width: 8,
664
+ height: 8,
665
+ borderRadius: 2,
666
+ background: color
667
+ }
668
+ }), k), /*#__PURE__*/React.createElement("div", {
669
+ style: {
670
+ fontFamily: 'var(--font-mono)',
671
+ fontSize: 15,
672
+ fontWeight: 600,
673
+ color: 'var(--fg1)',
674
+ fontVariantNumeric: 'tabular-nums',
675
+ marginTop: 2
676
+ }
677
+ }, n)))), /*#__PURE__*/React.createElement("div", {
678
+ style: {
679
+ height: 48,
680
+ background: 'var(--bg-sunken)',
681
+ borderRadius: 6,
682
+ display: 'flex',
683
+ overflow: 'hidden',
684
+ border: '1px solid var(--line)'
685
+ }
686
+ }, SEGS.map(([k, n, color]) => /*#__PURE__*/React.createElement("div", {
687
+ key: k,
688
+ title: `${n} ${k}`,
689
+ style: {
690
+ width: `${n / total * 100}%`,
691
+ background: color,
692
+ display: 'flex',
693
+ alignItems: 'center',
694
+ justifyContent: 'flex-start',
695
+ padding: '0 10px',
696
+ color: '#fff',
697
+ fontFamily: 'var(--font-mono)',
698
+ fontSize: 12,
699
+ fontWeight: 700
700
+ }
701
+ }, n / total >= 0.05 ? n : ''))), /*#__PURE__*/React.createElement("div", {
702
+ style: {
703
+ display: 'flex',
704
+ justifyContent: 'space-between',
705
+ alignItems: 'center',
706
+ marginTop: 10,
707
+ fontFamily: 'var(--font-mono)',
708
+ fontSize: 11,
709
+ color: 'var(--fg3)'
710
+ }
711
+ }, /*#__PURE__*/React.createElement("span", null, total, " test", total === 1 ? '' : 's', " \xB7 1 run total"), /*#__PURE__*/React.createElement("span", {
712
+ style: {
713
+ display: 'inline-flex',
714
+ alignItems: 'center',
715
+ gap: 6
716
+ }
717
+ }, /*#__PURE__*/React.createElement("span", {
718
+ style: {
719
+ width: 6,
720
+ height: 6,
721
+ borderRadius: 999,
722
+ background: 'var(--brand-blue-500)'
723
+ }
724
+ }), "History will populate after the next ", /*#__PURE__*/React.createElement("code", {
725
+ style: {
726
+ fontFamily: 'var(--font-mono)',
727
+ fontSize: 10.5,
728
+ background: 'var(--bg-sunken)',
729
+ padding: '1px 6px',
730
+ borderRadius: 3,
731
+ color: 'var(--fg2)'
732
+ }
733
+ }, "kensho generate"))));
734
+ }
735
+ function EnvEmptyState() {
736
+ const SUPPORTED = [['Source control', ['branch', 'commit', 'author', 'commitMsg', 'repoUrl']], ['CI', ['ci', 'runUrl', 'workers', 'trigger']], ['App under test', ['stage', 'baseUrl', 'appVersion', 'release']], ['Browser / device', ['browsers', 'device', 'viewport', 'locale']], ['System', ['os', 'osVersion', 'arch', 'timezone']], ['Custom', ['vars (open key/value bag)']]];
737
+ return /*#__PURE__*/React.createElement("div", {
738
+ style: {
739
+ padding: '8px 0 4px'
740
+ }
741
+ }, /*#__PURE__*/React.createElement("div", {
742
+ style: {
743
+ color: 'var(--fg2)',
744
+ fontFamily: 'var(--font-body)',
745
+ fontSize: 13,
746
+ marginBottom: 14,
747
+ lineHeight: 1.5
748
+ }
749
+ }, "No environment variables in this run. Populate ", /*#__PURE__*/React.createElement("code", {
750
+ style: {
751
+ fontFamily: 'var(--font-mono)',
752
+ fontSize: 12,
753
+ background: 'var(--bg-sunken)',
754
+ padding: '1px 6px',
755
+ borderRadius: 3
756
+ }
757
+ }, "run.env.*"), " from your reporter to see them here."), /*#__PURE__*/React.createElement("div", {
758
+ style: {
759
+ display: 'flex',
760
+ flexDirection: 'column',
761
+ gap: 10
762
+ }
763
+ }, SUPPORTED.map(([group, keys]) => /*#__PURE__*/React.createElement("div", {
764
+ key: group
765
+ }, /*#__PURE__*/React.createElement("div", {
766
+ style: {
767
+ fontFamily: 'var(--font-mono)',
768
+ fontSize: 10.5,
769
+ color: 'var(--fg3)',
770
+ letterSpacing: '.08em',
771
+ textTransform: 'uppercase',
772
+ marginBottom: 4
773
+ }
774
+ }, group), /*#__PURE__*/React.createElement("div", {
775
+ style: {
776
+ display: 'flex',
777
+ flexWrap: 'wrap',
778
+ gap: 4
779
+ }
780
+ }, keys.map(k => /*#__PURE__*/React.createElement("span", {
781
+ key: k,
782
+ style: {
783
+ fontFamily: 'var(--font-mono)',
784
+ fontSize: 11,
785
+ color: 'var(--fg2)',
786
+ background: 'var(--bg-sunken)',
787
+ border: '1px solid var(--line)',
788
+ borderRadius: 4,
789
+ padding: '2px 6px'
790
+ }
791
+ }, k)))))));
792
+ }
793
+ function TestsCard({
794
+ tests,
795
+ onOpen
796
+ }) {
797
+ const [filter, setFilter] = useStateA('all');
798
+ const [page, setPage] = useStateA(0);
799
+ const PAGE_SIZE = 20;
800
+ const counts = {
801
+ all: tests.length,
802
+ passed: 0,
803
+ failed: 0,
804
+ broken: 0,
805
+ skipped: 0
806
+ };
807
+ for (const t of tests) counts[t.status] = (counts[t.status] || 0) + 1;
808
+ const SEV_RANK = {
809
+ blocker: 0,
810
+ critical: 1,
811
+ normal: 2,
812
+ minor: 3,
813
+ trivial: 4
814
+ };
815
+ const STATUS_RANK = {
816
+ failed: 0,
817
+ broken: 1,
818
+ skipped: 2,
819
+ passed: 3
820
+ };
821
+ const filtered = filter === 'all' ? [...tests].sort((a, b) => {
822
+ const oa = STATUS_RANK[a.status] ?? 9;
823
+ const ob = STATUS_RANK[b.status] ?? 9;
824
+ if (oa !== ob) return oa - ob;
825
+ if (a.status === 'failed' || a.status === 'broken') {
826
+ return (SEV_RANK[a.severity] ?? 9) - (SEV_RANK[b.severity] ?? 9);
827
+ }
828
+ return (a.order || 0) - (b.order || 0);
829
+ }) : tests.filter(t => t.status === filter);
830
+ const total = filtered.length;
831
+ const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
832
+ const safePage = Math.min(page, pages - 1);
833
+ const start = safePage * PAGE_SIZE;
834
+ const end = Math.min(total, start + PAGE_SIZE);
835
+ const visible = filtered.slice(start, end);
836
+ const PILLS = [['all', 'All'], ['failed', 'Failed'], ['broken', 'Broken'], ['skipped', 'Skipped'], ['passed', 'Passed']].filter(([id]) => id === 'all' || (counts[id] || 0) > 0);
837
+ const setF = f => {
838
+ setFilter(f);
839
+ setPage(0);
840
+ };
841
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
842
+ style: {
843
+ display: 'flex',
844
+ gap: 6,
845
+ padding: '2px 0 12px',
846
+ flexWrap: 'wrap'
847
+ }
848
+ }, PILLS.map(([id, label]) => {
849
+ const active = filter === id;
850
+ const tone = id === 'all' ? null : id;
851
+ return /*#__PURE__*/React.createElement("button", {
852
+ key: id,
853
+ onClick: () => setF(id),
854
+ style: {
855
+ display: 'inline-flex',
856
+ alignItems: 'center',
857
+ gap: 6,
858
+ padding: '4px 10px',
859
+ borderRadius: 999,
860
+ border: '1px solid ' + (active ? 'var(--brand-blue-500)' : 'var(--line)'),
861
+ background: active ? 'var(--brand-blue-500)' : tone ? `var(--status-${tone}-bg)` : 'var(--bg-elev)',
862
+ color: active ? '#fff' : tone ? `var(--status-${tone})` : 'var(--fg2)',
863
+ fontFamily: 'var(--font-body)',
864
+ fontSize: 12,
865
+ fontWeight: 600,
866
+ cursor: 'pointer',
867
+ transition: 'background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast)'
868
+ }
869
+ }, label, /*#__PURE__*/React.createElement("span", {
870
+ style: {
871
+ fontFamily: 'var(--font-mono)',
872
+ fontSize: 11,
873
+ opacity: 0.9
874
+ }
875
+ }, counts[id]));
876
+ })), /*#__PURE__*/React.createElement("div", {
877
+ style: {
878
+ marginLeft: -20,
879
+ marginRight: -20
880
+ }
881
+ }, visible.length === 0 ? /*#__PURE__*/React.createElement("div", {
882
+ style: {
883
+ padding: '30px 20px',
884
+ textAlign: 'center',
885
+ color: 'var(--fg3)',
886
+ fontFamily: 'var(--font-mono)',
887
+ fontSize: 12
888
+ }
889
+ }, "No ", filter === 'all' ? '' : filter + ' ', "tests in this run.") : visible.map(t => /*#__PURE__*/React.createElement(TestRow, {
890
+ key: t.id,
891
+ test: {
892
+ ns: '',
893
+ name: t.name,
894
+ status: t.status,
895
+ duration: t.dur,
896
+ last: t.lastRun,
897
+ retries: t.retries,
898
+ richId: t.id
899
+ },
900
+ onOpen: () => onOpen({
901
+ ns: '',
902
+ name: t.name,
903
+ status: t.status,
904
+ duration: t.dur,
905
+ retries: t.retries,
906
+ richId: t.id
907
+ })
908
+ }))), total > PAGE_SIZE && /*#__PURE__*/React.createElement("div", {
909
+ style: {
910
+ display: 'flex',
911
+ alignItems: 'center',
912
+ justifyContent: 'space-between',
913
+ padding: '12px 0 0',
914
+ marginTop: 8,
915
+ borderTop: '1px solid var(--line)',
916
+ flexWrap: 'wrap',
917
+ gap: 8
918
+ }
919
+ }, /*#__PURE__*/React.createElement("div", {
920
+ style: {
921
+ fontFamily: 'var(--font-mono)',
922
+ fontSize: 12,
923
+ color: 'var(--fg3)'
924
+ }
925
+ }, "Showing ", /*#__PURE__*/React.createElement("b", {
926
+ style: {
927
+ color: 'var(--fg1)'
928
+ }
929
+ }, start + 1, "\u2013", end), " of ", /*#__PURE__*/React.createElement("b", {
930
+ style: {
931
+ color: 'var(--fg1)'
932
+ }
933
+ }, total), filter !== 'all' ? ' ' + filter : '', filter !== 'all' ? /*#__PURE__*/React.createElement("span", null, " \xB7 ") : null, filter !== 'all' ? /*#__PURE__*/React.createElement("span", null, tests.length, " total") : null), /*#__PURE__*/React.createElement("div", {
934
+ style: {
935
+ display: 'flex',
936
+ gap: 4,
937
+ alignItems: 'center'
938
+ }
939
+ }, /*#__PURE__*/React.createElement("button", {
940
+ className: "btn btn-ghost",
941
+ style: {
942
+ height: 28,
943
+ fontSize: 12
944
+ },
945
+ disabled: safePage === 0,
946
+ onClick: () => setPage(p => Math.max(0, p - 1))
947
+ }, "\u2190 Prev"), /*#__PURE__*/React.createElement("span", {
948
+ style: {
949
+ fontFamily: 'var(--font-mono)',
950
+ fontSize: 11,
951
+ color: 'var(--fg3)',
952
+ padding: '0 6px'
953
+ }
954
+ }, "page ", safePage + 1, " / ", pages), /*#__PURE__*/React.createElement("button", {
955
+ className: "btn btn-ghost",
956
+ style: {
957
+ height: 28,
958
+ fontSize: 12
959
+ },
960
+ disabled: safePage >= pages - 1,
961
+ onClick: () => setPage(p => Math.min(pages - 1, p + 1))
962
+ }, "Next \u2192"), /*#__PURE__*/React.createElement("button", {
963
+ className: "btn btn-secondary",
964
+ style: {
965
+ height: 28,
966
+ fontSize: 12,
967
+ marginLeft: 8
968
+ },
969
+ onClick: () => window.__navTo?.('suites')
970
+ }, "View all in Suites \u2192"))));
971
+ }
972
+ function Overview({
973
+ onOpen
974
+ }) {
975
+ const RUN = window.RUN;
976
+ const SUITES = window.SUITES || [];
977
+ const ENV = window.ENV || [];
978
+ const allTests = Object.values(window.RICH_TESTS || {}).sort((a, b) => a.order - b.order);
979
+ const counts = {
980
+ passed: 0,
981
+ failed: 0,
982
+ broken: 0,
983
+ skipped: 0
984
+ };
985
+ for (const t of allTests) counts[t.status] = (counts[t.status] || 0) + 1;
986
+ const sevCounts = {
987
+ blocker: 0,
988
+ critical: 0,
989
+ normal: 0,
990
+ minor: 0,
991
+ trivial: 0
992
+ };
993
+ for (const t of allTests) {
994
+ const s = (t.severity || 'normal').toLowerCase();
995
+ if (sevCounts[s] != null) sevCounts[s]++;else sevCounts.normal++;
996
+ }
997
+ const sevTotal = Object.values(sevCounts).reduce((a, b) => a + b, 0);
998
+ const total = allTests.length || 1;
999
+ const passRate = Math.round(counts.passed / total * 100);
1000
+ const durSamples = allTests.filter(t => t.durMs > 0).map(t => t.durMs);
1001
+ const meanDur = durSamples.length ? Math.round(durSamples.reduce((a, b) => a + b, 0) / durSamples.length) : 0;
1002
+ const slowest = [...allTests].filter(t => t.durMs > 0).sort((a, b) => b.durMs - a.durMs)[0];
1003
+ const retriedCount = allTests.filter(t => (t.retries || 0) > 0).length;
1004
+ const flakyCount = allTests.filter(t => (t.retries || 0) > 0 || t.status === 'broken').length;
1005
+ const fmt = window._kenshoFmtDuration || (ms => ms + 'ms');
1006
+ const cards = {
1007
+ summary: {
1008
+ title: 'Run summary',
1009
+ meta: RUN.duration,
1010
+ span: 2,
1011
+ body: /*#__PURE__*/React.createElement("div", {
1012
+ style: {
1013
+ display: 'grid',
1014
+ gridTemplateColumns: 'minmax(280px, 360px) 1fr',
1015
+ gap: 28,
1016
+ alignItems: 'center'
1017
+ }
1018
+ }, /*#__PURE__*/React.createElement("div", {
1019
+ style: {
1020
+ display: 'flex',
1021
+ gap: 18,
1022
+ alignItems: 'center'
1023
+ }
1024
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1025
+ className: "statnum"
1026
+ }, allTests.length), /*#__PURE__*/React.createElement("div", {
1027
+ className: "statlbl"
1028
+ }, "test cases")), /*#__PURE__*/React.createElement("div", {
1029
+ style: {
1030
+ flex: 1
1031
+ }
1032
+ }, /*#__PURE__*/React.createElement(StatusDonut, {
1033
+ counts: counts
1034
+ }))), /*#__PURE__*/React.createElement("div", {
1035
+ style: {
1036
+ display: 'grid',
1037
+ gridTemplateColumns: 'repeat(3, 1fr)',
1038
+ gap: 0,
1039
+ border: '1px solid var(--line)',
1040
+ borderRadius: 10,
1041
+ overflow: 'hidden',
1042
+ background: 'var(--bg-elev)'
1043
+ }
1044
+ }, /*#__PURE__*/React.createElement(SummaryKpi, {
1045
+ label: "Pass rate",
1046
+ value: `${passRate}%`,
1047
+ accent: passRate >= 95 ? 'var(--status-passed)' : passRate >= 80 ? 'var(--status-broken)' : 'var(--status-failed)'
1048
+ }), /*#__PURE__*/React.createElement(SummaryKpi, {
1049
+ label: "Mean duration",
1050
+ value: meanDur ? fmt(meanDur) : '—',
1051
+ accent: "var(--brand-blue-500)",
1052
+ border: "left"
1053
+ }), /*#__PURE__*/React.createElement(SummaryKpi, {
1054
+ label: "Slowest",
1055
+ value: slowest ? slowest.dur : '—',
1056
+ hint: slowest ? slowest.name : '',
1057
+ accent: "var(--status-broken)",
1058
+ border: "left",
1059
+ onClick: slowest ? () => window.__openTest?.(slowest.id) : null
1060
+ }), /*#__PURE__*/React.createElement(SummaryKpi, {
1061
+ label: "Failures",
1062
+ value: counts.failed,
1063
+ accent: "var(--status-failed)",
1064
+ border: "top",
1065
+ onClick: counts.failed > 0 ? () => window.__navTo?.('categories') : null
1066
+ }), /*#__PURE__*/React.createElement(SummaryKpi, {
1067
+ label: "Skipped",
1068
+ value: counts.skipped,
1069
+ accent: "var(--status-skipped-fg)",
1070
+ border: "top-left"
1071
+ }), /*#__PURE__*/React.createElement(SummaryKpi, {
1072
+ label: "Flaky",
1073
+ value: flakyCount,
1074
+ hint: flakyCount > 0 ? `${retriedCount} retried · ${counts.broken} broken` : 'clean run',
1075
+ accent: flakyCount > 0 ? 'var(--status-broken)' : 'var(--status-passed)',
1076
+ border: "top-left",
1077
+ onClick: flakyCount > 0 ? () => window.__navTo?.('flaky') : null
1078
+ })))
1079
+ },
1080
+ trend: {
1081
+ title: 'Trend',
1082
+ meta: (window.TREND_RUNS?.length || 0) + ' run' + (window.TREND_RUNS?.length === 1 ? '' : 's') + ' · stacked',
1083
+ span: 1,
1084
+ body: window.TREND_RUNS?.length ? /*#__PURE__*/React.createElement(TrendChartV2, {
1085
+ runs: window.TREND_RUNS
1086
+ }) : /*#__PURE__*/React.createElement("div", {
1087
+ style: {
1088
+ padding: 30,
1089
+ color: 'var(--fg3)',
1090
+ textAlign: 'center',
1091
+ fontFamily: 'var(--font-mono)',
1092
+ fontSize: 12
1093
+ }
1094
+ }, "Run history will populate after the next generate.")
1095
+ },
1096
+ severity: {
1097
+ title: 'Severity distribution',
1098
+ meta: `${sevTotal} test${sevTotal === 1 ? '' : 's'}`,
1099
+ span: 1,
1100
+ body: /*#__PURE__*/React.createElement(SeverityDistribution, {
1101
+ tests: allTests
1102
+ })
1103
+ },
1104
+ slowest: {
1105
+ title: 'Slowest tests',
1106
+ meta: 'top 6 by duration',
1107
+ span: 1,
1108
+ action: /*#__PURE__*/React.createElement("a", {
1109
+ className: "btn btn-ghost",
1110
+ style: {
1111
+ height: 24,
1112
+ padding: '0 8px',
1113
+ fontSize: 12,
1114
+ cursor: 'pointer'
1115
+ },
1116
+ onClick: e => {
1117
+ e.stopPropagation();
1118
+ window.__navTo?.('timeline');
1119
+ }
1120
+ }, "See timeline \u2192"),
1121
+ body: /*#__PURE__*/React.createElement(SlowestTestsList, {
1122
+ tests: allTests,
1123
+ limit: 6,
1124
+ onOpen: onOpen
1125
+ })
1126
+ },
1127
+ suites: {
1128
+ title: `Suites · ${SUITES.length} items total`,
1129
+ span: 1,
1130
+ action: /*#__PURE__*/React.createElement("a", {
1131
+ className: "btn btn-ghost",
1132
+ style: {
1133
+ height: 24,
1134
+ padding: '0 8px',
1135
+ fontSize: 12,
1136
+ cursor: 'pointer'
1137
+ },
1138
+ onClick: e => {
1139
+ e.stopPropagation();
1140
+ window.__navTo?.('suites');
1141
+ }
1142
+ }, "Show all \u2192"),
1143
+ body: /*#__PURE__*/React.createElement(React.Fragment, null, SUITES.slice(0, 8).map(s => /*#__PURE__*/React.createElement(SuiteBar, _extends({
1144
+ key: s.name
1145
+ }, s))))
1146
+ },
1147
+ environment: {
1148
+ title: 'Environment',
1149
+ meta: ENV.length ? ENV.length + ' vars' : 'no vars',
1150
+ span: 1,
1151
+ body: ENV.length ? /*#__PURE__*/React.createElement(EnvTable, {
1152
+ env: ENV
1153
+ }) : /*#__PURE__*/React.createElement(EnvEmptyState, null)
1154
+ },
1155
+ tests: {
1156
+ title: `Tests · ${allTests.length} items total`,
1157
+ span: 2,
1158
+ action: /*#__PURE__*/React.createElement("a", {
1159
+ className: "btn btn-ghost",
1160
+ style: {
1161
+ height: 24,
1162
+ padding: '0 8px',
1163
+ fontSize: 12,
1164
+ cursor: 'pointer'
1165
+ },
1166
+ onClick: e => {
1167
+ e.stopPropagation();
1168
+ window.__navTo?.('suites');
1169
+ }
1170
+ }, "View all in Suites \u2192"),
1171
+ body: /*#__PURE__*/React.createElement(TestsCard, {
1172
+ tests: allTests,
1173
+ onOpen: onOpen
1174
+ })
1175
+ }
1176
+ };
1177
+ const DEFAULT_ORDER = ['summary', 'trend', 'severity', 'slowest', 'suites', 'environment', 'tests'];
1178
+ const [order, setOrder] = useStateA(() => {
1179
+ try {
1180
+ const stored = JSON.parse(localStorage.getItem('kensho.overview.order') || 'null');
1181
+ if (!Array.isArray(stored)) return DEFAULT_ORDER;
1182
+ const valid = stored.filter(id => DEFAULT_ORDER.includes(id));
1183
+ const missing = DEFAULT_ORDER.filter(id => !valid.includes(id));
1184
+ return [...valid, ...missing];
1185
+ } catch {
1186
+ return DEFAULT_ORDER;
1187
+ }
1188
+ });
1189
+ const [dragId, setDragId] = useStateA(null);
1190
+ const [overId, setOverId] = useStateA(null);
1191
+ React.useEffect(() => {
1192
+ localStorage.setItem('kensho.overview.order', JSON.stringify(order));
1193
+ }, [order]);
1194
+ const onDragStart = id => e => {
1195
+ setDragId(id);
1196
+ e.dataTransfer.effectAllowed = 'move';
1197
+ e.dataTransfer.setData('text/plain', id);
1198
+ };
1199
+ const onDragOver = id => e => {
1200
+ e.preventDefault();
1201
+ if (id !== dragId) setOverId(id);
1202
+ };
1203
+ const onDragEnd = () => {
1204
+ setDragId(null);
1205
+ setOverId(null);
1206
+ };
1207
+ const onDrop = id => e => {
1208
+ e.preventDefault();
1209
+ if (!dragId || dragId === id) {
1210
+ onDragEnd();
1211
+ return;
1212
+ }
1213
+ const next = [...order];
1214
+ const fromIdx = next.indexOf(dragId);
1215
+ const toIdx = next.indexOf(id);
1216
+ next.splice(fromIdx, 1);
1217
+ next.splice(toIdx, 0, dragId);
1218
+ setOrder(next);
1219
+ onDragEnd();
1220
+ };
1221
+ const project = window.KENSHO_INDEX?.project || {};
1222
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1223
+ style: {
1224
+ display: 'flex',
1225
+ alignItems: 'baseline',
1226
+ justifyContent: 'space-between',
1227
+ marginBottom: 18
1228
+ }
1229
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1230
+ className: "k-overline"
1231
+ }, project.name || 'Kensho', " \xB7 ", RUN.startedAt), /*#__PURE__*/React.createElement("h1", {
1232
+ className: "k-h1",
1233
+ style: {
1234
+ marginTop: 4
1235
+ }
1236
+ }, "Run ", RUN.id, " ", /*#__PURE__*/React.createElement("span", {
1237
+ style: {
1238
+ color: 'var(--fg3)',
1239
+ fontWeight: 500
1240
+ }
1241
+ }, "\xB7 ", RUN.branch, RUN.commit ? ' · ' + RUN.commit : ''))), /*#__PURE__*/React.createElement("div", {
1242
+ style: {
1243
+ display: 'flex',
1244
+ gap: 8
1245
+ }
1246
+ }, (() => {
1247
+ const repo = (RUN.repoUrl || '').replace(/\/$/, '');
1248
+ if (!repo) return null;
1249
+ const branchUrl = `${repo}/tree/${encodeURIComponent(RUN.branch)}`;
1250
+ const commitUrl = RUN.commitFull ? `${repo}/commit/${RUN.commitFull}` : '';
1251
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("a", {
1252
+ className: "btn btn-secondary",
1253
+ href: branchUrl,
1254
+ target: "_blank",
1255
+ rel: "noopener noreferrer",
1256
+ title: `Open ${RUN.branch} on the repo`
1257
+ }, /*#__PURE__*/React.createElement(Icon, {
1258
+ name: "git-branch"
1259
+ }), RUN.branch), commitUrl && /*#__PURE__*/React.createElement("a", {
1260
+ className: "btn btn-secondary",
1261
+ href: commitUrl,
1262
+ target: "_blank",
1263
+ rel: "noopener noreferrer",
1264
+ title: `Open commit ${RUN.commit}`
1265
+ }, /*#__PURE__*/React.createElement(Icon, {
1266
+ name: "git-commit"
1267
+ }), RUN.commit));
1268
+ })(), /*#__PURE__*/React.createElement("button", {
1269
+ className: "btn btn-ghost",
1270
+ title: "Reset card order",
1271
+ onClick: () => setOrder(DEFAULT_ORDER)
1272
+ }, /*#__PURE__*/React.createElement(Icon, {
1273
+ name: "rotate-ccw",
1274
+ size: 14
1275
+ })))), /*#__PURE__*/React.createElement("div", {
1276
+ style: {
1277
+ display: 'grid',
1278
+ gridTemplateColumns: '1.1fr 1.6fr',
1279
+ gap: 16,
1280
+ alignItems: 'start'
1281
+ }
1282
+ }, order.map(id => {
1283
+ const c = cards[id];
1284
+ if (!c) return null;
1285
+ const isDrag = dragId === id;
1286
+ const isOver = overId === id;
1287
+ return /*#__PURE__*/React.createElement("div", {
1288
+ key: id,
1289
+ draggable: true,
1290
+ onDragStart: onDragStart(id),
1291
+ onDragOver: onDragOver(id),
1292
+ onDrop: onDrop(id),
1293
+ onDragEnd: onDragEnd,
1294
+ className: "card",
1295
+ style: {
1296
+ gridColumn: c.span === 2 ? 'span 2' : 'auto',
1297
+ opacity: isDrag ? 0.4 : 1,
1298
+ outline: isOver ? '2px solid var(--accent)' : 'none',
1299
+ outlineOffset: isOver ? -2 : 0,
1300
+ transition: 'outline var(--dur-fast), opacity var(--dur-fast)',
1301
+ cursor: 'default'
1302
+ }
1303
+ }, /*#__PURE__*/React.createElement("div", {
1304
+ className: "hd",
1305
+ style: {
1306
+ alignItems: 'center'
1307
+ }
1308
+ }, /*#__PURE__*/React.createElement("span", {
1309
+ style: {
1310
+ display: 'inline-flex',
1311
+ alignItems: 'center',
1312
+ gap: 6,
1313
+ cursor: 'grab',
1314
+ color: 'var(--fg3)',
1315
+ userSelect: 'none'
1316
+ },
1317
+ title: "Drag to reorder"
1318
+ }, /*#__PURE__*/React.createElement(Icon, {
1319
+ name: "grip-vertical",
1320
+ size: 14
1321
+ })), /*#__PURE__*/React.createElement("h3", {
1322
+ style: {
1323
+ margin: 0,
1324
+ flex: 1
1325
+ }
1326
+ }, c.title), c.meta && /*#__PURE__*/React.createElement("div", {
1327
+ className: "meta"
1328
+ }, c.meta), c.action), c.body);
1329
+ })));
1330
+ }
1331
+ function Detail({
1332
+ test,
1333
+ onBack
1334
+ }) {
1335
+ const richId = test.richId || test.id;
1336
+ const richTest = window.RICH_TESTS?.[richId];
1337
+ if (!richTest) return /*#__PURE__*/React.createElement("div", {
1338
+ className: "card",
1339
+ style: {
1340
+ padding: 30,
1341
+ textAlign: 'center',
1342
+ color: 'var(--fg3)',
1343
+ fontFamily: 'var(--font-mono)',
1344
+ fontSize: 13
1345
+ }
1346
+ }, "Test not found in this run.", /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
1347
+ className: "btn btn-ghost",
1348
+ onClick: onBack,
1349
+ style: {
1350
+ marginTop: 14
1351
+ }
1352
+ }, /*#__PURE__*/React.createElement(Icon, {
1353
+ name: "arrow-left"
1354
+ }), "Back")));
1355
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
1356
+ className: "btn btn-ghost",
1357
+ style: {
1358
+ marginBottom: 12
1359
+ },
1360
+ onClick: onBack
1361
+ }, /*#__PURE__*/React.createElement(Icon, {
1362
+ name: "arrow-left"
1363
+ }), "Back to overview"), /*#__PURE__*/React.createElement("div", {
1364
+ style: {
1365
+ background: 'var(--bg-elev)',
1366
+ border: '1px solid var(--line)',
1367
+ borderRadius: 'var(--r-lg)',
1368
+ display: 'flex',
1369
+ flexDirection: 'column',
1370
+ minHeight: 'calc(100vh - 220px)',
1371
+ overflow: 'hidden'
1372
+ }
1373
+ }, /*#__PURE__*/React.createElement(DetailPane, {
1374
+ test: richTest,
1375
+ defaultTab: "steps"
1376
+ })));
1377
+ }
1378
+
1379
+ // Boot: wait for data-bridge to populate window globals, THEN mount React.
1380
+ // Mount target is configurable so embedders can host the viewer without
1381
+ // renaming their own #app root. The static report ships index.html with
1382
+ // `<div id="app">` and no override, so this still defaults correctly.
1383
+ window.__KENSHO_BOOT.then(() => {
1384
+ const target = window.__KENSHO_MOUNT || document.querySelector('[data-kensho-viewer-mount]') || document.getElementById('app');
1385
+ if (!target) {
1386
+ console.error('[kensho] no mount target found');
1387
+ return;
1388
+ }
1389
+ ReactDOM.createRoot(target).render(/*#__PURE__*/React.createElement(App, null));
1390
+ });
1391
+
1392
+ // Tiny CSS for the toast animation — injected at boot so we don't need a
1393
+ // separate stylesheet just for one keyframe.
1394
+ const _styleEl = document.createElement('style');
1395
+ _styleEl.textContent = `@keyframes kvToastIn { from { opacity:0; transform: translateY(-6px); } to { opacity:1; transform: translateY(0); } }`;
1396
+ document.head.appendChild(_styleEl);