executable-stories-react 0.1.16 → 0.2.1

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