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