executable-stories-react 0.13.0 → 0.15.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.
@@ -8,8 +8,8 @@ import {
8
8
  useDeferredValue,
9
9
  useId as useId2,
10
10
  useMemo as useMemo3,
11
- useRef as useRef5,
12
- useState as useState9
11
+ useRef as useRef6,
12
+ useState as useState11
13
13
  } from "react";
14
14
 
15
15
  // src/components/ui/empty.tsx
@@ -206,7 +206,7 @@ function formatValue(value) {
206
206
  function DocKv({ entry }) {
207
207
  return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
208
208
  /* @__PURE__ */ jsx6("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
209
- /* @__PURE__ */ jsx6("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
209
+ /* @__PURE__ */ jsx6("dd", { className: "min-w-0 break-words font-mono text-foreground", children: formatValue(entry.value) })
210
210
  ] });
211
211
  }
212
212
 
@@ -414,7 +414,7 @@ function DocHtml({ entry }) {
414
414
  loading: "lazy",
415
415
  title,
416
416
  className: "block w-full border-0",
417
- style: { height }
417
+ style: { height, colorScheme: "light", background: "#fff" }
418
418
  }
419
419
  )
420
420
  ] });
@@ -1138,6 +1138,24 @@ function scenarioToPrompt(scenario) {
1138
1138
  }
1139
1139
  return lines.join("\n");
1140
1140
  }
1141
+ function failuresToPrompt(scenarios) {
1142
+ if (scenarios.length === 0) return "";
1143
+ const noun = scenarios.length === 1 ? "test scenario is" : "test scenarios are";
1144
+ const lines = [
1145
+ `${scenarios.length} ${noun} failing. Investigate the likely cause (they may share one) and propose a fix.`
1146
+ ];
1147
+ scenarios.forEach((scenario, i) => {
1148
+ lines.push("", `${i + 1}. ${scenario.title}`, "Steps:");
1149
+ for (const step of scenario.steps) {
1150
+ const mark = step.status === "failed" ? " \u27F5 failed here" : "";
1151
+ lines.push(` ${step.keyword} ${step.text}${mark}`);
1152
+ }
1153
+ if (scenario.errorMessage) {
1154
+ lines.push("Error:", scenario.errorMessage.trim());
1155
+ }
1156
+ });
1157
+ return lines.join("\n");
1158
+ }
1141
1159
  function scenarioPermalink(scenario) {
1142
1160
  if (typeof location === "undefined") return `#${scenario.id}`;
1143
1161
  return `${location.href.split("#")[0]}#${scenario.id}`;
@@ -1263,57 +1281,68 @@ function ReportScenario({ scenario, hideTitle = false }) {
1263
1281
  className: "gap-0 py-0",
1264
1282
  children: [
1265
1283
  /* @__PURE__ */ jsxs17(CardHeader, { className: "px-4 py-3", children: [
1266
- /* @__PURE__ */ jsxs17("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1267
- hideTitle ? null : /* @__PURE__ */ jsxs17("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1268
- collapsible ? /* @__PURE__ */ jsx26(
1269
- "button",
1270
- {
1271
- type: "button",
1272
- onClick: () => collapse.toggle(scenario.id),
1273
- "aria-expanded": !collapsed,
1274
- "aria-controls": bodyId,
1275
- "aria-label": `Toggle ${scenario.title}`,
1276
- className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
1277
- children: /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1278
- }
1279
- ) : null,
1280
- /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1281
- /* @__PURE__ */ jsx26("span", { children: scenario.title })
1282
- ] }),
1283
- /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1284
- /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1285
- /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1286
- scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1287
- meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1288
- actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
1289
- /* @__PURE__ */ jsx26(
1290
- DropdownMenuTrigger,
1291
- {
1292
- "aria-label": `Actions for ${scenario.title}`,
1293
- className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
1294
- children: /* @__PURE__ */ jsx26(MoreHorizontalIcon, { "aria-hidden": true })
1295
- }
1296
- ),
1297
- /* @__PURE__ */ jsxs17(DropdownMenuContent, { align: "end", children: [
1298
- /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1299
- /* @__PURE__ */ jsx26(LinkIcon, {}),
1300
- " Copy link"
1301
- ] }),
1302
- /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1303
- /* @__PURE__ */ jsx26(FileTextIcon, {}),
1304
- " Copy as Markdown"
1305
- ] }),
1306
- /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1307
- /* @__PURE__ */ jsx26(SparklesIcon, {}),
1308
- " Explain with AI"
1309
- ] })
1284
+ /* @__PURE__ */ jsxs17(
1285
+ "div",
1286
+ {
1287
+ className: cn(
1288
+ // Wraps rather than squeezing: on a phone the status/duration group
1289
+ // is ~160px, which leaves a title column a few characters wide.
1290
+ "flex flex-wrap items-start gap-x-4 gap-y-1",
1291
+ hideTitle ? "justify-end" : "justify-between"
1292
+ ),
1293
+ children: [
1294
+ hideTitle ? null : /* @__PURE__ */ jsxs17("h3", { id: titleId, className: "flex min-w-0 flex-1 basis-48 items-center gap-2 text-sm font-medium text-foreground", children: [
1295
+ collapsible ? /* @__PURE__ */ jsx26(
1296
+ "button",
1297
+ {
1298
+ type: "button",
1299
+ onClick: () => collapse.toggle(scenario.id),
1300
+ "aria-expanded": !collapsed,
1301
+ "aria-controls": bodyId,
1302
+ "aria-label": `Toggle ${scenario.title}`,
1303
+ className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
1304
+ children: /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1305
+ }
1306
+ ) : null,
1307
+ /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1308
+ /* @__PURE__ */ jsx26("span", { children: scenario.title })
1309
+ ] }),
1310
+ /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1311
+ /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1312
+ /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1313
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1314
+ meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1315
+ actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
1316
+ /* @__PURE__ */ jsx26(
1317
+ DropdownMenuTrigger,
1318
+ {
1319
+ "aria-label": `Actions for ${scenario.title}`,
1320
+ className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
1321
+ children: /* @__PURE__ */ jsx26(MoreHorizontalIcon, { "aria-hidden": true })
1322
+ }
1323
+ ),
1324
+ /* @__PURE__ */ jsxs17(DropdownMenuContent, { align: "end", children: [
1325
+ /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1326
+ /* @__PURE__ */ jsx26(LinkIcon, {}),
1327
+ " Copy link"
1328
+ ] }),
1329
+ /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1330
+ /* @__PURE__ */ jsx26(FileTextIcon, {}),
1331
+ " Copy as Markdown"
1332
+ ] }),
1333
+ /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1334
+ /* @__PURE__ */ jsx26(SparklesIcon, {}),
1335
+ " Explain with AI"
1336
+ ] })
1337
+ ] })
1338
+ ] }) : null
1310
1339
  ] })
