executable-stories-react 0.10.1 → 0.12.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.
@@ -1,45 +1,145 @@
1
1
  "use client";
2
2
 
3
3
  // src/interactive/ReportInteractive.tsx
4
+ import { Monitor, Moon, Sun } from "lucide-react";
4
5
  import {
5
6
  useCallback as useCallback5,
6
7
  useDeferredValue,
7
8
  useId as useId2,
8
9
  useMemo as useMemo3,
9
10
  useRef as useRef5,
10
- useState as useState8
11
+ useState as useState9
11
12
  } from "react";
12
13
 
13
- // src/result.ts
14
- var ok = (data) => ({ ok: true, data });
15
- function unwrapReport(value) {
16
- if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
17
- return value;
14
+ // src/components/ui/empty.tsx
15
+ import { cva } from "class-variance-authority";
16
+
17
+ // src/lib/utils.ts
18
+ import { clsx } from "clsx";
19
+ import { twMerge } from "tailwind-merge";
20
+ function cn(...inputs) {
21
+ return twMerge(clsx(inputs));
22
+ }
23
+
24
+ // src/components/ui/empty.tsx
25
+ import { jsx } from "react/jsx-runtime";
26
+ function Empty({ className, ...props }) {
27
+ return /* @__PURE__ */ jsx(
28
+ "div",
29
+ {
30
+ "data-slot": "empty",
31
+ className: cn(
32
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
33
+ className
34
+ ),
35
+ ...props
36
+ }
37
+ );
38
+ }
39
+ function EmptyHeader({ className, ...props }) {
40
+ return /* @__PURE__ */ jsx(
41
+ "div",
42
+ {
43
+ "data-slot": "empty-header",
44
+ className: cn(
45
+ "flex max-w-sm flex-col items-center gap-2 text-center",
46
+ className
47
+ ),
48
+ ...props
49
+ }
50
+ );
51
+ }
52
+ var emptyMediaVariants = cva(
53
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
54
+ {
55
+ variants: {
56
+ variant: {
57
+ default: "bg-transparent",
58
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
59
+ }
60
+ },
61
+ defaultVariants: {
62
+ variant: "default"
63
+ }
18
64
  }
19
- return ok(value);
65
+ );
66
+ function EmptyTitle({ className, ...props }) {
67
+ return /* @__PURE__ */ jsx(
68
+ "div",
69
+ {
70
+ "data-slot": "empty-title",
71
+ className: cn("text-lg font-medium tracking-tight", className),
72
+ ...props
73
+ }
74
+ );
20
75
  }
21
76
 
22
- // src/context/ReportRoot.tsx
23
- import { useMemo } from "react";
77
+ // src/components/ReportEmpty.tsx
78
+ import { jsx as jsx2 } from "react/jsx-runtime";
79
+ function ReportEmpty({ message }) {
80
+ return /* @__PURE__ */ jsx2(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx2(EmptyHeader, { children: /* @__PURE__ */ jsx2(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
81
+ }
82
+
83
+ // src/hooks/useReport.ts
84
+ import { useContext } from "react";
24
85
 
25
86
  // src/context/ReportContext.ts
26
87
  import { createContext } from "react";
27
88
  var ReportContext = createContext(null);
28
89
 
29
- // src/components/ui/badge.tsx
30
- import "react";
31
- import { cva } from "class-variance-authority";
32
- import { useRender } from "@base-ui/react/use-render";
90
+ // src/hooks/useReport.ts
91
+ function useReport() {
92
+ const ctx = useContext(ReportContext);
93
+ if (!ctx) {
94
+ throw new Error(
95
+ "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
96
+ );
97
+ }
98
+ return ctx.report;
99
+ }
33
100
 
34
- // src/lib/utils.ts
35
- import { clsx } from "clsx";
36
- import { twMerge } from "tailwind-merge";
37
- function cn(...inputs) {
38
- return twMerge(clsx(inputs));
101
+ // src/components/ReportScenario.tsx
102
+ import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
103
+
104
+ // src/lib/step-params.tsx
105
+ import { jsx as jsx3 } from "react/jsx-runtime";
106
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
107
+ function highlightStepParams(text) {
108
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
109
+ if (matches.length === 0) return text;
110
+ const nodes = [];
111
+ let lastIndex = 0;
112
+ matches.forEach((match, i) => {
113
+ const start = match.index;
114
+ const end = start + match[0].length;
115
+ if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
116
+ nodes.push(
117
+ /* @__PURE__ */ jsx3("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
118
+ );
119
+ lastIndex = end;
120
+ });
121
+ if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
122
+ return nodes;
123
+ }
124
+
125
+ // src/components/ReportSteps.tsx
126
+ import {
127
+ assertionState,
128
+ assertiveSteps
129
+ } from "executable-stories-core/utils/assertive-steps";
130
+ import { formatDuration } from "executable-stories-core/utils/duration";
131
+
132
+ // src/components/doc/DocNote.tsx
133
+ import { jsx as jsx4 } from "react/jsx-runtime";
134
+ function DocNote({ entry }) {
135
+ return /* @__PURE__ */ jsx4("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
39
136
  }
40
137
 
41
138
  // src/components/ui/badge.tsx
42
- var badgeVariants = cva(
139
+ import "react";
140
+ import { cva as cva2 } from "class-variance-authority";
141
+ import { useRender } from "@base-ui/react/use-render";
142
+ var badgeVariants = cva2(
43
143
  "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
44
144
  {
45
145
  variants: {
@@ -84,334 +184,119 @@ function Badge({
84
184
  });
85
185
  }
86
186
 
87
- // src/components/doc/NarrativeBlocks.tsx
88
- import { jsx, jsxs } from "react/jsx-runtime";
89
- var FILE_TREE_TYPE = "file-tree";
90
- var DATA_MODEL_TYPE = "data-model";
91
- function isChangeKind(value) {
92
- return value === "added" || value === "modified" || value === "removed" || value === "renamed";
187
+ // src/components/doc/DocTag.tsx
188
+ import { jsx as jsx5 } from "react/jsx-runtime";
189
+ function DocTag({ entry }) {
190
+ return /* @__PURE__ */ jsx5("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(Badge, { variant: "tag", children: n }) }, n)) });
93
191
  }
94
- function isRecord(value) {
95
- return typeof value === "object" && value !== null && !Array.isArray(value);
192
+
193
+ // src/components/doc/DocKv.tsx
194
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
195
+ function formatValue(value) {
196
+ if (value === null) return "null";
197
+ if (typeof value === "string") return value;
198
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
199
+ try {
200
+ return JSON.stringify(value);
201
+ } catch {
202
+ return String(value);
203
+ }
96
204
  }
97
- function str(value) {
98
- return typeof value === "string" && value !== "" ? value : void 0;
205
+ function DocKv({ entry }) {
206
+ return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
207
+ /* @__PURE__ */ jsx6("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
208
+ /* @__PURE__ */ jsx6("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
209
+ ] });
99
210
  }
100
- function AuthoredMarker({ authored }) {
101
- if (authored !== "agent") return null;
102
- return /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
211
+
212
+ // src/hooks/useRenderers.ts
213
+ import { useContext as useContext2 } from "react";
214
+ var EMPTY_CUSTOM = {};
215
+ var EMPTY_RENDERERS = {};
216
+ function useCustomRenderers() {
217
+ const ctx = useContext2(ReportContext);
218
+ return ctx?.customRenderers ?? EMPTY_CUSTOM;
103
219
  }
104
- function BlockFrame({
105
- title,
106
- authored,
107
- children
108
- }) {
109
- return /* @__PURE__ */ jsxs("figure", { className: "my-3", children: [
110
- title || authored === "agent" ? /* @__PURE__ */ jsxs("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
111
- title ? /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
112
- /* @__PURE__ */ jsx(AuthoredMarker, { authored })
113
- ] }) : null,
114
- children
220
+ function useBuiltinRenderers() {
221
+ const ctx = useContext2(ReportContext);
222
+ return ctx?.renderers ?? EMPTY_RENDERERS;
223
+ }
224
+
225
+ // src/components/doc/DocCode.tsx
226
+ import { Fragment, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
227
+ function CodeFigure({ label, children }) {
228
+ return /* @__PURE__ */ jsxs2("figure", { className: "my-3", children: [
229
+ label ? /* @__PURE__ */ jsx7("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
230
+ /* @__PURE__ */ jsx7("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
115
231
  ] });
116
232
  }
117
- function ChangeBadge({ change }) {
118
- return /* @__PURE__ */ jsx(Badge, { variant: "tag", className: "text-[10px]", children: change });
233
+ function DocCode({ entry }) {
234
+ const renderers = useBuiltinRenderers();
235
+ if (renderers.code) {
236
+ return /* @__PURE__ */ jsx7(Fragment, { children: renderers.code(entry) });
237
+ }
238
+ return /* @__PURE__ */ jsx7(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx7("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
119
239
  }
120
- function parseFileTree(data) {
121
- if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
122
- const files = [];
123
- for (const raw of data.files) {
124
- if (typeof raw === "string") {
125
- if (raw) files.push({ path: raw });
126
- continue;
240
+
241
+ // src/components/doc/DocTable.tsx
242
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
243
+ function DocTable({ entry }) {
244
+ return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
245
+ entry.label ? /* @__PURE__ */ jsx8("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
246
+ /* @__PURE__ */ jsx8("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs3("table", { className: "w-full border-collapse text-sm", children: [
247
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsx8("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx8("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
248
+ /* @__PURE__ */ jsx8("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx8("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx8("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
249
+ ] }) })
250
+ ] });
251
+ }
252
+
253
+ // src/components/doc/DocLink.tsx
254
+ import { jsx as jsx9 } from "react/jsx-runtime";
255
+ function DocLink({ entry }) {
256
+ return /* @__PURE__ */ jsx9(
257
+ "a",
258
+ {
259
+ className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
260
+ href: entry.url,
261
+ rel: "noreferrer noopener",
262
+ target: "_blank",
263
+ children: entry.label
127
264
  }
128
- if (!isRecord(raw)) continue;
129
- const path = str(raw.path);
130
- if (!path) continue;
131
- files.push({
132
- path,
133
- change: isChangeKind(raw.change) ? raw.change : void 0,
134
- note: str(raw.note)
135
- });
136
- }
137
- if (files.length === 0) return void 0;
138
- return { title: str(data.title), authored: str(data.authored), files };
265
+ );
139
266
  }
140
- function buildTree(files) {
141
- const root = { name: "", children: /* @__PURE__ */ new Map() };
142
- for (const file of files) {
143
- const segments = file.path.split("/").filter(Boolean);
144
- let node = root;
145
- segments.forEach((segment, index) => {
146
- let child = node.children.get(segment);
147
- if (!child) {
148
- child = { name: segment, children: /* @__PURE__ */ new Map() };
149
- node.children.set(segment, child);
150
- }
151
- if (index === segments.length - 1) child.file = file;
152
- node = child;
153
- });
154
- }
155
- return root;
267
+
268
+ // src/lib/markdown.ts
269
+ import { marked } from "marked";
270
+
271
+ // src/lib/url.ts
272
+ import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
273
+
274
+ // src/lib/markdown.ts
275
+ function safeMarkdownHtml(markdown) {
276
+ const raw = marked.parse(markdown, { async: false });
277
+ return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
156
278
  }
157
- function sortedChildren(node) {
158
- return [...node.children.values()].sort((a, b) => {
159
- const aDir = a.children.size > 0;
160
- const bDir = b.children.size > 0;
161
- if (aDir !== bDir) return aDir ? -1 : 1;
162
- return a.name.localeCompare(b.name);
163
- });
279
+ function dedent(text) {
280
+ const lines = text.replace(/\t/g, " ").split("\n");
281
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
282
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
283
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
164
284
  }
165
- function TreeBranch({ node, depth }) {
166
- const children = sortedChildren(node);
167
- return /* @__PURE__ */ jsx("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
168
- const isDir = child.children.size > 0;
169
- return /* @__PURE__ */ jsxs("li", { children: [
170
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [
171
- /* @__PURE__ */ jsx(
172
- "span",
173
- {
174
- className: cn(
175
- "font-mono text-xs",
176
- isDir ? "text-muted-foreground" : "text-foreground"
177
- ),
178
- children: isDir ? `${child.name}/` : child.name
179
- }
180
- ),
181
- child.file?.change ? /* @__PURE__ */ jsx(ChangeBadge, { change: child.file.change }) : null,
182
- child.file?.note ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
183
- ] }),
184
- isDir ? /* @__PURE__ */ jsx(TreeBranch, { node: child, depth: depth + 1 }) : null
185
- ] }, child.name);
186
- }) });
187
- }
188
- function FileTreeBlock({ entry }) {
189
- const data = parseFileTree(entry.data);
190
- if (!data) return /* @__PURE__ */ jsx(UnreadableBlock, { entry });
191
- return /* @__PURE__ */ jsx(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ jsx("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ jsx(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
192
- }
193
- function parseDataModel(data) {
194
- if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
195
- const fields = [];
196
- for (const raw of data.fields) {
197
- if (!isRecord(raw)) continue;
198
- const name = str(raw.name);
199
- if (!name) continue;
200
- fields.push({
201
- name,
202
- type: str(raw.type),
203
- note: str(raw.note),
204
- change: isChangeKind(raw.change) ? raw.change : void 0
205
- });
206
- }
207
- if (fields.length === 0) return void 0;
208
- return {
209
- title: str(data.title),
210
- authored: str(data.authored),
211
- name: str(data.name),
212
- fields
213
- };
214
- }
215
- function DataModelBlock({ entry }) {
216
- const data = parseDataModel(entry.data);
217
- if (!data) return /* @__PURE__ */ jsx(UnreadableBlock, { entry });
218
- const hasNotes = data.fields.some((f) => f.note);
219
- return /* @__PURE__ */ jsx(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ jsx("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-sm", children: [
220
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-muted/50", children: [
221
- /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
222
- /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
223
- hasNotes ? /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
224
- ] }) }),
225
- /* @__PURE__ */ jsx("tbody", { children: data.fields.map((field) => /* @__PURE__ */ jsxs("tr", { children: [
226
- /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [
227
- /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-foreground", children: field.name }),
228
- field.change ? /* @__PURE__ */ jsx(ChangeBadge, { change: field.change }) : null
229
- ] }) }),
230
- /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
231
- hasNotes ? /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
232
- ] }, field.name)) })
233
- ] }) }) });
234
- }
235
- function UnreadableBlock({ entry }) {
236
- return /* @__PURE__ */ jsxs("div", { className: "my-2", "data-type": entry.type, children: [
237
- /* @__PURE__ */ jsxs("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
238
- entry.type,
239
- " (unrecognised shape)"
240
- ] }),
241
- /* @__PURE__ */ jsx(
242
- "pre",
243
- {
244
- tabIndex: 0,
245
- className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
246
- children: safeStringify(entry.data)
247
- }
248
- )
249
- ] });
250
- }
251
- function safeStringify(value) {
252
- try {
253
- return JSON.stringify(value, null, 2);
254
- } catch {
255
- return String(value);
256
- }
257
- }
258
- var narrativeBlockRenderers = {
259
- [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ jsx(FileTreeBlock, { entry }),
260
- [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ jsx(DataModelBlock, { entry })
261
- };
262
-
263
- // src/context/ReportRoot.tsx
264
- import { jsx as jsx2 } from "react/jsx-runtime";
265
- var EMPTY_RENDERERS = {};
266
- function ReportRoot({
267
- report,
268
- customRenderers,
269
- renderers,
270
- children
271
- }) {
272
- const value = useMemo(
273
- () => ({
274
- report,
275
- // Narrative blocks render everywhere the report does (static SSR, the
276
- // island, Astro) without every host wiring them up. A host renderer for
277
- // the same type still wins.
278
- customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
279
- renderers: renderers ?? EMPTY_RENDERERS
280
- }),
281
- [report, customRenderers, renderers]
282
- );
283
- return /* @__PURE__ */ jsx2(ReportContext.Provider, { value, children });
284
- }
285
-
286
- // src/hooks/useReport.ts
287
- import { useContext } from "react";
288
- function useReport() {
289
- const ctx = useContext(ReportContext);
290
- if (!ctx) {
291
- throw new Error(
292
- "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
293
- );
294
- }
295
- return ctx.report;
296
- }
297
-
298
- // src/components/ReportScenario.tsx
299
- import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
300
-
301
- // src/components/ReportSteps.tsx
302
- import { formatDuration } from "executable-stories-core/utils/duration";
303
-
304
- // src/components/doc/DocNote.tsx
305
- import { jsx as jsx3 } from "react/jsx-runtime";
306
- function DocNote({ entry }) {
307
- return /* @__PURE__ */ jsx3("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
308
- }
309
-
310
- // src/components/doc/DocTag.tsx
311
- import { jsx as jsx4 } from "react/jsx-runtime";
312
- function DocTag({ entry }) {
313
- return /* @__PURE__ */ jsx4("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(Badge, { variant: "tag", children: n }) }, n)) });
314
- }
315
-
316
- // src/components/doc/DocKv.tsx
317
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
318
- function formatValue(value) {
319
- if (value === null) return "null";
320
- if (typeof value === "string") return value;
321
- if (typeof value === "number" || typeof value === "boolean") return String(value);
322
- try {
323
- return JSON.stringify(value);
324
- } catch {
325
- return String(value);
326
- }
327
- }
328
- function DocKv({ entry }) {
329
- return /* @__PURE__ */ jsxs2("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
330
- /* @__PURE__ */ jsx5("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
331
- /* @__PURE__ */ jsx5("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
332
- ] });
333
- }
334
-
335
- // src/hooks/useRenderers.ts
336
- import { useContext as useContext2 } from "react";
337
- var EMPTY_CUSTOM = {};
338
- var EMPTY_RENDERERS2 = {};
339
- function useCustomRenderers() {
340
- const ctx = useContext2(ReportContext);
341
- return ctx?.customRenderers ?? EMPTY_CUSTOM;
342
- }
343
- function useBuiltinRenderers() {
344
- const ctx = useContext2(ReportContext);
345
- return ctx?.renderers ?? EMPTY_RENDERERS2;
346
- }
347
-
348
- // src/components/doc/DocCode.tsx
349
- import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
350
- function CodeFigure({ label, children }) {
351
- return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
352
- label ? /* @__PURE__ */ jsx6("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
353
- /* @__PURE__ */ jsx6("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
354
- ] });
355
- }
356
- function DocCode({ entry }) {
357
- const renderers = useBuiltinRenderers();
358
- if (renderers.code) {
359
- return /* @__PURE__ */ jsx6(Fragment, { children: renderers.code(entry) });
360
- }
361
- return /* @__PURE__ */ jsx6(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx6("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
362
- }
363
-
364
- // src/components/doc/DocTable.tsx
365
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
366
- function DocTable({ entry }) {
367
- return /* @__PURE__ */ jsxs4("figure", { className: "my-3", children: [
368
- entry.label ? /* @__PURE__ */ jsx7("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
369
- /* @__PURE__ */ jsx7("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs4("table", { className: "w-full border-collapse text-sm", children: [
370
- /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx7("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
371
- /* @__PURE__ */ jsx7("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx7("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
372
- ] }) })
373
- ] });
374
- }
375
-
376
- // src/components/doc/DocLink.tsx
377
- import { jsx as jsx8 } from "react/jsx-runtime";
378
- function DocLink({ entry }) {
379
- return /* @__PURE__ */ jsx8(
380
- "a",
381
- {
382
- className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
383
- href: entry.url,
384
- rel: "noreferrer noopener",
385
- target: "_blank",
386
- children: entry.label
387
- }
388
- );
389
- }
390
-
391
- // src/components/doc/DocSection.tsx
392
- import { marked } from "marked";
393
-
394
- // src/lib/url.ts
395
- import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
396
-
397
- // src/components/doc/DocSection.tsx
398
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
399
285
  function neutralizeUrl(value) {
400
286
  return safeUrl(value) ?? "#";
401
287
  }
402
- function safeMarkdownHtml(markdown) {
403
- const raw = marked.parse(markdown, { async: false });
404
- return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
405
- }
288
+
289
+ // src/components/doc/DocSection.tsx
290
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
406
291
  function DocSection({ entry }) {
407
292
  const renderers = useBuiltinRenderers();
408
293
  if (renderers.section) {
409
- return /* @__PURE__ */ jsx9(Fragment2, { children: renderers.section(entry) });
294
+ return /* @__PURE__ */ jsx10(Fragment2, { children: renderers.section(entry) });
410
295
  }
411
296
  const html = safeMarkdownHtml(entry.markdown);
412
- return /* @__PURE__ */ jsxs5("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
413
- entry.title ? /* @__PURE__ */ jsx9("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
414
- /* @__PURE__ */ jsx9(
297
+ return /* @__PURE__ */ jsxs4("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
298
+ entry.title ? /* @__PURE__ */ jsx10("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
299
+ /* @__PURE__ */ jsx10(
415
300
  "div",
416
301
  {
417
302
  className: "es-doc-prose prose prose-sm max-w-none",
@@ -422,11 +307,11 @@ function DocSection({ entry }) {
422
307
  }
423
308
 
424
309
  // src/components/doc/DocMermaid.tsx
425
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
310
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
426
311
  function MermaidSource({ entry, ref }) {
427
- return /* @__PURE__ */ jsxs6("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
428
- entry.title ? /* @__PURE__ */ jsx10("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
429
- /* @__PURE__ */ jsx10(
312
+ return /* @__PURE__ */ jsxs5("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
313
+ entry.title ? /* @__PURE__ */ jsx11("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
314
+ /* @__PURE__ */ jsx11(
430
315
  "pre",
431
316
  {
432
317
  "data-mermaid": true,
@@ -440,24 +325,24 @@ function MermaidSource({ entry, ref }) {
440
325
  function DocMermaid({ entry }) {
441
326
  const renderers = useBuiltinRenderers();
442
327
  if (renderers.mermaid) {
443
- return /* @__PURE__ */ jsx10(Fragment3, { children: renderers.mermaid(entry) });
328
+ return /* @__PURE__ */ jsx11(Fragment3, { children: renderers.mermaid(entry) });
444
329
  }
445
- return /* @__PURE__ */ jsx10(MermaidSource, { entry });
330
+ return /* @__PURE__ */ jsx11(MermaidSource, { entry });
446
331
  }
447
332
 
448
333
  // src/components/doc/DocScreenshot.tsx
449
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
334
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
450
335
  function DocScreenshot({ entry }) {
451
336
  const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
452
337
  if (!src) {
453
- return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
454
- /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Screenshot unavailable" }),
455
- /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
456
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
338
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
339
+ /* @__PURE__ */ jsx12("div", { className: "font-medium", children: "Screenshot unavailable" }),
340
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
341
+ entry.alt ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
457
342
  ] });
458
343
  }
459
- return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
460
- /* @__PURE__ */ jsx11(
344
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
345
+ /* @__PURE__ */ jsx12(
461
346
  "img",
462
347
  {
463
348
  src,
@@ -466,24 +351,24 @@ function DocScreenshot({ entry }) {
466
351
  className: "max-w-full rounded-md border border-border"
467
352
  }
468
353
  ),
469
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
354
+ entry.alt ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
470
355
  ] });
471
356
  }
472
357
 
473
358
  // src/components/doc/DocVideo.tsx
474
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
359
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
475
360
  function DocVideo({ entry }) {
476
361
  if (isLocalFsPath(entry.path)) {
477
- return /* @__PURE__ */ jsxs8("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
478
- /* @__PURE__ */ jsx12("div", { className: "font-medium", children: "Video unavailable" }),
479
- /* @__PURE__ */ jsx12("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
480
- entry.caption ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
362
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
363
+ /* @__PURE__ */ jsx13("div", { className: "font-medium", children: "Video unavailable" }),
364
+ /* @__PURE__ */ jsx13("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
365
+ entry.caption ? /* @__PURE__ */ jsx13("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
481
366
  ] });
482
367
  }
483
368
  const src = safeUrl(entry.path);
484
369
  const poster = safeUrl(entry.poster);
485
- return /* @__PURE__ */ jsxs8("figure", { className: "my-3", children: [
486
- /* @__PURE__ */ jsx12(
370
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
371
+ /* @__PURE__ */ jsx13(
487
372
  "video",
488
373
  {
489
374
  controls: true,
@@ -493,21 +378,21 @@ function DocVideo({ entry }) {
493
378
  className: "max-w-full rounded-md border border-border"
494
379
  }
495
380
  ),
496
- entry.caption ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
381
+ entry.caption ? /* @__PURE__ */ jsx13("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
497
382
  ] });
498
383
  }
499
384
 
500
385
  // src/components/doc/DocHtml.tsx
501
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
386
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
502
387
  function DocHtml({ entry }) {
503
388
  const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
504
389
  const title = entry.title ?? "Embedded HTML";
505
390
  const safeHref = safeUrl(entry.url ?? entry.path);
506
391
  const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
507
- return /* @__PURE__ */ jsxs9("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
508
- /* @__PURE__ */ jsxs9("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
509
- /* @__PURE__ */ jsx13("span", { children: entry.title ?? "HTML" }),
510
- safeHref ? /* @__PURE__ */ jsx13(
392
+ return /* @__PURE__ */ jsxs8("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
393
+ /* @__PURE__ */ jsxs8("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
394
+ /* @__PURE__ */ jsx14("span", { children: entry.title ?? "HTML" }),
395
+ safeHref ? /* @__PURE__ */ jsx14(
511
396
  "a",
512
397
  {
513
398
  href: safeHref,
@@ -520,7 +405,7 @@ function DocHtml({ entry }) {
520
405
  }
521
406
  ) : null
522
407
  ] }),
523
- /* @__PURE__ */ jsx13(
408
+ /* @__PURE__ */ jsx14(
524
409
  "iframe",
525
410
  {
526
411
  ...frameProps,
@@ -535,7 +420,7 @@ function DocHtml({ entry }) {
535
420
  }
536
421
 
537
422
  // src/components/doc/DocState.tsx
538
- import { jsx as jsx14 } from "react/jsx-runtime";
423
+ import { jsx as jsx15 } from "react/jsx-runtime";
539
424
  function DocState({ entry }) {
540
425
  let json;
541
426
  try {
@@ -543,23 +428,23 @@ function DocState({ entry }) {
543
428
  } catch {
544
429
  json = String(entry.value);
545
430
  }
546
- return /* @__PURE__ */ jsx14(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ jsx14("code", { className: "font-mono text-xs language-json", children: json }) });
431
+ return /* @__PURE__ */ jsx15(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ jsx15("code", { className: "font-mono text-xs language-json", children: json }) });
547
432
  }
548
433
 
549
434
  // src/components/doc/DocCustom.tsx
550
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
435
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
551
436
  function DocCustom({ entry }) {
552
437
  const renderers = useCustomRenderers();
553
438
  const renderer = renderers[entry.type];
554
439
  if (renderer) {
555
- return /* @__PURE__ */ jsx15(Fragment4, { children: renderer(entry) });
440
+ return /* @__PURE__ */ jsx16(Fragment4, { children: renderer(entry) });
556
441
  }
557
- return /* @__PURE__ */ jsxs10("div", { className: "my-2", "data-type": entry.type, children: [
558
- /* @__PURE__ */ jsx15("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
559
- /* @__PURE__ */ jsx15("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify2(entry.data) })
442
+ return /* @__PURE__ */ jsxs9("div", { className: "my-2", "data-type": entry.type, children: [
443
+ /* @__PURE__ */ jsx16("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
444
+ /* @__PURE__ */ jsx16("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
560
445
  ] });
561
446
  }
562
- function safeStringify2(value) {
447
+ function safeStringify(value) {
563
448
  try {
564
449
  return JSON.stringify(value, null, 2);
565
450
  } catch {
@@ -568,68 +453,47 @@ function safeStringify2(value) {
568
453
  }
569
454
 
570
455
  // src/components/doc/DocEntry.tsx
571
- import { jsx as jsx16 } from "react/jsx-runtime";
456
+ import { jsx as jsx17 } from "react/jsx-runtime";
572
457
  function DocEntry({ entry }) {
573
458
  switch (entry.kind) {
574
459
  case "note":
575
- return /* @__PURE__ */ jsx16(DocNote, { entry });
460
+ return /* @__PURE__ */ jsx17(DocNote, { entry });
576
461
  case "tag":
577
- return /* @__PURE__ */ jsx16(DocTag, { entry });
462
+ return /* @__PURE__ */ jsx17(DocTag, { entry });
578
463
  case "kv":
579
- return /* @__PURE__ */ jsx16(DocKv, { entry });
464
+ return /* @__PURE__ */ jsx17(DocKv, { entry });
580
465
  case "code":
581
- return /* @__PURE__ */ jsx16(DocCode, { entry });
466
+ return /* @__PURE__ */ jsx17(DocCode, { entry });
582
467
  case "table":
583
- return /* @__PURE__ */ jsx16(DocTable, { entry });
468
+ return /* @__PURE__ */ jsx17(DocTable, { entry });
584
469
  case "link":
585
- return /* @__PURE__ */ jsx16(DocLink, { entry });
470
+ return /* @__PURE__ */ jsx17(DocLink, { entry });
586
471
  case "section":
587
- return /* @__PURE__ */ jsx16(DocSection, { entry });
472
+ return /* @__PURE__ */ jsx17(DocSection, { entry });
588
473
  case "mermaid":
589
- return /* @__PURE__ */ jsx16(DocMermaid, { entry });
474
+ return /* @__PURE__ */ jsx17(DocMermaid, { entry });
590
475
  case "screenshot":
591
- return /* @__PURE__ */ jsx16(DocScreenshot, { entry });
476
+ return /* @__PURE__ */ jsx17(DocScreenshot, { entry });
592
477
  case "video":
593
- return /* @__PURE__ */ jsx16(DocVideo, { entry });
478
+ return /* @__PURE__ */ jsx17(DocVideo, { entry });
594
479
  case "html":
595
- return /* @__PURE__ */ jsx16(DocHtml, { entry });
480
+ return /* @__PURE__ */ jsx17(DocHtml, { entry });
596
481
  case "state":
597
- return /* @__PURE__ */ jsx16(DocState, { entry });
482
+ return /* @__PURE__ */ jsx17(DocState, { entry });
598
483
  case "custom":
599
- return /* @__PURE__ */ jsx16(DocCustom, { entry });
484
+ return /* @__PURE__ */ jsx17(DocCustom, { entry });
600
485
  }
601
486
  }
602
487
 
603
488
  // src/components/ReportDocEntries.tsx
604
- import { Fragment as Fragment5, jsx as jsx17 } from "react/jsx-runtime";
489
+ import { Fragment as Fragment5, jsx as jsx18 } from "react/jsx-runtime";
605
490
  function ReportDocEntries({ entries }) {
606
491
  if (entries.length === 0) return null;
607
- return /* @__PURE__ */ jsx17(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx17(DocEntry, { entry }, i)) });
608
- }
609
-
610
- // src/lib/step-params.tsx
611
- import { jsx as jsx18 } from "react/jsx-runtime";
612
- var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
613
- function highlightStepParams(text) {
614
- const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
615
- if (matches.length === 0) return text;
616
- const nodes = [];
617
- let lastIndex = 0;
618
- matches.forEach((match, i) => {
619
- const start = match.index;
620
- const end = start + match[0].length;
621
- if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
622
- nodes.push(
623
- /* @__PURE__ */ jsx18("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
624
- );
625
- lastIndex = end;
626
- });
627
- if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
628
- return nodes;
492
+ return /* @__PURE__ */ jsx18(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx18(DocEntry, { entry }, i)) });
629
493
  }
630
494
 
631
495
  // src/components/ReportSteps.tsx
632
- import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
496
+ import { jsx as jsx19, jsxs as jsxs10 } from "react/jsx-runtime";
633
497
  var STEP_GLYPH = {
634
498
  passed: "\u2713",
635
499
  failed: "\u2717",
@@ -644,20 +508,46 @@ var STEP_GLYPH_COLOR = {
644
508
  };
645
509
  function ReportSteps({ scenario }) {
646
510
  if (scenario.steps.length === 0) return null;
647
- return /* @__PURE__ */ jsx19("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx19(ReportStepItem, { step }, step.id)) });
511
+ const unasserted = new Set(
512
+ assertionState(scenario.steps) === "unasserted" ? assertiveSteps(scenario.steps).map((step) => step.id) : []
513
+ );
514
+ return /* @__PURE__ */ jsx19("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx19(
515
+ ReportStepItem,
516
+ {
517
+ step,
518
+ unasserted: unasserted.has(step.id)
519
+ },
520
+ step.id
521
+ )) });
648
522
  }
649
- function ReportStepItem({ step }) {
523
+ function ReportStepItem({
524
+ step,
525
+ unasserted = false
526
+ }) {
650
527
  const kw = step.keyword.toLowerCase();
651
528
  const continuation = kw === "and" || kw === "but";
652
- return /* @__PURE__ */ jsxs11(
529
+ return /* @__PURE__ */ jsxs10(
653
530
  "li",
654
531
  {
655
532
  id: step.id,
656
533
  "data-status": step.status,
657
- className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
534
+ className: cn(
535
+ "py-1.5 text-[0.8125rem] leading-normal",
536
+ continuation && "pl-5"
537
+ ),
658
538
  children: [
659
- /* @__PURE__ */ jsxs11("div", { className: "flex items-baseline gap-2", children: [
660
- /* @__PURE__ */ jsx19("span", { "aria-hidden": true, className: cn("w-4 shrink-0 text-center text-xs", STEP_GLYPH_COLOR[step.status]), children: STEP_GLYPH[step.status] ?? "\u25CB" }),
539
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-baseline gap-2", children: [
540
+ /* @__PURE__ */ jsx19(
541
+ "span",
542
+ {
543
+ "aria-hidden": true,
544
+ className: cn(
545
+ "w-4 shrink-0 text-center text-xs",
546
+ STEP_GLYPH_COLOR[step.status]
547
+ ),
548
+ children: STEP_GLYPH[step.status] ?? "\u25CB"
549
+ }
550
+ ),
661
551
  /* @__PURE__ */ jsx19(
662
552
  "span",
663
553
  {
@@ -669,6 +559,7 @@ function ReportStepItem({ step }) {
669
559
  }
670
560
  ),
671
561
  /* @__PURE__ */ jsx19("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
562
+ unasserted ? /* @__PURE__ */ jsx19("span", { className: "shrink-0 rounded-full border border-pend-border bg-pend-bg px-2 py-0.5 text-[0.6875rem] font-medium text-pend", children: "No assertion" }) : null,
672
563
  step.durationMs > 0 ? /* @__PURE__ */ jsx19("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
673
564
  ] }),
674
565
  step.errorMessage ? /* @__PURE__ */ jsx19(
@@ -689,7 +580,7 @@ function ReportStepItem({ step }) {
689
580
  import { extractStoryboardFrames } from "executable-stories-core/storyboard";
690
581
  import { summarizeStateChanges } from "executable-stories-core/state-diff";
691
582
  import { isLocalFsPath as isLocalFsPath2, safeImageUrl as safeImageUrl2 } from "executable-stories-core/utils/url";
692
- import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
583
+ import { jsx as jsx20, jsxs as jsxs11 } from "react/jsx-runtime";
693
584
  var FRAME_GLYPH = {
694
585
  passed: "\u2713",
695
586
  failed: "\u2717",
@@ -705,7 +596,7 @@ var FRAME_GLYPH_COLOR = {
705
596
  function ReportStoryboard({ scenario }) {
706
597
  const frames = extractStoryboardFrames(scenario);
707
598
  if (frames.length < 2) return null;
708
- return /* @__PURE__ */ jsxs12("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
599
+ return /* @__PURE__ */ jsxs11("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
709
600
  /* @__PURE__ */ jsx20("h4", { className: "mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Storyboard" }),
710
601
  /* @__PURE__ */ jsx20("ol", { className: "flex items-start gap-3 overflow-x-auto pb-2", children: frames.map((frame) => /* @__PURE__ */ jsx20(StoryboardFrameItem, { frame }, frame.stepId)) })
711
602
  ] });
@@ -713,9 +604,9 @@ function ReportStoryboard({ scenario }) {
713
604
  function StoryboardFrameItem({ frame }) {
714
605
  const label = `${frame.keyword} ${frame.text}`;
715
606
  const hasStates = frame.states.length > 0;
716
- const caption = /* @__PURE__ */ jsxs12("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
607
+ const caption = /* @__PURE__ */ jsxs11("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
717
608
  /* @__PURE__ */ jsx20("span", { "aria-hidden": true, className: cn("shrink-0", FRAME_GLYPH_COLOR[frame.status]), children: FRAME_GLYPH[frame.status] ?? "\u25CB" }),
718
- /* @__PURE__ */ jsxs12("span", { className: "min-w-0", children: [
609
+ /* @__PURE__ */ jsxs11("span", { className: "min-w-0", children: [
719
610
  /* @__PURE__ */ jsx20("span", { className: "font-mono font-semibold text-keyword", children: frame.keyword }),
720
611
  " ",
721
612
  /* @__PURE__ */ jsx20("span", { className: "text-muted-foreground", children: frame.screenshot?.alt ?? frame.text })
@@ -724,7 +615,7 @@ function StoryboardFrameItem({ frame }) {
724
615
  return (
725
616
  // State cards need room for diff rows; screenshot-only frames keep the
726
617
  // original narrow film-cell width (and DOM) unchanged.
727
- /* @__PURE__ */ jsxs12("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
618
+ /* @__PURE__ */ jsxs11("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
728
619
  frame.screenshot ? /* @__PURE__ */ jsx20(FrameScreenshot, { frame, label }) : null,
729
620
  hasStates ? /* @__PURE__ */ jsx20("div", { className: cn("flex flex-col gap-1.5", frame.screenshot && "mt-1.5"), children: frame.states.map((card, i) => /* @__PURE__ */ jsx20(StateCardView, { card }, card.label ?? i)) }) : null,
730
621
  frame.screenshot ? caption : (
@@ -771,8 +662,8 @@ function prettyJson(value) {
771
662
  function StateCardView({ card }) {
772
663
  const json = prettyJson(card.value);
773
664
  const firstCapture = card.changes === void 0;
774
- return /* @__PURE__ */ jsxs12("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
775
- /* @__PURE__ */ jsxs12("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
665
+ return /* @__PURE__ */ jsxs11("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
666
+ /* @__PURE__ */ jsxs11("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
776
667
  /* @__PURE__ */ jsx20("span", { className: "inline-flex rounded-full border border-border px-1.5 font-mono text-[0.625rem] font-medium text-muted-foreground", children: card.label ?? "State" }),
777
668
  firstCapture ? /* @__PURE__ */ jsx20("span", { className: "text-[0.625rem] text-muted-foreground italic", children: "first capture" }) : null
778
669
  ] }),
@@ -787,7 +678,7 @@ function StateCardView({ card }) {
787
678
  },
788
679
  i
789
680
  )) }) : /* @__PURE__ */ jsx20("p", { className: "mt-1.5 text-[0.625rem] text-muted-foreground", children: "No changes" })),
790
- /* @__PURE__ */ jsxs12(
681
+ /* @__PURE__ */ jsxs11(
791
682
  "details",
792
683
  {
793
684
  open: firstCapture && json.length <= 240,
@@ -802,7 +693,7 @@ function StateCardView({ card }) {
802
693
  }
803
694
 
804
695
  // src/components/ReportAttachments.tsx
805
- import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
696
+ import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
806
697
  function ReportAttachments({ attachments }) {
807
698
  if (attachments.length === 0) return null;
808
699
  return (
@@ -811,7 +702,7 @@ function ReportAttachments({ attachments }) {
811
702
  /* @__PURE__ */ jsx21("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
812
703
  const dataUri = att.contentEncoding === "BASE64" ? `data:${att.mediaType};base64,${att.body}` : `data:${att.mediaType},${encodeURIComponent(att.body)}`;
813
704
  const isImage = att.mediaType.startsWith("image/");
814
- return /* @__PURE__ */ jsx21("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs13(Fragment6, { children: [
705
+ return /* @__PURE__ */ jsx21("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs12(Fragment6, { children: [
815
706
  /* @__PURE__ */ jsx21(
816
707
  "img",
817
708
  {
@@ -822,7 +713,7 @@ function ReportAttachments({ attachments }) {
822
713
  }
823
714
  ),
824
715
  /* @__PURE__ */ jsx21("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
825
- ] }) : /* @__PURE__ */ jsxs13(
716
+ ] }) : /* @__PURE__ */ jsxs12(
826
717
  "a",
827
718
  {
828
719
  href: dataUri,
@@ -831,7 +722,7 @@ function ReportAttachments({ attachments }) {
831
722
  children: [
832
723
  att.name,
833
724
  " ",
834
- /* @__PURE__ */ jsxs13("span", { className: "text-muted-foreground", children: [
725
+ /* @__PURE__ */ jsxs12("span", { className: "text-muted-foreground", children: [
835
726
  "(",
836
727
  att.mediaType,
837
728
  ")"
@@ -922,7 +813,7 @@ function traceWindow(rows) {
922
813
  }
923
814
 
924
815
  // src/components/ReportTrace.tsx
925
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
816
+ import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
926
817
  var BAR_COLOR = {
927
818
  ok: "bg-pass",
928
819
  error: "bg-fail",
@@ -933,8 +824,8 @@ function ReportTrace({ spans }) {
933
824
  const rows = toTraceRows(spans);
934
825
  if (rows.length === 0) return null;
935
826
  const { minStartMs, totalMs } = traceWindow(rows);
936
- return /* @__PURE__ */ jsxs14("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
937
- /* @__PURE__ */ jsxs14("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
827
+ return /* @__PURE__ */ jsxs13("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
828
+ /* @__PURE__ */ jsxs13("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
938
829
  "Trace (",
939
830
  rows.length,
940
831
  " span",
@@ -946,7 +837,7 @@ function ReportTrace({ spans }) {
946
837
  /* @__PURE__ */ jsx22("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
947
838
  const leftPct = (row.startTimeMs - minStartMs) / totalMs * 100;
948
839
  const widthPct = Math.max(row.durationMs / totalMs * 100, 0.5);
949
- return /* @__PURE__ */ jsxs14("li", { className: "flex items-center gap-2", children: [
840
+ return /* @__PURE__ */ jsxs13("li", { className: "flex items-center gap-2", children: [
950
841
  /* @__PURE__ */ jsx22(
951
842
  "span",
952
843
  {
@@ -1032,7 +923,7 @@ function diffSinceLastRun(history, scenarios) {
1032
923
  }
1033
924
 
1034
925
  // src/components/ScenarioRunHistory.tsx
1035
- import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
926
+ import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1036
927
  var MAX_DOTS = 10;
1037
928
  var DOT_COLOR = {
1038
929
  passed: "bg-pass",
@@ -1052,7 +943,7 @@ function ScenarioRunHistory({ scenarioId }) {
1052
943
  const recent = entries.slice(-MAX_DOTS);
1053
944
  const flaky = flakinessOf(recent) === "flaky";
1054
945
  const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
1055
- return /* @__PURE__ */ jsxs15(Fragment7, { children: [
946
+ return /* @__PURE__ */ jsxs14(Fragment7, { children: [
1056
947
  flaky ? /* @__PURE__ */ jsx23(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
1057
948
  /* @__PURE__ */ jsx23(
1058
949
  "span",
@@ -1082,32 +973,111 @@ function ScenarioRunHistory({ scenarioId }) {
1082
973
  ] });
1083
974
  }
1084
975
 
1085
- // src/interactive/collapse-context.ts
1086
- import { createContext as createContext3, useCallback, useContext as useContext4, useState } from "react";
1087
- var CollapseContext = createContext3(null);
1088
- var CollapseProvider = CollapseContext.Provider;
1089
- function useCollapse() {
1090
- return useContext4(CollapseContext);
976
+ // src/components/ScenarioStaleness.tsx
977
+ import { useEffect, useState } from "react";
978
+
979
+ // src/lib/provenance.ts
980
+ function reportLastRunMs(report) {
981
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
982
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
983
+ return void 0;
1091
984
  }
1092
- var STORAGE_KEY = "es-collapsed-ids";
1093
- function readPersisted() {
1094
- if (typeof localStorage === "undefined") return null;
1095
- try {
1096
- const raw = localStorage.getItem(STORAGE_KEY);
1097
- return raw ? new Set(JSON.parse(raw)) : null;
1098
- } catch {
1099
- return null;
1100
- }
985
+ function formatRelativeAge(thenMs, nowMs) {
986
+ const deltaMs = Math.max(0, nowMs - thenMs);
987
+ const minutes = Math.floor(deltaMs / 6e4);
988
+ if (minutes < 1) return "just now";
989
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
990
+ const hours = Math.floor(minutes / 60);
991
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
992
+ const days = Math.floor(hours / 24);
993
+ return `${days} day${days === 1 ? "" : "s"} ago`;
1101
994
  }
1102
- function persist(ids) {
1103
- if (typeof localStorage === "undefined") return;
1104
- try {
1105
- localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]));
995
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
996
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
997
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
998
+ }
999
+ var CI_DISPLAY_NAMES = {
1000
+ github: "GitHub Actions",
1001
+ gitlab: "GitLab CI",
1002
+ circleci: "CircleCI",
1003
+ jenkins: "Jenkins",
1004
+ azure: "Azure DevOps",
1005
+ buildkite: "Buildkite",
1006
+ travis: "Travis CI",
1007
+ ci: "CI"
1008
+ };
1009
+ function ciDisplayName(name) {
1010
+ return CI_DISPLAY_NAMES[name] ?? name;
1011
+ }
1012
+ function commitUrl(ci, sha) {
1013
+ const commitSha = ci?.commitSha ?? sha;
1014
+ if (!ci?.url || !commitSha) return void 0;
1015
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1016
+ if (github) return `${github[1]}/commit/${commitSha}`;
1017
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1018
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1019
+ return void 0;
1020
+ }
1021
+ function prUrl(ci) {
1022
+ if (!ci?.url || !ci.prNumber) return void 0;
1023
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1024
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1025
+ return void 0;
1026
+ }
1027
+
1028
+ // src/interactive/scenario-freshness-context.tsx
1029
+ import { createContext as createContext3, useContext as useContext4 } from "react";
1030
+ var ScenarioFreshnessContext = createContext3(null);
1031
+ var ScenarioFreshnessProvider = ScenarioFreshnessContext.Provider;
1032
+ function useScenarioFreshness() {
1033
+ return useContext4(ScenarioFreshnessContext);
1034
+ }
1035
+
1036
+ // src/components/ScenarioStaleness.tsx
1037
+ import { jsxs as jsxs15 } from "react/jsx-runtime";
1038
+ function ScenarioStaleness({ scenario }) {
1039
+ const freshness = useScenarioFreshness();
1040
+ const fixedNow = freshness?.nowMs;
1041
+ const [now, setNow] = useState(fixedNow);
1042
+ useEffect(() => {
1043
+ if (fixedNow === void 0) setNow(Date.now());
1044
+ }, [fixedNow]);
1045
+ if (!freshness || now === void 0) return null;
1046
+ const lastRunAtMs = scenario.lastRunAtMs;
1047
+ if (!lastRunAtMs || lastRunAtMs <= 0) return null;
1048
+ if (!isReportStale(lastRunAtMs, freshness.staleAfterDays, now)) return null;
1049
+ return /* @__PURE__ */ jsxs15(Badge, { variant: "outline", "aria-label": "Scenario freshness", children: [
1050
+ "Last ran ",
1051
+ formatRelativeAge(lastRunAtMs, now)
1052
+ ] });
1053
+ }
1054
+
1055
+ // src/interactive/collapse-context.ts
1056
+ import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState2 } from "react";
1057
+ var CollapseContext = createContext4(null);
1058
+ var CollapseProvider = CollapseContext.Provider;
1059
+ function useCollapse() {
1060
+ return useContext5(CollapseContext);
1061
+ }
1062
+ var STORAGE_KEY = "es-collapsed-ids";
1063
+ function readPersisted() {
1064
+ if (typeof localStorage === "undefined") return null;
1065
+ try {
1066
+ const raw = localStorage.getItem(STORAGE_KEY);
1067
+ return raw ? new Set(JSON.parse(raw)) : null;
1068
+ } catch {
1069
+ return null;
1070
+ }
1071
+ }
1072
+ function persist(ids) {
1073
+ if (typeof localStorage === "undefined") return;
1074
+ try {
1075
+ localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]));
1106
1076
  } catch {
1107
1077
  }
1108
1078
  }
1109
1079
  function useCollapseState(computeDefault) {
1110
- const [collapsed, setCollapsed] = useState(
1080
+ const [collapsed, setCollapsed] = useState2(
1111
1081
  () => readPersisted() ?? computeDefault?.() ?? /* @__PURE__ */ new Set()
1112
1082
  );
1113
1083
  const isCollapsed = useCallback((id) => collapsed.has(id), [collapsed]);
@@ -1138,12 +1108,12 @@ function useCollapseState(computeDefault) {
1138
1108
  }
1139
1109
 
1140
1110
  // src/interactive/scenario-actions.tsx
1141
- import { createContext as createContext4, useContext as useContext5 } from "react";
1111
+ import { createContext as createContext5, useContext as useContext6 } from "react";
1142
1112
  import { scenarioToMarkdown as coreScenarioToMarkdown } from "executable-stories-core/utils/scenario-markdown";
1143
- var ScenarioActionsContext = createContext4(null);
1113
+ var ScenarioActionsContext = createContext5(null);
1144
1114
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
1145
1115
  function useScenarioActions() {
1146
- return useContext5(ScenarioActionsContext);
1116
+ return useContext6(ScenarioActionsContext);
1147
1117
  }
1148
1118
  function scenarioToMarkdown(scenario) {
1149
1119
  return coreScenarioToMarkdown(scenario, { variant: "compact" });
@@ -1311,6 +1281,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1311
1281
  ] }),
1312
1282
  /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1313
1283
  /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1284
+ /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1314
1285
  scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1315
1286
  meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1316
1287
  actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
@@ -1370,8 +1341,35 @@ function ReportScenarioList({ feature }) {
1370
1341
  return /* @__PURE__ */ jsx27(Fragment8, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx27(ReportScenario, { scenario }, scenario.id)) });
1371
1342
  }
1372
1343
 
1344
+ // src/components/ReportFeatureIntro.tsx
1345
+ import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1346
+ function ReportFeatureIntro({ feature }) {
1347
+ const hasNarrative = Boolean(feature.narrative?.trim());
1348
+ const hasGlossary = Boolean(feature.glossary?.length);
1349
+ if (!hasNarrative && !hasGlossary) return null;
1350
+ return /* @__PURE__ */ jsxs18("div", { className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-sm", children: [
1351
+ hasNarrative ? /* @__PURE__ */ jsx28(
1352
+ "div",
1353
+ {
1354
+ className: "es-doc-prose prose prose-sm max-w-none text-muted-foreground",
1355
+ dangerouslySetInnerHTML: { __html: safeMarkdownHtml(dedent(feature.narrative)) }
1356
+ }
1357
+ ) : null,
1358
+ hasGlossary ? /* @__PURE__ */ jsx28("dl", { className: "grid gap-1.5", "aria-label": "Glossary", children: feature.glossary.map((entry) => /* @__PURE__ */ jsxs18("div", { className: "flex flex-wrap gap-x-2 text-xs", children: [
1359
+ /* @__PURE__ */ jsx28("dt", { className: "font-medium text-foreground", children: entry.term }),
1360
+ /* @__PURE__ */ jsx28("dd", { className: "flex-1 text-muted-foreground", children: entry.definition })
1361
+ ] }, entry.term)) }) : null
1362
+ ] });
1363
+ }
1364
+ function featureKindLabel(kind) {
1365
+ if (kind === "ability") return "Ability";
1366
+ if (kind === "business-need") return "Business need";
1367
+ if (kind === "feature") return "Feature";
1368
+ return null;
1369
+ }
1370
+
1373
1371
  // src/components/ReportFeature.tsx
1374
- import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1372
+ import { Fragment as Fragment9, jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
1375
1373
  function ReportFeature({ feature }) {
1376
1374
  const titleId = `${feature.id}-title`;
1377
1375
  const bodyId = `${feature.id}-body`;
@@ -1379,10 +1377,11 @@ function ReportFeature({ feature }) {
1379
1377
  const skipped = s.skipped + s.pending;
1380
1378
  const collapse = useCollapse();
1381
1379
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
1380
+ const kindLabel = featureKindLabel(feature.kind);
1382
1381
  return (
1383
1382
  // Suite = a section heading + a divider, NOT another bordered card. Drops a
1384
1383
  // layer of nesting so the scenario cards below read as the primary unit.
1385
- /* @__PURE__ */ jsxs18(
1384
+ /* @__PURE__ */ jsxs19(
1386
1385
  "section",
1387
1386
  {
1388
1387
  id: feature.id,
@@ -1390,9 +1389,9 @@ function ReportFeature({ feature }) {
1390
1389
  "aria-labelledby": titleId,
1391
1390
  className: "flex flex-col gap-3",
1392
1391
  children: [
1393
- /* @__PURE__ */ jsxs18("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1394
- /* @__PURE__ */ jsxs18("div", { className: "min-w-0", children: [
1395
- /* @__PURE__ */ jsx28("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ jsxs18(
1392
+ /* @__PURE__ */ jsxs19("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1393
+ /* @__PURE__ */ jsxs19("div", { className: "min-w-0", children: [
1394
+ /* @__PURE__ */ jsx29("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ jsxs19(
1396
1395
  "button",
1397
1396
  {
1398
1397
  type: "button",
@@ -1401,31 +1400,34 @@ function ReportFeature({ feature }) {
1401
1400
  "aria-controls": bodyId,
1402
1401
  className: "flex w-full cursor-pointer items-center gap-2 text-left hover:text-foreground/80",
1403
1402
  children: [
1404
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1405
- /* @__PURE__ */ jsx28("span", { className: "truncate", children: feature.title })
1403
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1404
+ /* @__PURE__ */ jsx29("span", { className: "truncate", children: feature.title })
1406
1405
  ]
1407
1406
  }
1408
1407
  ) : feature.title }),
1409
- /* @__PURE__ */ jsx28("p", { className: cn("mt-0.5 truncate font-mono text-xs text-muted-foreground", collapse && "pl-5"), children: feature.sourceFile })
1408
+ /* @__PURE__ */ jsxs19("p", { className: cn("mt-0.5 flex items-center gap-2 text-xs text-muted-foreground", collapse && "pl-5"), children: [
1409
+ kindLabel ? /* @__PURE__ */ jsx29("span", { className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 font-medium text-foreground", children: kindLabel }) : null,
1410
+ /* @__PURE__ */ jsx29("span", { className: "truncate font-mono", children: feature.sourceFile })
1411
+ ] })
1410
1412
  ] }),
1411
- /* @__PURE__ */ jsxs18(
1413
+ /* @__PURE__ */ jsxs19(
1412
1414
  "p",
1413
1415
  {
1414
1416
  className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground",
1415
1417
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
1416
1418
  children: [
1417
- /* @__PURE__ */ jsxs18("span", { children: [
1419
+ /* @__PURE__ */ jsxs19("span", { children: [
1418
1420
  s.passed,
1419
1421
  " passed"
1420
1422
  ] }),
1421
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, children: "\xB7" }),
1422
- /* @__PURE__ */ jsxs18("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1423
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, children: "\xB7" }),
1424
+ /* @__PURE__ */ jsxs19("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1423
1425
  s.failed,
1424
1426
  " failed"
1425
1427
  ] }),
1426
- skipped > 0 ? /* @__PURE__ */ jsxs18(Fragment9, { children: [
1427
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, children: "\xB7" }),
1428
- /* @__PURE__ */ jsxs18("span", { children: [
1428
+ skipped > 0 ? /* @__PURE__ */ jsxs19(Fragment9, { children: [
1429
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, children: "\xB7" }),
1430
+ /* @__PURE__ */ jsxs19("span", { children: [
1429
1431
  skipped,
1430
1432
  " skipped"
1431
1433
  ] })
@@ -1434,7 +1436,10 @@ function ReportFeature({ feature }) {
1434
1436
  }
1435
1437
  )
1436
1438
  ] }),
1437
- /* @__PURE__ */ jsx28("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: /* @__PURE__ */ jsx28(ReportScenarioList, { feature }) })
1439
+ /* @__PURE__ */ jsxs19("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: [
1440
+ /* @__PURE__ */ jsx29(ReportFeatureIntro, { feature }),
1441
+ /* @__PURE__ */ jsx29(ReportScenarioList, { feature })
1442
+ ] })
1438
1443
  ]
1439
1444
  }
1440
1445
  )
@@ -1442,74 +1447,77 @@ function ReportFeature({ feature }) {
1442
1447
  }
1443
1448
 
1444
1449
  // src/components/ReportFeatureList.tsx
1445
- import { jsx as jsx29 } from "react/jsx-runtime";
1450
+ import { jsx as jsx30 } from "react/jsx-runtime";
1446
1451
  function ReportFeatureList() {
1447
1452
  const report = useReport();
1448
- return /* @__PURE__ */ jsx29("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx29(ReportFeature, { feature }, feature.id)) });
1453
+ return /* @__PURE__ */ jsx30("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx30(ReportFeature, { feature }, feature.id)) });
1449
1454
  }
1450
1455
 
1451
- // src/components/ui/empty.tsx
1452
- import { cva as cva2 } from "class-variance-authority";
1453
- import { jsx as jsx30 } from "react/jsx-runtime";
1454
- function Empty({ className, ...props }) {
1455
- return /* @__PURE__ */ jsx30(
1456
- "div",
1457
- {
1458
- "data-slot": "empty",
1459
- className: cn(
1460
- "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
1461
- className
1462
- ),
1463
- ...props
1464
- }
1465
- );
1466
- }
1467
- function EmptyHeader({ className, ...props }) {
1468
- return /* @__PURE__ */ jsx30(
1469
- "div",
1470
- {
1471
- "data-slot": "empty-header",
1472
- className: cn(
1473
- "flex max-w-sm flex-col items-center gap-2 text-center",
1474
- className
1475
- ),
1476
- ...props
1477
- }
1478
- );
1479
- }
1480
- var emptyMediaVariants = cva2(
1481
- "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
1482
- {
1483
- variants: {
1484
- variant: {
1485
- default: "bg-transparent",
1486
- icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
1487
- }
1488
- },
1489
- defaultVariants: {
1490
- variant: "default"
1491
- }
1456
+ // src/components/ReportMeta.tsx
1457
+ import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1458
+ import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1459
+ function ReportMeta() {
1460
+ const report = useReport();
1461
+ const items = [];
1462
+ const linkClass = "text-primary underline underline-offset-2";
1463
+ if (report.startedAtMs) {
1464
+ items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1492
1465
  }
1493
- );
1494
- function EmptyTitle({ className, ...props }) {
1495
- return /* @__PURE__ */ jsx30(
1496
- "div",
1497
- {
1498
- "data-slot": "empty-title",
1499
- className: cn("text-lg font-medium tracking-tight", className),
1500
- ...props
1501
- }
1466
+ if (report.durationMs) {
1467
+ items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1468
+ }
1469
+ if (report.packageVersion) {
1470
+ items.push({ label: "Version", value: report.packageVersion });
1471
+ }
1472
+ if (report.ci?.branch) {
1473
+ items.push({ label: "Branch", value: report.ci.branch });
1474
+ }
1475
+ const sha = report.ci?.commitSha ?? report.gitSha;
1476
+ if (sha) {
1477
+ const url = commitUrl(report.ci, report.gitSha);
1478
+ const short = sha.slice(0, 8);
1479
+ items.push({
1480
+ label: "Commit",
1481
+ value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1482
+ });
1483
+ }
1484
+ if (report.ci) {
1485
+ items.push({
1486
+ label: "CI",
1487
+ value: report.ci.url ? /* @__PURE__ */ jsxs20("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1488
+ ciDisplayName(report.ci.name),
1489
+ report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1490
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1491
+ });
1492
+ }
1493
+ if (report.ci?.prNumber) {
1494
+ const url = prUrl(report.ci);
1495
+ const label = `#${report.ci.prNumber}`;
1496
+ items.push({
1497
+ label: "PR",
1498
+ value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1499
+ });
1500
+ }
1501
+ if (items.length === 0) return null;
1502
+ return (
1503
+ // Run metadata is useful but secondary to "what failed" — tuck it into a
1504
+ // collapsed disclosure so it doesn't push the result down. Native <details>
1505
+ // so it works with no JS (the SSR/no-hydration report still toggles).
1506
+ /* @__PURE__ */ jsxs20("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1507
+ /* @__PURE__ */ jsxs20("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1508
+ /* @__PURE__ */ jsx31("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1509
+ "Run details"
1510
+ ] }),
1511
+ /* @__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: [
1512
+ /* @__PURE__ */ jsx31("dt", { className: "font-medium", children: item.label }),
1513
+ /* @__PURE__ */ jsx31("dd", { className: "font-mono text-foreground", children: item.value })
1514
+ ] }, item.label)) })
1515
+ ] })
1502
1516
  );
1503
1517
  }
1504
1518
 
1505
- // src/components/ReportEmpty.tsx
1506
- import { jsx as jsx31 } from "react/jsx-runtime";
1507
- function ReportEmpty({ message }) {
1508
- return /* @__PURE__ */ jsx31(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx31(EmptyHeader, { children: /* @__PURE__ */ jsx31(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1509
- }
1510
-
1511
1519
  // src/components/ReportSummary.tsx
1512
- import { jsx as jsx32, jsxs as jsxs19 } from "react/jsx-runtime";
1520
+ import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
1513
1521
  function ReportSummary({ className }) {
1514
1522
  const report = useReport();
1515
1523
  return /* @__PURE__ */ jsx32(
@@ -1551,13 +1559,13 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1551
1559
  "aria-label": ariaLabel,
1552
1560
  children: cards.map((c) => {
1553
1561
  const tone = TONE[c.status];
1554
- return /* @__PURE__ */ jsxs19(
1562
+ return /* @__PURE__ */ jsxs21(
1555
1563
  "div",
1556
1564
  {
1557
1565
  "data-status": c.status,
1558
1566
  className: "rounded-lg border bg-card px-4 py-3",
1559
1567
  children: [
1560
- /* @__PURE__ */ jsxs19("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1568
+ /* @__PURE__ */ jsxs21("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1561
1569
  /* @__PURE__ */ jsx32("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1562
1570
  c.label
1563
1571
  ] }),
@@ -1571,165 +1579,48 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1571
1579
  );
1572
1580
  }
1573
1581
 
1574
- // src/components/ReportMeta.tsx
1575
- import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1576
-
1577
- // src/lib/provenance.ts
1578
- function reportLastRunMs(report) {
1579
- if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1580
- if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1581
- return void 0;
1582
- }
1583
- function formatRelativeAge(thenMs, nowMs) {
1584
- const deltaMs = Math.max(0, nowMs - thenMs);
1585
- const minutes = Math.floor(deltaMs / 6e4);
1586
- if (minutes < 1) return "just now";
1587
- if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1588
- const hours = Math.floor(minutes / 60);
1589
- if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1590
- const days = Math.floor(hours / 24);
1591
- return `${days} day${days === 1 ? "" : "s"} ago`;
1592
- }
1593
- function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1594
- if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1595
- return nowMs - lastRunMs >= staleAfterDays * 864e5;
1596
- }
1597
- var CI_DISPLAY_NAMES = {
1598
- github: "GitHub Actions",
1599
- gitlab: "GitLab CI",
1600
- circleci: "CircleCI",
1601
- jenkins: "Jenkins",
1602
- azure: "Azure DevOps",
1603
- buildkite: "Buildkite",
1604
- travis: "Travis CI",
1605
- ci: "CI"
1606
- };
1607
- function ciDisplayName(name) {
1608
- return CI_DISPLAY_NAMES[name] ?? name;
1609
- }
1610
- function commitUrl(ci, sha) {
1611
- const commitSha = ci?.commitSha ?? sha;
1612
- if (!ci?.url || !commitSha) return void 0;
1613
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1614
- if (github) return `${github[1]}/commit/${commitSha}`;
1615
- const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1616
- if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1617
- return void 0;
1582
+ // src/components/ReportSchemaError.tsx
1583
+ import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
1584
+ function ReportSchemaError({ error }) {
1585
+ return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1586
+ /* @__PURE__ */ jsx33("p", { children: /* @__PURE__ */ jsx33("strong", { children: "Report could not be displayed." }) }),
1587
+ /* @__PURE__ */ jsx33("p", { children: error.message }),
1588
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1589
+ "The report bundle is newer than this version of ",
1590
+ /* @__PURE__ */ jsx33("code", { children: "executable-stories-react" }),
1591
+ ". Upgrade the package, or regenerate the report with an older formatters CLI."
1592
+ ] }) : null,
1593
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1594
+ /* @__PURE__ */ jsxs22("summary", { children: [
1595
+ error.issues.length,
1596
+ " validation issue",
1597
+ error.issues.length === 1 ? "" : "s"
1598
+ ] }),
1599
+ /* @__PURE__ */ jsx33("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1600
+ ] }) : null
1601
+ ] });
1618
1602
  }
1619
- function prUrl(ci) {
1620
- if (!ci?.url || !ci.prNumber) return void 0;
1621
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1622
- if (github) return `${github[1]}/pull/${ci.prNumber}`;
1623
- return void 0;
1603
+
1604
+ // src/components/ReportShell.tsx
1605
+ import { Fragment as Fragment10, jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
1606
+ function ReportErrorShell({
1607
+ error,
1608
+ className,
1609
+ title,
1610
+ dataTheme
1611
+ }) {
1612
+ return /* @__PURE__ */ jsx34("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx34(ReportSchemaError, { error }) });
1624
1613
  }
1625
1614
 
1626
- // src/components/ReportMeta.tsx
1627
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1628
- function ReportMeta() {
1629
- const report = useReport();
1630
- const items = [];
1631
- const linkClass = "text-primary underline underline-offset-2";
1632
- if (report.startedAtMs) {
1633
- items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1634
- }
1635
- if (report.durationMs) {
1636
- items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1637
- }
1638
- if (report.packageVersion) {
1639
- items.push({ label: "Version", value: report.packageVersion });
1640
- }
1641
- if (report.ci?.branch) {
1642
- items.push({ label: "Branch", value: report.ci.branch });
1643
- }
1644
- const sha = report.ci?.commitSha ?? report.gitSha;
1645
- if (sha) {
1646
- const url = commitUrl(report.ci, report.gitSha);
1647
- const short = sha.slice(0, 8);
1648
- items.push({
1649
- label: "Commit",
1650
- value: url ? /* @__PURE__ */ jsx33("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1651
- });
1652
- }
1653
- if (report.ci) {
1654
- items.push({
1655
- label: "CI",
1656
- value: report.ci.url ? /* @__PURE__ */ jsxs20("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1657
- ciDisplayName(report.ci.name),
1658
- report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1659
- ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1660
- });
1661
- }
1662
- if (report.ci?.prNumber) {
1663
- const url = prUrl(report.ci);
1664
- const label = `#${report.ci.prNumber}`;
1665
- items.push({
1666
- label: "PR",
1667
- value: url ? /* @__PURE__ */ jsx33("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1668
- });
1669
- }
1670
- if (items.length === 0) return null;
1671
- return (
1672
- // Run metadata is useful but secondary to "what failed" — tuck it into a
1673
- // collapsed disclosure so it doesn't push the result down. Native <details>
1674
- // so it works with no JS (the SSR/no-hydration report still toggles).
1675
- /* @__PURE__ */ jsxs20("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1676
- /* @__PURE__ */ jsxs20("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1677
- /* @__PURE__ */ jsx33("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1678
- "Run details"
1679
- ] }),
1680
- /* @__PURE__ */ jsx33("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: [
1681
- /* @__PURE__ */ jsx33("dt", { className: "font-medium", children: item.label }),
1682
- /* @__PURE__ */ jsx33("dd", { className: "font-mono text-foreground", children: item.value })
1683
- ] }, item.label)) })
1684
- ] })
1685
- );
1686
- }
1687
-
1688
- // src/components/ReportSchemaError.tsx
1689
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1690
- function ReportSchemaError({ error }) {
1691
- return /* @__PURE__ */ jsxs21("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1692
- /* @__PURE__ */ jsx34("p", { children: /* @__PURE__ */ jsx34("strong", { children: "Report could not be displayed." }) }),
1693
- /* @__PURE__ */ jsx34("p", { children: error.message }),
1694
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs21("p", { children: [
1695
- "The report bundle is newer than this version of ",
1696
- /* @__PURE__ */ jsx34("code", { children: "executable-stories-react" }),
1697
- ". Upgrade the package, or regenerate the report with an older formatters CLI."
1698
- ] }) : null,
1699
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs21("details", { children: [
1700
- /* @__PURE__ */ jsxs21("summary", { children: [
1701
- error.issues.length,
1702
- " validation issue",
1703
- error.issues.length === 1 ? "" : "s"
1704
- ] }),
1705
- /* @__PURE__ */ jsx34("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1706
- ] }) : null
1707
- ] });
1708
- }
1709
-
1710
- // src/components/ReportShell.tsx
1711
- import { Fragment as Fragment10, jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1712
- function ReportErrorShell({
1713
- error,
1714
- className,
1715
- title,
1716
- dataTheme
1717
- }) {
1718
- return /* @__PURE__ */ jsx35("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx35(ReportSchemaError, { error }) });
1719
- }
1720
-
1721
- // src/interactive/ReportInteractive.tsx
1722
- import { Monitor, Moon, Sun } from "lucide-react";
1723
-
1724
1615
  // src/components/ui/switch.tsx
1725
1616
  import "react";
1726
1617
  import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
1727
- import { jsx as jsx36 } from "react/jsx-runtime";
1618
+ import { jsx as jsx35 } from "react/jsx-runtime";
1728
1619
  function Switch({
1729
1620
  className,
1730
1621
  ...props
1731
1622
  }) {
1732
- return /* @__PURE__ */ jsx36(
1623
+ return /* @__PURE__ */ jsx35(
1733
1624
  SwitchPrimitive.Root,
1734
1625
  {
1735
1626
  "data-slot": "switch",
@@ -1738,7 +1629,7 @@ function Switch({
1738
1629
  className
1739
1630
  ),
1740
1631
  ...props,
1741
- children: /* @__PURE__ */ jsx36(
1632
+ children: /* @__PURE__ */ jsx35(
1742
1633
  SwitchPrimitive.Thumb,
1743
1634
  {
1744
1635
  "data-slot": "switch-thumb",
@@ -1754,7 +1645,7 @@ import * as React5 from "react";
1754
1645
  import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
1755
1646
  import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
1756
1647
  import { cva as cva3 } from "class-variance-authority";
1757
- import { jsx as jsx37 } from "react/jsx-runtime";
1648
+ import { jsx as jsx36 } from "react/jsx-runtime";
1758
1649
  var toggleItemVariants = cva3(
1759
1650
  "inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap text-muted-foreground transition-[color,background-color,box-shadow] outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-background data-[pressed]:text-foreground data-[pressed]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1760
1651
  {
@@ -1776,13 +1667,13 @@ function ToggleGroup({
1776
1667
  children,
1777
1668
  ...props
1778
1669
  }) {
1779
- return /* @__PURE__ */ jsx37(
1670
+ return /* @__PURE__ */ jsx36(
1780
1671
  ToggleGroupPrimitive,
1781
1672
  {
1782
1673
  "data-slot": "toggle-group",
1783
1674
  className: cn("flex w-fit items-center gap-0.5 rounded-lg bg-muted p-0.5", className),
1784
1675
  ...props,
1785
- children: /* @__PURE__ */ jsx37(ToggleGroupContext.Provider, { value: { size }, children })
1676
+ children: /* @__PURE__ */ jsx36(ToggleGroupContext.Provider, { value: { size }, children })
1786
1677
  }
1787
1678
  );
1788
1679
  }
@@ -1793,7 +1684,7 @@ function ToggleGroupItem({
1793
1684
  ...props
1794
1685
  }) {
1795
1686
  const context = React5.useContext(ToggleGroupContext);
1796
- return /* @__PURE__ */ jsx37(
1687
+ return /* @__PURE__ */ jsx36(
1797
1688
  TogglePrimitive,
1798
1689
  {
1799
1690
  "data-slot": "toggle-group-item",
@@ -1804,129 +1695,234 @@ function ToggleGroupItem({
1804
1695
  );
1805
1696
  }
1806
1697
 
1807
- // src/interactive/ReportSearch.tsx
1808
- import {
1809
- forwardRef,
1810
- useId
1811
- } from "react";
1812
- import { Search } from "lucide-react";
1698
+ // src/context/ReportRoot.tsx
1699
+ import { useMemo } from "react";
1813
1700
 
1814
- // src/components/ui/input.tsx
1815
- import "react";
1816
- import { jsx as jsx38 } from "react/jsx-runtime";
1817
- function Input({ className, type, ...props }) {
1818
- return /* @__PURE__ */ jsx38(
1819
- "input",
1820
- {
1821
- type,
1822
- "data-slot": "input",
1823
- className: cn(
1824
- "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
1825
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
1826
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
1827
- className
1828
- ),
1829
- ...props
1830
- }
1831
- );
1701
+ // src/components/doc/NarrativeBlocks.tsx
1702
+ import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1703
+ var FILE_TREE_TYPE = "file-tree";
1704
+ var DATA_MODEL_TYPE = "data-model";
1705
+ function isChangeKind(value) {
1706
+ return value === "added" || value === "modified" || value === "removed" || value === "renamed";
1832
1707
  }
1833
-
1834
- // src/interactive/ReportSearch.tsx
1835
- import { jsx as jsx39, jsxs as jsxs23 } from "react/jsx-runtime";
1836
- var ReportSearch = forwardRef(
1837
- function ReportSearch2(props, ref) {
1838
- const {
1839
- value,
1840
- onChange,
1841
- matchedCount,
1842
- totalCount,
1843
- placeholder = "Search scenarios or tags\u2026",
1844
- className
1845
- } = props;
1846
- const inputId = useId();
1847
- const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
1848
- function handleChange(e) {
1849
- onChange(e.target.value);
1708
+ function isRecord(value) {
1709
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1710
+ }
1711
+ function str(value) {
1712
+ return typeof value === "string" && value !== "" ? value : void 0;
1713
+ }
1714
+ function AuthoredMarker({ authored }) {
1715
+ if (authored !== "agent") return null;
1716
+ return /* @__PURE__ */ jsx37(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
1717
+ }
1718
+ function BlockFrame({
1719
+ title,
1720
+ authored,
1721
+ children
1722
+ }) {
1723
+ return /* @__PURE__ */ jsxs24("figure", { className: "my-3", children: [
1724
+ title || authored === "agent" ? /* @__PURE__ */ jsxs24("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
1725
+ title ? /* @__PURE__ */ jsx37("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
1726
+ /* @__PURE__ */ jsx37(AuthoredMarker, { authored })
1727
+ ] }) : null,
1728
+ children
1729
+ ] });
1730
+ }
1731
+ function ChangeBadge({ change }) {
1732
+ return /* @__PURE__ */ jsx37(Badge, { variant: "tag", className: "text-[10px]", children: change });
1733
+ }
1734
+ function parseFileTree(data) {
1735
+ if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
1736
+ const files = [];
1737
+ for (const raw of data.files) {
1738
+ if (typeof raw === "string") {
1739
+ if (raw) files.push({ path: raw });
1740
+ continue;
1850
1741
  }
1851
- function handleKeyDown(e) {
1852
- if (e.key === "Escape" && value !== "") {
1853
- onChange("");
1854
- e.preventDefault();
1742
+ if (!isRecord(raw)) continue;
1743
+ const path = str(raw.path);
1744
+ if (!path) continue;
1745
+ files.push({
1746
+ path,
1747
+ change: isChangeKind(raw.change) ? raw.change : void 0,
1748
+ note: str(raw.note)
1749
+ });
1750
+ }
1751
+ if (files.length === 0) return void 0;
1752
+ return { title: str(data.title), authored: str(data.authored), files };
1753
+ }
1754
+ function buildTree(files) {
1755
+ const root = { name: "", children: /* @__PURE__ */ new Map() };
1756
+ for (const file of files) {
1757
+ const segments = file.path.split("/").filter(Boolean);
1758
+ let node = root;
1759
+ segments.forEach((segment, index) => {
1760
+ let child = node.children.get(segment);
1761
+ if (!child) {
1762
+ child = { name: segment, children: /* @__PURE__ */ new Map() };
1763
+ node.children.set(segment, child);
1855
1764
  }
1856
- }
1857
- return /* @__PURE__ */ jsxs23("div", { className: cn("es-search flex flex-col gap-1", className), children: [
1858
- /* @__PURE__ */ jsx39("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
1859
- /* @__PURE__ */ jsxs23("div", { className: "relative w-full sm:w-72", children: [
1860
- /* @__PURE__ */ jsx39(
1861
- Search,
1765
+ if (index === segments.length - 1) child.file = file;
1766
+ node = child;
1767
+ });
1768
+ }
1769
+ return root;
1770
+ }
1771
+ function sortedChildren(node) {
1772
+ return [...node.children.values()].sort((a, b) => {
1773
+ const aDir = a.children.size > 0;
1774
+ const bDir = b.children.size > 0;
1775
+ if (aDir !== bDir) return aDir ? -1 : 1;
1776
+ return a.name.localeCompare(b.name);
1777
+ });
1778
+ }
1779
+ function TreeBranch({ node, depth }) {
1780
+ const children = sortedChildren(node);
1781
+ return /* @__PURE__ */ jsx37("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
1782
+ const isDir = child.children.size > 0;
1783
+ return /* @__PURE__ */ jsxs24("li", { children: [
1784
+ /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-baseline gap-2", children: [
1785
+ /* @__PURE__ */ jsx37(
1786
+ "span",
1862
1787
  {
1863
- "aria-hidden": true,
1864
- className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
1788
+ className: cn(
1789
+ "font-mono text-xs",
1790
+ isDir ? "text-muted-foreground" : "text-foreground"
1791
+ ),
1792
+ children: isDir ? `${child.name}/` : child.name
1865
1793
  }
1866
1794
  ),
1867
- /* @__PURE__ */ jsx39(
1868
- Input,
1869
- {
1870
- ref,
1871
- id: inputId,
1872
- type: "search",
1873
- value,
1874
- onChange: handleChange,
1875
- onKeyDown: handleKeyDown,
1876
- placeholder,
1877
- autoComplete: "off",
1878
- spellCheck: false,
1879
- "aria-keyshortcuts": "/",
1880
- className: "pl-8"
1881
- }
1882
- )
1795
+ child.file?.change ? /* @__PURE__ */ jsx37(ChangeBadge, { change: child.file.change }) : null,
1796
+ child.file?.note ? /* @__PURE__ */ jsx37("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
1883
1797
  ] }),
1884
- showCounts ? /* @__PURE__ */ jsxs23(
1885
- "span",
1886
- {
1887
- className: "text-xs tabular-nums text-muted-foreground",
1888
- "aria-live": "polite",
1889
- children: [
1890
- matchedCount,
1891
- " of ",
1892
- totalCount,
1893
- " scenarios"
1894
- ]
1895
- }
1896
- ) : null
1897
- ] });
1898
- }
1899
- );
1900
-
1901
- // src/interactive/ReportFailureBanner.tsx
1902
- import { useCallback as useCallback2 } from "react";
1903
-
1904
- // src/lib/hash-state.ts
1905
- var DEFAULT_URL_STATE = {
1906
- query: "",
1907
- status: "all",
1908
- tags: [],
1909
- detail: "full"
1910
- };
1911
- var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
1912
- function splitHash(hash) {
1913
- const raw = hash.replace(/^#/, "");
1914
- const split = raw.indexOf("?");
1915
- if (split === -1) return { anchor: raw, params: new URLSearchParams() };
1916
- return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
1798
+ isDir ? /* @__PURE__ */ jsx37(TreeBranch, { node: child, depth: depth + 1 }) : null
1799
+ ] }, child.name);
1800
+ }) });
1917
1801
  }
1918
- function decodeUrlState(params) {
1919
- const status = params.get("status");
1920
- const tags = params.get("tags");
1921
- return {
1922
- query: params.get("q") ?? DEFAULT_URL_STATE.query,
1923
- status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
1924
- tags: tags ? tags.split(",").filter(Boolean) : [],
1925
- detail: params.get("docs") === "0" ? "minimal" : "full"
1926
- };
1802
+ function FileTreeBlock({ entry }) {
1803
+ const data = parseFileTree(entry.data);
1804
+ if (!data) return /* @__PURE__ */ jsx37(UnreadableBlock, { entry });
1805
+ return /* @__PURE__ */ jsx37(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ jsx37("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ jsx37(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
1927
1806
  }
1928
- function encodeUrlState(state) {
1929
- const params = new URLSearchParams();
1807
+ function parseDataModel(data) {
1808
+ if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
1809
+ const fields = [];
1810
+ for (const raw of data.fields) {
1811
+ if (!isRecord(raw)) continue;
1812
+ const name = str(raw.name);
1813
+ if (!name) continue;
1814
+ fields.push({
1815
+ name,
1816
+ type: str(raw.type),
1817
+ note: str(raw.note),
1818
+ change: isChangeKind(raw.change) ? raw.change : void 0
1819
+ });
1820
+ }
1821
+ if (fields.length === 0) return void 0;
1822
+ return {
1823
+ title: str(data.title),
1824
+ authored: str(data.authored),
1825
+ name: str(data.name),
1826
+ fields
1827
+ };
1828
+ }
1829
+ function DataModelBlock({ entry }) {
1830
+ const data = parseDataModel(entry.data);
1831
+ if (!data) return /* @__PURE__ */ jsx37(UnreadableBlock, { entry });
1832
+ const hasNotes = data.fields.some((f) => f.note);
1833
+ return /* @__PURE__ */ jsx37(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ jsx37("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs24("table", { className: "w-full border-collapse text-sm", children: [
1834
+ /* @__PURE__ */ jsx37("thead", { children: /* @__PURE__ */ jsxs24("tr", { className: "bg-muted/50", children: [
1835
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
1836
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
1837
+ hasNotes ? /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
1838
+ ] }) }),
1839
+ /* @__PURE__ */ jsx37("tbody", { children: data.fields.map((field) => /* @__PURE__ */ jsxs24("tr", { children: [
1840
+ /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs24("span", { className: "flex flex-wrap items-center gap-2", children: [
1841
+ /* @__PURE__ */ jsx37("span", { className: "font-mono text-xs text-foreground", children: field.name }),
1842
+ field.change ? /* @__PURE__ */ jsx37(ChangeBadge, { change: field.change }) : null
1843
+ ] }) }),
1844
+ /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
1845
+ hasNotes ? /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
1846
+ ] }, field.name)) })
1847
+ ] }) }) });
1848
+ }
1849
+ function UnreadableBlock({ entry }) {
1850
+ return /* @__PURE__ */ jsxs24("div", { className: "my-2", "data-type": entry.type, children: [
1851
+ /* @__PURE__ */ jsxs24("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
1852
+ entry.type,
1853
+ " (unrecognised shape)"
1854
+ ] }),
1855
+ /* @__PURE__ */ jsx37(
1856
+ "pre",
1857
+ {
1858
+ tabIndex: 0,
1859
+ className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
1860
+ children: safeStringify2(entry.data)
1861
+ }
1862
+ )
1863
+ ] });
1864
+ }
1865
+ function safeStringify2(value) {
1866
+ try {
1867
+ return JSON.stringify(value, null, 2);
1868
+ } catch {
1869
+ return String(value);
1870
+ }
1871
+ }
1872
+ var narrativeBlockRenderers = {
1873
+ [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ jsx37(FileTreeBlock, { entry }),
1874
+ [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ jsx37(DataModelBlock, { entry })
1875
+ };
1876
+
1877
+ // src/context/ReportRoot.tsx
1878
+ import { jsx as jsx38 } from "react/jsx-runtime";
1879
+ var EMPTY_RENDERERS2 = {};
1880
+ function ReportRoot({
1881
+ report,
1882
+ customRenderers,
1883
+ renderers,
1884
+ children
1885
+ }) {
1886
+ const value = useMemo(
1887
+ () => ({
1888
+ report,
1889
+ // Narrative blocks render everywhere the report does (static SSR, the
1890
+ // island, Astro) without every host wiring them up. A host renderer for
1891
+ // the same type still wins.
1892
+ customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
1893
+ renderers: renderers ?? EMPTY_RENDERERS2
1894
+ }),
1895
+ [report, customRenderers, renderers]
1896
+ );
1897
+ return /* @__PURE__ */ jsx38(ReportContext.Provider, { value, children });
1898
+ }
1899
+
1900
+ // src/lib/hash-state.ts
1901
+ var DEFAULT_URL_STATE = {
1902
+ query: "",
1903
+ status: "all",
1904
+ tags: [],
1905
+ detail: "full"
1906
+ };
1907
+ var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
1908
+ function splitHash(hash) {
1909
+ const raw = hash.replace(/^#/, "");
1910
+ const split = raw.indexOf("?");
1911
+ if (split === -1) return { anchor: raw, params: new URLSearchParams() };
1912
+ return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
1913
+ }
1914
+ function decodeUrlState(params) {
1915
+ const status = params.get("status");
1916
+ const tags = params.get("tags");
1917
+ return {
1918
+ query: params.get("q") ?? DEFAULT_URL_STATE.query,
1919
+ status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
1920
+ tags: tags ? tags.split(",").filter(Boolean) : [],
1921
+ detail: params.get("docs") === "0" ? "minimal" : "full"
1922
+ };
1923
+ }
1924
+ function encodeUrlState(state) {
1925
+ const params = new URLSearchParams();
1930
1926
  if (state.query) params.set("q", state.query);
1931
1927
  if (state.status !== "all") params.set("status", state.status);
1932
1928
  if (state.tags.length > 0) params.set("tags", state.tags.join(","));
@@ -1969,8 +1965,112 @@ function scrollToScenarioId(scenarioId, options = {}) {
1969
1965
  }
1970
1966
  }
1971
1967
 
1968
+ // src/result.ts
1969
+ var ok = (data) => ({ ok: true, data });
1970
+ function unwrapReport(value) {
1971
+ if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
1972
+ return value;
1973
+ }
1974
+ return ok(value);
1975
+ }
1976
+
1977
+ // src/interactive/filter.ts
1978
+ function normalizeQuery(query) {
1979
+ return query.trim().toLowerCase();
1980
+ }
1981
+ function allTags(report) {
1982
+ const seen = /* @__PURE__ */ new Set();
1983
+ for (const feature of report.features) {
1984
+ for (const scenario of feature.scenarios) {
1985
+ for (const tag of scenario.tags) seen.add(tag);
1986
+ }
1987
+ }
1988
+ return [...seen];
1989
+ }
1990
+ function queryMatches(scenario, q) {
1991
+ if (q === "") return true;
1992
+ if (scenario.title.toLowerCase().includes(q)) return true;
1993
+ for (const tag of scenario.tags) {
1994
+ if (tag.toLowerCase().includes(q)) return true;
1995
+ }
1996
+ for (const step of scenario.steps) {
1997
+ if (step.text.toLowerCase().includes(q)) return true;
1998
+ }
1999
+ return false;
2000
+ }
2001
+ function scenarioMatches(scenario, q, status, tags) {
2002
+ if (status !== "all" && scenario.status !== status) return false;
2003
+ if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2004
+ return queryMatches(scenario, q);
2005
+ }
2006
+ function summarizeScenarios(scenarios) {
2007
+ let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
2008
+ for (const s of scenarios) {
2009
+ total += 1;
2010
+ durationMs += s.durationMs;
2011
+ if (s.status === "passed") passed += 1;
2012
+ else if (s.status === "failed") failed += 1;
2013
+ else if (s.status === "skipped") skipped += 1;
2014
+ else pending += 1;
2015
+ }
2016
+ return { total, passed, failed, skipped, pending, durationMs };
2017
+ }
2018
+ function filterReport(report, criteria) {
2019
+ const c = typeof criteria === "string" ? { query: criteria } : criteria;
2020
+ const q = normalizeQuery(c.query ?? "");
2021
+ const status = c.status ?? "all";
2022
+ const tags = c.tags ?? [];
2023
+ if (q === "" && status === "all" && tags.length === 0) return report;
2024
+ const features = [];
2025
+ let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2026
+ for (const feature of report.features) {
2027
+ const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2028
+ if (matched.length === 0) continue;
2029
+ const summary = summarizeScenarios(matched);
2030
+ features.push({ ...feature, summary, scenarios: matched });
2031
+ topTotal += summary.total;
2032
+ topPassed += summary.passed;
2033
+ topFailed += summary.failed;
2034
+ topSkipped += summary.skipped;
2035
+ topPending += summary.pending;
2036
+ topDuration += summary.durationMs;
2037
+ }
2038
+ return {
2039
+ ...report,
2040
+ summary: {
2041
+ total: topTotal,
2042
+ passed: topPassed,
2043
+ failed: topFailed,
2044
+ skipped: topSkipped,
2045
+ pending: topPending,
2046
+ durationMs: topDuration
2047
+ },
2048
+ features
2049
+ };
2050
+ }
2051
+ function listFailures(report) {
2052
+ const out = [];
2053
+ for (const feature of report.features) {
2054
+ for (const scenario of feature.scenarios) {
2055
+ if (scenario.status === "failed") {
2056
+ const ref = {
2057
+ featureId: feature.id,
2058
+ scenarioId: scenario.id,
2059
+ scenarioTitle: scenario.title
2060
+ };
2061
+ if (scenario.errorMessage !== void 0) {
2062
+ ref.errorMessage = scenario.errorMessage;
2063
+ }
2064
+ out.push(ref);
2065
+ }
2066
+ }
2067
+ }
2068
+ return out;
2069
+ }
2070
+
1972
2071
  // src/interactive/ReportFailureBanner.tsx
1973
- import { jsx as jsx40, jsxs as jsxs24 } from "react/jsx-runtime";
2072
+ import { useCallback as useCallback2 } from "react";
2073
+ import { jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
1974
2074
  function ReportFailureBanner({ failures }) {
1975
2075
  const first = failures[0];
1976
2076
  const jumpToFirst = useCallback2(() => {
@@ -1978,7 +2078,7 @@ function ReportFailureBanner({ failures }) {
1978
2078
  scrollToScenarioId(first.scenarioId);
1979
2079
  }, [first]);
1980
2080
  if (failures.length === 0) return null;
1981
- return /* @__PURE__ */ jsxs24(
2081
+ return /* @__PURE__ */ jsxs25(
1982
2082
  "div",
1983
2083
  {
1984
2084
  className: "es-failure-banner",
@@ -1986,13 +2086,13 @@ function ReportFailureBanner({ failures }) {
1986
2086
  "aria-live": "polite",
1987
2087
  "aria-label": "Failure summary",
1988
2088
  children: [
1989
- /* @__PURE__ */ jsxs24("span", { className: "es-failure-banner-text", children: [
1990
- /* @__PURE__ */ jsx40("strong", { children: failures.length }),
2089
+ /* @__PURE__ */ jsxs25("span", { className: "es-failure-banner-text", children: [
2090
+ /* @__PURE__ */ jsx39("strong", { children: failures.length }),
1991
2091
  " ",
1992
2092
  "failure",
1993
2093
  failures.length === 1 ? "" : "s"
1994
2094
  ] }),
1995
- /* @__PURE__ */ jsx40(
2095
+ /* @__PURE__ */ jsx39(
1996
2096
  "button",
1997
2097
  {
1998
2098
  type: "button",
@@ -2007,70 +2107,144 @@ function ReportFailureBanner({ failures }) {
2007
2107
  );
2008
2108
  }
2009
2109
 
2010
- // src/interactive/ReportFreshness.tsx
2011
- import { useEffect, useState as useState2 } from "react";
2012
- import { Fragment as Fragment11, jsx as jsx41, jsxs as jsxs25 } from "react/jsx-runtime";
2013
- function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2014
- const [now, setNow] = useState2(nowMs);
2015
- useEffect(() => {
2016
- if (nowMs === void 0) setNow(Date.now());
2017
- }, [nowMs]);
2018
- if (lastRunMs === void 0 || now === void 0) return null;
2019
- const age = formatRelativeAge(lastRunMs, now);
2020
- const runLink = ciUrl ? /* @__PURE__ */ jsx41(
2021
- "a",
2022
- {
2023
- href: ciUrl,
2024
- target: "_blank",
2025
- rel: "noreferrer noopener",
2026
- className: "underline underline-offset-2",
2027
- children: "View CI run"
2028
- }
2029
- ) : null;
2030
- if (isReportStale(lastRunMs, staleAfterDays, now)) {
2031
- return /* @__PURE__ */ jsxs25("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2032
- /* @__PURE__ */ jsxs25("span", { children: [
2033
- /* @__PURE__ */ jsxs25("strong", { children: [
2034
- "Last verified ",
2035
- age,
2036
- "."
2037
- ] }),
2038
- " This report may no longer match the current behavior. Look for a newer run."
2039
- ] }),
2040
- runLink
2041
- ] });
2042
- }
2043
- return /* @__PURE__ */ jsxs25("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2044
- "Verified ",
2045
- age,
2046
- runLink ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
2047
- " \xB7 ",
2048
- runLink
2049
- ] }) : null
2050
- ] });
2051
- }
2052
-
2053
- // src/interactive/ReportLastRunDelta.tsx
2054
- import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs26 } from "react/jsx-runtime";
2055
- var MAX_LINKS = 5;
2056
- function ScenarioLinks({ scenarios, tone }) {
2057
- return /* @__PURE__ */ jsxs26(Fragment12, { children: [
2058
- scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs26("span", { children: [
2059
- i > 0 ? ", " : "",
2060
- /* @__PURE__ */ jsx42("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2061
- ] }, s.id)),
2062
- scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs26("span", { children: [
2063
- " and ",
2064
- scenarios.length - MAX_LINKS,
2065
- " more"
2066
- ] }) : null
2067
- ] });
2068
- }
2069
- function ReportLastRunDelta({
2070
- history,
2071
- report
2072
- }) {
2073
- if (!history) return null;
2110
+ // src/interactive/ReportFilters.tsx
2111
+ import { useState as useState3 } from "react";
2112
+ import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
2113
+ var TAG_LIMIT = 12;
2114
+ function ReportFilters({
2115
+ statuses,
2116
+ status,
2117
+ onStatus,
2118
+ tags,
2119
+ activeTags,
2120
+ onToggleTag
2121
+ }) {
2122
+ const [showAllTags, setShowAllTags] = useState3(false);
2123
+ if (statuses.length === 0 && tags.length === 0) return null;
2124
+ const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2125
+ const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2126
+ return /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-2", children: [
2127
+ statuses.length > 0 ? /* @__PURE__ */ jsx40("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2128
+ const active = status === opt.key;
2129
+ return /* @__PURE__ */ jsxs26(
2130
+ "button",
2131
+ {
2132
+ type: "button",
2133
+ "aria-pressed": active,
2134
+ onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
2135
+ className: cn(
2136
+ "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
2137
+ active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
2138
+ ),
2139
+ children: [
2140
+ opt.label,
2141
+ /* @__PURE__ */ jsx40("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2142
+ ]
2143
+ },
2144
+ opt.key
2145
+ );
2146
+ }) }) : null,
2147
+ tags.length > 0 ? /* @__PURE__ */ jsxs26("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2148
+ visibleTags.map((tag) => {
2149
+ const active = activeTags.includes(tag);
2150
+ return /* @__PURE__ */ jsx40("li", { children: /* @__PURE__ */ jsx40(
2151
+ "button",
2152
+ {
2153
+ type: "button",
2154
+ "aria-pressed": active,
2155
+ onClick: () => onToggleTag(tag),
2156
+ className: "cursor-pointer",
2157
+ children: /* @__PURE__ */ jsx40(
2158
+ Badge,
2159
+ {
2160
+ variant: "tag",
2161
+ className: cn(
2162
+ active && "border-primary bg-primary text-primary-foreground"
2163
+ ),
2164
+ children: tag
2165
+ }
2166
+ )
2167
+ }
2168
+ ) }, tag);
2169
+ }),
2170
+ tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx40("li", { children: /* @__PURE__ */ jsx40(
2171
+ "button",
2172
+ {
2173
+ type: "button",
2174
+ "aria-expanded": showAllTags,
2175
+ onClick: () => setShowAllTags((v) => !v),
2176
+ className: "cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2177
+ children: tagsCollapsed ? `+${tags.length - TAG_LIMIT} more` : "Show fewer"
2178
+ }
2179
+ ) }) : null
2180
+ ] }) : null
2181
+ ] });
2182
+ }
2183
+
2184
+ // src/interactive/ReportFreshness.tsx
2185
+ import { useEffect as useEffect2, useState as useState4 } from "react";
2186
+ import { Fragment as Fragment11, jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
2187
+ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2188
+ const [now, setNow] = useState4(nowMs);
2189
+ useEffect2(() => {
2190
+ if (nowMs === void 0) setNow(Date.now());
2191
+ }, [nowMs]);
2192
+ if (lastRunMs === void 0 || now === void 0) return null;
2193
+ const age = formatRelativeAge(lastRunMs, now);
2194
+ const runLink = ciUrl ? /* @__PURE__ */ jsx41(
2195
+ "a",
2196
+ {
2197
+ href: ciUrl,
2198
+ target: "_blank",
2199
+ rel: "noreferrer noopener",
2200
+ className: "underline underline-offset-2",
2201
+ children: "View CI run"
2202
+ }
2203
+ ) : null;
2204
+ if (isReportStale(lastRunMs, staleAfterDays, now)) {
2205
+ return /* @__PURE__ */ jsxs27("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2206
+ /* @__PURE__ */ jsxs27("span", { children: [
2207
+ /* @__PURE__ */ jsxs27("strong", { children: [
2208
+ "Last verified ",
2209
+ age,
2210
+ "."
2211
+ ] }),
2212
+ " This report may no longer match the current behavior. Look for a newer run."
2213
+ ] }),
2214
+ runLink
2215
+ ] });
2216
+ }
2217
+ return /* @__PURE__ */ jsxs27("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2218
+ "Verified ",
2219
+ age,
2220
+ runLink ? /* @__PURE__ */ jsxs27(Fragment11, { children: [
2221
+ " \xB7 ",
2222
+ runLink
2223
+ ] }) : null
2224
+ ] });
2225
+ }
2226
+
2227
+ // src/interactive/ReportLastRunDelta.tsx
2228
+ import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs28 } from "react/jsx-runtime";
2229
+ var MAX_LINKS = 5;
2230
+ function ScenarioLinks({ scenarios, tone }) {
2231
+ return /* @__PURE__ */ jsxs28(Fragment12, { children: [
2232
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs28("span", { children: [
2233
+ i > 0 ? ", " : "",
2234
+ /* @__PURE__ */ jsx42("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2235
+ ] }, s.id)),
2236
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs28("span", { children: [
2237
+ " and ",
2238
+ scenarios.length - MAX_LINKS,
2239
+ " more"
2240
+ ] }) : null
2241
+ ] });
2242
+ }
2243
+ function ReportLastRunDelta({
2244
+ history,
2245
+ report
2246
+ }) {
2247
+ if (!history) return null;
2074
2248
  const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
2075
2249
  const delta = diffSinceLastRun(history, scenarios);
2076
2250
  if (!delta) return null;
@@ -2078,29 +2252,123 @@ function ReportLastRunDelta({
2078
2252
  if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
2079
2253
  return /* @__PURE__ */ jsx42("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2080
2254
  }
2081
- return /* @__PURE__ */ jsxs26("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2255
+ return /* @__PURE__ */ jsxs28("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2082
2256
  /* @__PURE__ */ jsx42("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2083
- newlyFailing.length > 0 ? /* @__PURE__ */ jsxs26("span", { className: "ml-2 text-fail", children: [
2257
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2 text-fail", children: [
2084
2258
  newlyFailing.length,
2085
2259
  " newly failing (",
2086
2260
  /* @__PURE__ */ jsx42(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2087
2261
  ")"
2088
2262
  ] }) : null,
2089
- newlyPassing.length > 0 ? /* @__PURE__ */ jsxs26("span", { className: "ml-2 text-pass", children: [
2263
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2 text-pass", children: [
2090
2264
  newlyPassing.length,
2091
2265
  " fixed (",
2092
2266
  /* @__PURE__ */ jsx42(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2093
2267
  ")"
2094
2268
  ] }) : null,
2095
- added.length > 0 ? /* @__PURE__ */ jsxs26("span", { className: "ml-2", children: [
2269
+ added.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2", children: [
2096
2270
  added.length,
2097
2271
  " new"
2098
2272
  ] }) : null
2099
2273
  ] });
2100
2274
  }
2101
2275
 
2276
+ // src/interactive/ReportSearch.tsx
2277
+ import {
2278
+ forwardRef,
2279
+ useId
2280
+ } from "react";
2281
+ import { Search } from "lucide-react";
2282
+
2283
+ // src/components/ui/input.tsx
2284
+ import "react";
2285
+ import { jsx as jsx43 } from "react/jsx-runtime";
2286
+ function Input({ className, type, ...props }) {
2287
+ return /* @__PURE__ */ jsx43(
2288
+ "input",
2289
+ {
2290
+ type,
2291
+ "data-slot": "input",
2292
+ className: cn(
2293
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
2294
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
2295
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
2296
+ className
2297
+ ),
2298
+ ...props
2299
+ }
2300
+ );
2301
+ }
2302
+
2303
+ // src/interactive/ReportSearch.tsx
2304
+ import { jsx as jsx44, jsxs as jsxs29 } from "react/jsx-runtime";
2305
+ var ReportSearch = forwardRef(
2306
+ function ReportSearch2(props, ref) {
2307
+ const {
2308
+ value,
2309
+ onChange,
2310
+ matchedCount,
2311
+ totalCount,
2312
+ placeholder = "Search scenarios or tags\u2026",
2313
+ className
2314
+ } = props;
2315
+ const inputId = useId();
2316
+ const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
2317
+ function handleChange(e) {
2318
+ onChange(e.target.value);
2319
+ }
2320
+ function handleKeyDown(e) {
2321
+ if (e.key === "Escape" && value !== "") {
2322
+ onChange("");
2323
+ e.preventDefault();
2324
+ }
2325
+ }
2326
+ return /* @__PURE__ */ jsxs29("div", { className: cn("es-search flex flex-col gap-1", className), children: [
2327
+ /* @__PURE__ */ jsx44("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
2328
+ /* @__PURE__ */ jsxs29("div", { className: "relative w-full sm:w-72", children: [
2329
+ /* @__PURE__ */ jsx44(
2330
+ Search,
2331
+ {
2332
+ "aria-hidden": true,
2333
+ className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
2334
+ }
2335
+ ),
2336
+ /* @__PURE__ */ jsx44(
2337
+ Input,
2338
+ {
2339
+ ref,
2340
+ id: inputId,
2341
+ type: "search",
2342
+ value,
2343
+ onChange: handleChange,
2344
+ onKeyDown: handleKeyDown,
2345
+ placeholder,
2346
+ autoComplete: "off",
2347
+ spellCheck: false,
2348
+ "aria-keyshortcuts": "/",
2349
+ className: "pl-8"
2350
+ }
2351
+ )
2352
+ ] }),
2353
+ showCounts ? /* @__PURE__ */ jsxs29(
2354
+ "span",
2355
+ {
2356
+ className: "text-xs tabular-nums text-muted-foreground",
2357
+ "aria-live": "polite",
2358
+ children: [
2359
+ matchedCount,
2360
+ " of ",
2361
+ totalCount,
2362
+ " scenarios"
2363
+ ]
2364
+ }
2365
+ ) : null
2366
+ ] });
2367
+ }
2368
+ );
2369
+
2102
2370
  // src/interactive/ReportShortcutsHelp.tsx
2103
- import { useEffect as useEffect2, useRef } from "react";
2371
+ import { useEffect as useEffect3, useRef } from "react";
2104
2372
 
2105
2373
  // src/components/ui/button.tsx
2106
2374
  import "react";
@@ -2149,329 +2417,60 @@ function Button({
2149
2417
  "data-slot": "button",
2150
2418
  "data-variant": variant,
2151
2419
  "data-size": size,
2152
- className: cn(buttonVariants({ variant, size, className })),
2153
- ...props
2154
- }
2155
- });
2156
- }
2157
-
2158
- // src/interactive/ReportShortcutsHelp.tsx
2159
- import { jsx as jsx43, jsxs as jsxs27 } from "react/jsx-runtime";
2160
- var SHORTCUTS = [
2161
- { keys: "/", description: "Focus search" },
2162
- { keys: "f", description: "Jump to next failure" },
2163
- { keys: "Shift F", description: "Jump to previous failure" },
2164
- { keys: "e", description: "Expand all" },
2165
- { keys: "c", description: "Collapse all" },
2166
- { keys: "?", description: "Toggle this help" },
2167
- { keys: "Esc", description: "Clear search / close" }
2168
- ];
2169
- function Keys({ keys }) {
2170
- const parts = keys.split(" ");
2171
- return /* @__PURE__ */ jsx43("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs27("span", { className: "inline-flex items-center gap-1", children: [
2172
- i > 0 ? /* @__PURE__ */ jsx43("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2173
- /* @__PURE__ */ jsx43("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 })
2174
- ] }, k)) });
2175
- }
2176
- function ReportShortcutsHelp({ open, onClose }) {
2177
- const dialogRef = useRef(null);
2178
- useEffect2(() => {
2179
- const dialog = dialogRef.current;
2180
- if (!dialog) return;
2181
- if (open && !dialog.open) dialog.showModal();
2182
- else if (!open && dialog.open) dialog.close();
2183
- }, [open]);
2184
- return /* @__PURE__ */ jsxs27(
2185
- "dialog",
2186
- {
2187
- ref: dialogRef,
2188
- className: "es-shortcuts-help m-auto w-[min(90vw,28rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2189
- "aria-label": "Keyboard shortcuts",
2190
- onClose,
2191
- children: [
2192
- /* @__PURE__ */ jsx43("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2193
- /* @__PURE__ */ jsx43("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs27("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2194
- /* @__PURE__ */ jsx43("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx43(Keys, { keys: s.keys }) }),
2195
- /* @__PURE__ */ jsx43("dd", { className: "text-sm text-muted-foreground", children: s.description })
2196
- ] }, s.keys)) }),
2197
- /* @__PURE__ */ jsx43("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx43(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2198
- ]
2199
- }
2200
- );
2201
- }
2202
-
2203
- // src/interactive/use-keyboard-shortcuts.ts
2204
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
2205
- function isEditableTarget(target) {
2206
- if (!(target instanceof HTMLElement)) return false;
2207
- const tag = target.tagName;
2208
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2209
- if (target.isContentEditable) return true;
2210
- return false;
2211
- }
2212
- function useKeyboardShortcuts(handlers) {
2213
- const ref = useRef2(handlers);
2214
- ref.current = handlers;
2215
- useEffect3(() => {
2216
- function onKeyDown(e) {
2217
- const h = ref.current;
2218
- if (e.key === "Escape") {
2219
- h.onEscape?.();
2220
- return;
2221
- }
2222
- if (isEditableTarget(e.target)) return;
2223
- if (e.metaKey || e.ctrlKey || e.altKey) return;
2224
- switch (e.key) {
2225
- case "/":
2226
- h.onFocusSearch?.();
2227
- e.preventDefault();
2228
- return;
2229
- case "?":
2230
- h.onToggleHelp?.();
2231
- e.preventDefault();
2232
- return;
2233
- case "f":
2234
- h.onNextFailure?.();
2235
- e.preventDefault();
2236
- return;
2237
- case "F":
2238
- h.onPrevFailure?.();
2239
- e.preventDefault();
2240
- return;
2241
- case "e":
2242
- h.onExpandAll?.();
2243
- e.preventDefault();
2244
- return;
2245
- case "c":
2246
- h.onCollapseAll?.();
2247
- e.preventDefault();
2248
- return;
2249
- default:
2250
- return;
2251
- }
2252
- }
2253
- window.addEventListener("keydown", onKeyDown);
2254
- return () => window.removeEventListener("keydown", onKeyDown);
2255
- }, []);
2256
- }
2257
-
2258
- // src/interactive/use-deep-link-scroll.ts
2259
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
2260
- function useDeepLinkScroll() {
2261
- const lastAnchor = useRef3(null);
2262
- useEffect4(() => {
2263
- function scrollToHash() {
2264
- const { anchor } = splitHash(window.location.hash);
2265
- if (!anchor || anchor === lastAnchor.current) return;
2266
- lastAnchor.current = anchor;
2267
- requestAnimationFrame(() => {
2268
- scrollToScenarioId(anchor, { updateHash: false });
2269
- });
2270
- }
2271
- scrollToHash();
2272
- window.addEventListener("hashchange", scrollToHash);
2273
- return () => window.removeEventListener("hashchange", scrollToHash);
2274
- }, []);
2275
- }
2276
-
2277
- // src/interactive/filter.ts
2278
- function normalizeQuery(query) {
2279
- return query.trim().toLowerCase();
2280
- }
2281
- function allTags(report) {
2282
- const seen = /* @__PURE__ */ new Set();
2283
- for (const feature of report.features) {
2284
- for (const scenario of feature.scenarios) {
2285
- for (const tag of scenario.tags) seen.add(tag);
2286
- }
2287
- }
2288
- return [...seen];
2289
- }
2290
- function queryMatches(scenario, q) {
2291
- if (q === "") return true;
2292
- if (scenario.title.toLowerCase().includes(q)) return true;
2293
- for (const tag of scenario.tags) {
2294
- if (tag.toLowerCase().includes(q)) return true;
2295
- }
2296
- for (const step of scenario.steps) {
2297
- if (step.text.toLowerCase().includes(q)) return true;
2298
- }
2299
- return false;
2300
- }
2301
- function scenarioMatches(scenario, q, status, tags) {
2302
- if (status !== "all" && scenario.status !== status) return false;
2303
- if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2304
- return queryMatches(scenario, q);
2305
- }
2306
- function summarizeScenarios(scenarios) {
2307
- let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
2308
- for (const s of scenarios) {
2309
- total += 1;
2310
- durationMs += s.durationMs;
2311
- if (s.status === "passed") passed += 1;
2312
- else if (s.status === "failed") failed += 1;
2313
- else if (s.status === "skipped") skipped += 1;
2314
- else pending += 1;
2315
- }
2316
- return { total, passed, failed, skipped, pending, durationMs };
2317
- }
2318
- function filterReport(report, criteria) {
2319
- const c = typeof criteria === "string" ? { query: criteria } : criteria;
2320
- const q = normalizeQuery(c.query ?? "");
2321
- const status = c.status ?? "all";
2322
- const tags = c.tags ?? [];
2323
- if (q === "" && status === "all" && tags.length === 0) return report;
2324
- const features = [];
2325
- let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2326
- for (const feature of report.features) {
2327
- const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2328
- if (matched.length === 0) continue;
2329
- const summary = summarizeScenarios(matched);
2330
- features.push({ ...feature, summary, scenarios: matched });
2331
- topTotal += summary.total;
2332
- topPassed += summary.passed;
2333
- topFailed += summary.failed;
2334
- topSkipped += summary.skipped;
2335
- topPending += summary.pending;
2336
- topDuration += summary.durationMs;
2337
- }
2338
- return {
2339
- ...report,
2340
- summary: {
2341
- total: topTotal,
2342
- passed: topPassed,
2343
- failed: topFailed,
2344
- skipped: topSkipped,
2345
- pending: topPending,
2346
- durationMs: topDuration
2347
- },
2348
- features
2349
- };
2350
- }
2351
- function listFailures(report) {
2352
- const out = [];
2353
- for (const feature of report.features) {
2354
- for (const scenario of feature.scenarios) {
2355
- if (scenario.status === "failed") {
2356
- const ref = {
2357
- featureId: feature.id,
2358
- scenarioId: scenario.id,
2359
- scenarioTitle: scenario.title
2360
- };
2361
- if (scenario.errorMessage !== void 0) {
2362
- ref.errorMessage = scenario.errorMessage;
2363
- }
2364
- out.push(ref);
2365
- }
2366
- }
2367
- }
2368
- return out;
2369
- }
2370
-
2371
- // src/interactive/triage-default.ts
2372
- function needsAttention(status) {
2373
- return status === "failed";
2374
- }
2375
- function defaultCollapsedIds(report) {
2376
- const collapsed = /* @__PURE__ */ new Set();
2377
- if (report.summary.failed === 0) return collapsed;
2378
- for (const feature of report.features) {
2379
- const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2380
- if (!featureHasFailure) collapsed.add(feature.id);
2381
- for (const scenario of feature.scenarios) {
2382
- if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2420
+ className: cn(buttonVariants({ variant, size, className })),
2421
+ ...props
2383
2422
  }
2384
- }
2385
- return collapsed;
2386
- }
2387
- function failuresFirst(report) {
2388
- if (report.summary.failed === 0) return report;
2389
- const withFailures = [];
2390
- const rest = [];
2391
- for (const feature of report.features) {
2392
- (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2393
- }
2394
- if (withFailures.length === 0) return report;
2395
- return { ...report, features: [...withFailures, ...rest] };
2423
+ });
2396
2424
  }
2397
2425
 
2398
- // src/interactive/ReportFilters.tsx
2399
- import { useState as useState3 } from "react";
2400
- import { jsx as jsx44, jsxs as jsxs28 } from "react/jsx-runtime";
2401
- var TAG_LIMIT = 12;
2402
- function ReportFilters({
2403
- statuses,
2404
- status,
2405
- onStatus,
2406
- tags,
2407
- activeTags,
2408
- onToggleTag
2409
- }) {
2410
- const [showAllTags, setShowAllTags] = useState3(false);
2411
- if (statuses.length === 0 && tags.length === 0) return null;
2412
- const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2413
- const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2414
- return /* @__PURE__ */ jsxs28("div", { className: "flex flex-col gap-2", children: [
2415
- statuses.length > 0 ? /* @__PURE__ */ jsx44("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2416
- const active = status === opt.key;
2417
- return /* @__PURE__ */ jsxs28(
2418
- "button",
2419
- {
2420
- type: "button",
2421
- "aria-pressed": active,
2422
- onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
2423
- className: cn(
2424
- "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
2425
- active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
2426
- ),
2427
- children: [
2428
- opt.label,
2429
- /* @__PURE__ */ jsx44("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2430
- ]
2431
- },
2432
- opt.key
2433
- );
2434
- }) }) : null,
2435
- tags.length > 0 ? /* @__PURE__ */ jsxs28("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2436
- visibleTags.map((tag) => {
2437
- const active = activeTags.includes(tag);
2438
- return /* @__PURE__ */ jsx44("li", { children: /* @__PURE__ */ jsx44(
2439
- "button",
2440
- {
2441
- type: "button",
2442
- "aria-pressed": active,
2443
- onClick: () => onToggleTag(tag),
2444
- className: "cursor-pointer",
2445
- children: /* @__PURE__ */ jsx44(
2446
- Badge,
2447
- {
2448
- variant: "tag",
2449
- className: cn(
2450
- active && "border-primary bg-primary text-primary-foreground"
2451
- ),
2452
- children: tag
2453
- }
2454
- )
2455
- }
2456
- ) }, tag);
2457
- }),
2458
- tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx44("li", { children: /* @__PURE__ */ jsx44(
2459
- "button",
2460
- {
2461
- type: "button",
2462
- "aria-expanded": showAllTags,
2463
- onClick: () => setShowAllTags((v) => !v),
2464
- className: "cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2465
- children: tagsCollapsed ? `+${tags.length - TAG_LIMIT} more` : "Show fewer"
2466
- }
2467
- ) }) : null
2468
- ] }) : null
2469
- ] });
2426
+ // src/interactive/ReportShortcutsHelp.tsx
2427
+ import { jsx as jsx45, jsxs as jsxs30 } from "react/jsx-runtime";
2428
+ var SHORTCUTS = [
2429
+ { keys: "/", description: "Focus search" },
2430
+ { keys: "f", description: "Jump to next failure" },
2431
+ { keys: "Shift F", description: "Jump to previous failure" },
2432
+ { keys: "e", description: "Expand all" },
2433
+ { keys: "c", description: "Collapse all" },
2434
+ { keys: "?", description: "Toggle this help" },
2435
+ { keys: "Esc", description: "Clear search / close" }
2436
+ ];
2437
+ function Keys({ keys }) {
2438
+ const parts = keys.split(" ");
2439
+ return /* @__PURE__ */ jsx45("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs30("span", { className: "inline-flex items-center gap-1", children: [
2440
+ i > 0 ? /* @__PURE__ */ jsx45("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2441
+ /* @__PURE__ */ jsx45("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 })
2442
+ ] }, k)) });
2443
+ }
2444
+ function ReportShortcutsHelp({ open, onClose }) {
2445
+ const dialogRef = useRef(null);
2446
+ useEffect3(() => {
2447
+ const dialog = dialogRef.current;
2448
+ if (!dialog) return;
2449
+ if (open && !dialog.open) dialog.showModal();
2450
+ else if (!open && dialog.open) dialog.close();
2451
+ }, [open]);
2452
+ return /* @__PURE__ */ jsxs30(
2453
+ "dialog",
2454
+ {
2455
+ ref: dialogRef,
2456
+ className: "es-shortcuts-help m-auto w-[min(90vw,28rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2457
+ "aria-label": "Keyboard shortcuts",
2458
+ onClose,
2459
+ children: [
2460
+ /* @__PURE__ */ jsx45("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2461
+ /* @__PURE__ */ jsx45("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs30("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2462
+ /* @__PURE__ */ jsx45("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx45(Keys, { keys: s.keys }) }),
2463
+ /* @__PURE__ */ jsx45("dd", { className: "text-sm text-muted-foreground", children: s.description })
2464
+ ] }, s.keys)) }),
2465
+ /* @__PURE__ */ jsx45("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx45(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2466
+ ]
2467
+ }
2468
+ );
2470
2469
  }
2471
2470
 
2472
2471
  // src/interactive/ReportToc.tsx
2473
- import { useEffect as useEffect5, useMemo as useMemo2, useState as useState4 } from "react";
2474
- import { jsx as jsx45, jsxs as jsxs29 } from "react/jsx-runtime";
2472
+ import { useEffect as useEffect4, useMemo as useMemo2, useState as useState5 } from "react";
2473
+ import { jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2475
2474
  var DOT = {
2476
2475
  passed: "bg-pass",
2477
2476
  failed: "bg-fail",
@@ -2480,12 +2479,12 @@ var DOT = {
2480
2479
  };
2481
2480
  function TocContent({ onNavigate }) {
2482
2481
  const report = useReport();
2483
- const [activeId, setActiveId] = useState4(null);
2482
+ const [activeId, setActiveId] = useState5(null);
2484
2483
  const scenarioIds = useMemo2(
2485
2484
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
2486
2485
  [report]
2487
2486
  );
2488
- useEffect5(() => {
2487
+ useEffect4(() => {
2489
2488
  if (typeof IntersectionObserver === "undefined") return;
2490
2489
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
2491
2490
  if (els.length === 0) return;
@@ -2500,11 +2499,11 @@ function TocContent({ onNavigate }) {
2500
2499
  return () => observer.disconnect();
2501
2500
  }, [scenarioIds]);
2502
2501
  if (report.features.length === 0) return null;
2503
- return /* @__PURE__ */ jsx45("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs29("li", { children: [
2504
- /* @__PURE__ */ jsx45("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2505
- /* @__PURE__ */ jsx45("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2502
+ return /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs31("li", { children: [
2503
+ /* @__PURE__ */ jsx46("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2504
+ /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2506
2505
  const active = activeId === scenario.id;
2507
- return /* @__PURE__ */ jsx45("li", { children: /* @__PURE__ */ jsxs29(
2506
+ return /* @__PURE__ */ jsx46("li", { children: /* @__PURE__ */ jsxs31(
2508
2507
  "a",
2509
2508
  {
2510
2509
  href: `#${scenario.id}`,
@@ -2519,14 +2518,14 @@ function TocContent({ onNavigate }) {
2519
2518
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
2520
2519
  ),
2521
2520
  children: [
2522
- /* @__PURE__ */ jsx45(
2521
+ /* @__PURE__ */ jsx46(
2523
2522
  "span",
2524
2523
  {
2525
2524
  "aria-hidden": true,
2526
2525
  className: cn("mt-1 size-1.5 shrink-0 rounded-full", DOT[scenario.status])
2527
2526
  }
2528
2527
  ),
2529
- /* @__PURE__ */ jsx45("span", { className: "break-words", children: scenario.title })
2528
+ /* @__PURE__ */ jsx46("span", { className: "break-words", children: scenario.title })
2530
2529
  ]
2531
2530
  }
2532
2531
  ) }, scenario.id);
@@ -2536,55 +2535,156 @@ function TocContent({ onNavigate }) {
2536
2535
  function ReportToc() {
2537
2536
  const report = useReport();
2538
2537
  if (report.features.length === 0) return null;
2539
- return /* @__PURE__ */ jsxs29(
2538
+ return /* @__PURE__ */ jsxs31(
2540
2539
  "nav",
2541
2540
  {
2542
2541
  "aria-label": "Table of contents",
2543
2542
  className: "sticky top-4 hidden h-fit w-64 shrink-0 self-start text-xs lg:block",
2544
2543
  children: [
2545
- /* @__PURE__ */ jsx45("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2546
- /* @__PURE__ */ jsx45(TocContent, {})
2544
+ /* @__PURE__ */ jsx46("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2545
+ /* @__PURE__ */ jsx46(TocContent, {})
2547
2546
  ]
2548
2547
  }
2549
2548
  );
2550
2549
  }
2551
2550
 
2552
2551
  // src/interactive/ReportTocDrawer.tsx
2553
- import { useState as useState5 } from "react";
2552
+ import { useState as useState6 } from "react";
2554
2553
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2555
2554
  import { ListIcon, XIcon } from "lucide-react";
2556
- import { jsx as jsx46, jsxs as jsxs30 } from "react/jsx-runtime";
2555
+ import { jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2557
2556
  function ReportTocDrawer() {
2558
2557
  const report = useReport();
2559
- const [open, setOpen] = useState5(false);
2558
+ const [open, setOpen] = useState6(false);
2560
2559
  if (report.features.length === 0) return null;
2561
- return /* @__PURE__ */ jsxs30(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2562
- /* @__PURE__ */ jsxs30(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: [
2563
- /* @__PURE__ */ jsx46(ListIcon, { "aria-hidden": true }),
2560
+ return /* @__PURE__ */ jsxs32(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2561
+ /* @__PURE__ */ jsxs32(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: [
2562
+ /* @__PURE__ */ jsx47(ListIcon, { "aria-hidden": true }),
2564
2563
  " Contents"
2565
2564
  ] }),
2566
- /* @__PURE__ */ jsxs30(DialogPrimitive.Portal, { children: [
2567
- /* @__PURE__ */ jsx46(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" }),
2568
- /* @__PURE__ */ jsxs30(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: [
2569
- /* @__PURE__ */ jsxs30("div", { className: "flex items-center justify-between", children: [
2570
- /* @__PURE__ */ jsx46(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2571
- /* @__PURE__ */ jsx46(
2565
+ /* @__PURE__ */ jsxs32(DialogPrimitive.Portal, { children: [
2566
+ /* @__PURE__ */ jsx47(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" }),
2567
+ /* @__PURE__ */ jsxs32(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: [
2568
+ /* @__PURE__ */ jsxs32("div", { className: "flex items-center justify-between", children: [
2569
+ /* @__PURE__ */ jsx47(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2570
+ /* @__PURE__ */ jsx47(
2572
2571
  DialogPrimitive.Close,
2573
2572
  {
2574
2573
  "aria-label": "Close contents",
2575
2574
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
2576
- children: /* @__PURE__ */ jsx46(XIcon, { "aria-hidden": true })
2575
+ children: /* @__PURE__ */ jsx47(XIcon, { "aria-hidden": true })
2577
2576
  }
2578
2577
  )
2579
2578
  ] }),
2580
- /* @__PURE__ */ jsx46(TocContent, { onNavigate: () => setOpen(false) })
2579
+ /* @__PURE__ */ jsx47(TocContent, { onNavigate: () => setOpen(false) })
2581
2580
  ] })
2582
2581
  ] })
2583
2582
  ] });
2584
2583
  }
2585
2584
 
2585
+ // src/interactive/triage-default.ts
2586
+ function needsAttention(status) {
2587
+ return status === "failed";
2588
+ }
2589
+ function defaultCollapsedIds(report) {
2590
+ const collapsed = /* @__PURE__ */ new Set();
2591
+ if (report.summary.failed === 0) return collapsed;
2592
+ for (const feature of report.features) {
2593
+ const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2594
+ if (!featureHasFailure) collapsed.add(feature.id);
2595
+ for (const scenario of feature.scenarios) {
2596
+ if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2597
+ }
2598
+ }
2599
+ return collapsed;
2600
+ }
2601
+ function failuresFirst(report) {
2602
+ if (report.summary.failed === 0) return report;
2603
+ const withFailures = [];
2604
+ const rest = [];
2605
+ for (const feature of report.features) {
2606
+ (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2607
+ }
2608
+ if (withFailures.length === 0) return report;
2609
+ return { ...report, features: [...withFailures, ...rest] };
2610
+ }
2611
+
2612
+ // src/interactive/use-deep-link-scroll.ts
2613
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
2614
+ function useDeepLinkScroll() {
2615
+ const lastAnchor = useRef2(null);
2616
+ useEffect5(() => {
2617
+ function scrollToHash() {
2618
+ const { anchor } = splitHash(window.location.hash);
2619
+ if (!anchor || anchor === lastAnchor.current) return;
2620
+ lastAnchor.current = anchor;
2621
+ requestAnimationFrame(() => {
2622
+ scrollToScenarioId(anchor, { updateHash: false });
2623
+ });
2624
+ }
2625
+ scrollToHash();
2626
+ window.addEventListener("hashchange", scrollToHash);
2627
+ return () => window.removeEventListener("hashchange", scrollToHash);
2628
+ }, []);
2629
+ }
2630
+
2631
+ // src/interactive/use-keyboard-shortcuts.ts
2632
+ import { useEffect as useEffect6, useRef as useRef3 } from "react";
2633
+ function isEditableTarget(target) {
2634
+ if (!(target instanceof HTMLElement)) return false;
2635
+ const tag = target.tagName;
2636
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2637
+ if (target.isContentEditable) return true;
2638
+ return false;
2639
+ }
2640
+ function useKeyboardShortcuts(handlers) {
2641
+ const ref = useRef3(handlers);
2642
+ ref.current = handlers;
2643
+ useEffect6(() => {
2644
+ function onKeyDown(e) {
2645
+ const h = ref.current;
2646
+ if (e.key === "Escape") {
2647
+ h.onEscape?.();
2648
+ return;
2649
+ }
2650
+ if (isEditableTarget(e.target)) return;
2651
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
2652
+ switch (e.key) {
2653
+ case "/":
2654
+ h.onFocusSearch?.();
2655
+ e.preventDefault();
2656
+ return;
2657
+ case "?":
2658
+ h.onToggleHelp?.();
2659
+ e.preventDefault();
2660
+ return;
2661
+ case "f":
2662
+ h.onNextFailure?.();
2663
+ e.preventDefault();
2664
+ return;
2665
+ case "F":
2666
+ h.onPrevFailure?.();
2667
+ e.preventDefault();
2668
+ return;
2669
+ case "e":
2670
+ h.onExpandAll?.();
2671
+ e.preventDefault();
2672
+ return;
2673
+ case "c":
2674
+ h.onCollapseAll?.();
2675
+ e.preventDefault();
2676
+ return;
2677
+ default:
2678
+ return;
2679
+ }
2680
+ }
2681
+ window.addEventListener("keydown", onKeyDown);
2682
+ return () => window.removeEventListener("keydown", onKeyDown);
2683
+ }, []);
2684
+ }
2685
+
2586
2686
  // src/interactive/use-theme.ts
2587
- import { useCallback as useCallback3, useEffect as useEffect6, useState as useState6 } from "react";
2687
+ import { useCallback as useCallback3, useEffect as useEffect7, useState as useState7 } from "react";
2588
2688
  var KEY = "es-theme";
2589
2689
  function systemDark() {
2590
2690
  return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
@@ -2605,13 +2705,13 @@ function initialPref() {
2605
2705
  return "system";
2606
2706
  }
2607
2707
  function useTheme() {
2608
- const [pref, setPrefState] = useState6("system");
2609
- useEffect6(() => {
2708
+ const [pref, setPrefState] = useState7("system");
2709
+ useEffect7(() => {
2610
2710
  const p = initialPref();
2611
2711
  setPrefState(p);
2612
2712
  applyTheme(p);
2613
2713
  }, []);
2614
- useEffect6(() => {
2714
+ useEffect7(() => {
2615
2715
  if (typeof matchMedia === "undefined") return;
2616
2716
  const mq = matchMedia("(prefers-color-scheme: dark)");
2617
2717
  const onChange = () => {
@@ -2632,19 +2732,19 @@ function useTheme() {
2632
2732
  }
2633
2733
 
2634
2734
  // src/interactive/use-url-state.ts
2635
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef4, useState as useState7 } from "react";
2735
+ import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef4, useState as useState8 } from "react";
2636
2736
  var WRITE_DELAY_MS = 250;
2637
2737
  function useUrlState() {
2638
- const [state, setState] = useState7(DEFAULT_URL_STATE);
2738
+ const [state, setState] = useState8(DEFAULT_URL_STATE);
2639
2739
  const ready = useRef4(false);
2640
- useEffect7(() => {
2740
+ useEffect8(() => {
2641
2741
  const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2642
2742
  read();
2643
2743
  ready.current = true;
2644
2744
  window.addEventListener("hashchange", read);
2645
2745
  return () => window.removeEventListener("hashchange", read);
2646
2746
  }, []);
2647
- useEffect7(() => {
2747
+ useEffect8(() => {
2648
2748
  if (!ready.current) return;
2649
2749
  const timer = setTimeout(
2650
2750
  () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
@@ -2660,22 +2760,33 @@ function useUrlState() {
2660
2760
  }
2661
2761
 
2662
2762
  // src/interactive/ReportInteractive.tsx
2663
- import { Fragment as Fragment13, jsx as jsx47, jsxs as jsxs31 } from "react/jsx-runtime";
2763
+ import { Fragment as Fragment13, jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
2664
2764
  function ReportInteractive(props) {
2665
2765
  const { report, className, title, dataTheme } = props;
2666
2766
  const result = unwrapReport(report);
2667
2767
  if (!result.ok) {
2668
- return /* @__PURE__ */ jsx47(ReportErrorShell, { error: result.error, className, title, dataTheme });
2768
+ return /* @__PURE__ */ jsx48(
2769
+ ReportErrorShell,
2770
+ {
2771
+ error: result.error,
2772
+ className,
2773
+ title,
2774
+ dataTheme
2775
+ }
2776
+ );
2669
2777
  }
2670
- return /* @__PURE__ */ jsx47(ReportInteractiveView, { ...props, report: result.data });
2778
+ return /* @__PURE__ */ jsx48(ReportInteractiveView, { ...props, report: result.data });
2671
2779
  }
2672
2780
  var THEME_OPTIONS = [
2673
2781
  { value: "light", label: "Light", Icon: Sun },
2674
2782
  { value: "system", label: "System", Icon: Monitor },
2675
2783
  { value: "dark", label: "Dark", Icon: Moon }
2676
2784
  ];
2677
- function ThemeSegment({ pref, onPref }) {
2678
- return /* @__PURE__ */ jsx47(
2785
+ function ThemeSegment({
2786
+ pref,
2787
+ onPref
2788
+ }) {
2789
+ return /* @__PURE__ */ jsx48(
2679
2790
  ToggleGroup,
2680
2791
  {
2681
2792
  size: "sm",
@@ -2685,7 +2796,17 @@ function ThemeSegment({ pref, onPref }) {
2685
2796
  if (next) onPref(next);
2686
2797
  },
2687
2798
  "aria-label": "Color theme",
2688
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx47(ToggleGroupItem, { value, "aria-label": label, title: label, className: "px-2", children: /* @__PURE__ */ jsx47(Icon, {}) }, value))
2799
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx48(
2800
+ ToggleGroupItem,
2801
+ {
2802
+ value,
2803
+ "aria-label": label,
2804
+ title: label,
2805
+ className: "px-2",
2806
+ children: /* @__PURE__ */ jsx48(Icon, {})
2807
+ },
2808
+ value
2809
+ ))
2689
2810
  }
2690
2811
  );
2691
2812
  }
@@ -2703,7 +2824,10 @@ function ReportInteractiveView({
2703
2824
  }) {
2704
2825
  const [urlState, setUrlState] = useUrlState();
2705
2826
  const { query, status: statusFilter, tags: activeTags, detail } = urlState;
2706
- const setQuery = useCallback5((next) => setUrlState({ query: next }), [setUrlState]);
2827
+ const setQuery = useCallback5(
2828
+ (next) => setUrlState({ query: next }),
2829
+ [setUrlState]
2830
+ );
2707
2831
  const setStatusFilter = useCallback5(
2708
2832
  (next) => setUrlState({ status: next }),
2709
2833
  [setUrlState]
@@ -2712,7 +2836,7 @@ function ReportInteractiveView({
2712
2836
  (next) => setUrlState({ detail: next }),
2713
2837
  [setUrlState]
2714
2838
  );
2715
- const [helpOpen, setHelpOpen] = useState8(false);
2839
+ const [helpOpen, setHelpOpen] = useState9(false);
2716
2840
  const { pref: themePref, setPref: setThemePref } = useTheme();
2717
2841
  const searchRef = useRef5(null);
2718
2842
  const expandId = useId2();
@@ -2722,7 +2846,13 @@ function ReportInteractiveView({
2722
2846
  const filtered = useMemo3(
2723
2847
  // Features carrying failures float to the top so the first thing on screen
2724
2848
  // is the thing that needs attention; an all-green run keeps source order.
2725
- () => failuresFirst(filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags })),
2849
+ () => failuresFirst(
2850
+ filterReport(report, {
2851
+ query: deferredQuery,
2852
+ status: statusFilter,
2853
+ tags: activeTags
2854
+ })
2855
+ ),
2726
2856
  [report, deferredQuery, statusFilter, activeTags]
2727
2857
  );
2728
2858
  const isFiltering = query !== deferredQuery;
@@ -2731,10 +2861,14 @@ function ReportInteractiveView({
2731
2861
  const opts = [
2732
2862
  { key: "all", label: "All", count: s.total }
2733
2863
  ];
2734
- if (s.passed) opts.push({ key: "passed", label: "Passed", count: s.passed });
2735
- if (s.failed) opts.push({ key: "failed", label: "Failed", count: s.failed });
2736
- if (s.skipped) opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2737
- if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
2864
+ if (s.passed)
2865
+ opts.push({ key: "passed", label: "Passed", count: s.passed });
2866
+ if (s.failed)
2867
+ opts.push({ key: "failed", label: "Failed", count: s.failed });
2868
+ if (s.skipped)
2869
+ opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2870
+ if (s.pending)
2871
+ opts.push({ key: "pending", label: "Pending", count: s.pending });
2738
2872
  return opts;
2739
2873
  }, [report]);
2740
2874
  const tagOptions = useMemo3(() => allTags(report), [report]);
@@ -2744,7 +2878,7 @@ function ReportInteractiveView({
2744
2878
  }),
2745
2879
  [activeTags, setUrlState]
2746
2880
  );
2747
- const [toast, setToast] = useState8(null);
2881
+ const [toast, setToast] = useState9(null);
2748
2882
  const toastTimer = useRef5(null);
2749
2883
  const showToast = useCallback5((msg) => {
2750
2884
  setToast(msg);
@@ -2754,7 +2888,10 @@ function ReportInteractiveView({
2754
2888
  const copy = useCallback5(
2755
2889
  (text, msg) => {
2756
2890
  if (typeof navigator !== "undefined" && navigator.clipboard) {
2757
- navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
2891
+ navigator.clipboard.writeText(text).then(
2892
+ () => showToast(msg),
2893
+ () => showToast("Copy failed")
2894
+ );
2758
2895
  }
2759
2896
  },
2760
2897
  [showToast]
@@ -2772,9 +2909,21 @@ function ReportInteractiveView({
2772
2909
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
2773
2910
  [report]
2774
2911
  );
2775
- const collapse = useCollapseState(useCallback5(() => defaultCollapsedIds(report), [report]));
2776
- const collapseAll = useCallback5(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2777
- const [expandedAll, setExpandedAll] = useState8(() => report.summary.failed === 0);
2912
+ const collapse = useCollapseState(
2913
+ useCallback5(() => defaultCollapsedIds(report), [report])
2914
+ );
2915
+ const revealFilteredResults = deferredQuery.trim() !== "" || statusFilter !== "all" || activeTags.length > 0;
2916
+ const visibleCollapse = useMemo3(
2917
+ () => revealFilteredResults ? { ...collapse.api, isCollapsed: () => false } : collapse.api,
2918
+ [collapse.api, revealFilteredResults]
2919
+ );
2920
+ const collapseAll = useCallback5(
2921
+ () => collapse.collapseAll(allCollapsibleIds),
2922
+ [collapse, allCollapsibleIds]
2923
+ );
2924
+ const [expandedAll, setExpandedAll] = useState9(
2925
+ () => report.summary.failed === 0
2926
+ );
2778
2927
  const setExpanded = useCallback5(
2779
2928
  (expanded) => {
2780
2929
  if (expanded) collapse.expandAll();
@@ -2801,7 +2950,7 @@ function ReportInteractiveView({
2801
2950
  const escape = useCallback5(() => {
2802
2951
  if (helpOpen) setHelpOpen(false);
2803
2952
  else if (query !== "") setQuery("");
2804
- }, [helpOpen, query]);
2953
+ }, [helpOpen, query, setQuery]);
2805
2954
  useKeyboardShortcuts({
2806
2955
  onFocusSearch: focusSearch,
2807
2956
  onNextFailure: () => stepFailure(1),
@@ -2816,13 +2965,13 @@ function ReportInteractiveView({
2816
2965
  const totalScenarios = report.summary.total;
2817
2966
  const matchedScenarios = filtered.summary.total;
2818
2967
  const showToc = !hideToc && totalScenarios > 3;
2819
- return /* @__PURE__ */ jsx47(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ jsx47(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx47(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx47(
2968
+ return /* @__PURE__ */ jsx48(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx48(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx48(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx48(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx48(
2820
2969
  ReportRoot,
2821
2970
  {
2822
2971
  report: filtered,
2823
2972
  customRenderers,
2824
2973
  renderers,
2825
- children: /* @__PURE__ */ jsxs31(
2974
+ children: /* @__PURE__ */ jsxs33(
2826
2975
  "main",
2827
2976
  {
2828
2977
  className: cn("es-report", "es-report-interactive", className),
@@ -2831,8 +2980,8 @@ function ReportInteractiveView({
2831
2980
  "data-theme": dataTheme,
2832
2981
  "data-detail-level": detail,
2833
2982
  children: [
2834
- /* @__PURE__ */ jsxs31("header", { className: "es-report-header", children: [
2835
- /* @__PURE__ */ jsxs31(
2983
+ /* @__PURE__ */ jsxs33("header", { className: "es-report-header", children: [
2984
+ /* @__PURE__ */ jsxs33(
2836
2985
  "div",
2837
2986
  {
2838
2987
  "data-es-navbar": true,
@@ -2841,9 +2990,9 @@ function ReportInteractiveView({
2841
2990
  hideHeader ? "justify-end" : "justify-between"
2842
2991
  ),
2843
2992
  children: [
2844
- hideHeader ? null : /* @__PURE__ */ jsx47("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2845
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center gap-2", children: [
2846
- /* @__PURE__ */ jsx47(
2993
+ hideHeader ? null : /* @__PURE__ */ jsx48("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2994
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
2995
+ /* @__PURE__ */ jsx48(
2847
2996
  ReportSearch,
2848
2997
  {
2849
2998
  ref: searchRef,
@@ -2853,16 +3002,16 @@ function ReportInteractiveView({
2853
3002
  totalCount: totalScenarios
2854
3003
  }
2855
3004
  ),
2856
- /* @__PURE__ */ jsx47(ThemeSegment, { pref: themePref, onPref: setThemePref })
3005
+ /* @__PURE__ */ jsx48(ThemeSegment, { pref: themePref, onPref: setThemePref })
2857
3006
  ] })
2858
3007
  ]
2859
3008
  }
2860
3009
  ),
2861
- hideHeader ? null : /* @__PURE__ */ jsxs31(Fragment13, { children: [
2862
- /* @__PURE__ */ jsx47(ReportSummary, {}),
2863
- /* @__PURE__ */ jsx47(ReportMeta, {})
3010
+ hideHeader ? null : /* @__PURE__ */ jsxs33(Fragment13, { children: [
3011
+ /* @__PURE__ */ jsx48(ReportSummary, {}),
3012
+ /* @__PURE__ */ jsx48(ReportMeta, {})
2864
3013
  ] }),
2865
- /* @__PURE__ */ jsx47(
3014
+ /* @__PURE__ */ jsx48(
2866
3015
  ReportFreshness,
2867
3016
  {
2868
3017
  lastRunMs: reportLastRunMs(report),
@@ -2870,8 +3019,14 @@ function ReportInteractiveView({
2870
3019
  staleAfterDays
2871
3020
  }
2872
3021
  ),
2873
- /* @__PURE__ */ jsx47(ReportLastRunDelta, { history: scenarioHistory, report }),
2874
- /* @__PURE__ */ jsx47(
3022
+ /* @__PURE__ */ jsx48(
3023
+ ReportLastRunDelta,
3024
+ {
3025
+ history: scenarioHistory,
3026
+ report
3027
+ }
3028
+ ),
3029
+ /* @__PURE__ */ jsx48(
2875
3030
  ReportFilters,
2876
3031
  {
2877
3032
  statuses: statusOptions,
@@ -2882,14 +3037,28 @@ function ReportInteractiveView({
2882
3037
  onToggleTag: toggleTag
2883
3038
  }
2884
3039
  ),
2885
- /* @__PURE__ */ jsxs31("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
2886
- showToc ? /* @__PURE__ */ jsx47(ReportTocDrawer, {}) : null,
2887
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center gap-2", children: [
2888
- /* @__PURE__ */ jsx47(Switch, { id: expandId, checked: expandedAll, onCheckedChange: setExpanded }),
2889
- /* @__PURE__ */ jsx47("label", { htmlFor: expandId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Expand all" })
3040
+ /* @__PURE__ */ jsxs33("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3041
+ showToc ? /* @__PURE__ */ jsx48(ReportTocDrawer, {}) : null,
3042
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
3043
+ /* @__PURE__ */ jsx48(
3044
+ Switch,
3045
+ {
3046
+ id: expandId,
3047
+ checked: expandedAll,
3048
+ onCheckedChange: setExpanded
3049
+ }
3050
+ ),
3051
+ /* @__PURE__ */ jsx48(
3052
+ "label",
3053
+ {
3054
+ htmlFor: expandId,
3055
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3056
+ children: "Expand all"
3057
+ }
3058
+ )
2890
3059
  ] }),
2891
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center gap-2", children: [
2892
- /* @__PURE__ */ jsx47(
3060
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
3061
+ /* @__PURE__ */ jsx48(
2893
3062
  Switch,
2894
3063
  {
2895
3064
  id: docsId,
@@ -2897,16 +3066,28 @@ function ReportInteractiveView({
2897
3066
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
2898
3067
  }
2899
3068
  ),
2900
- /* @__PURE__ */ jsx47("label", { htmlFor: docsId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Show documentation" })
3069
+ /* @__PURE__ */ jsx48(
3070
+ "label",
3071
+ {
3072
+ htmlFor: docsId,
3073
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3074
+ children: "Show documentation"
3075
+ }
3076
+ )
2901
3077
  ] })
2902
3078
  ] })
2903
3079
  ] }),
2904
- /* @__PURE__ */ jsx47(ReportFailureBanner, { failures }),
2905
- hasContent ? showToc ? /* @__PURE__ */ jsxs31("div", { className: "flex gap-6", children: [
2906
- /* @__PURE__ */ jsx47(ReportToc, {}),
2907
- /* @__PURE__ */ jsx47("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx47(ReportFeatureList, {}) })
2908
- ] }) : /* @__PURE__ */ jsx47("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx47(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx47(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
2909
- /* @__PURE__ */ jsx47(
3080
+ /* @__PURE__ */ jsx48(ReportFailureBanner, { failures }),
3081
+ hasContent ? showToc ? /* @__PURE__ */ jsxs33("div", { className: "flex gap-6", children: [
3082
+ /* @__PURE__ */ jsx48(ReportToc, {}),
3083
+ /* @__PURE__ */ jsx48("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) })
3084
+ ] }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx48(
3085
+ ReportEmpty,
3086
+ {
3087
+ message: query ? "No scenarios match the search." : void 0
3088
+ }
3089
+ ),
3090
+ /* @__PURE__ */ jsx48(
2910
3091
  "button",
2911
3092
  {
2912
3093
  type: "button",
@@ -2917,8 +3098,14 @@ function ReportInteractiveView({
2917
3098
  children: "?"
2918
3099
  }
2919
3100
  ),
2920
- /* @__PURE__ */ jsx47(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
2921
- toast ? /* @__PURE__ */ jsx47(
3101
+ /* @__PURE__ */ jsx48(
3102
+ ReportShortcutsHelp,
3103
+ {
3104
+ open: helpOpen,
3105
+ onClose: () => setHelpOpen(false)
3106
+ }
3107
+ ),
3108
+ toast ? /* @__PURE__ */ jsx48(
2922
3109
  "div",
2923
3110
  {
2924
3111
  role: "status",
@@ -2931,18 +3118,18 @@ function ReportInteractiveView({
2931
3118
  }
2932
3119
  )
2933
3120
  }
2934
- ) }) }) });
3121
+ ) }) }) }) });
2935
3122
  }
2936
3123
 
2937
3124
  // src/interactive/ReportInteractiveIsland.tsx
2938
3125
  import { useMemo as useMemo4 } from "react";
2939
3126
 
2940
3127
  // src/interactive/island-renderers.tsx
2941
- import { createElement, useEffect as useEffect9, useState as useState10 } from "react";
3128
+ import { createElement, useEffect as useEffect10, useState as useState11 } from "react";
2942
3129
 
2943
3130
  // src/components/doc/MermaidView.tsx
2944
- import { useEffect as useEffect8, useRef as useRef6, useState as useState9 } from "react";
2945
- import { Fragment as Fragment14, jsx as jsx48, jsxs as jsxs32 } from "react/jsx-runtime";
3131
+ import { useEffect as useEffect9, useRef as useRef6, useState as useState10 } from "react";
3132
+ import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
2946
3133
  function prefersDark(el) {
2947
3134
  const themed = el?.closest("[data-theme]");
2948
3135
  if (themed?.dataset.theme === "dark") return true;
@@ -2951,9 +3138,9 @@ function prefersDark(el) {
2951
3138
  }
2952
3139
  function MermaidView({ entry, load }) {
2953
3140
  const ref = useRef6(null);
2954
- const [svg, setSvg] = useState9(null);
2955
- const [error, setError] = useState9(null);
2956
- useEffect8(() => {
3141
+ const [svg, setSvg] = useState10(null);
3142
+ const [error, setError] = useState10(null);
3143
+ useEffect9(() => {
2957
3144
  let cancelled = false;
2958
3145
  const draw = async () => {
2959
3146
  let mermaid;
@@ -2996,9 +3183,9 @@ function MermaidView({ entry, load }) {
2996
3183
  };
2997
3184
  }, [entry.code, load]);
2998
3185
  if (svg) {
2999
- return /* @__PURE__ */ jsxs32("figure", { ref, className: "my-3", children: [
3000
- entry.title ? /* @__PURE__ */ jsx48("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3001
- /* @__PURE__ */ jsx48(
3186
+ return /* @__PURE__ */ jsxs34("figure", { ref, className: "my-3", children: [
3187
+ entry.title ? /* @__PURE__ */ jsx49("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3188
+ /* @__PURE__ */ jsx49(
3002
3189
  "div",
3003
3190
  {
3004
3191
  role: "img",
@@ -3009,8 +3196,8 @@ function MermaidView({ entry, load }) {
3009
3196
  )
3010
3197
  ] });
3011
3198
  }
3012
- return /* @__PURE__ */ jsxs32(Fragment14, { children: [
3013
- error ? /* @__PURE__ */ jsxs32(
3199
+ return /* @__PURE__ */ jsxs34(Fragment14, { children: [
3200
+ error ? /* @__PURE__ */ jsxs34(
3014
3201
  "p",
3015
3202
  {
3016
3203
  role: "alert",
@@ -3022,12 +3209,12 @@ function MermaidView({ entry, load }) {
3022
3209
  ]
3023
3210
  }
3024
3211
  ) : null,
3025
- /* @__PURE__ */ jsx48(MermaidSource, { entry, ref })
3212
+ /* @__PURE__ */ jsx49(MermaidSource, { entry, ref })
3026
3213
  ] });
3027
3214
  }
3028
3215
 
3029
3216
  // src/interactive/island-renderers.tsx
3030
- import { jsx as jsx49 } from "react/jsx-runtime";
3217
+ import { jsx as jsx50 } from "react/jsx-runtime";
3031
3218
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
3032
3219
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
3033
3220
  function cachedCdnModule(url) {
@@ -3044,8 +3231,8 @@ function cachedCdnModule(url) {
3044
3231
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3045
3232
  var loadHljs = cachedCdnModule(HLJS_ESM);
3046
3233
  function CdnHighlightedCode({ entry }) {
3047
- const [html, setHtml] = useState10(null);
3048
- useEffect9(() => {
3234
+ const [html, setHtml] = useState11(null);
3235
+ useEffect10(() => {
3049
3236
  let cancelled = false;
3050
3237
  void (async () => {
3051
3238
  try {
@@ -3061,13 +3248,13 @@ function CdnHighlightedCode({ entry }) {
3061
3248
  cancelled = true;
3062
3249
  };
3063
3250
  }, [entry.content, entry.lang]);
3064
- return /* @__PURE__ */ jsx49(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx49(
3251
+ return /* @__PURE__ */ jsx50(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx50(
3065
3252
  "code",
3066
3253
  {
3067
3254
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
3068
3255
  dangerouslySetInnerHTML: { __html: html }
3069
3256
  }
3070
- ) : /* @__PURE__ */ jsx49("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3257
+ ) : /* @__PURE__ */ jsx50("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3071
3258
  }
3072
3259
  function buildIslandRenderers(opts) {
3073
3260
  const renderers = {};
@@ -3081,7 +3268,7 @@ function buildIslandRenderers(opts) {
3081
3268
  }
3082
3269
 
3083
3270
  // src/interactive/ReportInteractiveIsland.tsx
3084
- import { jsx as jsx50 } from "react/jsx-runtime";
3271
+ import { jsx as jsx51 } from "react/jsx-runtime";
3085
3272
  function ReportInteractiveIsland({
3086
3273
  report,
3087
3274
  title,
@@ -3095,7 +3282,7 @@ function ReportInteractiveIsland({
3095
3282
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
3096
3283
  [syntaxHighlighting, mermaid]
3097
3284
  );
3098
- return /* @__PURE__ */ jsx50(
3285
+ return /* @__PURE__ */ jsx51(
3099
3286
  ReportInteractive,
3100
3287
  {
3101
3288
  report,