executable-stories-react 0.2.0 → 0.3.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.
@@ -22,6 +22,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var interactive_exports = {};
23
23
  __export(interactive_exports, {
24
24
  ReportFailureBanner: () => ReportFailureBanner,
25
+ ReportFreshness: () => ReportFreshness,
25
26
  ReportInteractive: () => ReportInteractive,
26
27
  ReportInteractiveIsland: () => ReportInteractiveIsland,
27
28
  ReportSearch: () => ReportSearch,
@@ -36,7 +37,7 @@ __export(interactive_exports, {
36
37
  module.exports = __toCommonJS(interactive_exports);
37
38
 
38
39
  // src/interactive/ReportInteractive.tsx
39
- var import_react14 = require("react");
40
+ var import_react16 = require("react");
40
41
 
41
42
  // src/result.ts
42
43
  var ok = (data) => ({ ok: true, data });
@@ -87,11 +88,23 @@ function useReport() {
87
88
  return ctx.report;
88
89
  }
89
90
 
90
- // src/components/ReportScenario.tsx
91
- var import_duration3 = require("executable-stories-core/utils/duration");
92
-
93
- // src/components/ReportSteps.tsx
94
- var import_duration = require("executable-stories-core/utils/duration");
91
+ // ../executable-stories-core/dist/utils/duration.js
92
+ function formatDuration(ms) {
93
+ if (ms < 1e3) {
94
+ return `${Math.round(ms)} ms`;
95
+ }
96
+ if (ms < 6e4) {
97
+ return `${(ms / 1e3).toFixed(2)} s`;
98
+ }
99
+ const minutes = Math.floor(ms / 6e4);
100
+ const seconds = Math.floor(ms % 6e4 / 1e3);
101
+ if (minutes < 60) {
102
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
103
+ }
104
+ const hours = Math.floor(minutes / 60);
105
+ const remainingMinutes = minutes % 60;
106
+ return `${hours}h ${remainingMinutes}m`;
107
+ }
95
108
 
96
109
  // src/components/doc/DocNote.tsx
97
110
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -245,6 +258,15 @@ function safeUrl(value) {
245
258
  if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
246
259
  return trimmed;
247
260
  }
261
+ function safeImageUrl(value) {
262
+ if (!value) return void 0;
263
+ const trimmed = value.trim();
264
+ if (/^data:image\//i.test(trimmed)) return trimmed;
265
+ return safeUrl(trimmed);
266
+ }
267
+ function isLocalFsPath(value) {
268
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
269
+ }
248
270
 
249
271
  // src/components/doc/DocSection.tsx
250
272
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -300,11 +322,19 @@ function DocMermaid({ entry }) {
300
322
  // src/components/doc/DocScreenshot.tsx
301
323
  var import_jsx_runtime10 = require("react/jsx-runtime");
302
324
  function DocScreenshot({ entry }) {
325
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
326
+ if (!src) {
327
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
328
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "font-medium", children: "Screenshot unavailable" }),
329
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
330
+ entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
331
+ ] });
332
+ }
303
333
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("figure", { className: "my-3", children: [
304
334
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
305
335
  "img",
306
336
  {
307
- src: entry.path,
337
+ src,
308
338
  alt: entry.alt ?? "",
309
339
  loading: "lazy",
310
340
  className: "max-w-full rounded-md border border-border"
@@ -317,6 +347,13 @@ function DocScreenshot({ entry }) {
317
347
  // src/components/doc/DocVideo.tsx
318
348
  var import_jsx_runtime11 = require("react/jsx-runtime");
319
349
  function DocVideo({ entry }) {
350
+ if (isLocalFsPath(entry.path)) {
351
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
352
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "font-medium", children: "Video unavailable" }),
353
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
354
+ entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
355
+ ] });
356
+ }
320
357
  const src = safeUrl(entry.path);
321
358
  const poster = safeUrl(entry.poster);
322
359
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { className: "my-3", children: [
@@ -492,7 +529,7 @@ function ReportStepItem({ step }) {
492
529
  }
493
530
  ),
494
531
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
495
- step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration.formatDuration)(step.durationMs) }) : null
532
+ 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
496
533
  ] }),
497
534
  step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
498
535
  "pre",
@@ -550,9 +587,6 @@ function ReportAttachments({ attachments }) {
550
587
  );
551
588
  }
552
589
 
553
- // src/components/ReportTrace.tsx
554
- var import_duration2 = require("executable-stories-core/utils/duration");
555
-
556
590
  // src/lib/trace.ts
557
591
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
558
592
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -647,7 +681,7 @@ function ReportTrace({ spans }) {
647
681
  " span",
648
682
  rows.length === 1 ? "" : "s",
649
683
  ", ",
650
- (0, import_duration2.formatDuration)(totalMs),
684
+ formatDuration(totalMs),
651
685
  ")"
652
686
  ] }),
653
687
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -670,18 +704,131 @@ function ReportTrace({ spans }) {
670
704
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
671
705
  }
672
706
  ) }),
673
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: (0, import_duration2.formatDuration)(row.durationMs) })
707
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
674
708
  ] }, `${row.spanId}-${i}`);
675
709
  }) })
676
710
  ] });
677
711
  }
678
712
 
679
- // src/interactive/collapse-context.ts
713
+ // src/interactive/scenario-history-context.ts
680
714
  var import_react5 = require("react");
681
- var CollapseContext = (0, import_react5.createContext)(null);
715
+ var ScenarioHistoryContext = (0, import_react5.createContext)(null);
716
+ var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
717
+ function useScenarioHistory() {
718
+ return (0, import_react5.useContext)(ScenarioHistoryContext);
719
+ }
720
+
721
+ // src/lib/run-history.ts
722
+ function currentStreak(entries) {
723
+ const last = entries[entries.length - 1];
724
+ if (!last) return void 0;
725
+ let count = 0;
726
+ for (let i = entries.length - 1; i >= 0 && entries[i].status === last.status; i--) count++;
727
+ return { status: last.status, count };
728
+ }
729
+ var STREAK_VERB = {
730
+ passed: "Passing",
731
+ failed: "Failing",
732
+ skipped: "Skipped",
733
+ pending: "Pending"
734
+ };
735
+ function describeRunHistory(entries) {
736
+ const passed = entries.filter((e) => e.status === "passed").length;
737
+ const base = `${passed}/${entries.length} runs passed`;
738
+ const streak = currentStreak(entries);
739
+ if (!streak || entries.length < 2) return base;
740
+ return `${base} \xB7 ${STREAK_VERB[streak.status]} for the last ${streak.count === 1 ? "run" : `${streak.count} runs`}`;
741
+ }
742
+ var MIN_FLAKINESS_SAMPLES = 3;
743
+ function flakinessOf(entries) {
744
+ const countable = entries.filter((e) => e.status === "passed" || e.status === "failed");
745
+ if (countable.length < MIN_FLAKINESS_SAMPLES) return "stable";
746
+ let transitions = 0;
747
+ for (let i = 1; i < countable.length; i++) {
748
+ if (countable[i].status !== countable[i - 1].status) transitions++;
749
+ }
750
+ const transitionScore = transitions / (countable.length - 1);
751
+ const failureRate = countable.filter((e) => e.status === "failed").length / countable.length;
752
+ if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) return "flaky";
753
+ if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
754
+ return "stable";
755
+ }
756
+ function diffSinceLastRun(history2, scenarios) {
757
+ let hasPreviousRun = false;
758
+ const delta = { newlyFailing: [], newlyPassing: [], added: [] };
759
+ for (const scenario of scenarios) {
760
+ const entries = history2[scenario.id];
761
+ if (!entries || entries.length === 0) continue;
762
+ const last = entries[entries.length - 1];
763
+ const prev = entries[entries.length - 2];
764
+ if (!prev) {
765
+ delta.added.push(scenario);
766
+ continue;
767
+ }
768
+ hasPreviousRun = true;
769
+ if (last.status === "failed" && prev.status !== "failed") delta.newlyFailing.push(scenario);
770
+ else if (last.status === "passed" && prev.status === "failed") delta.newlyPassing.push(scenario);
771
+ }
772
+ return hasPreviousRun ? delta : void 0;
773
+ }
774
+
775
+ // src/components/ScenarioRunHistory.tsx
776
+ var import_jsx_runtime20 = require("react/jsx-runtime");
777
+ var MAX_DOTS = 10;
778
+ var DOT_COLOR = {
779
+ passed: "bg-pass",
780
+ failed: "bg-fail",
781
+ skipped: "bg-skip",
782
+ pending: "bg-pend"
783
+ };
784
+ function dotTitle(e) {
785
+ const parts = [new Date(e.timestamp).toLocaleString(), e.status];
786
+ if (e.commitSha) parts.push(e.commitSha.slice(0, 8));
787
+ return parts.join(" \xB7 ");
788
+ }
789
+ function ScenarioRunHistory({ scenarioId }) {
790
+ const history2 = useScenarioHistory();
791
+ const entries = history2?.[scenarioId];
792
+ if (!entries || entries.length < 2) return null;
793
+ const recent = entries.slice(-MAX_DOTS);
794
+ const flaky = flakinessOf(recent) === "flaky";
795
+ const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
796
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
797
+ flaky ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
798
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
799
+ "span",
800
+ {
801
+ role: "img",
802
+ "aria-label": `Run history: ${summary}`,
803
+ title: summary,
804
+ className: "flex shrink-0 items-center gap-[3px]",
805
+ children: recent.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
806
+ "span",
807
+ {
808
+ "aria-hidden": true,
809
+ title: dotTitle(e),
810
+ className: cn(
811
+ "inline-block size-1.5 rounded-full",
812
+ DOT_COLOR[e.status],
813
+ // The newest run (rightmost dot) is the one the card's status
814
+ // badge already describes; ring it so the timeline reads as
815
+ // "…leading up to now".
816
+ i === recent.length - 1 && "ring-1 ring-border ring-offset-1 ring-offset-card"
817
+ )
818
+ },
819
+ `${e.timestamp}-${i}`
820
+ ))
821
+ }
822
+ )
823
+ ] });
824
+ }
825
+
826
+ // src/interactive/collapse-context.ts
827
+ var import_react6 = require("react");
828
+ var CollapseContext = (0, import_react6.createContext)(null);
682
829
  var CollapseProvider = CollapseContext.Provider;
