executable-stories-react 0.7.0 → 0.9.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.
@@ -47,7 +47,7 @@ __export(interactive_exports, {
47
47
  module.exports = __toCommonJS(interactive_exports);
48
48
 
49
49
  // src/interactive/ReportInteractive.tsx
50
- var import_react18 = require("react");
50
+ var import_react19 = require("react");
51
51
 
52
52
  // src/result.ts
53
53
  var ok = (data) => ({ ok: true, data });
@@ -422,17 +422,29 @@ function DocHtml({ entry }) {
422
422
  ] });
423
423
  }
424
424
 
425
- // src/components/doc/DocCustom.tsx
425
+ // src/components/doc/DocState.tsx
426
426
  var import_jsx_runtime13 = require("react/jsx-runtime");
427
+ function DocState({ entry }) {
428
+ let json;
429
+ try {
430
+ json = JSON.stringify(entry.value, null, 2) ?? String(entry.value);
431
+ } catch {
432
+ json = String(entry.value);
433
+ }
434
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("code", { className: "font-mono text-xs language-json", children: json }) });
435
+ }
436
+
437
+ // src/components/doc/DocCustom.tsx
438
+ var import_jsx_runtime14 = require("react/jsx-runtime");
427
439
  function DocCustom({ entry }) {
428
440
  const renderers = useCustomRenderers();
429
441
  const renderer = renderers[entry.type];
430
442
  if (renderer) {
431
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children: renderer(entry) });
443
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_jsx_runtime14.Fragment, { children: renderer(entry) });
432
444
  }
433
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "my-2", "data-type": entry.type, children: [
434
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
435
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
445
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "my-2", "data-type": entry.type, children: [
446
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
447
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
436
448
  ] });
437
449
  }
438
450
  function safeStringify(value) {
@@ -444,45 +456,47 @@ function safeStringify(value) {
444
456
  }
445
457
 
446
458
  // src/components/doc/DocEntry.tsx
447
- var import_jsx_runtime14 = require("react/jsx-runtime");
459
+ var import_jsx_runtime15 = require("react/jsx-runtime");
448
460
  function DocEntry({ entry }) {
449
461
  switch (entry.kind) {
450
462
  case "note":
451
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocNote, { entry });
463
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocNote, { entry });
452
464
  case "tag":
453
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocTag, { entry });
465
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocTag, { entry });
454
466
  case "kv":
455
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocKv, { entry });
467
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocKv, { entry });
456
468
  case "code":
457
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocCode, { entry });
469
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocCode, { entry });
458
470
  case "table":
459
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocTable, { entry });
471
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocTable, { entry });
460
472
  case "link":
461
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocLink, { entry });
473
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocLink, { entry });
462
474
  case "section":
463
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocSection, { entry });
475
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocSection, { entry });
464
476
  case "mermaid":
465
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocMermaid, { entry });
477
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocMermaid, { entry });
466
478
  case "screenshot":
467
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocScreenshot, { entry });
479
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocScreenshot, { entry });
468
480
  case "video":
469
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocVideo, { entry });
481
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocVideo, { entry });
470
482
  case "html":
471
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocHtml, { entry });
483
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocHtml, { entry });
484
+ case "state":
485
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocState, { entry });
472
486
  case "custom":
473
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(DocCustom, { entry });
487
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocCustom, { entry });
474
488
  }
475
489
  }
476
490
 
477
491
  // src/components/ReportDocEntries.tsx
478
- var import_jsx_runtime15 = require("react/jsx-runtime");
492
+ var import_jsx_runtime16 = require("react/jsx-runtime");
479
493
  function ReportDocEntries({ entries }) {
480
494
  if (entries.length === 0) return null;
481
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, { children: entries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(DocEntry, { entry }, i)) });
495
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_jsx_runtime16.Fragment, { children: entries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocEntry, { entry }, i)) });
482
496
  }
483
497
 
484
498
  // src/lib/step-params.tsx
485
- var import_jsx_runtime16 = require("react/jsx-runtime");
499
+ var import_jsx_runtime17 = require("react/jsx-runtime");
486
500
  var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
487
501
  function highlightStepParams(text) {
488
502
  const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
@@ -494,7 +508,7 @@ function highlightStepParams(text) {
494
508
  const end = start + match[0].length;
495
509
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
496
510
  nodes.push(
497
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
511
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
498
512
  );
499
513
  lastIndex = end;
500
514
  });
@@ -503,7 +517,7 @@ function highlightStepParams(text) {
503
517
  }
504
518
 
505
519
  // src/components/ReportSteps.tsx
506
- var import_jsx_runtime17 = require("react/jsx-runtime");
520
+ var import_jsx_runtime18 = require("react/jsx-runtime");
507
521
  var STEP_GLYPH = {
508
522
  passed: "\u2713",
509
523
  failed: "\u2717",
@@ -518,21 +532,21 @@ var STEP_GLYPH_COLOR = {
518
532
  };
519
533
  function ReportSteps({ scenario }) {
520
534
  if (scenario.steps.length === 0) return null;
521
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ReportStepItem, { step }, step.id)) });
535
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ReportStepItem, { step }, step.id)) });
522
536
  }
523
537
  function ReportStepItem({ step }) {
524
538
  const kw = step.keyword.toLowerCase();
525
539
  const continuation = kw === "and" || kw === "but";
526
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
540
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
527
541
  "li",
528
542
  {
529
543
  id: step.id,
530
544
  "data-status": step.status,
531
545
  className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
532
546
  children: [
533
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-baseline gap-2", children: [
534
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { "aria-hidden": true, className: cn("w-4 shrink-0 text-center text-xs", STEP_GLYPH_COLOR[step.status]), children: STEP_GLYPH[step.status] ?? "\u25CB" }),
535
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
547
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-baseline gap-2", children: [
548
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { "aria-hidden": true, className: cn("w-4 shrink-0 text-center text-xs", STEP_GLYPH_COLOR[step.status]), children: STEP_GLYPH[step.status] ?? "\u25CB" }),
549
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
536
550
  "span",
537
551
  {
538
552
  className: cn(
@@ -542,10 +556,10 @@ function ReportStepItem({ step }) {
542
556
  children: step.keyword
543
557
  }
544
558
  ),
545
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
546
- step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
559
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
560
+ step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
547
561
  ] }),
548
- step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
562
+ step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
549
563
  "pre",
550
564
  {
551
565
  role: "alert",
@@ -553,12 +567,72 @@ function ReportStepItem({ step }) {
553
567
  children: step.errorMessage
554
568
  }
555
569
  ) : null,
556
- step.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { "data-es-docs": true, className: "mt-2 ml-6", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ReportDocEntries, { entries: step.docEntries }) }) : null
570
+ step.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-es-docs": true, className: "mt-2 ml-6", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ReportDocEntries, { entries: step.docEntries }) }) : null
557
571
  ]
558
572
  }
559
573
  );
560
574
  }
561
575
 
576
+ // ../executable-stories-core/dist/state-diff.js
577
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
578
+ function deepEqual(a, b) {
579
+ if (Object.is(a, b)) return true;
580
+ if (Array.isArray(a) && Array.isArray(b)) {
581
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
582
+ }
583
+ if (isPlainObject(a) && isPlainObject(b)) {
584
+ const ak = Object.keys(a);
585
+ const bk = Object.keys(b);
586
+ return ak.length === bk.length && ak.every((k) => k in b && deepEqual(a[k], b[k]));
587
+ }
588
+ return false;
589
+ }
590
+ var joinKey = (path, key) => path === "" ? key : `${path}.${key}`;
591
+ function walk(path, before, after, out) {
592
+ if (Object.is(before, after)) return;
593
+ if (Array.isArray(before) && Array.isArray(after)) {
594
+ const shared = Math.min(before.length, after.length);
595
+ for (let i = 0; i < shared; i++) walk(`${path}[${i}]`, before[i], after[i], out);
596
+ for (let i = shared; i < after.length; i++) out.push({ path: `${path}[${i}]`, kind: "added", after: after[i] });
597
+ for (let i = shared; i < before.length; i++) out.push({ path: `${path}[${i}]`, kind: "removed", before: before[i] });
598
+ return;
599
+ }
600
+ if (isPlainObject(before) && isPlainObject(after)) {
601
+ for (const key of Object.keys(after)) {
602
+ const p = joinKey(path, key);
603
+ if (key in before) walk(p, before[key], after[key], out);
604
+ else out.push({ path: p, kind: "added", after: after[key] });
605
+ }
606
+ for (const key of Object.keys(before)) {
607
+ if (!(key in after)) out.push({ path: joinKey(path, key), kind: "removed", before: before[key] });
608
+ }
609
+ return;
610
+ }
611
+ if (!deepEqual(before, after)) out.push({ path, kind: "changed", before, after });
612
+ }
613
+ function diffStateValues(before, after) {
614
+ const out = [];
615
+ walk("", before, after, out);
616
+ return out;
617
+ }
618
+ function formatStateValue(value, maxLen = 60) {
619
+ const s = value === void 0 ? "undefined" : JSON.stringify(value);
620
+ return s.length > maxLen ? `${s.slice(0, maxLen - 1)}\u2026` : s;
621
+ }
622
+ function summarizeStateChanges(changes) {
623
+ return changes.map((c) => {
624
+ const at = c.path === "" ? "(root)" : c.path;
625
+ switch (c.kind) {
626
+ case "added":
627
+ return `+ ${at}: ${formatStateValue(c.after)}`;
628
+ case "removed":
629
+ return `\u2212 ${at}`;
630
+ case "changed":
631
+ return `${at}: ${formatStateValue(c.before)} \u2192 ${formatStateValue(c.after)}`;
632
+ }
633
+ });
634
+ }
635
+
562
636
  // ../executable-stories-core/dist/storyboard.js