1311
- ] }) : null
1312
- ] })
1313
- ] }),
1340
+ ]
1341
+ }
1342
+ ),
1314
1343
  scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs17("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1315
1344
  scenario.tags.map((t) => /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsx26(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1316
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx26("li", { children: ticket.url ? /* @__PURE__ */ jsx26("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1345
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx26("li", { children: ticket.url ? /* @__PURE__ */ jsx26("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", className: "inline-flex min-h-6 items-center", children: /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1317
1346
  ] }) : null
1318
1347
  ] }),
1319
1348
  /* @__PURE__ */ jsxs17(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
@@ -1456,6 +1485,7 @@ function ReportFeatureList() {
1456
1485
 
1457
1486
  // src/components/ReportMeta.tsx
1458
1487
  import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1488
+ import { slowestScenarios } from "executable-stories-core/report-queries";
1459
1489
  import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1460
1490
  function ReportMeta() {
1461
1491
  const report = useReport();
@@ -1499,7 +1529,9 @@ function ReportMeta() {
1499
1529
  value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1500
1530
  });
1501
1531
  }
1502
- if (items.length === 0) return null;
1532
+ const slowest = slowestScenarios(report, 5);
1533
+ const showSlowest = slowest.length >= 3;
1534
+ if (items.length === 0 && !showSlowest) return null;
1503
1535
  return (
1504
1536
  // Run metadata is useful but secondary to "what failed" — tuck it into a
1505
1537
  // collapsed disclosure so it doesn't push the result down. Native <details>
@@ -1512,7 +1544,14 @@ function ReportMeta() {
1512
1544
  /* @__PURE__ */ jsx31("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ jsxs20("div", { className: "flex items-baseline gap-1.5", children: [
1513
1545
  /* @__PURE__ */ jsx31("dt", { className: "font-medium", children: item.label }),
1514
1546
  /* @__PURE__ */ jsx31("dd", { className: "font-mono text-foreground", children: item.value })
1515
- ] }, item.label)) })
1547
+ ] }, item.label)) }),
1548
+ showSlowest ? /* @__PURE__ */ jsxs20("div", { className: "border-t border-border px-4 py-3", children: [
1549
+ /* @__PURE__ */ jsx31("div", { className: "mb-1.5 font-medium text-muted-foreground", children: "Slowest scenarios" }),
1550
+ /* @__PURE__ */ jsx31("ol", { className: "flex flex-col gap-1", "aria-label": "Slowest scenarios", children: slowest.map((s) => /* @__PURE__ */ jsxs20("li", { className: "flex items-baseline justify-between gap-4", children: [
1551
+ /* @__PURE__ */ jsx31("a", { href: `#${s.id}`, className: "min-w-0 text-primary underline underline-offset-2", children: s.title }),
1552
+ /* @__PURE__ */ jsx31("span", { className: "shrink-0 font-mono tabular-nums text-muted-foreground", children: formatDuration4(s.durationMs) })
1553
+ ] }, s.id)) })
1554
+ ] }) : null
1516
1555
  ] })
1517
1556
  );
1518
1557
  }
@@ -1988,21 +2027,26 @@ function allTags(report) {
1988
2027
  }
1989
2028
  return [...seen];
1990
2029
  }