683
830
  function useCollapse() {
684
- return (0, import_react5.useContext)(CollapseContext);
831
+ return (0, import_react6.useContext)(CollapseContext);
685
832
  }
686
833
  var STORAGE_KEY = "es-collapsed-ids";
687
834
  function readPersisted() {
@@ -701,9 +848,9 @@ function persist(ids) {
701
848
  }
702
849
  }
703
850
  function useCollapseState() {
704
- const [collapsed, setCollapsed] = (0, import_react5.useState)(readPersisted);
705
- const isCollapsed = (0, import_react5.useCallback)((id) => collapsed.has(id), [collapsed]);
706
- const toggle = (0, import_react5.useCallback)((id) => {
851
+ const [collapsed, setCollapsed] = (0, import_react6.useState)(readPersisted);
852
+ const isCollapsed = (0, import_react6.useCallback)((id) => collapsed.has(id), [collapsed]);
853
+ const toggle = (0, import_react6.useCallback)((id) => {
707
854
  setCollapsed((prev) => {
708
855
  const next = new Set(prev);
709
856
  if (next.has(id)) next.delete(id);
@@ -712,14 +859,14 @@ function useCollapseState() {
712
859
  return next;
713
860
  });
714
861
  }, []);
715
- const collapseAll = (0, import_react5.useCallback)((ids) => {
862
+ const collapseAll = (0, import_react6.useCallback)((ids) => {
716
863
  setCollapsed(() => {
717
864
  const next = new Set(ids);
718
865
  persist(next);
719
866
  return next;
720
867
  });
721
868
  }, []);
722
- const expandAll = (0, import_react5.useCallback)(() => {
869
+ const expandAll = (0, import_react6.useCallback)(() => {
723
870
  setCollapsed(() => {
724
871
  const next = /* @__PURE__ */ new Set();
725
872
  persist(next);
@@ -730,11 +877,11 @@ function useCollapseState() {
730
877
  }
731
878
 
732
879
  // src/interactive/scenario-actions.tsx
733
- var import_react6 = require("react");
734
- var ScenarioActionsContext = (0, import_react6.createContext)(null);
880
+ var import_react7 = require("react");
881
+ var ScenarioActionsContext = (0, import_react7.createContext)(null);
735
882
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
736
883
  function useScenarioActions() {
737
- return (0, import_react6.useContext)(ScenarioActionsContext);
884
+ return (0, import_react7.useContext)(ScenarioActionsContext);
738
885
  }
739
886
  function scenarioToMarkdown(scenario) {
740
887
  const lines = [`## ${scenario.title} _(${scenario.status})_`, ""];
@@ -772,9 +919,9 @@ function scenarioPermalink(scenario) {
772
919
 
773
920
  // src/components/ui/card.tsx
774
921
  var React2 = require("react");
775
- var import_jsx_runtime20 = require("react/jsx-runtime");
922
+ var import_jsx_runtime21 = require("react/jsx-runtime");
776
923
  function Card({ className, ...props }) {
777
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
924
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
778
925
  "div",
779
926
  {
780
927
  "data-slot": "card",
@@ -787,7 +934,7 @@ function Card({ className, ...props }) {
787
934
  );
788
935
  }
789
936
  function CardHeader({ className, ...props }) {
790
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
937
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
791
938
  "div",
792
939
  {
793
940
  "data-slot": "card-header",
@@ -800,7 +947,7 @@ function CardHeader({ className, ...props }) {
800
947
  );
801
948
  }
802
949
  function CardContent({ className, ...props }) {
803
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
950
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
804
951
  "div",
805
952
  {
806
953
  "data-slot": "card-content",
@@ -811,7 +958,7 @@ function CardContent({ className, ...props }) {
811
958
  }
812
959
 
813
960
  // src/components/ReportScenario.tsx
814
- var import_jsx_runtime21 = require("react/jsx-runtime");
961
+ var import_jsx_runtime22 = require("react/jsx-runtime");
815
962
  var STATUS_LABEL = {
816
963
  passed: "Passed",
817
964
  failed: "Failed",
@@ -839,12 +986,12 @@ var STATUS_BADGE = {
839
986
  function ReportScenario({ scenario, hideTitle = false }) {
840
987
  const titleId = `${scenario.id}-title`;
841
988
  const bodyId = `${scenario.id}-body`;
842
- const label = STATUS_LABEL[scenario.status];
989
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
843
990
  const collapse = useCollapse();
844
991
  const collapsible = collapse !== null && !hideTitle;
845
992
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
846
993
  const actions = useScenarioActions();
847
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
994
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
848
995
  Card,
849
996
  {
850
997
  id: scenario.id,
@@ -852,10 +999,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
852
999
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
853
1000
  className: "gap-0 py-0",
854
1001
  children: [
855
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardHeader, { className: "px-4 py-3", children: [
856
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
857
- hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
858
- collapsible ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1002
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(CardHeader, { className: "px-4 py-3", children: [
1003
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1004
+ hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1005
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
859
1006
  "button",
860
1007
  {
861
1008
  type: "button",
@@ -864,17 +1011,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
864
1011
  "aria-controls": bodyId,
865
1012
  "aria-label": `Toggle ${scenario.title}`,
866
1013
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
867
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1014
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
868
1015
  }
869
1016
  ) : null,
870
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
871
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: scenario.title })
1017
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
1018
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: scenario.title })
872
1019
  ] }),
873
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
874
- scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration3.formatDuration)(scenario.durationMs) }) : null,
875
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
876
- actions ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-0.5", children: [
877
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1020
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
1021
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
1022
+ scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
1023
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
1024
+ actions ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-0.5", children: [
1025
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
878
1026
  "button",
879
1027
  {
880
1028
  type: "button",
@@ -885,7 +1033,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
885
1033
  children: "link"
886
1034
  }
887
1035
  ),
888
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1036
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
889
1037
  "button",
890
1038
  {
891
1039
  type: "button",
@@ -896,7 +1044,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
896
1044
  children: "md"
897
1045
  }
898
1046
  ),
899
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1047
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
900
1048
  "button",
901
1049
  {
902
1050
  type: "button",
@@ -910,13 +1058,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
910
1058
  ] }) : null
911
1059
  ] })
912
1060
  ] }),
913
- scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
914
- scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
915
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1061
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1062
+ scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1063
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
916
1064
  ] }) : null
917
1065
  ] }),
918
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
919
- scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1066
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1067
+ scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
920
1068
  "pre",
921
1069
  {
922
1070
  role: "alert",
@@ -924,10 +1072,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
924
1072
  children: scenario.errorMessage
925
1073
  }
926
1074
  ) : null,