563
637
  function findScreenshot(entries) {
564
638
  for (const entry of entries) {
@@ -570,26 +644,55 @@ function findScreenshot(entries) {
570
644
  }
571
645
  return void 0;
572
646
  }
647
+ function findStates(entries, out = []) {
648
+ for (const entry of entries) {
649
+ if (entry.kind === "state") out.push(entry);
650
+ if (entry.children) findStates(entry.children, out);
651
+ }
652
+ return out;
653
+ }
654
+ var laneOf = (label) => label ?? "";
573
655
  function extractStoryboardFrames(scenario) {
574
656
  const frames = [];
657
+ const laneOrder = [];
658
+ const previous = /* @__PURE__ */ new Map();
575
659
  for (const step of scenario.steps) {
576
660
  const shot = findScreenshot(step.docEntries);
577
- if (!shot) continue;
661
+ const stateDocs = findStates(step.docEntries);
662
+ if (!shot && stateDocs.length === 0) continue;
663
+ const byLane = /* @__PURE__ */ new Map();
664
+ for (const doc of stateDocs) {
665
+ const lane = laneOf(doc.label);
666
+ if (!byLane.has(lane) && !laneOrder.includes(lane)) laneOrder.push(lane);
667
+ byLane.set(lane, doc);
668
+ }
669
+ const states = [];
670
+ for (const lane of laneOrder) {
671
+ const doc = byLane.get(lane);
672
+ if (!doc) continue;
673
+ const card = {
674
+ ...doc.label !== void 0 && { label: doc.label },
675
+ value: doc.value
676
+ };
677
+ if (previous.has(lane)) card.changes = diffStateValues(previous.get(lane), doc.value);
678
+ previous.set(lane, doc.value);
679
+ states.push(card);
680
+ }
578
681
  frames.push({
579
682
  stepId: step.id,
580
683
  stepIndex: step.index,
581
684
  keyword: step.keyword,
582
685
  text: step.text,
583
686
  status: step.status,
584
- path: shot.path,
585
- ...shot.alt !== void 0 && { alt: shot.alt }
687
+ ...shot && { screenshot: { path: shot.path, ...shot.alt !== void 0 && { alt: shot.alt } } },
688
+ states
586
689
  });
587
690
  }
588
691
  return frames;
589
692
  }
590
693
 
591
694
  // src/components/ReportStoryboard.tsx
592
- var import_jsx_runtime18 = require("react/jsx-runtime");
695
+ var import_jsx_runtime19 = require("react/jsx-runtime");
593
696
  var FRAME_GLYPH = {
594
697
  passed: "\u2713",
595
698
  failed: "\u2717",
@@ -605,55 +708,114 @@ var FRAME_GLYPH_COLOR = {
605
708
  function ReportStoryboard({ scenario }) {
606
709
  const frames = extractStoryboardFrames(scenario);
607
710
  if (frames.length < 2) return null;
608
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
609
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h4", { className: "mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Storyboard" }),
610
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ol", { className: "flex gap-3 overflow-x-auto pb-2", children: frames.map((frame) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(StoryboardFrameItem, { frame }, frame.stepId)) })
711
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
712
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h4", { className: "mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Storyboard" }),
713
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ol", { className: "flex items-start gap-3 overflow-x-auto pb-2", children: frames.map((frame) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(StoryboardFrameItem, { frame }, frame.stepId)) })
611
714
  ] });
612
715
  }
613
716
  function StoryboardFrameItem({ frame }) {
614
- const src = isLocalFsPath(frame.path) ? void 0 : safeImageUrl(frame.path);
615
717
  const label = `${frame.keyword} ${frame.text}`;
616
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("li", { className: "w-40 shrink-0", children: [
617
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
618
- "a",
718
+ const hasStates = frame.states.length > 0;
719
+ const caption = /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
720
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { "aria-hidden": true, className: cn("shrink-0", FRAME_GLYPH_COLOR[frame.status]), children: FRAME_GLYPH[frame.status] ?? "\u25CB" }),
721
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "min-w-0", children: [
722
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "font-mono font-semibold text-keyword", children: frame.keyword }),
723
+ " ",
724
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-muted-foreground", children: frame.screenshot?.alt ?? frame.text })
725
+ ] })
726
+ ] });
727
+ return (
728
+ // State cards need room for diff rows; screenshot-only frames keep the
729
+ // original narrow film-cell width (and DOM) unchanged.
730
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
731
+ frame.screenshot ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(FrameScreenshot, { frame, label }) : null,
732
+ hasStates ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: cn("flex flex-col gap-1.5", frame.screenshot && "mt-1.5"), children: frame.states.map((card, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(StateCardView, { card }, card.label ?? i)) }) : null,
733
+ frame.screenshot ? caption : (
734
+ // No screenshot means no image anchor, so the caption carries the
735
+ // jump-to-step link instead.
736
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("a", { href: `#${frame.stepId}`, "aria-label": `Jump to step: ${label}`, className: "block hover:underline", children: caption })
737
+ )
738
+ ] })
739
+ );
740
+ }
741
+ function FrameScreenshot({ frame, label }) {
742
+ const path = frame.screenshot?.path ?? "";
743
+ const src = isLocalFsPath(path) ? void 0 : safeImageUrl(path);
744
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
745
+ "a",
746
+ {
747
+ href: `#${frame.stepId}`,
748
+ "aria-label": `Jump to step: ${label}`,
749
+ className: "block overflow-hidden rounded-md border border-border transition-colors hover:border-ring",
750
+ children: src ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
751
+ "img",
752
+ {
753
+ src,
754
+ alt: frame.screenshot?.alt ?? label,
755
+ loading: "lazy",
756
+ className: "h-24 w-full bg-muted object-cover object-top"
757
+ }
758
+ ) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "flex h-24 w-full items-center justify-center border-b border-dashed border-border bg-muted px-2 text-center text-[0.6875rem] text-muted-foreground", children: "Screenshot unavailable" })
759
+ }
760
+ );
761
+ }
762
+ var CHANGE_TONE = {
763
+ added: "bg-pass-bg text-pass",
764
+ removed: "bg-fail-bg text-fail",
765
+ changed: "bg-skip-bg text-skip"
766
+ };
767
+ function prettyJson(value) {
768
+ try {
769
+ return JSON.stringify(value, null, 2) ?? String(value);
770
+ } catch {
771
+ return String(value);
772
+ }
773
+ }
774
+ function StateCardView({ card }) {
775
+ const json = prettyJson(card.value);
776
+ const firstCapture = card.changes === void 0;
777
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
778
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
779
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "inline-flex rounded-full border border-border px-1.5 font-mono text-[0.625rem] font-medium text-muted-foreground", children: card.label ?? "State" }),
780
+ firstCapture ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[0.625rem] text-muted-foreground italic", children: "first capture" }) : null
781
+ ] }),
782
+ !firstCapture && (card.changes && card.changes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ul", { className: "mt-1.5 flex flex-col gap-0.5", children: card.changes.map((change, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
783
+ "li",
619
784
  {
620
- href: `#${frame.stepId}`,
621
- "aria-label": `Jump to step: ${label}`,
622
- className: "block overflow-hidden rounded-md border border-border transition-colors hover:border-ring",
623
- children: src ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
624
- "img",
625
- {
626
- src,
627
- alt: frame.alt ?? label,
628
- loading: "lazy",
629
- className: "h-24 w-full bg-muted object-cover object-top"
630
- }
631
- ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "flex h-24 w-full items-center justify-center border-b border-dashed border-border bg-muted px-2 text-center text-[0.6875rem] text-muted-foreground", children: "Screenshot unavailable" })
785
+ className: cn(
786
+ "rounded-sm px-1.5 py-0.5 font-mono text-[0.625rem] leading-snug break-all",
787
+ CHANGE_TONE[change.kind]
788
+ ),
789
+ children: summarizeStateChanges([change])[0]
790
+ },
791
+ i
792
+ )) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "mt-1.5 text-[0.625rem] text-muted-foreground", children: "No changes" })),
793
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
794
+ "details",
795
+ {
796
+ open: firstCapture && json.length <= 240,
797
+ className: cn(firstCapture ? "mt-1.5" : "mt-1"),
798
+ children: [
799
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("summary", { className: "cursor-pointer text-[0.625rem] text-muted-foreground hover:text-foreground", children: "Snapshot" }),
800
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("pre", { tabIndex: 0, className: "mt-1 max-h-48 overflow-auto rounded-sm bg-muted p-1.5 font-mono text-[0.625rem] leading-snug", children: json })
801
+ ]
632
802
  }
633
- ),
634
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
635
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { "aria-hidden": true, className: cn("shrink-0", FRAME_GLYPH_COLOR[frame.status]), children: FRAME_GLYPH[frame.status] ?? "\u25CB" }),
636
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "min-w-0", children: [
637
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "font-mono font-semibold text-keyword", children: frame.keyword }),
638
- " ",
639
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-muted-foreground", children: frame.alt ?? frame.text })
640
- ] })
641
- ] })
803
+ )
642
804
  ] });
643
805
  }
644
806
 
645
807
  // src/components/ReportAttachments.tsx
646
- var import_jsx_runtime19 = require("react/jsx-runtime");
808
+ var import_jsx_runtime20 = require("react/jsx-runtime");
647
809
  function ReportAttachments({ attachments }) {
648
810
  if (attachments.length === 0) return null;
649
811
  return (
650
812
  // role="group" (not a labelled <section>) so multiple scenarios' attachment
651
813
  // blocks don't create duplicate "Attachments" region landmarks.
652
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
814
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
653
815
  const dataUri = att.contentEncoding === "BASE64" ? `data:${att.mediaType};base64,${att.body}` : `data:${att.mediaType},${encodeURIComponent(att.body)}`;
654
816
  const isImage = att.mediaType.startsWith("image/");
655
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
656
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
817
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
818
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
657
819
  "img",
658
820
  {
659
821
  src: dataUri,
@@ -662,8 +824,8 @@ function ReportAttachments({ attachments }) {
662
824
  className: "max-w-full rounded-md border border-border"
663
825
  }
664
826
  ),
665
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
666
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
827
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
828
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
667
829
  "a",
668
830
  {
669
831
  href: dataUri,
@@ -672,7 +834,7 @@ function ReportAttachments({ attachments }) {
672
834
  children: [
673
835
  att.name,
674
836
  " ",
675
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-muted-foreground", children: [
837
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "text-muted-foreground", children: [
676
838
  "(",
677
839
  att.mediaType,
678
840
  ")"
@@ -736,15 +898,15 @@ function toTraceRows(spans) {
736
898
  roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
737
899
  const rows = [];
738
900
  const seen = /* @__PURE__ */ new Set();
739
- const walk = (node, depth) => {
901
+ const walk2 = (node, depth) => {
740
902
  if (seen.has(node.span.spanId)) return;
741
903
  seen.add(node.span.spanId);
742
904
  rows.push({ ...node.span, depth });
743
- for (const child of node.children) walk(child, depth + 1);
905
+ for (const child of node.children) walk2(child, depth + 1);
744
906
  };
745
- for (const root of roots) walk(root, 0);
907
+ for (const root of roots) walk2(root, 0);
746
908
  for (const node of byId.values()) {
747
- if (!seen.has(node.span.spanId)) walk(node, 0);
909
+ if (!seen.has(node.span.spanId)) walk2(node, 0);
748
910
  }
749
911
  return rows;
750
912
  }
@@ -760,7 +922,7 @@ function traceWindow(rows) {
760
922
  }
761
923
 
762
924
  // src/components/ReportTrace.tsx
763
- var import_jsx_runtime20 = require("react/jsx-runtime");
925
+ var import_jsx_runtime21 = require("react/jsx-runtime");
764
926
  var BAR_COLOR = {
765
927
  ok: "bg-pass",
766
928
  error: "bg-fail",
@@ -771,8 +933,8 @@ function ReportTrace({ spans }) {
771
933
  const rows = toTraceRows(spans);
772
934
  if (rows.length === 0) return null;
773
935
  const { minStartMs, totalMs } = traceWindow(rows);
774
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
775
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
936
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
937
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
776
938
  "Trace (",
777
939
  rows.length,
778
940
  " span",
@@ -781,11 +943,11 @@ function ReportTrace({ spans }) {
781
943
  formatDuration(totalMs),
782
944
  ")"
783
945
  ] }),
784
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
946
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
785
947
  const leftPct = (row.startTimeMs - minStartMs) / totalMs * 100;
786
948
  const widthPct = Math.max(row.durationMs / totalMs * 100, 0.5);
787
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("li", { className: "flex items-center gap-2", children: [
788
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
949
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("li", { className: "flex items-center gap-2", children: [
950
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
789
951
  "span",
790
952
  {
791
953
  className: "min-w-0 flex-1 truncate text-foreground",
@@ -794,14 +956,14 @@ function ReportTrace({ spans }) {
794
956
  children: row.name
795
957
  }
796
958
  ),
797
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "relative h-3 w-1/2 shrink-0 overflow-hidden rounded-sm bg-border/40", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
959
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "relative h-3 w-1/2 shrink-0 overflow-hidden rounded-sm bg-border/40", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
798
960
  "span",
799
961
  {
800
962
  className: cn("absolute top-0 h-full rounded-sm", BAR_COLOR[row.status]),
801
963
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
802
964
  }
803
965
  ) }),
804
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
966
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
805
967
  ] }, `${row.spanId}-${i}`);
806
968
  }) })
807
969
  ] });
