executable-stories-react 0.1.16 → 0.2.1

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.
@@ -2,12 +2,22 @@
2
2
 
3
3
  // src/interactive/ReportInteractive.tsx
4
4
  import {
5
- useCallback as useCallback2,
6
- useMemo as useMemo2,
5
+ useCallback as useCallback4,
6
+ useDeferredValue,
7
+ useMemo as useMemo3,
7
8
  useRef as useRef3,
8
- useState
9
+ useState as useState4
9
10
  } from "react";
10
11
 
12
+ // src/result.ts
13
+ var ok = (data) => ({ ok: true, data });
14
+ function unwrapReport(value) {
15
+ if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
16
+ return value;
17
+ }
18
+ return ok(value);
19
+ }
20
+
11
21
  // src/context/ReportRoot.tsx
12
22
  import { useMemo } from "react";
13
23
 
@@ -48,74 +58,80 @@ function useReport() {
48
58
  return ctx.report;
49
59
  }
50
60
 
51
- // src/components/ReportSummary.tsx
52
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
53
- function ReportSummary({ className }) {
54
- const report = useReport();
55
- return /* @__PURE__ */ jsx2(
56
- ReportSummaryView,
57
- {
58
- summary: report.summary,
59
- ...className !== void 0 && { className },
60
- ariaLabel: "Run summary"
61
- }
62
- );
63
- }
64
- function ReportSummaryView({ summary, className, ariaLabel }) {
65
- return /* @__PURE__ */ jsxs(
66
- "p",
67
- {
68
- className: ["es-report-summary", className].filter(Boolean).join(" "),
69
- "aria-label": ariaLabel,
70
- children: [
71
- /* @__PURE__ */ jsxs("span", { children: [
72
- /* @__PURE__ */ jsx2("strong", { children: summary.total }),
73
- " scenario",
74
- summary.total === 1 ? "" : "s"
75
- ] }),
76
- " \xB7 ",
77
- /* @__PURE__ */ jsxs("span", { "data-status": "passed", children: [
78
- summary.passed,
79
- " passed"
80
- ] }),
81
- " \xB7 ",
82
- /* @__PURE__ */ jsxs("span", { "data-status": "failed", children: [
83
- summary.failed,
84
- " failed"
85
- ] }),
86
- summary.skipped > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
87
- " \xB7 ",
88
- /* @__PURE__ */ jsxs("span", { "data-status": "skipped", children: [
89
- summary.skipped,
90
- " skipped"
91
- ] })
92
- ] }) : null,
93
- summary.pending > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
94
- " \xB7 ",
95
- /* @__PURE__ */ jsxs("span", { "data-status": "pending", children: [
96
- summary.pending,
97
- " pending"
98
- ] })
99
- ] }) : null
100
- ]
101
- }
102
- );
103
- }
61
+ // src/components/ReportScenario.tsx
62
+ import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
63
+
64
+ // src/components/ReportSteps.tsx
65
+ import { formatDuration } from "executable-stories-core/utils/duration";
104
66
 
105
67
  // src/components/doc/DocNote.tsx