927
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportSteps, { scenario }),
928
- scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
929
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportAttachments, { attachments: scenario.attachments }),
930
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportTrace, { spans: scenario.otelSpans })
1075
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportSteps, { scenario }),
1076
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1077
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportAttachments, { attachments: scenario.attachments }),
1078
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportTrace, { spans: scenario.otelSpans })
931
1079
  ] })
932
1080
  ]
933
1081
  }
@@ -935,13 +1083,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
935
1083
  }
936
1084
 
937
1085
  // src/components/ReportScenarioList.tsx
938
- var import_jsx_runtime22 = require("react/jsx-runtime");
1086
+ var import_jsx_runtime23 = require("react/jsx-runtime");
939
1087
  function ReportScenarioList({ feature }) {
940
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_jsx_runtime22.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportScenario, { scenario }, scenario.id)) });
1088
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_jsx_runtime23.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ReportScenario, { scenario }, scenario.id)) });
941
1089
  }
942
1090
 
943
1091
  // src/components/ReportFeature.tsx
944
- var import_jsx_runtime23 = require("react/jsx-runtime");
1092
+ var import_jsx_runtime24 = require("react/jsx-runtime");
945
1093
  function ReportFeature({ feature }) {
946
1094
  const titleId = `${feature.id}-title`;
947
1095
  const bodyId = `${feature.id}-body`;
@@ -949,7 +1097,7 @@ function ReportFeature({ feature }) {
949
1097
  const skipped = s.skipped + s.pending;
950
1098
  const collapse = useCollapse();
951
1099
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
952
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1100
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
953
1101
  "section",
954
1102
  {
955
1103
  id: feature.id,
@@ -957,9 +1105,9 @@ function ReportFeature({ feature }) {
957
1105
  "aria-labelledby": titleId,
958
1106
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
959
1107
  children: [
960
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
961
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
962
- collapse ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
1108
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
1109
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
1110
+ collapse ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
963
1111
  "button",
964
1112
  {
965
1113
  type: "button",
@@ -968,30 +1116,30 @@ function ReportFeature({ feature }) {
968
1116
  "aria-controls": bodyId,
969
1117
  "aria-label": `Toggle ${feature.title}`,
970
1118
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
971
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1119
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
972
1120
  }
973
1121
  ) : null,
974
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "min-w-0", children: [
975
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
976
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
1122
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "min-w-0", children: [
1123
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
1124
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
977
1125
  ] })
978
1126
  ] }),
979
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1127
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
980
1128
  "div",
981
1129
  {
982
1130
  role: "img",
983
1131
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
984
1132
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
985
1133
  children: [
986
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-pass", children: [
1134
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-pass", children: [
987
1135
  "\u2713",
988
1136
  s.passed
989
1137
  ] }),
990
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-fail", children: [
1138
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-fail", children: [
991
1139
  "\u2717",
992
1140
  s.failed
993
1141
  ] }),
994
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-skip", children: [
1142
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-skip", children: [
995
1143
  "\u25CB",
996
1144
  skipped
997
1145
  ] })
@@ -999,24 +1147,24 @@ function ReportFeature({ feature }) {
999
1147
  }
1000
1148
  )
1001
1149
  ] }),
1002
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(ReportScenarioList, { feature }) })
1150
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportScenarioList, { feature }) })
1003
1151
  ]
1004
1152
  }
1005
1153
  );
1006
1154
  }
1007
1155
 
1008
1156
  // src/components/ReportFeatureList.tsx
1009
- var import_jsx_runtime24 = require("react/jsx-runtime");
1157
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1010
1158
  function ReportFeatureList() {
1011
1159
  const report = useReport();
1012
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_jsx_runtime24.Fragment, { children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReportFeature, { feature }, feature.id)) });
1160
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_jsx_runtime25.Fragment, { children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReportFeature, { feature }, feature.id)) });
1013
1161
  }
1014
1162
 
1015
1163
  // src/components/ui/empty.tsx
1016
1164
  var import_class_variance_authority2 = require("class-variance-authority");
1017
- var import_jsx_runtime25 = require("react/jsx-runtime");
1165
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1018
1166
  function Empty({ className, ...props }) {
1019
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1167
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1020
1168
  "div",
1021
1169
  {
1022
1170
  "data-slot": "empty",
@@ -1029,7 +1177,7 @@ function Empty({ className, ...props }) {
1029
1177
  );
1030
1178
  }
1031
1179
  function EmptyHeader({ className, ...props }) {
1032
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1180
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1033
1181
  "div",
1034
1182
  {
1035
1183
  "data-slot": "empty-header",
@@ -1056,7 +1204,7 @@ var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
1056
1204
  }
1057
1205
  );
1058
1206
  function EmptyTitle({ className, ...props }) {
1059
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1207
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1060
1208
  "div",
1061
1209
  {
1062
1210
  "data-slot": "empty-title",
@@ -1067,16 +1215,16 @@ function EmptyTitle({ className, ...props }) {
1067
1215
  }
1068
1216
 
1069
1217
  // src/components/ReportEmpty.tsx
1070
- var import_jsx_runtime26 = require("react/jsx-runtime");
1218
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1071
1219
  function ReportEmpty({ message }) {
1072
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1220
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1073
1221
  }
1074
1222
 
1075
1223
  // src/components/ReportSummary.tsx
1076
- var import_jsx_runtime27 = require("react/jsx-runtime");
1224
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1077
1225
  function ReportSummary({ className }) {
1078
1226
  const report = useReport();
1079
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1227
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1080
1228
  ReportSummaryView,
1081
1229
  {
1082
1230
  summary: report.summary,
@@ -1086,32 +1234,32 @@ function ReportSummary({ className }) {
1086
1234
  );
1087
1235
  }
1088
1236
  function ReportSummaryView({ summary, className, ariaLabel }) {
1089
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1090
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { children: [
1091
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("strong", { className: "font-semibold text-foreground", children: summary.total }),
1237
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1238
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { children: [
1239
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("strong", { className: "font-semibold text-foreground", children: summary.total }),
1092
1240
  " scenario",
1093
1241
  summary.total === 1 ? "" : "s"
1094
1242
  ] }),
1095
1243
  " \xB7 ",
1096
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "passed", className: "text-pass", children: [
1244
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "passed", className: "text-pass", children: [
1097
1245
  summary.passed,
1098
1246
  " passed"
1099
1247
  ] }),
1100
1248
  " \xB7 ",
1101
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "failed", className: "text-fail", children: [
1249
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "failed", className: "text-fail", children: [
1102
1250
  summary.failed,
1103
1251
  " failed"
1104
1252
  ] }),
1105
- summary.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1253
+ summary.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1106
1254
  " \xB7 ",
1107
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "skipped", className: "text-skip", children: [
1255
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "skipped", className: "text-skip", children: [
1108
1256
  summary.skipped,
1109
1257
  " skipped"
1110
1258
  ] })
1111
1259
  ] }) : null,