@@ -850,11 +1012,11 @@ function flakinessOf(entries) {
850
1012
  if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
851
1013
  return "stable";
852
1014
  }
853
- function diffSinceLastRun(history2, scenarios) {
1015
+ function diffSinceLastRun(history, scenarios) {
854
1016
  let hasPreviousRun = false;
855
1017
  const delta = { newlyFailing: [], newlyPassing: [], added: [] };
856
1018
  for (const scenario of scenarios) {
857
- const entries = history2[scenario.id];
1019
+ const entries = history[scenario.id];
858
1020
  if (!entries || entries.length === 0) continue;
859
1021
  const last = entries[entries.length - 1];
860
1022
  const prev = entries[entries.length - 2];
@@ -870,7 +1032,7 @@ function diffSinceLastRun(history2, scenarios) {
870
1032
  }
871
1033
 
872
1034
  // src/components/ScenarioRunHistory.tsx
873
- var import_jsx_runtime21 = require("react/jsx-runtime");
1035
+ var import_jsx_runtime22 = require("react/jsx-runtime");
874
1036
  var MAX_DOTS = 10;
875
1037
  var DOT_COLOR = {
876
1038
  passed: "bg-pass",
@@ -884,22 +1046,22 @@ function dotTitle(e) {
884
1046
  return parts.join(" \xB7 ");
885
1047
  }
886
1048
  function ScenarioRunHistory({ scenarioId }) {
887
- const history2 = useScenarioHistory();
888
- const entries = history2?.[scenarioId];
1049
+ const history = useScenarioHistory();
1050
+ const entries = history?.[scenarioId];
889
1051
  if (!entries || entries.length < 2) return null;
890
1052
  const recent = entries.slice(-MAX_DOTS);
891
1053
  const flaky = flakinessOf(recent) === "flaky";
892
1054
  const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
893
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
894
- flaky ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
895
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1055
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
1056
+ flaky ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
1057
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
896
1058
  "span",
897
1059
  {
898
1060
  role: "img",
899
1061
  "aria-label": `Run history: ${summary}`,
900
1062
  title: summary,
901
1063
  className: "flex shrink-0 items-center gap-[3px]",
902
- children: recent.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1064
+ children: recent.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
903
1065
  "span",
904
1066
  {
905
1067
  "aria-hidden": true,
@@ -990,7 +1152,7 @@ function stringify(value) {
990
1152
  return String(value);
991
1153
  }
992
1154
  }
993
- function docEntryToMarkdown(entry) {
1155
+ function docEntryToMarkdown(entry, stateLanes) {
994
1156
  const lines = [];
995
1157
  switch (entry.kind) {
996
1158
  case "note":
@@ -1042,13 +1204,24 @@ function docEntryToMarkdown(entry) {
1042
1204
  case "custom":
1043
1205
  lines.push(`**${entry.type}**`, "", "```json", stringify(entry.data), "```");
1044
1206
  break;
1207
+ case "state": {
1208
+ const lane = entry.label ?? "";
1209
+ lines.push(`**${entry.label ?? "State"}**`);
1210
+ if (stateLanes?.has(lane)) {
1211
+ const summary = summarizeStateChanges(diffStateValues(stateLanes.get(lane), entry.value));
1212
+ if (summary.length > 0) lines.push("", ...summary.map((l) => `- ${l}`));
1213
+ }
1214
+ stateLanes?.set(lane, entry.value);
1215
+ lines.push("", "<details>", "<summary>snapshot</summary>", "", "```json", stringify(entry.value ?? null), "```", "", "</details>");
1216
+ break;
1217
+ }
1045
1218
  }
1046
1219
  for (const child of entry.children ?? []) {
1047
- lines.push("", ...docEntryToMarkdown(child));
1220
+ lines.push("", ...docEntryToMarkdown(child, stateLanes));
1048
1221
  }
1049
1222
  return lines;
1050
1223
  }
1051
- function stepLines(step, ordinal, full) {
1224
+ function stepLines(step, ordinal, full, stateLanes) {
1052
1225
  const status = step.status === "passed" ? "" : ` \u2014 **${step.status}**`;
1053
1226
  const lines = [`${ordinal}. **${step.keyword}** ${step.text}${status}`];
1054
1227
  if (!full) return lines;
@@ -1056,7 +1229,7 @@ function stepLines(step, ordinal, full) {
1056
1229
  lines.push(...step.errorMessage.split("\n").map((l) => ` > ${l}`));
1057
1230
  }
1058
1231
  for (const entry of step.docEntries) {
1059
- lines.push("", ...docEntryToMarkdown(entry).map((l) => l === "" ? l : ` ${l}`));
1232
+ lines.push("", ...docEntryToMarkdown(entry, stateLanes).map((l) => l === "" ? l : ` ${l}`));
1060
1233
  }
1061
1234
  return lines;
1062
1235
  }
@@ -1065,9 +1238,10 @@ function scenarioToMarkdown(scenario, options = {}) {
1065
1238
  const full = variant === "full";
1066
1239
  const lines = full ? [`# ${scenario.title}`, ""] : [`## ${scenario.title} _(${scenario.status})_`, ""];
1067
1240
  if (full && meta.length > 0) lines.push(...meta);
1241
+ const stateLanes = /* @__PURE__ */ new Map();
1068
1242
  if (scenario.steps.length > 0) {
1069
1243
  if (full) lines.push("", "## Steps", "");
1070
- scenario.steps.forEach((step, i) => lines.push(...stepLines(step, i + 1, full)));
1244
+ scenario.steps.forEach((step, i) => lines.push(...stepLines(step, i + 1, full, stateLanes)));
1071
1245
  }
1072
1246
  if (scenario.errorMessage) {
1073
1247
  if (full) lines.push("", "## Failure", "");
@@ -1076,7 +1250,7 @@ function scenarioToMarkdown(scenario, options = {}) {
1076
1250
  }
1077
1251
  if (full && scenario.docEntries.length > 0) {
1078
1252
  lines.push("", "## Docs", "");
1079
- for (const entry of scenario.docEntries) lines.push(...docEntryToMarkdown(entry), "");
1253
+ for (const entry of scenario.docEntries) lines.push(...docEntryToMarkdown(entry, stateLanes), "");
1080
1254
  }
1081
1255
  return lines.join("\n").replaceAll(/\n{3,}/g, "\n\n").trimEnd() + (full ? "\n" : "");
1082
1256
  }
@@ -1116,9 +1290,9 @@ function scenarioPermalink(scenario) {
1116
1290
 
1117
1291
  // src/components/ui/card.tsx
1118
1292
  var React2 = require("react");
1119
- var import_jsx_runtime22 = require("react/jsx-runtime");
1293
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1120
1294
  function Card({ className, ...props }) {
1121
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1295
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1122
1296
  "div",
1123
1297
  {
1124
1298
  "data-slot": "card",
@@ -1131,7 +1305,7 @@ function Card({ className, ...props }) {
1131
1305
  );
1132
1306
  }
1133
1307
  function CardHeader({ className, ...props }) {
1134
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1308
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1135
1309
  "div",
1136
1310
  {
1137
1311
  "data-slot": "card-header",
@@ -1144,7 +1318,7 @@ function CardHeader({ className, ...props }) {
1144
1318
  );
1145
1319
  }
1146
1320
  function CardContent({ className, ...props }) {
1147
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1321
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1148
1322
  "div",
1149
1323
  {
1150
1324
  "data-slot": "card-content",
@@ -1158,13 +1332,13 @@ function CardContent({ className, ...props }) {
1158
1332
  var React3 = require("react");
1159
1333
  var import_menu = require("@base-ui/react/menu");
1160
1334
  var import_lucide_react = require("lucide-react");
1161
- var import_jsx_runtime23 = require("react/jsx-runtime");
1335
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1162
1336
  var itemClasses = "relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg:not([class*='text-'])]:text-muted-foreground";
1163
1337
  function DropdownMenu(props) {
1164
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
1338
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
1165
1339
  }
1166
1340
  function DropdownMenuTrigger(props) {
1167
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_menu.Menu.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
1341
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_menu.Menu.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
1168
1342
  }
1169
1343
  function DropdownMenuContent({
1170
1344
  className,
@@ -1172,14 +1346,14 @@ function DropdownMenuContent({
1172
1346
  sideOffset = 4,
1173
1347
  ...props
1174
1348
  }) {
1175
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_menu.Menu.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1349
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_menu.Menu.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1176
1350
  import_menu.Menu.Positioner,
1177
1351
  {
1178
1352
  "data-slot": "dropdown-menu-positioner",
1179
1353
  className: "z-50",
1180
1354
  align,
1181
1355
  sideOffset,
1182
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1356
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1183
1357
  import_menu.Menu.Popup,
1184
1358
  {
1185
1359
  "data-slot": "dropdown-menu-content",
@@ -1197,7 +1371,7 @@ function DropdownMenuItem({
1197
1371
  className,
1198
1372
  ...props
1199
1373
  }) {
1200
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1374
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1201
1375
  import_menu.Menu.Item,
1202
1376
  {
1203
1377
  "data-slot": "dropdown-menu-item",
@@ -1209,7 +1383,7 @@ function DropdownMenuItem({
1209
1383
 
1210
1384
  // src/components/ReportScenario.tsx
1211
1385
  var import_lucide_react2 = require("lucide-react");
1212
- var import_jsx_runtime24 = require("react/jsx-runtime");
1386
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1213
1387
  var STATUS_META = {
1214
1388
  passed: { label: "Passed", glyph: "\u2713", glyphColor: "text-pass", badgeVariant: "passed", showPill: false },
1215
1389
  failed: { label: "Failed", glyph: "\u2717", glyphColor: "text-fail", badgeVariant: "failed", showPill: true },
@@ -1225,7 +1399,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1225
1399
  const collapsible = collapse !== null && !hideTitle;
1226
1400
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
1227
1401
  const actions = useScenarioActions();
1228
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
1402
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
1229
1403
  Card,
1230
1404
  {
1231
1405
  id: scenario.id,
@@ -1233,10 +1407,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
1233
1407
  ...hideTitle ? { "aria-label": `${scenario.title}, ${label}` } : { "aria-labelledby": titleId },
1234
1408
  className: "gap-0 py-0",
1235
1409
  children: [
1236
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(CardHeader, { className: "px-4 py-3", children: [
1237
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1238
- hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1239
- collapsible ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1410
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(CardHeader, { className: "px-4 py-3", children: [
1411
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1412
+ hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1413
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1240
1414
  "button",
1241
1415
  {
1242
1416
  type: "button",
@@ -1245,49 +1419,49 @@ function ReportScenario({ scenario, hideTitle = false }) {
1245
1419
  "aria-controls": bodyId,
1246
1420
  "aria-label": `Toggle ${scenario.title}`,
1247
1421
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
1248
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1422
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1249
1423
  }
1250
1424
  ) : null,
1251
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1252
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { children: scenario.title })
1425
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1426
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { children: scenario.title })
1253
1427
  ] }),
1254
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
1255
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
1256
- scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
1257
- meta.showPill ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", children: `Status: ${label}` }),
1258
- actions ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(DropdownMenu, { children: [
1259
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1428
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
1429
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
1430
+ scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
1431
+ meta.showPill ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "sr-only", children: `Status: ${label}` }),
1432
+ actions ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(DropdownMenu, { children: [
1433
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1260
1434
  DropdownMenuTrigger,
1261
1435
  {
1262
1436
  "aria-label": `Actions for ${scenario.title}`,
1263
1437
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
1264
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react2.MoreHorizontalIcon, { "aria-hidden": true })
1438
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react2.MoreHorizontalIcon, { "aria-hidden": true })
1265
1439
  }
1266
1440
  ),
1267
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(DropdownMenuContent, { align: "end", children: [
1268
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1269
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react2.LinkIcon, {}),
1441
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(DropdownMenuContent, { align: "end", children: [
1442
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1443
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react2.LinkIcon, {}),
1270
1444
  " Copy link"
1271
1445
  ] }),
1272
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1273
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react2.FileTextIcon, {}),
1446
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1447
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react2.FileTextIcon, {}),
1274
1448
  " Copy as Markdown"
1275
1449
  ] }),
1276
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1277
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react2.SparklesIcon, {}),
1450
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1451
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react2.SparklesIcon, {}),
1278
1452
  " Explain with AI"
1279
1453
  ] })
1280
1454
  ] })
1281
1455
  ] }) : null
1282
1456
  ] })
1283
1457
  ] }),
1284
- scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1285
- scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1286
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1458
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1459
+ scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1460
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1287
1461
  ] }) : null
1288
1462
  ] }),
1289
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1290
- scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1463
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1464
+ scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1291
1465
  "pre",
1292
1466
  {
1293
1467
  role: "alert",
@@ -1295,11 +1469,11 @@ function ReportScenario({ scenario, hideTitle = false }) {
1295
1469
  children: scenario.errorMessage
1296
1470
  }
1297
1471
  ) : null,
1298
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportStoryboard, { scenario }),
1299
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportSteps, { scenario }),
1300
- scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1301
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportAttachments, { attachments: scenario.attachments }),
1302
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportTrace, { spans: scenario.otelSpans })
1472
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportStoryboard, { scenario }),
1473
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportSteps, { scenario }),
1474
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1475
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportAttachments, { attachments: scenario.attachments }),
1476
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportTrace, { spans: scenario.otelSpans })
1303
1477
  ] })
