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.
@@ -6,7 +6,7 @@ import {
6
6
  useDeferredValue,
7
7
  useMemo as useMemo3,
8
8
  useRef as useRef3,
9
- useState as useState4
9
+ useState as useState5
10
10
  } from "react";
11
11
 
12
12
  // src/result.ts
@@ -58,11 +58,23 @@ function useReport() {
58
58
  return ctx.report;
59
59
  }
60
60
 
61
- // src/components/ReportScenario.tsx
62
- import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
63
-
64
- // src/components/ReportSteps.tsx
65
- import { formatDuration } from "executable-stories-core/utils/duration";
61
+ // ../executable-stories-core/dist/utils/duration.js
62
+ function formatDuration(ms) {
63
+ if (ms < 1e3) {
64
+ return `${Math.round(ms)} ms`;
65
+ }
66
+ if (ms < 6e4) {
67
+ return `${(ms / 1e3).toFixed(2)} s`;
68
+ }
69
+ const minutes = Math.floor(ms / 6e4);
70
+ const seconds = Math.floor(ms % 6e4 / 1e3);
71
+ if (minutes < 60) {
72
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
73
+ }
74
+ const hours = Math.floor(minutes / 60);
75
+ const remainingMinutes = minutes % 60;
76
+ return `${hours}h ${remainingMinutes}m`;
77
+ }
66
78
 
67
79
  // src/components/doc/DocNote.tsx
68
80
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -216,6 +228,15 @@ function safeUrl(value) {
216
228
  if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
217
229
  return trimmed;
218
230
  }
231
+ function safeImageUrl(value) {
232
+ if (!value) return void 0;
233
+ const trimmed = value.trim();
234
+ if (/^data:image\//i.test(trimmed)) return trimmed;
235
+ return safeUrl(trimmed);
236
+ }
237
+ function isLocalFsPath(value) {
238
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
239
+ }
219
240
 
220
241
  // src/components/doc/DocSection.tsx
221
242
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -271,11 +292,19 @@ function DocMermaid({ entry }) {
271
292
  // src/components/doc/DocScreenshot.tsx
272
293
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
273
294
  function DocScreenshot({ entry }) {
295
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
296
+ if (!src) {
297
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
298
+ /* @__PURE__ */ jsx10("div", { className: "font-medium", children: "Screenshot unavailable" }),
299
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
300
+ entry.alt ? /* @__PURE__ */ jsx10("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
301
+ ] });
302
+ }
274
303
  return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
275
304
  /* @__PURE__ */ jsx10(
276
305
  "img",
277
306
  {
278
- src: entry.path,
307
+ src,
279
308
  alt: entry.alt ?? "",
280
309
  loading: "lazy",
281
310
  className: "max-w-full rounded-md border border-border"
@@ -288,6 +317,13 @@ function DocScreenshot({ entry }) {
288
317
  // src/components/doc/DocVideo.tsx
289
318
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
290
319
  function DocVideo({ entry }) {
320
+ if (isLocalFsPath(entry.path)) {
321
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
322
+ /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Video unavailable" }),
323
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
324
+ entry.caption ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
325
+ ] });
326
+ }
291
327
  const src = safeUrl(entry.path);
292
328
  const poster = safeUrl(entry.poster);
293
329
  return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
@@ -521,9 +557,6 @@ function ReportAttachments({ attachments }) {
521
557
  );
522
558
  }
523
559
 
524
- // src/components/ReportTrace.tsx
525
- import { formatDuration as formatDuration2 } from "executable-stories-core/utils/duration";
526
-
527
560
  // src/lib/trace.ts
528
561
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
529
562
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -618,7 +651,7 @@ function ReportTrace({ spans }) {
618
651
  " span",
619
652
  rows.length === 1 ? "" : "s",
620
653
  ", ",
621
- formatDuration2(totalMs),
654
+ formatDuration(totalMs),
622
655
  ")"
623
656
  ] }),
624
657
  /* @__PURE__ */ jsx19("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -641,18 +674,131 @@ function ReportTrace({ spans }) {
641
674
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
642
675
  }
643
676
  ) }),
644
- /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration2(row.durationMs) })
677
+ /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
645
678
  ] }, `${row.spanId}-${i}`);
646
679
  }) })
647
680
  ] });
648
681
  }
649
682
 