1991
- function queryMatches(scenario, q) {
2030
+ function queryMatches(scenario, q, sourceFile) {
1992
2031
  if (q === "") return true;
1993
2032
  if (scenario.title.toLowerCase().includes(q)) return true;
2033
+ if (sourceFile?.toLowerCase().includes(q)) return true;
2034
+ if (scenario.errorMessage?.toLowerCase().includes(q)) return true;
1994
2035
  for (const tag of scenario.tags) {
1995
2036
  if (tag.toLowerCase().includes(q)) return true;
1996
2037
  }
2038
+ for (const ticket of scenario.tickets ?? []) {
2039
+ if (ticket.id.toLowerCase().includes(q)) return true;
2040
+ }
1997
2041
  for (const step of scenario.steps) {
1998
2042
  if (step.text.toLowerCase().includes(q)) return true;
1999
2043
  }
2000
2044
  return false;
2001
2045
  }
2002
- function scenarioMatches(scenario, q, status, tags) {
2046
+ function scenarioMatches(scenario, q, status, tags, sourceFile) {
2003
2047
  if (status !== "all" && scenario.status !== status) return false;
2004
2048
  if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2005
- return queryMatches(scenario, q);
2049
+ return queryMatches(scenario, q, sourceFile);
2006
2050
  }
2007
2051
  function summarizeScenarios(scenarios) {
2008
2052
  let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
@@ -2025,7 +2069,9 @@ function filterReport(report, criteria) {
2025
2069
  const features = [];
2026
2070
  let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2027
2071
  for (const feature of report.features) {
2028
- const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2072
+ const matched = feature.scenarios.filter(
2073
+ (s) => scenarioMatches(s, q, status, tags, feature.sourceFile)
2074
+ );
2029
2075
  if (matched.length === 0) continue;
2030
2076
  const summary = summarizeScenarios(matched);
2031
2077
  features.push({ ...feature, summary, scenarios: matched });
@@ -2049,6 +2095,9 @@ function filterReport(report, criteria) {
2049
2095
  features
2050
2096
  };
2051
2097
  }
2098
+ function failedScenarios(report) {
2099
+ return report.features.flatMap((f) => f.scenarios.filter((s) => s.status === "failed"));
2100
+ }
2052
2101
  function listFailures(report) {
2053
2102
  const out = [];
2054
2103
  for (const feature of report.features) {
@@ -2132,7 +2181,7 @@ function ReportAgentFilterNotice({
2132
2181
  // src/interactive/ReportFailureBanner.tsx
2133
2182
  import { useCallback as useCallback2 } from "react";
2134
2183
  import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
2135
- function ReportFailureBanner({ failures }) {
2184
+ function ReportFailureBanner({ failures, onCopyAll }) {
2136
2185
  const first = failures[0];
2137
2186
  const jumpToFirst = useCallback2(() => {
2138
2187
  if (!first) return;
@@ -2162,7 +2211,23 @@ function ReportFailureBanner({ failures }) {
2162
2211
  "aria-label": "View first failure",
2163
2212
  children: "View first failure \u2193"
2164
2213
  }
2165
- )
2214
+ ),
2215
+ onCopyAll ? /* @__PURE__ */ jsxs26(
2216
+ "button",
2217
+ {
2218
+ type: "button",
2219
+ className: "es-failure-banner-jump",
2220
+ onClick: onCopyAll,
2221
+ "aria-label": "Copy failures for an agent",
2222
+ children: [
2223
+ "Copy ",
2224
+ failures.length,
2225
+ " failure",
2226
+ failures.length === 1 ? "" : "s",
2227
+ " for an agent"
2228
+ ]
2229
+ }
2230
+ ) : null
2166
2231
  ]
2167
2232
  }
2168
2233
  );
@@ -2370,7 +2435,7 @@ var ReportSearch = forwardRef(
2370
2435
  onChange,
2371
2436
  matchedCount,
2372
2437
  totalCount,
2373
- placeholder = "Search scenarios or tags\u2026",
2438
+ placeholder = "Search scenarios, tags, tickets, errors\u2026",
2374
2439
  className
2375
2440
  } = props;
2376
2441
  const inputId = useId();
@@ -2428,8 +2493,9 @@ var ReportSearch = forwardRef(
2428
2493
  }
2429
2494
  );
2430
2495
 
2431
- // src/interactive/ReportShortcutsHelp.tsx
2432
- import { useEffect as useEffect3, useRef } from "react";
2496
+ // src/interactive/ReportShare.tsx
2497
+ import { Share2 } from "lucide-react";
2498
+ import { useEffect as useEffect3, useRef, useState as useState5 } from "react";
2433
2499
 
2434
2500
  // src/components/ui/button.tsx
2435
2501
  import "react";
@@ -2484,8 +2550,192 @@ function Button({
2484
2550
  });
2485
2551
  }
2486
2552
 
2553
+ // src/interactive/ReportShare.tsx
2554
+ import { Fragment as Fragment14, jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2555
+ var DEFAULT_SHARE_COMMAND = "npx executable-stories share reports/";
2556
+ function ReportShare({
2557
+ command = DEFAULT_SHARE_COMMAND,
2558
+ onCopy
2559
+ }) {
2560
+ const [open, setOpen] = useState5(false);
2561
+ const dialogRef = useRef(null);
2562
+ useEffect3(() => {
2563
+ const dialog = dialogRef.current;
2564
+ if (!dialog) return;
2565
+ if (open && !dialog.open) dialog.showModal();
2566
+ else if (!open && dialog.open) dialog.close();
2567
+ }, [open]);
2568
+ return /* @__PURE__ */ jsxs31(Fragment14, { children: [
2569
+ /* @__PURE__ */ jsxs31(
2570
+ Button,
2571
+ {
2572
+ type: "button",
2573
+ variant: "ghost",
2574
+ size: "sm",
2575
+ "aria-label": "Share this report",
2576
+ title: "Share this report",
2577
+ onClick: () => setOpen(true),
2578
+ children: [
2579
+ /* @__PURE__ */ jsx46(Share2, { "aria-hidden": "true", className: "size-4" }),
2580
+ /* @__PURE__ */ jsx46("span", { className: "sr-only sm:not-sr-only", children: "Share" })
2581
+ ]
2582
+ }
2583
+ ),
2584
+ /* @__PURE__ */ jsxs31(
2585
+ "dialog",
2586
+ {
2587
+ ref: dialogRef,
2588
+ className: "es-report-share m-auto w-[min(90vw,32rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2589
+ "aria-label": "Share this report",
2590
+ onClose: () => setOpen(false),
2591
+ children: [
2592
+ /* @__PURE__ */ jsx46("h2", { className: "text-base font-semibold", children: "Share this report" }),
2593
+ /* @__PURE__ */ jsx46("p", { className: "mt-2 text-sm text-muted-foreground", children: "Publish it to Executable Stories Cloud and send a link. Screenshots and videos go with it. Run this from your project:" }),
2594
+ /* @__PURE__ */ jsxs31("div", { className: "mt-4 flex items-center gap-2", children: [
2595
+ /* @__PURE__ */ jsx46("code", { className: "min-w-0 flex-1 truncate rounded border border-border bg-muted px-3 py-2 font-mono text-xs", children: command }),
2596
+ /* @__PURE__ */ jsx46(
2597
+ Button,
2598
+ {
2599
+ type: "button",
2600
+ size: "sm",
2601
+ onClick: () => onCopy(command, "Command copied"),
2602
+ children: "Copy"
2603
+ }
2604
+ )
2605
+ ] }),
2606
+ /* @__PURE__ */ jsx46("p", { className: "mt-3 text-xs text-muted-foreground", children: "The file you are reading cannot upload itself, so the command does it: only the CLI can read the assets this report links to." }),
2607
+ /* @__PURE__ */ jsxs31("div", { className: "mt-6 flex items-center justify-between gap-4", children: [
2608
+ /* @__PURE__ */ jsx46(
2609
+ "a",
2610
+ {
2611
+ className: "text-xs underline underline-offset-2",
2612
+ href: "https://executablestories.com/guides/sharing-reports/",
2613
+ target: "_blank",
2614
+ rel: "noreferrer",
2615
+ children: "How sharing works"
2616
+ }
2617
+ ),
2618
+ /* @__PURE__ */ jsx46(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setOpen(false), children: "Close" })
2619
+ ] })
2620
+ ]
2621
+ }
2622
+ )
2623
+ ] });
2624
+ }
2625
+
2626
+ // src/interactive/ReportSinceLastVisit.tsx
2627
+ import { useEffect as useEffect4, useState as useState6 } from "react";
2628
+
2629
+ // src/interactive/last-visit.ts
2630
+ function snapshotOf(report, atMs) {
2631
+ const statuses = {};
2632
+ for (const feature of report.features) {
2633
+ for (const scenario of feature.scenarios) {
2634
+ statuses[scenario.id] = scenario.status;
2635
+ }
2636
+ }
2637
+ return { runId: report.runId, atMs, statuses };
2638
+ }
2639
+ function diffSinceVisit(previous, report) {
2640
+ if (!previous) return null;
2641
+ if (previous.runId === report.runId) return null;
2642
+ const newlyFailing = [];
2643
+ const newlyPassing = [];
2644
+ const added = [];
2645
+ for (const feature of report.features) {
2646
+ for (const scenario of feature.scenarios) {
2647
+ const ref = { id: scenario.id, title: scenario.title };
2648
+ const before = previous.statuses[scenario.id];
2649
+ if (before === void 0) {
2650
+ added.push(ref);
2651
+ } else if (before !== "failed" && scenario.status === "failed") {
2652
+ newlyFailing.push(ref);
2653
+ } else if (before === "failed" && scenario.status === "passed") {
2654
+ newlyPassing.push(ref);
2655
+ }
2656
+ }
2657
+ }
2658
+ if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) return null;
2659
+ return { sinceMs: previous.atMs, newlyFailing, newlyPassing, added };
2660
+ }
2661
+
2662
+ // src/interactive/ReportSinceLastVisit.tsx
2663
+ import { Fragment as Fragment15, jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2664
+ var KEY = "es-last-visit";
2665
+ var MAX_LINKS2 = 5;
2666
+ function readVisit() {
2667
+ try {
2668
+ const raw = localStorage.getItem(KEY);
2669
+ if (!raw) return null;
2670
+ const parsed = JSON.parse(raw);
2671
+ if (typeof parsed !== "object" || parsed === null) return null;
2672
+ const v = parsed;
2673
+ if (typeof v.runId !== "string" || typeof v.atMs !== "number" || typeof v.statuses !== "object") {
2674
+ return null;
2675
+ }
2676
+ return { runId: v.runId, atMs: v.atMs, statuses: v.statuses };
2677
+ } catch {
2678
+ return null;
2679
+ }
2680
+ }
2681
+ function writeVisit(snapshot) {
2682
+ try {
2683
+ localStorage.setItem(KEY, JSON.stringify(snapshot));
2684
+ } catch {
2685
+ }
2686
+ }
2687
+ function ScenarioLinks2({ scenarios, tone }) {
2688
+ return /* @__PURE__ */ jsxs32(Fragment15, { children: [
2689
+ scenarios.slice(0, MAX_LINKS2).map((s, i) => /* @__PURE__ */ jsxs32("span", { children: [
2690
+ i > 0 ? ", " : "",
2691
+ /* @__PURE__ */ jsx47("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2692
+ ] }, s.id)),
2693
+ scenarios.length > MAX_LINKS2 ? /* @__PURE__ */ jsxs32("span", { children: [
2694
+ " and ",
2695
+ scenarios.length - MAX_LINKS2,
2696
+ " more"
2697
+ ] }) : null
2698
+ ] });
2699
+ }
2700
+ function ReportSinceLastVisit({ report }) {
2701
+ const [delta, setDelta] = useState6(null);
2702
+ const [nowMs, setNowMs] = useState6(0);
2703
+ useEffect4(() => {
2704
+ const now = Date.now();
2705
+ setDelta(diffSinceVisit(readVisit(), report));
2706
+ setNowMs(now);
2707
+ writeVisit(snapshotOf(report, now));
2708
+ }, [report]);
2709
+ if (!delta) return null;
2710
+ const { newlyFailing, newlyPassing, added } = delta;
2711
+ return /* @__PURE__ */ jsxs32("p", { className: "text-xs text-muted-foreground", "data-testid": "es-since-last-visit", children: [
2712
+ /* @__PURE__ */ jsxs32("span", { className: "font-medium text-foreground", children: [
2713
+ "Since your last visit (",
2714
+ formatRelativeAge(delta.sinceMs, nowMs),
2715
+ "):"
2716
+ ] }),
2717
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs32("span", { className: "ml-2 text-fail", children: [
2718
+ newlyFailing.length,
2719
+ " started failing (",
2720
+ /* @__PURE__ */ jsx47(ScenarioLinks2, { scenarios: newlyFailing, tone: "text-fail" }),
2721
+ ")"
2722
+ ] }) : null,
2723
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs32("span", { className: "ml-2 text-pass", children: [
2724
+ newlyPassing.length,
2725
+ " fixed (",
2726
+ /* @__PURE__ */ jsx47(ScenarioLinks2, { scenarios: newlyPassing, tone: "text-pass" }),
2727
+ ")"
2728
+ ] }) : null,
2729
+ added.length > 0 ? /* @__PURE__ */ jsxs32("span", { className: "ml-2", children: [
2730
+ added.length,
2731
+ " new"
2732
+ ] }) : null
2733
+ ] });
2734
+ }
2735
+
2487
2736
  // src/interactive/ReportShortcutsHelp.tsx
2488
- import { jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2737
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
2738
+ import { jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
2489
2739
  var SHORTCUTS = [
2490
2740
  { keys: "/", description: "Focus search" },
2491
2741
  { keys: "f", description: "Jump to next failure" },
@@ -2497,20 +2747,20 @@ var SHORTCUTS = [
2497
2747
  ];
2498
2748
  function Keys({ keys }) {
2499
2749
  const parts = keys.split(" ");
2500
- return /* @__PURE__ */ jsx46("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs31("span", { className: "inline-flex items-center gap-1", children: [
2501
- i > 0 ? /* @__PURE__ */ jsx46("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2502
- /* @__PURE__ */ jsx46("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2750
+ return /* @__PURE__ */ jsx48("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs33("span", { className: "inline-flex items-center gap-1", children: [
2751
+ i > 0 ? /* @__PURE__ */ jsx48("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2752
+ /* @__PURE__ */ jsx48("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2503
2753
  ] }, k)) });
2504
2754
  }
2505
2755
  function ReportShortcutsHelp({ open, onClose }) {
2506
- const dialogRef = useRef(null);
2507
- useEffect3(() => {
2756
+ const dialogRef = useRef2(null);
2757
+ useEffect5(() => {
2508
2758
  const dialog = dialogRef.current;
2509
2759
  if (!dialog) return;
2510
2760
  if (open && !dialog.open) dialog.showModal();
2511
2761
  else if (!open && dialog.open) dialog.close();
2512
2762
  }, [open]);
2513
- return /* @__PURE__ */ jsxs31(
2763
+ return /* @__PURE__ */ jsxs33(
2514
2764
  "dialog",
2515
2765
  {
2516
2766
  ref: dialogRef,
@@ -2518,20 +2768,20 @@ function ReportShortcutsHelp({ open, onClose }) {
2518
2768
  "aria-label": "Keyboard shortcuts",
2519
2769
  onClose,
2520
2770
  children: [
2521
- /* @__PURE__ */ jsx46("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2522
- /* @__PURE__ */ jsx46("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs31("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2523
- /* @__PURE__ */ jsx46("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx46(Keys, { keys: s.keys }) }),
2524
- /* @__PURE__ */ jsx46("dd", { className: "text-sm text-muted-foreground", children: s.description })
2771
+ /* @__PURE__ */ jsx48("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2772
+ /* @__PURE__ */ jsx48("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs33("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2773
+ /* @__PURE__ */ jsx48("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx48(Keys, { keys: s.keys }) }),
2774
+ /* @__PURE__ */ jsx48("dd", { className: "text-sm text-muted-foreground", children: s.description })
2525
2775
  ] }, s.keys)) }),
2526
- /* @__PURE__ */ jsx46("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx46(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2776
+ /* @__PURE__ */ jsx48("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx48(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2527
2777
  ]
2528
2778
  }
2529
2779
  );
2530
2780
  }
2531
2781
 
2532
2782
  // src/interactive/ReportToc.tsx
2533
- import { useEffect as useEffect4, useMemo as useMemo2, useState as useState5 } from "react";
2534
- import { jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2783
+ import { useEffect as useEffect6, useMemo as useMemo2, useState as useState7 } from "react";
2784
+ import { jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
2535
2785
  var DOT = {
2536
2786
  passed: "bg-pass",
2537
2787
  failed: "bg-fail",
@@ -2540,12 +2790,12 @@ var DOT = {
2540
2790
  };
2541
2791
  function TocContent({ onNavigate }) {
2542
2792
  const report = useReport();
2543
- const [activeId, setActiveId] = useState5(null);
2793
+ const [activeId, setActiveId] = useState7(null);
2544
2794
  const scenarioIds = useMemo2(
2545
2795
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
2546
2796
  [report]
2547
2797
  );
2548
- useEffect4(() => {
2798
+ useEffect6(() => {
2549
2799
  if (typeof IntersectionObserver === "undefined") return;
2550
2800
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
2551
2801
  if (els.length === 0) return;
@@ -2560,11 +2810,11 @@ function TocContent({ onNavigate }) {
2560
2810
  return () => observer.disconnect();
2561
2811
  }, [scenarioIds]);
2562
2812
  if (report.features.length === 0) return null;
2563
- return /* @__PURE__ */ jsx47("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs32("li", { children: [
2564
- /* @__PURE__ */ jsx47("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2565
- /* @__PURE__ */ jsx47("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2813
+ return /* @__PURE__ */ jsx49("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs34("li", { children: [
2814
+ /* @__PURE__ */ jsx49("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2815
+ /* @__PURE__ */ jsx49("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2566
2816
  const active = activeId === scenario.id;
2567
- return /* @__PURE__ */ jsx47("li", { children: /* @__PURE__ */ jsxs32(
2817
+ return /* @__PURE__ */ jsx49("li", { children: /* @__PURE__ */ jsxs34(
2568
2818
  "a",
2569
2819
  {
2570
2820
  href: `#${scenario.id}`,
@@ -2579,14 +2829,14 @@ function TocContent({ onNavigate }) {
2579
2829
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
2580
2830
  ),
2581
2831
  children: [
2582
- /* @__PURE__ */ jsx47(
2832
+ /* @__PURE__ */ jsx49(
2583
2833
  "span",
2584
2834
  {
2585
2835
  "aria-hidden": true,
2586
2836
  className: cn("mt-1 size-1.5 shrink-0 rounded-full", DOT[scenario.status])
2587
2837
  }
2588
2838
  ),
2589
- /* @__PURE__ */ jsx47("span", { className: "break-words", children: scenario.title })
2839
+ /* @__PURE__ */ jsx49("span", { className: "break-words", children: scenario.title })
2590
2840
  ]
2591
2841
  }
2592
2842
  ) }, scenario.id);
@@ -2596,48 +2846,48 @@ function TocContent({ onNavigate }) {
2596
2846
  function ReportToc() {
2597
2847
  const report = useReport();
2598
2848
  if (report.features.length === 0) return null;
2599
- return /* @__PURE__ */ jsxs32(
2849
+ return /* @__PURE__ */ jsxs34(
2600
2850
  "nav",
2601
2851
  {
2602
2852
  "aria-label": "Table of contents",
2603
2853
  className: "sticky top-4 hidden h-fit w-64 shrink-0 self-start text-xs lg:block",
2604
2854
  children: [
2605
- /* @__PURE__ */ jsx47("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2606
- /* @__PURE__ */ jsx47(TocContent, {})
2855
+ /* @__PURE__ */ jsx49("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2856
+ /* @__PURE__ */ jsx49(TocContent, {})
2607
2857
  ]
2608
2858
  }
2609
2859
  );
2610
2860
  }
2611
2861
 
2612
2862
  // src/interactive/ReportTocDrawer.tsx
2613
- import { useState as useState6 } from "react";
2863
+ import { useState as useState8 } from "react";
2614
2864
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2615
2865
  import { ListIcon, XIcon } from "lucide-react";
2616
- import { jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
2866
+ import { jsx as jsx50, jsxs as jsxs35 } from "react/jsx-runtime";
2617
2867
  function ReportTocDrawer() {
2618
2868
  const report = useReport();
2619
- const [open, setOpen] = useState6(false);
2869
+ const [open, setOpen] = useState8(false);
2620
2870
  if (report.features.length === 0) return null;
2621
- return /* @__PURE__ */ jsxs33(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2622
- /* @__PURE__ */ jsxs33(DialogPrimitive.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2623
- /* @__PURE__ */ jsx48(ListIcon, { "aria-hidden": true }),
2871
+ return /* @__PURE__ */ jsxs35(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2872
+ /* @__PURE__ */ jsxs35(DialogPrimitive.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2873
+ /* @__PURE__ */ jsx50(ListIcon, { "aria-hidden": true }),
2624
2874
  " Contents"
2625
2875
  ] }),
2626
- /* @__PURE__ */ jsxs33(DialogPrimitive.Portal, { children: [
2627
- /* @__PURE__ */ jsx48(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2628
- /* @__PURE__ */ jsxs33(DialogPrimitive.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2629
- /* @__PURE__ */ jsxs33("div", { className: "flex items-center justify-between", children: [
2630
- /* @__PURE__ */ jsx48(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2631
- /* @__PURE__ */ jsx48(
2876
+ /* @__PURE__ */ jsxs35(DialogPrimitive.Portal, { children: [
2877
+ /* @__PURE__ */ jsx50(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2878
+ /* @__PURE__ */ jsxs35(DialogPrimitive.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2879
+ /* @__PURE__ */ jsxs35("div", { className: "flex items-center justify-between", children: [
2880
+ /* @__PURE__ */ jsx50(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2881
+ /* @__PURE__ */ jsx50(
2632
2882
  DialogPrimitive.Close,
2633
2883
  {
2634
2884
  "aria-label": "Close contents",
2635
2885
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
2636
- children: /* @__PURE__ */ jsx48(XIcon, { "aria-hidden": true })
2886
+ children: /* @__PURE__ */ jsx50(XIcon, { "aria-hidden": true })
2637
2887
  }
2638
2888
  )
2639
2889
  ] }),
2640
- /* @__PURE__ */ jsx48(TocContent, { onNavigate: () => setOpen(false) })
2890
+ /* @__PURE__ */ jsx50(TocContent, { onNavigate: () => setOpen(false) })
2641
2891
  ] })
2642
2892
  ] })
2643
2893
  ] });
@@ -2671,10 +2921,10 @@ function failuresFirst(report) {
2671
2921
  }
2672
2922
 
2673
2923
  // src/interactive/use-deep-link-scroll.ts
2674
- import { useEffect as useEffect5, useRef as useRef2 } from "react";
2924
+ import { useEffect as useEffect7, useRef as useRef3 } from "react";
2675
2925
  function useDeepLinkScroll() {
2676
- const lastAnchor = useRef2(null);
2677
- useEffect5(() => {
2926
+ const lastAnchor = useRef3(null);
2927
+ useEffect7(() => {
2678
2928
  function scrollToHash() {
2679
2929
  const { anchor } = splitHash(window.location.hash);
2680
2930
  if (!anchor || anchor === lastAnchor.current) return;
@@ -2690,7 +2940,7 @@ function useDeepLinkScroll() {
2690
2940
  }
2691
2941
 
2692
2942
  // src/interactive/use-keyboard-shortcuts.ts
2693
- import { useEffect as useEffect6, useRef as useRef3 } from "react";
2943
+ import { useEffect as useEffect8, useRef as useRef4 } from "react";
2694
2944
  function isEditableTarget(target) {
2695
2945
  if (!(target instanceof HTMLElement)) return false;
2696
2946
  const tag = target.tagName;
@@ -2699,9 +2949,9 @@ function isEditableTarget(target) {
2699
2949
  return false;
2700
2950
  }
2701
2951
  function useKeyboardShortcuts(handlers) {
2702
- const ref = useRef3(handlers);
2952
+ const ref = useRef4(handlers);
2703
2953
  ref.current = handlers;
2704
- useEffect6(() => {
2954
+ useEffect8(() => {
2705
2955
  function onKeyDown(e) {
2706
2956
  const h = ref.current;
2707
2957
  if (e.key === "Escape") {
@@ -2745,8 +2995,8 @@ function useKeyboardShortcuts(handlers) {
2745
2995
  }
2746
2996
 
2747
2997
  // src/interactive/use-theme.ts
2748
- import { useCallback as useCallback3, useEffect as useEffect7, useState as useState7 } from "react";
2749
- var KEY = "es-theme";
2998
+ import { useCallback as useCallback3, useEffect as useEffect9, useState as useState9 } from "react";
2999
+ var KEY2 = "es-theme";
2750
3000
  function systemDark() {
2751
3001
  return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
2752
3002
  }
@@ -2760,19 +3010,19 @@ function applyTheme(pref) {
2760
3010
  }
2761
3011
  function initialPref() {
2762
3012
  if (typeof localStorage !== "undefined") {
2763
- const stored = localStorage.getItem(KEY);
3013
+ const stored = localStorage.getItem(KEY2);
2764
3014
  if (stored === "light" || stored === "dark" || stored === "system") return stored;
2765
3015
  }
2766
3016
  return "system";
2767
3017
  }
2768
3018
  function useTheme() {
2769
- const [pref, setPrefState] = useState7("system");
2770
- useEffect7(() => {
3019
+ const [pref, setPrefState] = useState9("system");
3020
+ useEffect9(() => {
2771
3021
  const p = initialPref();
2772
3022
  setPrefState(p);
2773
3023
  applyTheme(p);
2774
3024
  }, []);
2775
- useEffect7(() => {
3025
+ useEffect9(() => {
2776
3026
  if (typeof matchMedia === "undefined") return;
2777
3027
  const mq = matchMedia("(prefers-color-scheme: dark)");
2778
3028
  const onChange = () => {
@@ -2785,7 +3035,7 @@ function useTheme() {
2785
3035
  setPrefState(p);
2786
3036
  applyTheme(p);
2787
3037
  try {
2788
- localStorage.setItem(KEY, p);
3038
+ localStorage.setItem(KEY2, p);
2789
3039
  } catch {
2790
3040
  }
2791
3041
  }, []);
@@ -2793,19 +3043,19 @@ function useTheme() {
2793
3043
  }
2794
3044
 
2795
3045
  // src/interactive/use-url-state.ts
2796
- import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef4, useState as useState8 } from "react";
3046
+ import { useCallback as useCallback4, useEffect as useEffect10, useRef as useRef5, useState as useState10 } from "react";
2797
3047
  var WRITE_DELAY_MS = 250;
2798
3048
  function useUrlState() {
2799
- const [state, setState] = useState8(DEFAULT_URL_STATE);
2800
- const ready = useRef4(false);
2801
- useEffect8(() => {
3049
+ const [state, setState] = useState10(DEFAULT_URL_STATE);
3050
+ const ready = useRef5(false);
3051
+ useEffect10(() => {
2802
3052
  const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2803
3053
  read();
2804
3054
  ready.current = true;
2805
3055
  window.addEventListener("hashchange", read);
2806
3056
  return () => window.removeEventListener("hashchange", read);
2807
3057
  }, []);
2808
- useEffect8(() => {
3058
+ useEffect10(() => {
2809
3059
  if (!ready.current) return;
2810
3060
  const timer = setTimeout(
2811
3061
  () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
@@ -3041,12 +3291,12 @@ function reportTools(deps) {
3041
3291
  }
3042
3292
 
3043
3293
  // src/interactive/ReportInteractive.tsx
3044
- import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
3294
+ import { Fragment as Fragment16, jsx as jsx51, jsxs as jsxs36 } from "react/jsx-runtime";
3045
3295
  function ReportInteractive(props) {
3046
3296
  const { report, className, title, dataTheme } = props;
3047
3297
  const result = unwrapReport(report);
3048
3298
  if (!result.ok) {
3049
- return /* @__PURE__ */ jsx49(
3299
+ return /* @__PURE__ */ jsx51(
3050
3300
  ReportErrorShell,
3051
3301
  {
3052
3302
  error: result.error,
@@ -3056,7 +3306,7 @@ function ReportInteractive(props) {
3056
3306
  }
3057
3307
  );
3058
3308
  }
3059
- return /* @__PURE__ */ jsx49(ReportInteractiveView, { ...props, report: result.data });
3309
+ return /* @__PURE__ */ jsx51(ReportInteractiveView, { ...props, report: result.data });
3060
3310
  }
3061
3311
  var THEME_OPTIONS = [
3062
3312
  { value: "light", label: "Light", Icon: Sun },
@@ -3067,7 +3317,7 @@ function ThemeSegment({
3067
3317
  pref,
3068
3318
  onPref
3069
3319
  }) {
3070
- return /* @__PURE__ */ jsx49(
3320
+ return /* @__PURE__ */ jsx51(
3071
3321
  ToggleGroup,
3072
3322
  {
3073
3323
  size: "sm",
@@ -3077,14 +3327,14 @@ function ThemeSegment({
3077
3327
  if (next) onPref(next);
3078
3328
  },
3079
3329
  "aria-label": "Color theme",
3080
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx49(
3330
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx51(
3081
3331
  ToggleGroupItem,
3082
3332
  {
3083
3333
  value,
3084
3334
  "aria-label": label,
3085
3335
  title: label,
3086
3336
  className: "px-2",
3087
- children: /* @__PURE__ */ jsx49(Icon, {})
3337
+ children: /* @__PURE__ */ jsx51(Icon, {})
3088
3338
  },
3089
3339
  value
3090
3340
  ))
@@ -3101,7 +3351,9 @@ function ReportInteractiveView({
3101
3351
  hideHeader = false,
3102
3352
  hideToc = false,
3103
3353
  staleAfterDays = 7,
3104
- scenarioHistory
3354
+ scenarioHistory,
3355
+ share = false,
3356
+ shareCommand
3105
3357
  }) {
3106
3358
  const [urlState, setUrlState] = useUrlState();
3107
3359
  const { query, status: statusFilter, tags: activeTags, detail } = urlState;
@@ -3117,9 +3369,9 @@ function ReportInteractiveView({
3117
3369
  (next) => setUrlState({ detail: next }),
3118
3370
  [setUrlState]
3119
3371
  );
3120
- const [helpOpen, setHelpOpen] = useState9(false);
3372
+ const [helpOpen, setHelpOpen] = useState11(false);
3121
3373
  const { pref: themePref, setPref: setThemePref } = useTheme();
3122
- const searchRef = useRef5(null);
3374
+ const searchRef = useRef6(null);
3123
3375
  const expandId = useId2();
3124
3376
  const docsId = useId2();
3125
3377
  const deferredQuery = useDeferredValue(query);
@@ -3159,8 +3411,8 @@ function ReportInteractiveView({
3159
3411
  }),
3160
3412
  [activeTags, setUrlState]
3161
3413
  );
3162
- const [toast, setToast] = useState9(null);
3163
- const toastTimer = useRef5(null);
3414
+ const [toast, setToast] = useState11(null);
3415
+ const toastTimer = useRef6(null);
3164
3416
  const showToast = useCallback5((msg) => {
3165
3417
  setToast(msg);
3166
3418
  if (toastTimer.current) clearTimeout(toastTimer.current);
@@ -3185,7 +3437,11 @@ function ReportInteractiveView({
3185
3437
  }),
3186
3438
  [copy]
3187
3439
  );
3188
- const failureIndexRef = useRef5(0);
3440
+ const copyAllFailures = useCallback5(
3441
+ () => copy(failuresToPrompt(failedScenarios(filtered)), "Failures copied"),
3442
+ [copy, filtered]
3443
+ );
3444
+ const failureIndexRef = useRef6(0);
3189
3445
  const allCollapsibleIds = useMemo3(
3190
3446
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
3191
3447
  [report]
@@ -3202,7 +3458,7 @@ function ReportInteractiveView({
3202
3458
  () => collapse.collapseAll(allCollapsibleIds),
3203
3459
  [collapse, allCollapsibleIds]
3204
3460
  );
3205
- const [expandedAll, setExpandedAll] = useState9(
3461
+ const [expandedAll, setExpandedAll] = useState11(
3206
3462
  () => report.summary.failed === 0
3207
3463
  );
3208
3464
  const setExpanded = useCallback5(
@@ -3213,7 +3469,7 @@ function ReportInteractiveView({
3213
3469
  },
3214
3470
  [collapse, collapseAll]
3215
3471
  );
3216
- const [agentFilter, setAgentFilter] = useState9(null);
3472
+ const [agentFilter, setAgentFilter] = useState11(null);
3217
3473
  useTools(
3218
3474
  reportTools({
3219
3475
  report,
@@ -3260,13 +3516,13 @@ function ReportInteractiveView({
3260
3516
  const totalScenarios = report.summary.total;
3261
3517
  const matchedScenarios = filtered.summary.total;
3262
3518
  const showToc = !hideToc && totalScenarios > 3;
3263
- return /* @__PURE__ */ jsx49(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx49(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx49(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx49(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx49(
3519
+ return /* @__PURE__ */ jsx51(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx51(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx51(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx51(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx51(
3264
3520
  ReportRoot,
3265
3521
  {
3266
3522
  report: filtered,
3267
3523
  customRenderers,
3268
3524
  renderers,
3269
- children: /* @__PURE__ */ jsxs34(
3525
+ children: /* @__PURE__ */ jsxs36(
3270
3526
  "main",
3271
3527
  {
3272
3528
  className: cn("es-report", "es-report-interactive", className),
@@ -3275,8 +3531,8 @@ function ReportInteractiveView({
3275
3531
  "data-theme": dataTheme,
3276
3532
  "data-detail-level": detail,
3277
3533
  children: [
3278
- /* @__PURE__ */ jsxs34("header", { className: "es-report-header", children: [
3279
- /* @__PURE__ */ jsxs34(
3534
+ /* @__PURE__ */ jsxs36("header", { className: "es-report-header", children: [
3535
+ /* @__PURE__ */ jsxs36(
3280
3536
  "div",
3281
3537
  {
3282
3538
  "data-es-navbar": true,
@@ -3285,9 +3541,9 @@ function ReportInteractiveView({
3285
3541
  hideHeader ? "justify-end" : "justify-between"
3286
3542
  ),
3287
3543
  children: [
3288
- hideHeader ? null : /* @__PURE__ */ jsx49("h1", { className: "min-w-0", children: title ?? "Story Report" }),
3289
- /* @__PURE__ */ jsxs34("div", { className: "flex items-center gap-2", children: [
3290
- /* @__PURE__ */ jsx49(
3544
+ hideHeader ? null : /* @__PURE__ */ jsx51("h1", { className: "min-w-0", children: title ?? "Story Report" }),
3545
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
3546
+ /* @__PURE__ */ jsx51(
3291
3547
  ReportSearch,
3292
3548
  {
3293
3549
  ref: searchRef,
@@ -3297,16 +3553,17 @@ function ReportInteractiveView({
3297
3553
  totalCount: totalScenarios
3298
3554
  }
3299
3555
  ),
3300
- /* @__PURE__ */ jsx49(ThemeSegment, { pref: themePref, onPref: setThemePref })
3556
+ share ? /* @__PURE__ */ jsx51(ReportShare, { command: shareCommand, onCopy: copy }) : null,
3557
+ /* @__PURE__ */ jsx51(ThemeSegment, { pref: themePref, onPref: setThemePref })
3301
3558
  ] })
3302
3559
  ]
3303
3560
  }
3304
3561
  ),
3305
- hideHeader ? null : /* @__PURE__ */ jsxs34(Fragment14, { children: [
3306
- /* @__PURE__ */ jsx49(ReportSummary, {}),
3307
- /* @__PURE__ */ jsx49(ReportMeta, {})
3562
+ hideHeader ? null : /* @__PURE__ */ jsxs36(Fragment16, { children: [
3563
+ /* @__PURE__ */ jsx51(ReportSummary, {}),
3564
+ /* @__PURE__ */ jsx51(ReportMeta, {})
3308
3565
  ] }),
3309
- /* @__PURE__ */ jsx49(
3566
+ /* @__PURE__ */ jsx51(
3310
3567
  ReportFreshness,
3311
3568
  {
3312
3569
  lastRunMs: reportLastRunMs(report),
@@ -3314,14 +3571,15 @@ function ReportInteractiveView({
3314
3571
  staleAfterDays
3315
3572
  }
3316
3573
  ),
3317
- /* @__PURE__ */ jsx49(
3574
+ /* @__PURE__ */ jsx51(
3318
3575
  ReportLastRunDelta,
3319
3576
  {
3320
3577
  history: scenarioHistory,
3321
3578
  report
3322
3579
  }
3323
3580
  ),
3324
- /* @__PURE__ */ jsx49(
3581
+ /* @__PURE__ */ jsx51(ReportSinceLastVisit, { report }),
3582
+ /* @__PURE__ */ jsx51(
3325
3583
  ReportFilters,
3326
3584
  {
3327
3585
  statuses: statusOptions,
@@ -3332,10 +3590,10 @@ function ReportInteractiveView({
3332
3590
  onToggleTag: toggleTag
3333
3591
  }
3334
3592
  ),
3335
- /* @__PURE__ */ jsxs34("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3336
- showToc ? /* @__PURE__ */ jsx49(ReportTocDrawer, {}) : null,
3337
- /* @__PURE__ */ jsxs34("div", { className: "flex items-center gap-2", children: [
3338
- /* @__PURE__ */ jsx49(
3593
+ /* @__PURE__ */ jsxs36("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3594
+ showToc ? /* @__PURE__ */ jsx51(ReportTocDrawer, {}) : null,
3595
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
3596
+ /* @__PURE__ */ jsx51(
3339
3597
  Switch,
3340
3598
  {
3341
3599
  id: expandId,
@@ -3343,7 +3601,7 @@ function ReportInteractiveView({
3343
3601
  onCheckedChange: setExpanded
3344
3602
  }
3345
3603
  ),
3346
- /* @__PURE__ */ jsx49(
3604
+ /* @__PURE__ */ jsx51(
3347
3605
  "label",
3348
3606
  {
3349
3607
  htmlFor: expandId,
@@ -3352,8 +3610,8 @@ function ReportInteractiveView({
3352
3610
  }
3353
3611
  )
3354
3612
  ] }),
3355
- /* @__PURE__ */ jsxs34("div", { className: "flex items-center gap-2", children: [
3356
- /* @__PURE__ */ jsx49(
3613
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
3614
+ /* @__PURE__ */ jsx51(
3357
3615
  Switch,
3358
3616
  {
3359
3617
  id: docsId,
@@ -3361,7 +3619,7 @@ function ReportInteractiveView({
3361
3619
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
3362
3620
  }
3363
3621
  ),
3364
- /* @__PURE__ */ jsx49(
3622
+ /* @__PURE__ */ jsx51(
3365
3623
  "label",
3366
3624
  {
3367
3625
  htmlFor: docsId,
@@ -3372,7 +3630,7 @@ function ReportInteractiveView({
3372
3630
  ] })
3373
3631
  ] })
3374
3632
  ] }),
3375
- showAgentFilter ? /* @__PURE__ */ jsx49(
3633
+ showAgentFilter ? /* @__PURE__ */ jsx51(
3376
3634
  ReportAgentFilterNotice,
3377
3635
  {
3378
3636
  applied: agentFilter,
@@ -3380,17 +3638,17 @@ function ReportInteractiveView({
3380
3638
  onDismiss: () => setAgentFilter(null)
3381
3639
  }
3382
3640
  ) : null,
3383
- /* @__PURE__ */ jsx49(ReportFailureBanner, { failures }),
3384
- hasContent ? showToc ? /* @__PURE__ */ jsxs34("div", { className: "flex gap-6", children: [
3385
- /* @__PURE__ */ jsx49(ReportToc, {}),
3386
- /* @__PURE__ */ jsx49("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx49(ReportFeatureList, {}) })
3387
- ] }) : /* @__PURE__ */ jsx49("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx49(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx49(
3641
+ /* @__PURE__ */ jsx51(ReportFailureBanner, { failures, onCopyAll: copyAllFailures }),
3642
+ hasContent ? showToc ? /* @__PURE__ */ jsxs36("div", { className: "flex gap-6", children: [
3643
+ /* @__PURE__ */ jsx51(ReportToc, {}),
3644
+ /* @__PURE__ */ jsx51("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx51(ReportFeatureList, {}) })
3645
+ ] }) : /* @__PURE__ */ jsx51("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx51(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx51(
3388
3646
  ReportEmpty,
3389
3647
  {
3390
3648
  message: query ? "No scenarios match the search." : void 0
3391
3649
  }
3392
3650
  ),
3393
- /* @__PURE__ */ jsx49(
3651
+ /* @__PURE__ */ jsx51(
3394
3652
  "button",
3395
3653
  {
3396
3654
  type: "button",
@@ -3401,14 +3659,14 @@ function ReportInteractiveView({
3401
3659
  children: "?"
3402
3660
  }
3403
3661
  ),
3404
- /* @__PURE__ */ jsx49(
3662
+ /* @__PURE__ */ jsx51(
3405
3663
  ReportShortcutsHelp,
3406
3664
  {
3407
3665
  open: helpOpen,
3408
3666
  onClose: () => setHelpOpen(false)
3409
3667
  }
3410
3668
  ),
3411
- toast ? /* @__PURE__ */ jsx49(
3669
+ toast ? /* @__PURE__ */ jsx51(
3412
3670
  "div",
3413
3671
  {
3414
3672
  role: "status",
@@ -3428,11 +3686,11 @@ function ReportInteractiveView({
3428
3686
  import { useMemo as useMemo4 } from "react";
3429
3687
 
3430
3688
  // src/interactive/island-renderers.tsx
3431
- import { createElement, useEffect as useEffect10, useState as useState11 } from "react";
3689
+ import { createElement, useEffect as useEffect12, useState as useState13 } from "react";
3432
3690
 
3433
3691
  // src/components/doc/MermaidView.tsx
3434
- import { useEffect as useEffect9, useRef as useRef6, useState as useState10 } from "react";
3435
- import { Fragment as Fragment15, jsx as jsx50, jsxs as jsxs35 } from "react/jsx-runtime";
3692
+ import { useEffect as useEffect11, useRef as useRef7, useState as useState12 } from "react";
3693
+ import { Fragment as Fragment17, jsx as jsx52, jsxs as jsxs37 } from "react/jsx-runtime";
3436
3694
  function prefersDark(el) {
3437
3695
  const themed = el?.closest("[data-theme]");
3438
3696
  if (themed?.dataset.theme === "dark") return true;
@@ -3440,10 +3698,10 @@ function prefersDark(el) {
3440
3698
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
3441
3699
  }
3442
3700
  function MermaidView({ entry, load }) {
3443
- const ref = useRef6(null);
3444
- const [svg, setSvg] = useState10(null);
3445
- const [error, setError] = useState10(null);
3446
- useEffect9(() => {
3701
+ const ref = useRef7(null);
3702
+ const [svg, setSvg] = useState12(null);
3703
+ const [error, setError] = useState12(null);
3704
+ useEffect11(() => {
3447
3705
  let cancelled = false;
3448
3706
  const draw = async () => {
3449
3707
  let mermaid;
@@ -3486,9 +3744,9 @@ function MermaidView({ entry, load }) {
3486
3744
  };
3487
3745
  }, [entry.code, load]);
3488
3746
  if (svg) {
3489
- return /* @__PURE__ */ jsxs35("figure", { ref, className: "my-3", children: [
3490
- entry.title ? /* @__PURE__ */ jsx50("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3491
- /* @__PURE__ */ jsx50(
3747
+ return /* @__PURE__ */ jsxs37("figure", { ref, className: "my-3", children: [
3748
+ entry.title ? /* @__PURE__ */ jsx52("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3749
+ /* @__PURE__ */ jsx52(
3492
3750
  "div",
3493
3751
  {
3494
3752
  role: "img",
@@ -3499,8 +3757,8 @@ function MermaidView({ entry, load }) {
3499
3757
  )
3500
3758
  ] });
3501
3759
  }
3502
- return /* @__PURE__ */ jsxs35(Fragment15, { children: [
3503
- error ? /* @__PURE__ */ jsxs35(
3760
+ return /* @__PURE__ */ jsxs37(Fragment17, { children: [
3761
+ error ? /* @__PURE__ */ jsxs37(
3504
3762
  "p",
3505
3763
  {
3506
3764
  role: "alert",
@@ -3512,12 +3770,12 @@ function MermaidView({ entry, load }) {
3512
3770
  ]
3513
3771
  }
3514
3772
  ) : null,
3515
- /* @__PURE__ */ jsx50(MermaidSource, { entry, ref })
3773
+ /* @__PURE__ */ jsx52(MermaidSource, { entry, ref })
3516
3774
  ] });
3517
3775
  }
3518
3776
 
3519
3777
  // src/interactive/island-renderers.tsx
3520
- import { jsx as jsx51 } from "react/jsx-runtime";
3778
+ import { jsx as jsx53 } from "react/jsx-runtime";
3521
3779
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
3522
3780
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
3523
3781
  function cachedCdnModule(url) {
@@ -3534,8 +3792,8 @@ function cachedCdnModule(url) {
3534
3792
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3535
3793
  var loadHljs = cachedCdnModule(HLJS_ESM);
3536
3794
  function CdnHighlightedCode({ entry }) {
3537
- const [html, setHtml] = useState11(null);
3538
- useEffect10(() => {
3795
+ const [html, setHtml] = useState13(null);
3796
+ useEffect12(() => {
3539
3797
  let cancelled = false;
3540
3798
  void (async () => {
3541
3799
  try {
@@ -3551,13 +3809,13 @@ function CdnHighlightedCode({ entry }) {
3551
3809
  cancelled = true;
3552
3810
  };
3553
3811
  }, [entry.content, entry.lang]);
3554
- return /* @__PURE__ */ jsx51(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx51(
3812
+ return /* @__PURE__ */ jsx53(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx53(
3555
3813
  "code",
3556
3814
  {
3557
3815
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
3558
3816
  dangerouslySetInnerHTML: { __html: html }
3559
3817
  }
3560
- ) : /* @__PURE__ */ jsx51("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3818
+ ) : /* @__PURE__ */ jsx53("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3561
3819
  }
3562
3820
  function buildIslandRenderers(opts) {
3563
3821
  const renderers = {};
@@ -3571,7 +3829,7 @@ function buildIslandRenderers(opts) {
3571
3829
  }
3572
3830
 
3573
3831
  // src/interactive/ReportInteractiveIsland.tsx
3574
- import { jsx as jsx52 } from "react/jsx-runtime";
3832
+ import { jsx as jsx54 } from "react/jsx-runtime";
3575
3833
  function ReportInteractiveIsland({
3576
3834
  report,
3577
3835
  title,
@@ -3585,7 +3843,7 @@ function ReportInteractiveIsland({
3585
3843
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
3586
3844
  [syntaxHighlighting, mermaid]
3587
3845
  );
3588
- return /* @__PURE__ */ jsx52(
3846
+ return /* @__PURE__ */ jsx54(
3589
3847
  ReportInteractive,
3590
3848
  {
3591
3849
  report,