1304
1478
  ]
1305
1479
  }
@@ -1307,13 +1481,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
1307
1481
  }
1308
1482
 
1309
1483
  // src/components/ReportScenarioList.tsx
1310
- var import_jsx_runtime25 = require("react/jsx-runtime");
1484
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1311
1485
  function ReportScenarioList({ feature }) {
1312
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_jsx_runtime25.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportScenario, { scenario }, scenario.id)) });
1486
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_jsx_runtime26.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportScenario, { scenario }, scenario.id)) });
1313
1487
  }
1314
1488
 
1315
1489
  // src/components/ReportFeature.tsx
1316
- var import_jsx_runtime26 = require("react/jsx-runtime");
1490
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1317
1491
  function ReportFeature({ feature }) {
1318
1492
  const titleId = `${feature.id}-title`;
1319
1493
  const bodyId = `${feature.id}-body`;
@@ -1324,7 +1498,7 @@ function ReportFeature({ feature }) {
1324
1498
  return (
1325
1499
  // Suite = a section heading + a divider, NOT another bordered card. Drops a
1326
1500
  // layer of nesting so the scenario cards below read as the primary unit.
1327
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1501
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1328
1502
  "section",
1329
1503
  {
1330
1504
  id: feature.id,
@@ -1332,9 +1506,9 @@ function ReportFeature({ feature }) {
1332
1506
  "aria-labelledby": titleId,
1333
1507
  className: "flex flex-col gap-3",
1334
1508
  children: [
1335
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1336
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "min-w-0", children: [
1337
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1509
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1510
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "min-w-0", children: [
1511
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1338
1512
  "button",
1339
1513
  {
1340
1514
  type: "button",
@@ -1343,31 +1517,31 @@ function ReportFeature({ feature }) {
1343
1517
  "aria-controls": bodyId,
1344
1518
  className: "flex w-full cursor-pointer items-center gap-2 text-left hover:text-foreground/80",
1345
1519
  children: [
1346
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1347
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "truncate", children: feature.title })
1520
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1521
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "truncate", children: feature.title })
1348
1522
  ]
1349
1523
  }
1350
1524
  ) : feature.title }),
1351
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: cn("mt-0.5 truncate font-mono text-xs text-muted-foreground", collapse && "pl-5"), children: feature.sourceFile })
1525
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { className: cn("mt-0.5 truncate font-mono text-xs text-muted-foreground", collapse && "pl-5"), children: feature.sourceFile })
1352
1526
  ] }),
1353
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1527
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1354
1528
  "p",
1355
1529
  {
1356
1530
  className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground",
1357
1531
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
1358
1532
  children: [
1359
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { children: [
1533
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { children: [
1360
1534
  s.passed,
1361
1535
  " passed"
1362
1536
  ] }),
1363
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1364
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1537
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1538
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1365
1539
  s.failed,
1366
1540
  " failed"
1367
1541
  ] }),
1368
- skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
1369
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1370
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { children: [
1542
+ skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1543
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1544
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { children: [
1371
1545
  skipped,
1372
1546
  " skipped"
1373
1547
  ] })
@@ -1376,7 +1550,7 @@ function ReportFeature({ feature }) {
1376
1550
  }
1377
1551
  )
1378
1552
  ] }),
1379
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportScenarioList, { feature }) })
1553
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportScenarioList, { feature }) })
1380
1554
  ]
1381
1555
  }
1382
1556
  )
@@ -1384,17 +1558,17 @@ function ReportFeature({ feature }) {
1384
1558
  }
1385
1559
 
1386
1560
  // src/components/ReportFeatureList.tsx
1387
- var import_jsx_runtime27 = require("react/jsx-runtime");
1561
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1388
1562
  function ReportFeatureList() {
1389
1563
  const report = useReport();
1390
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportFeature, { feature }, feature.id)) });
1564
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ReportFeature, { feature }, feature.id)) });
1391
1565
  }
1392
1566
 
1393
1567
  // src/components/ui/empty.tsx
1394
1568
  var import_class_variance_authority2 = require("class-variance-authority");
1395
- var import_jsx_runtime28 = require("react/jsx-runtime");
1569
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1396
1570
  function Empty({ className, ...props }) {
1397
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1398
1572
  "div",
1399
1573
  {
1400
1574
  "data-slot": "empty",
@@ -1407,7 +1581,7 @@ function Empty({ className, ...props }) {
1407
1581
  );
1408
1582
  }
1409
1583
  function EmptyHeader({ className, ...props }) {
1410
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1584
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1411
1585
  "div",
1412
1586
  {
1413
1587
  "data-slot": "empty-header",
@@ -1434,7 +1608,7 @@ var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
1434
1608
  }
1435
1609
  );
1436
1610
  function EmptyTitle({ className, ...props }) {
1437
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1611
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1438
1612
  "div",
1439
1613
  {
1440
1614
  "data-slot": "empty-title",
@@ -1445,16 +1619,16 @@ function EmptyTitle({ className, ...props }) {
1445
1619
  }
1446
1620
 
1447
1621
  // src/components/ReportEmpty.tsx
1448
- var import_jsx_runtime29 = require("react/jsx-runtime");
1622
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1449
1623
  function ReportEmpty({ message }) {
1450
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1624
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1451
1625
  }
1452
1626
 
1453
1627
  // src/components/ReportSummary.tsx
1454
- var import_jsx_runtime30 = require("react/jsx-runtime");
1628
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1455
1629
  function ReportSummary({ className }) {
1456
1630
  const report = useReport();
1457
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1631
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1458
1632
  ReportSummaryView,
1459
1633
  {
1460
1634
  summary: report.summary,
@@ -1480,7 +1654,7 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1480
1654
  if (summary.pending > 0) {
1481
1655
  cards.push({ status: "pending", label: "Pending", value: summary.pending });
1482
1656
  }
1483
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
1657
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1484
1658
  "dl",
1485
1659
  {
1486
1660
  className: cn(
@@ -1493,17 +1667,17 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1493
1667
  "aria-label": ariaLabel,
1494
1668
  children: cards.map((c) => {
1495
1669
  const tone = TONE[c.status];
1496
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1670
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
1497
1671
  "div",
1498
1672
  {
1499
1673
  "data-status": c.status,
1500
1674
  className: "rounded-lg border bg-card px-4 py-3",
1501
1675
  children: [
1502
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1503
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1676
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1677
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1504
1678
  c.label
1505
1679
  ] }),
1506
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1680
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1507
1681
  ]
1508
1682
  },
1509
1683
  c.status
@@ -1563,7 +1737,7 @@ function prUrl(ci) {
1563
1737
  }
1564
1738
 
1565
1739
  // src/components/ReportMeta.tsx
1566
- var import_jsx_runtime31 = require("react/jsx-runtime");
1740
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1567
1741
  function ReportMeta() {
1568
1742
  const report = useReport();
1569
1743
  const items = [];
@@ -1586,13 +1760,13 @@ function ReportMeta() {
1586
1760
  const short = sha.slice(0, 8);
1587
1761
  items.push({
1588
1762
  label: "Commit",
1589
- value: url ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1763
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1590
1764
  });
1591
1765
  }
1592
1766
  if (report.ci) {
1593
1767
  items.push({
1594
1768
  label: "CI",
1595
- value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1769
+ value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1596
1770
  ciDisplayName(report.ci.name),
1597
1771
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1598
1772
  ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
@@ -1603,7 +1777,7 @@ function ReportMeta() {
1603
1777
  const label = `#${report.ci.prNumber}`;
1604
1778
  items.push({
1605
1779
  label: "PR",
1606
- value: url ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1780
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1607
1781
  });
1608
1782
  }
1609
1783
  if (items.length === 0) return null;
@@ -1611,50 +1785,50 @@ function ReportMeta() {
1611
1785
  // Run metadata is useful but secondary to "what failed" — tuck it into a
1612
1786
  // collapsed disclosure so it doesn't push the result down. Native <details>
1613
1787
  // so it works with no JS (the SSR/no-hydration report still toggles).
1614
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1615
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1616
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1788
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1790
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1617
1791
  "Run details"
1618
1792
  ] }),
1619
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex items-baseline gap-1.5", children: [
1620
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("dt", { className: "font-medium", children: item.label }),
1621
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1793
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex items-baseline gap-1.5", children: [
1794
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dt", { className: "font-medium", children: item.label }),
1795
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1622
1796
  ] }, item.label)) })
1623
1797
  ] })
1624
1798
  );
1625
1799
  }
1626
1800
 
1627
1801
  // src/components/ReportSchemaError.tsx
1628
- var import_jsx_runtime32 = require("react/jsx-runtime");
1802
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1629
1803
  function ReportSchemaError({ error }) {
1630
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1631
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("strong", { children: "Report could not be displayed." }) }),
1632
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { children: error.message }),
1633
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("p", { children: [
1804
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1805
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("strong", { children: "Report could not be displayed." }) }),
1806
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { children: error.message }),
1807
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("p", { children: [
1634
1808
  "The report bundle is newer than this version of ",
1635
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("code", { children: "executable-stories-react" }),
1809
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("code", { children: "executable-stories-react" }),
1636
1810
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1637
1811
  ] }) : null,
