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.js CHANGED
@@ -51,11 +51,23 @@ function useReport() {
51
51
  return ctx.report;
52
52
  }
53
53
 
54
- // src/components/ReportScenario.tsx
55
- import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
56
-
57
- // src/components/ReportSteps.tsx
58
- import { formatDuration } from "executable-stories-core/utils/duration";
54
+ // ../executable-stories-core/dist/utils/duration.js
55
+ function formatDuration(ms) {
56
+ if (ms < 1e3) {
57
+ return `${Math.round(ms)} ms`;
58
+ }
59
+ if (ms < 6e4) {
60
+ return `${(ms / 1e3).toFixed(2)} s`;
61
+ }
62
+ const minutes = Math.floor(ms / 6e4);
63
+ const seconds = Math.floor(ms % 6e4 / 1e3);
64
+ if (minutes < 60) {
65
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
66
+ }
67
+ const hours = Math.floor(minutes / 60);
68
+ const remainingMinutes = minutes % 60;
69
+ return `${hours}h ${remainingMinutes}m`;
70
+ }
59
71
 
60
72
  // src/components/doc/DocNote.tsx
61
73
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -209,6 +221,15 @@ function safeUrl(value) {
209
221
  if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
210
222
  return trimmed;
211
223
  }
224
+ function safeImageUrl(value) {
225
+ if (!value) return void 0;
226
+ const trimmed = value.trim();
227
+ if (/^data:image\//i.test(trimmed)) return trimmed;
228
+ return safeUrl(trimmed);
229
+ }
230
+ function isLocalFsPath(value) {
231
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
232
+ }
212
233
 
213
234
  // src/components/doc/DocSection.tsx
214
235
  import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -264,11 +285,19 @@ function DocMermaid({ entry }) {
264
285
  // src/components/doc/DocScreenshot.tsx
265
286
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
266
287
  function DocScreenshot({ entry }) {
288
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
289
+ if (!src) {
290
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
291
+ /* @__PURE__ */ jsx10("div", { className: "font-medium", children: "Screenshot unavailable" }),
292
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
293
+ entry.alt ? /* @__PURE__ */ jsx10("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
294
+ ] });
295
+ }
267
296
  return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
268
297
  /* @__PURE__ */ jsx10(
269
298
  "img",
270
299
  {
271
- src: entry.path,
300
+ src,
272
301
  alt: entry.alt ?? "",
273
302
  loading: "lazy",
274
303
  className: "max-w-full rounded-md border border-border"
@@ -281,6 +310,13 @@ function DocScreenshot({ entry }) {
281
310
  // src/components/doc/DocVideo.tsx
282
311
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
283
312
  function DocVideo({ entry }) {
313
+ if (isLocalFsPath(entry.path)) {
314
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
315
+ /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Video unavailable" }),
316
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
317
+ entry.caption ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
318
+ ] });
319
+ }
284
320
  const src = safeUrl(entry.path);
285
321
  const poster = safeUrl(entry.poster);
286
322
  return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
@@ -514,9 +550,6 @@ function ReportAttachments({ attachments }) {
514
550
  );
515
551
  }
516
552
 
517
- // src/components/ReportTrace.tsx
518
- import { formatDuration as formatDuration2 } from "executable-stories-core/utils/duration";
519
-
520
553
  // src/lib/trace.ts
521
554
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
522
555
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -611,7 +644,7 @@ function ReportTrace({ spans }) {
611
644
  " span",
612
645
  rows.length === 1 ? "" : "s",
613
646
  ", ",
614
- formatDuration2(totalMs),
647
+ formatDuration(totalMs),
615
648
  ")"
616
649
  ] }),
617
650
  /* @__PURE__ */ jsx19("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -634,33 +667,128 @@ function ReportTrace({ spans }) {
634
667
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
635
668
  }
636
669
  ) }),
637
- /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration2(row.durationMs) })
670
+ /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
638
671
  ] }, `${row.spanId}-${i}`);
639
672
  }) })
640
673
  ] });
641
674
  }
642
675
 