106
- import { jsx as jsx3 } from "react/jsx-runtime";
68
+ import { jsx as jsx2 } from "react/jsx-runtime";
107
69
  function DocNote({ entry }) {
108
- return /* @__PURE__ */ jsx3("p", { className: "es-doc es-doc-note", children: entry.text });
70
+ return /* @__PURE__ */ jsx2("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
71
+ }
72
+
73
+ // src/components/ui/badge.tsx
74
+ import "react";
75
+ import { cva } from "class-variance-authority";
76
+ import { useRender } from "@base-ui/react/use-render";
77
+
78
+ // src/lib/utils.ts
79
+ import { clsx } from "clsx";
80
+ import { twMerge } from "tailwind-merge";
81
+ function cn(...inputs) {
82
+ return twMerge(clsx(inputs));
83
+ }
84
+
85
+ // src/components/ui/badge.tsx
86
+ var badgeVariants = cva(
87
+ "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",
88
+ {
89
+ variants: {
90
+ variant: {
91
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
92
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
93
+ destructive: "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
94
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
95
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
96
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
97
+ passed: "border-pass-border bg-pass-bg text-pass font-mono uppercase",
98
+ failed: "border-fail-border bg-fail-bg text-fail font-mono uppercase",
99
+ skipped: "border-skip-border bg-skip-bg text-skip font-mono uppercase",
100
+ pending: "border-pend-border bg-pend-bg text-pend font-mono uppercase",
101
+ tag: "border-pass-border bg-pass-bg text-pass font-mono font-medium"
102
+ }
103
+ },
104
+ defaultVariants: {
105
+ variant: "default"
106
+ }
107
+ }
108
+ );
109
+ function Badge({
110
+ className,
111
+ variant = "default",
112
+ render,
113
+ ...props
114
+ }) {
115
+ return useRender({
116
+ defaultTagName: "span",
117
+ render,
118
+ props: {
119
+ "data-slot": "badge",
120
+ "data-variant": variant,
121
+ className: cn(badgeVariants({ variant }), className),
122
+ ...props
123
+ }
124
+ });
109
125
  }
110
126
 
111
127
  // src/components/doc/DocTag.tsx
112
- import { jsx as jsx4 } from "react/jsx-runtime";
128
+ import { jsx as jsx3 } from "react/jsx-runtime";
113
129
  function DocTag({ entry }) {
114
- return /* @__PURE__ */ jsx4("ul", { className: "es-doc es-tags", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx4("li", { children: n }, n)) });
130
+ return /* @__PURE__ */ jsx3("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3(Badge, { variant: "tag", children: n }) }, n)) });
115
131
  }
116
132
 
117
133
  // src/components/doc/DocKv.tsx
118
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
134
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
119
135
  function formatValue(value) {
120
136
  if (value === null) return "null";
121
137
  if (typeof value === "string") return value;
@@ -127,9 +143,9 @@ function formatValue(value) {
127
143
  }
128
144
  }
129
145
  function DocKv({ entry }) {
130
- return /* @__PURE__ */ jsxs2("dl", { className: "es-doc es-doc-kv", children: [
131
- /* @__PURE__ */ jsx5("dt", { children: entry.label }),
132
- /* @__PURE__ */ jsx5("dd", { children: formatValue(entry.value) })
146
+ return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
147
+ /* @__PURE__ */ jsx4("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
148
+ /* @__PURE__ */ jsx4("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
133
149
  ] });
134
150
  }
135
151
 
@@ -147,37 +163,40 @@ function useBuiltinRenderers() {
147
163
  }
148
164
 
149
165
  // src/components/doc/DocCode.tsx
150
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
166
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
167
+ function CodeFigure({ label, children }) {
168
+ return /* @__PURE__ */ jsxs2("figure", { className: "my-3", children: [
169
+ label ? /* @__PURE__ */ jsx5("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
170
+ /* @__PURE__ */ jsx5("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
171
+ ] });
172
+ }
151
173
  function DocCode({ entry }) {
152
174
  const renderers = useBuiltinRenderers();
153
175
  if (renderers.code) {
154
- return /* @__PURE__ */ jsx6(Fragment2, { children: renderers.code(entry) });
176
+ return /* @__PURE__ */ jsx5(Fragment, { children: renderers.code(entry) });
155
177
  }
156
- return /* @__PURE__ */ jsxs3("figure", { className: "es-doc es-doc-code", children: [
157
- /* @__PURE__ */ jsx6("figcaption", { children: entry.label }),
158
- /* @__PURE__ */ jsx6("pre", { children: /* @__PURE__ */ jsx6("code", { className: entry.lang ? `language-${entry.lang}` : void 0, children: entry.content }) })
159
- ] });
178
+ return /* @__PURE__ */ jsx5(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx5("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
160
179
  }
161
180
 
162
181
  // src/components/doc/DocTable.tsx
163
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
182
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
164
183
  function DocTable({ entry }) {
165
- return /* @__PURE__ */ jsxs4("figure", { className: "es-doc es-doc-table", children: [
166
- /* @__PURE__ */ jsx7("figcaption", { children: entry.label }),
167
- /* @__PURE__ */ jsxs4("table", { children: [
168
- /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { children: entry.columns.map((c) => /* @__PURE__ */ jsx7("th", { scope: "col", children: c }, c)) }) }),
169
- /* @__PURE__ */ jsx7("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx7("td", { children: cell }, j)) }, i)) })
170
- ] })
184
+ return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
185
+ entry.label ? /* @__PURE__ */ jsx6("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
186
+ /* @__PURE__ */ jsx6("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs3("table", { className: "w-full border-collapse text-sm", children: [
187
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx6("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
188
+ /* @__PURE__ */ jsx6("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx6("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx6("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
189
+ ] }) })
171
190
  ] });
172
191
  }
173
192
 
174
193
  // src/components/doc/DocLink.tsx
175
- import { jsx as jsx8 } from "react/jsx-runtime";
194
+ import { jsx as jsx7 } from "react/jsx-runtime";
176
195
  function DocLink({ entry }) {
177
- return /* @__PURE__ */ jsx8(
196
+ return /* @__PURE__ */ jsx7(
178
197
  "a",
179
198
  {
180
- className: "es-doc es-doc-link",
199
+ className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
181
200
  href: entry.url,
182
201
  rel: "noreferrer noopener",
183
202
  target: "_blank",
@@ -188,23 +207,46 @@ function DocLink({ entry }) {
188
207
 
189
208
  // src/components/doc/DocSection.tsx
190
209
  import { marked } from "marked";
191
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
210
+
211
+ // src/lib/url.ts
212
+ function safeUrl(value) {
213
+ if (!value) return void 0;
214
+ const trimmed = value.trim();
215
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
216
+ if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
217
+ return trimmed;
218
+ }
219
+ function safeImageUrl(value) {
220
+ if (!value) return void 0;
221
+ const trimmed = value.trim();
222
+ if (/^data:image\//i.test(trimmed)) return trimmed;
223
+ return safeUrl(trimmed);
224
+ }
225
+ function isLocalFsPath(value) {
226
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
227
+ }
228
+
229
+ // src/components/doc/DocSection.tsx
230
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
231
+ function neutralizeUrl(value) {
232
+ return safeUrl(value) ?? "#";
233
+ }
192
234
  function safeMarkdownHtml(markdown) {
193
235
  const raw = marked.parse(markdown, { async: false });
194
- return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/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*"\s*javascript:[^"]*"/gi, '$1="#"').replace(/(href|src)\s*=\s*'\s*javascript:[^']*'/gi, "$1='#'");
236
+ 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)}'`);
195
237
  }
196
238
  function DocSection({ entry }) {
197
239
  const renderers = useBuiltinRenderers();
198
240
  if (renderers.section) {
199
- return /* @__PURE__ */ jsx9(Fragment3, { children: renderers.section(entry) });
241
+ return /* @__PURE__ */ jsx8(Fragment2, { children: renderers.section(entry) });
200
242
  }
201
243
  const html = safeMarkdownHtml(entry.markdown);
202
- return /* @__PURE__ */ jsxs5("section", { className: "es-doc es-doc-section", "aria-label": entry.title, children: [
203
- entry.title ? /* @__PURE__ */ jsx9("h4", { className: "es-doc-section-title", children: entry.title }) : null,
204
- /* @__PURE__ */ jsx9(
244
+ return /* @__PURE__ */ jsxs4("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
245
+ entry.title ? /* @__PURE__ */ jsx8("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
246
+ /* @__PURE__ */ jsx8(
205
247
  "div",
206
248
  {
207
- className: "es-doc-section-content",
249
+ className: "es-doc-prose text-muted-foreground",
208
250
  dangerouslySetInnerHTML: { __html: html }
209
251
  }
210
252
  )
@@ -212,45 +254,129 @@ function DocSection({ entry }) {
212
254
  }
213
255
 
214
256
  // src/components/doc/DocMermaid.tsx
215
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
257
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
258
+ function MermaidSource({ entry, ref }) {
259
+ return /* @__PURE__ */ jsxs5("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
260
+ entry.title ? /* @__PURE__ */ jsx9("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
261
+ /* @__PURE__ */ jsx9(
262
+ "pre",
263
+ {
264
+ "data-mermaid": true,
265
+ tabIndex: 0,
266
+ className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
267
+ children: entry.code
268
+ }
269
+ )
270
+ ] });
271
+ }
216
272
  function DocMermaid({ entry }) {
217
273
  const renderers = useBuiltinRenderers();
218
274
  if (renderers.mermaid) {
219
- return /* @__PURE__ */ jsx10(Fragment4, { children: renderers.mermaid(entry) });
275
+ return /* @__PURE__ */ jsx9(Fragment3, { children: renderers.mermaid(entry) });
220
276
  }
221
- return /* @__PURE__ */ jsxs6(
222
- "figure",
223
- {
224
- className: "es-doc es-doc-mermaid",
225
- "aria-label": entry.title ?? "Diagram",
226
- children: [
227
- entry.title ? /* @__PURE__ */ jsx10("figcaption", { children: entry.title }) : null,
228
- /* @__PURE__ */ jsx10("pre", { "data-mermaid": true, children: entry.code })
229
- ]
230
- }
231
- );
277
+ return /* @__PURE__ */ jsx9(MermaidSource, { entry });
232
278
  }
233
279
 
234
280
  // src/components/doc/DocScreenshot.tsx
235
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
281
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
236
282
  function DocScreenshot({ entry }) {
237
- return /* @__PURE__ */ jsxs7("figure", { className: "es-doc es-doc-screenshot", children: [
238
- /* @__PURE__ */ jsx11("img", { src: entry.path, alt: entry.alt ?? "", loading: "lazy" }),
239
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { children: entry.alt }) : null
283
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
284
+ if (!src) {
285
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
286
+ /* @__PURE__ */ jsx10("div", { className: "font-medium", children: "Screenshot unavailable" }),
287
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
288
+ entry.alt ? /* @__PURE__ */ jsx10("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
289
+ ] });
290
+ }
291
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
292
+ /* @__PURE__ */ jsx10(
293
+ "img",
294
+ {
295
+ src,
296
+ alt: entry.alt ?? "",
297
+ loading: "lazy",
298
+ className: "max-w-full rounded-md border border-border"
299
+ }
300
+ ),
301
+ entry.alt ? /* @__PURE__ */ jsx10("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
302
+ ] });
303
+ }
304
+
305
+ // src/components/doc/DocVideo.tsx
306
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
307
+ function DocVideo({ entry }) {
308
+ if (isLocalFsPath(entry.path)) {
309
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
310
+ /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Video unavailable" }),
311
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
312
+ entry.caption ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
313
+ ] });
314
+ }
315
+ const src = safeUrl(entry.path);
316
+ const poster = safeUrl(entry.poster);
317
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
318
+ /* @__PURE__ */ jsx11(
319
+ "video",
320
+ {
321
+ controls: true,
322
+ preload: "metadata",
323
+ ...src ? { src } : {},
324
+ ...poster ? { poster } : {},
325
+ className: "max-w-full rounded-md border border-border"
326
+ }
327
+ ),
328
+ entry.caption ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
329
+ ] });
330
+ }
331
+
332
+ // src/components/doc/DocHtml.tsx
333
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
334
+ function DocHtml({ entry }) {
335
+ const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
336
+ const title = entry.title ?? "Embedded HTML";
337
+ const safeHref = safeUrl(entry.url ?? entry.path);
338
+ const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
339
+ return /* @__PURE__ */ jsxs8("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
340
+ /* @__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: [
341
+ /* @__PURE__ */ jsx12("span", { children: entry.title ?? "HTML" }),
342
+ safeHref ? /* @__PURE__ */ jsx12(
343
+ "a",
344
+ {
345
+ href: safeHref,
346
+ target: "_blank",
347
+ rel: "noreferrer noopener",
348
+ "aria-label": "Open in new tab",
349
+ title: "Open in new tab",
350
+ className: "text-primary",
351
+ children: "\u2197"
352
+ }
353
+ ) : null
354
+ ] }),
355
+ /* @__PURE__ */ jsx12(
356
+ "iframe",
357
+ {
358
+ ...frameProps,
359
+ sandbox: "allow-scripts",
360
+ loading: "lazy",
361
+ title,
362
+ className: "block w-full border-0",
363
+ style: { height }
364
+ }
365
+ )
240
366
  ] });
241
367
  }
242
368
 
243
369
  // src/components/doc/DocCustom.tsx
244
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
370
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
245
371
  function DocCustom({ entry }) {
246
372
  const renderers = useCustomRenderers();
247
373
  const renderer = renderers[entry.type];
248
374
  if (renderer) {
249
- return /* @__PURE__ */ jsx12(Fragment5, { children: renderer(entry) });
375
+ return /* @__PURE__ */ jsx13(Fragment4, { children: renderer(entry) });
250
376
  }
251
- return /* @__PURE__ */ jsxs8("div", { className: "es-doc es-doc-custom", "data-type": entry.type, children: [
252
- /* @__PURE__ */ jsx12("p", { className: "es-doc-custom-type", children: entry.type }),
253
- /* @__PURE__ */ jsx12("pre", { children: safeStringify(entry.data) })
377
+ return /* @__PURE__ */ jsxs9("div", { className: "my-2", "data-type": entry.type, children: [
378
+ /* @__PURE__ */ jsx13("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
379
+ /* @__PURE__ */ jsx13("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
254
380
  ] });
255
381
  }
256
382
  function safeStringify(value) {
@@ -262,160 +388,812 @@ function safeStringify(value) {
262
388
  }
263
389
 
264
390
  // src/components/doc/DocEntry.tsx
265
- import { jsx as jsx13 } from "react/jsx-runtime";
391
+ import { jsx as jsx14 } from "react/jsx-runtime";
266
392
  function DocEntry({ entry }) {
267
393
  switch (entry.kind) {
268
394
  case "note":
269
- return /* @__PURE__ */ jsx13(DocNote, { entry });
395
+ return /* @__PURE__ */ jsx14(DocNote, { entry });
270
396
  case "tag":
271
- return /* @__PURE__ */ jsx13(DocTag, { entry });
397
+ return /* @__PURE__ */ jsx14(DocTag, { entry });
272
398
  case "kv":
273
- return /* @__PURE__ */ jsx13(DocKv, { entry });
399
+ return /* @__PURE__ */ jsx14(DocKv, { entry });
274
400
  case "code":
275
- return /* @__PURE__ */ jsx13(DocCode, { entry });
401
+ return /* @__PURE__ */ jsx14(DocCode, { entry });
276
402
  case "table":
277
- return /* @__PURE__ */ jsx13(DocTable, { entry });
403
+ return /* @__PURE__ */ jsx14(DocTable, { entry });
278
404
  case "link":
279
- return /* @__PURE__ */ jsx13(DocLink, { entry });
405
+ return /* @__PURE__ */ jsx14(DocLink, { entry });
280
406
  case "section":
281
- return /* @__PURE__ */ jsx13(DocSection, { entry });
407
+ return /* @__PURE__ */ jsx14(DocSection, { entry });
282
408
  case "mermaid":
283
- return /* @__PURE__ */ jsx13(DocMermaid, { entry });
409
+ return /* @__PURE__ */ jsx14(DocMermaid, { entry });
284
410
  case "screenshot":
285
- return /* @__PURE__ */ jsx13(DocScreenshot, { entry });
411
+ return /* @__PURE__ */ jsx14(DocScreenshot, { entry });
412
+ case "video":
413
+ return /* @__PURE__ */ jsx14(DocVideo, { entry });
414
+ case "html":
415
+ return /* @__PURE__ */ jsx14(DocHtml, { entry });
286
416
  case "custom":
287
- return /* @__PURE__ */ jsx13(DocCustom, { entry });
417
+ return /* @__PURE__ */ jsx14(DocCustom, { entry });
288
418
  }
289
419
  }
290
420
 
291
421
  // src/components/ReportDocEntries.tsx
292
- import { Fragment as Fragment6, jsx as jsx14 } from "react/jsx-runtime";
422
+ import { Fragment as Fragment5, jsx as jsx15 } from "react/jsx-runtime";
293
423
  function ReportDocEntries({ entries }) {
294
424
  if (entries.length === 0) return null;
295
- return /* @__PURE__ */ jsx14(Fragment6, { children: entries.map((entry, i) => /* @__PURE__ */ jsx14(DocEntry, { entry }, i)) });
425
+ return /* @__PURE__ */ jsx15(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx15(DocEntry, { entry }, i)) });
426
+ }
427
+
428
+ // src/lib/step-params.tsx
429
+ import { jsx as jsx16 } from "react/jsx-runtime";
430
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
431
+ function highlightStepParams(text) {
432
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
433
+ if (matches.length === 0) return text;
434
+ const nodes = [];
435
+ let lastIndex = 0;
436
+ matches.forEach((match, i) => {
437
+ const start = match.index;
438
+ const end = start + match[0].length;
439
+ if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
440
+ nodes.push(
441
+ /* @__PURE__ */ jsx16("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
442
+ );
443
+ lastIndex = end;
444
+ });
445
+ if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
446
+ return nodes;
296
447
  }
297
448
 
298
449
  // src/components/ReportSteps.tsx
299
- import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
450
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
451
+ var STEP_GLYPH = {
452
+ passed: "\u2713",
453
+ failed: "\u2717",
454
+ skipped: "\u25CB",
455
+ pending: "\u25CB"
456
+ };
457
+ var STEP_GLYPH_COLOR = {
458
+ passed: "text-pass",
459
+ failed: "text-fail",
460
+ skipped: "text-skip",
461
+ pending: "text-pend"
462
+ };
300
463
  function ReportSteps({ scenario }) {
301
464
  if (scenario.steps.length === 0) return null;
302
- return /* @__PURE__ */ jsx15("ol", { className: "es-steps", children: scenario.steps.map((step) => /* @__PURE__ */ jsx15(ReportStepItem, { step }, step.id)) });
465
+ return /* @__PURE__ */ jsx17("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx17(ReportStepItem, { step }, step.id)) });
303
466
  }
304
467
  function ReportStepItem({ step }) {
305
- return /* @__PURE__ */ jsxs9(
468
+ const kw = step.keyword.toLowerCase();
469
+ const continuation = kw === "and" || kw === "but";
470
+ return /* @__PURE__ */ jsxs10(
306
471
  "li",
307
472
  {
308
473
  id: step.id,
309
- className: `es-step es-step-${step.status}`,
310
474
  "data-status": step.status,
475
+ className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
311
476
  children: [
312
- /* @__PURE__ */ jsx15("span", { className: "es-step-keyword", children: step.keyword }),
313
- /* @__PURE__ */ jsx15("span", { className: "es-step-text", children: step.text }),
314
- step.errorMessage ? /* @__PURE__ */ jsx15("pre", { className: "es-scenario-error", role: "alert", children: step.errorMessage }) : null,
315
- step.docEntries.length > 0 ? /* @__PURE__ */ jsx15("div", { className: "es-step-docs", children: /* @__PURE__ */ jsx15(ReportDocEntries, { entries: step.docEntries }) }) : null
477
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-baseline gap-2", children: [
478
+ /* @__PURE__ */ jsx17("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" }),
479
+ /* @__PURE__ */ jsx17(
480
+ "span",
481
+ {
482
+ className: cn(
483
+ "min-w-[52px] shrink-0 font-mono text-xs font-semibold capitalize",
484
+ continuation ? "font-medium text-muted-foreground" : "text-keyword"
485
+ ),
486
+ children: step.keyword
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsx17("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
490
+ step.durationMs > 0 ? /* @__PURE__ */ jsx17("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
491
+ ] }),
492
+ step.errorMessage ? /* @__PURE__ */ jsx17(
493
+ "pre",
494
+ {
495
+ role: "alert",
496
+ className: "mt-2.5 ml-6 overflow-x-auto rounded-md border border-fail-border bg-fail-bg px-4 py-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap text-fail",
497
+ children: step.errorMessage
498
+ }
499
+ ) : null,
500
+ step.docEntries.length > 0 ? /* @__PURE__ */ jsx17("div", { "data-es-docs": true, className: "mt-2 ml-6", children: /* @__PURE__ */ jsx17(ReportDocEntries, { entries: step.docEntries }) }) : null
316
501
  ]
317
502
  }
318
503
  );
319
504
  }
320
505
 
506
+ // src/components/ReportAttachments.tsx
507
+ import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
508
+ function ReportAttachments({ attachments }) {
509
+ if (attachments.length === 0) return null;
510
+ return (
511
+ // role="group" (not a labelled <section>) so multiple scenarios' attachment
512
+ // blocks don't create duplicate "Attachments" region landmarks.
513
+ /* @__PURE__ */ jsx18("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
514
+ const dataUri = att.contentEncoding === "BASE64" ? `data:${att.mediaType};base64,${att.body}` : `data:${att.mediaType},${encodeURIComponent(att.body)}`;
515
+ const isImage = att.mediaType.startsWith("image/");
516
+ return /* @__PURE__ */ jsx18("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
517
+ /* @__PURE__ */ jsx18(
518
+ "img",
519
+ {
520
+ src: dataUri,
521
+ alt: att.name,
522
+ loading: "lazy",
523
+ className: "max-w-full rounded-md border border-border"
524
+ }
525
+ ),
526
+ /* @__PURE__ */ jsx18("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
527
+ ] }) : /* @__PURE__ */ jsxs11(
528
+ "a",
529
+ {
530
+ href: dataUri,
531
+ download: att.name,
532
+ className: "text-xs text-link underline underline-offset-2",
533
+ children: [
534
+ att.name,
535
+ " ",
536
+ /* @__PURE__ */ jsxs11("span", { className: "text-muted-foreground", children: [
537
+ "(",
538
+ att.mediaType,
539
+ ")"
540
+ ] })
541
+ ]
542
+ }
543
+ ) }, `${att.name}-${i}`);
544
+ }) })
545
+ );
546
+ }
547
+
548
+ // src/components/ReportTrace.tsx
549
+ import { formatDuration as formatDuration2 } from "executable-stories-core/utils/duration";
550
+
551
+ // src/lib/trace.ts
552
+ var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
553
+ var safeStatus = (s) => VALID.has(s) ? s : "unset";
554
+ function normalizeSpans(spans) {
555
+ const out = [];
556
+ for (const span of spans) {
557
+ if (!span || typeof span.spanId !== "string" || typeof span.name !== "string") continue;
558
+ let startTimeMs;
559
+ let durationMs;
560
+ if (span.startTimeMs != null && span.durationMs != null) {
561
+ startTimeMs = span.startTimeMs;
562
+ durationMs = span.durationMs;
563
+ } else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
564
+ startTimeMs = span.startTimeUnixNano / 1e6;
565
+ durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
566
+ } else {
567
+ continue;
568
+ }
569
+ durationMs = Math.max(0, durationMs);
570
+ if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
571
+ out.push({
572
+ spanId: span.spanId,
573
+ parentSpanId: span.parentSpanId,
574
+ name: span.name,
575
+ startTimeMs,
576
+ durationMs,
577
+ status: safeStatus(span.status),
578
+ statusMessage: span.statusMessage
579
+ });
580
+ }
581
+ return out;
582
+ }
583
+ function toTraceRows(spans) {
584
+ const normalized = normalizeSpans(spans);
585
+ if (normalized.length === 0) return [];
586
+ const byId = /* @__PURE__ */ new Map();
587
+ for (const span of normalized) {
588
+ let key = span.spanId;
589
+ let suffix = 2;
590
+ while (byId.has(key)) key = `${span.spanId}__dup${suffix++}`;
591
+ byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
592
+ }
593
+ const roots = [];
594
+ for (const node of byId.values()) {
595
+ const parent = node.span.parentSpanId ? byId.get(node.span.parentSpanId) : void 0;
596
+ if (parent && parent !== node) parent.children.push(node);
597
+ else roots.push(node);
598
+ }
599
+ for (const node of byId.values()) node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
600
+ roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
601
+ const rows = [];
602
+ const seen = /* @__PURE__ */ new Set();
603
+ const walk = (node, depth) => {
604
+ if (seen.has(node.span.spanId)) return;
605
+ seen.add(node.span.spanId);
606
+ rows.push({ ...node.span, depth });
607
+ for (const child of node.children) walk(child, depth + 1);
608
+ };
609
+ for (const root of roots) walk(root, 0);
610
+ for (const node of byId.values()) {
611
+ if (!seen.has(node.span.spanId)) walk(node, 0);
612
+ }
613
+ return rows;
614
+ }
615
+ function traceWindow(rows) {
616
+ let min = Infinity;
617
+ let max = -Infinity;
618
+ for (const r of rows) {
619
+ min = Math.min(min, r.startTimeMs);
620
+ max = Math.max(max, r.startTimeMs + r.durationMs);
621
+ }
622
+ const total = max - min;
623
+ return { minStartMs: isFinite(min) ? min : 0, totalMs: total > 0 ? total : 1 };
624
+ }
625
+
626
+ // src/components/ReportTrace.tsx
627
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
628
+ var BAR_COLOR = {
629
+ ok: "bg-pass",
630
+ error: "bg-fail",
631
+ unset: "bg-skip"
632
+ };
633
+ function ReportTrace({ spans }) {
634
+ if (!spans || spans.length === 0) return null;
635
+ const rows = toTraceRows(spans);
636
+ if (rows.length === 0) return null;
637
+ const { minStartMs, totalMs } = traceWindow(rows);
638
+ return /* @__PURE__ */ jsxs12("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
639
+ /* @__PURE__ */ jsxs12("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
640
+ "Trace (",
641
+ rows.length,
642
+ " span",
643
+ rows.length === 1 ? "" : "s",
644
+ ", ",
645
+ formatDuration2(totalMs),
646
+ ")"
647
+ ] }),
648
+ /* @__PURE__ */ jsx19("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
649
+ const leftPct = (row.startTimeMs - minStartMs) / totalMs * 100;
650
+ const widthPct = Math.max(row.durationMs / totalMs * 100, 0.5);
651
+ return /* @__PURE__ */ jsxs12("li", { className: "flex items-center gap-2", children: [
652
+ /* @__PURE__ */ jsx19(
653
+ "span",
654
+ {
655
+ className: "min-w-0 flex-1 truncate text-foreground",
656
+ style: { paddingLeft: `${row.depth * 12}px` },
657
+ title: row.statusMessage ? `${row.name} \u2014 ${row.statusMessage}` : row.name,
658
+ children: row.name
659
+ }
660
+ ),
661
+ /* @__PURE__ */ jsx19("span", { className: "relative h-3 w-1/2 shrink-0 overflow-hidden rounded-sm bg-border/40", children: /* @__PURE__ */ jsx19(
662
+ "span",
663
+ {
664
+ className: cn("absolute top-0 h-full rounded-sm", BAR_COLOR[row.status]),
665
+ style: { left: `${leftPct}%`, width: `${widthPct}%` }
666
+ }
667
+ ) }),
668
+ /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration2(row.durationMs) })
669
+ ] }, `${row.spanId}-${i}`);
670
+ }) })
671
+ ] });
672
+ }
673
+
674
+ // src/interactive/collapse-context.ts
675
+ import { createContext as createContext2, useCallback, useContext as useContext3, useState } from "react";
676
+ var CollapseContext = createContext2(null);
677
+ var CollapseProvider = CollapseContext.Provider;
678
+ function useCollapse() {
679
+ return useContext3(CollapseContext);
680
+ }
681
+ var STORAGE_KEY = "es-collapsed-ids";
682
+ function readPersisted() {
683
+ if (typeof localStorage === "undefined") return /* @__PURE__ */ new Set();
684
+ try {
685
+ const raw = localStorage.getItem(STORAGE_KEY);
686
+ return raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
687
+ } catch {
688
+ return /* @__PURE__ */ new Set();
689
+ }
690
+ }
691
+ function persist(ids) {
692
+ if (typeof localStorage === "undefined") return;
693
+ try {
694
+ localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]));
695
+ } catch {
696
+ }
697
+ }
698
+ function useCollapseState() {
699
+ const [collapsed, setCollapsed] = useState(readPersisted);
700
+ const isCollapsed = useCallback((id) => collapsed.has(id), [collapsed]);
701
+ const toggle = useCallback((id) => {
702
+ setCollapsed((prev) => {
703
+ const next = new Set(prev);
704
+ if (next.has(id)) next.delete(id);
705
+ else next.add(id);
706
+ persist(next);
707
+ return next;
708
+ });
709
+ }, []);
710
+ const collapseAll = useCallback((ids) => {
711
+ setCollapsed(() => {
712
+ const next = new Set(ids);
713
+ persist(next);
714
+ return next;
715
+ });
716
+ }, []);
717
+ const expandAll = useCallback(() => {
718
+ setCollapsed(() => {
719
+ const next = /* @__PURE__ */ new Set();
720
+ persist(next);
721
+ return next;
722
+ });
723
+ }, []);
724
+ return { api: { isCollapsed, toggle }, collapseAll, expandAll };
725
+ }
726
+
727
+ // src/interactive/scenario-actions.tsx
728
+ import { createContext as createContext3, useContext as useContext4 } from "react";
729
+ var ScenarioActionsContext = createContext3(null);
730
+ var ScenarioActionsProvider = ScenarioActionsContext.Provider;
731
+ function useScenarioActions() {
732
+ return useContext4(ScenarioActionsContext);
733
+ }
734
+ function scenarioToMarkdown(scenario) {
735
+ const lines = [`## ${scenario.title} _(${scenario.status})_`, ""];
736
+ for (const step of scenario.steps) {
737
+ lines.push(`- **${step.keyword}** ${step.text}`);
738
+ }
739
+ if (scenario.errorMessage) {
740
+ lines.push("", "```", scenario.errorMessage.trim(), "```");
741
+ }
742
+ return lines.join("\n");
743
+ }
744
+ function scenarioToPrompt(scenario) {
745
+ const lines = [
746
+ `A test scenario is ${scenario.status}. Investigate the likely cause and propose a fix.`,
747
+ "",
748
+ `Scenario: ${scenario.title}`,
749
+ "Steps:"
750
+ ];
751
+ for (const step of scenario.steps) {
752
+ const mark = step.status === "failed" ? " \u27F5 failed here" : "";
753
+ lines.push(` ${step.keyword} ${step.text}${mark}`);
754
+ }
755
+ if (scenario.errorMessage) {
756
+ lines.push("", "Error:", scenario.errorMessage.trim());
757
+ }
758
+ if (scenario.errorStack) {
759
+ lines.push("", "Stack:", scenario.errorStack.trim());
760
+ }
761
+ return lines.join("\n");
762
+ }
763
+ function scenarioPermalink(scenario) {
764
+ if (typeof location === "undefined") return `#${scenario.id}`;
765
+ return `${location.href.split("#")[0]}#${scenario.id}`;
766
+ }
767
+
768
+ // src/components/ui/card.tsx
769
+ import "react";
770
+ import { jsx as jsx20 } from "react/jsx-runtime";
771
+ function Card({ className, ...props }) {
772
+ return /* @__PURE__ */ jsx20(
773
+ "div",
774
+ {
775
+ "data-slot": "card",
776
+ className: cn(
777
+ "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
778
+ className
779
+ ),
780
+ ...props
781
+ }
782
+ );
783
+ }
784
+ function CardHeader({ className, ...props }) {
785
+ return /* @__PURE__ */ jsx20(
786
+ "div",
787
+ {
788
+ "data-slot": "card-header",
789
+ className: cn(
790
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
791
+ className
792
+ ),
793
+ ...props
794
+ }
795
+ );
796
+ }
797
+ function CardContent({ className, ...props }) {
798
+ return /* @__PURE__ */ jsx20(
799
+ "div",
800
+ {
801
+ "data-slot": "card-content",
802
+ className: cn("px-6", className),
803
+ ...props
804
+ }
805
+ );
806
+ }
807
+
321
808
  // src/components/ReportScenario.tsx