1638
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("details", { children: [
1639
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("summary", { children: [
1812
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("details", { children: [
1813
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("summary", { children: [
1640
1814
  error.issues.length,
1641
1815
  " validation issue",
1642
1816
  error.issues.length === 1 ? "" : "s"
1643
1817
  ] }),
1644
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1818
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1645
1819
  ] }) : null
1646
1820
  ] });
1647
1821
  }
1648
1822
 
1649
1823
  // src/components/ReportShell.tsx
1650
- var import_jsx_runtime33 = require("react/jsx-runtime");
1824
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1651
1825
  function ReportErrorShell({
1652
1826
  error,
1653
1827
  className,
1654
1828
  title,
1655
1829
  dataTheme
1656
1830
  }) {
1657
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ReportSchemaError, { error }) });
1831
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(ReportSchemaError, { error }) });
1658
1832
  }
1659
1833
 
1660
1834
  // src/interactive/ReportInteractive.tsx
@@ -1663,12 +1837,12 @@ var import_lucide_react5 = require("lucide-react");
1663
1837
  // src/components/ui/switch.tsx
1664
1838
  var React4 = require("react");
1665
1839
  var import_switch = require("@base-ui/react/switch");
1666
- var import_jsx_runtime34 = require("react/jsx-runtime");
1840
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1667
1841
  function Switch({
1668
1842
  className,
1669
1843
  ...props
1670
1844
  }) {
1671
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1845
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1672
1846
  import_switch.Switch.Root,
1673
1847
  {
1674
1848
  "data-slot": "switch",
@@ -1677,7 +1851,7 @@ function Switch({
1677
1851
  className
1678
1852
  ),
1679
1853
  ...props,
1680
- children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1854
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1681
1855
  import_switch.Switch.Thumb,
1682
1856
  {
1683
1857
  "data-slot": "switch-thumb",
@@ -1693,7 +1867,7 @@ var React5 = __toESM(require("react"), 1);
1693
1867
  var import_toggle_group = require("@base-ui/react/toggle-group");
1694
1868
  var import_toggle = require("@base-ui/react/toggle");
1695
1869
  var import_class_variance_authority3 = require("class-variance-authority");
1696
- var import_jsx_runtime35 = require("react/jsx-runtime");
1870
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1697
1871
  var toggleItemVariants = (0, import_class_variance_authority3.cva)(
1698
1872
  "inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap text-muted-foreground transition-[color,background-color,box-shadow] outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-background data-[pressed]:text-foreground data-[pressed]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1699
1873
  {
@@ -1715,13 +1889,13 @@ function ToggleGroup({
1715
1889
  children,
1716
1890
  ...props
1717
1891
  }) {
1718
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1892
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1719
1893
  import_toggle_group.ToggleGroup,
1720
1894
  {
1721
1895
  "data-slot": "toggle-group",
1722
1896
  className: cn("flex w-fit items-center gap-0.5 rounded-lg bg-muted p-0.5", className),
1723
1897
  ...props,
1724
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ToggleGroupContext.Provider, { value: { size }, children })
1898
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ToggleGroupContext.Provider, { value: { size }, children })
1725
1899
  }
1726
1900
  );
1727
1901
  }
@@ -1732,7 +1906,7 @@ function ToggleGroupItem({
1732
1906
  ...props
1733
1907
  }) {
1734
1908
  const context = React5.useContext(ToggleGroupContext);
1735
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1909
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1736
1910
  import_toggle.Toggle,
1737
1911
  {
1738
1912
  "data-slot": "toggle-group-item",
@@ -1749,9 +1923,9 @@ var import_lucide_react3 = require("lucide-react");
1749
1923
 
1750
1924
  // src/components/ui/input.tsx
1751
1925
  var React6 = require("react");
1752
- var import_jsx_runtime36 = require("react/jsx-runtime");
1926
+ var import_jsx_runtime37 = require("react/jsx-runtime");
1753
1927
  function Input({ className, type, ...props }) {
1754
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1928
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1755
1929
  "input",
1756
1930
  {
1757
1931
  type,
@@ -1768,7 +1942,7 @@ function Input({ className, type, ...props }) {
1768
1942
  }
1769
1943
 
1770
1944
  // src/interactive/ReportSearch.tsx
1771
- var import_jsx_runtime37 = require("react/jsx-runtime");
1945
+ var import_jsx_runtime38 = require("react/jsx-runtime");
1772
1946
  var ReportSearch = (0, import_react8.forwardRef)(
1773
1947
  function ReportSearch2(props, ref) {
1774
1948
  const {
@@ -1790,17 +1964,17 @@ var ReportSearch = (0, import_react8.forwardRef)(
1790
1964
  e.preventDefault();
1791
1965
  }
1792
1966
  }
1793
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: cn("es-search flex flex-col gap-1", className), children: [
1794
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
1795
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "relative w-full sm:w-72", children: [
1796
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1967
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: cn("es-search flex flex-col gap-1", className), children: [
1968
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
1969
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "relative w-full sm:w-72", children: [
1970
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1797
1971
  import_lucide_react3.Search,
1798
1972
  {
1799
1973
  "aria-hidden": true,
1800
1974
  className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
1801
1975
  }
1802
1976
  ),
1803
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1977
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1804
1978
  Input,
1805
1979
  {
1806
1980
  ref,
@@ -1817,7 +1991,7 @@ var ReportSearch = (0, import_react8.forwardRef)(
1817
1991
  }
1818
1992
  )
1819
1993
  ] }),
1820
- showCounts ? /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
1994
+ showCounts ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
1821
1995
  "span",
1822
1996
  {
1823
1997
  className: "text-xs tabular-nums text-muted-foreground",
@@ -1837,6 +2011,54 @@ var ReportSearch = (0, import_react8.forwardRef)(
1837
2011
  // src/interactive/ReportFailureBanner.tsx
1838
2012
  var import_react9 = require("react");
1839
2013
 
2014
+ // src/lib/hash-state.ts
2015
+ var DEFAULT_URL_STATE = {
2016
+ query: "",
2017
+ status: "all",
2018
+ tags: [],
2019
+ detail: "full"
2020
+ };
2021
+ var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
2022
+ function splitHash(hash) {
2023
+ const raw = hash.replace(/^#/, "");
2024
+ const split = raw.indexOf("?");
2025
+ if (split === -1) return { anchor: raw, params: new URLSearchParams() };
2026
+ return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
2027
+ }
2028
+ function decodeUrlState(params) {
2029
+ const status = params.get("status");
2030
+ const tags = params.get("tags");
2031
+ return {
2032
+ query: params.get("q") ?? DEFAULT_URL_STATE.query,
2033
+ status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
2034
+ tags: tags ? tags.split(",").filter(Boolean) : [],
2035
+ detail: params.get("docs") === "0" ? "minimal" : "full"
2036
+ };
2037
+ }
2038
+ function encodeUrlState(state) {
2039
+ const params = new URLSearchParams();
2040
+ if (state.query) params.set("q", state.query);
2041
+ if (state.status !== "all") params.set("status", state.status);
2042
+ if (state.tags.length > 0) params.set("tags", state.tags.join(","));
2043
+ if (state.detail === "minimal") params.set("docs", "0");
2044
+ return params.toString();
2045
+ }
2046
+ function formatHash(anchor, params) {
2047
+ if (params) return `#${anchor}?${params}`;
2048
+ return anchor ? `#${anchor}` : "";
2049
+ }
2050
+ function writeHash(anchor, params) {
2051
+ if (typeof window === "undefined") return;
2052
+ const next = formatHash(anchor, params);
2053
+ if (next === window.location.hash) return;
2054
+ const url = next || window.location.href.split("#")[0];
2055
+ try {
2056
+ window.history.replaceState(window.history.state, "", url);
2057
+ } catch {
2058
+ window.location.hash = next.slice(1);
2059
+ }
2060
+ }
2061
+
1840
2062
  // src/lib/scroll.ts
1841
2063
  function prefersReducedMotion() {
1842
2064
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -1852,13 +2074,13 @@ function scrollToScenarioId(scenarioId, options = {}) {
1852
2074
  behavior: prefersReducedMotion() ? "auto" : "smooth",
1853
2075
  block: "start"
1854
2076
  });
1855
- if (options.updateHash !== false && typeof history !== "undefined") {
1856
- history.replaceState(null, "", `#${scenarioId}`);
2077
+ if (options.updateHash !== false) {
2078
+ writeHash(scenarioId, splitHash(window.location.hash).params.toString());
1857
2079
  }
1858
2080
  }
1859
2081
 
1860
2082
  // src/interactive/ReportFailureBanner.tsx
1861
- var import_jsx_runtime38 = require("react/jsx-runtime");
2083
+ var import_jsx_runtime39 = require("react/jsx-runtime");
1862
2084
  function ReportFailureBanner({ failures }) {
1863
2085
  const first = failures[0];
1864
2086
  const jumpToFirst = (0, import_react9.useCallback)(() => {
@@ -1866,7 +2088,7 @@ function ReportFailureBanner({ failures }) {
1866
2088
  scrollToScenarioId(first.scenarioId);
1867
2089
  }, [first]);
1868
2090
  if (failures.length === 0) return null;
1869
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
2091
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
1870
2092
  "div",
1871
2093
  {
1872
2094
  className: "es-failure-banner",
@@ -1874,13 +2096,13 @@ function ReportFailureBanner({ failures }) {
1874
2096
  "aria-live": "polite",
1875
2097
  "aria-label": "Failure summary",
1876
2098
  children: [
1877
- /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "es-failure-banner-text", children: [
1878
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("strong", { children: failures.length }),
2099
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "es-failure-banner-text", children: [
2100
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("strong", { children: failures.length }),
1879
2101
  " ",
1880
2102
  "failure",
1881
2103
  failures.length === 1 ? "" : "s"
1882
2104
  ] }),
1883
- /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2105
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1884
2106
  "button",
1885
2107
  {
1886
2108
  type: "button",
@@ -1897,7 +2119,7 @@ function ReportFailureBanner({ failures }) {
1897
2119
 
1898
2120
  // src/interactive/ReportFreshness.tsx
1899
2121
  var import_react10 = require("react");
1900
- var import_jsx_runtime39 = require("react/jsx-runtime");
2122
+ var import_jsx_runtime40 = require("react/jsx-runtime");
1901
2123
  function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1902
2124
  const [now, setNow] = (0, import_react10.useState)(nowMs);
1903
2125
  (0, import_react10.useEffect)(() => {
@@ -1905,7 +2127,7 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1905
2127
  }, [nowMs]);
1906
2128
  if (lastRunMs === void 0 || now === void 0) return null;
1907
2129
  const age = formatRelativeAge(lastRunMs, now);
1908
- const runLink = ciUrl ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2130
+ const runLink = ciUrl ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
1909
2131
  "a",
1910
2132
  {
1911
2133
  href: ciUrl,
@@ -1916,9 +2138,9 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1916
2138
  }
1917
2139
  ) : null;
1918
2140
  if (isReportStale(lastRunMs, staleAfterDays, now)) {
1919
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
1920
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { children: [
1921
- /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("strong", { children: [
2141
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2142
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { children: [
2143
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("strong", { children: [
1922
2144
  "Last verified ",
1923
2145
  age,
1924
2146
  "."
@@ -1928,10 +2150,10 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1928
2150
  runLink
1929
2151
  ] });
1930
2152
  }
1931
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2153
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
1932
2154
  "Verified ",
1933
2155
  age,
1934
- runLink ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_jsx_runtime39.Fragment, { children: [
2156
+ runLink ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
1935
2157
  " \xB7 ",
1936
2158
  runLink
1937
2159
  ] }) : null
@@ -1939,15 +2161,15 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1939
2161
  }
1940
2162
 
1941
2163
  // src/interactive/ReportLastRunDelta.tsx
1942
- var import_jsx_runtime40 = require("react/jsx-runtime");
2164
+ var import_jsx_runtime41 = require("react/jsx-runtime");
1943
2165
  var MAX_LINKS = 5;
1944
2166
  function ScenarioLinks({ scenarios, tone }) {
1945
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
1946
- scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { children: [
2167
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
2168
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { children: [
1947
2169
  i > 0 ? ", " : "",
1948
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2170
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
1949
2171
  ] }, s.id)),
1950
- scenarios.length > MAX_LINKS ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { children: [
2172
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { children: [
1951
2173
  " and ",
1952
2174
  scenarios.length - MAX_LINKS,
1953
2175
  " more"
@@ -1955,32 +2177,32 @@ function ScenarioLinks({ scenarios, tone }) {
1955
2177
  ] });
1956
2178
  }
1957
2179
  function ReportLastRunDelta({
1958
- history: history2,
2180
+ history,
1959
2181
  report
1960
2182
  }) {
1961
- if (!history2) return null;
2183
+ if (!history) return null;
1962
2184
  const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
1963
- const delta = diffSinceLastRun(history2, scenarios);
2185
+ const delta = diffSinceLastRun(history, scenarios);
1964
2186
  if (!delta) return null;
1965
2187
  const { newlyFailing, newlyPassing, added } = delta;
1966
2188
  if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
1967
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2189
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
1968
2190
  }
1969
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
1970
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "font-medium text-foreground", children: "Since last run:" }),
1971
- newlyFailing.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "ml-2 text-fail", children: [
2191
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2192
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2193
+ newlyFailing.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "ml-2 text-fail", children: [
1972
2194
  newlyFailing.length,
1973
2195
  " newly failing (",
1974
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2196
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
1975
2197
  ")"
1976
2198
  ] }) : null,
1977
- newlyPassing.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "ml-2 text-pass", children: [
2199
+ newlyPassing.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "ml-2 text-pass", children: [
1978
2200
  newlyPassing.length,
1979
2201
  " fixed (",
1980
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2202
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
1981
2203
  ")"
1982
2204
  ] }) : null,
1983
- added.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "ml-2", children: [
2205
+ added.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "ml-2", children: [
1984
2206
  added.length,
1985
2207
  " new"
1986
2208
  ] }) : null
@@ -2044,7 +2266,7 @@ function Button({
2044
2266
  }
2045
2267
 
2046
2268
  // src/interactive/ReportShortcutsHelp.tsx
2047
- var import_jsx_runtime41 = require("react/jsx-runtime");
2269
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2048
2270
  var SHORTCUTS = [
2049
2271
  { keys: "/", description: "Focus search" },
2050
2272
  { keys: "f", description: "Jump to next failure" },
@@ -2056,9 +2278,9 @@ var SHORTCUTS = [
2056
2278
  ];
2057
2279
  function Keys({ keys }) {
2058
2280
  const parts = keys.split(" ");
2059
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
2060
- i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2061
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2281
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
2282
+ i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2283
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2062
2284
  ] }, k)) });
2063
2285
  }
2064
2286
  function ReportShortcutsHelp({ open, onClose }) {
@@ -2069,7 +2291,7 @@ function ReportShortcutsHelp({ open, onClose }) {
2069
2291
  if (open && !dialog.open) dialog.showModal();
2070
2292
  else if (!open && dialog.open) dialog.close();
2071
2293
  }, [open]);
2072
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2294
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2073
2295
  "dialog",
2074
2296
  {
2075
2297
  ref: dialogRef,
@@ -2077,12 +2299,12 @@ function ReportShortcutsHelp({ open, onClose }) {
2077
2299
  "aria-label": "Keyboard shortcuts",
2078
2300
  onClose,
2079
2301
  children: [
2080
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2081
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2082
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("dt", { className: "justify-self-start", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Keys, { keys: s.keys }) }),
2083
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("dd", { className: "text-sm text-muted-foreground", children: s.description })
2302
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2304
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("dt", { className: "justify-self-start", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(Keys, { keys: s.keys }) }),
2305
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("dd", { className: "text-sm text-muted-foreground", children: s.description })
2084
2306
  ] }, s.keys)) }),
2085
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2307
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2086
2308
  ]
2087
2309
  }
2088
2310
  );
@@ -2146,12 +2368,14 @@ function useKeyboardShortcuts(handlers) {
2146
2368
  // src/interactive/use-deep-link-scroll.ts
2147
2369
  var import_react13 = require("react");
2148
2370
  function useDeepLinkScroll() {
2371
+ const lastAnchor = (0, import_react13.useRef)(null);
2149
2372
  (0, import_react13.useEffect)(() => {
2150
2373
  function scrollToHash() {
2151
- const hash = window.location.hash.replace(/^#/, "");
2152
- if (!hash) return;
2374
+ const { anchor } = splitHash(window.location.hash);
2375
+ if (!anchor || anchor === lastAnchor.current) return;
2376
+ lastAnchor.current = anchor;
2153
2377
  requestAnimationFrame(() => {
2154
- scrollToScenarioId(hash, { updateHash: false });
2378
+ scrollToScenarioId(anchor, { updateHash: false });
2155
2379
  });
2156
2380
  }
2157
2381
  scrollToHash();
@@ -2283,7 +2507,7 @@ function failuresFirst(report) {
2283
2507
 
2284
2508
  // src/interactive/ReportFilters.tsx
2285
2509
  var import_react14 = require("react");
2286
- var import_jsx_runtime42 = require("react/jsx-runtime");
2510
+ var import_jsx_runtime43 = require("react/jsx-runtime");
2287
2511
  var TAG_LIMIT = 12;
2288
2512
  function ReportFilters({
2289
2513
  statuses,
@@ -2297,10 +2521,10 @@ function ReportFilters({
2297
2521
  if (statuses.length === 0 && tags.length === 0) return null;
2298
2522
  const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2299
2523
  const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2300
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "flex flex-col gap-2", children: [
2301
- statuses.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2524
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex flex-col gap-2", children: [
2525
+ statuses.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2302
2526
  const active = status === opt.key;
2303
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2527
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2304
2528
  "button",
2305
2529
  {
2306
2530
  type: "button",
@@ -2312,23 +2536,23 @@ function ReportFilters({
2312
2536
  ),
2313
2537
  children: [
2314
2538
  opt.label,
2315
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2539
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2316
2540
  ]
2317
2541
  },
2318
2542
  opt.key
2319
2543
  );
2320
2544
  }) }) : null,
2321
- tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2545
+ tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2322
2546
  visibleTags.map((tag) => {
2323
2547
  const active = activeTags.includes(tag);
2324
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2548
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2325
2549
  "button",
2326
2550
  {
2327
2551
  type: "button",
2328
2552
  "aria-pressed": active,
2329
2553
  onClick: () => onToggleTag(tag),
2330
2554
  className: "cursor-pointer",
2331
- children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2555
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2332
2556
  Badge,
2333
2557
  {
2334
2558
  variant: "tag",
@@ -2341,7 +2565,7 @@ function ReportFilters({
2341
2565
  }
2342
2566
  ) }, tag);
2343
2567
  }),
2344
- tags.length > TAG_LIMIT ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2568
+ tags.length > TAG_LIMIT ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2345
2569
  "button",
2346
2570
  {
2347
2571
  type: "button",
@@ -2357,7 +2581,7 @@ function ReportFilters({
2357
2581
 
2358
2582
  // src/interactive/ReportToc.tsx
2359
2583
  var import_react15 = require("react");
2360
- var import_jsx_runtime43 = require("react/jsx-runtime");
2584
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2361
2585
  var DOT = {
2362
2586
  passed: "bg-pass",
2363
2587
  failed: "bg-fail",
@@ -2386,11 +2610,11 @@ function TocContent({ onNavigate }) {
2386
2610
  return () => observer.disconnect();
2387
2611
  }, [scenarioIds]);
2388
2612
  if (report.features.length === 0) return null;
2389
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("li", { children: [
2390
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2391
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2613
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("li", { children: [
2614
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2615
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2392
2616
  const active = activeId === scenario.id;
2393
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2617
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
2394
2618
  "a",
2395
2619
  {
2396
2620
  href: `#${scenario.id}`,
@@ -2405,14 +2629,14 @@ function TocContent({ onNavigate }) {
2405
2629
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
2406
2630
  ),
2407
2631
  children: [
2408
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
2632
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2409
2633
  "span",
2410
2634
  {
2411
2635
  "aria-hidden": true,
2412
2636
  className: cn("mt-1 size-1.5 shrink-0 rounded-full", DOT[scenario.status])
2413
2637
  }
2414
2638
  ),
2415
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "break-words", children: scenario.title })
2639
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "break-words", children: scenario.title })
2416
2640
  ]
2417
2641
  }
2418
2642
  ) }, scenario.id);
@@ -2422,14 +2646,14 @@ function TocContent({ onNavigate }) {
2422
2646
  function ReportToc() {
2423
2647
  const report = useReport();
2424
2648
  if (report.features.length === 0) return null;
2425
- return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
2649
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
2426
2650
  "nav",
2427
2651
  {
2428
2652
  "aria-label": "Table of contents",
2429
2653
  className: "sticky top-4 hidden h-fit w-64 shrink-0 self-start text-xs lg:block",
2430
2654
  children: [
2431
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2432
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(TocContent, {})
2655
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2656
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(TocContent, {})
2433
2657
  ]
2434
2658
  }
2435
2659
  );
@@ -2439,31 +2663,31 @@ function ReportToc() {
2439
2663
  var import_react16 = require("react");
2440
2664
  var import_dialog = require("@base-ui/react/dialog");
2441
2665
  var import_lucide_react4 = require("lucide-react");
2442
- var import_jsx_runtime44 = require("react/jsx-runtime");
2666
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2443
2667
  function ReportTocDrawer() {
2444
2668
  const report = useReport();
2445
2669
  const [open, setOpen] = (0, import_react16.useState)(false);
2446
2670
  if (report.features.length === 0) return null;
2447
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_dialog.Dialog.Root, { open, onOpenChange: setOpen, children: [
2448
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_dialog.Dialog.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2449
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react4.ListIcon, { "aria-hidden": true }),
2671
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_dialog.Dialog.Root, { open, onOpenChange: setOpen, children: [
2672
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_dialog.Dialog.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2673
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react4.ListIcon, { "aria-hidden": true }),
2450
2674
  " Contents"
2451
2675
  ] }),
2452
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_dialog.Dialog.Portal, { children: [
2453
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_dialog.Dialog.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2454
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_dialog.Dialog.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2455
- /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex items-center justify-between", children: [
2456
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_dialog.Dialog.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2457
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2676
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_dialog.Dialog.Portal, { children: [
2677
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_dialog.Dialog.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2678
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_dialog.Dialog.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2679
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center justify-between", children: [
2680
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_dialog.Dialog.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2681
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2458
2682
  import_dialog.Dialog.Close,
2459
2683
  {
2460
2684
  "aria-label": "Close contents",
2461
2685
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
2462
- children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react4.XIcon, { "aria-hidden": true })
2686
+ children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_lucide_react4.XIcon, { "aria-hidden": true })
2463
2687
  }
2464
2688
  )
2465
2689
  ] }),
2466
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(TocContent, { onNavigate: () => setOpen(false) })
2690
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(TocContent, { onNavigate: () => setOpen(false) })
2467
2691
  ] })
2468
2692
  ] })
2469
2693
  ] });
@@ -2517,15 +2741,43 @@ function useTheme() {
2517
2741
  return { pref, setPref };
2518
2742
  }
2519
2743
 
2744
+ // src/interactive/use-url-state.ts
2745
+ var import_react18 = require("react");
2746
+ var WRITE_DELAY_MS = 250;
2747
+ function useUrlState() {
2748
+ const [state, setState] = (0, import_react18.useState)(DEFAULT_URL_STATE);
2749
+ const ready = (0, import_react18.useRef)(false);
2750
+ (0, import_react18.useEffect)(() => {
2751
+ const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2752
+ read();
2753
+ ready.current = true;
2754
+ window.addEventListener("hashchange", read);
2755
+ return () => window.removeEventListener("hashchange", read);
2756
+ }, []);
2757
+ (0, import_react18.useEffect)(() => {
2758
+ if (!ready.current) return;
2759
+ const timer = setTimeout(
2760
+ () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
2761
+ WRITE_DELAY_MS
2762
+ );
2763
+ return () => clearTimeout(timer);
2764
+ }, [state]);
2765
+ const update = (0, import_react18.useCallback)(
2766
+ (patch) => setState((prev) => ({ ...prev, ...patch })),
2767
+ []
2768
+ );
2769
+ return [state, update];
2770
+ }
2771
+
2520
2772
  // src/interactive/ReportInteractive.tsx
2521
- var import_jsx_runtime45 = require("react/jsx-runtime");
2773
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2522
2774
  function ReportInteractive(props) {
2523
2775
  const { report, className, title, dataTheme } = props;
2524
2776
  const result = unwrapReport(report);
2525
2777
  if (!result.ok) {
2526
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
2778
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
2527
2779
  }
2528
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportInteractiveView, { ...props, report: result.data });
2780
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportInteractiveView, { ...props, report: result.data });
2529
2781
  }
2530
2782
  var THEME_OPTIONS = [
2531
2783
  { value: "light", label: "Light", Icon: import_lucide_react5.Sun },
@@ -2533,7 +2785,7 @@ var THEME_OPTIONS = [
2533
2785
  { value: "dark", label: "Dark", Icon: import_lucide_react5.Moon }
2534
2786
  ];
2535
2787
  function ThemeSegment({ pref, onPref }) {
2536
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2788
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2537
2789
  ToggleGroup,
2538
2790
  {
2539
2791
  size: "sm",
@@ -2543,7 +2795,7 @@ function ThemeSegment({ pref, onPref }) {
2543
2795
  if (next) onPref(next);
2544
2796
  },
2545
2797
  "aria-label": "Color theme",
2546
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ToggleGroupItem, { value, "aria-label": label, title: label, className: "px-2", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Icon, {}) }, value))
2798
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ToggleGroupItem, { value, "aria-label": label, title: label, className: "px-2", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(Icon, {}) }, value))
2547
2799
  }
2548
2800
  );
2549
2801
  }