1112
- summary.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1260
+ summary.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1113
1261
  " \xB7 ",
1114
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "pending", className: "text-pend", children: [
1262
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "pending", className: "text-pend", children: [
1115
1263
  summary.pending,
1116
1264
  " pending"
1117
1265
  ] })
@@ -1119,69 +1267,135 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1119
1267
  ] });
1120
1268
  }
1121
1269
 
1270
+ // src/lib/provenance.ts
1271
+ function reportLastRunMs(report) {
1272
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1273
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1274
+ return void 0;
1275
+ }
1276
+ function formatRelativeAge(thenMs, nowMs) {
1277
+ const deltaMs = Math.max(0, nowMs - thenMs);
1278
+ const minutes = Math.floor(deltaMs / 6e4);
1279
+ if (minutes < 1) return "just now";
1280
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1281
+ const hours = Math.floor(minutes / 60);
1282
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1283
+ const days = Math.floor(hours / 24);
1284
+ return `${days} day${days === 1 ? "" : "s"} ago`;
1285
+ }
1286
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1287
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1288
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
1289
+ }
1290
+ var CI_DISPLAY_NAMES = {
1291
+ github: "GitHub Actions",
1292
+ gitlab: "GitLab CI",
1293
+ circleci: "CircleCI",
1294
+ jenkins: "Jenkins",
1295
+ azure: "Azure DevOps",
1296
+ buildkite: "Buildkite",
1297
+ travis: "Travis CI",
1298
+ ci: "CI"
1299
+ };
1300
+ function ciDisplayName(name) {
1301
+ return CI_DISPLAY_NAMES[name] ?? name;
1302
+ }
1303
+ function commitUrl(ci, sha) {
1304
+ const commitSha = ci?.commitSha ?? sha;
1305
+ if (!ci?.url || !commitSha) return void 0;
1306
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1307
+ if (github) return `${github[1]}/commit/${commitSha}`;
1308
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1309
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1310
+ return void 0;
1311
+ }
1312
+ function prUrl(ci) {
1313
+ if (!ci?.url || !ci.prNumber) return void 0;
1314
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1315
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1316
+ return void 0;
1317
+ }
1318
+
1122
1319
  // src/components/ReportMeta.tsx
1123
- var import_duration4 = require("executable-stories-core/utils/duration");
1124
- var import_jsx_runtime28 = require("react/jsx-runtime");
1320
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1125
1321
  function ReportMeta() {
1126
1322
  const report = useReport();
1127
1323
  const items = [];
1324
+ const linkClass = "text-primary underline underline-offset-2";
1128
1325
  if (report.startedAtMs) {
1129
1326
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1130
1327
  }
1131
1328
  if (report.durationMs) {
1132
- items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1329
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1133
1330
  }
1134
1331
  if (report.packageVersion) {
1135
1332
  items.push({ label: "Version", value: report.packageVersion });
1136
1333
  }
1137
- if (report.gitSha) {
1138
- items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1334
+ if (report.ci?.branch) {
1335
+ items.push({ label: "Branch", value: report.ci.branch });
1336
+ }
1337
+ const sha = report.ci?.commitSha ?? report.gitSha;
1338
+ if (sha) {
1339
+ const url = commitUrl(report.ci, report.gitSha);
1340
+ const short = sha.slice(0, 8);
1341
+ items.push({
1342
+ label: "Commit",
1343
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1344
+ });
1139
1345
  }
1140
1346
  if (report.ci) {
1141
1347
  items.push({
1142
1348
  label: "CI",
1143
- value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1144
- report.ci.name,
1349
+ value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1350
+ ciDisplayName(report.ci.name),
1145
1351
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1146
- ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1352
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1353
+ });
1354
+ }
1355
+ if (report.ci?.prNumber) {
1356
+ const url = prUrl(report.ci);
1357
+ const label = `#${report.ci.prNumber}`;
1358
+ items.push({
1359
+ label: "PR",
1360
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1147
1361
  });
1148
1362
  }
1149
1363
  if (items.length === 0) return null;
1150
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex gap-1.5", children: [
1151
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dt", { className: "font-medium", children: item.label }),
1152
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1364
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex gap-1.5", children: [
1365
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dt", { className: "font-medium", children: item.label }),
1366
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1153
1367
  ] }, item.label)) });
1154
1368
  }
1155
1369
 
1156
1370
  // src/components/ReportSchemaError.tsx
1157
- var import_jsx_runtime29 = require("react/jsx-runtime");
1371
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1158
1372
  function ReportSchemaError({ error }) {
1159
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1160
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("strong", { children: "Report could not be displayed." }) }),
1161
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: error.message }),
1162
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { children: [
1373
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1374
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("strong", { children: "Report could not be displayed." }) }),
1375
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { children: error.message }),
1376
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("p", { children: [
1163
1377
  "The report bundle is newer than this version of ",
1164
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("code", { children: "executable-stories-react" }),
1378
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("code", { children: "executable-stories-react" }),
1165
1379
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1166
1380
  ] }) : null,
1167
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("details", { children: [
1168
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("summary", { children: [
1381
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("details", { children: [
1382
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("summary", { children: [
1169
1383
  error.issues.length,
1170
1384
  " validation issue",
1171
1385
  error.issues.length === 1 ? "" : "s"
1172
1386
  ] }),
1173
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1387
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1174
1388
  ] }) : null
1175
1389
  ] });
1176
1390
  }
1177
1391
 
1178
1392
  // src/components/ReportShell.tsx
1179
- var import_jsx_runtime30 = require("react/jsx-runtime");
1393
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1180
1394
  function ReportTitleBlock({ title }) {
1181
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1182
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h1", { children: title ?? "Story Report" }),
1183
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportSummary, {}),
1184
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportMeta, {})
1395
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1396
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h1", { children: title ?? "Story Report" }),
1397
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportSummary, {}),
1398
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportMeta, {})
1185
1399
  ] });
1186
1400
  }
1187
1401
  function ReportErrorShell({
@@ -1190,13 +1404,13 @@ function ReportErrorShell({
1190
1404
  title,
1191
1405
  dataTheme
1192
1406
  }) {
1193
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportSchemaError, { error }) });
1407
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportSchemaError, { error }) });
1194
1408
  }
1195
1409
 
1196
1410
  // src/interactive/ReportSearch.tsx
1197
- var import_react7 = require("react");
1198
- var import_jsx_runtime31 = require("react/jsx-runtime");
1199
- var ReportSearch = (0, import_react7.forwardRef)(
1411
+ var import_react8 = require("react");
1412
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1413
+ var ReportSearch = (0, import_react8.forwardRef)(
1200
1414
  function ReportSearch2(props, ref) {
1201
1415
  const {
1202
1416
  value,
@@ -1206,7 +1420,7 @@ var ReportSearch = (0, import_react7.forwardRef)(
1206
1420
  placeholder = "Search scenarios, tags, or step text\u2026",
1207
1421
  className
1208
1422
  } = props;
1209
- const inputId = (0, import_react7.useId)();
1423
+ const inputId = (0, import_react8.useId)();
1210
1424
  const showCounts = typeof matchedCount === "number" && typeof totalCount === "number";
1211
1425
  function handleChange(e) {
1212
1426
  onChange(e.target.value);
@@ -1217,9 +1431,9 @@ var ReportSearch = (0, import_react7.forwardRef)(
1217
1431
  e.preventDefault();
1218
1432
  }
1219
1433
  }
1220
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
1221
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
1222
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1434
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
1435
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
1436
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1223
1437
  "input",
1224
1438
  {
1225
1439
  ref,
@@ -1234,13 +1448,13 @@ var ReportSearch = (0, import_react7.forwardRef)(
1234
1448
  "aria-keyshortcuts": "/"
1235
1449
  }
1236
1450
  ),
1237
- showCounts ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
1451
+ showCounts ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
1238
1452
  ] });
1239
1453
  }