322
- import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
809
+ import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
323
810
  var STATUS_LABEL = {
324
811
  passed: "Passed",
325
812
  failed: "Failed",
326
813
  skipped: "Skipped",
327
814
  pending: "Pending"
328
815
  };
329
- function ReportScenario({ scenario }) {
816
+ var STATUS_GLYPH = {
817
+ passed: "\u2713",
818
+ failed: "\u2717",
819
+ skipped: "\u25CB",
820
+ pending: "\u25CB"
821
+ };
822
+ var STATUS_GLYPH_COLOR = {
823
+ passed: "text-pass",
824
+ failed: "text-fail",
825
+ skipped: "text-skip",
826
+ pending: "text-pend"
827
+ };
828
+ var STATUS_BADGE = {
829
+ passed: "passed",
830
+ failed: "failed",
831
+ skipped: "skipped",
832
+ pending: "pending"
833
+ };
834
+ function ReportScenario({ scenario, hideTitle = false }) {
330
835
  const titleId = `${scenario.id}-title`;
331
- return /* @__PURE__ */ jsxs10(
332
- "article",
836
+ const bodyId = `${scenario.id}-body`;
837
+ const label = STATUS_LABEL[scenario.status];
838
+ const collapse = useCollapse();
839
+ const collapsible = collapse !== null && !hideTitle;
840
+ const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
841
+ const actions = useScenarioActions();
842
+ return /* @__PURE__ */ jsxs13(
843
+ Card,
333
844
  {
334
845
  id: scenario.id,
335
- className: `es-scenario es-status-${scenario.status}`,
336
- "aria-labelledby": titleId,
337
846
  "data-status": scenario.status,
847
+ ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
848
+ className: "gap-0 py-0",
338
849
  children: [
339
- /* @__PURE__ */ jsxs10("h3", { id: titleId, className: "es-scenario-title", children: [
340
- /* @__PURE__ */ jsx16("span", { children: scenario.title }),
341
- /* @__PURE__ */ jsx16("span", { className: "es-scenario-status", "aria-label": `Status: ${STATUS_LABEL[scenario.status]}`, children: STATUS_LABEL[scenario.status] })
850
+ /* @__PURE__ */ jsxs13(CardHeader, { className: "px-4 py-3", children: [
851
+ /* @__PURE__ */ jsxs13("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
852
+ hideTitle ? null : /* @__PURE__ */ jsxs13("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
853
+ collapsible ? /* @__PURE__ */ jsx21(
854
+ "button",
855
+ {
856
+ type: "button",
857
+ onClick: () => collapse.toggle(scenario.id),
858
+ "aria-expanded": !collapsed,
859
+ "aria-controls": bodyId,
860
+ "aria-label": `Toggle ${scenario.title}`,
861
+ className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
862
+ children: /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
863
+ }
864
+ ) : null,
865
+ /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
866
+ /* @__PURE__ */ jsx21("span", { children: scenario.title })
867
+ ] }),
868
+ /* @__PURE__ */ jsxs13("div", { className: "flex shrink-0 items-center gap-2", children: [
869
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx21("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
870
+ /* @__PURE__ */ jsx21(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
871
+ actions ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-0.5", children: [
872
+ /* @__PURE__ */ jsx21(
873
+ "button",
874
+ {
875
+ type: "button",
876
+ onClick: () => actions.copyLink(scenario),
877
+ "aria-label": `Copy link to ${scenario.title}`,
878
+ title: "Copy permalink",
879
+ className: "cursor-pointer rounded px-1 py-0.5 font-mono text-[0.625rem] text-muted-foreground hover:bg-muted hover:text-foreground",
880
+ children: "link"
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsx21(
884
+ "button",
885
+ {
886
+ type: "button",
887
+ onClick: () => actions.copyMarkdown(scenario),
888
+ "aria-label": `Copy ${scenario.title} as Markdown`,
889
+ title: "Copy as Markdown",
890
+ className: "cursor-pointer rounded px-1 py-0.5 font-mono text-[0.625rem] text-muted-foreground hover:bg-muted hover:text-foreground",
891
+ children: "md"
892
+ }
893
+ ),
894
+ /* @__PURE__ */ jsx21(
895
+ "button",
896
+ {
897
+ type: "button",
898
+ onClick: () => actions.copyPrompt(scenario),
899
+ "aria-label": `Copy ${scenario.title} as an AI investigation prompt`,
900
+ title: "Copy as AI prompt",
901
+ className: "cursor-pointer rounded px-1 py-0.5 font-mono text-[0.625rem] text-muted-foreground hover:bg-muted hover:text-foreground",
902
+ children: "ai"
903
+ }
904
+ )
905
+ ] }) : null
906
+ ] })
907
+ ] }),
908
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs13("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
909
+ scenario.tags.map((t) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
910
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx21("li", { children: ticket.url ? /* @__PURE__ */ jsx21("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
911
+ ] }) : null
342
912
  ] }),
343
- scenario.tags.length > 0 ? /* @__PURE__ */ jsx16("ul", { className: "es-tags", "aria-label": "Tags", children: scenario.tags.map((t) => /* @__PURE__ */ jsx16("li", { children: t }, t)) }) : null,
344
- scenario.errorMessage ? /* @__PURE__ */ jsx16("pre", { className: "es-scenario-error", role: "alert", children: scenario.errorMessage }) : null,
345
- scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx16("div", { className: "es-scenario-docs", children: /* @__PURE__ */ jsx16(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
346
- /* @__PURE__ */ jsx16(ReportSteps, { scenario })
913
+ /* @__PURE__ */ jsxs13(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
914
+ scenario.errorMessage ? /* @__PURE__ */ jsx21(
915
+ "pre",
916
+ {
917
+ role: "alert",
918
+ className: "mb-1 overflow-x-auto rounded-md border border-fail-border bg-fail-bg px-4 py-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap text-fail",
919
+ children: scenario.errorMessage
920
+ }
921
+ ) : null,
922
+ /* @__PURE__ */ jsx21(ReportSteps, { scenario }),
923
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx21("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx21(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
924
+ /* @__PURE__ */ jsx21(ReportAttachments, { attachments: scenario.attachments }),
925
+ /* @__PURE__ */ jsx21(ReportTrace, { spans: scenario.otelSpans })
926
+ ] })
347
927
  ]
348
928
  }
349
929
  );
350
930
  }
351
931
 
352
932
  // src/components/ReportScenarioList.tsx
353
- import { Fragment as Fragment7, jsx as jsx17 } from "react/jsx-runtime";
933
+ import { Fragment as Fragment7, jsx as jsx22 } from "react/jsx-runtime";
354
934
  function ReportScenarioList({ feature }) {
355
- return /* @__PURE__ */ jsx17(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx17(ReportScenario, { scenario }, scenario.id)) });
935
+ return /* @__PURE__ */ jsx22(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx22(ReportScenario, { scenario }, scenario.id)) });
356
936
  }
357
937
 
358
938
  // src/components/ReportFeature.tsx
359
- import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
939
+ import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
360
940
  function ReportFeature({ feature }) {
361
941
  const titleId = `${feature.id}-title`;
362
- return /* @__PURE__ */ jsxs11(
942
+ const bodyId = `${feature.id}-body`;
943
+ const s = feature.summary;
944
+ const skipped = s.skipped + s.pending;
945
+ const collapse = useCollapse();
946
+ const collapsed = collapse?.isCollapsed(feature.id) ?? false;
947
+ return /* @__PURE__ */ jsxs14(
363
948
  "section",
364
949
  {
365
950
  id: feature.id,
366
- className: "es-feature",
951
+ "data-slot": "feature",
367
952
  "aria-labelledby": titleId,
953
+ className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
368
954
  children: [
369
- /* @__PURE__ */ jsx18("h2", { id: titleId, className: "es-feature-title", children: feature.title }),
370
- /* @__PURE__ */ jsx18("p", { className: "es-feature-source", children: feature.sourceFile }),
371
- /* @__PURE__ */ jsx18(ReportSummaryView, { summary: feature.summary, className: "es-feature-summary" }),
372
- /* @__PURE__ */ jsx18(ReportScenarioList, { feature })
955
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
956
+ /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 items-center gap-2", children: [
957
+ collapse ? /* @__PURE__ */ jsx23(
958
+ "button",
959
+ {
960
+ type: "button",
961
+ onClick: () => collapse.toggle(feature.id),
962
+ "aria-expanded": !collapsed,
963
+ "aria-controls": bodyId,
964
+ "aria-label": `Toggle ${feature.title}`,
965
+ className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
966
+ children: /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
967
+ }
968
+ ) : null,
969
+ /* @__PURE__ */ jsxs14("div", { className: "min-w-0", children: [
970
+ /* @__PURE__ */ jsx23("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
971
+ /* @__PURE__ */ jsx23("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
972
+ ] })
973
+ ] }),
974
+ /* @__PURE__ */ jsxs14(
975
+ "div",
976
+ {
977
+ role: "img",
978
+ className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
979
+ "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
980
+ children: [
981
+ /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-pass", children: [
982
+ "\u2713",
983
+ s.passed
984
+ ] }),
985
+ /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-fail", children: [
986
+ "\u2717",
987
+ s.failed
988
+ ] }),
989
+ /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-skip", children: [
990
+ "\u25CB",
991
+ skipped
992
+ ] })
993
+ ]
994
+ }
995
+ )
996
+ ] }),
997
+ /* @__PURE__ */ jsx23("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx23(ReportScenarioList, { feature }) })
373
998
  ]