676
+ // src/interactive/scenario-history-context.ts
677
+ import { createContext as createContext2, useContext as useContext3 } from "react";
678
+ var ScenarioHistoryContext = createContext2(null);
679
+ var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
680
+ function useScenarioHistory() {
681
+ return useContext3(ScenarioHistoryContext);
682
+ }
683
+
684
+ // src/lib/run-history.ts
685
+ function currentStreak(entries) {
686
+ const last = entries[entries.length - 1];
687
+ if (!last) return void 0;
688
+ let count = 0;
689
+ for (let i = entries.length - 1; i >= 0 && entries[i].status === last.status; i--) count++;
690
+ return { status: last.status, count };
691
+ }
692
+ var STREAK_VERB = {
693
+ passed: "Passing",
694
+ failed: "Failing",
695
+ skipped: "Skipped",
696
+ pending: "Pending"
697
+ };
698
+ function describeRunHistory(entries) {
699
+ const passed = entries.filter((e) => e.status === "passed").length;
700
+ const base = `${passed}/${entries.length} runs passed`;
701
+ const streak = currentStreak(entries);
702
+ if (!streak || entries.length < 2) return base;
703
+ return `${base} \xB7 ${STREAK_VERB[streak.status]} for the last ${streak.count === 1 ? "run" : `${streak.count} runs`}`;
704
+ }
705
+ var MIN_FLAKINESS_SAMPLES = 3;
706
+ function flakinessOf(entries) {
707
+ const countable = entries.filter((e) => e.status === "passed" || e.status === "failed");
708
+ if (countable.length < MIN_FLAKINESS_SAMPLES) return "stable";
709
+ let transitions = 0;
710
+ for (let i = 1; i < countable.length; i++) {
711
+ if (countable[i].status !== countable[i - 1].status) transitions++;
712
+ }
713
+ const transitionScore = transitions / (countable.length - 1);
714
+ const failureRate = countable.filter((e) => e.status === "failed").length / countable.length;
715
+ if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) return "flaky";
716
+ if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
717
+ return "stable";
718
+ }
719
+
720
+ // src/components/ScenarioRunHistory.tsx
721
+ import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
722
+ var MAX_DOTS = 10;
723
+ var DOT_COLOR = {
724
+ passed: "bg-pass",
725
+ failed: "bg-fail",
726
+ skipped: "bg-skip",
727
+ pending: "bg-pend"
728
+ };
729
+ function dotTitle(e) {
730
+ const parts = [new Date(e.timestamp).toLocaleString(), e.status];
731
+ if (e.commitSha) parts.push(e.commitSha.slice(0, 8));
732
+ return parts.join(" \xB7 ");
733
+ }
734
+ function ScenarioRunHistory({ scenarioId }) {
735
+ const history = useScenarioHistory();
736
+ const entries = history?.[scenarioId];
737
+ if (!entries || entries.length < 2) return null;
738
+ const recent = entries.slice(-MAX_DOTS);
739
+ const flaky = flakinessOf(recent) === "flaky";
740
+ const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
741
+ return /* @__PURE__ */ jsxs13(Fragment7, { children: [
742
+ flaky ? /* @__PURE__ */ jsx20(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
743
+ /* @__PURE__ */ jsx20(
744
+ "span",
745
+ {
746
+ role: "img",
747
+ "aria-label": `Run history: ${summary}`,
748
+ title: summary,
749
+ className: "flex shrink-0 items-center gap-[3px]",
750
+ children: recent.map((e, i) => /* @__PURE__ */ jsx20(
751
+ "span",
752
+ {
753
+ "aria-hidden": true,
754
+ title: dotTitle(e),
755
+ className: cn(
756
+ "inline-block size-1.5 rounded-full",
757
+ DOT_COLOR[e.status],
758
+ // The newest run (rightmost dot) is the one the card's status
759
+ // badge already describes; ring it so the timeline reads as
760
+ // "…leading up to now".
761
+ i === recent.length - 1 && "ring-1 ring-border ring-offset-1 ring-offset-card"
762
+ )
763
+ },
764
+ `${e.timestamp}-${i}`
765
+ ))
766
+ }
767
+ )
768
+ ] });
769
+ }
770
+
643
771
  // src/interactive/collapse-context.ts
644
- import { createContext as createContext2, useCallback, useContext as useContext3, useState } from "react";
645
- var CollapseContext = createContext2(null);
772
+ import { createContext as createContext3, useCallback, useContext as useContext4, useState } from "react";
773
+ var CollapseContext = createContext3(null);
646
774
  var CollapseProvider = CollapseContext.Provider;
647
775
  function useCollapse() {
648
- return useContext3(CollapseContext);
776
+ return useContext4(CollapseContext);
649
777
  }
650
778
 
651
779
  // src/interactive/scenario-actions.tsx
652
- import { createContext as createContext3, useContext as useContext4 } from "react";
653
- var ScenarioActionsContext = createContext3(null);
780
+ import { createContext as createContext4, useContext as useContext5 } from "react";
781
+ var ScenarioActionsContext = createContext4(null);
654
782
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
655
783
  function useScenarioActions() {
656
- return useContext4(ScenarioActionsContext);
784
+ return useContext5(ScenarioActionsContext);
657
785
  }
658
786
 
659
787
  // src/components/ui/card.tsx
660
788
  import "react";
661
- import { jsx as jsx20 } from "react/jsx-runtime";
789
+ import { jsx as jsx21 } from "react/jsx-runtime";
662
790
  function Card({ className, ...props }) {
663
- return /* @__PURE__ */ jsx20(
791
+ return /* @__PURE__ */ jsx21(
664
792
  "div",
665
793
  {
666
794
  "data-slot": "card",
@@ -673,7 +801,7 @@ function Card({ className, ...props }) {
673
801
  );
674
802
  }
675
803
  function CardHeader({ className, ...props }) {
676
- return /* @__PURE__ */ jsx20(
804
+ return /* @__PURE__ */ jsx21(
677
805
  "div",
678
806
  {
679
807
  "data-slot": "card-header",
@@ -686,7 +814,7 @@ function CardHeader({ className, ...props }) {
686
814
  );
687
815
  }
688
816
  function CardContent({ className, ...props }) {
689
- return /* @__PURE__ */ jsx20(
817
+ return /* @__PURE__ */ jsx21(
690
818
  "div",
691
819
  {
692
820
  "data-slot": "card-content",
@@ -697,7 +825,7 @@ function CardContent({ className, ...props }) {
697
825
  }
698
826
 
699
827
  // src/components/ReportScenario.tsx
700
- import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
828
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
701
829
  var STATUS_LABEL = {
702
830
  passed: "Passed",
703
831
  failed: "Failed",
@@ -725,12 +853,12 @@ var STATUS_BADGE = {
725
853
  function ReportScenario({ scenario, hideTitle = false }) {
726
854
  const titleId = `${scenario.id}-title`;
727
855
  const bodyId = `${scenario.id}-body`;
728
- const label = STATUS_LABEL[scenario.status];
856
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
729
857
  const collapse = useCollapse();
730
858
  const collapsible = collapse !== null && !hideTitle;
731
859
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
732
860
  const actions = useScenarioActions();
733
- return /* @__PURE__ */ jsxs13(
861
+ return /* @__PURE__ */ jsxs14(
734
862
  Card,
735
863
  {
736
864
  id: scenario.id,
@@ -738,10 +866,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
738
866
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
739
867
  className: "gap-0 py-0",
740
868
  children: [
741
- /* @__PURE__ */ jsxs13(CardHeader, { className: "px-4 py-3", children: [
742
- /* @__PURE__ */ jsxs13("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
743
- hideTitle ? null : /* @__PURE__ */ jsxs13("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
744
- collapsible ? /* @__PURE__ */ jsx21(
869
+ /* @__PURE__ */ jsxs14(CardHeader, { className: "px-4 py-3", children: [
870
+ /* @__PURE__ */ jsxs14("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
871
+ hideTitle ? null : /* @__PURE__ */ jsxs14("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
872
+ collapsible ? /* @__PURE__ */ jsx22(
745
873
  "button",
746
874
  {
747
875
  type: "button",
@@ -750,17 +878,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
750
878
  "aria-controls": bodyId,
751
879
  "aria-label": `Toggle ${scenario.title}`,
752
880
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
753
- children: /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
881
+ children: /* @__PURE__ */ jsx22("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
754
882
  }
755
883
  ) : null,
756
- /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
757
- /* @__PURE__ */ jsx21("span", { children: scenario.title })
884
+ /* @__PURE__ */ jsx22("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
885
+ /* @__PURE__ */ jsx22("span", { children: scenario.title })
758
886
  ] }),
759
- /* @__PURE__ */ jsxs13("div", { className: "flex shrink-0 items-center gap-2", children: [
760
- scenario.durationMs > 0 ? /* @__PURE__ */ jsx21("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
761
- /* @__PURE__ */ jsx21(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
762
- actions ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-0.5", children: [
763
- /* @__PURE__ */ jsx21(
887
+ /* @__PURE__ */ jsxs14("div", { className: "flex shrink-0 items-center gap-2", children: [
888
+ /* @__PURE__ */ jsx22(ScenarioRunHistory, { scenarioId: scenario.id }),
889
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx22("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
890
+ /* @__PURE__ */ jsx22(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
891
+ actions ? /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-0.5", children: [
892
+ /* @__PURE__ */ jsx22(
764
893
  "button",
765
894
  {
766
895
  type: "button",
@@ -771,7 +900,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
771
900
  children: "link"
772
901
  }
773
902
  ),
774
- /* @__PURE__ */ jsx21(
903
+ /* @__PURE__ */ jsx22(
775
904
  "button",
776
905
  {
777
906
  type: "button",
@@ -782,7 +911,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
782
911
  children: "md"
783
912
  }
784
913
  ),
785
- /* @__PURE__ */ jsx21(
914
+ /* @__PURE__ */ jsx22(
786
915
  "button",
787
916
  {
788
917
  type: "button",
@@ -796,13 +925,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
796
925
  ] }) : null
797
926
  ] })
798
927
  ] }),
799
- 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: [
800
- scenario.tags.map((t) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
801
- (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}`))
928
+ 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: [
929
+ scenario.tags.map((t) => /* @__PURE__ */ jsx22("li", { children: /* @__PURE__ */ jsx22(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
930
+ (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}`))
802
931
  ] }) : null
803
932
  ] }),
804
- /* @__PURE__ */ jsxs13(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
805
- scenario.errorMessage ? /* @__PURE__ */ jsx21(
933
+ /* @__PURE__ */ jsxs14(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
934
+ scenario.errorMessage ? /* @__PURE__ */ jsx22(
806
935
  "pre",
807
936
  {
808
937
  role: "alert",
@@ -810,10 +939,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
810
939
  children: scenario.errorMessage
811
940
  }
812
941
  ) : null,
813
- /* @__PURE__ */ jsx21(ReportSteps, { scenario }),
814
- scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx21("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx21(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
815
- /* @__PURE__ */ jsx21(ReportAttachments, { attachments: scenario.attachments }),
816
- /* @__PURE__ */ jsx21(ReportTrace, { spans: scenario.otelSpans })
942
+ /* @__PURE__ */ jsx22(ReportSteps, { scenario }),
943
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx22("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx22(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
944
+ /* @__PURE__ */ jsx22(ReportAttachments, { attachments: scenario.attachments }),
945
+ /* @__PURE__ */ jsx22(ReportTrace, { spans: scenario.otelSpans })
817
946
  ] })
818
947
  ]
819
948
  }
@@ -821,13 +950,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
821
950
  }
822
951
 
823
952
  // src/components/ReportScenarioList.tsx
824
- import { Fragment as Fragment7, jsx as jsx22 } from "react/jsx-runtime";
953
+ import { Fragment as Fragment8, jsx as jsx23 } from "react/jsx-runtime";
825
954
  function ReportScenarioList({ feature }) {
826
- return /* @__PURE__ */ jsx22(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx22(ReportScenario, { scenario }, scenario.id)) });
955
+ return /* @__PURE__ */ jsx23(Fragment8, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx23(ReportScenario, { scenario }, scenario.id)) });
827
956
  }
828
957
 
829
958
  // src/components/ReportFeature.tsx
830
- import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
959
+ import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
831
960
  function ReportFeature({ feature }) {
832
961
  const titleId = `${feature.id}-title`;
833
962
  const bodyId = `${feature.id}-body`;
@@ -835,7 +964,7 @@ function ReportFeature({ feature }) {
835
964
  const skipped = s.skipped + s.pending;
836
965
  const collapse = useCollapse();
837
966
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
838
- return /* @__PURE__ */ jsxs14(
967
+ return /* @__PURE__ */ jsxs15(
839
968
  "section",
840
969
  {
841
970
  id: feature.id,
@@ -843,9 +972,9 @@ function ReportFeature({ feature }) {
843
972
  "aria-labelledby": titleId,
844
973
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
845
974
  children: [
846
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
847
- /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 items-center gap-2", children: [
848
- collapse ? /* @__PURE__ */ jsx23(
975
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
976
+ /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 items-center gap-2", children: [
977
+ collapse ? /* @__PURE__ */ jsx24(
849
978
  "button",
850
979
  {
851
980
  type: "button",
@@ -854,30 +983,30 @@ function ReportFeature({ feature }) {
854
983
  "aria-controls": bodyId,
855
984
  "aria-label": `Toggle ${feature.title}`,
856
985
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
857
- children: /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
986
+ children: /* @__PURE__ */ jsx24("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
858
987
  }
859
988
  ) : null,
860
- /* @__PURE__ */ jsxs14("div", { className: "min-w-0", children: [
861
- /* @__PURE__ */ jsx23("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
862
- /* @__PURE__ */ jsx23("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
989
+ /* @__PURE__ */ jsxs15("div", { className: "min-w-0", children: [
990
+ /* @__PURE__ */ jsx24("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
991
+ /* @__PURE__ */ jsx24("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
863
992
  ] })
864
993
  ] }),
865
- /* @__PURE__ */ jsxs14(
994
+ /* @__PURE__ */ jsxs15(
866
995
  "div",
867
996
  {
868
997
  role: "img",
869
998
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
870
999
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
871
1000
  children: [
872
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-pass", children: [
1001
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-pass", children: [
873
1002
  "\u2713",
874
1003
  s.passed
875
1004
  ] }),
876
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-fail", children: [
1005
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-fail", children: [
877
1006
  "\u2717",
878
1007
  s.failed
879
1008
  ] }),
880
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-skip", children: [
1009
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-skip", children: [
881
1010
  "\u25CB",
882
1011
  skipped
883
1012
  ] })
@@ -885,24 +1014,24 @@ function ReportFeature({ feature }) {
885
1014
  }
886
1015
  )
887
1016
  ] }),
888
- /* @__PURE__ */ jsx23("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx23(ReportScenarioList, { feature }) })
1017
+ /* @__PURE__ */ jsx24("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx24(ReportScenarioList, { feature }) })
889
1018
  ]
890
1019
  }
891
1020
  );
892
1021
  }
893
1022
 
894
1023
  // src/components/ReportFeatureList.tsx
895
- import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
1024
+ import { Fragment as Fragment9, jsx as jsx25 } from "react/jsx-runtime";
896
1025
  function ReportFeatureList() {
897
1026
  const report = useReport();
898
- return /* @__PURE__ */ jsx24(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx24(ReportFeature, { feature }, feature.id)) });
1027
+ return /* @__PURE__ */ jsx25(Fragment9, { children: report.features.map((feature) => /* @__PURE__ */ jsx25(ReportFeature, { feature }, feature.id)) });
899
1028
  }
900
1029
 
901
1030
  // src/components/ui/empty.tsx
902
1031
  import { cva as cva2 } from "class-variance-authority";
903
- import { jsx as jsx25 } from "react/jsx-runtime";
1032
+ import { jsx as jsx26 } from "react/jsx-runtime";
904
1033
  function Empty({ className, ...props }) {
905
- return /* @__PURE__ */ jsx25(
1034
+ return /* @__PURE__ */ jsx26(
906
1035
  "div",
907
1036
  {
908
1037
  "data-slot": "empty",
@@ -915,7 +1044,7 @@ function Empty({ className, ...props }) {
915
1044
  );
916
1045
  }
917
1046
  function EmptyHeader({ className, ...props }) {
918
- return /* @__PURE__ */ jsx25(
1047
+ return /* @__PURE__ */ jsx26(
919
1048
  "div",
920
1049
  {
921
1050
  "data-slot": "empty-header",
@@ -942,7 +1071,7 @@ var emptyMediaVariants = cva2(
942
1071
  }
943
1072
  );
944
1073
  function EmptyTitle({ className, ...props }) {
945
- return /* @__PURE__ */ jsx25(
1074
+ return /* @__PURE__ */ jsx26(
946
1075
  "div",
947
1076
  {
948
1077
  "data-slot": "empty-title",
@@ -953,16 +1082,16 @@ function EmptyTitle({ className, ...props }) {
953
1082
  }
954
1083
 
955
1084
  // src/components/ReportEmpty.tsx
956
- import { jsx as jsx26 } from "react/jsx-runtime";
1085
+ import { jsx as jsx27 } from "react/jsx-runtime";
957
1086
  function ReportEmpty({ message }) {
958
- return /* @__PURE__ */ jsx26(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx26(EmptyHeader, { children: /* @__PURE__ */ jsx26(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1087
+ return /* @__PURE__ */ jsx27(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx27(EmptyHeader, { children: /* @__PURE__ */ jsx27(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
959
1088
  }
960
1089
 
961
1090
  // src/components/ReportSummary.tsx
962
- import { Fragment as Fragment9, jsx as jsx27, jsxs as jsxs15 } from "react/jsx-runtime";
1091
+ import { Fragment as Fragment10, jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
963
1092
  function ReportSummary({ className }) {
964
1093
  const report = useReport();
965
- return /* @__PURE__ */ jsx27(
1094
+ return /* @__PURE__ */ jsx28(
966
1095
  ReportSummaryView,
967
1096
  {
968
1097
  summary: report.summary,
@@ -972,32 +1101,32 @@ function ReportSummary({ className }) {
972
1101
  );
973
1102
  }
974
1103
  function ReportSummaryView({ summary, className, ariaLabel }) {
975
- return /* @__PURE__ */ jsxs15("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
976
- /* @__PURE__ */ jsxs15("span", { children: [
977
- /* @__PURE__ */ jsx27("strong", { className: "font-semibold text-foreground", children: summary.total }),
1104
+ return /* @__PURE__ */ jsxs16("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1105
+ /* @__PURE__ */ jsxs16("span", { children: [
1106
+ /* @__PURE__ */ jsx28("strong", { className: "font-semibold text-foreground", children: summary.total }),
978
1107
  " scenario",
979
1108
  summary.total === 1 ? "" : "s"
980
1109
  ] }),
981
1110
  " \xB7 ",
982
- /* @__PURE__ */ jsxs15("span", { "data-status": "passed", className: "text-pass", children: [
1111
+ /* @__PURE__ */ jsxs16("span", { "data-status": "passed", className: "text-pass", children: [
983
1112
  summary.passed,
984
1113
  " passed"
985
1114
  ] }),
986
1115
  " \xB7 ",
987
- /* @__PURE__ */ jsxs15("span", { "data-status": "failed", className: "text-fail", children: [
1116
+ /* @__PURE__ */ jsxs16("span", { "data-status": "failed", className: "text-fail", children: [
988
1117
  summary.failed,
989
1118
  " failed"
990
1119
  ] }),
991
- summary.skipped > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1120
+ summary.skipped > 0 ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
992
1121
  " \xB7 ",
993
- /* @__PURE__ */ jsxs15("span", { "data-status": "skipped", className: "text-skip", children: [
1122
+ /* @__PURE__ */ jsxs16("span", { "data-status": "skipped", className: "text-skip", children: [
994
1123
  summary.skipped,
995
1124
  " skipped"
996
1125
  ] })
997
1126
  ] }) : null,
998
- summary.pending > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1127
+ summary.pending > 0 ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
999
1128
  " \xB7 ",
1000
- /* @__PURE__ */ jsxs15("span", { "data-status": "pending", className: "text-pend", children: [
1129
+ /* @__PURE__ */ jsxs16("span", { "data-status": "pending", className: "text-pend", children: [
1001
1130
  summary.pending,
1002
1131
  " pending"
1003
1132
  ] })
@@ -1005,69 +1134,116 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1005
1134
  ] });
1006
1135
  }
1007
1136
 
1137
+ // src/lib/provenance.ts
1138
+ var CI_DISPLAY_NAMES = {
1139
+ github: "GitHub Actions",
1140
+ gitlab: "GitLab CI",
1141
+ circleci: "CircleCI",
1142
+ jenkins: "Jenkins",
1143
+ azure: "Azure DevOps",
1144
+ buildkite: "Buildkite",
1145
+ travis: "Travis CI",
1146
+ ci: "CI"
1147
+ };
1148
+ function ciDisplayName(name) {
1149
+ return CI_DISPLAY_NAMES[name] ?? name;
1150
+ }
1151
+ function commitUrl(ci, sha) {
1152
+ const commitSha = ci?.commitSha ?? sha;
1153
+ if (!ci?.url || !commitSha) return void 0;
1154
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1155
+ if (github) return `${github[1]}/commit/${commitSha}`;
1156
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1157
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1158
+ return void 0;
1159
+ }
1160
+ function prUrl(ci) {
1161
+ if (!ci?.url || !ci.prNumber) return void 0;
1162
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1163
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1164
+ return void 0;
1165
+ }
1166
+
1008
1167
  // src/components/ReportMeta.tsx
1009
- import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1010
- import { jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
1168
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1011
1169
  function ReportMeta() {
1012
1170
  const report = useReport();
1013
1171
  const items = [];
1172
+ const linkClass = "text-primary underline underline-offset-2";
1014
1173
  if (report.startedAtMs) {
1015
1174
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1016
1175
  }
1017
1176
  if (report.durationMs) {
1018
- items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1177
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1019
1178
  }
1020
1179
  if (report.packageVersion) {
1021
1180
  items.push({ label: "Version", value: report.packageVersion });
1022
1181
  }
1023
- if (report.gitSha) {
1024
- items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1182
+ if (report.ci?.branch) {
1183
+ items.push({ label: "Branch", value: report.ci.branch });
1184
+ }
1185
+ const sha = report.ci?.commitSha ?? report.gitSha;
1186
+ if (sha) {
1187
+ const url = commitUrl(report.ci, report.gitSha);
1188
+ const short = sha.slice(0, 8);
1189
+ items.push({
1190
+ label: "Commit",
1191
+ value: url ? /* @__PURE__ */ jsx29("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1192
+ });
1025
1193
  }
1026
1194
  if (report.ci) {
1027
1195
  items.push({
1028
1196
  label: "CI",
1029
- value: report.ci.url ? /* @__PURE__ */ jsxs16("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1030
- report.ci.name,
1197
+ value: report.ci.url ? /* @__PURE__ */ jsxs17("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1198
+ ciDisplayName(report.ci.name),
1031
1199
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1032
- ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1200
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1201
+ });
1202
+ }
1203
+ if (report.ci?.prNumber) {
1204
+ const url = prUrl(report.ci);
1205
+ const label = `#${report.ci.prNumber}`;
1206
+ items.push({
1207
+ label: "PR",
1208
+ value: url ? /* @__PURE__ */ jsx29("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1033
1209
  });
1034
1210
  }
1035
1211
  if (items.length === 0) return null;
1036
- 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: [
1037
- /* @__PURE__ */ jsx28("dt", { className: "font-medium", children: item.label }),
1038
- /* @__PURE__ */ jsx28("dd", { className: "font-mono text-foreground", children: item.value })
1212
+ 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: [
1213
+ /* @__PURE__ */ jsx29("dt", { className: "font-medium", children: item.label }),
1214
+ /* @__PURE__ */ jsx29("dd", { className: "font-mono text-foreground", children: item.value })
1039
1215
  ] }, item.label)) });
1040
1216
  }
1041
1217
 
1042
1218
  // src/components/ReportSchemaError.tsx
1043
- import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1219
+ import { jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1044
1220
  function ReportSchemaError({ error }) {
1045
- return /* @__PURE__ */ jsxs17("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1046
- /* @__PURE__ */ jsx29("p", { children: /* @__PURE__ */ jsx29("strong", { children: "Report could not be displayed." }) }),
1047
- /* @__PURE__ */ jsx29("p", { children: error.message }),
1048
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs17("p", { children: [
1221
+ return /* @__PURE__ */ jsxs18("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1222
+ /* @__PURE__ */ jsx30("p", { children: /* @__PURE__ */ jsx30("strong", { children: "Report could not be displayed." }) }),
1223
+ /* @__PURE__ */ jsx30("p", { children: error.message }),
1224
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs18("p", { children: [
1049
1225
  "The report bundle is newer than this version of ",
1050
- /* @__PURE__ */ jsx29("code", { children: "executable-stories-react" }),
1226
+ /* @__PURE__ */ jsx30("code", { children: "executable-stories-react" }),
1051
1227
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1052
1228
  ] }) : null,
1053
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs17("details", { children: [
1054
- /* @__PURE__ */ jsxs17("summary", { children: [
1229
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs18("details", { children: [
1230
+ /* @__PURE__ */ jsxs18("summary", { children: [
1055
1231
  error.issues.length,
1056
1232
  " validation issue",
1057
1233
  error.issues.length === 1 ? "" : "s"
1058
1234
  ] }),
1059
- /* @__PURE__ */ jsx29("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1235
+ /* @__PURE__ */ jsx30("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1060
1236
  ] }) : null
1061
1237
  ] });
1062
1238
  }
1063
1239
 
1064
1240
  // src/components/ReportShell.tsx
1065
- import { Fragment as Fragment10, jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1241
+ import { Fragment as Fragment11, jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1066
1242
  function ReportTitleBlock({ title }) {
1067
- return /* @__PURE__ */ jsxs18(Fragment10, { children: [
1068
- /* @__PURE__ */ jsx30("h1", { children: title ?? "Story Report" }),
1069
- /* @__PURE__ */ jsx30(ReportSummary, {}),
1070
- /* @__PURE__ */ jsx30(ReportMeta, {})
1243
+ return /* @__PURE__ */ jsxs19(Fragment11, { children: [
1244
+ /* @__PURE__ */ jsx31("h1", { children: title ?? "Story Report" }),
1245
+ /* @__PURE__ */ jsx31(ReportSummary, {}),
1246
+ /* @__PURE__ */ jsx31(ReportMeta, {})
1071
1247
  ] });
1072
1248
  }
1073
1249
  function ReportErrorShell({
@@ -1076,28 +1252,29 @@ function ReportErrorShell({
1076
1252
  title,
1077
1253
  dataTheme
1078
1254
  }) {
1079
- return /* @__PURE__ */ jsx30("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx30(ReportSchemaError, { error }) });
1255
+ return /* @__PURE__ */ jsx31("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx31(ReportSchemaError, { error }) });
1080
1256
  }
1081
1257
 
1082
1258
  // src/components/Report.tsx
1083
- import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1259
+ import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
1084
1260
  function Report(props) {
1085
1261
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1086
1262
  const result = unwrapReport(report);
1087
1263
  if (!result.ok) {
1088
- return /* @__PURE__ */ jsx31(ReportErrorShell, { error: result.error, className, title, dataTheme });
1264
+ return /* @__PURE__ */ jsx32(ReportErrorShell, { error: result.error, className, title, dataTheme });
1089
1265
  }
1090
1266
  const data = result.data;
1091
1267
  const hasContent = data.features.length > 0;
1092
- return /* @__PURE__ */ jsx31(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs19("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1093
- /* @__PURE__ */ jsx31("header", { className: "es-report-header", children: /* @__PURE__ */ jsx31(ReportTitleBlock, { title }) }),
1094
- hasContent ? /* @__PURE__ */ jsx31(ReportFeatureList, {}) : /* @__PURE__ */ jsx31(ReportEmpty, {})
1268
+ return /* @__PURE__ */ jsx32(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs20("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1269
+ /* @__PURE__ */ jsx32("header", { className: "es-report-header", children: /* @__PURE__ */ jsx32(ReportTitleBlock, { title }) }),
1270
+ hasContent ? /* @__PURE__ */ jsx32(ReportFeatureList, {}) : /* @__PURE__ */ jsx32(ReportEmpty, {})
1095
1271
  ] }) });
1096
1272
  }
1097
1273
 
1098
1274
  // src/ssr-entry.ts
1099
1275
  var ROOT_ID = "es-report-root";
1100
1276
  var DATA_ID = "es-report-data";
1277
+ var HISTORY_ID = "es-report-history";
1101
1278
  function escapeJsonForScript(json) {
1102
1279
  return json.replace(/[<\u2028\u2029]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
1103
1280
  }
@@ -1146,7 +1323,9 @@ function renderReportToHtml(report, options = {}) {
1146
1323
  lang = "en",
1147
1324
  syntaxHighlighting = true,
1148
1325
  mermaid = true,
1149
- islandScript = ""
1326
+ islandScript = "",
1327
+ staleAfterDays = 7,
1328
+ scenarioHistory
1150
1329
  } = options;
1151
1330
  const markup = renderToStaticMarkup(
1152
1331
  createElement(Report, { report, title })
@@ -1156,9 +1335,10 @@ function renderReportToHtml(report, options = {}) {
1156
1335
  const rawReport = parsed.ok ? parsed.data : void 0;
1157
1336
  const interactive = islandScript.length > 0 && rawReport !== void 0;
1158
1337
  const dataScript = interactive ? `<script type="application/json" id="${DATA_ID}">${escapeJsonForScript(JSON.stringify(rawReport))}</script>` : "";
1338
+ const historyScript = interactive && scenarioHistory && Object.keys(scenarioHistory).length > 0 ? `<script type="application/json" id="${HISTORY_ID}">${escapeJsonForScript(JSON.stringify(scenarioHistory))}</script>` : "";
1159
1339
  const islandTag = interactive ? `<script>${islandScript}</script>` : "";
1160
1340
  const cdnBody = interactive ? "" : cdn.body;
1161
- const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}"` : "";
1341
+ 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}"` : "";
1162
1342
  const rootAttrs = interactive ? ` id="${ROOT_ID}" data-title="${escapeHtml(title)}"${islandConfigAttrs}` : "";
1163
1343
  return `<!DOCTYPE html>
1164
1344
  <html lang="${escapeHtml(lang)}" data-theme="${theme === "dark" ? "dark" : "light"}">
@@ -1174,6 +1354,7 @@ ${headExtra}
1174
1354
  <body>
1175
1355
  <div${rootAttrs} class="es-report-island font-sans text-foreground">${markup}</div>
1176
1356
  ${dataScript}
1357
+ ${historyScript}
1177
1358
  ${bodyExtra}
1178
1359
  ${cdnBody}
1179
1360
  ${islandTag}