1240
1454
  );
1241
1455
 
1242
1456
  // src/interactive/ReportFailureBanner.tsx
1243
- var import_react8 = require("react");
1457
+ var import_react9 = require("react");
1244
1458
 
1245
1459
  // src/lib/scroll.ts
1246
1460
  function prefersReducedMotion() {
@@ -1263,15 +1477,15 @@ function scrollToScenarioId(scenarioId, options = {}) {
1263
1477
  }
1264
1478
 
1265
1479
  // src/interactive/ReportFailureBanner.tsx
1266
- var import_jsx_runtime32 = require("react/jsx-runtime");
1480
+ var import_jsx_runtime33 = require("react/jsx-runtime");
1267
1481
  function ReportFailureBanner({ failures }) {
1268
1482
  const first = failures[0];
1269
- const jumpToFirst = (0, import_react8.useCallback)(() => {
1483
+ const jumpToFirst = (0, import_react9.useCallback)(() => {
1270
1484
  if (!first) return;
1271
1485
  scrollToScenarioId(first.scenarioId);
1272
1486
  }, [first]);
1273
1487
  if (failures.length === 0) return null;
1274
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
1488
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1275
1489
  "div",
1276
1490
  {
1277
1491
  className: "es-failure-banner",
@@ -1279,13 +1493,13 @@ function ReportFailureBanner({ failures }) {
1279
1493
  "aria-live": "polite",
1280
1494
  "aria-label": "Failure summary",
1281
1495
  children: [
1282
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "es-failure-banner-text", children: [
1283
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("strong", { children: failures.length }),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("span", { className: "es-failure-banner-text", children: [
1497
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("strong", { children: failures.length }),
1284
1498
  " ",
1285
1499
  "failure",
1286
1500
  failures.length === 1 ? "" : "s"
1287
1501
  ] }),
1288
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
1502
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
1289
1503
  "button",
1290
1504
  {
1291
1505
  type: "button",
@@ -1300,9 +1514,101 @@ function ReportFailureBanner({ failures }) {
1300
1514
  );
1301
1515
  }
1302
1516
 
1517
+ // src/interactive/ReportFreshness.tsx
1518
+ var import_react10 = require("react");
1519
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1520
+ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1521
+ const [now, setNow] = (0, import_react10.useState)(nowMs);
1522
+ (0, import_react10.useEffect)(() => {
1523
+ if (nowMs === void 0) setNow(Date.now());
1524
+ }, [nowMs]);
1525
+ if (lastRunMs === void 0 || now === void 0) return null;
1526
+ const age = formatRelativeAge(lastRunMs, now);
1527
+ const runLink = ciUrl ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1528
+ "a",
1529
+ {
1530
+ href: ciUrl,
1531
+ target: "_blank",
1532
+ rel: "noreferrer noopener",
1533
+ className: "underline underline-offset-2",
1534
+ children: "View CI run"
1535
+ }
1536
+ ) : null;
1537
+ if (isReportStale(lastRunMs, staleAfterDays, now)) {
1538
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
1539
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("span", { children: [
1540
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("strong", { children: [
1541
+ "Last verified ",
1542
+ age,
1543
+ "."
1544
+ ] }),
1545
+ " This report may no longer match the current behavior \u2014 look for a newer run."
1546
+ ] }),
1547
+ runLink
1548
+ ] });
1549
+ }
1550
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
1551
+ "Verified ",
1552
+ age,
1553
+ runLink ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
1554
+ " \xB7 ",
1555
+ runLink
1556
+ ] }) : null
1557
+ ] });
1558
+ }
1559
+
1560
+ // src/interactive/ReportLastRunDelta.tsx
1561
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1562
+ var MAX_LINKS = 5;
1563
+ function ScenarioLinks({ scenarios, tone }) {
1564
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
1565
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { children: [
1566
+ i > 0 ? ", " : "",
1567
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
1568
+ ] }, s.id)),
1569
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { children: [
1570
+ " and ",
1571
+ scenarios.length - MAX_LINKS,
1572
+ " more"
1573
+ ] }) : null
1574
+ ] });
1575
+ }
1576
+ function ReportLastRunDelta({
1577
+ history: history2,
1578
+ report
1579
+ }) {
1580
+ if (!history2) return null;
1581
+ const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
1582
+ const delta = diffSinceLastRun(history2, scenarios);
1583
+ if (!delta) return null;
1584
+ const { newlyFailing, newlyPassing, added } = delta;
1585
+ if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
1586
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
1587
+ }
1588
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
1589
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "font-medium text-foreground", children: "Since last run:" }),
1590
+ newlyFailing.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "ml-2 text-fail", children: [
1591
+ newlyFailing.length,
1592
+ " newly failing (",
1593
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
1594
+ ")"
1595
+ ] }) : null,
1596
+ newlyPassing.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "ml-2 text-pass", children: [
1597
+ newlyPassing.length,
1598
+ " fixed (",
1599
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
1600
+ ")"
1601
+ ] }) : null,
1602
+ added.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("span", { className: "ml-2", children: [
1603
+ added.length,
1604
+ " new"
1605
+ ] }) : null
1606
+ ] });
1607
+ }
1608
+
1303
1609
  // src/interactive/ReportShortcutsHelp.tsx