374
999
  }
375
1000
  );
376
1001
  }
377
1002
 
378
1003
  // src/components/ReportFeatureList.tsx
379
- import { Fragment as Fragment8, jsx as jsx19 } from "react/jsx-runtime";
1004
+ import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
380
1005
  function ReportFeatureList() {
381
1006
  const report = useReport();
382
- return /* @__PURE__ */ jsx19(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx19(ReportFeature, { feature }, feature.id)) });
1007
+ return /* @__PURE__ */ jsx24(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx24(ReportFeature, { feature }, feature.id)) });
1008
+ }
1009
+
1010
+ // src/components/ui/empty.tsx
1011
+ import { cva as cva2 } from "class-variance-authority";
1012
+ import { jsx as jsx25 } from "react/jsx-runtime";
1013
+ function Empty({ className, ...props }) {
1014
+ return /* @__PURE__ */ jsx25(
1015
+ "div",
1016
+ {
1017
+ "data-slot": "empty",
1018
+ className: cn(
1019
+ "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",
1020
+ className
1021
+ ),
1022
+ ...props
1023
+ }
1024
+ );
1025
+ }
1026
+ function EmptyHeader({ className, ...props }) {
1027
+ return /* @__PURE__ */ jsx25(
1028
+ "div",
1029
+ {
1030
+ "data-slot": "empty-header",
1031
+ className: cn(
1032
+ "flex max-w-sm flex-col items-center gap-2 text-center",
1033
+ className
1034
+ ),
1035
+ ...props
1036
+ }
1037
+ );
1038
+ }
1039
+ var emptyMediaVariants = cva2(
1040
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
1041
+ {
1042
+ variants: {
1043
+ variant: {
1044
+ default: "bg-transparent",
1045
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
1046
+ }
1047
+ },
1048
+ defaultVariants: {
1049
+ variant: "default"
1050
+ }
1051
+ }
1052
+ );
1053
+ function EmptyTitle({ className, ...props }) {
1054
+ return /* @__PURE__ */ jsx25(
1055
+ "div",
1056
+ {
1057
+ "data-slot": "empty-title",
1058
+ className: cn("text-lg font-medium tracking-tight", className),
1059
+ ...props
1060
+ }
1061
+ );
383
1062
  }