@@ -2559,25 +2811,32 @@ function ReportInteractiveView({
2559
2811
  staleAfterDays = 7,
2560
2812
  scenarioHistory
2561
2813
  }) {
2562
- const [query, setQuery] = (0, import_react18.useState)("");
2563
- const [statusFilter, setStatusFilter] = (0, import_react18.useState)("all");
2564
- const [activeTags, setActiveTags] = (0, import_react18.useState)([]);
2565
- const [detail, setDetail] = (0, import_react18.useState)("full");
2566
- const [helpOpen, setHelpOpen] = (0, import_react18.useState)(false);
2814
+ const [urlState, setUrlState] = useUrlState();
2815
+ const { query, status: statusFilter, tags: activeTags, detail } = urlState;
2816
+ const setQuery = (0, import_react19.useCallback)((next) => setUrlState({ query: next }), [setUrlState]);
2817
+ const setStatusFilter = (0, import_react19.useCallback)(
2818
+ (next) => setUrlState({ status: next }),
2819
+ [setUrlState]
2820
+ );
2821
+ const setDetail = (0, import_react19.useCallback)(
2822
+ (next) => setUrlState({ detail: next }),
2823
+ [setUrlState]
2824
+ );
2825
+ const [helpOpen, setHelpOpen] = (0, import_react19.useState)(false);
2567
2826
  const { pref: themePref, setPref: setThemePref } = useTheme();
2568
- const searchRef = (0, import_react18.useRef)(null);
2569
- const expandId = (0, import_react18.useId)();
2570
- const docsId = (0, import_react18.useId)();
2571
- const deferredQuery = (0, import_react18.useDeferredValue)(query);
2572
- const failures = (0, import_react18.useMemo)(() => listFailures(report), [report]);
2573
- const filtered = (0, import_react18.useMemo)(
2827
+ const searchRef = (0, import_react19.useRef)(null);
2828
+ const expandId = (0, import_react19.useId)();
2829
+ const docsId = (0, import_react19.useId)();
2830
+ const deferredQuery = (0, import_react19.useDeferredValue)(query);
2831
+ const failures = (0, import_react19.useMemo)(() => listFailures(report), [report]);
2832
+ const filtered = (0, import_react19.useMemo)(
2574
2833
  // Features carrying failures float to the top so the first thing on screen
2575
2834
  // is the thing that needs attention; an all-green run keeps source order.
2576
2835
  () => failuresFirst(filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags })),
2577
2836
  [report, deferredQuery, statusFilter, activeTags]
2578
2837
  );