1304
- var import_react9 = require("react");
1305
- var import_jsx_runtime33 = require("react/jsx-runtime");
1610
+ var import_react11 = require("react");
1611
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1306
1612
  var SHORTCUTS = [
1307
1613
  { keys: "/", description: "Focus search" },
1308
1614
  { keys: "f", description: "Jump to next failure" },
@@ -1311,14 +1617,14 @@ var SHORTCUTS = [
1311
1617
  { keys: "?", description: "Toggle this help" }
1312
1618
  ];
1313
1619
  function ReportShortcutsHelp({ open, onClose }) {
1314
- const dialogRef = (0, import_react9.useRef)(null);
1315
- (0, import_react9.useEffect)(() => {
1620
+ const dialogRef = (0, import_react11.useRef)(null);
1621
+ (0, import_react11.useEffect)(() => {
1316
1622
  const dialog = dialogRef.current;
1317
1623
  if (!dialog) return;
1318
1624
  if (open && !dialog.open) dialog.showModal();
1319
1625
  else if (!open && dialog.open) dialog.close();
1320
1626
  }, [open]);
1321
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
1627
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
1322
1628
  "dialog",
1323
1629
  {
1324
1630
  ref: dialogRef,
@@ -1326,19 +1632,19 @@ function ReportShortcutsHelp({ open, onClose }) {
1326
1632
  "aria-label": "Keyboard shortcuts",
1327
1633
  onClose,
1328
1634
  children: [
1329
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("h2", { children: "Keyboard shortcuts" }),
1330
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { children: [
1331
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dt", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("kbd", { children: s.keys }) }),
1332
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("dd", { children: s.description })
1635
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("h2", { children: "Keyboard shortcuts" }),
1636
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { children: [
1637
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("dt", { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("kbd", { children: s.keys }) }),
1638
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("dd", { children: s.description })
1333
1639
  ] }, s.keys)) }),
1334
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("form", { method: "dialog", children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
1640
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("form", { method: "dialog", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
1335
1641
  ]
1336
1642
  }
1337
1643
  );
1338
1644
  }
1339
1645
 
1340
1646
  // src/interactive/use-keyboard-shortcuts.ts
1341
- var import_react10 = require("react");
1647
+ var import_react12 = require("react");
1342
1648
  function isEditableTarget(target) {
1343
1649
  if (!(target instanceof HTMLElement)) return false;
1344
1650
  const tag = target.tagName;
@@ -1347,9 +1653,9 @@ function isEditableTarget(target) {
1347
1653
  return false;
1348
1654
  }
1349
1655
  function useKeyboardShortcuts(handlers) {
1350
- const ref = (0, import_react10.useRef)(handlers);
1656
+ const ref = (0, import_react12.useRef)(handlers);
1351
1657
  ref.current = handlers;
1352
- (0, import_react10.useEffect)(() => {
1658
+ (0, import_react12.useEffect)(() => {
1353
1659
  function onKeyDown(e) {
1354
1660
  const h = ref.current;
1355
1661
  if (e.key === "Escape") {
@@ -1393,9 +1699,9 @@ function useKeyboardShortcuts(handlers) {
1393
1699
  }
1394
1700
 
1395
1701
  // src/interactive/use-deep-link-scroll.ts
1396
- var import_react11 = require("react");
1702
+ var import_react13 = require("react");
1397
1703
  function useDeepLinkScroll() {
1398
- (0, import_react11.useEffect)(() => {
1704
+ (0, import_react13.useEffect)(() => {
1399
1705
  function scrollToHash() {
1400
1706
  const hash = window.location.hash.replace(/^#/, "");
1401
1707
  if (!hash) return;
@@ -1504,7 +1810,7 @@ function listFailures(report) {
1504
1810
  }
1505
1811
 
1506
1812
  // src/interactive/ReportFilters.tsx
1507
- var import_jsx_runtime34 = require("react/jsx-runtime");
1813
+ var import_jsx_runtime37 = require("react/jsx-runtime");
1508
1814
  function ReportFilters({
1509
1815
  statuses,
1510
1816
  status,
@@ -1514,10 +1820,10 @@ function ReportFilters({
1514
1820
  onToggleTag
1515
1821
  }) {
1516
1822
  if (statuses.length === 0 && tags.length === 0) return null;
1517
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col gap-2", children: [
1518
- statuses.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
1823
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "flex flex-col gap-2", children: [
1824
+ statuses.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
1519
1825
  const active = status === opt.key;
1520
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(
1826
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
1521
1827
  "button",
1522
1828
  {
1523
1829
  type: "button",
@@ -1529,22 +1835,22 @@ function ReportFilters({
1529
1835
  ),
1530
1836
  children: [
1531
1837
  opt.label,
1532
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
1838
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
1533
1839
  ]
1534
1840
  },
1535
1841
  opt.key
1536
1842
  );
1537
1843
  }) }) : null,
1538
- tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap gap-1.5", children: tags.map((tag) => {
1844
+ tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap gap-1.5", children: tags.map((tag) => {
1539
1845
  const active = activeTags.includes(tag);
1540
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
1846
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1541
1847
  "button",
1542
1848
  {
1543
1849
  type: "button",
1544
1850
  "aria-pressed": active,
1545
1851
  onClick: () => onToggleTag(tag),
1546
1852
  className: "cursor-pointer",
1547
- children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Badge, { variant: active ? "default" : "tag", children: tag })
1853
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Badge, { variant: active ? "default" : "tag", children: tag })
1548
1854
  }
1549
1855
  ) }, tag);
1550
1856
  }) }) : null
@@ -1552,8 +1858,8 @@ function ReportFilters({
1552
1858
  }
1553
1859
 
1554
1860
  // src/interactive/ReportToc.tsx
1555
- var import_react12 = require("react");
1556
- var import_jsx_runtime35 = require("react/jsx-runtime");
1861
+ var import_react14 = require("react");
1862
+ var import_jsx_runtime38 = require("react/jsx-runtime");
1557
1863
  var DOT = {
1558
1864
  passed: "text-pass",
1559
1865
  failed: "text-fail",
@@ -1562,12 +1868,12 @@ var DOT = {
1562
1868
  };
1563
1869
  function ReportToc() {
1564
1870
  const report = useReport();
1565
- const [activeId, setActiveId] = (0, import_react12.useState)(null);
1566
- const scenarioIds = (0, import_react12.useMemo)(
1871
+ const [activeId, setActiveId] = (0, import_react14.useState)(null);
1872
+ const scenarioIds = (0, import_react14.useMemo)(
1567
1873
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
1568
1874
  [report]
1569
1875
  );
1570
- (0, import_react12.useEffect)(() => {
1876
+ (0, import_react14.useEffect)(() => {
1571
1877
  if (typeof IntersectionObserver === "undefined") return;
1572
1878
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
1573
1879
  if (els.length === 0) return;
@@ -1582,16 +1888,16 @@ function ReportToc() {
1582
1888
  return () => observer.disconnect();
1583
1889
  }, [scenarioIds]);
1584
1890
  if (report.features.length === 0) return null;
1585
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
1891
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1586
1892
  "nav",
1587
1893
  {
1588
1894
  "aria-label": "Table of contents",
1589
1895
  className: "sticky top-4 hidden h-fit w-56 shrink-0 self-start text-xs lg:block",
1590
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "flex flex-col gap-3", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { children: [
1591
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mb-1 truncate font-semibold text-foreground", children: feature.title }),
1592
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "flex flex-col", children: feature.scenarios.map((scenario) => {
1896
+ children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { className: "flex flex-col gap-3", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { children: [
1897
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("p", { className: "mb-1 truncate font-semibold text-foreground", children: feature.title }),
1898
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { className: "flex flex-col", children: feature.scenarios.map((scenario) => {
1593
1899
  const active = activeId === scenario.id;
1594
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
1900
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
1595
1901
  "a",
1596
1902
  {
1597
1903
  href: `#${scenario.id}`,
@@ -1605,8 +1911,8 @@ function ReportToc() {
1605
1911
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
1606
1912
  ),
1607
1913
  children: [
1608
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { "aria-hidden": true, className: cn("shrink-0", DOT[scenario.status]), children: "\u2022" }),
1609
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "truncate", children: scenario.title })
1914
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { "aria-hidden": true, className: cn("shrink-0", DOT[scenario.status]), children: "\u2022" }),
1915
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "truncate", children: scenario.title })
1610
1916
  ]
1611
1917
  }
1612
1918
  ) }, scenario.id);
@@ -1617,7 +1923,7 @@ function ReportToc() {
1617
1923
  }
1618
1924
 
1619
1925
  // src/interactive/use-theme.ts
1620
- var import_react13 = require("react");
1926
+ var import_react15 = require("react");
1621
1927
  var KEY = "es-theme";
1622
1928
  function applyTheme(theme) {
1623
1929
  if (typeof document !== "undefined") {
@@ -1638,13 +1944,13 @@ function initialTheme() {
1638
1944
  return "light";
1639
1945
  }
1640
1946
  function useTheme() {
1641
- const [theme, setTheme] = (0, import_react13.useState)("light");
1642
- (0, import_react13.useEffect)(() => {
1947
+ const [theme, setTheme] = (0, import_react15.useState)("light");
1948
+ (0, import_react15.useEffect)(() => {
1643
1949
  const t = initialTheme();
1644
1950
  setTheme(t);
1645
1951
  applyTheme(t);
1646
1952
  }, []);
1647
- const toggle = (0, import_react13.useCallback)(() => {
1953
+ const toggle = (0, import_react15.useCallback)(() => {
1648
1954
  setTheme((prev) => {
1649
1955
  const next = prev === "dark" ? "light" : "dark";
1650
1956
  applyTheme(next);
@@ -1659,14 +1965,14 @@ function useTheme() {
1659
1965
  }
1660
1966
 
1661
1967
  // src/interactive/ReportInteractive.tsx
1662
- var import_jsx_runtime36 = require("react/jsx-runtime");
1968
+ var import_jsx_runtime39 = require("react/jsx-runtime");
1663
1969
  function ReportInteractive(props) {
1664
1970
  const { report, className, title, dataTheme } = props;
1665
1971
  const result = unwrapReport(report);
1666
1972
  if (!result.ok) {
1667
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1973
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1668
1974
  }
1669
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportInteractiveView, { ...props, report: result.data });
1975
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportInteractiveView, { ...props, report: result.data });
1670
1976
  }
1671
1977
  function ReportInteractiveView({
1672
1978
  report,
@@ -1675,23 +1981,25 @@ function ReportInteractiveView({
1675
1981
  className,
1676
1982
  title,
1677
1983
  dataTheme,
1678
- hideHeader = false
1984
+ hideHeader = false,
1985
+ staleAfterDays = 7,
1986
+ scenarioHistory
1679
1987
  }) {
1680
- const [query, setQuery] = (0, import_react14.useState)("");
1681
- const [statusFilter, setStatusFilter] = (0, import_react14.useState)("all");
1682
- const [activeTags, setActiveTags] = (0, import_react14.useState)([]);
1683
- const [detail, setDetail] = (0, import_react14.useState)("full");
1684
- const [helpOpen, setHelpOpen] = (0, import_react14.useState)(false);
1988
+ const [query, setQuery] = (0, import_react16.useState)("");
1989
+ const [statusFilter, setStatusFilter] = (0, import_react16.useState)("all");
1990
+ const [activeTags, setActiveTags] = (0, import_react16.useState)([]);
1991
+ const [detail, setDetail] = (0, import_react16.useState)("full");
1992
+ const [helpOpen, setHelpOpen] = (0, import_react16.useState)(false);
1685
1993
  const { theme, toggle: toggleTheme } = useTheme();
1686
- const searchRef = (0, import_react14.useRef)(null);
1687
- const deferredQuery = (0, import_react14.useDeferredValue)(query);
1688
- const failures = (0, import_react14.useMemo)(() => listFailures(report), [report]);
1689
- const filtered = (0, import_react14.useMemo)(
1994
+ const searchRef = (0, import_react16.useRef)(null);
1995
+ const deferredQuery = (0, import_react16.useDeferredValue)(query);
1996
+ const failures = (0, import_react16.useMemo)(() => listFailures(report), [report]);
1997
+ const filtered = (0, import_react16.useMemo)(
1690
1998
  () => filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags }),
1691
1999
  [report, deferredQuery, statusFilter, activeTags]
1692
2000
  );
1693
2001
  const isFiltering = query !== deferredQuery;
1694
- const statusOptions = (0, import_react14.useMemo)(() => {
2002
+ const statusOptions = (0, import_react16.useMemo)(() => {
1695
2003
  const s = report.summary;
1696
2004
  const opts = [
1697
2005
  { key: "all", label: "All", count: s.total }
@@ -1702,19 +2010,19 @@ function ReportInteractiveView({
1702
2010
  if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
1703
2011
  return opts;
1704
2012
  }, [report]);
1705
- const tagOptions = (0, import_react14.useMemo)(() => allTags(report), [report]);
1706
- const toggleTag = (0, import_react14.useCallback)(
2013
+ const tagOptions = (0, import_react16.useMemo)(() => allTags(report), [report]);
2014
+ const toggleTag = (0, import_react16.useCallback)(
1707
2015
  (tag) => setActiveTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]),
1708
2016
  []
1709
2017
  );
1710
- const [toast, setToast] = (0, import_react14.useState)(null);
1711
- const toastTimer = (0, import_react14.useRef)(null);
1712
- const showToast = (0, import_react14.useCallback)((msg) => {
2018
+ const [toast, setToast] = (0, import_react16.useState)(null);
2019
+ const toastTimer = (0, import_react16.useRef)(null);
2020
+ const showToast = (0, import_react16.useCallback)((msg) => {
1713
2021
  setToast(msg);
1714
2022
  if (toastTimer.current) clearTimeout(toastTimer.current);
1715
2023
  toastTimer.current = setTimeout(() => setToast(null), 1800);
1716
2024
  }, []);
1717
- const copy = (0, import_react14.useCallback)(
2025
+ const copy = (0, import_react16.useCallback)(
1718
2026
  (text, msg) => {
1719
2027
  if (typeof navigator !== "undefined" && navigator.clipboard) {
1720
2028
  navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
@@ -1722,7 +2030,7 @@ function ReportInteractiveView({
1722
2030
  },
1723
2031
  [showToast]
1724
2032
  );
1725
- const scenarioActions = (0, import_react14.useMemo)(
2033
+ const scenarioActions = (0, import_react16.useMemo)(
1726
2034
  () => ({
1727
2035
  copyLink: (s) => copy(scenarioPermalink(s), "Link copied"),
1728
2036
  copyMarkdown: (s) => copy(scenarioToMarkdown(s), "Markdown copied"),
@@ -1730,17 +2038,17 @@ function ReportInteractiveView({
1730
2038
  }),
1731
2039
  [copy]
1732
2040
  );
1733
- const failureIndexRef = (0, import_react14.useRef)(0);
2041
+ const failureIndexRef = (0, import_react16.useRef)(0);
1734
2042
  const collapse = useCollapseState();
1735
- const allCollapsibleIds = (0, import_react14.useMemo)(
2043
+ const allCollapsibleIds = (0, import_react16.useMemo)(
1736
2044
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
1737
2045
  [report]
1738
2046
  );
1739
- const collapseAll = (0, import_react14.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
1740
- const focusSearch = (0, import_react14.useCallback)(() => {
2047
+ const collapseAll = (0, import_react16.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2048
+ const focusSearch = (0, import_react16.useCallback)(() => {
1741
2049
  searchRef.current?.focus();
1742
2050
  }, []);
1743
- const stepFailure = (0, import_react14.useCallback)(
2051
+ const stepFailure = (0, import_react16.useCallback)(
1744
2052
  (direction) => {
1745
2053
  if (failures.length === 0) return;
1746
2054
  failureIndexRef.current = (failureIndexRef.current + direction + failures.length) % failures.length;
@@ -1749,10 +2057,10 @@ function ReportInteractiveView({
1749
2057
  },
1750
2058
  [failures]
1751
2059
  );
1752
- const toggleHelp = (0, import_react14.useCallback)(() => {
2060
+ const toggleHelp = (0, import_react16.useCallback)(() => {
1753
2061
  setHelpOpen((v) => !v);
1754
2062
  }, []);
1755
- const escape = (0, import_react14.useCallback)(() => {
2063
+ const escape = (0, import_react16.useCallback)(() => {
1756
2064
  if (helpOpen) setHelpOpen(false);
1757
2065
  else if (query !== "") setQuery("");
1758
2066
  }, [helpOpen, query]);
@@ -1769,13 +2077,13 @@ function ReportInteractiveView({
1769
2077
  const hasContent = filtered.features.length > 0;
1770
2078
  const totalScenarios = report.summary.total;
1771
2079
  const matchedScenarios = filtered.summary.total;
1772
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2080
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1773
2081
  ReportRoot,
1774
2082
  {
1775
2083
  report: filtered,
1776
2084
  customRenderers,
1777
2085
  renderers,
1778
- children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2086
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
1779
2087
  "main",
1780
2088
  {
1781
2089
  className: cn("es-report", "es-report-interactive", className),
@@ -1784,10 +2092,19 @@ function ReportInteractiveView({
1784
2092
  "data-theme": dataTheme,
1785
2093
  "data-detail-level": detail,
1786
2094
  children: [
1787
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("header", { className: "es-report-header", children: [
1788
- hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportTitleBlock, { title }),
1789
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
1790
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2095
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("header", { className: "es-report-header", children: [
2096
+ hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportTitleBlock, { title }),
2097
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2098
+ ReportFreshness,
2099
+ {
2100
+ lastRunMs: reportLastRunMs(report),
2101
+ ciUrl: report.ci?.url,
2102
+ staleAfterDays
2103
+ }
2104
+ ),
2105
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportLastRunDelta, { history: scenarioHistory, report }),
2106
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
2107
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1791
2108
  ReportSearch,
1792
2109
  {
1793
2110
  ref: searchRef,
@@ -1797,8 +2114,8 @@ function ReportInteractiveView({
1797
2114
  totalCount: totalScenarios
1798
2115
  }
1799
2116
  ),
1800
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex shrink-0 gap-1", children: [
1801
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2117
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex shrink-0 gap-1", children: [
2118
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1802
2119
  "button",
1803
2120
  {
1804
2121
  type: "button",
@@ -1808,7 +2125,7 @@ function ReportInteractiveView({
1808
2125
  children: "Expand all"
1809
2126
  }
1810
2127
  ),
1811
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2128
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1812
2129
  "button",
1813
2130
  {
1814
2131
  type: "button",
@@ -1818,7 +2135,7 @@ function ReportInteractiveView({
1818
2135
  children: "Collapse all"
1819
2136
  }
1820
2137
  ),
1821
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2138
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1822
2139
  "button",
1823
2140
  {
1824
2141
  type: "button",
@@ -1828,7 +2145,7 @@ function ReportInteractiveView({
1828
2145
  children: detail === "full" ? "Hide docs" : "Show docs"
1829
2146
  }
1830
2147
  ),
1831
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2148
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1832
2149
  "button",
1833
2150
  {
1834
2151
  type: "button",
@@ -1840,7 +2157,7 @@ function ReportInteractiveView({
1840
2157
  )
1841
2158
  ] })
1842
2159
  ] }),
1843
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2160
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1844
2161
  ReportFilters,
1845
2162
  {
1846
2163
  statuses: statusOptions,
@@ -1852,12 +2169,12 @@ function ReportInteractiveView({
1852
2169
  }
1853
2170
  )
1854
2171
  ] }),
1855
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportFailureBanner, { failures }),
1856
- hasContent ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "flex gap-6", children: [
1857
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportToc, {}),
1858
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportFeatureList, {}) })
1859
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
1860
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2172
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportFailureBanner, { failures }),
2173
+ hasContent ? /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex gap-6", children: [
2174
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportToc, {}),
2175
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportFeatureList, {}) })
2176
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
2177
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1861
2178
  "button",
1862
2179
  {
1863
2180
  type: "button",
@@ -1868,8 +2185,8 @@ function ReportInteractiveView({
1868
2185
  children: "?"
1869
2186
  }
1870
2187
  ),
1871
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
1872
- toast ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2188
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
2189
+ toast ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1873
2190
  "div",
1874
2191
  {
1875
2192
  role: "status",
@@ -1882,18 +2199,18 @@ function ReportInteractiveView({
1882
2199
  }
1883
2200
  )
1884
2201
  }
1885
- ) }) });
2202
+ ) }) }) });
1886
2203
  }
1887
2204
 
1888
2205
  // src/interactive/ReportInteractiveIsland.tsx
1889
- var import_react17 = require("react");
2206
+ var import_react19 = require("react");
1890
2207
 
1891
2208
  // src/interactive/island-renderers.tsx
1892
- var import_react16 = require("react");
2209
+ var import_react18 = require("react");
1893
2210
 
1894
2211
  // src/components/doc/MermaidView.tsx
1895
- var import_react15 = require("react");
1896
- var import_jsx_runtime37 = require("react/jsx-runtime");
2212
+ var import_react17 = require("react");
2213
+ var import_jsx_runtime40 = require("react/jsx-runtime");
1897
2214
  function prefersDark(el) {
1898
2215
  const themed = el?.closest("[data-theme]");
1899
2216
  if (themed?.dataset.theme === "dark") return true;
@@ -1901,9 +2218,9 @@ function prefersDark(el) {
1901
2218
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
1902
2219
  }
1903
2220
  function MermaidView({ entry, load }) {
1904
- const ref = (0, import_react15.useRef)(null);
1905
- const [svg, setSvg] = (0, import_react15.useState)(null);
1906
- (0, import_react15.useEffect)(() => {
2221
+ const ref = (0, import_react17.useRef)(null);
2222
+ const [svg, setSvg] = (0, import_react17.useState)(null);
2223
+ (0, import_react17.useEffect)(() => {
1907
2224
  let cancelled = false;
1908
2225
  const draw = async () => {
1909
2226
  try {
@@ -1930,9 +2247,9 @@ function MermaidView({ entry, load }) {
1930
2247
  };
1931
2248
  }, [entry.code, load]);
1932
2249
  if (svg) {
1933
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("figure", { ref, className: "my-3", children: [
1934
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1935
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2250
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("figure", { ref, className: "my-3", children: [
2251
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
2252
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
1936
2253
  "div",
1937
2254
  {
1938
2255
  role: "img",
@@ -1943,11 +2260,11 @@ function MermaidView({ entry, load }) {
1943
2260
  )
1944
2261
  ] });
1945
2262
  }
1946
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(MermaidSource, { entry, ref });
2263
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(MermaidSource, { entry, ref });
1947
2264
  }
1948
2265
 
1949
2266
  // src/interactive/island-renderers.tsx
1950
- var import_jsx_runtime38 = require("react/jsx-runtime");
2267
+ var import_jsx_runtime41 = require("react/jsx-runtime");
1951
2268
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
1952
2269
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
1953
2270
  function cachedCdnModule(url) {
@@ -1964,8 +2281,8 @@ function cachedCdnModule(url) {
1964
2281
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
1965
2282
  var loadHljs = cachedCdnModule(HLJS_ESM);
1966
2283
  function CdnHighlightedCode({ entry }) {
1967
- const [html, setHtml] = (0, import_react16.useState)(null);
1968
- (0, import_react16.useEffect)(() => {
2284
+ const [html, setHtml] = (0, import_react18.useState)(null);
2285
+ (0, import_react18.useEffect)(() => {
1969
2286
  let cancelled = false;
1970
2287
  void (async () => {
1971
2288
  try {
@@ -1981,27 +2298,27 @@ function CdnHighlightedCode({ entry }) {
1981
2298
  cancelled = true;
1982
2299
  };
1983
2300
  }, [entry.content, entry.lang]);
1984
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
2301
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
1985
2302
  "code",
1986
2303
  {
1987
2304
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
1988
2305
  dangerouslySetInnerHTML: { __html: html }
1989
2306
  }
1990
- ) : /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
2307
+ ) : /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
1991
2308
  }
1992
2309
  function buildIslandRenderers(opts) {
1993
2310
  const renderers = {};
1994
2311
  if (opts.mermaid) {
1995
- renderers.mermaid = (entry) => (0, import_react16.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
2312
+ renderers.mermaid = (entry) => (0, import_react18.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
1996
2313
  }
1997
2314
  if (opts.syntaxHighlighting) {
1998
- renderers.code = (entry) => (0, import_react16.createElement)(CdnHighlightedCode, { entry });
2315
+ renderers.code = (entry) => (0, import_react18.createElement)(CdnHighlightedCode, { entry });
1999
2316
  }
2000
2317
  return renderers;
2001
2318
  }
2002
2319
 
2003
2320
  // src/interactive/ReportInteractiveIsland.tsx
2004
- var import_jsx_runtime39 = require("react/jsx-runtime");
2321
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2005
2322
  function ReportInteractiveIsland({
2006
2323
  report,
2007
2324
  title,
@@ -2010,11 +2327,11 @@ function ReportInteractiveIsland({
2010
2327
  syntaxHighlighting = true,
2011
2328
  mermaid = true
2012
2329
  }) {
2013
- const renderers = (0, import_react17.useMemo)(
2330
+ const renderers = (0, import_react19.useMemo)(
2014
2331
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
2015
2332
  [syntaxHighlighting, mermaid]
2016
2333
  );
2017
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2334
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2018
2335
  ReportInteractive,
2019
2336
  {
2020
2337
  report,
@@ -2028,6 +2345,7 @@ function ReportInteractiveIsland({
2028
2345
  // Annotate the CommonJS export names for ESM import in node:
2029
2346
  0 && (module.exports = {
2030
2347
  ReportFailureBanner,
2348
+ ReportFreshness,
2031
2349
  ReportInteractive,
2032
2350
  ReportInteractiveIsland,
2033
2351
  ReportSearch,