384
1063
 
385
1064
  // src/components/ReportEmpty.tsx
386
- import { jsx as jsx20 } from "react/jsx-runtime";
1065
+ import { jsx as jsx26 } from "react/jsx-runtime";
387
1066
  function ReportEmpty({ message }) {
388
- return /* @__PURE__ */ jsx20("section", { className: "es-empty", "aria-live": "polite", children: /* @__PURE__ */ jsx20("p", { children: message ?? "No scenarios in this report." }) });
1067
+ return /* @__PURE__ */ jsx26(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx26(EmptyHeader, { children: /* @__PURE__ */ jsx26(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1068
+ }
1069
+
1070
+ // src/components/ReportSummary.tsx
1071
+ import { Fragment as Fragment9, jsx as jsx27, jsxs as jsxs15 } from "react/jsx-runtime";
1072
+ function ReportSummary({ className }) {
1073
+ const report = useReport();
1074
+ return /* @__PURE__ */ jsx27(
1075
+ ReportSummaryView,
1076
+ {
1077
+ summary: report.summary,
1078
+ ...className !== void 0 && { className },
1079
+ ariaLabel: "Run summary"
1080
+ }
1081
+ );
1082
+ }
1083
+ function ReportSummaryView({ summary, className, ariaLabel }) {
1084
+ return /* @__PURE__ */ jsxs15("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1085
+ /* @__PURE__ */ jsxs15("span", { children: [
1086
+ /* @__PURE__ */ jsx27("strong", { className: "font-semibold text-foreground", children: summary.total }),
1087
+ " scenario",
1088
+ summary.total === 1 ? "" : "s"
1089
+ ] }),
1090
+ " \xB7 ",
1091
+ /* @__PURE__ */ jsxs15("span", { "data-status": "passed", className: "text-pass", children: [
1092
+ summary.passed,
1093
+ " passed"
1094
+ ] }),
1095
+ " \xB7 ",
1096
+ /* @__PURE__ */ jsxs15("span", { "data-status": "failed", className: "text-fail", children: [
1097
+ summary.failed,
1098
+ " failed"
1099
+ ] }),
1100
+ summary.skipped > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1101
+ " \xB7 ",
1102
+ /* @__PURE__ */ jsxs15("span", { "data-status": "skipped", className: "text-skip", children: [
1103
+ summary.skipped,
1104
+ " skipped"
1105
+ ] })
1106
+ ] }) : null,
1107
+ summary.pending > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1108
+ " \xB7 ",
1109
+ /* @__PURE__ */ jsxs15("span", { "data-status": "pending", className: "text-pend", children: [
1110
+ summary.pending,
1111
+ " pending"
1112
+ ] })
1113
+ ] }) : null
1114
+ ] });
1115
+ }
1116
+
1117
+ // src/components/ReportMeta.tsx
1118
+ import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1119
+ import { jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
1120
+ function ReportMeta() {
1121
+ const report = useReport();
1122
+ const items = [];
1123
+ if (report.startedAtMs) {
1124
+ items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1125
+ }
1126
+ if (report.durationMs) {
1127
+ items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1128
+ }
1129
+ if (report.packageVersion) {
1130
+ items.push({ label: "Version", value: report.packageVersion });
1131
+ }
1132
+ if (report.gitSha) {
1133
+ items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1134
+ }
1135
+ if (report.ci) {
1136
+ items.push({
1137
+ label: "CI",
1138
+ value: report.ci.url ? /* @__PURE__ */ jsxs16("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1139
+ report.ci.name,
1140
+ report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1141
+ ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1142
+ });
1143
+ }
1144
+ if (items.length === 0) return null;
1145
+ return /* @__PURE__ */ jsx28("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ jsxs16("div", { className: "flex gap-1.5", children: [
1146
+ /* @__PURE__ */ jsx28("dt", { className: "font-medium", children: item.label }),
1147
+ /* @__PURE__ */ jsx28("dd", { className: "font-mono text-foreground", children: item.value })
1148
+ ] }, item.label)) });
389
1149
  }
390
1150
 
391
1151
  // src/components/ReportSchemaError.tsx