2579
2838
  const isFiltering = query !== deferredQuery;
2580
- const statusOptions = (0, import_react18.useMemo)(() => {
2839
+ const statusOptions = (0, import_react19.useMemo)(() => {
2581
2840
  const s = report.summary;
2582
2841
  const opts = [
2583
2842
  { key: "all", label: "All", count: s.total }
@@ -2588,19 +2847,21 @@ function ReportInteractiveView({
2588
2847
  if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
2589
2848
  return opts;
2590
2849
  }, [report]);
2591
- const tagOptions = (0, import_react18.useMemo)(() => allTags(report), [report]);
2592
- const toggleTag = (0, import_react18.useCallback)(
2593
- (tag) => setActiveTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]),
2594
- []
2850
+ const tagOptions = (0, import_react19.useMemo)(() => allTags(report), [report]);
2851
+ const toggleTag = (0, import_react19.useCallback)(
2852
+ (tag) => setUrlState({
2853
+ tags: activeTags.includes(tag) ? activeTags.filter((t) => t !== tag) : [...activeTags, tag]
2854
+ }),
2855
+ [activeTags, setUrlState]
2595
2856
  );
2596
- const [toast, setToast] = (0, import_react18.useState)(null);
2597
- const toastTimer = (0, import_react18.useRef)(null);
2598
- const showToast = (0, import_react18.useCallback)((msg) => {
2857
+ const [toast, setToast] = (0, import_react19.useState)(null);
2858
+ const toastTimer = (0, import_react19.useRef)(null);
2859
+ const showToast = (0, import_react19.useCallback)((msg) => {
2599
2860
  setToast(msg);
2600
2861
  if (toastTimer.current) clearTimeout(toastTimer.current);
2601
2862
  toastTimer.current = setTimeout(() => setToast(null), 1800);
2602
2863
  }, []);
2603
- const copy = (0, import_react18.useCallback)(
2864
+ const copy = (0, import_react19.useCallback)(
2604
2865
  (text, msg) => {
2605
2866
  if (typeof navigator !== "undefined" && navigator.clipboard) {
2606
2867
  navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
@@ -2608,7 +2869,7 @@ function ReportInteractiveView({
2608
2869
  },
2609
2870
  [showToast]
2610
2871
  );
2611
- const scenarioActions = (0, import_react18.useMemo)(
2872
+ const scenarioActions = (0, import_react19.useMemo)(
2612
2873
  () => ({
2613
2874
  copyLink: (s) => copy(scenarioPermalink(s), "Link copied"),
2614
2875
  copyMarkdown: (s) => copy(scenarioToMarkdown2(s), "Markdown copied"),
@@ -2616,15 +2877,15 @@ function ReportInteractiveView({
2616
2877
  }),
2617
2878
  [copy]
2618
2879
  );
2619
- const failureIndexRef = (0, import_react18.useRef)(0);
2620
- const allCollapsibleIds = (0, import_react18.useMemo)(
2880
+ const failureIndexRef = (0, import_react19.useRef)(0);
2881
+ const allCollapsibleIds = (0, import_react19.useMemo)(
2621
2882
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
2622
2883
  [report]
2623
2884
  );
2624
- const collapse = useCollapseState((0, import_react18.useCallback)(() => defaultCollapsedIds(report), [report]));
2625
- const collapseAll = (0, import_react18.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2626
- const [expandedAll, setExpandedAll] = (0, import_react18.useState)(() => report.summary.failed === 0);
2627
- const setExpanded = (0, import_react18.useCallback)(
2885
+ const collapse = useCollapseState((0, import_react19.useCallback)(() => defaultCollapsedIds(report), [report]));
2886
+ const collapseAll = (0, import_react19.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2887
+ const [expandedAll, setExpandedAll] = (0, import_react19.useState)(() => report.summary.failed === 0);
2888
+ const setExpanded = (0, import_react19.useCallback)(
2628
2889
  (expanded) => {
2629
2890
  if (expanded) collapse.expandAll();
2630
2891
  else collapseAll();
@@ -2632,10 +2893,10 @@ function ReportInteractiveView({
2632
2893
  },
2633
2894
  [collapse, collapseAll]
2634
2895
  );
2635
- const focusSearch = (0, import_react18.useCallback)(() => {
2896
+ const focusSearch = (0, import_react19.useCallback)(() => {
2636
2897
  searchRef.current?.focus();
2637
2898
  }, []);
2638
- const stepFailure = (0, import_react18.useCallback)(
2899
+ const stepFailure = (0, import_react19.useCallback)(
2639
2900
  (direction) => {
2640
2901
  if (failures.length === 0) return;
2641
2902
  failureIndexRef.current = (failureIndexRef.current + direction + failures.length) % failures.length;
@@ -2644,10 +2905,10 @@ function ReportInteractiveView({
2644
2905
  },
2645
2906
  [failures]
2646
2907
  );
2647
- const toggleHelp = (0, import_react18.useCallback)(() => {
2908
+ const toggleHelp = (0, import_react19.useCallback)(() => {
2648
2909
  setHelpOpen((v) => !v);
2649
2910
  }, []);
2650
- const escape = (0, import_react18.useCallback)(() => {
2911
+ const escape = (0, import_react19.useCallback)(() => {
2651
2912
  if (helpOpen) setHelpOpen(false);
2652
2913
  else if (query !== "") setQuery("");
2653
2914
  }, [helpOpen, query]);
@@ -2665,13 +2926,13 @@ function ReportInteractiveView({
2665
2926
  const totalScenarios = report.summary.total;
2666
2927
  const matchedScenarios = filtered.summary.total;
2667
2928
  const showToc = !hideToc && totalScenarios > 3;
2668
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2929
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2669
2930
  ReportRoot,
2670
2931
  {
2671
2932
  report: filtered,
2672
2933
  customRenderers,
2673
2934
  renderers,
2674
- children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2935
+ children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2675
2936
  "main",
2676
2937
  {
2677
2938
  className: cn("es-report", "es-report-interactive", className),
@@ -2680,8 +2941,8 @@ function ReportInteractiveView({
2680
2941
  "data-theme": dataTheme,
2681
2942
  "data-detail-level": detail,
2682
2943
  children: [
2683
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("header", { className: "es-report-header", children: [
2684
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2944
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("header", { className: "es-report-header", children: [
2945
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2685
2946
  "div",
2686
2947
  {
2687
2948
  "data-es-navbar": true,
@@ -2690,9 +2951,9 @@ function ReportInteractiveView({
2690
2951
  hideHeader ? "justify-end" : "justify-between"
2691
2952
  ),
2692
2953
  children: [
2693
- hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2694
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
2695
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2954
+ hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2955
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
2956
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2696
2957
  ReportSearch,
2697
2958
  {
2698
2959
  ref: searchRef,
@@ -2702,16 +2963,16 @@ function ReportInteractiveView({
2702
2963
  totalCount: totalScenarios
2703
2964
  }
2704
2965
  ),
2705
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ThemeSegment, { pref: themePref, onPref: setThemePref })
2966
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ThemeSegment, { pref: themePref, onPref: setThemePref })
2706
2967
  ] })
2707
2968
  ]
2708
2969
  }
2709
2970
  ),
2710
- hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
2711
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportSummary, {}),
2712
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportMeta, {})
2971
+ hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
2972
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportSummary, {}),
2973
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportMeta, {})
2713
2974
  ] }),
2714
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2975
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2715
2976
  ReportFreshness,
2716
2977
  {
2717
2978
  lastRunMs: reportLastRunMs(report),
@@ -2719,8 +2980,8 @@ function ReportInteractiveView({
2719
2980
  staleAfterDays
2720
2981
  }
2721
2982
  ),
2722
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportLastRunDelta, { history: scenarioHistory, report }),
2723
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2983
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportLastRunDelta, { history: scenarioHistory, report }),
2984
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2724
2985
  ReportFilters,
2725
2986
  {
2726
2987
  statuses: statusOptions,
@@ -2731,14 +2992,14 @@ function ReportInteractiveView({
2731
2992
  onToggleTag: toggleTag
2732
2993
  }
2733
2994
  ),
2734
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
2735
- showToc ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportTocDrawer, {}) : null,
2736
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
2737
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Switch, { id: expandId, checked: expandedAll, onCheckedChange: setExpanded }),
2738
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("label", { htmlFor: expandId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Expand all" })
2995
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
2996
+ showToc ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportTocDrawer, {}) : null,
2997
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
2998
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(Switch, { id: expandId, checked: expandedAll, onCheckedChange: setExpanded }),
2999
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("label", { htmlFor: expandId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Expand all" })
2739
3000
  ] }),
2740
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
2741
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3001
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex items-center gap-2", children: [
3002
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2742
3003
  Switch,
2743
3004
  {
2744
3005
  id: docsId,
@@ -2746,16 +3007,16 @@ function ReportInteractiveView({
2746
3007
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
2747
3008
  }
2748
3009
  ),
2749
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("label", { htmlFor: docsId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Show documentation" })
3010
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("label", { htmlFor: docsId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Show documentation" })
2750
3011
  ] })
2751
3012
  ] })
2752
3013
  ] }),
2753
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportFailureBanner, { failures }),
2754
- hasContent ? showToc ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex gap-6", children: [
2755
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportToc, {}),
2756
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportFeatureList, {}) })
2757
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportFeatureList, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
2758
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3014
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportFailureBanner, { failures }),
3015
+ hasContent ? showToc ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "flex gap-6", children: [
3016
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportToc, {}),
3017
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportFeatureList, {}) })
3018
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportFeatureList, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
3019
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2759
3020
  "button",
2760
3021
  {
2761
3022
  type: "button",
@@ -2766,8 +3027,8 @@ function ReportInteractiveView({
2766
3027
  children: "?"
2767
3028
  }
2768
3029
  ),
2769
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
2770
- toast ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3030
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
3031
+ toast ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2771
3032
  "div",
2772
3033
  {
2773
3034
  role: "status",
@@ -2784,14 +3045,14 @@ function ReportInteractiveView({
2784
3045
  }
2785
3046
 
2786
3047
  // src/interactive/ReportInteractiveIsland.tsx
2787
- var import_react21 = require("react");
3048
+ var import_react22 = require("react");
2788
3049
 
2789
3050
  // src/interactive/island-renderers.tsx
2790
- var import_react20 = require("react");
3051
+ var import_react21 = require("react");
2791
3052
 
2792
3053
  // src/components/doc/MermaidView.tsx
2793
- var import_react19 = require("react");
2794
- var import_jsx_runtime46 = require("react/jsx-runtime");
3054
+ var import_react20 = require("react");
3055
+ var import_jsx_runtime47 = require("react/jsx-runtime");
2795
3056
  function prefersDark(el) {
2796
3057
  const themed = el?.closest("[data-theme]");
2797
3058
  if (themed?.dataset.theme === "dark") return true;
@@ -2799,23 +3060,40 @@ function prefersDark(el) {
2799
3060
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
2800
3061
  }
2801
3062
  function MermaidView({ entry, load }) {
2802
- const ref = (0, import_react19.useRef)(null);
2803
- const [svg, setSvg] = (0, import_react19.useState)(null);
2804
- (0, import_react19.useEffect)(() => {
3063
+ const ref = (0, import_react20.useRef)(null);
3064
+ const [svg, setSvg] = (0, import_react20.useState)(null);
3065
+ const [error, setError] = (0, import_react20.useState)(null);
3066
+ (0, import_react20.useEffect)(() => {
2805
3067
  let cancelled = false;
2806
3068
  const draw = async () => {
3069
+ let mermaid;
3070
+ try {
3071
+ mermaid = await load();
3072
+ } catch {
3073
+ if (!cancelled) {
3074
+ setSvg(null);
3075
+ setError(null);
3076
+ }
3077
+ return;
3078
+ }
2807
3079
  try {
2808
- const mermaid = await load();
2809
3080
  mermaid.initialize({
2810
3081
  startOnLoad: false,
2811
3082
  securityLevel: "strict",
2812
3083
  theme: prefersDark(ref.current) ? "dark" : "default"
2813
3084
  });
3085
+ await mermaid.parse(entry.code);
2814
3086
  const id = `es-mermaid-${Math.random().toString(36).slice(2)}`;
2815
3087
  const { svg: rendered } = await mermaid.render(id, entry.code);
2816
- if (!cancelled) setSvg(rendered);
2817
- } catch {
2818
- if (!cancelled) setSvg(null);
3088
+ if (!cancelled) {
3089
+ setSvg(rendered);
3090
+ setError(null);
3091
+ }
3092
+ } catch (err) {
3093
+ if (!cancelled) {
3094
+ setSvg(null);
3095
+ setError(err instanceof Error ? err.message : String(err));
3096
+ }
2819
3097
  }
2820
3098
  };
2821
3099
  void draw();
@@ -2828,9 +3106,9 @@ function MermaidView({ entry, load }) {
2828
3106
  };
2829
3107
  }, [entry.code, load]);
2830
3108
  if (svg) {
2831
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("figure", { ref, className: "my-3", children: [
2832
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
2833
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3109
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("figure", { ref, className: "my-3", children: [
3110
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3111
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2834
3112
  "div",
2835
3113
  {
2836
3114
  role: "img",
@@ -2841,11 +3119,25 @@ function MermaidView({ entry, load }) {
2841
3119
  )
2842
3120
  ] });
2843
3121
  }
2844
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(MermaidSource, { entry, ref });
3122
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_jsx_runtime47.Fragment, { children: [
3123
+ error ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3124
+ "p",
3125
+ {
3126
+ role: "alert",
3127
+ "data-es-mermaid-error": true,
3128
+ className: "mt-3 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
3129
+ children: [
3130
+ "Diagram failed to render: ",
3131
+ error
3132
+ ]
3133
+ }
3134
+ ) : null,
3135
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(MermaidSource, { entry, ref })
3136
+ ] });
2845
3137
  }
2846
3138
 
2847
3139
  // src/interactive/island-renderers.tsx
2848
- var import_jsx_runtime47 = require("react/jsx-runtime");
3140
+ var import_jsx_runtime48 = require("react/jsx-runtime");
2849
3141
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
2850
3142
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
2851
3143
  function cachedCdnModule(url) {
@@ -2862,8 +3154,8 @@ function cachedCdnModule(url) {
2862
3154
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
2863
3155
  var loadHljs = cachedCdnModule(HLJS_ESM);
2864
3156
  function CdnHighlightedCode({ entry }) {
2865
- const [html, setHtml] = (0, import_react20.useState)(null);
2866
- (0, import_react20.useEffect)(() => {
3157
+ const [html, setHtml] = (0, import_react21.useState)(null);
3158
+ (0, import_react21.useEffect)(() => {
2867
3159
  let cancelled = false;
2868
3160
  void (async () => {
2869
3161
  try {
@@ -2879,27 +3171,27 @@ function CdnHighlightedCode({ entry }) {
2879
3171
  cancelled = true;
2880
3172
  };
2881
3173
  }, [entry.content, entry.lang]);
2882
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3174
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2883
3175
  "code",
2884
3176
  {
2885
3177
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
2886
3178
  dangerouslySetInnerHTML: { __html: html }
2887
3179
  }
2888
- ) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3180
+ ) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
2889
3181
  }
2890
3182
  function buildIslandRenderers(opts) {
2891
3183
  const renderers = {};
2892
3184
  if (opts.mermaid) {
2893
- renderers.mermaid = (entry) => (0, import_react20.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
3185
+ renderers.mermaid = (entry) => (0, import_react21.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
2894
3186
  }
2895
3187
  if (opts.syntaxHighlighting) {
2896
- renderers.code = (entry) => (0, import_react20.createElement)(CdnHighlightedCode, { entry });
3188
+ renderers.code = (entry) => (0, import_react21.createElement)(CdnHighlightedCode, { entry });
2897
3189
  }
2898
3190
  return renderers;
2899
3191
  }
2900
3192
 
2901
3193
  // src/interactive/ReportInteractiveIsland.tsx
2902
- var import_jsx_runtime48 = require("react/jsx-runtime");
3194
+ var import_jsx_runtime49 = require("react/jsx-runtime");
2903
3195
  function ReportInteractiveIsland({
2904
3196
  report,
2905
3197
  title,
@@ -2909,11 +3201,11 @@ function ReportInteractiveIsland({
2909
3201
  syntaxHighlighting = true,
2910
3202
  mermaid = true
2911
3203
  }) {
2912
- const renderers = (0, import_react21.useMemo)(
3204
+ const renderers = (0, import_react22.useMemo)(
2913
3205
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
2914
3206
  [syntaxHighlighting, mermaid]
2915
3207
  );
2916
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3208
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2917
3209
  ReportInteractive,
2918
3210
  {
2919
3211
  report,