executable-stories-react 0.1.15 → 0.2.0

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