392
- import { jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
1152
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
393
1153
  function ReportSchemaError({ error }) {
394
- return /* @__PURE__ */ jsxs12("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
395
- /* @__PURE__ */ jsx21("p", { children: /* @__PURE__ */ jsx21("strong", { children: "Report could not be displayed." }) }),
396
- /* @__PURE__ */ jsx21("p", { children: error.message }),
397
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs12("p", { children: [
1154
+ return /* @__PURE__ */ jsxs17("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1155
+ /* @__PURE__ */ jsx29("p", { children: /* @__PURE__ */ jsx29("strong", { children: "Report could not be displayed." }) }),
1156
+ /* @__PURE__ */ jsx29("p", { children: error.message }),
1157
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs17("p", { children: [
398
1158
  "The report bundle is newer than this version of ",
399
- /* @__PURE__ */ jsx21("code", { children: "executable-stories-react" }),
1159
+ /* @__PURE__ */ jsx29("code", { children: "executable-stories-react" }),
400
1160
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
401
1161
  ] }) : null,
402
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs12("details", { children: [
403
- /* @__PURE__ */ jsxs12("summary", { children: [
1162
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs17("details", { children: [
1163
+ /* @__PURE__ */ jsxs17("summary", { children: [
404
1164
  error.issues.length,
405
1165
  " validation issue",
406
1166
  error.issues.length === 1 ? "" : "s"
407
1167
  ] }),
408
- /* @__PURE__ */ jsx21("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1168
+ /* @__PURE__ */ jsx29("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
409
1169
  ] }) : null
410
1170
  ] });
411
1171
  }
412
1172
 
1173
+ // src/components/ReportShell.tsx
1174
+ import { Fragment as Fragment10, jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1175
+ function ReportTitleBlock({ title }) {
1176
+ return /* @__PURE__ */ jsxs18(Fragment10, { children: [
1177
+ /* @__PURE__ */ jsx30("h1", { children: title ?? "Story Report" }),
1178
+ /* @__PURE__ */ jsx30(ReportSummary, {}),
1179
+ /* @__PURE__ */ jsx30(ReportMeta, {})
1180
+ ] });
1181
+ }
1182
+ function ReportErrorShell({
1183
+ error,
1184
+ className,
1185
+ title,
1186
+ dataTheme
1187
+ }) {
1188
+ return /* @__PURE__ */ jsx30("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx30(ReportSchemaError, { error }) });
1189
+ }
1190
+
413
1191
  // src/interactive/ReportSearch.tsx
414
1192
  import {
415
1193
  forwardRef,
416
1194
  useId
417
1195
  } from "react";
418
- import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
1196
+ import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
419
1197
  var ReportSearch = forwardRef(
420
1198
  function ReportSearch2(props, ref) {
421
1199
  const {
@@ -437,9 +1215,9 @@ var ReportSearch = forwardRef(
437
1215
  e.preventDefault();
438
1216
  }
439
1217
  }
440
- return /* @__PURE__ */ jsxs13("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
441
- /* @__PURE__ */ jsx22("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
442
- /* @__PURE__ */ jsx22(
1218
+ return /* @__PURE__ */ jsxs19("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
1219
+ /* @__PURE__ */ jsx31("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
1220
+ /* @__PURE__ */ jsx31(
443
1221
  "input",
444
1222
  {
445
1223
  ref,
@@ -454,41 +1232,58 @@ var ReportSearch = forwardRef(
454
1232
  "aria-keyshortcuts": "/"
455
1233
  }
456
1234
  ),
457
- showCounts ? /* @__PURE__ */ jsx22("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
1235
+ showCounts ? /* @__PURE__ */ jsx31("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
458
1236
  ] });
459
1237
  }
460
1238
  );
461
1239
 
462
1240
  // src/interactive/ReportFailureBanner.tsx
463
- import { useCallback } from "react";
464
- import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1241
+ import { useCallback as useCallback2 } from "react";
1242
+
1243
+ // src/lib/scroll.ts
1244
+ function prefersReducedMotion() {
1245
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1246
+ return false;
1247
+ }
1248
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1249
+ }
1250
+ function scrollToScenarioId(scenarioId, options = {}) {
1251
+ if (typeof document === "undefined") return;
1252
+ const el = document.getElementById(scenarioId);
1253
+ if (!el) return;
1254
+ el.scrollIntoView({
1255
+ behavior: prefersReducedMotion() ? "auto" : "smooth",
1256
+ block: "start"
1257
+ });
1258
+ if (options.updateHash !== false && typeof history !== "undefined") {
1259
+ history.replaceState(null, "", `#${scenarioId}`);
1260
+ }
1261
+ }
1262
+
1263
+ // src/interactive/ReportFailureBanner.tsx
1264
+ import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
465
1265
  function ReportFailureBanner({ failures }) {
466
1266
  const first = failures[0];
467
- const jumpToFirst = useCallback(() => {
1267
+ const jumpToFirst = useCallback2(() => {
468
1268
  if (!first) return;
469
- if (typeof window === "undefined") return;
470
- const el = document.getElementById(first.scenarioId);
471
- el?.scrollIntoView({ behavior: "smooth", block: "start" });
472
- if (typeof history !== "undefined") {
473
- history.replaceState(null, "", `#${first.scenarioId}`);
474
- }
1269
+ scrollToScenarioId(first.scenarioId);
475
1270
  }, [first]);
476
1271
  if (failures.length === 0) return null;
477
- return /* @__PURE__ */ jsxs14(
478
- "aside",
1272
+ return /* @__PURE__ */ jsxs20(
1273
+ "div",
479
1274
  {
480
1275
  className: "es-failure-banner",
481
1276
  role: "status",
482
1277
  "aria-live": "polite",
483
1278
  "aria-label": "Failure summary",
484
1279
  children: [
485
- /* @__PURE__ */ jsxs14("span", { className: "es-failure-banner-text", children: [
486
- /* @__PURE__ */ jsx23("strong", { children: failures.length }),
1280
+ /* @__PURE__ */ jsxs20("span", { className: "es-failure-banner-text", children: [
1281
+ /* @__PURE__ */ jsx32("strong", { children: failures.length }),
487
1282
  " ",
488
1283
  "failure",
489
1284
  failures.length === 1 ? "" : "s"
490
1285
  ] }),
491
- /* @__PURE__ */ jsx23(
1286
+ /* @__PURE__ */ jsx32(
492
1287
  "button",
493
1288
  {
494
1289
  type: "button",
@@ -505,7 +1300,7 @@ function ReportFailureBanner({ failures }) {
505
1300
 
506
1301
  // src/interactive/ReportShortcutsHelp.tsx
507
1302
  import { useEffect, useRef } from "react";
508
- import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
1303
+ import { jsx as jsx33, jsxs as jsxs21 } from "react/jsx-runtime";
509
1304
  var SHORTCUTS = [
510
1305
  { keys: "/", description: "Focus search" },
511
1306
  { keys: "f", description: "Jump to next failure" },
@@ -521,7 +1316,7 @@ function ReportShortcutsHelp({ open, onClose }) {
521
1316
  if (open && !dialog.open) dialog.showModal();
522
1317
  else if (!open && dialog.open) dialog.close();
523
1318
  }, [open]);
524
- return /* @__PURE__ */ jsxs15(
1319
+ return /* @__PURE__ */ jsxs21(
525
1320
  "dialog",
526
1321
  {
527
1322
  ref: dialogRef,
@@ -529,12 +1324,12 @@ function ReportShortcutsHelp({ open, onClose }) {
529
1324
  "aria-label": "Keyboard shortcuts",
530
1325
  onClose,
531
1326
  children: [
532
- /* @__PURE__ */ jsx24("h2", { children: "Keyboard shortcuts" }),
533
- /* @__PURE__ */ jsx24("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs15("div", { children: [
534
- /* @__PURE__ */ jsx24("dt", { children: /* @__PURE__ */ jsx24("kbd", { children: s.keys }) }),
535
- /* @__PURE__ */ jsx24("dd", { children: s.description })
1327
+ /* @__PURE__ */ jsx33("h2", { children: "Keyboard shortcuts" }),
1328
+ /* @__PURE__ */ jsx33("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs21("div", { children: [
1329
+ /* @__PURE__ */ jsx33("dt", { children: /* @__PURE__ */ jsx33("kbd", { children: s.keys }) }),
1330
+ /* @__PURE__ */ jsx33("dd", { children: s.description })
536
1331
  ] }, s.keys)) }),
537
- /* @__PURE__ */ jsx24("form", { method: "dialog", children: /* @__PURE__ */ jsx24("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
1332
+ /* @__PURE__ */ jsx33("form", { method: "dialog", children: /* @__PURE__ */ jsx33("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
538
1333
  ]
539
1334
  }
540
1335
  );
@@ -578,6 +1373,14 @@ function useKeyboardShortcuts(handlers) {
578
1373
  h.onPrevFailure?.();
579
1374
  e.preventDefault();
580
1375
  return;
1376
+ case "e":
1377
+ h.onExpandAll?.();
1378
+ e.preventDefault();
1379
+ return;
1380
+ case "c":
1381
+ h.onCollapseAll?.();
1382
+ e.preventDefault();
1383
+ return;
581
1384
  default:
582
1385
  return;
583
1386
  }
@@ -594,10 +1397,8 @@ function useDeepLinkScroll() {
594
1397
  function scrollToHash() {
595
1398
  const hash = window.location.hash.replace(/^#/, "");
596
1399
  if (!hash) return;
597
- const el = document.getElementById(hash);
598
- if (!el) return;
599
1400
  requestAnimationFrame(() => {
600
- el.scrollIntoView({ behavior: "smooth", block: "start" });
1401
+ scrollToScenarioId(hash, { updateHash: false });
601
1402
  });
602
1403
  }
603
1404
  scrollToHash();
@@ -610,7 +1411,16 @@ function useDeepLinkScroll() {
610
1411
  function normalizeQuery(query) {
611
1412
  return query.trim().toLowerCase();
612
1413
  }
613
- function scenarioMatches(scenario, q) {
1414
+ function allTags(report) {
1415
+ const seen = /* @__PURE__ */ new Set();
1416
+ for (const feature of report.features) {
1417
+ for (const scenario of feature.scenarios) {
1418
+ for (const tag of scenario.tags) seen.add(tag);
1419
+ }
1420
+ }
1421
+ return [...seen];
1422
+ }
1423
+ function queryMatches(scenario, q) {
614
1424
  if (q === "") return true;
615
1425
  if (scenario.title.toLowerCase().includes(q)) return true;
616
1426
  for (const tag of scenario.tags) {
@@ -621,6 +1431,11 @@ function scenarioMatches(scenario, q) {
621
1431
  }
622
1432
  return false;
623
1433
  }
1434
+ function scenarioMatches(scenario, q, status, tags) {
1435
+ if (status !== "all" && scenario.status !== status) return false;
1436
+ if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
1437
+ return queryMatches(scenario, q);
1438
+ }
624
1439
  function summarizeScenarios(scenarios) {
625
1440
  let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
626
1441
  for (const s of scenarios) {
@@ -633,13 +1448,16 @@ function summarizeScenarios(scenarios) {
633
1448
  }
634
1449
  return { total, passed, failed, skipped, pending, durationMs };
635
1450
  }
636
- function filterReport(report, query) {
637
- const q = normalizeQuery(query);
638
- if (q === "") return report;
1451
+ function filterReport(report, criteria) {
1452
+ const c = typeof criteria === "string" ? { query: criteria } : criteria;
1453
+ const q = normalizeQuery(c.query ?? "");
1454
+ const status = c.status ?? "all";
1455
+ const tags = c.tags ?? [];
1456
+ if (q === "" && status === "all" && tags.length === 0) return report;
639
1457
  const features = [];
640
1458
  let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
641
1459
  for (const feature of report.features) {
642
- const matched = feature.scenarios.filter((s) => scenarioMatches(s, q));
1460
+ const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
643
1461
  if (matched.length === 0) continue;
644
1462
  const summary = summarizeScenarios(matched);
645
1463
  features.push({ ...feature, summary, scenarios: matched });
@@ -683,28 +1501,170 @@ function listFailures(report) {
683
1501
  return out;
684
1502
  }
685
1503
 
686
- // src/interactive/ReportInteractive.tsx
687
- import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
688
- function isResult(value) {
689
- return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
690
- }
691
- function ReportInteractive(props) {
692
- const { report, className, title, dataTheme } = props;
693
- if (isResult(report)) {
694
- if (!report.ok) {
695
- return /* @__PURE__ */ jsx25(
696
- "main",
1504
+ // src/interactive/ReportFilters.tsx
1505
+ import { jsx as jsx34, jsxs as jsxs22 } from "react/jsx-runtime";
1506
+ function ReportFilters({
1507
+ statuses,
1508
+ status,
1509
+ onStatus,
1510
+ tags,
1511
+ activeTags,
1512
+ onToggleTag
1513
+ }) {
1514
+ if (statuses.length === 0 && tags.length === 0) return null;
1515
+ return /* @__PURE__ */ jsxs22("div", { className: "flex flex-col gap-2", children: [
1516
+ statuses.length > 0 ? /* @__PURE__ */ jsx34("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
1517
+ const active = status === opt.key;
1518
+ return /* @__PURE__ */ jsxs22(
1519
+ "button",
697
1520
  {
698
- className: ["es-report", className].filter(Boolean).join(" "),
699
- "aria-label": title ?? "Test report",
700
- "data-theme": dataTheme,
701
- children: /* @__PURE__ */ jsx25(ReportSchemaError, { error: report.error })
702
- }
1521
+ type: "button",
1522
+ "aria-pressed": active,
1523
+ onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
1524
+ className: cn(
1525
+ "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
1526
+ active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
1527
+ ),
1528
+ children: [
1529
+ opt.label,
1530
+ /* @__PURE__ */ jsx34("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
1531
+ ]
1532
+ },
1533
+ opt.key
703
1534
  );
1535
+ }) }) : null,
1536
+ tags.length > 0 ? /* @__PURE__ */ jsx34("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap gap-1.5", children: tags.map((tag) => {
1537
+ const active = activeTags.includes(tag);
1538
+ return /* @__PURE__ */ jsx34("li", { children: /* @__PURE__ */ jsx34(
1539
+ "button",
1540
+ {
1541
+ type: "button",
1542
+ "aria-pressed": active,
1543
+ onClick: () => onToggleTag(tag),
1544
+ className: "cursor-pointer",
1545
+ children: /* @__PURE__ */ jsx34(Badge, { variant: active ? "default" : "tag", children: tag })
1546
+ }
1547
+ ) }, tag);
1548
+ }) }) : null
1549
+ ] });
1550
+ }
1551
+
1552
+ // src/interactive/ReportToc.tsx
1553
+ import { useEffect as useEffect4, useMemo as useMemo2, useState as useState2 } from "react";
1554
+ import { jsx as jsx35, jsxs as jsxs23 } from "react/jsx-runtime";
1555
+ var DOT = {
1556
+ passed: "text-pass",
1557
+ failed: "text-fail",
1558
+ skipped: "text-skip",
1559
+ pending: "text-pend"
1560
+ };
1561
+ function ReportToc() {
1562
+ const report = useReport();
1563
+ const [activeId, setActiveId] = useState2(null);
1564
+ const scenarioIds = useMemo2(
1565
+ () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
1566
+ [report]
1567
+ );
1568
+ useEffect4(() => {
1569
+ if (typeof IntersectionObserver === "undefined") return;
1570
+ const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
1571
+ if (els.length === 0) return;
1572
+ const observer = new IntersectionObserver(
1573
+ (entries) => {
1574
+ const visible = entries.filter((e) => e.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
1575
+ if (visible[0]) setActiveId(visible[0].target.id);
1576
+ },
1577
+ { rootMargin: "-8% 0px -80% 0px" }
1578
+ );
1579
+ els.forEach((el) => observer.observe(el));
1580
+ return () => observer.disconnect();
1581
+ }, [scenarioIds]);
1582
+ if (report.features.length === 0) return null;
1583
+ return /* @__PURE__ */ jsx35(
1584
+ "nav",
1585
+ {
1586
+ "aria-label": "Table of contents",
1587
+ className: "sticky top-4 hidden h-fit w-56 shrink-0 self-start text-xs lg:block",
1588
+ children: /* @__PURE__ */ jsx35("ul", { className: "flex flex-col gap-3", children: report.features.map((feature) => /* @__PURE__ */ jsxs23("li", { children: [
1589
+ /* @__PURE__ */ jsx35("p", { className: "mb-1 truncate font-semibold text-foreground", children: feature.title }),
1590
+ /* @__PURE__ */ jsx35("ul", { className: "flex flex-col", children: feature.scenarios.map((scenario) => {
1591
+ const active = activeId === scenario.id;
1592
+ return /* @__PURE__ */ jsx35("li", { children: /* @__PURE__ */ jsxs23(
1593
+ "a",
1594
+ {
1595
+ href: `#${scenario.id}`,
1596
+ onClick: (e) => {
1597
+ e.preventDefault();
1598
+ scrollToScenarioId(scenario.id, { updateHash: true });
1599
+ },
1600
+ "aria-current": active ? "true" : void 0,
1601
+ className: cn(
1602
+ "flex items-center gap-1.5 truncate border-l-2 py-0.5 pl-2 hover:text-foreground",
1603
+ active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
1604
+ ),
1605
+ children: [
1606
+ /* @__PURE__ */ jsx35("span", { "aria-hidden": true, className: cn("shrink-0", DOT[scenario.status]), children: "\u2022" }),
1607
+ /* @__PURE__ */ jsx35("span", { className: "truncate", children: scenario.title })
1608
+ ]
1609
+ }
1610
+ ) }, scenario.id);
1611
+ }) })
1612
+ ] }, feature.id)) })
704
1613
  }
705
- return /* @__PURE__ */ jsx25(ReportInteractiveView, { ...props, report: report.data });
1614
+ );
1615
+ }
1616
+
1617
+ // src/interactive/use-theme.ts
1618
+ import { useCallback as useCallback3, useEffect as useEffect5, useState as useState3 } from "react";
1619
+ var KEY = "es-theme";
1620
+ function applyTheme(theme) {
1621
+ if (typeof document !== "undefined") {
1622
+ document.documentElement.setAttribute("data-theme", theme);
1623
+ }
1624
+ }
1625
+ function initialTheme() {
1626
+ if (typeof localStorage !== "undefined") {
1627
+ const stored = localStorage.getItem(KEY);
1628
+ if (stored === "light" || stored === "dark") return stored;
1629
+ }
1630
+ if (typeof document !== "undefined" && document.documentElement.getAttribute("data-theme") === "dark") {
1631
+ return "dark";
1632
+ }
1633
+ if (typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches) {
1634
+ return "dark";
1635
+ }
1636
+ return "light";
1637
+ }
1638
+ function useTheme() {
1639
+ const [theme, setTheme] = useState3("light");
1640
+ useEffect5(() => {
1641
+ const t = initialTheme();
1642
+ setTheme(t);
1643
+ applyTheme(t);
1644
+ }, []);
1645
+ const toggle = useCallback3(() => {
1646
+ setTheme((prev) => {
1647
+ const next = prev === "dark" ? "light" : "dark";
1648
+ applyTheme(next);
1649
+ try {
1650
+ localStorage.setItem(KEY, next);
1651
+ } catch {
1652
+ }
1653
+ return next;
1654
+ });
1655
+ }, []);
1656
+ return { theme, toggle };
1657
+ }
1658
+
1659
+ // src/interactive/ReportInteractive.tsx
1660
+ import { jsx as jsx36, jsxs as jsxs24 } from "react/jsx-runtime";
1661
+ function ReportInteractive(props) {
1662
+ const { report, className, title, dataTheme } = props;
1663
+ const result = unwrapReport(report);
1664
+ if (!result.ok) {
1665
+ return /* @__PURE__ */ jsx36(ReportErrorShell, { error: result.error, className, title, dataTheme });
706
1666
  }
707
- return /* @__PURE__ */ jsx25(ReportInteractiveView, { ...props, report });
1667
+ return /* @__PURE__ */ jsx36(ReportInteractiveView, { ...props, report: result.data });
708
1668
  }
709
1669
  function ReportInteractiveView({
710
1670
  report,
@@ -712,39 +1672,85 @@ function ReportInteractiveView({
712
1672
  renderers,
713
1673
  className,
714
1674
  title,
715
- dataTheme
1675
+ dataTheme,
1676
+ hideHeader = false
716
1677
  }) {
717
- const [query, setQuery] = useState("");
718
- const [helpOpen, setHelpOpen] = useState(false);
1678
+ const [query, setQuery] = useState4("");
1679
+ const [statusFilter, setStatusFilter] = useState4("all");
1680
+ const [activeTags, setActiveTags] = useState4([]);
1681
+ const [detail, setDetail] = useState4("full");
1682
+ const [helpOpen, setHelpOpen] = useState4(false);
1683
+ const { theme, toggle: toggleTheme } = useTheme();
719
1684
  const searchRef = useRef3(null);
720
- const failures = useMemo2(() => listFailures(report), [report]);
721
- const filtered = useMemo2(() => filterReport(report, query), [report, query]);
1685
+ const deferredQuery = useDeferredValue(query);
1686
+ const failures = useMemo3(() => listFailures(report), [report]);
1687
+ const filtered = useMemo3(
1688
+ () => filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags }),
1689
+ [report, deferredQuery, statusFilter, activeTags]
1690
+ );
1691
+ const isFiltering = query !== deferredQuery;
1692
+ const statusOptions = useMemo3(() => {
1693
+ const s = report.summary;
1694
+ const opts = [
1695
+ { key: "all", label: "All", count: s.total }
1696
+ ];
1697
+ if (s.passed) opts.push({ key: "passed", label: "Passed", count: s.passed });
1698
+ if (s.failed) opts.push({ key: "failed", label: "Failed", count: s.failed });
1699
+ if (s.skipped) opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
1700
+ if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
1701
+ return opts;
1702
+ }, [report]);
1703
+ const tagOptions = useMemo3(() => allTags(report), [report]);
1704
+ const toggleTag = useCallback4(
1705
+ (tag) => setActiveTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]),
1706
+ []
1707
+ );
1708
+ const [toast, setToast] = useState4(null);
1709
+ const toastTimer = useRef3(null);
1710
+ const showToast = useCallback4((msg) => {
1711
+ setToast(msg);
1712
+ if (toastTimer.current) clearTimeout(toastTimer.current);
1713
+ toastTimer.current = setTimeout(() => setToast(null), 1800);
1714
+ }, []);
1715
+ const copy = useCallback4(
1716
+ (text, msg) => {
1717
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
1718
+ navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
1719
+ }
1720
+ },
1721
+ [showToast]
1722
+ );
1723
+ const scenarioActions = useMemo3(
1724
+ () => ({
1725
+ copyLink: (s) => copy(scenarioPermalink(s), "Link copied"),
1726
+ copyMarkdown: (s) => copy(scenarioToMarkdown(s), "Markdown copied"),
1727
+ copyPrompt: (s) => copy(scenarioToPrompt(s), "Prompt copied")
1728
+ }),
1729
+ [copy]
1730
+ );
722
1731
  const failureIndexRef = useRef3(0);
723
- const focusSearch = useCallback2(() => {
1732
+ const collapse = useCollapseState();
1733
+ const allCollapsibleIds = useMemo3(
1734
+ () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
1735
+ [report]
1736
+ );
1737
+ const collapseAll = useCallback4(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
1738
+ const focusSearch = useCallback4(() => {
724
1739
  searchRef.current?.focus();
725
1740
  }, []);
726
- const scrollToScenario = useCallback2((scenarioId) => {
727
- if (typeof document === "undefined") return;
728
- const el = document.getElementById(scenarioId);
729
- if (!el) return;
730
- el.scrollIntoView({ behavior: "smooth", block: "start" });
731
- if (typeof history !== "undefined") {
732
- history.replaceState(null, "", `#${scenarioId}`);
733
- }
734
- }, []);
735
- const stepFailure = useCallback2(
1741
+ const stepFailure = useCallback4(
736
1742
  (direction) => {
737
1743
  if (failures.length === 0) return;
738
1744
  failureIndexRef.current = (failureIndexRef.current + direction + failures.length) % failures.length;
739
1745
  const target = failures[failureIndexRef.current];
740
- if (target) scrollToScenario(target.scenarioId);
1746
+ if (target) scrollToScenarioId(target.scenarioId);
741
1747
  },
742
- [failures, scrollToScenario]
1748
+ [failures]
743
1749
  );
744
- const toggleHelp = useCallback2(() => {
1750
+ const toggleHelp = useCallback4(() => {
745
1751
  setHelpOpen((v) => !v);
746
1752
  }, []);
747
- const escape = useCallback2(() => {
1753
+ const escape = useCallback4(() => {
748
1754
  if (helpOpen) setHelpOpen(false);
749
1755
  else if (query !== "") setQuery("");
750
1756
  }, [helpOpen, query]);
@@ -753,42 +1759,103 @@ function ReportInteractiveView({
753
1759
  onNextFailure: () => stepFailure(1),
754
1760
  onPrevFailure: () => stepFailure(-1),
755
1761
  onToggleHelp: toggleHelp,
756
- onEscape: escape
1762
+ onEscape: escape,
1763
+ onExpandAll: collapse.expandAll,
1764
+ onCollapseAll: collapseAll
757
1765
  });
758
1766
  useDeepLinkScroll();
759
1767
  const hasContent = filtered.features.length > 0;
760
1768
  const totalScenarios = report.summary.total;
761
1769
  const matchedScenarios = filtered.summary.total;
762
- return /* @__PURE__ */ jsx25(
1770
+ return /* @__PURE__ */ jsx36(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ jsx36(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx36(
763
1771
  ReportRoot,
764
1772
  {
765
1773
  report: filtered,
766
1774
  customRenderers,
767
1775
  renderers,
768
- children: /* @__PURE__ */ jsxs16(
1776
+ children: /* @__PURE__ */ jsxs24(
769
1777
  "main",
770
1778
  {
771
- className: ["es-report", "es-report-interactive", className].filter(Boolean).join(" "),
1779
+ className: cn("es-report", "es-report-interactive", className),
772
1780
  "aria-label": title ?? "Test report",
1781
+ "aria-busy": isFiltering,
773
1782
  "data-theme": dataTheme,
1783
+ "data-detail-level": detail,
774
1784
  children: [
775
- /* @__PURE__ */ jsxs16("header", { className: "es-report-header", children: [
776
- /* @__PURE__ */ jsx25("h1", { children: title ?? "Story Report" }),
777
- /* @__PURE__ */ jsx25(ReportSummary, {}),
778
- /* @__PURE__ */ jsx25(
779
- ReportSearch,
1785
+ /* @__PURE__ */ jsxs24("header", { className: "es-report-header", children: [
1786
+ hideHeader ? null : /* @__PURE__ */ jsx36(ReportTitleBlock, { title }),
1787
+ /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-center gap-2", children: [
1788
+ /* @__PURE__ */ jsx36(
1789
+ ReportSearch,
1790
+ {
1791
+ ref: searchRef,
1792
+ value: query,
1793
+ onChange: setQuery,
1794
+ matchedCount: matchedScenarios,
1795
+ totalCount: totalScenarios
1796
+ }
1797
+ ),
1798
+ /* @__PURE__ */ jsxs24("div", { className: "flex shrink-0 gap-1", children: [
1799
+ /* @__PURE__ */ jsx36(
1800
+ "button",
1801
+ {
1802
+ type: "button",
1803
+ onClick: collapse.expandAll,
1804
+ "aria-keyshortcuts": "e",
1805
+ className: "cursor-pointer rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground",
1806
+ children: "Expand all"
1807
+ }
1808
+ ),
1809
+ /* @__PURE__ */ jsx36(
1810
+ "button",
1811
+ {
1812
+ type: "button",
1813
+ onClick: collapseAll,
1814
+ "aria-keyshortcuts": "c",
1815
+ className: "cursor-pointer rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground",
1816
+ children: "Collapse all"
1817
+ }
1818
+ ),
1819
+ /* @__PURE__ */ jsx36(
1820
+ "button",
1821
+ {
1822
+ type: "button",
1823
+ onClick: () => setDetail((d) => d === "full" ? "minimal" : "full"),
1824
+ "aria-pressed": detail === "minimal",
1825
+ className: "cursor-pointer rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground",
1826
+ children: detail === "full" ? "Hide docs" : "Show docs"
1827
+ }
1828
+ ),
1829
+ /* @__PURE__ */ jsx36(
1830
+ "button",
1831
+ {
1832
+ type: "button",
1833
+ onClick: toggleTheme,
1834
+ "aria-label": `Switch to ${theme === "dark" ? "light" : "dark"} theme`,
1835
+ className: "cursor-pointer rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground",
1836
+ children: theme === "dark" ? "\u2600 Light" : "\u263E Dark"
1837
+ }
1838
+ )
1839
+ ] })
1840
+ ] }),
1841
+ /* @__PURE__ */ jsx36(
1842
+ ReportFilters,
780
1843
  {
781
- ref: searchRef,
782
- value: query,
783
- onChange: setQuery,
784
- matchedCount: matchedScenarios,
785
- totalCount: totalScenarios
1844
+ statuses: statusOptions,
1845
+ status: statusFilter,
1846
+ onStatus: setStatusFilter,
1847
+ tags: tagOptions,
1848
+ activeTags,
1849
+ onToggleTag: toggleTag
786
1850
  }
787
1851
  )
788
1852
  ] }),
789
- /* @__PURE__ */ jsx25(ReportFailureBanner, { failures }),
790
- hasContent ? /* @__PURE__ */ jsx25(ReportFeatureList, {}) : /* @__PURE__ */ jsx25(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
791
- /* @__PURE__ */ jsx25(
1853
+ /* @__PURE__ */ jsx36(ReportFailureBanner, { failures }),
1854
+ hasContent ? /* @__PURE__ */ jsxs24("div", { className: "flex gap-6", children: [
1855
+ /* @__PURE__ */ jsx36(ReportToc, {}),
1856
+ /* @__PURE__ */ jsx36("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx36(ReportFeatureList, {}) })
1857
+ ] }) : /* @__PURE__ */ jsx36(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
1858
+ /* @__PURE__ */ jsx36(
792
1859
  "button",
793
1860
  {
794
1861
  type: "button",
@@ -799,18 +1866,170 @@ function ReportInteractiveView({
799
1866
  children: "?"
800
1867
  }
801
1868
  ),
802
- /* @__PURE__ */ jsx25(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) })
1869
+ /* @__PURE__ */ jsx36(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
1870
+ toast ? /* @__PURE__ */ jsx36(
1871
+ "div",
1872
+ {
1873
+ role: "status",
1874
+ "aria-live": "polite",
1875
+ className: "fixed bottom-4 left-1/2 -translate-x-1/2 rounded-md bg-foreground px-3 py-1.5 text-xs font-medium text-background shadow-lg",
1876
+ children: toast
1877
+ }
1878
+ ) : null
803
1879
  ]
804
1880
  }
805
1881
  )
806
1882
  }
1883
+ ) }) });
1884
+ }
1885
+
1886
+ // src/interactive/ReportInteractiveIsland.tsx
1887
+ import { useMemo as useMemo4 } from "react";
1888
+
1889
+ // src/interactive/island-renderers.tsx
1890
+ import { createElement, useEffect as useEffect7, useState as useState6 } from "react";
1891
+
1892
+ // src/components/doc/MermaidView.tsx
1893
+ import { useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
1894
+ import { jsx as jsx37, jsxs as jsxs25 } from "react/jsx-runtime";
1895
+ function prefersDark(el) {
1896
+ const themed = el?.closest("[data-theme]");
1897
+ if (themed?.dataset.theme === "dark") return true;
1898
+ if (themed?.dataset.theme === "light") return false;
1899
+ return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
1900
+ }
1901
+ function MermaidView({ entry, load }) {
1902
+ const ref = useRef4(null);
1903
+ const [svg, setSvg] = useState5(null);
1904
+ useEffect6(() => {
1905
+ let cancelled = false;
1906
+ const draw = async () => {
1907
+ try {
1908
+ const mermaid = await load();
1909
+ mermaid.initialize({
1910
+ startOnLoad: false,
1911
+ securityLevel: "strict",
1912
+ theme: prefersDark(ref.current) ? "dark" : "default"
1913
+ });
1914
+ const id = `es-mermaid-${Math.random().toString(36).slice(2)}`;
1915
+ const { svg: rendered } = await mermaid.render(id, entry.code);
1916
+ if (!cancelled) setSvg(rendered);
1917
+ } catch {
1918
+ if (!cancelled) setSvg(null);
1919
+ }
1920
+ };
1921
+ void draw();
1922
+ const themed = ref.current?.closest("[data-theme]");
1923
+ const observer = themed ? new MutationObserver(() => void draw()) : void 0;
1924
+ observer?.observe(themed, { attributes: true, attributeFilter: ["data-theme"] });
1925
+ return () => {
1926
+ cancelled = true;
1927
+ observer?.disconnect();
1928
+ };
1929
+ }, [entry.code, load]);
1930
+ if (svg) {
1931
+ return /* @__PURE__ */ jsxs25("figure", { ref, className: "my-3", children: [
1932
+ entry.title ? /* @__PURE__ */ jsx37("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1933
+ /* @__PURE__ */ jsx37(
1934
+ "div",
1935
+ {
1936
+ role: "img",
1937
+ "aria-label": entry.title ?? "Mermaid diagram",
1938
+ className: "overflow-x-auto rounded-md border border-border bg-card p-3 [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full",
1939
+ dangerouslySetInnerHTML: { __html: svg }
1940
+ }
1941
+ )
1942
+ ] });
1943
+ }
1944
+ return /* @__PURE__ */ jsx37(MermaidSource, { entry, ref });
1945
+ }
1946
+
1947
+ // src/interactive/island-renderers.tsx
1948
+ import { jsx as jsx38 } from "react/jsx-runtime";
1949
+ var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
1950
+ var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
1951
+ function cachedCdnModule(url) {
1952
+ let loader;
1953
+ return () => {
1954
+ loader ??= import(
1955
+ /* @vite-ignore */
1956
+ /* webpackIgnore: true */
1957
+ url
1958
+ ).then((m) => m.default ?? m);
1959
+ return loader;
1960
+ };
1961
+ }
1962
+ var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
1963
+ var loadHljs = cachedCdnModule(HLJS_ESM);
1964
+ function CdnHighlightedCode({ entry }) {
1965
+ const [html, setHtml] = useState6(null);
1966
+ useEffect7(() => {
1967
+ let cancelled = false;
1968
+ void (async () => {
1969
+ try {
1970
+ const hljs = await loadHljs();
1971
+ const lang = entry.lang && hljs.getLanguage(entry.lang) ? entry.lang : void 0;
1972
+ const result = lang ? hljs.highlight(entry.content, { language: lang, ignoreIllegals: true }) : hljs.highlightAuto(entry.content);
1973
+ if (!cancelled) setHtml(result.value);
1974
+ } catch {
1975
+ if (!cancelled) setHtml(null);
1976
+ }
1977
+ })();
1978
+ return () => {
1979
+ cancelled = true;
1980
+ };
1981
+ }, [entry.content, entry.lang]);
1982
+ return /* @__PURE__ */ jsx38(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx38(
1983
+ "code",
1984
+ {
1985
+ className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
1986
+ dangerouslySetInnerHTML: { __html: html }
1987
+ }
1988
+ ) : /* @__PURE__ */ jsx38("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
1989
+ }
1990
+ function buildIslandRenderers(opts) {
1991
+ const renderers = {};
1992
+ if (opts.mermaid) {
1993
+ renderers.mermaid = (entry) => createElement(MermaidView, { entry, load: loadMermaidFromCdn });
1994
+ }
1995
+ if (opts.syntaxHighlighting) {
1996
+ renderers.code = (entry) => createElement(CdnHighlightedCode, { entry });
1997
+ }
1998
+ return renderers;
1999
+ }
2000
+
2001
+ // src/interactive/ReportInteractiveIsland.tsx
2002
+ import { jsx as jsx39 } from "react/jsx-runtime";
2003
+ function ReportInteractiveIsland({
2004
+ report,
2005
+ title,
2006
+ className,
2007
+ hideHeader,
2008
+ syntaxHighlighting = true,
2009
+ mermaid = true
2010
+ }) {
2011
+ const renderers = useMemo4(
2012
+ () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
2013
+ [syntaxHighlighting, mermaid]
2014
+ );
2015
+ return /* @__PURE__ */ jsx39(
2016
+ ReportInteractive,
2017
+ {
2018
+ report,
2019
+ title,
2020
+ className,
2021
+ hideHeader,
2022
+ renderers
2023
+ }
807
2024
  );
808
2025
  }
809
2026
  export {
810
2027
  ReportFailureBanner,
811
2028
  ReportInteractive,
2029
+ ReportInteractiveIsland,
812
2030
  ReportSearch,
813
2031
  ReportShortcutsHelp,
2032
+ buildIslandRenderers,
814
2033
  filterReport,
815
2034
  listFailures,
816
2035
  normalizeQuery,