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.
package/dist/ssr.cjs CHANGED
@@ -23,7 +23,7 @@ __export(ssr_entry_exports, {
23
23
  renderReportToHtml: () => renderReportToHtml
24
24
  });
25
25
  module.exports = __toCommonJS(ssr_entry_exports);
26
- var import_react7 = require("react");
26
+ var import_react8 = require("react");
27
27
  var import_server = require("react-dom/server");
28
28
 
29
29
  // src/result.ts
@@ -75,11 +75,23 @@ function useReport() {
75
75
  return ctx.report;
76
76
  }
77
77
 
78
- // src/components/ReportScenario.tsx
79
- var import_duration3 = require("executable-stories-core/utils/duration");
80
-
81
- // src/components/ReportSteps.tsx
82
- var import_duration = require("executable-stories-core/utils/duration");
78
+ // ../executable-stories-core/dist/utils/duration.js
79
+ function formatDuration(ms) {
80
+ if (ms < 1e3) {
81
+ return `${Math.round(ms)} ms`;
82
+ }
83
+ if (ms < 6e4) {
84
+ return `${(ms / 1e3).toFixed(2)} s`;
85
+ }
86
+ const minutes = Math.floor(ms / 6e4);
87
+ const seconds = Math.floor(ms % 6e4 / 1e3);
88
+ if (minutes < 60) {
89
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
90
+ }
91
+ const hours = Math.floor(minutes / 60);
92
+ const remainingMinutes = minutes % 60;
93
+ return `${hours}h ${remainingMinutes}m`;
94
+ }
83
95
 
84
96
  // src/components/doc/DocNote.tsx
85
97
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -233,6 +245,15 @@ function safeUrl(value) {
233
245
  if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
234
246
  return trimmed;
235
247
  }
248
+ function safeImageUrl(value) {
249
+ if (!value) return void 0;
250
+ const trimmed = value.trim();
251
+ if (/^data:image\//i.test(trimmed)) return trimmed;
252
+ return safeUrl(trimmed);
253
+ }
254
+ function isLocalFsPath(value) {
255
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
256
+ }
236
257
 
237
258
  // src/components/doc/DocSection.tsx
238
259
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -288,11 +309,19 @@ function DocMermaid({ entry }) {
288
309
  // src/components/doc/DocScreenshot.tsx
289
310
  var import_jsx_runtime10 = require("react/jsx-runtime");
290
311
  function DocScreenshot({ entry }) {
312
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
313
+ if (!src) {
314
+ 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: [
315
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "font-medium", children: "Screenshot unavailable" }),
316
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
317
+ entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
318
+ ] });
319
+ }
291
320
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("figure", { className: "my-3", children: [
292
321
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
293
322
  "img",
294
323
  {
295
- src: entry.path,
324
+ src,
296
325
  alt: entry.alt ?? "",
297
326
  loading: "lazy",
298
327
  className: "max-w-full rounded-md border border-border"
@@ -305,6 +334,13 @@ function DocScreenshot({ entry }) {
305
334
  // src/components/doc/DocVideo.tsx
306
335
  var import_jsx_runtime11 = require("react/jsx-runtime");
307
336
  function DocVideo({ entry }) {
337
+ if (isLocalFsPath(entry.path)) {
338
+ 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: [
339
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "font-medium", children: "Video unavailable" }),
340
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
341
+ entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
342
+ ] });
343
+ }
308
344
  const src = safeUrl(entry.path);
309
345
  const poster = safeUrl(entry.poster);
310
346
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { className: "my-3", children: [
@@ -480,7 +516,7 @@ function ReportStepItem({ step }) {
480
516
  }
481
517
  ),
482
518
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
483
- 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
519
+ 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
484
520
  ] }),
485
521
  step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
486
522
  "pre",
@@ -538,9 +574,6 @@ function ReportAttachments({ attachments }) {
538
574
  );
539
575
  }
540
576
 
541
- // src/components/ReportTrace.tsx
542
- var import_duration2 = require("executable-stories-core/utils/duration");
543
-
544
577
  // src/lib/trace.ts
545
578
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
546
579
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -635,7 +668,7 @@ function ReportTrace({ spans }) {
635
668
  " span",
636
669
  rows.length === 1 ? "" : "s",
637
670
  ", ",
638
- (0, import_duration2.formatDuration)(totalMs),
671
+ formatDuration(totalMs),
639
672
  ")"
640
673
  ] }),