683
+ // src/interactive/scenario-history-context.ts
684
+ import { createContext as createContext2, useContext as useContext3 } from "react";
685
+ var ScenarioHistoryContext = createContext2(null);
686
+ var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
687
+ function useScenarioHistory() {
688
+ return useContext3(ScenarioHistoryContext);
689
+ }
690
+
691
+ // src/lib/run-history.ts
692
+ function currentStreak(entries) {
693
+ const last = entries[entries.length - 1];
694
+ if (!last) return void 0;
695
+ let count = 0;
696
+ for (let i = entries.length - 1; i >= 0 && entries[i].status === last.status; i--) count++;
697
+ return { status: last.status, count };
698
+ }
699
+ var STREAK_VERB = {
700
+ passed: "Passing",
701
+ failed: "Failing",
702
+ skipped: "Skipped",
703
+ pending: "Pending"
704
+ };
705
+ function describeRunHistory(entries) {
706
+ const passed = entries.filter((e) => e.status === "passed").length;
707
+ const base = `${passed}/${entries.length} runs passed`;
708
+ const streak = currentStreak(entries);
709
+ if (!streak || entries.length < 2) return base;
710
+ return `${base} \xB7 ${STREAK_VERB[streak.status]} for the last ${streak.count === 1 ? "run" : `${streak.count} runs`}`;
711
+ }
712
+ var MIN_FLAKINESS_SAMPLES = 3;
713
+ function flakinessOf(entries) {
714
+ const countable = entries.filter((e) => e.status === "passed" || e.status === "failed");
715
+ if (countable.length < MIN_FLAKINESS_SAMPLES) return "stable";
716
+ let transitions = 0;
717
+ for (let i = 1; i < countable.length; i++) {
718
+ if (countable[i].status !== countable[i - 1].status) transitions++;
719
+ }
720
+ const transitionScore = transitions / (countable.length - 1);
721
+ const failureRate = countable.filter((e) => e.status === "failed").length / countable.length;
722
+ if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) return "flaky";
723
+ if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
724
+ return "stable";
725
+ }
726
+ function diffSinceLastRun(history2, scenarios) {
727
+ let hasPreviousRun = false;
728
+ const delta = { newlyFailing: [], newlyPassing: [], added: [] };
729
+ for (const scenario of scenarios) {
730
+ const entries = history2[scenario.id];
731
+ if (!entries || entries.length === 0) continue;
732
+ const last = entries[entries.length - 1];
733
+ const prev = entries[entries.length - 2];
734
+ if (!prev) {
735
+ delta.added.push(scenario);
736
+ continue;
737
+ }
738
+ hasPreviousRun = true;
739
+ if (last.status === "failed" && prev.status !== "failed") delta.newlyFailing.push(scenario);
740
+ else if (last.status === "passed" && prev.status === "failed") delta.newlyPassing.push(scenario);
741
+ }
742
+ return hasPreviousRun ? delta : void 0;
743
+ }
744
+
745
+ // src/components/ScenarioRunHistory.tsx
746
+ import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
747
+ var MAX_DOTS = 10;
748
+ var DOT_COLOR = {
749
+ passed: "bg-pass",
750
+ failed: "bg-fail",
751
+ skipped: "bg-skip",
752
+ pending: "bg-pend"
753
+ };
754
+ function dotTitle(e) {
755
+ const parts = [new Date(e.timestamp).toLocaleString(), e.status];
756
+ if (e.commitSha) parts.push(e.commitSha.slice(0, 8));
757
+ return parts.join(" \xB7 ");
758
+ }
759
+ function ScenarioRunHistory({ scenarioId }) {
760
+ const history2 = useScenarioHistory();
761
+ const entries = history2?.[scenarioId];
762
+ if (!entries || entries.length < 2) return null;
763
+ const recent = entries.slice(-MAX_DOTS);
764
+ const flaky = flakinessOf(recent) === "flaky";
765
+ const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
766
+ return /* @__PURE__ */ jsxs13(Fragment7, { children: [
767
+ flaky ? /* @__PURE__ */ jsx20(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
768
+ /* @__PURE__ */ jsx20(
769
+ "span",
770
+ {
771
+ role: "img",
772
+ "aria-label": `Run history: ${summary}`,
773
+ title: summary,
774
+ className: "flex shrink-0 items-center gap-[3px]",
775
+ children: recent.map((e, i) => /* @__PURE__ */ jsx20(
776
+ "span",
777
+ {
778
+ "aria-hidden": true,
779
+ title: dotTitle(e),
780
+ className: cn(
781
+ "inline-block size-1.5 rounded-full",
782
+ DOT_COLOR[e.status],
783
+ // The newest run (rightmost dot) is the one the card's status
784
+ // badge already describes; ring it so the timeline reads as
785
+ // "…leading up to now".
786
+ i === recent.length - 1 && "ring-1 ring-border ring-offset-1 ring-offset-card"
787
+ )
788
+ },
789
+ `${e.timestamp}-${i}`
790
+ ))
791
+ }
792
+ )
793
+ ] });
794
+ }
795
+
650
796
  // src/interactive/collapse-context.ts
651
- import { createContext as createContext2, useCallback, useContext as useContext3, useState } from "react";
652
- var CollapseContext = createContext2(null);
797
+ import { createContext as createContext3, useCallback, useContext as useContext4, useState } from "react";
798
+ var CollapseContext = createContext3(null);
653
799
  var CollapseProvider = CollapseContext.Provider;
654
800
  function useCollapse() {
655
- return useContext3(CollapseContext);
801
+ return useContext4(CollapseContext);
656
802
  }
657
803
  var STORAGE_KEY = "es-collapsed-ids";
658
804
  function readPersisted() {
@@ -701,11 +847,11 @@ function useCollapseState() {
701
847
  }
702
848
 
703
849
  // src/interactive/scenario-actions.tsx
704
- import { createContext as createContext3, useContext as useContext4 } from "react";
705
- var ScenarioActionsContext = createContext3(null);
850
+ import { createContext as createContext4, useContext as useContext5 } from "react";
851
+ var ScenarioActionsContext = createContext4(null);
706
852
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
707
853
  function useScenarioActions() {
708
- return useContext4(ScenarioActionsContext);
854
+ return useContext5(ScenarioActionsContext);
709
855
  }
710
856
  function scenarioToMarkdown(scenario) {
711
857
  const lines = [`## ${scenario.title} _(${scenario.status})_`, ""];
@@ -743,9 +889,9 @@ function scenarioPermalink(scenario) {
743
889
 
744
890
  // src/components/ui/card.tsx
745
891
  import "react";
746
- import { jsx as jsx20 } from "react/jsx-runtime";
892
+ import { jsx as jsx21 } from "react/jsx-runtime";
747
893
  function Card({ className, ...props }) {
748
- return /* @__PURE__ */ jsx20(
894
+ return /* @__PURE__ */ jsx21(
749
895
  "div",
750
896
  {
751
897
  "data-slot": "card",
@@ -758,7 +904,7 @@ function Card({ className, ...props }) {
758
904
  );
759
905
  }
760
906
  function CardHeader({ className, ...props }) {
761
- return /* @__PURE__ */ jsx20(
907
+ return /* @__PURE__ */ jsx21(
762
908
  "div",
763
909
  {
764
910
  "data-slot": "card-header",
@@ -771,7 +917,7 @@ function CardHeader({ className, ...props }) {
771
917
  );
772
918
  }
773
919
  function CardContent({ className, ...props }) {
774
- return /* @__PURE__ */ jsx20(
920
+ return /* @__PURE__ */ jsx21(
775
921
  "div",
776
922
  {
777
923
  "data-slot": "card-content",
@@ -782,7 +928,7 @@ function CardContent({ className, ...props }) {
782
928
  }
783
929
 
784
930
  // src/components/ReportScenario.tsx
785
- import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
931
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
786
932
  var STATUS_LABEL = {
787
933
  passed: "Passed",
788
934
  failed: "Failed",
@@ -810,12 +956,12 @@ var STATUS_BADGE = {
810
956
  function ReportScenario({ scenario, hideTitle = false }) {
811
957
  const titleId = `${scenario.id}-title`;
812
958
  const bodyId = `${scenario.id}-body`;
813
- const label = STATUS_LABEL[scenario.status];
959
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
814
960
  const collapse = useCollapse();
815
961
  const collapsible = collapse !== null && !hideTitle;
816
962
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
817
963
  const actions = useScenarioActions();
818
- return /* @__PURE__ */ jsxs13(
964
+ return /* @__PURE__ */ jsxs14(
819
965
  Card,
820
966
  {
821
967
  id: scenario.id,
@@ -823,10 +969,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
823
969
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
824
970
  className: "gap-0 py-0",
825
971
  children: [
826
- /* @__PURE__ */ jsxs13(CardHeader, { className: "px-4 py-3", children: [
827
- /* @__PURE__ */ jsxs13("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
828
- hideTitle ? null : /* @__PURE__ */ jsxs13("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
829
- collapsible ? /* @__PURE__ */ jsx21(
972
+ /* @__PURE__ */ jsxs14(CardHeader, { className: "px-4 py-3", children: [
973
+ /* @__PURE__ */ jsxs14("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
974
+ hideTitle ? null : /* @__PURE__ */ jsxs14("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
975
+ collapsible ? /* @__PURE__ */ jsx22(
830
976
  "button",
831
977
  {
832
978
  type: "button",
@@ -835,17 +981,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
835
981
  "aria-controls": bodyId,
836
982
  "aria-label": `Toggle ${scenario.title}`,
837
983
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
838
- children: /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
984
+ children: /* @__PURE__ */ jsx22("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
839
985
  }
840
986
  ) : null,
841
- /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
842
- /* @__PURE__ */ jsx21("span", { children: scenario.title })
987
+ /* @__PURE__ */ jsx22("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
988
+ /* @__PURE__ */ jsx22("span", { children: scenario.title })
843
989
  ] }),
844
- /* @__PURE__ */ jsxs13("div", { className: "flex shrink-0 items-center gap-2", children: [
845
- scenario.durationMs > 0 ? /* @__PURE__ */ jsx21("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
846
- /* @__PURE__ */ jsx21(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
847
- actions ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-0.5", children: [
848
- /* @__PURE__ */ jsx21(
990
+ /* @__PURE__ */ jsxs14("div", { className: "flex shrink-0 items-center gap-2", children: [
991
+ /* @__PURE__ */ jsx22(ScenarioRunHistory, { scenarioId: scenario.id }),
992
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx22("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
993
+ /* @__PURE__ */ jsx22(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
994
+ actions ? /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-0.5", children: [
995
+ /* @__PURE__ */ jsx22(
849
996
  "button",
850
997
  {
851
998
  type: "button",
@@ -856,7 +1003,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
856
1003
  children: "link"
857
1004
  }
858
1005
  ),
859
- /* @__PURE__ */ jsx21(
1006
+ /* @__PURE__ */ jsx22(
860
1007
  "button",
861
1008
  {
862
1009
  type: "button",
@@ -867,7 +1014,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
867
1014
  children: "md"
868
1015
  }
869
1016
  ),
870
- /* @__PURE__ */ jsx21(
1017
+ /* @__PURE__ */ jsx22(
871
1018
  "button",
872
1019
  {
873
1020
  type: "button",
@@ -881,13 +1028,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
881
1028
  ] }) : null
882
1029
  ] })
883
1030
  ] }),
884
- scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs13("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
885
- scenario.tags.map((t) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
886
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx21("li", { children: ticket.url ? /* @__PURE__ */ jsx21("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1031
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs14("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1032
+ scenario.tags.map((t) => /* @__PURE__ */ jsx22("li", { children: /* @__PURE__ */ jsx22(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1033
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx22("li", { children: ticket.url ? /* @__PURE__ */ jsx22("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx22(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx22(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
887
1034
  ] }) : null
888
1035
  ] }),
889
- /* @__PURE__ */ jsxs13(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
890
- scenario.errorMessage ? /* @__PURE__ */ jsx21(
1036
+ /* @__PURE__ */ jsxs14(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1037
+ scenario.errorMessage ? /* @__PURE__ */ jsx22(
891
1038
  "pre",
892
1039
  {
893
1040
  role: "alert",
@@ -895,10 +1042,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
895
1042
  children: scenario.errorMessage
896
1043
  }
897
1044
  ) : null,
898
- /* @__PURE__ */ jsx21(ReportSteps, { scenario }),
899
- scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx21("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx21(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
900
- /* @__PURE__ */ jsx21(ReportAttachments, { attachments: scenario.attachments }),
901
- /* @__PURE__ */ jsx21(ReportTrace, { spans: scenario.otelSpans })
1045
+ /* @__PURE__ */ jsx22(ReportSteps, { scenario }),
1046
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx22("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx22(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1047
+ /* @__PURE__ */ jsx22(ReportAttachments, { attachments: scenario.attachments }),
1048
+ /* @__PURE__ */ jsx22(ReportTrace, { spans: scenario.otelSpans })
902
1049
  ] })
903
1050
  ]
904
1051
  }
@@ -906,13 +1053,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
906
1053
  }
907
1054
 
908
1055
  // src/components/ReportScenarioList.tsx
909
- import { Fragment as Fragment7, jsx as jsx22 } from "react/jsx-runtime";
1056
+ import { Fragment as Fragment8, jsx as jsx23 } from "react/jsx-runtime";
910
1057
  function ReportScenarioList({ feature }) {
911
- return /* @__PURE__ */ jsx22(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx22(ReportScenario, { scenario }, scenario.id)) });
1058
+ return /* @__PURE__ */ jsx23(Fragment8, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx23(ReportScenario, { scenario }, scenario.id)) });
912
1059
  }
913
1060
 
914
1061
  // src/components/ReportFeature.tsx
915
- import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1062
+ import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
916
1063
  function ReportFeature({ feature }) {
917
1064
  const titleId = `${feature.id}-title`;
918
1065
  const bodyId = `${feature.id}-body`;
@@ -920,7 +1067,7 @@ function ReportFeature({ feature }) {
920
1067
  const skipped = s.skipped + s.pending;
921
1068
  const collapse = useCollapse();
922
1069
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
923
- return /* @__PURE__ */ jsxs14(
1070
+ return /* @__PURE__ */ jsxs15(
924
1071
  "section",
925
1072
  {
926
1073
  id: feature.id,
@@ -928,9 +1075,9 @@ function ReportFeature({ feature }) {
928
1075
  "aria-labelledby": titleId,
929
1076
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
930
1077
  children: [
931
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
932
- /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 items-center gap-2", children: [
933
- collapse ? /* @__PURE__ */ jsx23(
1078
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
1079
+ /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 items-center gap-2", children: [
1080
+ collapse ? /* @__PURE__ */ jsx24(
934
1081
  "button",
935
1082
  {
936
1083
  type: "button",
@@ -939,30 +1086,30 @@ function ReportFeature({ feature }) {
939
1086
  "aria-controls": bodyId,
940
1087
  "aria-label": `Toggle ${feature.title}`,
941
1088
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
942
- children: /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1089
+ children: /* @__PURE__ */ jsx24("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
943
1090
  }
944
1091
  ) : null,
945
- /* @__PURE__ */ jsxs14("div", { className: "min-w-0", children: [
946
- /* @__PURE__ */ jsx23("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
947
- /* @__PURE__ */ jsx23("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
1092
+ /* @__PURE__ */ jsxs15("div", { className: "min-w-0", children: [
1093
+ /* @__PURE__ */ jsx24("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
1094
+ /* @__PURE__ */ jsx24("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
948
1095
  ] })
949
1096
  ] }),
950
- /* @__PURE__ */ jsxs14(
1097
+ /* @__PURE__ */ jsxs15(
951
1098
  "div",
952
1099
  {
953
1100
  role: "img",
954
1101
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
955
1102
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
956
1103
  children: [
957
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-pass", children: [
1104
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-pass", children: [
958
1105
  "\u2713",
959
1106
  s.passed
960
1107
  ] }),
961
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-fail", children: [
1108
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-fail", children: [
962
1109
  "\u2717",
963
1110
  s.failed
964
1111
  ] }),
965
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-skip", children: [
1112
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-skip", children: [
966
1113
  "\u25CB",
967
1114
  skipped
968
1115
  ] })
@@ -970,24 +1117,24 @@ function ReportFeature({ feature }) {
970
1117
  }
971
1118
  )
972
1119
  ] }),
973
- /* @__PURE__ */ jsx23("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx23(ReportScenarioList, { feature }) })
1120
+ /* @__PURE__ */ jsx24("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx24(ReportScenarioList, { feature }) })
974
1121
  ]
975
1122
  }
976
1123
  );
977
1124
  }
978
1125
 
979
1126
  // src/components/ReportFeatureList.tsx
980
- import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
1127
+ import { Fragment as Fragment9, jsx as jsx25 } from "react/jsx-runtime";
981
1128
  function ReportFeatureList() {
982
1129
  const report = useReport();
983
- return /* @__PURE__ */ jsx24(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx24(ReportFeature, { feature }, feature.id)) });
1130
+ return /* @__PURE__ */ jsx25(Fragment9, { children: report.features.map((feature) => /* @__PURE__ */ jsx25(ReportFeature, { feature }, feature.id)) });
984
1131
  }
985
1132
 
986
1133
  // src/components/ui/empty.tsx
987
1134
  import { cva as cva2 } from "class-variance-authority";
988
- import { jsx as jsx25 } from "react/jsx-runtime";
1135
+ import { jsx as jsx26 } from "react/jsx-runtime";
989
1136
  function Empty({ className, ...props }) {
990
- return /* @__PURE__ */ jsx25(
1137
+ return /* @__PURE__ */ jsx26(
991
1138
  "div",
992
1139
  {
993
1140
  "data-slot": "empty",
@@ -1000,7 +1147,7 @@ function Empty({ className, ...props }) {
1000
1147
  );
1001
1148
  }
1002
1149
  function EmptyHeader({ className, ...props }) {
1003
- return /* @__PURE__ */ jsx25(
1150
+ return /* @__PURE__ */ jsx26(
1004
1151
  "div",
1005
1152
  {
1006
1153
  "data-slot": "empty-header",
@@ -1027,7 +1174,7 @@ var emptyMediaVariants = cva2(
1027
1174
  }
1028
1175
  );
1029
1176
  function EmptyTitle({ className, ...props }) {
1030
- return /* @__PURE__ */ jsx25(
1177
+ return /* @__PURE__ */ jsx26(
1031
1178
  "div",
1032
1179
  {
1033
1180
  "data-slot": "empty-title",
@@ -1038,16 +1185,16 @@ function EmptyTitle({ className, ...props }) {
1038
1185
  }
1039
1186
 
1040
1187
  // src/components/ReportEmpty.tsx
1041
- import { jsx as jsx26 } from "react/jsx-runtime";
1188
+ import { jsx as jsx27 } from "react/jsx-runtime";
1042
1189
  function ReportEmpty({ message }) {
1043
- return /* @__PURE__ */ jsx26(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx26(EmptyHeader, { children: /* @__PURE__ */ jsx26(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1190
+ return /* @__PURE__ */ jsx27(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx27(EmptyHeader, { children: /* @__PURE__ */ jsx27(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1044
1191
  }
1045
1192
 
1046
1193
  // src/components/ReportSummary.tsx
1047
- import { Fragment as Fragment9, jsx as jsx27, jsxs as jsxs15 } from "react/jsx-runtime";
1194
+ import { Fragment as Fragment10, jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
1048
1195
  function ReportSummary({ className }) {
1049
1196
  const report = useReport();
1050
- return /* @__PURE__ */ jsx27(
1197
+ return /* @__PURE__ */ jsx28(
1051
1198
  ReportSummaryView,
1052
1199
  {
1053
1200
  summary: report.summary,
@@ -1057,32 +1204,32 @@ function ReportSummary({ className }) {
1057
1204
  );
1058
1205
  }
1059
1206
  function ReportSummaryView({ summary, className, ariaLabel }) {
1060
- return /* @__PURE__ */ jsxs15("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1061
- /* @__PURE__ */ jsxs15("span", { children: [
1062
- /* @__PURE__ */ jsx27("strong", { className: "font-semibold text-foreground", children: summary.total }),
1207
+ return /* @__PURE__ */ jsxs16("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1208
+ /* @__PURE__ */ jsxs16("span", { children: [
1209
+ /* @__PURE__ */ jsx28("strong", { className: "font-semibold text-foreground", children: summary.total }),
1063
1210
  " scenario",
1064
1211
  summary.total === 1 ? "" : "s"
1065
1212
  ] }),
1066
1213
  " \xB7 ",
1067
- /* @__PURE__ */ jsxs15("span", { "data-status": "passed", className: "text-pass", children: [
1214
+ /* @__PURE__ */ jsxs16("span", { "data-status": "passed", className: "text-pass", children: [
1068
1215
  summary.passed,
1069
1216
  " passed"
1070
1217
  ] }),
1071
1218
  " \xB7 ",
1072
- /* @__PURE__ */ jsxs15("span", { "data-status": "failed", className: "text-fail", children: [
1219
+ /* @__PURE__ */ jsxs16("span", { "data-status": "failed", className: "text-fail", children: [
1073
1220
  summary.failed,
1074
1221
  " failed"
1075
1222
  ] }),
1076
- summary.skipped > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1223
+ summary.skipped > 0 ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
1077
1224
  " \xB7 ",
1078
- /* @__PURE__ */ jsxs15("span", { "data-status": "skipped", className: "text-skip", children: [
1225
+ /* @__PURE__ */ jsxs16("span", { "data-status": "skipped", className: "text-skip", children: [
1079
1226
  summary.skipped,
1080
1227
  " skipped"
1081
1228
  ] })
1082
1229
  ] }) : null,
1083
- summary.pending > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1230
+ summary.pending > 0 ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
1084
1231
  " \xB7 ",
1085
- /* @__PURE__ */ jsxs15("span", { "data-status": "pending", className: "text-pend", children: [
1232
+ /* @__PURE__ */ jsxs16("span", { "data-status": "pending", className: "text-pend", children: [
1086
1233
  summary.pending,
1087
1234
  " pending"
1088
1235
  ] })
@@ -1090,69 +1237,135 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1090
1237
  ] });
1091
1238
  }
1092
1239
 
1240
+ // src/lib/provenance.ts
1241
+ function reportLastRunMs(report) {
1242
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1243
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1244
+ return void 0;
1245
+ }
1246
+ function formatRelativeAge(thenMs, nowMs) {
1247
+ const deltaMs = Math.max(0, nowMs - thenMs);
1248
+ const minutes = Math.floor(deltaMs / 6e4);
1249
+ if (minutes < 1) return "just now";
1250
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1251
+ const hours = Math.floor(minutes / 60);
1252
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1253
+ const days = Math.floor(hours / 24);
1254
+ return `${days} day${days === 1 ? "" : "s"} ago`;
1255
+ }
1256
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1257
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1258
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
1259
+ }
1260
+ var CI_DISPLAY_NAMES = {
1261
+ github: "GitHub Actions",
1262
+ gitlab: "GitLab CI",
1263
+ circleci: "CircleCI",
1264
+ jenkins: "Jenkins",
1265
+ azure: "Azure DevOps",
1266
+ buildkite: "Buildkite",
1267
+ travis: "Travis CI",
1268
+ ci: "CI"
1269
+ };
1270
+ function ciDisplayName(name) {
1271
+ return CI_DISPLAY_NAMES[name] ?? name;
1272
+ }
1273
+ function commitUrl(ci, sha) {
1274
+ const commitSha = ci?.commitSha ?? sha;
1275
+ if (!ci?.url || !commitSha) return void 0;
1276
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1277
+ if (github) return `${github[1]}/commit/${commitSha}`;
1278
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1279
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1280
+ return void 0;
1281
+ }
1282
+ function prUrl(ci) {
1283
+ if (!ci?.url || !ci.prNumber) return void 0;
1284
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1285
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1286
+ return void 0;
1287
+ }
1288
+
1093
1289
  // src/components/ReportMeta.tsx
1094
- import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1095
- import { jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
1290
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1096
1291
  function ReportMeta() {
1097
1292
  const report = useReport();
1098
1293
  const items = [];
1294
+ const linkClass = "text-primary underline underline-offset-2";
1099
1295
  if (report.startedAtMs) {
1100
1296
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1101
1297
  }
1102
1298
  if (report.durationMs) {
1103
- items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1299
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1104
1300
  }
1105
1301
  if (report.packageVersion) {
1106
1302
  items.push({ label: "Version", value: report.packageVersion });
1107
1303
  }
1108
- if (report.gitSha) {
1109
- items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1304
+ if (report.ci?.branch) {
1305
+ items.push({ label: "Branch", value: report.ci.branch });
1306
+ }
1307
+ const sha = report.ci?.commitSha ?? report.gitSha;
1308
+ if (sha) {
1309
+ const url = commitUrl(report.ci, report.gitSha);
1310
+ const short = sha.slice(0, 8);
1311
+ items.push({
1312
+ label: "Commit",
1313
+ value: url ? /* @__PURE__ */ jsx29("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1314
+ });
1110
1315
  }
1111
1316
  if (report.ci) {
1112
1317
  items.push({
1113
1318
  label: "CI",
1114
- value: report.ci.url ? /* @__PURE__ */ jsxs16("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1115
- report.ci.name,
1319
+ value: report.ci.url ? /* @__PURE__ */ jsxs17("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1320
+ ciDisplayName(report.ci.name),
1116
1321
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1117
- ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1322
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1323
+ });
1324
+ }
1325
+ if (report.ci?.prNumber) {
1326
+ const url = prUrl(report.ci);
1327
+ const label = `#${report.ci.prNumber}`;
1328
+ items.push({
1329
+ label: "PR",
1330
+ value: url ? /* @__PURE__ */ jsx29("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1118
1331
  });
1119
1332
  }
1120
1333
  if (items.length === 0) return null;
1121
- return /* @__PURE__ */ jsx28("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__ */ jsxs16("div", { className: "flex gap-1.5", children: [
1122
- /* @__PURE__ */ jsx28("dt", { className: "font-medium", children: item.label }),
1123
- /* @__PURE__ */ jsx28("dd", { className: "font-mono text-foreground", children: item.value })
1334
+ return /* @__PURE__ */ jsx29("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__ */ jsxs17("div", { className: "flex gap-1.5", children: [
1335
+ /* @__PURE__ */ jsx29("dt", { className: "font-medium", children: item.label }),
1336
+ /* @__PURE__ */ jsx29("dd", { className: "font-mono text-foreground", children: item.value })
1124
1337
  ] }, item.label)) });
1125
1338
  }
1126
1339
 
1127
1340
  // src/components/ReportSchemaError.tsx
1128
- import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1341
+ import { jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1129
1342
  function ReportSchemaError({ error }) {
1130
- return /* @__PURE__ */ jsxs17("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1131
- /* @__PURE__ */ jsx29("p", { children: /* @__PURE__ */ jsx29("strong", { children: "Report could not be displayed." }) }),
1132
- /* @__PURE__ */ jsx29("p", { children: error.message }),
1133
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs17("p", { children: [
1343
+ return /* @__PURE__ */ jsxs18("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1344
+ /* @__PURE__ */ jsx30("p", { children: /* @__PURE__ */ jsx30("strong", { children: "Report could not be displayed." }) }),
1345
+ /* @__PURE__ */ jsx30("p", { children: error.message }),
1346
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs18("p", { children: [
1134
1347
  "The report bundle is newer than this version of ",
1135
- /* @__PURE__ */ jsx29("code", { children: "executable-stories-react" }),
1348
+ /* @__PURE__ */ jsx30("code", { children: "executable-stories-react" }),
1136
1349
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1137
1350
  ] }) : null,
1138
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs17("details", { children: [
1139
- /* @__PURE__ */ jsxs17("summary", { children: [
1351
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs18("details", { children: [
1352
+ /* @__PURE__ */ jsxs18("summary", { children: [
1140
1353
  error.issues.length,
1141
1354
  " validation issue",
1142
1355
  error.issues.length === 1 ? "" : "s"
1143
1356
  ] }),
1144
- /* @__PURE__ */ jsx29("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1357
+ /* @__PURE__ */ jsx30("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1145
1358
  ] }) : null
1146
1359
  ] });
1147
1360
  }
1148
1361
 
1149
1362
  // src/components/ReportShell.tsx
1150
- import { Fragment as Fragment10, jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1363
+ import { Fragment as Fragment11, jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1151
1364
  function ReportTitleBlock({ title }) {
1152
- return /* @__PURE__ */ jsxs18(Fragment10, { children: [
1153
- /* @__PURE__ */ jsx30("h1", { children: title ?? "Story Report" }),
1154
- /* @__PURE__ */ jsx30(ReportSummary, {}),
1155
- /* @__PURE__ */ jsx30(ReportMeta, {})
1365
+ return /* @__PURE__ */ jsxs19(Fragment11, { children: [
1366
+ /* @__PURE__ */ jsx31("h1", { children: title ?? "Story Report" }),
1367
+ /* @__PURE__ */ jsx31(ReportSummary, {}),
1368
+ /* @__PURE__ */ jsx31(ReportMeta, {})
1156
1369
  ] });
1157
1370
  }
1158
1371
  function ReportErrorShell({
@@ -1161,7 +1374,7 @@ function ReportErrorShell({
1161
1374
  title,
1162
1375
  dataTheme
1163
1376
  }) {
1164
- return /* @__PURE__ */ jsx30("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx30(ReportSchemaError, { error }) });
1377
+ return /* @__PURE__ */ jsx31("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx31(ReportSchemaError, { error }) });
1165
1378
  }
1166
1379
 
1167
1380
  // src/interactive/ReportSearch.tsx
@@ -1169,7 +1382,7 @@ import {
1169
1382
  forwardRef,
1170
1383
  useId
1171
1384
  } from "react";
1172
- import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1385
+ import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
1173
1386
  var ReportSearch = forwardRef(
1174
1387
  function ReportSearch2(props, ref) {
1175
1388
  const {
@@ -1191,9 +1404,9 @@ var ReportSearch = forwardRef(
1191
1404
  e.preventDefault();
1192
1405
  }
1193
1406
  }
1194
- return /* @__PURE__ */ jsxs19("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
1195
- /* @__PURE__ */ jsx31("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
1196
- /* @__PURE__ */ jsx31(
1407
+ return /* @__PURE__ */ jsxs20("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
1408
+ /* @__PURE__ */ jsx32("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
1409
+ /* @__PURE__ */ jsx32(
1197
1410
  "input",
1198
1411
  {
1199
1412
  ref,
@@ -1208,7 +1421,7 @@ var ReportSearch = forwardRef(
1208
1421
  "aria-keyshortcuts": "/"
1209
1422
  }
1210
1423
  ),
1211
- showCounts ? /* @__PURE__ */ jsx31("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
1424
+ showCounts ? /* @__PURE__ */ jsx32("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
1212
1425
  ] });
1213
1426
  }
1214
1427
  );
@@ -1237,7 +1450,7 @@ function scrollToScenarioId(scenarioId, options = {}) {
1237
1450
  }
1238
1451
 
1239
1452
  // src/interactive/ReportFailureBanner.tsx
1240
- import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
1453
+ import { jsx as jsx33, jsxs as jsxs21 } from "react/jsx-runtime";
1241
1454
  function ReportFailureBanner({ failures }) {
1242
1455
  const first = failures[0];
1243
1456
  const jumpToFirst = useCallback2(() => {
@@ -1245,7 +1458,7 @@ function ReportFailureBanner({ failures }) {
1245
1458
  scrollToScenarioId(first.scenarioId);
1246
1459
  }, [first]);
1247
1460
  if (failures.length === 0) return null;
1248
- return /* @__PURE__ */ jsxs20(
1461
+ return /* @__PURE__ */ jsxs21(
1249
1462
  "div",
1250
1463
  {
1251
1464
  className: "es-failure-banner",
@@ -1253,13 +1466,13 @@ function ReportFailureBanner({ failures }) {
1253
1466
  "aria-live": "polite",
1254
1467
  "aria-label": "Failure summary",
1255
1468
  children: [
1256
- /* @__PURE__ */ jsxs20("span", { className: "es-failure-banner-text", children: [
1257
- /* @__PURE__ */ jsx32("strong", { children: failures.length }),
1469
+ /* @__PURE__ */ jsxs21("span", { className: "es-failure-banner-text", children: [
1470
+ /* @__PURE__ */ jsx33("strong", { children: failures.length }),
1258
1471
  " ",
1259
1472
  "failure",
1260
1473
  failures.length === 1 ? "" : "s"
1261
1474
  ] }),
1262
- /* @__PURE__ */ jsx32(
1475
+ /* @__PURE__ */ jsx33(
1263
1476
  "button",
1264
1477
  {
1265
1478
  type: "button",
@@ -1274,9 +1487,101 @@ function ReportFailureBanner({ failures }) {
1274
1487
  );
1275
1488
  }
1276
1489
 
1490
+ // src/interactive/ReportFreshness.tsx
1491
+ import { useEffect, useState as useState2 } from "react";
1492
+ import { Fragment as Fragment12, jsx as jsx34, jsxs as jsxs22 } from "react/jsx-runtime";
1493
+ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
1494
+ const [now, setNow] = useState2(nowMs);
1495
+ useEffect(() => {
1496
+ if (nowMs === void 0) setNow(Date.now());
1497
+ }, [nowMs]);
1498
+ if (lastRunMs === void 0 || now === void 0) return null;
1499
+ const age = formatRelativeAge(lastRunMs, now);
1500
+ const runLink = ciUrl ? /* @__PURE__ */ jsx34(
1501
+ "a",
1502
+ {
1503
+ href: ciUrl,
1504
+ target: "_blank",
1505
+ rel: "noreferrer noopener",
1506
+ className: "underline underline-offset-2",
1507
+ children: "View CI run"
1508
+ }
1509
+ ) : null;
1510
+ if (isReportStale(lastRunMs, staleAfterDays, now)) {
1511
+ return /* @__PURE__ */ jsxs22("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
1512
+ /* @__PURE__ */ jsxs22("span", { children: [
1513
+ /* @__PURE__ */ jsxs22("strong", { children: [
1514
+ "Last verified ",
1515
+ age,
1516
+ "."
1517
+ ] }),
1518
+ " This report may no longer match the current behavior \u2014 look for a newer run."
1519
+ ] }),
1520
+ runLink
1521
+ ] });
1522
+ }
1523
+ return /* @__PURE__ */ jsxs22("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
1524
+ "Verified ",
1525
+ age,
1526
+ runLink ? /* @__PURE__ */ jsxs22(Fragment12, { children: [
1527
+ " \xB7 ",
1528
+ runLink
1529
+ ] }) : null
1530
+ ] });
1531
+ }
1532
+
1533
+ // src/interactive/ReportLastRunDelta.tsx
1534
+ import { Fragment as Fragment13, jsx as jsx35, jsxs as jsxs23 } from "react/jsx-runtime";
1535
+ var MAX_LINKS = 5;
1536
+ function ScenarioLinks({ scenarios, tone }) {
1537
+ return /* @__PURE__ */ jsxs23(Fragment13, { children: [
1538
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs23("span", { children: [
1539
+ i > 0 ? ", " : "",
1540
+ /* @__PURE__ */ jsx35("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
1541
+ ] }, s.id)),
1542
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs23("span", { children: [
1543
+ " and ",
1544
+ scenarios.length - MAX_LINKS,
1545
+ " more"
1546
+ ] }) : null
1547
+ ] });
1548
+ }
1549
+ function ReportLastRunDelta({
1550
+ history: history2,
1551
+ report
1552
+ }) {
1553
+ if (!history2) return null;
1554
+ const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
1555
+ const delta = diffSinceLastRun(history2, scenarios);
1556
+ if (!delta) return null;
1557
+ const { newlyFailing, newlyPassing, added } = delta;
1558
+ if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
1559
+ return /* @__PURE__ */ jsx35("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
1560
+ }
1561
+ return /* @__PURE__ */ jsxs23("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
1562
+ /* @__PURE__ */ jsx35("span", { className: "font-medium text-foreground", children: "Since last run:" }),
1563
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs23("span", { className: "ml-2 text-fail", children: [
1564
+ newlyFailing.length,
1565
+ " newly failing (",
1566
+ /* @__PURE__ */ jsx35(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
1567
+ ")"
1568
+ ] }) : null,
1569
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs23("span", { className: "ml-2 text-pass", children: [
1570
+ newlyPassing.length,
1571
+ " fixed (",
1572
+ /* @__PURE__ */ jsx35(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
1573
+ ")"
1574
+ ] }) : null,
1575
+ added.length > 0 ? /* @__PURE__ */ jsxs23("span", { className: "ml-2", children: [
1576
+ added.length,
1577
+ " new"
1578
+ ] }) : null
1579
+ ] });
1580
+ }
1581
+
1277
1582
  // src/interactive/ReportShortcutsHelp.tsx
1278
- import { useEffect, useRef } from "react";
1279
- import { jsx as jsx33, jsxs as jsxs21 } from "react/jsx-runtime";
1583
+ import { useEffect as useEffect2, useRef } from "react";
1584
+ import { jsx as jsx36, jsxs as jsxs24 } from "react/jsx-runtime";
1280
1585
  var SHORTCUTS = [
1281
1586
  { keys: "/", description: "Focus search" },
1282
1587
  { keys: "f", description: "Jump to next failure" },
@@ -1286,13 +1591,13 @@ var SHORTCUTS = [
1286
1591
  ];
1287
1592
  function ReportShortcutsHelp({ open, onClose }) {
1288
1593
  const dialogRef = useRef(null);
1289
- useEffect(() => {
1594
+ useEffect2(() => {
1290
1595
  const dialog = dialogRef.current;
1291
1596
  if (!dialog) return;
1292
1597
  if (open && !dialog.open) dialog.showModal();
1293
1598
  else if (!open && dialog.open) dialog.close();
1294
1599
  }, [open]);
1295
- return /* @__PURE__ */ jsxs21(
1600
+ return /* @__PURE__ */ jsxs24(
1296
1601
  "dialog",
1297
1602
  {
1298
1603
  ref: dialogRef,
@@ -1300,19 +1605,19 @@ function ReportShortcutsHelp({ open, onClose }) {
1300
1605
  "aria-label": "Keyboard shortcuts",
1301
1606
  onClose,
1302
1607
  children: [
1303
- /* @__PURE__ */ jsx33("h2", { children: "Keyboard shortcuts" }),
1304
- /* @__PURE__ */ jsx33("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs21("div", { children: [
1305
- /* @__PURE__ */ jsx33("dt", { children: /* @__PURE__ */ jsx33("kbd", { children: s.keys }) }),
1306
- /* @__PURE__ */ jsx33("dd", { children: s.description })
1608
+ /* @__PURE__ */ jsx36("h2", { children: "Keyboard shortcuts" }),
1609
+ /* @__PURE__ */ jsx36("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs24("div", { children: [
1610
+ /* @__PURE__ */ jsx36("dt", { children: /* @__PURE__ */ jsx36("kbd", { children: s.keys }) }),
1611
+ /* @__PURE__ */ jsx36("dd", { children: s.description })
1307
1612
  ] }, s.keys)) }),
1308
- /* @__PURE__ */ jsx33("form", { method: "dialog", children: /* @__PURE__ */ jsx33("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
1613
+ /* @__PURE__ */ jsx36("form", { method: "dialog", children: /* @__PURE__ */ jsx36("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
1309
1614
  ]
1310
1615
  }
1311
1616
  );
1312
1617
  }
1313
1618
 
1314
1619
  // src/interactive/use-keyboard-shortcuts.ts
1315
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
1620
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
1316
1621
  function isEditableTarget(target) {
1317
1622
  if (!(target instanceof HTMLElement)) return false;
1318
1623
  const tag = target.tagName;
@@ -1323,7 +1628,7 @@ function isEditableTarget(target) {
1323
1628
  function useKeyboardShortcuts(handlers) {
1324
1629
  const ref = useRef2(handlers);
1325
1630
  ref.current = handlers;
1326
- useEffect2(() => {
1631
+ useEffect3(() => {
1327
1632
  function onKeyDown(e) {
1328
1633
  const h = ref.current;
1329
1634
  if (e.key === "Escape") {
@@ -1367,9 +1672,9 @@ function useKeyboardShortcuts(handlers) {
1367
1672
  }
1368
1673
 
1369
1674
  // src/interactive/use-deep-link-scroll.ts
1370
- import { useEffect as useEffect3 } from "react";
1675
+ import { useEffect as useEffect4 } from "react";
1371
1676
  function useDeepLinkScroll() {
1372
- useEffect3(() => {
1677
+ useEffect4(() => {
1373
1678
  function scrollToHash() {
1374
1679
  const hash = window.location.hash.replace(/^#/, "");
1375
1680
  if (!hash) return;
@@ -1478,7 +1783,7 @@ function listFailures(report) {
1478
1783
  }
1479
1784
 
1480
1785
  // src/interactive/ReportFilters.tsx
1481
- import { jsx as jsx34, jsxs as jsxs22 } from "react/jsx-runtime";
1786
+ import { jsx as jsx37, jsxs as jsxs25 } from "react/jsx-runtime";
1482
1787
  function ReportFilters({
1483
1788
  statuses,
1484
1789
  status,
@@ -1488,10 +1793,10 @@ function ReportFilters({
1488
1793
  onToggleTag
1489
1794
  }) {
1490
1795
  if (statuses.length === 0 && tags.length === 0) return null;
1491
- return /* @__PURE__ */ jsxs22("div", { className: "flex flex-col gap-2", children: [
1492
- statuses.length > 0 ? /* @__PURE__ */ jsx34("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
1796
+ return /* @__PURE__ */ jsxs25("div", { className: "flex flex-col gap-2", children: [
1797
+ statuses.length > 0 ? /* @__PURE__ */ jsx37("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
1493
1798
  const active = status === opt.key;
1494
- return /* @__PURE__ */ jsxs22(
1799
+ return /* @__PURE__ */ jsxs25(
1495
1800
  "button",
1496
1801
  {
1497
1802
  type: "button",
@@ -1503,22 +1808,22 @@ function ReportFilters({
1503
1808
  ),
1504
1809
  children: [
1505
1810
  opt.label,
1506
- /* @__PURE__ */ jsx34("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
1811
+ /* @__PURE__ */ jsx37("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
1507
1812
  ]
1508
1813
  },
1509
1814
  opt.key
1510
1815
  );
1511
1816
  }) }) : null,
1512
- tags.length > 0 ? /* @__PURE__ */ jsx34("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap gap-1.5", children: tags.map((tag) => {
1817
+ tags.length > 0 ? /* @__PURE__ */ jsx37("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap gap-1.5", children: tags.map((tag) => {
1513
1818
  const active = activeTags.includes(tag);
1514
- return /* @__PURE__ */ jsx34("li", { children: /* @__PURE__ */ jsx34(
1819
+ return /* @__PURE__ */ jsx37("li", { children: /* @__PURE__ */ jsx37(
1515
1820
  "button",
1516
1821
  {
1517
1822
  type: "button",
1518
1823
  "aria-pressed": active,
1519
1824
  onClick: () => onToggleTag(tag),
1520
1825
  className: "cursor-pointer",
1521
- children: /* @__PURE__ */ jsx34(Badge, { variant: active ? "default" : "tag", children: tag })
1826
+ children: /* @__PURE__ */ jsx37(Badge, { variant: active ? "default" : "tag", children: tag })
1522
1827
  }
1523
1828
  ) }, tag);
1524
1829
  }) }) : null
@@ -1526,8 +1831,8 @@ function ReportFilters({
1526
1831
  }
1527
1832
 
1528
1833
  // src/interactive/ReportToc.tsx
1529
- import { useEffect as useEffect4, useMemo as useMemo2, useState as useState2 } from "react";
1530
- import { jsx as jsx35, jsxs as jsxs23 } from "react/jsx-runtime";
1834
+ import { useEffect as useEffect5, useMemo as useMemo2, useState as useState3 } from "react";
1835
+ import { jsx as jsx38, jsxs as jsxs26 } from "react/jsx-runtime";
1531
1836
  var DOT = {
1532
1837
  passed: "text-pass",
1533
1838
  failed: "text-fail",
@@ -1536,12 +1841,12 @@ var DOT = {
1536
1841
  };
1537
1842
  function ReportToc() {
1538
1843
  const report = useReport();
1539
- const [activeId, setActiveId] = useState2(null);
1844
+ const [activeId, setActiveId] = useState3(null);
1540
1845
  const scenarioIds = useMemo2(
1541
1846
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
1542
1847
  [report]
1543
1848
  );
1544
- useEffect4(() => {
1849
+ useEffect5(() => {
1545
1850
  if (typeof IntersectionObserver === "undefined") return;
1546
1851
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
1547
1852
  if (els.length === 0) return;
@@ -1556,16 +1861,16 @@ function ReportToc() {
1556
1861
  return () => observer.disconnect();
1557
1862
  }, [scenarioIds]);
1558
1863
  if (report.features.length === 0) return null;
1559
- return /* @__PURE__ */ jsx35(
1864
+ return /* @__PURE__ */ jsx38(
1560
1865
  "nav",
1561
1866
  {
1562
1867
  "aria-label": "Table of contents",
1563
1868
  className: "sticky top-4 hidden h-fit w-56 shrink-0 self-start text-xs lg:block",
1564
- children: /* @__PURE__ */ jsx35("ul", { className: "flex flex-col gap-3", children: report.features.map((feature) => /* @__PURE__ */ jsxs23("li", { children: [
1565
- /* @__PURE__ */ jsx35("p", { className: "mb-1 truncate font-semibold text-foreground", children: feature.title }),
1566
- /* @__PURE__ */ jsx35("ul", { className: "flex flex-col", children: feature.scenarios.map((scenario) => {
1869
+ children: /* @__PURE__ */ jsx38("ul", { className: "flex flex-col gap-3", children: report.features.map((feature) => /* @__PURE__ */ jsxs26("li", { children: [
1870
+ /* @__PURE__ */ jsx38("p", { className: "mb-1 truncate font-semibold text-foreground", children: feature.title }),
1871
+ /* @__PURE__ */ jsx38("ul", { className: "flex flex-col", children: feature.scenarios.map((scenario) => {
1567
1872
  const active = activeId === scenario.id;
1568
- return /* @__PURE__ */ jsx35("li", { children: /* @__PURE__ */ jsxs23(
1873
+ return /* @__PURE__ */ jsx38("li", { children: /* @__PURE__ */ jsxs26(
1569
1874
  "a",
1570
1875
  {
1571
1876
  href: `#${scenario.id}`,
@@ -1579,8 +1884,8 @@ function ReportToc() {
1579
1884
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
1580
1885
  ),
1581
1886
  children: [
1582
- /* @__PURE__ */ jsx35("span", { "aria-hidden": true, className: cn("shrink-0", DOT[scenario.status]), children: "\u2022" }),
1583
- /* @__PURE__ */ jsx35("span", { className: "truncate", children: scenario.title })
1887
+ /* @__PURE__ */ jsx38("span", { "aria-hidden": true, className: cn("shrink-0", DOT[scenario.status]), children: "\u2022" }),
1888
+ /* @__PURE__ */ jsx38("span", { className: "truncate", children: scenario.title })
1584
1889
  ]
1585
1890
  }
1586
1891
  ) }, scenario.id);
@@ -1591,7 +1896,7 @@ function ReportToc() {
1591
1896
  }
1592
1897
 
1593
1898
  // src/interactive/use-theme.ts
1594
- import { useCallback as useCallback3, useEffect as useEffect5, useState as useState3 } from "react";
1899
+ import { useCallback as useCallback3, useEffect as useEffect6, useState as useState4 } from "react";
1595
1900
  var KEY = "es-theme";
1596
1901
  function applyTheme(theme) {
1597
1902
  if (typeof document !== "undefined") {
@@ -1612,8 +1917,8 @@ function initialTheme() {
1612
1917
  return "light";
1613
1918
  }
1614
1919
  function useTheme() {
1615
- const [theme, setTheme] = useState3("light");
1616
- useEffect5(() => {
1920
+ const [theme, setTheme] = useState4("light");
1921
+ useEffect6(() => {
1617
1922
  const t = initialTheme();
1618
1923
  setTheme(t);
1619
1924
  applyTheme(t);
@@ -1633,14 +1938,14 @@ function useTheme() {
1633
1938
  }
1634
1939
 
1635
1940
  // src/interactive/ReportInteractive.tsx
1636
- import { jsx as jsx36, jsxs as jsxs24 } from "react/jsx-runtime";
1941
+ import { jsx as jsx39, jsxs as jsxs27 } from "react/jsx-runtime";
1637
1942
  function ReportInteractive(props) {
1638
1943
  const { report, className, title, dataTheme } = props;
1639
1944
  const result = unwrapReport(report);
1640
1945
  if (!result.ok) {
1641
- return /* @__PURE__ */ jsx36(ReportErrorShell, { error: result.error, className, title, dataTheme });
1946
+ return /* @__PURE__ */ jsx39(ReportErrorShell, { error: result.error, className, title, dataTheme });
1642
1947
  }
1643
- return /* @__PURE__ */ jsx36(ReportInteractiveView, { ...props, report: result.data });
1948
+ return /* @__PURE__ */ jsx39(ReportInteractiveView, { ...props, report: result.data });
1644
1949
  }
1645
1950
  function ReportInteractiveView({
1646
1951
  report,
@@ -1649,13 +1954,15 @@ function ReportInteractiveView({
1649
1954
  className,
1650
1955
  title,
1651
1956
  dataTheme,
1652
- hideHeader = false
1957
+ hideHeader = false,
1958
+ staleAfterDays = 7,
1959
+ scenarioHistory
1653
1960
  }) {
1654
- const [query, setQuery] = useState4("");
1655
- const [statusFilter, setStatusFilter] = useState4("all");
1656
- const [activeTags, setActiveTags] = useState4([]);
1657
- const [detail, setDetail] = useState4("full");
1658
- const [helpOpen, setHelpOpen] = useState4(false);
1961
+ const [query, setQuery] = useState5("");
1962
+ const [statusFilter, setStatusFilter] = useState5("all");
1963
+ const [activeTags, setActiveTags] = useState5([]);
1964
+ const [detail, setDetail] = useState5("full");
1965
+ const [helpOpen, setHelpOpen] = useState5(false);
1659
1966
  const { theme, toggle: toggleTheme } = useTheme();
1660
1967
  const searchRef = useRef3(null);
1661
1968
  const deferredQuery = useDeferredValue(query);
@@ -1681,7 +1988,7 @@ function ReportInteractiveView({
1681
1988
  (tag) => setActiveTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]),
1682
1989
  []
1683
1990
  );
1684
- const [toast, setToast] = useState4(null);
1991
+ const [toast, setToast] = useState5(null);
1685
1992
  const toastTimer = useRef3(null);
1686
1993
  const showToast = useCallback4((msg) => {
1687
1994
  setToast(msg);
@@ -1743,13 +2050,13 @@ function ReportInteractiveView({
1743
2050
  const hasContent = filtered.features.length > 0;
1744
2051
  const totalScenarios = report.summary.total;
1745
2052
  const matchedScenarios = filtered.summary.total;
1746
- return /* @__PURE__ */ jsx36(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ jsx36(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx36(
2053
+ return /* @__PURE__ */ jsx39(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ jsx39(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx39(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx39(
1747
2054
  ReportRoot,
1748
2055
  {
1749
2056
  report: filtered,
1750
2057
  customRenderers,
1751
2058
  renderers,
1752
- children: /* @__PURE__ */ jsxs24(
2059
+ children: /* @__PURE__ */ jsxs27(
1753
2060
  "main",
1754
2061
  {
1755
2062
  className: cn("es-report", "es-report-interactive", className),
@@ -1758,10 +2065,19 @@ function ReportInteractiveView({
1758
2065
  "data-theme": dataTheme,
1759
2066
  "data-detail-level": detail,
1760
2067
  children: [
1761
- /* @__PURE__ */ jsxs24("header", { className: "es-report-header", children: [
1762
- hideHeader ? null : /* @__PURE__ */ jsx36(ReportTitleBlock, { title }),
1763
- /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-center gap-2", children: [
1764
- /* @__PURE__ */ jsx36(
2068
+ /* @__PURE__ */ jsxs27("header", { className: "es-report-header", children: [
2069
+ hideHeader ? null : /* @__PURE__ */ jsx39(ReportTitleBlock, { title }),
2070
+ /* @__PURE__ */ jsx39(
2071
+ ReportFreshness,
2072
+ {
2073
+ lastRunMs: reportLastRunMs(report),
2074
+ ciUrl: report.ci?.url,
2075
+ staleAfterDays
2076
+ }
2077
+ ),
2078
+ /* @__PURE__ */ jsx39(ReportLastRunDelta, { history: scenarioHistory, report }),
2079
+ /* @__PURE__ */ jsxs27("div", { className: "flex flex-wrap items-center gap-2", children: [
2080
+ /* @__PURE__ */ jsx39(
1765
2081
  ReportSearch,
1766
2082
  {
1767
2083
  ref: searchRef,
@@ -1771,8 +2087,8 @@ function ReportInteractiveView({
1771
2087
  totalCount: totalScenarios
1772
2088
  }
1773
2089
  ),
1774
- /* @__PURE__ */ jsxs24("div", { className: "flex shrink-0 gap-1", children: [
1775
- /* @__PURE__ */ jsx36(
2090
+ /* @__PURE__ */ jsxs27("div", { className: "flex shrink-0 gap-1", children: [
2091
+ /* @__PURE__ */ jsx39(
1776
2092
  "button",
1777
2093
  {
1778
2094
  type: "button",
@@ -1782,7 +2098,7 @@ function ReportInteractiveView({
1782
2098
  children: "Expand all"
1783
2099
  }
1784
2100
  ),
1785
- /* @__PURE__ */ jsx36(
2101
+ /* @__PURE__ */ jsx39(
1786
2102
  "button",
1787
2103
  {
1788
2104
  type: "button",
@@ -1792,7 +2108,7 @@ function ReportInteractiveView({
1792
2108
  children: "Collapse all"
1793
2109
  }
1794
2110
  ),
1795
- /* @__PURE__ */ jsx36(
2111
+ /* @__PURE__ */ jsx39(
1796
2112
  "button",
1797
2113
  {
1798
2114
  type: "button",
@@ -1802,7 +2118,7 @@ function ReportInteractiveView({
1802
2118
  children: detail === "full" ? "Hide docs" : "Show docs"
1803
2119
  }
1804
2120
  ),
1805
- /* @__PURE__ */ jsx36(
2121
+ /* @__PURE__ */ jsx39(
1806
2122
  "button",
1807
2123
  {
1808
2124
  type: "button",
@@ -1814,7 +2130,7 @@ function ReportInteractiveView({
1814
2130
  )
1815
2131
  ] })
1816
2132
  ] }),
1817
- /* @__PURE__ */ jsx36(
2133
+ /* @__PURE__ */ jsx39(
1818
2134
  ReportFilters,
1819
2135
  {
1820
2136
  statuses: statusOptions,
@@ -1826,12 +2142,12 @@ function ReportInteractiveView({
1826
2142
  }
1827
2143
  )
1828
2144
  ] }),
1829
- /* @__PURE__ */ jsx36(ReportFailureBanner, { failures }),
1830
- hasContent ? /* @__PURE__ */ jsxs24("div", { className: "flex gap-6", children: [
1831
- /* @__PURE__ */ jsx36(ReportToc, {}),
1832
- /* @__PURE__ */ jsx36("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx36(ReportFeatureList, {}) })
1833
- ] }) : /* @__PURE__ */ jsx36(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
1834
- /* @__PURE__ */ jsx36(
2145
+ /* @__PURE__ */ jsx39(ReportFailureBanner, { failures }),
2146
+ hasContent ? /* @__PURE__ */ jsxs27("div", { className: "flex gap-6", children: [
2147
+ /* @__PURE__ */ jsx39(ReportToc, {}),
2148
+ /* @__PURE__ */ jsx39("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx39(ReportFeatureList, {}) })
2149
+ ] }) : /* @__PURE__ */ jsx39(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
2150
+ /* @__PURE__ */ jsx39(
1835
2151
  "button",
1836
2152
  {
1837
2153
  type: "button",
@@ -1842,8 +2158,8 @@ function ReportInteractiveView({
1842
2158
  children: "?"
1843
2159
  }
1844
2160
  ),
1845
- /* @__PURE__ */ jsx36(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
1846
- toast ? /* @__PURE__ */ jsx36(
2161
+ /* @__PURE__ */ jsx39(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
2162
+ toast ? /* @__PURE__ */ jsx39(
1847
2163
  "div",
1848
2164
  {
1849
2165
  role: "status",
@@ -1856,18 +2172,18 @@ function ReportInteractiveView({
1856
2172
  }
1857
2173
  )
1858
2174
  }
1859
- ) }) });
2175
+ ) }) }) });
1860
2176
  }
1861
2177
 
1862
2178
  // src/interactive/ReportInteractiveIsland.tsx
1863
2179
  import { useMemo as useMemo4 } from "react";
1864
2180
 
1865
2181
  // src/interactive/island-renderers.tsx
1866
- import { createElement, useEffect as useEffect7, useState as useState6 } from "react";
2182
+ import { createElement, useEffect as useEffect8, useState as useState7 } from "react";
1867
2183
 
1868
2184
  // src/components/doc/MermaidView.tsx
1869
- import { useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
1870
- import { jsx as jsx37, jsxs as jsxs25 } from "react/jsx-runtime";
2185
+ import { useEffect as useEffect7, useRef as useRef4, useState as useState6 } from "react";
2186
+ import { jsx as jsx40, jsxs as jsxs28 } from "react/jsx-runtime";
1871
2187
  function prefersDark(el) {
1872
2188
  const themed = el?.closest("[data-theme]");
1873
2189
  if (themed?.dataset.theme === "dark") return true;
@@ -1876,8 +2192,8 @@ function prefersDark(el) {
1876
2192
  }
1877
2193
  function MermaidView({ entry, load }) {
1878
2194
  const ref = useRef4(null);
1879
- const [svg, setSvg] = useState5(null);
1880
- useEffect6(() => {
2195
+ const [svg, setSvg] = useState6(null);
2196
+ useEffect7(() => {
1881
2197
  let cancelled = false;
1882
2198
  const draw = async () => {
1883
2199
  try {
@@ -1904,9 +2220,9 @@ function MermaidView({ entry, load }) {
1904
2220
  };
1905
2221
  }, [entry.code, load]);
1906
2222
  if (svg) {
1907
- return /* @__PURE__ */ jsxs25("figure", { ref, className: "my-3", children: [
1908
- entry.title ? /* @__PURE__ */ jsx37("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1909
- /* @__PURE__ */ jsx37(
2223
+ return /* @__PURE__ */ jsxs28("figure", { ref, className: "my-3", children: [
2224
+ entry.title ? /* @__PURE__ */ jsx40("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
2225
+ /* @__PURE__ */ jsx40(
1910
2226
  "div",
1911
2227
  {
1912
2228
  role: "img",
@@ -1917,11 +2233,11 @@ function MermaidView({ entry, load }) {
1917
2233
  )
1918
2234
  ] });
1919
2235
  }
1920
- return /* @__PURE__ */ jsx37(MermaidSource, { entry, ref });
2236
+ return /* @__PURE__ */ jsx40(MermaidSource, { entry, ref });
1921
2237
  }
1922
2238
 
1923
2239
  // src/interactive/island-renderers.tsx
1924
- import { jsx as jsx38 } from "react/jsx-runtime";
2240
+ import { jsx as jsx41 } from "react/jsx-runtime";
1925
2241
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
1926
2242
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
1927
2243
  function cachedCdnModule(url) {
@@ -1938,8 +2254,8 @@ function cachedCdnModule(url) {
1938
2254
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
1939
2255
  var loadHljs = cachedCdnModule(HLJS_ESM);
1940
2256
  function CdnHighlightedCode({ entry }) {
1941
- const [html, setHtml] = useState6(null);
1942
- useEffect7(() => {
2257
+ const [html, setHtml] = useState7(null);
2258
+ useEffect8(() => {
1943
2259
  let cancelled = false;
1944
2260
  void (async () => {
1945
2261
  try {
@@ -1955,13 +2271,13 @@ function CdnHighlightedCode({ entry }) {
1955
2271
  cancelled = true;
1956
2272
  };
1957
2273
  }, [entry.content, entry.lang]);
1958
- return /* @__PURE__ */ jsx38(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx38(
2274
+ return /* @__PURE__ */ jsx41(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx41(
1959
2275
  "code",
1960
2276
  {
1961
2277
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
1962
2278
  dangerouslySetInnerHTML: { __html: html }
1963
2279
  }
1964
- ) : /* @__PURE__ */ jsx38("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
2280
+ ) : /* @__PURE__ */ jsx41("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
1965
2281
  }
1966
2282
  function buildIslandRenderers(opts) {
1967
2283
  const renderers = {};
@@ -1975,7 +2291,7 @@ function buildIslandRenderers(opts) {
1975
2291
  }
1976
2292
 
1977
2293
  // src/interactive/ReportInteractiveIsland.tsx
1978
- import { jsx as jsx39 } from "react/jsx-runtime";
2294
+ import { jsx as jsx42 } from "react/jsx-runtime";
1979
2295
  function ReportInteractiveIsland({
1980
2296
  report,
1981
2297
  title,
@@ -1988,7 +2304,7 @@ function ReportInteractiveIsland({
1988
2304
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
1989
2305
  [syntaxHighlighting, mermaid]
1990
2306
  );
1991
- return /* @__PURE__ */ jsx39(
2307
+ return /* @__PURE__ */ jsx42(
1992
2308
  ReportInteractive,
1993
2309
  {
1994
2310
  report,
@@ -2001,6 +2317,7 @@ function ReportInteractiveIsland({
2001
2317
  }
2002
2318
  export {
2003
2319
  ReportFailureBanner,
2320
+ ReportFreshness,
2004
2321
  ReportInteractive,
2005
2322
  ReportInteractiveIsland,
2006
2323
  ReportSearch,