641
674
  /* @__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) => {
@@ -658,33 +691,128 @@ function ReportTrace({ spans }) {
658
691
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
659
692
  }
660
693
  ) }),
661
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: (0, import_duration2.formatDuration)(row.durationMs) })
694
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
662
695
  ] }, `${row.spanId}-${i}`);
663
696
  }) })
664
697
  ] });
665
698
  }
666
699
 
667
- // src/interactive/collapse-context.ts
700
+ // src/interactive/scenario-history-context.ts
668
701
  var import_react5 = require("react");
669
- var CollapseContext = (0, import_react5.createContext)(null);
702
+ var ScenarioHistoryContext = (0, import_react5.createContext)(null);
703
+ var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
704
+ function useScenarioHistory() {
705
+ return (0, import_react5.useContext)(ScenarioHistoryContext);
706
+ }
707
+
708
+ // src/lib/run-history.ts
709
+ function currentStreak(entries) {
710
+ const last = entries[entries.length - 1];
711
+ if (!last) return void 0;
712
+ let count = 0;
713
+ for (let i = entries.length - 1; i >= 0 && entries[i].status === last.status; i--) count++;
714
+ return { status: last.status, count };
715
+ }
716
+ var STREAK_VERB = {
717
+ passed: "Passing",
718
+ failed: "Failing",
719
+ skipped: "Skipped",
720
+ pending: "Pending"
721
+ };
722
+ function describeRunHistory(entries) {
723
+ const passed = entries.filter((e) => e.status === "passed").length;
724
+ const base = `${passed}/${entries.length} runs passed`;
725
+ const streak = currentStreak(entries);
726
+ if (!streak || entries.length < 2) return base;
727
+ return `${base} \xB7 ${STREAK_VERB[streak.status]} for the last ${streak.count === 1 ? "run" : `${streak.count} runs`}`;
728
+ }
729
+ var MIN_FLAKINESS_SAMPLES = 3;
730
+ function flakinessOf(entries) {
731
+ const countable = entries.filter((e) => e.status === "passed" || e.status === "failed");
732
+ if (countable.length < MIN_FLAKINESS_SAMPLES) return "stable";
733
+ let transitions = 0;
734
+ for (let i = 1; i < countable.length; i++) {
735
+ if (countable[i].status !== countable[i - 1].status) transitions++;
736
+ }
737
+ const transitionScore = transitions / (countable.length - 1);
738
+ const failureRate = countable.filter((e) => e.status === "failed").length / countable.length;
739
+ if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) return "flaky";
740
+ if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
741
+ return "stable";
742
+ }
743
+
744
+ // src/components/ScenarioRunHistory.tsx
745
+ var import_jsx_runtime20 = require("react/jsx-runtime");
746
+ var MAX_DOTS = 10;
747
+ var DOT_COLOR = {
748
+ passed: "bg-pass",
749
+ failed: "bg-fail",
750
+ skipped: "bg-skip",
751
+ pending: "bg-pend"
752
+ };
753
+ function dotTitle(e) {
754
+ const parts = [new Date(e.timestamp).toLocaleString(), e.status];
755
+ if (e.commitSha) parts.push(e.commitSha.slice(0, 8));
756
+ return parts.join(" \xB7 ");
757
+ }
758
+ function ScenarioRunHistory({ scenarioId }) {
759
+ const history = useScenarioHistory();
760
+ const entries = history?.[scenarioId];
761
+ if (!entries || entries.length < 2) return null;
762
+ const recent = entries.slice(-MAX_DOTS);
763
+ const flaky = flakinessOf(recent) === "flaky";
764
+ const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
765
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
766
+ flaky ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
767
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
768
+ "span",
769
+ {
770
+ role: "img",
771
+ "aria-label": `Run history: ${summary}`,
772
+ title: summary,
773
+ className: "flex shrink-0 items-center gap-[3px]",
774
+ children: recent.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
775
+ "span",
776
+ {
777
+ "aria-hidden": true,
778
+ title: dotTitle(e),
779
+ className: cn(
780
+ "inline-block size-1.5 rounded-full",
781
+ DOT_COLOR[e.status],
782
+ // The newest run (rightmost dot) is the one the card's status
783
+ // badge already describes; ring it so the timeline reads as
784
+ // "…leading up to now".
785
+ i === recent.length - 1 && "ring-1 ring-border ring-offset-1 ring-offset-card"
786
+ )
787
+ },
788
+ `${e.timestamp}-${i}`
789
+ ))
790
+ }
791
+ )
792
+ ] });
793
+ }
794
+
795
+ // src/interactive/collapse-context.ts
796
+ var import_react6 = require("react");
797
+ var CollapseContext = (0, import_react6.createContext)(null);
670
798
  var CollapseProvider = CollapseContext.Provider;
671
799
  function useCollapse() {
672
- return (0, import_react5.useContext)(CollapseContext);
800
+ return (0, import_react6.useContext)(CollapseContext);
673
801
  }
674
802
 
675
803
  // src/interactive/scenario-actions.tsx
676
- var import_react6 = require("react");
677
- var ScenarioActionsContext = (0, import_react6.createContext)(null);
804
+ var import_react7 = require("react");
805
+ var ScenarioActionsContext = (0, import_react7.createContext)(null);
678
806
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
679
807
  function useScenarioActions() {
680
- return (0, import_react6.useContext)(ScenarioActionsContext);
808
+ return (0, import_react7.useContext)(ScenarioActionsContext);
681
809
  }
682
810
 
683
811
  // src/components/ui/card.tsx
684
812
  var React2 = require("react");
685
- var import_jsx_runtime20 = require("react/jsx-runtime");
813
+ var import_jsx_runtime21 = require("react/jsx-runtime");
686
814
  function Card({ className, ...props }) {
687
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
815
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
688
816
  "div",
689
817
  {
690
818
  "data-slot": "card",
@@ -697,7 +825,7 @@ function Card({ className, ...props }) {
697
825
  );
698
826
  }
699
827
  function CardHeader({ className, ...props }) {
700
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
828
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
701
829
  "div",
702
830
  {
703
831
  "data-slot": "card-header",
@@ -710,7 +838,7 @@ function CardHeader({ className, ...props }) {
710
838
  );
711
839
  }
712
840
  function CardContent({ className, ...props }) {
713
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
841
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
714
842
  "div",
715
843
  {
716
844
  "data-slot": "card-content",
@@ -721,7 +849,7 @@ function CardContent({ className, ...props }) {
721
849
  }
722
850
 
723
851
  // src/components/ReportScenario.tsx
724
- var import_jsx_runtime21 = require("react/jsx-runtime");
852
+ var import_jsx_runtime22 = require("react/jsx-runtime");
725
853
  var STATUS_LABEL = {
726
854
  passed: "Passed",
727
855
  failed: "Failed",
@@ -749,12 +877,12 @@ var STATUS_BADGE = {
749
877
  function ReportScenario({ scenario, hideTitle = false }) {
750
878
  const titleId = `${scenario.id}-title`;
751
879
  const bodyId = `${scenario.id}-body`;
752
- const label = STATUS_LABEL[scenario.status];
880
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
753
881
  const collapse = useCollapse();
754
882
  const collapsible = collapse !== null && !hideTitle;
755
883
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
756
884
  const actions = useScenarioActions();
757
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
885
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
758
886
  Card,
759
887
  {
760
888
  id: scenario.id,
@@ -762,10 +890,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
762
890
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
763
891
  className: "gap-0 py-0",
764
892
  children: [
765
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardHeader, { className: "px-4 py-3", children: [
766
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
767
- hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
768
- collapsible ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
893
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(CardHeader, { className: "px-4 py-3", children: [
894
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
895
+ hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
896
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
769
897
  "button",
770
898
  {
771
899
  type: "button",
@@ -774,17 +902,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
774
902
  "aria-controls": bodyId,
775
903
  "aria-label": `Toggle ${scenario.title}`,
776
904
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
777
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
905
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
778
906
  }
779
907
  ) : null,
780
- /* @__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] }),
781
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: scenario.title })
908
+ /* @__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] }),
909
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: scenario.title })
782
910
  ] }),
783
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
784
- 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,
785
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
786
- actions ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-0.5", children: [
787
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
911
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
912
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
913
+ 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,
914
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
915
+ actions ? /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-0.5", children: [
916
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
788
917
  "button",
789
918
  {
790
919
  type: "button",
@@ -795,7 +924,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
795
924
  children: "link"
796
925
  }
797
926
  ),
798
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
927
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
799
928
  "button",
800
929
  {
801
930
  type: "button",
@@ -806,7 +935,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
806
935
  children: "md"
807
936
  }
808
937
  ),
809
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
938
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
810
939
  "button",
811
940
  {
812
941
  type: "button",
@@ -820,13 +949,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
820
949
  ] }) : null
821
950
  ] })
822
951
  ] }),
823
- 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: [
824
- 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}`)),
825
- (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}`))
952
+ 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: [
953
+ 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}`)),
954
+ (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}`))
826
955
  ] }) : null
827
956
  ] }),
828
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
829
- scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
957
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
958
+ scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
830
959
  "pre",
831
960
  {
832
961
  role: "alert",
@@ -834,10 +963,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
834
963
  children: scenario.errorMessage
835
964
  }
836
965
  ) : null,
837
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportSteps, { scenario }),
838
- 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,
839
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportAttachments, { attachments: scenario.attachments }),
840
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ReportTrace, { spans: scenario.otelSpans })
966
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportSteps, { scenario }),
967
+ 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,
968
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportAttachments, { attachments: scenario.attachments }),
969
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ReportTrace, { spans: scenario.otelSpans })
841
970
  ] })
842
971
  ]
843
972
  }
@@ -845,13 +974,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
845
974
  }
846
975
 
847
976
  // src/components/ReportScenarioList.tsx
848
- var import_jsx_runtime22 = require("react/jsx-runtime");
977
+ var import_jsx_runtime23 = require("react/jsx-runtime");
849
978
  function ReportScenarioList({ feature }) {
850
- 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)) });
979
+ 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)) });
851
980
  }
852
981
 
853
982
  // src/components/ReportFeature.tsx
854
- var import_jsx_runtime23 = require("react/jsx-runtime");
983
+ var import_jsx_runtime24 = require("react/jsx-runtime");
855
984
  function ReportFeature({ feature }) {
856
985
  const titleId = `${feature.id}-title`;
857
986
  const bodyId = `${feature.id}-body`;
@@ -859,7 +988,7 @@ function ReportFeature({ feature }) {
859
988
  const skipped = s.skipped + s.pending;
860
989
  const collapse = useCollapse();
861
990
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
862
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
991
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
863
992
  "section",
864
993
  {
865
994
  id: feature.id,
@@ -867,9 +996,9 @@ function ReportFeature({ feature }) {
867
996
  "aria-labelledby": titleId,
868
997
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
869
998
  children: [
870
- /* @__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: [
871
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
872
- collapse ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
999
+ /* @__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: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex min-w-0 items-center gap-2", children: [
1001
+ collapse ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
873
1002
  "button",
874
1003
  {
875
1004
  type: "button",
@@ -878,30 +1007,30 @@ function ReportFeature({ feature }) {
878
1007
  "aria-controls": bodyId,
879
1008
  "aria-label": `Toggle ${feature.title}`,
880
1009
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
881
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1010
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
882
1011
  }
883
1012
  ) : null,
884
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "min-w-0", children: [
885
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
886
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
1013
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "min-w-0", children: [
1014
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
1015
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
887
1016
  ] })
888
1017
  ] }),
889
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1018
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
890
1019
  "div",
891
1020
  {
892
1021
  role: "img",
893
1022
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
894
1023
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
895
1024
  children: [
896
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-pass", children: [
1025
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-pass", children: [
897
1026
  "\u2713",
898
1027
  s.passed
899
1028
  ] }),
900
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-fail", children: [
1029
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-fail", children: [
901
1030
  "\u2717",
902
1031
  s.failed
903
1032
  ] }),
904
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { "aria-hidden": true, className: "text-skip", children: [
1033
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { "aria-hidden": true, className: "text-skip", children: [
905
1034
  "\u25CB",
906
1035
  skipped
907
1036
  ] })
@@ -909,24 +1038,24 @@ function ReportFeature({ feature }) {
909
1038
  }
910
1039
  )
911
1040
  ] }),
912
- /* @__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 }) })
1041
+ /* @__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 }) })
913
1042
  ]
914
1043
  }
915
1044
  );
916
1045
  }
917
1046
 
918
1047
  // src/components/ReportFeatureList.tsx
919
- var import_jsx_runtime24 = require("react/jsx-runtime");
1048
+ var import_jsx_runtime25 = require("react/jsx-runtime");
920
1049
  function ReportFeatureList() {
921
1050
  const report = useReport();
922
- 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)) });
1051
+ 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)) });
923
1052
  }
924
1053
 
925
1054
  // src/components/ui/empty.tsx
926
1055
  var import_class_variance_authority2 = require("class-variance-authority");
927
- var import_jsx_runtime25 = require("react/jsx-runtime");
1056
+ var import_jsx_runtime26 = require("react/jsx-runtime");
928
1057
  function Empty({ className, ...props }) {
929
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1058
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
930
1059
  "div",
931
1060
  {
932
1061
  "data-slot": "empty",
@@ -939,7 +1068,7 @@ function Empty({ className, ...props }) {
939
1068
  );
940
1069
  }
941
1070
  function EmptyHeader({ className, ...props }) {
942
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1071
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
943
1072
  "div",
944
1073
  {
945
1074
  "data-slot": "empty-header",
@@ -966,7 +1095,7 @@ var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
966
1095
  }
967
1096
  );
968
1097
  function EmptyTitle({ className, ...props }) {
969
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1098
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
970
1099
  "div",
971
1100
  {
972
1101
  "data-slot": "empty-title",
@@ -977,16 +1106,16 @@ function EmptyTitle({ className, ...props }) {
977
1106
  }
978
1107
 
979
1108
  // src/components/ReportEmpty.tsx
980
- var import_jsx_runtime26 = require("react/jsx-runtime");
1109
+ var import_jsx_runtime27 = require("react/jsx-runtime");
981
1110
  function ReportEmpty({ message }) {
982
- 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." }) }) });
1111
+ 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." }) }) });
983
1112
  }
984
1113
 
985
1114
  // src/components/ReportSummary.tsx
986
- var import_jsx_runtime27 = require("react/jsx-runtime");
1115
+ var import_jsx_runtime28 = require("react/jsx-runtime");
987
1116
  function ReportSummary({ className }) {
988
1117
  const report = useReport();
989
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1118
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
990
1119
  ReportSummaryView,
991
1120
  {
992
1121
  summary: report.summary,
@@ -996,32 +1125,32 @@ function ReportSummary({ className }) {
996
1125
  );
997
1126
  }
998
1127
  function ReportSummaryView({ summary, className, ariaLabel }) {
999
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1000
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { children: [
1001
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("strong", { className: "font-semibold text-foreground", children: summary.total }),
1128
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1129
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { children: [
1130
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("strong", { className: "font-semibold text-foreground", children: summary.total }),
1002
1131
  " scenario",
1003
1132
  summary.total === 1 ? "" : "s"
1004
1133
  ] }),
1005
1134
  " \xB7 ",
1006
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "passed", className: "text-pass", children: [
1135
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "passed", className: "text-pass", children: [
1007
1136
  summary.passed,
1008
1137
  " passed"
1009
1138
  ] }),
1010
1139
  " \xB7 ",
1011
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "failed", className: "text-fail", children: [
1140
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "failed", className: "text-fail", children: [
1012
1141
  summary.failed,
1013
1142
  " failed"
1014
1143
  ] }),
1015
- summary.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1144
+ summary.skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1016
1145
  " \xB7 ",
1017
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "skipped", className: "text-skip", children: [
1146
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "skipped", className: "text-skip", children: [
1018
1147
  summary.skipped,
1019
1148
  " skipped"
1020
1149
  ] })
1021
1150
  ] }) : null,
1022
- summary.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
1151
+ summary.pending > 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
1023
1152
  " \xB7 ",
1024
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("span", { "data-status": "pending", className: "text-pend", children: [
1153
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { "data-status": "pending", className: "text-pend", children: [
1025
1154
  summary.pending,
1026
1155
  " pending"
1027
1156
  ] })
@@ -1029,69 +1158,116 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1029
1158
  ] });
1030
1159
  }
1031
1160
 
1161
+ // src/lib/provenance.ts
1162
+ var CI_DISPLAY_NAMES = {
1163
+ github: "GitHub Actions",
1164
+ gitlab: "GitLab CI",
1165
+ circleci: "CircleCI",
1166
+ jenkins: "Jenkins",
1167
+ azure: "Azure DevOps",
1168
+ buildkite: "Buildkite",
1169
+ travis: "Travis CI",
1170
+ ci: "CI"
1171
+ };
1172
+ function ciDisplayName(name) {
1173
+ return CI_DISPLAY_NAMES[name] ?? name;
1174
+ }
1175
+ function commitUrl(ci, sha) {
1176
+ const commitSha = ci?.commitSha ?? sha;
1177
+ if (!ci?.url || !commitSha) return void 0;
1178
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1179
+ if (github) return `${github[1]}/commit/${commitSha}`;
1180
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1181
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1182
+ return void 0;
1183
+ }
1184
+ function prUrl(ci) {
1185
+ if (!ci?.url || !ci.prNumber) return void 0;
1186
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1187
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1188
+ return void 0;
1189
+ }
1190
+
1032
1191
  // src/components/ReportMeta.tsx
1033
- var import_duration4 = require("executable-stories-core/utils/duration");
1034
- var import_jsx_runtime28 = require("react/jsx-runtime");
1192
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1035
1193
  function ReportMeta() {
1036
1194
  const report = useReport();
1037
1195
  const items = [];
1196
+ const linkClass = "text-primary underline underline-offset-2";
1038
1197
  if (report.startedAtMs) {
1039
1198
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1040
1199
  }
1041
1200
  if (report.durationMs) {
1042
- items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1201
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1043
1202
  }
1044
1203
  if (report.packageVersion) {
1045
1204
  items.push({ label: "Version", value: report.packageVersion });
1046
1205
  }
1047
- if (report.gitSha) {
1048
- items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1206
+ if (report.ci?.branch) {
1207
+ items.push({ label: "Branch", value: report.ci.branch });
1208
+ }
1209
+ const sha = report.ci?.commitSha ?? report.gitSha;
1210
+ if (sha) {
1211
+ const url = commitUrl(report.ci, report.gitSha);
1212
+ const short = sha.slice(0, 8);
1213
+ items.push({
1214
+ label: "Commit",
1215
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1216
+ });
1049
1217
  }
1050
1218
  if (report.ci) {
1051
1219
  items.push({
1052
1220
  label: "CI",
1053
- 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: [
1054
- report.ci.name,
1221
+ value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1222
+ ciDisplayName(report.ci.name),
1055
1223
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1056
- ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1224
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1225
+ });
1226
+ }
1227
+ if (report.ci?.prNumber) {
1228
+ const url = prUrl(report.ci);
1229
+ const label = `#${report.ci.prNumber}`;
1230
+ items.push({
1231
+ label: "PR",
1232
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1057
1233
  });
1058
1234
  }
1059
1235
  if (items.length === 0) return null;
1060
- 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: [
1061
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dt", { className: "font-medium", children: item.label }),
1062
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1236
+ 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: [
1237
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dt", { className: "font-medium", children: item.label }),
1238
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1063
1239
  ] }, item.label)) });
1064
1240
  }
1065
1241
 
1066
1242
  // src/components/ReportSchemaError.tsx
1067
- var import_jsx_runtime29 = require("react/jsx-runtime");
1243
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1068
1244
  function ReportSchemaError({ error }) {
1069
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1070
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("strong", { children: "Report could not be displayed." }) }),
1071
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { children: error.message }),
1072
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { children: [
1245
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1246
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("strong", { children: "Report could not be displayed." }) }),
1247
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { children: error.message }),
1248
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("p", { children: [
1073
1249
  "The report bundle is newer than this version of ",
1074
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("code", { children: "executable-stories-react" }),
1250
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("code", { children: "executable-stories-react" }),
1075
1251
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1076
1252
  ] }) : null,
1077
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("details", { children: [
1078
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("summary", { children: [
1253
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("details", { children: [
1254
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("summary", { children: [
1079
1255
  error.issues.length,
1080
1256
  " validation issue",
1081
1257
  error.issues.length === 1 ? "" : "s"
1082
1258
  ] }),
1083
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1259
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1084
1260
  ] }) : null
1085
1261
  ] });
1086
1262
  }
1087
1263
 
1088
1264
  // src/components/ReportShell.tsx
1089
- var import_jsx_runtime30 = require("react/jsx-runtime");
1265
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1090
1266
  function ReportTitleBlock({ title }) {
1091
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1092
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h1", { children: title ?? "Story Report" }),
1093
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportSummary, {}),
1094
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportMeta, {})
1267
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(import_jsx_runtime31.Fragment, { children: [
1268
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h1", { children: title ?? "Story Report" }),
1269
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportSummary, {}),
1270
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportMeta, {})
1095
1271
  ] });
1096
1272
  }
1097
1273
  function ReportErrorShell({
@@ -1100,28 +1276,29 @@ function ReportErrorShell({
1100
1276
  title,
1101
1277
  dataTheme
1102
1278
  }) {
1103
- 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 }) });
1279
+ 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 }) });
1104
1280
  }
1105
1281
 
1106
1282
  // src/components/Report.tsx
1107
- var import_jsx_runtime31 = require("react/jsx-runtime");
1283
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1108
1284
  function Report(props) {
1109
1285
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1110
1286
  const result = unwrapReport(report);
1111
1287
  if (!result.ok) {
1112
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1288
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
1113
1289
  }
1114
1290
  const data = result.data;
1115
1291
  const hasContent = data.features.length > 0;
1116
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1117
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportTitleBlock, { title }) }),
1118
- hasContent ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportFeatureList, {}) : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportEmpty, {})
1292
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1293
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("header", { className: "es-report-header", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportTitleBlock, { title }) }),
1294
+ hasContent ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportFeatureList, {}) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ReportEmpty, {})
1119
1295
  ] }) });
1120
1296
  }
1121
1297
 
1122
1298
  // src/ssr-entry.ts
1123
1299
  var ROOT_ID = "es-report-root";
1124
1300
  var DATA_ID = "es-report-data";
1301
+ var HISTORY_ID = "es-report-history";
1125
1302
  function escapeJsonForScript(json) {
1126
1303
  return json.replace(/[<\u2028\u2029]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
1127
1304
  }
@@ -1170,19 +1347,22 @@ function renderReportToHtml(report, options = {}) {
1170
1347
  lang = "en",
1171
1348
  syntaxHighlighting = true,
1172
1349
  mermaid = true,
1173
- islandScript = ""
1350
+ islandScript = "",
1351
+ staleAfterDays = 7,
1352
+ scenarioHistory
1174
1353
  } = options;
1175
1354
  const markup = (0, import_server.renderToStaticMarkup)(
1176
- (0, import_react7.createElement)(Report, { report, title })
1355
+ (0, import_react8.createElement)(Report, { report, title })
1177
1356
  );
1178
1357
  const cdn = cdnAssets(syntaxHighlighting, mermaid);
1179
1358
  const parsed = unwrapReport(report);
1180
1359
  const rawReport = parsed.ok ? parsed.data : void 0;
1181
1360
  const interactive = islandScript.length > 0 && rawReport !== void 0;
1182
1361
  const dataScript = interactive ? `<script type="application/json" id="${DATA_ID}">${escapeJsonForScript(JSON.stringify(rawReport))}</script>` : "";
1362
+ const historyScript = interactive && scenarioHistory && Object.keys(scenarioHistory).length > 0 ? `<script type="application/json" id="${HISTORY_ID}">${escapeJsonForScript(JSON.stringify(scenarioHistory))}</script>` : "";
1183
1363
  const islandTag = interactive ? `<script>${islandScript}</script>` : "";
1184
1364
  const cdnBody = interactive ? "" : cdn.body;
1185
- const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}"` : "";
1365
+ const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}" data-es-stale-days="${Number.isFinite(staleAfterDays) && staleAfterDays >= 0 ? staleAfterDays : 7}"` : "";
1186
1366
  const rootAttrs = interactive ? ` id="${ROOT_ID}" data-title="${escapeHtml(title)}"${islandConfigAttrs}` : "";
1187
1367
  return `<!DOCTYPE html>
1188
1368
  <html lang="${escapeHtml(lang)}" data-theme="${theme === "dark" ? "dark" : "light"}">
@@ -1198,6 +1378,7 @@ ${headExtra}
1198
1378
  <body>
1199
1379
  <div${rootAttrs} class="es-report-island font-sans text-foreground">${markup}</div>
1200
1380
  ${dataScript}
1381
+ ${historyScript}
1201
1382
  ${bodyExtra}
1202
1383
  ${cdnBody}
1203
1384
  ${islandTag}