executable-stories-react 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,38 +47,135 @@ __export(interactive_exports, {
47
47
  module.exports = __toCommonJS(interactive_exports);
48
48
 
49
49
  // src/interactive/ReportInteractive.tsx
50
- var import_react19 = require("react");
50
+ var import_lucide_react5 = require("lucide-react");
51
+ var import_react21 = require("react");
51
52
 
52
- // src/result.ts
53
- var ok = (data) => ({ ok: true, data });
54
- function unwrapReport(value) {
55
- if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
56
- return value;
53
+ // src/components/ui/empty.tsx
54
+ var import_class_variance_authority = require("class-variance-authority");
55
+
56
+ // src/lib/utils.ts
57
+ var import_clsx = require("clsx");
58
+ var import_tailwind_merge = require("tailwind-merge");
59
+ function cn(...inputs) {
60
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
61
+ }
62
+
63
+ // src/components/ui/empty.tsx
64
+ var import_jsx_runtime = require("react/jsx-runtime");
65
+ function Empty({ className, ...props }) {
66
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
67
+ "div",
68
+ {
69
+ "data-slot": "empty",
70
+ className: cn(
71
+ "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",
72
+ className
73
+ ),
74
+ ...props
75
+ }
76
+ );
77
+ }
78
+ function EmptyHeader({ className, ...props }) {
79
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
80
+ "div",
81
+ {
82
+ "data-slot": "empty-header",
83
+ className: cn(
84
+ "flex max-w-sm flex-col items-center gap-2 text-center",
85
+ className
86
+ ),
87
+ ...props
88
+ }
89
+ );
90
+ }
91
+ var emptyMediaVariants = (0, import_class_variance_authority.cva)(
92
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
93
+ {
94
+ variants: {
95
+ variant: {
96
+ default: "bg-transparent",
97
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
98
+ }
99
+ },
100
+ defaultVariants: {
101
+ variant: "default"
102
+ }
57
103
  }
58
- return ok(value);
104
+ );
105
+ function EmptyTitle({ className, ...props }) {
106
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
107
+ "div",
108
+ {
109
+ "data-slot": "empty-title",
110
+ className: cn("text-lg font-medium tracking-tight", className),
111
+ ...props
112
+ }
113
+ );
59
114
  }
60
115
 
61
- // src/context/ReportRoot.tsx
116
+ // src/components/ReportEmpty.tsx
117
+ var import_jsx_runtime2 = require("react/jsx-runtime");
118
+ function ReportEmpty({ message }) {
119
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
120
+ }
121
+
122
+ // src/hooks/useReport.ts
62
123
  var import_react2 = require("react");
63
124
 
64
125
  // src/context/ReportContext.ts
65
126
  var import_react = require("react");
66
127
  var ReportContext = (0, import_react.createContext)(null);
67
128
 
68
- // src/components/ui/badge.tsx
69
- var React = require("react");
70
- var import_class_variance_authority = require("class-variance-authority");
71
- var import_use_render = require("@base-ui/react/use-render");
129
+ // src/hooks/useReport.ts
130
+ function useReport() {
131
+ const ctx = (0, import_react2.useContext)(ReportContext);
132
+ if (!ctx) {
133
+ throw new Error(
134
+ "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
135
+ );
136
+ }
137
+ return ctx.report;
138
+ }
72
139
 
73
- // src/lib/utils.ts
74
- var import_clsx = require("clsx");
75
- var import_tailwind_merge = require("tailwind-merge");
76
- function cn(...inputs) {
77
- return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
140
+ // src/components/ReportScenario.tsx
141
+ var import_duration3 = require("executable-stories-core/utils/duration");
142
+
143
+ // src/lib/step-params.tsx
144
+ var import_jsx_runtime3 = require("react/jsx-runtime");
145
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
146
+ function highlightStepParams(text) {
147
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
148
+ if (matches.length === 0) return text;
149
+ const nodes = [];
150
+ let lastIndex = 0;
151
+ matches.forEach((match, i) => {
152
+ const start = match.index;
153
+ const end = start + match[0].length;
154
+ if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
155
+ nodes.push(
156
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
157
+ );
158
+ lastIndex = end;
159
+ });
160
+ if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
161
+ return nodes;
162
+ }
163
+
164
+ // src/components/ReportSteps.tsx
165
+ var import_assertive_steps = require("executable-stories-core/utils/assertive-steps");
166
+ var import_duration = require("executable-stories-core/utils/duration");
167
+
168
+ // src/components/doc/DocNote.tsx
169
+ var import_jsx_runtime4 = require("react/jsx-runtime");
170
+ function DocNote({ entry }) {
171
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
78
172
  }
79
173
 
80
174
  // src/components/ui/badge.tsx
81
- var badgeVariants = (0, import_class_variance_authority.cva)(
175
+ var React = require("react");
176
+ var import_class_variance_authority2 = require("class-variance-authority");
177
+ var import_use_render = require("@base-ui/react/use-render");
178
+ var badgeVariants = (0, import_class_variance_authority2.cva)(
82
179
  "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",
83
180
  {
84
181
  variants: {
@@ -123,342 +220,119 @@ function Badge({
123
220
  });
124
221
  }
125
222
 
126
- // src/components/doc/NarrativeBlocks.tsx
127
- var import_jsx_runtime = require("react/jsx-runtime");
128
- var FILE_TREE_TYPE = "file-tree";
129
- var DATA_MODEL_TYPE = "data-model";
130
- function isChangeKind(value) {
131
- return value === "added" || value === "modified" || value === "removed" || value === "renamed";
223
+ // src/components/doc/DocTag.tsx
224
+ var import_jsx_runtime5 = require("react/jsx-runtime");
225
+ function DocTag({ entry }) {
226
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Badge, { variant: "tag", children: n }) }, n)) });
132
227
  }
133
- function isRecord(value) {
134
- return typeof value === "object" && value !== null && !Array.isArray(value);
228
+
229
+ // src/components/doc/DocKv.tsx
230
+ var import_jsx_runtime6 = require("react/jsx-runtime");
231
+ function formatValue(value) {
232
+ if (value === null) return "null";
233
+ if (typeof value === "string") return value;
234
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
235
+ try {
236
+ return JSON.stringify(value);
237
+ } catch {
238
+ return String(value);
239
+ }
135
240
  }
136
- function str(value) {
137
- return typeof value === "string" && value !== "" ? value : void 0;
241
+ function DocKv({ entry }) {
242
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
243
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
244
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
245
+ ] });
138
246
  }
139
- function AuthoredMarker({ authored }) {
140
- if (authored !== "agent") return null;
141
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
247
+
248
+ // src/hooks/useRenderers.ts
249
+ var import_react3 = require("react");
250
+ var EMPTY_CUSTOM = {};
251
+ var EMPTY_RENDERERS = {};
252
+ function useCustomRenderers() {
253
+ const ctx = (0, import_react3.useContext)(ReportContext);
254
+ return ctx?.customRenderers ?? EMPTY_CUSTOM;
142
255
  }
143
- function BlockFrame({
144
- title,
145
- authored,
146
- children
147
- }) {
148
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figure", { className: "my-3", children: [
149
- title || authored === "agent" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
150
- title ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
151
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthoredMarker, { authored })
152
- ] }) : null,
153
- children
256
+ function useBuiltinRenderers() {
257
+ const ctx = (0, import_react3.useContext)(ReportContext);
258
+ return ctx?.renderers ?? EMPTY_RENDERERS;
259
+ }
260
+
261
+ // src/components/doc/DocCode.tsx
262
+ var import_jsx_runtime7 = require("react/jsx-runtime");
263
+ function CodeFigure({ label, children }) {
264
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("figure", { className: "my-3", children: [
265
+ label ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
266
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
154
267
  ] });
155
268
  }
156
- function ChangeBadge({ change }) {
157
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { variant: "tag", className: "text-[10px]", children: change });
269
+ function DocCode({ entry }) {
270
+ const renderers = useBuiltinRenderers();
271
+ if (renderers.code) {
272
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: renderers.code(entry) });
273
+ }
274
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CodeFigure, { label: entry.label, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
158
275
  }
159
- function parseFileTree(data) {
160
- if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
161
- const files = [];
162
- for (const raw of data.files) {
163
- if (typeof raw === "string") {
164
- if (raw) files.push({ path: raw });
165
- continue;
276
+
277
+ // src/components/doc/DocTable.tsx
278
+ var import_jsx_runtime8 = require("react/jsx-runtime");
279
+ function DocTable({ entry }) {
280
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("figure", { className: "my-3", children: [
281
+ entry.label ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
282
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("table", { className: "w-full border-collapse text-sm", children: [
283
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
284
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tr", { children: row.map((cell, j) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
285
+ ] }) })
286
+ ] });
287
+ }
288
+
289
+ // src/components/doc/DocLink.tsx
290
+ var import_jsx_runtime9 = require("react/jsx-runtime");
291
+ function DocLink({ entry }) {
292
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
293
+ "a",
294
+ {
295
+ className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
296
+ href: entry.url,
297
+ rel: "noreferrer noopener",
298
+ target: "_blank",
299
+ children: entry.label
166
300
  }
167
- if (!isRecord(raw)) continue;
168
- const path = str(raw.path);
169
- if (!path) continue;
170
- files.push({
171
- path,
172
- change: isChangeKind(raw.change) ? raw.change : void 0,
173
- note: str(raw.note)
174
- });
175
- }
176
- if (files.length === 0) return void 0;
177
- return { title: str(data.title), authored: str(data.authored), files };
301
+ );
178
302
  }
179
- function buildTree(files) {
180
- const root = { name: "", children: /* @__PURE__ */ new Map() };
181
- for (const file of files) {
182
- const segments = file.path.split("/").filter(Boolean);
183
- let node = root;
184
- segments.forEach((segment, index) => {
185
- let child = node.children.get(segment);
186
- if (!child) {
187
- child = { name: segment, children: /* @__PURE__ */ new Map() };
188
- node.children.set(segment, child);
189
- }
190
- if (index === segments.length - 1) child.file = file;
191
- node = child;
192
- });
193
- }
194
- return root;
303
+
304
+ // src/lib/markdown.ts
305
+ var import_marked = require("marked");
306
+
307
+ // src/lib/url.ts
308
+ var import_url = require("executable-stories-core/utils/url");
309
+
310
+ // src/lib/markdown.ts
311
+ function safeMarkdownHtml(markdown) {
312
+ const raw = import_marked.marked.parse(markdown, { async: false });
313
+ return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
195
314
  }
196
- function sortedChildren(node) {
197
- return [...node.children.values()].sort((a, b) => {
198
- const aDir = a.children.size > 0;
199
- const bDir = b.children.size > 0;
200
- if (aDir !== bDir) return aDir ? -1 : 1;
201
- return a.name.localeCompare(b.name);
202
- });
203
- }
204
- function TreeBranch({ node, depth }) {
205
- const children = sortedChildren(node);
206
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
207
- const isDir = child.children.size > 0;
208
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
209
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-baseline gap-2", children: [
210
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
211
- "span",
212
- {
213
- className: cn(
214
- "font-mono text-xs",
215
- isDir ? "text-muted-foreground" : "text-foreground"
216
- ),
217
- children: isDir ? `${child.name}/` : child.name
218
- }
219
- ),
220
- child.file?.change ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChangeBadge, { change: child.file.change }) : null,
221
- child.file?.note ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
222
- ] }),
223
- isDir ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeBranch, { node: child, depth: depth + 1 }) : null
224
- ] }, child.name);
225
- }) });
226
- }
227
- function FileTreeBlock({ entry }) {
228
- const data = parseFileTree(entry.data);
229
- if (!data) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UnreadableBlock, { entry });
230
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
231
- }
232
- function parseDataModel(data) {
233
- if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
234
- const fields = [];
235
- for (const raw of data.fields) {
236
- if (!isRecord(raw)) continue;
237
- const name = str(raw.name);
238
- if (!name) continue;
239
- fields.push({
240
- name,
241
- type: str(raw.type),
242
- note: str(raw.note),
243
- change: isChangeKind(raw.change) ? raw.change : void 0
244
- });
245
- }
246
- if (fields.length === 0) return void 0;
247
- return {
248
- title: str(data.title),
249
- authored: str(data.authored),
250
- name: str(data.name),
251
- fields
252
- };
253
- }
254
- function DataModelBlock({ entry }) {
255
- const data = parseDataModel(entry.data);
256
- if (!data) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UnreadableBlock, { entry });
257
- const hasNotes = data.fields.some((f) => f.note);
258
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { className: "w-full border-collapse text-sm", children: [
259
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { className: "bg-muted/50", children: [
260
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
261
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
262
- hasNotes ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
263
- ] }) }),
264
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: data.fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
265
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex flex-wrap items-center gap-2", children: [
266
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-mono text-xs text-foreground", children: field.name }),
267
- field.change ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChangeBadge, { change: field.change }) : null
268
- ] }) }),
269
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
270
- hasNotes ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
271
- ] }, field.name)) })
272
- ] }) }) });
273
- }
274
- function UnreadableBlock({ entry }) {
275
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "my-2", "data-type": entry.type, children: [
276
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
277
- entry.type,
278
- " (unrecognised shape)"
279
- ] }),
280
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
281
- "pre",
282
- {
283
- tabIndex: 0,
284
- className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
285
- children: safeStringify(entry.data)
286
- }
287
- )
288
- ] });
289
- }
290
- function safeStringify(value) {
291
- try {
292
- return JSON.stringify(value, null, 2);
293
- } catch {
294
- return String(value);
295
- }
296
- }
297
- var narrativeBlockRenderers = {
298
- [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FileTreeBlock, { entry }),
299
- [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataModelBlock, { entry })
300
- };
301
-
302
- // src/context/ReportRoot.tsx
303
- var import_jsx_runtime2 = require("react/jsx-runtime");
304
- var EMPTY_RENDERERS = {};
305
- function ReportRoot({
306
- report,
307
- customRenderers,
308
- renderers,
309
- children
310
- }) {
311
- const value = (0, import_react2.useMemo)(
312
- () => ({
313
- report,
314
- // Narrative blocks render everywhere the report does (static SSR, the
315
- // island, Astro) without every host wiring them up. A host renderer for
316
- // the same type still wins.
317
- customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
318
- renderers: renderers ?? EMPTY_RENDERERS
319
- }),
320
- [report, customRenderers, renderers]
321
- );
322
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ReportContext.Provider, { value, children });
323
- }
324
-
325
- // src/hooks/useReport.ts
326
- var import_react3 = require("react");
327
- function useReport() {
328
- const ctx = (0, import_react3.useContext)(ReportContext);
329
- if (!ctx) {
330
- throw new Error(
331
- "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
332
- );
333
- }
334
- return ctx.report;
335
- }
336
-
337
- // src/components/ReportScenario.tsx
338
- var import_duration3 = require("executable-stories-core/utils/duration");
339
-
340
- // src/components/ReportSteps.tsx
341
- var import_duration = require("executable-stories-core/utils/duration");
342
-
343
- // src/components/doc/DocNote.tsx
344
- var import_jsx_runtime3 = require("react/jsx-runtime");
345
- function DocNote({ entry }) {
346
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
347
- }
348
-
349
- // src/components/doc/DocTag.tsx
350
- var import_jsx_runtime4 = require("react/jsx-runtime");
351
- function DocTag({ entry }) {
352
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Badge, { variant: "tag", children: n }) }, n)) });
353
- }
354
-
355
- // src/components/doc/DocKv.tsx
356
- var import_jsx_runtime5 = require("react/jsx-runtime");
357
- function formatValue(value) {
358
- if (value === null) return "null";
359
- if (typeof value === "string") return value;
360
- if (typeof value === "number" || typeof value === "boolean") return String(value);
361
- try {
362
- return JSON.stringify(value);
363
- } catch {
364
- return String(value);
365
- }
366
- }
367
- function DocKv({ entry }) {
368
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
369
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
370
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
371
- ] });
372
- }
373
-
374
- // src/hooks/useRenderers.ts
375
- var import_react4 = require("react");
376
- var EMPTY_CUSTOM = {};
377
- var EMPTY_RENDERERS2 = {};
378
- function useCustomRenderers() {
379
- const ctx = (0, import_react4.useContext)(ReportContext);
380
- return ctx?.customRenderers ?? EMPTY_CUSTOM;
381
- }
382
- function useBuiltinRenderers() {
383
- const ctx = (0, import_react4.useContext)(ReportContext);
384
- return ctx?.renderers ?? EMPTY_RENDERERS2;
385
- }
386
-
387
- // src/components/doc/DocCode.tsx
388
- var import_jsx_runtime6 = require("react/jsx-runtime");
389
- function CodeFigure({ label, children }) {
390
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("figure", { className: "my-3", children: [
391
- label ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
392
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
393
- ] });
394
- }
395
- function DocCode({ entry }) {
396
- const renderers = useBuiltinRenderers();
397
- if (renderers.code) {
398
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: renderers.code(entry) });
399
- }
400
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CodeFigure, { label: entry.label, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
401
- }
402
-
403
- // src/components/doc/DocTable.tsx
404
- var import_jsx_runtime7 = require("react/jsx-runtime");
405
- function DocTable({ entry }) {
406
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("figure", { className: "my-3", children: [
407
- entry.label ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
408
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("table", { className: "w-full border-collapse text-sm", children: [
409
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
410
- /* @__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", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
411
- ] }) })
412
- ] });
413
- }
414
-
415
- // src/components/doc/DocLink.tsx
416
- var import_jsx_runtime8 = require("react/jsx-runtime");
417
- function DocLink({ entry }) {
418
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
419
- "a",
420
- {
421
- className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
422
- href: entry.url,
423
- rel: "noreferrer noopener",
424
- target: "_blank",
425
- children: entry.label
426
- }
427
- );
428
- }
429
-
430
- // src/lib/markdown.ts
431
- var import_marked = require("marked");
432
-
433
- // src/lib/url.ts
434
- var import_url = require("executable-stories-core/utils/url");
435
-
436
- // src/lib/markdown.ts
437
- function safeMarkdownHtml(markdown) {
438
- const raw = import_marked.marked.parse(markdown, { async: false });
439
- 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)}'`);
440
- }
441
- function dedent(text) {
442
- const lines = text.replace(/\t/g, " ").split("\n");
443
- const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
444
- const shortest = indents.length > 0 ? Math.min(...indents) : 0;
445
- return lines.map((line) => line.slice(shortest)).join("\n").trim();
315
+ function dedent(text) {
316
+ const lines = text.replace(/\t/g, " ").split("\n");
317
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
318
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
319
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
446
320
  }
447
321
  function neutralizeUrl(value) {
448
322
  return (0, import_url.safeUrl)(value) ?? "#";
449
323
  }
450
324
 
451
325
  // src/components/doc/DocSection.tsx
452
- var import_jsx_runtime9 = require("react/jsx-runtime");
326
+ var import_jsx_runtime10 = require("react/jsx-runtime");
453
327
  function DocSection({ entry }) {
454
328
  const renderers = useBuiltinRenderers();
455
329
  if (renderers.section) {
456
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_jsx_runtime9.Fragment, { children: renderers.section(entry) });
330
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: renderers.section(entry) });
457
331
  }
458
332
  const html = safeMarkdownHtml(entry.markdown);
459
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
460
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
461
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
333
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
334
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
335
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
462
336
  "div",
463
337
  {
464
338
  className: "es-doc-prose prose prose-sm max-w-none",
@@ -469,11 +343,11 @@ function DocSection({ entry }) {
469
343
  }
470
344
 
471
345
  // src/components/doc/DocMermaid.tsx
472
- var import_jsx_runtime10 = require("react/jsx-runtime");
346
+ var import_jsx_runtime11 = require("react/jsx-runtime");
473
347
  function MermaidSource({ entry, ref }) {
474
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
475
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
476
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
348
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
349
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
350
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
477
351
  "pre",
478
352
  {
479
353
  "data-mermaid": true,
@@ -487,24 +361,24 @@ function MermaidSource({ entry, ref }) {
487
361
  function DocMermaid({ entry }) {
488
362
  const renderers = useBuiltinRenderers();
489
363
  if (renderers.mermaid) {
490
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_jsx_runtime10.Fragment, { children: renderers.mermaid(entry) });
364
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_jsx_runtime11.Fragment, { children: renderers.mermaid(entry) });
491
365
  }
492
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MermaidSource, { entry });
366
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MermaidSource, { entry });
493
367
  }
494
368
 
495
369
  // src/components/doc/DocScreenshot.tsx
496
- var import_jsx_runtime11 = require("react/jsx-runtime");
370
+ var import_jsx_runtime12 = require("react/jsx-runtime");
497
371
  function DocScreenshot({ entry }) {
498
372
  const src = (0, import_url.isLocalFsPath)(entry.path) ? void 0 : (0, import_url.safeImageUrl)(entry.path);
499
373
  if (!src) {
500
- 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: [
501
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "font-medium", children: "Screenshot unavailable" }),
502
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
503
- entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
374
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
375
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "font-medium", children: "Screenshot unavailable" }),
376
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
377
+ entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
504
378
  ] });
505
379
  }
506
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("figure", { className: "my-3", children: [
507
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
380
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("figure", { className: "my-3", children: [
381
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
508
382
  "img",
509
383
  {
510
384
  src,
@@ -513,24 +387,24 @@ function DocScreenshot({ entry }) {
513
387
  className: "max-w-full rounded-md border border-border"
514
388
  }
515
389
  ),
516
- entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
390
+ entry.alt ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
517
391
  ] });
518
392
  }
519
393
 
520
394
  // src/components/doc/DocVideo.tsx
521
- var import_jsx_runtime12 = require("react/jsx-runtime");
395
+ var import_jsx_runtime13 = require("react/jsx-runtime");
522
396
  function DocVideo({ entry }) {
523
397
  if ((0, import_url.isLocalFsPath)(entry.path)) {
524
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
525
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "font-medium", children: "Video unavailable" }),
526
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
527
- entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
398
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
399
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "font-medium", children: "Video unavailable" }),
400
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
401
+ entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
528
402
  ] });
529
403
  }
530
404
  const src = (0, import_url.safeUrl)(entry.path);
531
405
  const poster = (0, import_url.safeUrl)(entry.poster);
532
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("figure", { className: "my-3", children: [
533
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
406
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("figure", { className: "my-3", children: [
407
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
534
408
  "video",
535
409
  {
536
410
  controls: true,
@@ -540,21 +414,21 @@ function DocVideo({ entry }) {
540
414
  className: "max-w-full rounded-md border border-border"
541
415
  }
542
416
  ),
543
- entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
417
+ entry.caption ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
544
418
  ] });
545
419
  }
546
420
 
547
421
  // src/components/doc/DocHtml.tsx
548
- var import_jsx_runtime13 = require("react/jsx-runtime");
422
+ var import_jsx_runtime14 = require("react/jsx-runtime");
549
423
  function DocHtml({ entry }) {
550
424
  const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
551
425
  const title = entry.title ?? "Embedded HTML";
552
426
  const safeHref = (0, import_url.safeUrl)(entry.url ?? entry.path);
553
427
  const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
554
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
555
- /* @__PURE__ */ (0, import_jsx_runtime13.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: [
556
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: entry.title ?? "HTML" }),
557
- safeHref ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
428
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime14.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: [
430
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { children: entry.title ?? "HTML" }),
431
+ safeHref ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
558
432
  "a",
559
433
  {
560
434
  href: safeHref,
@@ -567,7 +441,7 @@ function DocHtml({ entry }) {
567
441
  }
568
442
  ) : null
569
443
  ] }),
570
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
444
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
571
445
  "iframe",
572
446
  {
573
447
  ...frameProps,
@@ -582,7 +456,7 @@ function DocHtml({ entry }) {
582
456
  }
583
457
 
584
458
  // src/components/doc/DocState.tsx
585
- var import_jsx_runtime14 = require("react/jsx-runtime");
459
+ var import_jsx_runtime15 = require("react/jsx-runtime");
586
460
  function DocState({ entry }) {
587
461
  let json;
588
462
  try {
@@ -590,23 +464,23 @@ function DocState({ entry }) {
590
464
  } catch {
591
465
  json = String(entry.value);
592
466
  }
593
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("code", { className: "font-mono text-xs language-json", children: json }) });
467
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("code", { className: "font-mono text-xs language-json", children: json }) });
594
468
  }
595
469
 
596
470
  // src/components/doc/DocCustom.tsx
597
- var import_jsx_runtime15 = require("react/jsx-runtime");
471
+ var import_jsx_runtime16 = require("react/jsx-runtime");
598
472
  function DocCustom({ entry }) {
599
473
  const renderers = useCustomRenderers();
600
474
  const renderer = renderers[entry.type];
601
475
  if (renderer) {
602
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, { children: renderer(entry) });
476
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_jsx_runtime16.Fragment, { children: renderer(entry) });
603
477
  }
604
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "my-2", "data-type": entry.type, children: [
605
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
606
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify2(entry.data) })
478
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "my-2", "data-type": entry.type, children: [
479
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
607
481
  ] });
608
482
  }
609
- function safeStringify2(value) {
483
+ function safeStringify(value) {
610
484
  try {
611
485
  return JSON.stringify(value, null, 2);
612
486
  } catch {
@@ -615,64 +489,43 @@ function safeStringify2(value) {
615
489
  }
616
490
 
617
491
  // src/components/doc/DocEntry.tsx
618
- var import_jsx_runtime16 = require("react/jsx-runtime");
492
+ var import_jsx_runtime17 = require("react/jsx-runtime");
619
493
  function DocEntry({ entry }) {
620
494
  switch (entry.kind) {
621
495
  case "note":
622
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocNote, { entry });
496
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocNote, { entry });
623
497
  case "tag":
624
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocTag, { entry });
498
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocTag, { entry });
625
499
  case "kv":
626
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocKv, { entry });
500
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocKv, { entry });
627
501
  case "code":
628
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocCode, { entry });
502
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocCode, { entry });
629
503
  case "table":
630
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocTable, { entry });
504
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocTable, { entry });
631
505
  case "link":
632
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocLink, { entry });
506
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocLink, { entry });
633
507
  case "section":
634
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocSection, { entry });
508
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocSection, { entry });
635
509
  case "mermaid":
636
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocMermaid, { entry });
510
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocMermaid, { entry });
637
511
  case "screenshot":
638
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocScreenshot, { entry });
512
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocScreenshot, { entry });
639
513
  case "video":
640
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocVideo, { entry });
514
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocVideo, { entry });
641
515
  case "html":
642
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocHtml, { entry });
516
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocHtml, { entry });
643
517
  case "state":
644
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocState, { entry });
518
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocState, { entry });
645
519
  case "custom":
646
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DocCustom, { entry });
520
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocCustom, { entry });
647
521
  }
648
522
  }
649
523
 
650
524
  // src/components/ReportDocEntries.tsx
651
- var import_jsx_runtime17 = require("react/jsx-runtime");
525
+ var import_jsx_runtime18 = require("react/jsx-runtime");
652
526
  function ReportDocEntries({ entries }) {
653
527
  if (entries.length === 0) return null;
654
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_jsx_runtime17.Fragment, { children: entries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DocEntry, { entry }, i)) });
655
- }
656
-
657
- // src/lib/step-params.tsx
658
- var import_jsx_runtime18 = require("react/jsx-runtime");
659
- var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
660
- function highlightStepParams(text) {
661
- const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
662
- if (matches.length === 0) return text;
663
- const nodes = [];
664
- let lastIndex = 0;
665
- matches.forEach((match, i) => {
666
- const start = match.index;
667
- const end = start + match[0].length;
668
- if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
669
- nodes.push(
670
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
671
- );
672
- lastIndex = end;
673
- });
674
- if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
675
- return nodes;
528
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_jsx_runtime18.Fragment, { children: entries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DocEntry, { entry }, i)) });
676
529
  }
677
530
 
678
531
  // src/components/ReportSteps.tsx
@@ -691,9 +544,22 @@ var STEP_GLYPH_COLOR = {
691
544
  };
692
545
  function ReportSteps({ scenario }) {
693
546
  if (scenario.steps.length === 0) return null;
694
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ReportStepItem, { step }, step.id)) });
547
+ const unasserted = new Set(
548
+ (0, import_assertive_steps.assertionState)(scenario.steps) === "unasserted" ? (0, import_assertive_steps.assertiveSteps)(scenario.steps).map((step) => step.id) : []
549
+ );
550
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
551
+ ReportStepItem,
552
+ {
553
+ step,
554
+ unasserted: unasserted.has(step.id)
555
+ },
556
+ step.id
557
+ )) });
695
558
  }
696
- function ReportStepItem({ step }) {
559
+ function ReportStepItem({
560
+ step,
561
+ unasserted = false
562
+ }) {
697
563
  const kw = step.keyword.toLowerCase();
698
564
  const continuation = kw === "and" || kw === "but";
699
565
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
@@ -701,21 +567,35 @@ function ReportStepItem({ step }) {
701
567
  {
702
568
  id: step.id,
703
569
  "data-status": step.status,
704
- className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
570
+ className: cn(
571
+ "py-1.5 text-[0.8125rem] leading-normal",
572
+ continuation && "pl-5"
573
+ ),
705
574
  children: [
706
575
  /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-baseline gap-2", children: [
707
- /* @__PURE__ */ (0, import_jsx_runtime19.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" }),
708
576
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
709
577
  "span",
710
578
  {
579
+ "aria-hidden": true,
711
580
  className: cn(
712
- "min-w-[52px] shrink-0 font-mono text-xs font-semibold capitalize",
713
- continuation ? "font-medium text-muted-foreground" : "text-keyword"
581
+ "w-4 shrink-0 text-center text-xs",
582
+ STEP_GLYPH_COLOR[step.status]
583
+ ),
584
+ children: STEP_GLYPH[step.status] ?? "\u25CB"
585
+ }
586
+ ),
587
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
588
+ "span",
589
+ {
590
+ className: cn(
591
+ "min-w-[52px] shrink-0 font-mono text-xs font-semibold capitalize",
592
+ continuation ? "font-medium text-muted-foreground" : "text-keyword"
714
593
  ),
715
594
  children: step.keyword
716
595
  }
717
596
  ),
718
597
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
598
+ unasserted ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "shrink-0 rounded-full border border-pend-border bg-pend-bg px-2 py-0.5 text-[0.6875rem] font-medium text-pend", children: "No assertion" }) : null,
719
599
  step.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration.formatDuration)(step.durationMs) }) : null
720
600
  ] }),
721
601
  step.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -1017,11 +897,11 @@ function ReportTrace({ spans }) {
1017
897
  }
1018
898
 
1019
899
  // src/interactive/scenario-history-context.ts
1020
- var import_react5 = require("react");
1021
- var ScenarioHistoryContext = (0, import_react5.createContext)(null);
900
+ var import_react4 = require("react");
901
+ var ScenarioHistoryContext = (0, import_react4.createContext)(null);
1022
902
  var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
1023
903
  function useScenarioHistory() {
1024
- return (0, import_react5.useContext)(ScenarioHistoryContext);
904
+ return (0, import_react4.useContext)(ScenarioHistoryContext);
1025
905
  }
1026
906
 
1027
907
  // src/lib/run-history.ts
@@ -1129,12 +1009,91 @@ function ScenarioRunHistory({ scenarioId }) {
1129
1009
  ] });
1130
1010
  }
1131
1011
 
1132
- // src/interactive/collapse-context.ts
1012
+ // src/components/ScenarioStaleness.tsx
1133
1013
  var import_react6 = require("react");
1134
- var CollapseContext = (0, import_react6.createContext)(null);
1014
+
1015
+ // src/lib/provenance.ts
1016
+ function reportLastRunMs(report) {
1017
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1018
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1019
+ return void 0;
1020
+ }
1021
+ function formatRelativeAge(thenMs, nowMs) {
1022
+ const deltaMs = Math.max(0, nowMs - thenMs);
1023
+ const minutes = Math.floor(deltaMs / 6e4);
1024
+ if (minutes < 1) return "just now";
1025
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1026
+ const hours = Math.floor(minutes / 60);
1027
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1028
+ const days = Math.floor(hours / 24);
1029
+ return `${days} day${days === 1 ? "" : "s"} ago`;
1030
+ }
1031
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1032
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1033
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
1034
+ }
1035
+ var CI_DISPLAY_NAMES = {
1036
+ github: "GitHub Actions",
1037
+ gitlab: "GitLab CI",
1038
+ circleci: "CircleCI",
1039
+ jenkins: "Jenkins",
1040
+ azure: "Azure DevOps",
1041
+ buildkite: "Buildkite",
1042
+ travis: "Travis CI",
1043
+ ci: "CI"
1044
+ };
1045
+ function ciDisplayName(name) {
1046
+ return CI_DISPLAY_NAMES[name] ?? name;
1047
+ }
1048
+ function commitUrl(ci, sha) {
1049
+ const commitSha = ci?.commitSha ?? sha;
1050
+ if (!ci?.url || !commitSha) return void 0;
1051
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1052
+ if (github) return `${github[1]}/commit/${commitSha}`;
1053
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1054
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1055
+ return void 0;
1056
+ }
1057
+ function prUrl(ci) {
1058
+ if (!ci?.url || !ci.prNumber) return void 0;
1059
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1060
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1061
+ return void 0;
1062
+ }
1063
+
1064
+ // src/interactive/scenario-freshness-context.tsx
1065
+ var import_react5 = require("react");
1066
+ var ScenarioFreshnessContext = (0, import_react5.createContext)(null);
1067
+ var ScenarioFreshnessProvider = ScenarioFreshnessContext.Provider;
1068
+ function useScenarioFreshness() {
1069
+ return (0, import_react5.useContext)(ScenarioFreshnessContext);
1070
+ }
1071
+
1072
+ // src/components/ScenarioStaleness.tsx
1073
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1074
+ function ScenarioStaleness({ scenario }) {
1075
+ const freshness = useScenarioFreshness();
1076
+ const fixedNow = freshness?.nowMs;
1077
+ const [now, setNow] = (0, import_react6.useState)(fixedNow);
1078
+ (0, import_react6.useEffect)(() => {
1079
+ if (fixedNow === void 0) setNow(Date.now());
1080
+ }, [fixedNow]);
1081
+ if (!freshness || now === void 0) return null;
1082
+ const lastRunAtMs = scenario.lastRunAtMs;
1083
+ if (!lastRunAtMs || lastRunAtMs <= 0) return null;
1084
+ if (!isReportStale(lastRunAtMs, freshness.staleAfterDays, now)) return null;
1085
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(Badge, { variant: "outline", "aria-label": "Scenario freshness", children: [
1086
+ "Last ran ",
1087
+ formatRelativeAge(lastRunAtMs, now)
1088
+ ] });
1089
+ }
1090
+
1091
+ // src/interactive/collapse-context.ts
1092
+ var import_react7 = require("react");
1093
+ var CollapseContext = (0, import_react7.createContext)(null);
1135
1094
  var CollapseProvider = CollapseContext.Provider;
1136
1095
  function useCollapse() {
1137
- return (0, import_react6.useContext)(CollapseContext);
1096
+ return (0, import_react7.useContext)(CollapseContext);
1138
1097
  }
1139
1098
  var STORAGE_KEY = "es-collapsed-ids";
1140
1099
  function readPersisted() {
@@ -1154,11 +1113,11 @@ function persist(ids) {
1154
1113
  }
1155
1114
  }
1156
1115
  function useCollapseState(computeDefault) {
1157
- const [collapsed, setCollapsed] = (0, import_react6.useState)(
1116
+ const [collapsed, setCollapsed] = (0, import_react7.useState)(
1158
1117
  () => readPersisted() ?? computeDefault?.() ?? /* @__PURE__ */ new Set()
1159
1118
  );
1160
- const isCollapsed = (0, import_react6.useCallback)((id) => collapsed.has(id), [collapsed]);
1161
- const toggle = (0, import_react6.useCallback)((id) => {
1119
+ const isCollapsed = (0, import_react7.useCallback)((id) => collapsed.has(id), [collapsed]);
1120
+ const toggle = (0, import_react7.useCallback)((id) => {
1162
1121
  setCollapsed((prev) => {
1163
1122
  const next = new Set(prev);
1164
1123
  if (next.has(id)) next.delete(id);
@@ -1167,14 +1126,14 @@ function useCollapseState(computeDefault) {
1167
1126
  return next;
1168
1127
  });
1169
1128
  }, []);
1170
- const collapseAll = (0, import_react6.useCallback)((ids) => {
1129
+ const collapseAll = (0, import_react7.useCallback)((ids) => {
1171
1130
  setCollapsed(() => {
1172
1131
  const next = new Set(ids);
1173
1132
  persist(next);
1174
1133
  return next;
1175
1134
  });
1176
1135
  }, []);
1177
- const expandAll = (0, import_react6.useCallback)(() => {
1136
+ const expandAll = (0, import_react7.useCallback)(() => {
1178
1137
  setCollapsed(() => {
1179
1138
  const next = /* @__PURE__ */ new Set();
1180
1139
  persist(next);
@@ -1185,12 +1144,12 @@ function useCollapseState(computeDefault) {
1185
1144
  }
1186
1145
 
1187
1146
  // src/interactive/scenario-actions.tsx
1188
- var import_react7 = require("react");
1147
+ var import_react8 = require("react");
1189
1148
  var import_scenario_markdown = require("executable-stories-core/utils/scenario-markdown");
1190
- var ScenarioActionsContext = (0, import_react7.createContext)(null);
1149
+ var ScenarioActionsContext = (0, import_react8.createContext)(null);
1191
1150
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
1192
1151
  function useScenarioActions() {
1193
- return (0, import_react7.useContext)(ScenarioActionsContext);
1152
+ return (0, import_react8.useContext)(ScenarioActionsContext);
1194
1153
  }
1195
1154
  function scenarioToMarkdown(scenario) {
1196
1155
  return (0, import_scenario_markdown.scenarioToMarkdown)(scenario, { variant: "compact" });
@@ -1221,9 +1180,9 @@ function scenarioPermalink(scenario) {
1221
1180
 
1222
1181
  // src/components/ui/card.tsx
1223
1182
  var React2 = require("react");
1224
- var import_jsx_runtime24 = require("react/jsx-runtime");
1183
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1225
1184
  function Card({ className, ...props }) {
1226
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1185
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1227
1186
  "div",
1228
1187
  {
1229
1188
  "data-slot": "card",
@@ -1236,7 +1195,7 @@ function Card({ className, ...props }) {
1236
1195
  );
1237
1196
  }
1238
1197
  function CardHeader({ className, ...props }) {
1239
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1198
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1240
1199
  "div",
1241
1200
  {
1242
1201
  "data-slot": "card-header",
@@ -1249,7 +1208,7 @@ function CardHeader({ className, ...props }) {
1249
1208
  );
1250
1209
  }
1251
1210
  function CardContent({ className, ...props }) {
1252
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1211
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1253
1212
  "div",
1254
1213
  {
1255
1214
  "data-slot": "card-content",
@@ -1263,13 +1222,13 @@ function CardContent({ className, ...props }) {
1263
1222
  var React3 = require("react");
1264
1223
  var import_menu = require("@base-ui/react/menu");
1265
1224
  var import_lucide_react = require("lucide-react");
1266
- var import_jsx_runtime25 = require("react/jsx-runtime");
1225
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1267
1226
  var itemClasses = "relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg:not([class*='text-'])]:text-muted-foreground";
1268
1227
  function DropdownMenu(props) {
1269
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
1228
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
1270
1229
  }
1271
1230
  function DropdownMenuTrigger(props) {
1272
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_menu.Menu.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
1231
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_menu.Menu.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
1273
1232
  }
1274
1233
  function DropdownMenuContent({
1275
1234
  className,
@@ -1277,14 +1236,14 @@ function DropdownMenuContent({
1277
1236
  sideOffset = 4,
1278
1237
  ...props
1279
1238
  }) {
1280
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_menu.Menu.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1239
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_menu.Menu.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1281
1240
  import_menu.Menu.Positioner,
1282
1241
  {
1283
1242
  "data-slot": "dropdown-menu-positioner",
1284
1243
  className: "z-50",
1285
1244
  align,
1286
1245
  sideOffset,
1287
- children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1246
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1288
1247
  import_menu.Menu.Popup,
1289
1248
  {
1290
1249
  "data-slot": "dropdown-menu-content",
@@ -1302,7 +1261,7 @@ function DropdownMenuItem({
1302
1261
  className,
1303
1262
  ...props
1304
1263
  }) {
1305
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1264
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1306
1265
  import_menu.Menu.Item,
1307
1266
  {
1308
1267
  "data-slot": "dropdown-menu-item",
@@ -1314,7 +1273,7 @@ function DropdownMenuItem({
1314
1273
 
1315
1274
  // src/components/ReportScenario.tsx
1316
1275
  var import_lucide_react2 = require("lucide-react");
1317
- var import_jsx_runtime26 = require("react/jsx-runtime");
1276
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1318
1277
  var STATUS_META = {
1319
1278
  passed: { label: "Passed", glyph: "\u2713", glyphColor: "text-pass", badgeVariant: "passed", showPill: false },
1320
1279
  failed: { label: "Failed", glyph: "\u2717", glyphColor: "text-fail", badgeVariant: "failed", showPill: true },
@@ -1330,7 +1289,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1330
1289
  const collapsible = collapse !== null && !hideTitle;
1331
1290
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
1332
1291
  const actions = useScenarioActions();
1333
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
1292
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
1334
1293
  Card,
1335
1294
  {
1336
1295
  id: scenario.id,
@@ -1338,10 +1297,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
1338
1297
  ...hideTitle ? { "aria-label": `${scenario.title}, ${label}` } : { "aria-labelledby": titleId },
1339
1298
  className: "gap-0 py-0",
1340
1299
  children: [
1341
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(CardHeader, { className: "px-4 py-3", children: [
1342
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1343
- hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1344
- collapsible ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1300
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(CardHeader, { className: "px-4 py-3", children: [
1301
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1302
+ hideTitle ? null : /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1303
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1345
1304
  "button",
1346
1305
  {
1347
1306
  type: "button",
@@ -1350,49 +1309,50 @@ function ReportScenario({ scenario, hideTitle = false }) {
1350
1309
  "aria-controls": bodyId,
1351
1310
  "aria-label": `Toggle ${scenario.title}`,
1352
1311
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
1353
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1312
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1354
1313
  }
1355
1314
  ) : null,
1356
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1357
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { children: scenario.title })
1315
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1316
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { children: scenario.title })
1358
1317
  ] }),
1359
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
1360
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
1361
- scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration3.formatDuration)(scenario.durationMs) }) : null,
1362
- meta.showPill ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "sr-only", children: `Status: ${label}` }),
1363
- actions ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(DropdownMenu, { children: [
1364
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1318
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex shrink-0 items-center gap-2", children: [
1319
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ScenarioRunHistory, { scenarioId: scenario.id }),
1320
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ScenarioStaleness, { scenario }),
1321
+ scenario.durationMs > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: (0, import_duration3.formatDuration)(scenario.durationMs) }) : null,
1322
+ meta.showPill ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "sr-only", children: `Status: ${label}` }),
1323
+ actions ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(DropdownMenu, { children: [
1324
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1365
1325
  DropdownMenuTrigger,
1366
1326
  {
1367
1327
  "aria-label": `Actions for ${scenario.title}`,
1368
1328
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
1369
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react2.MoreHorizontalIcon, { "aria-hidden": true })
1329
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react2.MoreHorizontalIcon, { "aria-hidden": true })
1370
1330
  }
1371
1331
  ),
1372
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(DropdownMenuContent, { align: "end", children: [
1373
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1374
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react2.LinkIcon, {}),
1332
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(DropdownMenuContent, { align: "end", children: [
1333
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1334
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react2.LinkIcon, {}),
1375
1335
  " Copy link"
1376
1336
  ] }),
1377
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1378
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react2.FileTextIcon, {}),
1337
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1338
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react2.FileTextIcon, {}),
1379
1339
  " Copy as Markdown"
1380
1340
  ] }),
1381
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1382
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react2.SparklesIcon, {}),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1342
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react2.SparklesIcon, {}),
1383
1343
  " Explain with AI"
1384
1344
  ] })
1385
1345
  ] })
1386
1346
  ] }) : null
1387
1347
  ] })
1388
1348
  ] }),
1389
- scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1390
- scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1391
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1349
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1350
+ scenario.tags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1351
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("li", { children: ticket.url ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1392
1352
  ] }) : null
1393
1353
  ] }),
1394
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1395
- scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1354
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
1355
+ scenario.errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
1396
1356
  "pre",
1397
1357
  {
1398
1358
  role: "alert",
@@ -1400,11 +1360,11 @@ function ReportScenario({ scenario, hideTitle = false }) {
1400
1360
  children: scenario.errorMessage
1401
1361
  }
1402
1362
  ) : null,
1403
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportStoryboard, { scenario }),
1404
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportSteps, { scenario }),
1405
- scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1406
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportAttachments, { attachments: scenario.attachments }),
1407
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ReportTrace, { spans: scenario.otelSpans })
1363
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportStoryboard, { scenario }),
1364
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportSteps, { scenario }),
1365
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
1366
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportAttachments, { attachments: scenario.attachments }),
1367
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportTrace, { spans: scenario.otelSpans })
1408
1368
  ] })
1409
1369
  ]
1410
1370
  }
@@ -1412,28 +1372,28 @@ function ReportScenario({ scenario, hideTitle = false }) {
1412
1372
  }
1413
1373
 
1414
1374
  // src/components/ReportScenarioList.tsx
1415
- var import_jsx_runtime27 = require("react/jsx-runtime");
1375
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1416
1376
  function ReportScenarioList({ feature }) {
1417
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_jsx_runtime27.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReportScenario, { scenario }, scenario.id)) });
1377
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_jsx_runtime28.Fragment, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ReportScenario, { scenario }, scenario.id)) });
1418
1378
  }
1419
1379
 
1420
1380
  // src/components/ReportFeatureIntro.tsx
1421
- var import_jsx_runtime28 = require("react/jsx-runtime");
1381
+ var import_jsx_runtime29 = require("react/jsx-runtime");
1422
1382
  function ReportFeatureIntro({ feature }) {
1423
1383
  const hasNarrative = Boolean(feature.narrative?.trim());
1424
1384
  const hasGlossary = Boolean(feature.glossary?.length);
1425
1385
  if (!hasNarrative && !hasGlossary) return null;
1426
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-sm", children: [
1427
- hasNarrative ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
1386
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-sm", children: [
1387
+ hasNarrative ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
1428
1388
  "div",
1429
1389
  {
1430
1390
  className: "es-doc-prose prose prose-sm max-w-none text-muted-foreground",
1431
1391
  dangerouslySetInnerHTML: { __html: safeMarkdownHtml(dedent(feature.narrative)) }
1432
1392
  }
1433
1393
  ) : null,
1434
- hasGlossary ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dl", { className: "grid gap-1.5", "aria-label": "Glossary", children: feature.glossary.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-wrap gap-x-2 text-xs", children: [
1435
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dt", { className: "font-medium text-foreground", children: entry.term }),
1436
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("dd", { className: "flex-1 text-muted-foreground", children: entry.definition })
1394
+ hasGlossary ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dl", { className: "grid gap-1.5", "aria-label": "Glossary", children: feature.glossary.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-wrap gap-x-2 text-xs", children: [
1395
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dt", { className: "font-medium text-foreground", children: entry.term }),
1396
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("dd", { className: "flex-1 text-muted-foreground", children: entry.definition })
1437
1397
  ] }, entry.term)) }) : null
1438
1398
  ] });
1439
1399
  }
@@ -1445,7 +1405,7 @@ function featureKindLabel(kind) {
1445
1405
  }
1446
1406
 
1447
1407
  // src/components/ReportFeature.tsx
1448
- var import_jsx_runtime29 = require("react/jsx-runtime");
1408
+ var import_jsx_runtime30 = require("react/jsx-runtime");
1449
1409
  function ReportFeature({ feature }) {
1450
1410
  const titleId = `${feature.id}-title`;
1451
1411
  const bodyId = `${feature.id}-body`;
@@ -1457,7 +1417,7 @@ function ReportFeature({ feature }) {
1457
1417
  return (
1458
1418
  // Suite = a section heading + a divider, NOT another bordered card. Drops a
1459
1419
  // layer of nesting so the scenario cards below read as the primary unit.
1460
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1420
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1461
1421
  "section",
1462
1422
  {
1463
1423
  id: feature.id,
@@ -1465,9 +1425,9 @@ function ReportFeature({ feature }) {
1465
1425
  "aria-labelledby": titleId,
1466
1426
  className: "flex flex-col gap-3",
1467
1427
  children: [
1468
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1469
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "min-w-0", children: [
1470
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1428
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1429
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "min-w-0", children: [
1430
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1471
1431
  "button",
1472
1432
  {
1473
1433
  type: "button",
@@ -1476,34 +1436,34 @@ function ReportFeature({ feature }) {
1476
1436
  "aria-controls": bodyId,
1477
1437
  className: "flex w-full cursor-pointer items-center gap-2 text-left hover:text-foreground/80",
1478
1438
  children: [
1479
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1480
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate", children: feature.title })
1439
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1440
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "truncate", children: feature.title })
1481
1441
  ]
1482
1442
  }
1483
1443
  ) : feature.title }),
1484
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("p", { className: cn("mt-0.5 flex items-center gap-2 text-xs text-muted-foreground", collapse && "pl-5"), children: [
1485
- kindLabel ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 font-medium text-foreground", children: kindLabel }) : null,
1486
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "truncate font-mono", children: feature.sourceFile })
1444
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("p", { className: cn("mt-0.5 flex items-center gap-2 text-xs text-muted-foreground", collapse && "pl-5"), children: [
1445
+ kindLabel ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 font-medium text-foreground", children: kindLabel }) : null,
1446
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "truncate font-mono", children: feature.sourceFile })
1487
1447
  ] })
1488
1448
  ] }),
1489
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
1449
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
1490
1450
  "p",
1491
1451
  {
1492
1452
  className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground",
1493
1453
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
1494
1454
  children: [
1495
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { children: [
1496
1456
  s.passed,
1497
1457
  " passed"
1498
1458
  ] }),
1499
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1500
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1459
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1460
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1501
1461
  s.failed,
1502
1462
  " failed"
1503
1463
  ] }),
1504
- skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
1505
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1506
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { children: [
1464
+ skipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
1465
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { "aria-hidden": true, children: "\xB7" }),
1466
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("span", { children: [
1507
1467
  skipped,
1508
1468
  " skipped"
1509
1469
  ] })
@@ -1512,9 +1472,9 @@ function ReportFeature({ feature }) {
1512
1472
  }
1513
1473
  )
1514
1474
  ] }),
1515
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: [
1516
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ReportFeatureIntro, { feature }),
1517
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ReportScenarioList, { feature })
1475
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: [
1476
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportFeatureIntro, { feature }),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportScenarioList, { feature })
1518
1478
  ] })
1519
1479
  ]
1520
1480
  }
@@ -1523,72 +1483,75 @@ function ReportFeature({ feature }) {
1523
1483
  }
1524
1484
 
1525
1485
  // src/components/ReportFeatureList.tsx
1526
- var import_jsx_runtime30 = require("react/jsx-runtime");
1486
+ var import_jsx_runtime31 = require("react/jsx-runtime");
1527
1487
  function ReportFeatureList() {
1528
1488
  const report = useReport();
1529
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ReportFeature, { feature }, feature.id)) });
1489
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ReportFeature, { feature }, feature.id)) });
1530
1490
  }
1531
1491
 
1532
- // src/components/ui/empty.tsx
1533
- var import_class_variance_authority2 = require("class-variance-authority");
1534
- var import_jsx_runtime31 = require("react/jsx-runtime");
1535
- function Empty({ className, ...props }) {
1536
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1537
- "div",
1538
- {
1539
- "data-slot": "empty",
1540
- className: cn(
1541
- "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",
1542
- className
1543
- ),
1544
- ...props
1545
- }
1546
- );
1547
- }
1548
- function EmptyHeader({ className, ...props }) {
1549
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1550
- "div",
1551
- {
1552
- "data-slot": "empty-header",
1553
- className: cn(
1554
- "flex max-w-sm flex-col items-center gap-2 text-center",
1555
- className
1556
- ),
1557
- ...props
1558
- }
1559
- );
1560
- }
1561
- var emptyMediaVariants = (0, import_class_variance_authority2.cva)(
1562
- "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
1563
- {
1564
- variants: {
1565
- variant: {
1566
- default: "bg-transparent",
1567
- icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
1568
- }
1569
- },
1570
- defaultVariants: {
1571
- variant: "default"
1572
- }
1492
+ // src/components/ReportMeta.tsx
1493
+ var import_duration4 = require("executable-stories-core/utils/duration");
1494
+ var import_jsx_runtime32 = require("react/jsx-runtime");
1495
+ function ReportMeta() {
1496
+ const report = useReport();
1497
+ const items = [];
1498
+ const linkClass = "text-primary underline underline-offset-2";
1499
+ if (report.startedAtMs) {
1500
+ items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1573
1501
  }
1574
- );
1575
- function EmptyTitle({ className, ...props }) {
1576
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
1577
- "div",
1578
- {
1579
- "data-slot": "empty-title",
1580
- className: cn("text-lg font-medium tracking-tight", className),
1581
- ...props
1582
- }
1502
+ if (report.durationMs) {
1503
+ items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1504
+ }
1505
+ if (report.packageVersion) {
1506
+ items.push({ label: "Version", value: report.packageVersion });
1507
+ }
1508
+ if (report.ci?.branch) {
1509
+ items.push({ label: "Branch", value: report.ci.branch });
1510
+ }
1511
+ const sha = report.ci?.commitSha ?? report.gitSha;
1512
+ if (sha) {
1513
+ const url = commitUrl(report.ci, report.gitSha);
1514
+ const short = sha.slice(0, 8);
1515
+ items.push({
1516
+ label: "Commit",
1517
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1518
+ });
1519
+ }
1520
+ if (report.ci) {
1521
+ items.push({
1522
+ label: "CI",
1523
+ value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1524
+ ciDisplayName(report.ci.name),
1525
+ report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1526
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1527
+ });
1528
+ }
1529
+ if (report.ci?.prNumber) {
1530
+ const url = prUrl(report.ci);
1531
+ const label = `#${report.ci.prNumber}`;
1532
+ items.push({
1533
+ label: "PR",
1534
+ value: url ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1535
+ });
1536
+ }
1537
+ if (items.length === 0) return null;
1538
+ return (
1539
+ // Run metadata is useful but secondary to "what failed" — tuck it into a
1540
+ // collapsed disclosure so it doesn't push the result down. Native <details>
1541
+ // so it works with no JS (the SSR/no-hydration report still toggles).
1542
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1543
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1544
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1545
+ "Run details"
1546
+ ] }),
1547
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex items-baseline gap-1.5", children: [
1548
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dt", { className: "font-medium", children: item.label }),
1549
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1550
+ ] }, item.label)) })
1551
+ ] })
1583
1552
  );
1584
1553
  }
1585
1554
 
1586
- // src/components/ReportEmpty.tsx
1587
- var import_jsx_runtime32 = require("react/jsx-runtime");
1588
- function ReportEmpty({ message }) {
1589
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(EmptyHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1590
- }
1591
-
1592
1555
  // src/components/ReportSummary.tsx
1593
1556
  var import_jsx_runtime33 = require("react/jsx-runtime");
1594
1557
  function ReportSummary({ className }) {
@@ -1652,182 +1615,65 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1652
1615
  );
1653
1616
  }
1654
1617
 
1655
- // src/components/ReportMeta.tsx
1656
- var import_duration4 = require("executable-stories-core/utils/duration");
1618
+ // src/components/ReportSchemaError.tsx
1619
+ var import_jsx_runtime34 = require("react/jsx-runtime");
1620
+ function ReportSchemaError({ error }) {
1621
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1622
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("strong", { children: "Report could not be displayed." }) }),
1623
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { children: error.message }),
1624
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("p", { children: [
1625
+ "The report bundle is newer than this version of ",
1626
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("code", { children: "executable-stories-react" }),
1627
+ ". Upgrade the package, or regenerate the report with an older formatters CLI."
1628
+ ] }) : null,
1629
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("details", { children: [
1630
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("summary", { children: [
1631
+ error.issues.length,
1632
+ " validation issue",
1633
+ error.issues.length === 1 ? "" : "s"
1634
+ ] }),
1635
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1636
+ ] }) : null
1637
+ ] });
1638
+ }
1657
1639
 
1658
- // src/lib/provenance.ts
1659
- function reportLastRunMs(report) {
1660
- if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1661
- if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1662
- return void 0;
1640
+ // src/components/ReportShell.tsx
1641
+ var import_jsx_runtime35 = require("react/jsx-runtime");
1642
+ function ReportErrorShell({
1643
+ error,
1644
+ className,
1645
+ title,
1646
+ dataTheme
1647
+ }) {
1648
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(ReportSchemaError, { error }) });
1663
1649
  }
1664
- function formatRelativeAge(thenMs, nowMs) {
1665
- const deltaMs = Math.max(0, nowMs - thenMs);
1666
- const minutes = Math.floor(deltaMs / 6e4);
1667
- if (minutes < 1) return "just now";
1668
- if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1669
- const hours = Math.floor(minutes / 60);
1670
- if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1671
- const days = Math.floor(hours / 24);
1672
- return `${days} day${days === 1 ? "" : "s"} ago`;
1673
- }
1674
- function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1675
- if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1676
- return nowMs - lastRunMs >= staleAfterDays * 864e5;
1677
- }
1678
- var CI_DISPLAY_NAMES = {
1679
- github: "GitHub Actions",
1680
- gitlab: "GitLab CI",
1681
- circleci: "CircleCI",
1682
- jenkins: "Jenkins",
1683
- azure: "Azure DevOps",
1684
- buildkite: "Buildkite",
1685
- travis: "Travis CI",
1686
- ci: "CI"
1687
- };
1688
- function ciDisplayName(name) {
1689
- return CI_DISPLAY_NAMES[name] ?? name;
1690
- }
1691
- function commitUrl(ci, sha) {
1692
- const commitSha = ci?.commitSha ?? sha;
1693
- if (!ci?.url || !commitSha) return void 0;
1694
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1695
- if (github) return `${github[1]}/commit/${commitSha}`;
1696
- const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1697
- if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1698
- return void 0;
1699
- }
1700
- function prUrl(ci) {
1701
- if (!ci?.url || !ci.prNumber) return void 0;
1702
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1703
- if (github) return `${github[1]}/pull/${ci.prNumber}`;
1704
- return void 0;
1705
- }
1706
-
1707
- // src/components/ReportMeta.tsx
1708
- var import_jsx_runtime34 = require("react/jsx-runtime");
1709
- function ReportMeta() {
1710
- const report = useReport();
1711
- const items = [];
1712
- const linkClass = "text-primary underline underline-offset-2";
1713
- if (report.startedAtMs) {
1714
- items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1715
- }
1716
- if (report.durationMs) {
1717
- items.push({ label: "Duration", value: (0, import_duration4.formatDuration)(report.durationMs) });
1718
- }
1719
- if (report.packageVersion) {
1720
- items.push({ label: "Version", value: report.packageVersion });
1721
- }
1722
- if (report.ci?.branch) {
1723
- items.push({ label: "Branch", value: report.ci.branch });
1724
- }
1725
- const sha = report.ci?.commitSha ?? report.gitSha;
1726
- if (sha) {
1727
- const url = commitUrl(report.ci, report.gitSha);
1728
- const short = sha.slice(0, 8);
1729
- items.push({
1730
- label: "Commit",
1731
- value: url ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1732
- });
1733
- }
1734
- if (report.ci) {
1735
- items.push({
1736
- label: "CI",
1737
- value: report.ci.url ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1738
- ciDisplayName(report.ci.name),
1739
- report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1740
- ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1741
- });
1742
- }
1743
- if (report.ci?.prNumber) {
1744
- const url = prUrl(report.ci);
1745
- const label = `#${report.ci.prNumber}`;
1746
- items.push({
1747
- label: "PR",
1748
- value: url ? /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1749
- });
1750
- }
1751
- if (items.length === 0) return null;
1752
- return (
1753
- // Run metadata is useful but secondary to "what failed" — tuck it into a
1754
- // collapsed disclosure so it doesn't push the result down. Native <details>
1755
- // so it works with no JS (the SSR/no-hydration report still toggles).
1756
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1757
- /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1758
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1759
- "Run details"
1760
- ] }),
1761
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex items-baseline gap-1.5", children: [
1762
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("dt", { className: "font-medium", children: item.label }),
1763
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("dd", { className: "font-mono text-foreground", children: item.value })
1764
- ] }, item.label)) })
1765
- ] })
1766
- );
1767
- }
1768
-
1769
- // src/components/ReportSchemaError.tsx
1770
- var import_jsx_runtime35 = require("react/jsx-runtime");
1771
- function ReportSchemaError({ error }) {
1772
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1773
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("strong", { children: "Report could not be displayed." }) }),
1774
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { children: error.message }),
1775
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("p", { children: [
1776
- "The report bundle is newer than this version of ",
1777
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("code", { children: "executable-stories-react" }),
1778
- ". Upgrade the package, or regenerate the report with an older formatters CLI."
1779
- ] }) : null,
1780
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("details", { children: [
1781
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("summary", { children: [
1782
- error.issues.length,
1783
- " validation issue",
1784
- error.issues.length === 1 ? "" : "s"
1785
- ] }),
1786
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1787
- ] }) : null
1788
- ] });
1789
- }
1790
-
1791
- // src/components/ReportShell.tsx
1792
- var import_jsx_runtime36 = require("react/jsx-runtime");
1793
- function ReportErrorShell({
1794
- error,
1795
- className,
1796
- title,
1797
- dataTheme
1798
- }) {
1799
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ReportSchemaError, { error }) });
1800
- }
1801
-
1802
- // src/interactive/ReportInteractive.tsx
1803
- var import_lucide_react5 = require("lucide-react");
1804
-
1805
- // src/components/ui/switch.tsx
1806
- var React4 = require("react");
1807
- var import_switch = require("@base-ui/react/switch");
1808
- var import_jsx_runtime37 = require("react/jsx-runtime");
1809
- function Switch({
1810
- className,
1811
- ...props
1812
- }) {
1813
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1814
- import_switch.Switch.Root,
1815
- {
1816
- "data-slot": "switch",
1817
- className: cn(
1818
- "peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-primary data-[unchecked]:bg-input",
1819
- className
1820
- ),
1821
- ...props,
1822
- children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1823
- import_switch.Switch.Thumb,
1824
- {
1825
- "data-slot": "switch-thumb",
1826
- className: "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[checked]:translate-x-[calc(100%-2px)] data-[unchecked]:translate-x-0"
1827
- }
1828
- )
1829
- }
1830
- );
1650
+
1651
+ // src/components/ui/switch.tsx
1652
+ var React4 = require("react");
1653
+ var import_switch = require("@base-ui/react/switch");
1654
+ var import_jsx_runtime36 = require("react/jsx-runtime");
1655
+ function Switch({
1656
+ className,
1657
+ ...props
1658
+ }) {
1659
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1660
+ import_switch.Switch.Root,
1661
+ {
1662
+ "data-slot": "switch",
1663
+ className: cn(
1664
+ "peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-primary data-[unchecked]:bg-input",
1665
+ className
1666
+ ),
1667
+ ...props,
1668
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
1669
+ import_switch.Switch.Thumb,
1670
+ {
1671
+ "data-slot": "switch-thumb",
1672
+ className: "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[checked]:translate-x-[calc(100%-2px)] data-[unchecked]:translate-x-0"
1673
+ }
1674
+ )
1675
+ }
1676
+ );
1831
1677
  }
1832
1678
 
1833
1679
  // src/components/ui/toggle-group.tsx
@@ -1835,7 +1681,7 @@ var React5 = __toESM(require("react"), 1);
1835
1681
  var import_toggle_group = require("@base-ui/react/toggle-group");
1836
1682
  var import_toggle = require("@base-ui/react/toggle");
1837
1683
  var import_class_variance_authority3 = require("class-variance-authority");
1838
- var import_jsx_runtime38 = require("react/jsx-runtime");
1684
+ var import_jsx_runtime37 = require("react/jsx-runtime");
1839
1685
  var toggleItemVariants = (0, import_class_variance_authority3.cva)(
1840
1686
  "inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap text-muted-foreground transition-[color,background-color,box-shadow] outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-background data-[pressed]:text-foreground data-[pressed]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1841
1687
  {
@@ -1857,13 +1703,13 @@ function ToggleGroup({
1857
1703
  children,
1858
1704
  ...props
1859
1705
  }) {
1860
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1706
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1861
1707
  import_toggle_group.ToggleGroup,
1862
1708
  {
1863
1709
  "data-slot": "toggle-group",
1864
1710
  className: cn("flex w-fit items-center gap-0.5 rounded-lg bg-muted p-0.5", className),
1865
1711
  ...props,
1866
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ToggleGroupContext.Provider, { value: { size }, children })
1712
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ToggleGroupContext.Provider, { value: { size }, children })
1867
1713
  }
1868
1714
  );
1869
1715
  }
@@ -1874,7 +1720,7 @@ function ToggleGroupItem({
1874
1720
  ...props
1875
1721
  }) {
1876
1722
  const context = React5.useContext(ToggleGroupContext);
1877
- return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1723
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
1878
1724
  import_toggle.Toggle,
1879
1725
  {
1880
1726
  "data-slot": "toggle-group-item",
@@ -1885,99 +1731,207 @@ function ToggleGroupItem({
1885
1731
  );
1886
1732
  }
1887
1733
 
1888
- // src/interactive/ReportSearch.tsx
1889
- var import_react8 = require("react");
1890
- var import_lucide_react3 = require("lucide-react");
1734
+ // src/context/ReportRoot.tsx
1735
+ var import_react9 = require("react");
1891
1736
 
1892
- // src/components/ui/input.tsx
1893
- var React6 = require("react");
1894
- var import_jsx_runtime39 = require("react/jsx-runtime");
1895
- function Input({ className, type, ...props }) {
1896
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
1897
- "input",
1898
- {
1899
- type,
1900
- "data-slot": "input",
1901
- className: cn(
1902
- "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
1903
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
1904
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
1905
- className
1906
- ),
1907
- ...props
1908
- }
1909
- );
1737
+ // src/components/doc/NarrativeBlocks.tsx
1738
+ var import_jsx_runtime38 = require("react/jsx-runtime");
1739
+ var FILE_TREE_TYPE = "file-tree";
1740
+ var DATA_MODEL_TYPE = "data-model";
1741
+ function isChangeKind(value) {
1742
+ return value === "added" || value === "modified" || value === "removed" || value === "renamed";
1910
1743
  }
1911
-
1912
- // src/interactive/ReportSearch.tsx
1913
- var import_jsx_runtime40 = require("react/jsx-runtime");
1914
- var ReportSearch = (0, import_react8.forwardRef)(
1915
- function ReportSearch2(props, ref) {
1916
- const {
1917
- value,
1918
- onChange,
1919
- matchedCount,
1920
- totalCount,
1921
- placeholder = "Search scenarios or tags\u2026",
1922
- className
1923
- } = props;
1924
- const inputId = (0, import_react8.useId)();
1925
- const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
1926
- function handleChange(e) {
1927
- onChange(e.target.value);
1744
+ function isRecord(value) {
1745
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1746
+ }
1747
+ function str(value) {
1748
+ return typeof value === "string" && value !== "" ? value : void 0;
1749
+ }
1750
+ function AuthoredMarker({ authored }) {
1751
+ if (authored !== "agent") return null;
1752
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
1753
+ }
1754
+ function BlockFrame({
1755
+ title,
1756
+ authored,
1757
+ children
1758
+ }) {
1759
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("figure", { className: "my-3", children: [
1760
+ title || authored === "agent" ? /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
1761
+ title ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
1762
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(AuthoredMarker, { authored })
1763
+ ] }) : null,
1764
+ children
1765
+ ] });
1766
+ }
1767
+ function ChangeBadge({ change }) {
1768
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(Badge, { variant: "tag", className: "text-[10px]", children: change });
1769
+ }
1770
+ function parseFileTree(data) {
1771
+ if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
1772
+ const files = [];
1773
+ for (const raw of data.files) {
1774
+ if (typeof raw === "string") {
1775
+ if (raw) files.push({ path: raw });
1776
+ continue;
1928
1777
  }
1929
- function handleKeyDown(e) {
1930
- if (e.key === "Escape" && value !== "") {
1931
- onChange("");
1932
- e.preventDefault();
1778
+ if (!isRecord(raw)) continue;
1779
+ const path = str(raw.path);
1780
+ if (!path) continue;
1781
+ files.push({
1782
+ path,
1783
+ change: isChangeKind(raw.change) ? raw.change : void 0,
1784
+ note: str(raw.note)
1785
+ });
1786
+ }
1787
+ if (files.length === 0) return void 0;
1788
+ return { title: str(data.title), authored: str(data.authored), files };
1789
+ }
1790
+ function buildTree(files) {
1791
+ const root = { name: "", children: /* @__PURE__ */ new Map() };
1792
+ for (const file of files) {
1793
+ const segments = file.path.split("/").filter(Boolean);
1794
+ let node = root;
1795
+ segments.forEach((segment, index) => {
1796
+ let child = node.children.get(segment);
1797
+ if (!child) {
1798
+ child = { name: segment, children: /* @__PURE__ */ new Map() };
1799
+ node.children.set(segment, child);
1933
1800
  }
1934
- }
1935
- return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: cn("es-search flex flex-col gap-1", className), children: [
1936
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
1937
- /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: "relative w-full sm:w-72", children: [
1938
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
1939
- import_lucide_react3.Search,
1940
- {
1941
- "aria-hidden": true,
1942
- className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
1943
- }
1944
- ),
1945
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
1946
- Input,
1947
- {
1948
- ref,
1949
- id: inputId,
1950
- type: "search",
1951
- value,
1952
- onChange: handleChange,
1953
- onKeyDown: handleKeyDown,
1954
- placeholder,
1955
- autoComplete: "off",
1956
- spellCheck: false,
1957
- "aria-keyshortcuts": "/",
1958
- className: "pl-8"
1801
+ if (index === segments.length - 1) child.file = file;
1802
+ node = child;
1803
+ });
1804
+ }
1805
+ return root;
1806
+ }
1807
+ function sortedChildren(node) {
1808
+ return [...node.children.values()].sort((a, b) => {
1809
+ const aDir = a.children.size > 0;
1810
+ const bDir = b.children.size > 0;
1811
+ if (aDir !== bDir) return aDir ? -1 : 1;
1812
+ return a.name.localeCompare(b.name);
1813
+ });
1814
+ }
1815
+ function TreeBranch({ node, depth }) {
1816
+ const children = sortedChildren(node);
1817
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
1818
+ const isDir = child.children.size > 0;
1819
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("li", { children: [
1820
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "flex flex-wrap items-baseline gap-2", children: [
1821
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1822
+ "span",
1823
+ {
1824
+ className: cn(
1825
+ "font-mono text-xs",
1826
+ isDir ? "text-muted-foreground" : "text-foreground"
1827
+ ),
1828
+ children: isDir ? `${child.name}/` : child.name
1959
1829
  }
1960
- )
1830
+ ),
1831
+ child.file?.change ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ChangeBadge, { change: child.file.change }) : null,
1832
+ child.file?.note ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
1961
1833
  ] }),
1962
- showCounts ? /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
1963
- "span",
1964
- {
1965
- className: "text-xs tabular-nums text-muted-foreground",
1966
- "aria-live": "polite",
1967
- children: [
1968
- matchedCount,
1969
- " of ",
1970
- totalCount,
1971
- " scenarios"
1972
- ]
1973
- }
1974
- ) : null
1975
- ] });
1834
+ isDir ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(TreeBranch, { node: child, depth: depth + 1 }) : null
1835
+ ] }, child.name);
1836
+ }) });
1837
+ }
1838
+ function FileTreeBlock({ entry }) {
1839
+ const data = parseFileTree(entry.data);
1840
+ if (!data) return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(UnreadableBlock, { entry });
1841
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
1842
+ }
1843
+ function parseDataModel(data) {
1844
+ if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
1845
+ const fields = [];
1846
+ for (const raw of data.fields) {
1847
+ if (!isRecord(raw)) continue;
1848
+ const name = str(raw.name);
1849
+ if (!name) continue;
1850
+ fields.push({
1851
+ name,
1852
+ type: str(raw.type),
1853
+ note: str(raw.note),
1854
+ change: isChangeKind(raw.change) ? raw.change : void 0
1855
+ });
1976
1856
  }
1977
- );
1857
+ if (fields.length === 0) return void 0;
1858
+ return {
1859
+ title: str(data.title),
1860
+ authored: str(data.authored),
1861
+ name: str(data.name),
1862
+ fields
1863
+ };
1864
+ }
1865
+ function DataModelBlock({ entry }) {
1866
+ const data = parseDataModel(entry.data);
1867
+ if (!data) return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(UnreadableBlock, { entry });
1868
+ const hasNotes = data.fields.some((f) => f.note);
1869
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("table", { className: "w-full border-collapse text-sm", children: [
1870
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("tr", { className: "bg-muted/50", children: [
1871
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
1872
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
1873
+ hasNotes ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
1874
+ ] }) }),
1875
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("tbody", { children: data.fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("tr", { children: [
1876
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("span", { className: "flex flex-wrap items-center gap-2", children: [
1877
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("span", { className: "font-mono text-xs text-foreground", children: field.name }),
1878
+ field.change ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ChangeBadge, { change: field.change }) : null
1879
+ ] }) }),
1880
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
1881
+ hasNotes ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
1882
+ ] }, field.name)) })
1883
+ ] }) }) });
1884
+ }
1885
+ function UnreadableBlock({ entry }) {
1886
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("div", { className: "my-2", "data-type": entry.type, children: [
1887
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
1888
+ entry.type,
1889
+ " (unrecognised shape)"
1890
+ ] }),
1891
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
1892
+ "pre",
1893
+ {
1894
+ tabIndex: 0,
1895
+ className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
1896
+ children: safeStringify2(entry.data)
1897
+ }
1898
+ )
1899
+ ] });
1900
+ }
1901
+ function safeStringify2(value) {
1902
+ try {
1903
+ return JSON.stringify(value, null, 2);
1904
+ } catch {
1905
+ return String(value);
1906
+ }
1907
+ }
1908
+ var narrativeBlockRenderers = {
1909
+ [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(FileTreeBlock, { entry }),
1910
+ [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(DataModelBlock, { entry })
1911
+ };
1978
1912
 
1979
- // src/interactive/ReportFailureBanner.tsx
1980
- var import_react9 = require("react");
1913
+ // src/context/ReportRoot.tsx
1914
+ var import_jsx_runtime39 = require("react/jsx-runtime");
1915
+ var EMPTY_RENDERERS2 = {};
1916
+ function ReportRoot({
1917
+ report,
1918
+ customRenderers,
1919
+ renderers,
1920
+ children
1921
+ }) {
1922
+ const value = (0, import_react9.useMemo)(
1923
+ () => ({
1924
+ report,
1925
+ // Narrative blocks render everywhere the report does (static SSR, the
1926
+ // island, Astro) without every host wiring them up. A host renderer for
1927
+ // the same type still wins.
1928
+ customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
1929
+ renderers: renderers ?? EMPTY_RENDERERS2
1930
+ }),
1931
+ [report, customRenderers, renderers]
1932
+ );
1933
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(ReportContext.Provider, { value, children });
1934
+ }
1981
1935
 
1982
1936
  // src/lib/hash-state.ts
1983
1937
  var DEFAULT_URL_STATE = {
@@ -2047,16 +2001,120 @@ function scrollToScenarioId(scenarioId, options = {}) {
2047
2001
  }
2048
2002
  }
2049
2003
 
2004
+ // src/result.ts
2005
+ var ok = (data) => ({ ok: true, data });
2006
+ function unwrapReport(value) {
2007
+ if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
2008
+ return value;
2009
+ }
2010
+ return ok(value);
2011
+ }
2012
+
2013
+ // src/interactive/filter.ts
2014
+ function normalizeQuery(query) {
2015
+ return query.trim().toLowerCase();
2016
+ }
2017
+ function allTags(report) {
2018
+ const seen = /* @__PURE__ */ new Set();
2019
+ for (const feature of report.features) {
2020
+ for (const scenario of feature.scenarios) {
2021
+ for (const tag of scenario.tags) seen.add(tag);
2022
+ }
2023
+ }
2024
+ return [...seen];
2025
+ }
2026
+ function queryMatches(scenario, q) {
2027
+ if (q === "") return true;
2028
+ if (scenario.title.toLowerCase().includes(q)) return true;
2029
+ for (const tag of scenario.tags) {
2030
+ if (tag.toLowerCase().includes(q)) return true;
2031
+ }
2032
+ for (const step of scenario.steps) {
2033
+ if (step.text.toLowerCase().includes(q)) return true;
2034
+ }
2035
+ return false;
2036
+ }
2037
+ function scenarioMatches(scenario, q, status, tags) {
2038
+ if (status !== "all" && scenario.status !== status) return false;
2039
+ if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2040
+ return queryMatches(scenario, q);
2041
+ }
2042
+ function summarizeScenarios(scenarios) {
2043
+ let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
2044
+ for (const s of scenarios) {
2045
+ total += 1;
2046
+ durationMs += s.durationMs;
2047
+ if (s.status === "passed") passed += 1;
2048
+ else if (s.status === "failed") failed += 1;
2049
+ else if (s.status === "skipped") skipped += 1;
2050
+ else pending += 1;
2051
+ }
2052
+ return { total, passed, failed, skipped, pending, durationMs };
2053
+ }
2054
+ function filterReport(report, criteria) {
2055
+ const c = typeof criteria === "string" ? { query: criteria } : criteria;
2056
+ const q = normalizeQuery(c.query ?? "");
2057
+ const status = c.status ?? "all";
2058
+ const tags = c.tags ?? [];
2059
+ if (q === "" && status === "all" && tags.length === 0) return report;
2060
+ const features = [];
2061
+ let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2062
+ for (const feature of report.features) {
2063
+ const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2064
+ if (matched.length === 0) continue;
2065
+ const summary = summarizeScenarios(matched);
2066
+ features.push({ ...feature, summary, scenarios: matched });
2067
+ topTotal += summary.total;
2068
+ topPassed += summary.passed;
2069
+ topFailed += summary.failed;
2070
+ topSkipped += summary.skipped;
2071
+ topPending += summary.pending;
2072
+ topDuration += summary.durationMs;
2073
+ }
2074
+ return {
2075
+ ...report,
2076
+ summary: {
2077
+ total: topTotal,
2078
+ passed: topPassed,
2079
+ failed: topFailed,
2080
+ skipped: topSkipped,
2081
+ pending: topPending,
2082
+ durationMs: topDuration
2083
+ },
2084
+ features
2085
+ };
2086
+ }
2087
+ function listFailures(report) {
2088
+ const out = [];
2089
+ for (const feature of report.features) {
2090
+ for (const scenario of feature.scenarios) {
2091
+ if (scenario.status === "failed") {
2092
+ const ref = {
2093
+ featureId: feature.id,
2094
+ scenarioId: scenario.id,
2095
+ scenarioTitle: scenario.title
2096
+ };
2097
+ if (scenario.errorMessage !== void 0) {
2098
+ ref.errorMessage = scenario.errorMessage;
2099
+ }
2100
+ out.push(ref);
2101
+ }
2102
+ }
2103
+ }
2104
+ return out;
2105
+ }
2106
+
2050
2107
  // src/interactive/ReportFailureBanner.tsx
2051
- var import_jsx_runtime41 = require("react/jsx-runtime");
2108
+ var import_react10 = require("react");
2109
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2052
2110
  function ReportFailureBanner({ failures }) {
2053
2111
  const first = failures[0];
2054
- const jumpToFirst = (0, import_react9.useCallback)(() => {
2112
+ const jumpToFirst = (0, import_react10.useCallback)(() => {
2055
2113
  if (!first) return;
2056
2114
  scrollToScenarioId(first.scenarioId);
2057
2115
  }, [first]);
2058
2116
  if (failures.length === 0) return null;
2059
- return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2117
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2060
2118
  "div",
2061
2119
  {
2062
2120
  className: "es-failure-banner",
@@ -2064,13 +2122,13 @@ function ReportFailureBanner({ failures }) {
2064
2122
  "aria-live": "polite",
2065
2123
  "aria-label": "Failure summary",
2066
2124
  children: [
2067
- /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "es-failure-banner-text", children: [
2068
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("strong", { children: failures.length }),
2125
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "es-failure-banner-text", children: [
2126
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("strong", { children: failures.length }),
2069
2127
  " ",
2070
2128
  "failure",
2071
2129
  failures.length === 1 ? "" : "s"
2072
2130
  ] }),
2073
- /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2131
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2074
2132
  "button",
2075
2133
  {
2076
2134
  type: "button",
@@ -2085,37 +2143,111 @@ function ReportFailureBanner({ failures }) {
2085
2143
  );
2086
2144
  }
2087
2145
 
2088
- // src/interactive/ReportFreshness.tsx
2089
- var import_react10 = require("react");
2090
- var import_jsx_runtime42 = require("react/jsx-runtime");
2091
- function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2092
- const [now, setNow] = (0, import_react10.useState)(nowMs);
2093
- (0, import_react10.useEffect)(() => {
2094
- if (nowMs === void 0) setNow(Date.now());
2095
- }, [nowMs]);
2096
- if (lastRunMs === void 0 || now === void 0) return null;
2097
- const age = formatRelativeAge(lastRunMs, now);
2098
- const runLink = ciUrl ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2099
- "a",
2100
- {
2101
- href: ciUrl,
2102
- target: "_blank",
2103
- rel: "noreferrer noopener",
2104
- className: "underline underline-offset-2",
2105
- children: "View CI run"
2106
- }
2107
- ) : null;
2108
- if (isReportStale(lastRunMs, staleAfterDays, now)) {
2109
- return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2110
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { children: [
2111
- /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("strong", { children: [
2112
- "Last verified ",
2113
- age,
2114
- "."
2115
- ] }),
2116
- " This report may no longer match the current behavior. Look for a newer run."
2117
- ] }),
2118
- runLink
2146
+ // src/interactive/ReportFilters.tsx
2147
+ var import_react11 = require("react");
2148
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2149
+ var TAG_LIMIT = 12;
2150
+ function ReportFilters({
2151
+ statuses,
2152
+ status,
2153
+ onStatus,
2154
+ tags,
2155
+ activeTags,
2156
+ onToggleTag
2157
+ }) {
2158
+ const [showAllTags, setShowAllTags] = (0, import_react11.useState)(false);
2159
+ if (statuses.length === 0 && tags.length === 0) return null;
2160
+ const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2161
+ const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2162
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex flex-col gap-2", children: [
2163
+ statuses.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2164
+ const active = status === opt.key;
2165
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2166
+ "button",
2167
+ {
2168
+ type: "button",
2169
+ "aria-pressed": active,
2170
+ onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
2171
+ className: cn(
2172
+ "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
2173
+ active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
2174
+ ),
2175
+ children: [
2176
+ opt.label,
2177
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2178
+ ]
2179
+ },
2180
+ opt.key
2181
+ );
2182
+ }) }) : null,
2183
+ tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2184
+ visibleTags.map((tag) => {
2185
+ const active = activeTags.includes(tag);
2186
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2187
+ "button",
2188
+ {
2189
+ type: "button",
2190
+ "aria-pressed": active,
2191
+ onClick: () => onToggleTag(tag),
2192
+ className: "cursor-pointer",
2193
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2194
+ Badge,
2195
+ {
2196
+ variant: "tag",
2197
+ className: cn(
2198
+ active && "border-primary bg-primary text-primary-foreground"
2199
+ ),
2200
+ children: tag
2201
+ }
2202
+ )
2203
+ }
2204
+ ) }, tag);
2205
+ }),
2206
+ tags.length > TAG_LIMIT ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2207
+ "button",
2208
+ {
2209
+ type: "button",
2210
+ "aria-expanded": showAllTags,
2211
+ onClick: () => setShowAllTags((v) => !v),
2212
+ className: "cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2213
+ children: tagsCollapsed ? `+${tags.length - TAG_LIMIT} more` : "Show fewer"
2214
+ }
2215
+ ) }) : null
2216
+ ] }) : null
2217
+ ] });
2218
+ }
2219
+
2220
+ // src/interactive/ReportFreshness.tsx
2221
+ var import_react12 = require("react");
2222
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2223
+ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2224
+ const [now, setNow] = (0, import_react12.useState)(nowMs);
2225
+ (0, import_react12.useEffect)(() => {
2226
+ if (nowMs === void 0) setNow(Date.now());
2227
+ }, [nowMs]);
2228
+ if (lastRunMs === void 0 || now === void 0) return null;
2229
+ const age = formatRelativeAge(lastRunMs, now);
2230
+ const runLink = ciUrl ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2231
+ "a",
2232
+ {
2233
+ href: ciUrl,
2234
+ target: "_blank",
2235
+ rel: "noreferrer noopener",
2236
+ className: "underline underline-offset-2",
2237
+ children: "View CI run"
2238
+ }
2239
+ ) : null;
2240
+ if (isReportStale(lastRunMs, staleAfterDays, now)) {
2241
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2242
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { children: [
2243
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("strong", { children: [
2244
+ "Last verified ",
2245
+ age,
2246
+ "."
2247
+ ] }),
2248
+ " This report may no longer match the current behavior. Look for a newer run."
2249
+ ] }),
2250
+ runLink
2119
2251
  ] });
2120
2252
  }
2121
2253
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
@@ -2177,8 +2309,99 @@ function ReportLastRunDelta({
2177
2309
  ] });
2178
2310
  }
2179
2311
 
2312
+ // src/interactive/ReportSearch.tsx
2313
+ var import_react13 = require("react");
2314
+ var import_lucide_react3 = require("lucide-react");
2315
+
2316
+ // src/components/ui/input.tsx
2317
+ var React6 = require("react");
2318
+ var import_jsx_runtime44 = require("react/jsx-runtime");
2319
+ function Input({ className, type, ...props }) {
2320
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
2321
+ "input",
2322
+ {
2323
+ type,
2324
+ "data-slot": "input",
2325
+ className: cn(
2326
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
2327
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
2328
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
2329
+ className
2330
+ ),
2331
+ ...props
2332
+ }
2333
+ );
2334
+ }
2335
+
2336
+ // src/interactive/ReportSearch.tsx
2337
+ var import_jsx_runtime45 = require("react/jsx-runtime");
2338
+ var ReportSearch = (0, import_react13.forwardRef)(
2339
+ function ReportSearch2(props, ref) {
2340
+ const {
2341
+ value,
2342
+ onChange,
2343
+ matchedCount,
2344
+ totalCount,
2345
+ placeholder = "Search scenarios or tags\u2026",
2346
+ className
2347
+ } = props;
2348
+ const inputId = (0, import_react13.useId)();
2349
+ const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
2350
+ function handleChange(e) {
2351
+ onChange(e.target.value);
2352
+ }
2353
+ function handleKeyDown(e) {
2354
+ if (e.key === "Escape" && value !== "") {
2355
+ onChange("");
2356
+ e.preventDefault();
2357
+ }
2358
+ }
2359
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: cn("es-search flex flex-col gap-1", className), children: [
2360
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
2361
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "relative w-full sm:w-72", children: [
2362
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2363
+ import_lucide_react3.Search,
2364
+ {
2365
+ "aria-hidden": true,
2366
+ className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
2367
+ }
2368
+ ),
2369
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2370
+ Input,
2371
+ {
2372
+ ref,
2373
+ id: inputId,
2374
+ type: "search",
2375
+ value,
2376
+ onChange: handleChange,
2377
+ onKeyDown: handleKeyDown,
2378
+ placeholder,
2379
+ autoComplete: "off",
2380
+ spellCheck: false,
2381
+ "aria-keyshortcuts": "/",
2382
+ className: "pl-8"
2383
+ }
2384
+ )
2385
+ ] }),
2386
+ showCounts ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2387
+ "span",
2388
+ {
2389
+ className: "text-xs tabular-nums text-muted-foreground",
2390
+ "aria-live": "polite",
2391
+ children: [
2392
+ matchedCount,
2393
+ " of ",
2394
+ totalCount,
2395
+ " scenarios"
2396
+ ]
2397
+ }
2398
+ ) : null
2399
+ ] });
2400
+ }
2401
+ );
2402
+
2180
2403
  // src/interactive/ReportShortcutsHelp.tsx
2181
- var import_react11 = require("react");
2404
+ var import_react14 = require("react");
2182
2405
 
2183
2406
  // src/components/ui/button.tsx
2184
2407
  var React7 = require("react");
@@ -2230,326 +2453,57 @@ function Button({
2230
2453
  className: cn(buttonVariants({ variant, size, className })),
2231
2454
  ...props
2232
2455
  }
2233
- });
2234
- }
2235
-
2236
- // src/interactive/ReportShortcutsHelp.tsx
2237
- var import_jsx_runtime44 = require("react/jsx-runtime");
2238
- var SHORTCUTS = [
2239
- { keys: "/", description: "Focus search" },
2240
- { keys: "f", description: "Jump to next failure" },
2241
- { keys: "Shift F", description: "Jump to previous failure" },
2242
- { keys: "e", description: "Expand all" },
2243
- { keys: "c", description: "Collapse all" },
2244
- { keys: "?", description: "Toggle this help" },
2245
- { keys: "Esc", description: "Clear search / close" }
2246
- ];
2247
- function Keys({ keys }) {
2248
- const parts = keys.split(" ");
2249
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
2250
- i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2251
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2252
- ] }, k)) });
2253
- }
2254
- function ReportShortcutsHelp({ open, onClose }) {
2255
- const dialogRef = (0, import_react11.useRef)(null);
2256
- (0, import_react11.useEffect)(() => {
2257
- const dialog = dialogRef.current;
2258
- if (!dialog) return;
2259
- if (open && !dialog.open) dialog.showModal();
2260
- else if (!open && dialog.open) dialog.close();
2261
- }, [open]);
2262
- return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(
2263
- "dialog",
2264
- {
2265
- ref: dialogRef,
2266
- className: "es-shortcuts-help m-auto w-[min(90vw,28rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2267
- "aria-label": "Keyboard shortcuts",
2268
- onClose,
2269
- children: [
2270
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2271
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2272
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("dt", { className: "justify-self-start", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(Keys, { keys: s.keys }) }),
2273
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("dd", { className: "text-sm text-muted-foreground", children: s.description })
2274
- ] }, s.keys)) }),
2275
- /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2276
- ]
2277
- }
2278
- );
2279
- }
2280
-
2281
- // src/interactive/use-keyboard-shortcuts.ts
2282
- var import_react12 = require("react");
2283
- function isEditableTarget(target) {
2284
- if (!(target instanceof HTMLElement)) return false;
2285
- const tag = target.tagName;
2286
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2287
- if (target.isContentEditable) return true;
2288
- return false;
2289
- }
2290
- function useKeyboardShortcuts(handlers) {
2291
- const ref = (0, import_react12.useRef)(handlers);
2292
- ref.current = handlers;
2293
- (0, import_react12.useEffect)(() => {
2294
- function onKeyDown(e) {
2295
- const h = ref.current;
2296
- if (e.key === "Escape") {
2297
- h.onEscape?.();
2298
- return;
2299
- }
2300
- if (isEditableTarget(e.target)) return;
2301
- if (e.metaKey || e.ctrlKey || e.altKey) return;
2302
- switch (e.key) {
2303
- case "/":
2304
- h.onFocusSearch?.();
2305
- e.preventDefault();
2306
- return;
2307
- case "?":
2308
- h.onToggleHelp?.();
2309
- e.preventDefault();
2310
- return;
2311
- case "f":
2312
- h.onNextFailure?.();
2313
- e.preventDefault();
2314
- return;
2315
- case "F":
2316
- h.onPrevFailure?.();
2317
- e.preventDefault();
2318
- return;
2319
- case "e":
2320
- h.onExpandAll?.();
2321
- e.preventDefault();
2322
- return;
2323
- case "c":
2324
- h.onCollapseAll?.();
2325
- e.preventDefault();
2326
- return;
2327
- default:
2328
- return;
2329
- }
2330
- }
2331
- window.addEventListener("keydown", onKeyDown);
2332
- return () => window.removeEventListener("keydown", onKeyDown);
2333
- }, []);
2334
- }
2335
-
2336
- // src/interactive/use-deep-link-scroll.ts
2337
- var import_react13 = require("react");
2338
- function useDeepLinkScroll() {
2339
- const lastAnchor = (0, import_react13.useRef)(null);
2340
- (0, import_react13.useEffect)(() => {
2341
- function scrollToHash() {
2342
- const { anchor } = splitHash(window.location.hash);
2343
- if (!anchor || anchor === lastAnchor.current) return;
2344
- lastAnchor.current = anchor;
2345
- requestAnimationFrame(() => {
2346
- scrollToScenarioId(anchor, { updateHash: false });
2347
- });
2348
- }
2349
- scrollToHash();
2350
- window.addEventListener("hashchange", scrollToHash);
2351
- return () => window.removeEventListener("hashchange", scrollToHash);
2352
- }, []);
2353
- }
2354
-
2355
- // src/interactive/filter.ts
2356
- function normalizeQuery(query) {
2357
- return query.trim().toLowerCase();
2358
- }
2359
- function allTags(report) {
2360
- const seen = /* @__PURE__ */ new Set();
2361
- for (const feature of report.features) {
2362
- for (const scenario of feature.scenarios) {
2363
- for (const tag of scenario.tags) seen.add(tag);
2364
- }
2365
- }
2366
- return [...seen];
2367
- }
2368
- function queryMatches(scenario, q) {
2369
- if (q === "") return true;
2370
- if (scenario.title.toLowerCase().includes(q)) return true;
2371
- for (const tag of scenario.tags) {
2372
- if (tag.toLowerCase().includes(q)) return true;
2373
- }
2374
- for (const step of scenario.steps) {
2375
- if (step.text.toLowerCase().includes(q)) return true;
2376
- }
2377
- return false;
2378
- }
2379
- function scenarioMatches(scenario, q, status, tags) {
2380
- if (status !== "all" && scenario.status !== status) return false;
2381
- if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2382
- return queryMatches(scenario, q);
2383
- }
2384
- function summarizeScenarios(scenarios) {
2385
- let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
2386
- for (const s of scenarios) {
2387
- total += 1;
2388
- durationMs += s.durationMs;
2389
- if (s.status === "passed") passed += 1;
2390
- else if (s.status === "failed") failed += 1;
2391
- else if (s.status === "skipped") skipped += 1;
2392
- else pending += 1;
2393
- }
2394
- return { total, passed, failed, skipped, pending, durationMs };
2395
- }
2396
- function filterReport(report, criteria) {
2397
- const c = typeof criteria === "string" ? { query: criteria } : criteria;
2398
- const q = normalizeQuery(c.query ?? "");
2399
- const status = c.status ?? "all";
2400
- const tags = c.tags ?? [];
2401
- if (q === "" && status === "all" && tags.length === 0) return report;
2402
- const features = [];
2403
- let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2404
- for (const feature of report.features) {
2405
- const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2406
- if (matched.length === 0) continue;
2407
- const summary = summarizeScenarios(matched);
2408
- features.push({ ...feature, summary, scenarios: matched });
2409
- topTotal += summary.total;
2410
- topPassed += summary.passed;
2411
- topFailed += summary.failed;
2412
- topSkipped += summary.skipped;
2413
- topPending += summary.pending;
2414
- topDuration += summary.durationMs;
2415
- }
2416
- return {
2417
- ...report,
2418
- summary: {
2419
- total: topTotal,
2420
- passed: topPassed,
2421
- failed: topFailed,
2422
- skipped: topSkipped,
2423
- pending: topPending,
2424
- durationMs: topDuration
2425
- },
2426
- features
2427
- };
2428
- }
2429
- function listFailures(report) {
2430
- const out = [];
2431
- for (const feature of report.features) {
2432
- for (const scenario of feature.scenarios) {
2433
- if (scenario.status === "failed") {
2434
- const ref = {
2435
- featureId: feature.id,
2436
- scenarioId: scenario.id,
2437
- scenarioTitle: scenario.title
2438
- };
2439
- if (scenario.errorMessage !== void 0) {
2440
- ref.errorMessage = scenario.errorMessage;
2441
- }
2442
- out.push(ref);
2443
- }
2444
- }
2445
- }
2446
- return out;
2447
- }
2448
-
2449
- // src/interactive/triage-default.ts
2450
- function needsAttention(status) {
2451
- return status === "failed";
2452
- }
2453
- function defaultCollapsedIds(report) {
2454
- const collapsed = /* @__PURE__ */ new Set();
2455
- if (report.summary.failed === 0) return collapsed;
2456
- for (const feature of report.features) {
2457
- const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2458
- if (!featureHasFailure) collapsed.add(feature.id);
2459
- for (const scenario of feature.scenarios) {
2460
- if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2461
- }
2462
- }
2463
- return collapsed;
2464
- }
2465
- function failuresFirst(report) {
2466
- if (report.summary.failed === 0) return report;
2467
- const withFailures = [];
2468
- const rest = [];
2469
- for (const feature of report.features) {
2470
- (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2471
- }
2472
- if (withFailures.length === 0) return report;
2473
- return { ...report, features: [...withFailures, ...rest] };
2456
+ });
2474
2457
  }
2475
2458
 
2476
- // src/interactive/ReportFilters.tsx
2477
- var import_react14 = require("react");
2478
- var import_jsx_runtime45 = require("react/jsx-runtime");
2479
- var TAG_LIMIT = 12;
2480
- function ReportFilters({
2481
- statuses,
2482
- status,
2483
- onStatus,
2484
- tags,
2485
- activeTags,
2486
- onToggleTag
2487
- }) {
2488
- const [showAllTags, setShowAllTags] = (0, import_react14.useState)(false);
2489
- if (statuses.length === 0 && tags.length === 0) return null;
2490
- const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2491
- const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2492
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col gap-2", children: [
2493
- statuses.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2494
- const active = status === opt.key;
2495
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
2496
- "button",
2497
- {
2498
- type: "button",
2499
- "aria-pressed": active,
2500
- onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
2501
- className: cn(
2502
- "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
2503
- active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
2504
- ),
2505
- children: [
2506
- opt.label,
2507
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2508
- ]
2509
- },
2510
- opt.key
2511
- );
2512
- }) }) : null,
2513
- tags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2514
- visibleTags.map((tag) => {
2515
- const active = activeTags.includes(tag);
2516
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2517
- "button",
2518
- {
2519
- type: "button",
2520
- "aria-pressed": active,
2521
- onClick: () => onToggleTag(tag),
2522
- className: "cursor-pointer",
2523
- children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2524
- Badge,
2525
- {
2526
- variant: "tag",
2527
- className: cn(
2528
- active && "border-primary bg-primary text-primary-foreground"
2529
- ),
2530
- children: tag
2531
- }
2532
- )
2533
- }
2534
- ) }, tag);
2535
- }),
2536
- tags.length > TAG_LIMIT ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
2537
- "button",
2538
- {
2539
- type: "button",
2540
- "aria-expanded": showAllTags,
2541
- onClick: () => setShowAllTags((v) => !v),
2542
- className: "cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2543
- children: tagsCollapsed ? `+${tags.length - TAG_LIMIT} more` : "Show fewer"
2544
- }
2545
- ) }) : null
2546
- ] }) : null
2547
- ] });
2459
+ // src/interactive/ReportShortcutsHelp.tsx
2460
+ var import_jsx_runtime46 = require("react/jsx-runtime");
2461
+ var SHORTCUTS = [
2462
+ { keys: "/", description: "Focus search" },
2463
+ { keys: "f", description: "Jump to next failure" },
2464
+ { keys: "Shift F", description: "Jump to previous failure" },
2465
+ { keys: "e", description: "Expand all" },
2466
+ { keys: "c", description: "Collapse all" },
2467
+ { keys: "?", description: "Toggle this help" },
2468
+ { keys: "Esc", description: "Clear search / close" }
2469
+ ];
2470
+ function Keys({ keys }) {
2471
+ const parts = keys.split(" ");
2472
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
2473
+ i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2474
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2475
+ ] }, k)) });
2476
+ }
2477
+ function ReportShortcutsHelp({ open, onClose }) {
2478
+ const dialogRef = (0, import_react14.useRef)(null);
2479
+ (0, import_react14.useEffect)(() => {
2480
+ const dialog = dialogRef.current;
2481
+ if (!dialog) return;
2482
+ if (open && !dialog.open) dialog.showModal();
2483
+ else if (!open && dialog.open) dialog.close();
2484
+ }, [open]);
2485
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2486
+ "dialog",
2487
+ {
2488
+ ref: dialogRef,
2489
+ className: "es-shortcuts-help m-auto w-[min(90vw,28rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2490
+ "aria-label": "Keyboard shortcuts",
2491
+ onClose,
2492
+ children: [
2493
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2494
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2495
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("dt", { className: "justify-self-start", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(Keys, { keys: s.keys }) }),
2496
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("dd", { className: "text-sm text-muted-foreground", children: s.description })
2497
+ ] }, s.keys)) }),
2498
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2499
+ ]
2500
+ }
2501
+ );
2548
2502
  }
2549
2503
 
2550
2504
  // src/interactive/ReportToc.tsx
2551
2505
  var import_react15 = require("react");
2552
- var import_jsx_runtime46 = require("react/jsx-runtime");
2506
+ var import_jsx_runtime47 = require("react/jsx-runtime");
2553
2507
  var DOT = {
2554
2508
  passed: "bg-pass",
2555
2509
  failed: "bg-fail",
@@ -2578,11 +2532,11 @@ function TocContent({ onNavigate }) {
2578
2532
  return () => observer.disconnect();
2579
2533
  }, [scenarioIds]);
2580
2534
  if (report.features.length === 0) return null;
2581
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("li", { children: [
2582
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2583
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2535
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("li", { children: [
2536
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2537
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2584
2538
  const active = activeId === scenario.id;
2585
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2539
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
2586
2540
  "a",
2587
2541
  {
2588
2542
  href: `#${scenario.id}`,
@@ -2597,14 +2551,14 @@ function TocContent({ onNavigate }) {
2597
2551
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
2598
2552
  ),
2599
2553
  children: [
2600
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
2554
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2601
2555
  "span",
2602
2556
  {
2603
2557
  "aria-hidden": true,
2604
2558
  className: cn("mt-1 size-1.5 shrink-0 rounded-full", DOT[scenario.status])
2605
2559
  }
2606
2560
  ),
2607
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "break-words", children: scenario.title })
2561
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "break-words", children: scenario.title })
2608
2562
  ]
2609
2563
  }
2610
2564
  ) }, scenario.id);
@@ -2614,14 +2568,14 @@ function TocContent({ onNavigate }) {
2614
2568
  function ReportToc() {
2615
2569
  const report = useReport();
2616
2570
  if (report.features.length === 0) return null;
2617
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(
2571
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
2618
2572
  "nav",
2619
2573
  {
2620
2574
  "aria-label": "Table of contents",
2621
2575
  className: "sticky top-4 hidden h-fit w-64 shrink-0 self-start text-xs lg:block",
2622
2576
  children: [
2623
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2624
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(TocContent, {})
2577
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2578
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(TocContent, {})
2625
2579
  ]
2626
2580
  }
2627
2581
  );
@@ -2631,38 +2585,139 @@ function ReportToc() {
2631
2585
  var import_react16 = require("react");
2632
2586
  var import_dialog = require("@base-ui/react/dialog");
2633
2587
  var import_lucide_react4 = require("lucide-react");
2634
- var import_jsx_runtime47 = require("react/jsx-runtime");
2588
+ var import_jsx_runtime48 = require("react/jsx-runtime");
2635
2589
  function ReportTocDrawer() {
2636
2590
  const report = useReport();
2637
2591
  const [open, setOpen] = (0, import_react16.useState)(false);
2638
2592
  if (report.features.length === 0) return null;
2639
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_dialog.Dialog.Root, { open, onOpenChange: setOpen, children: [
2640
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_dialog.Dialog.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2641
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react4.ListIcon, { "aria-hidden": true }),
2593
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_dialog.Dialog.Root, { open, onOpenChange: setOpen, children: [
2594
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_dialog.Dialog.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2595
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react4.ListIcon, { "aria-hidden": true }),
2642
2596
  " Contents"
2643
2597
  ] }),
2644
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_dialog.Dialog.Portal, { children: [
2645
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_dialog.Dialog.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2646
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_dialog.Dialog.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2647
- /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex items-center justify-between", children: [
2648
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_dialog.Dialog.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2649
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
2598
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_dialog.Dialog.Portal, { children: [
2599
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_dialog.Dialog.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_dialog.Dialog.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2601
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center justify-between", children: [
2602
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_dialog.Dialog.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2603
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2650
2604
  import_dialog.Dialog.Close,
2651
2605
  {
2652
2606
  "aria-label": "Close contents",
2653
2607
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
2654
- children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react4.XIcon, { "aria-hidden": true })
2608
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_lucide_react4.XIcon, { "aria-hidden": true })
2655
2609
  }
2656
2610
  )
2657
2611
  ] }),
2658
- /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(TocContent, { onNavigate: () => setOpen(false) })
2612
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(TocContent, { onNavigate: () => setOpen(false) })
2659
2613
  ] })
2660
2614
  ] })
2661
2615
  ] });
2662
2616
  }
2663
2617
 
2664
- // src/interactive/use-theme.ts
2618
+ // src/interactive/triage-default.ts
2619
+ function needsAttention(status) {
2620
+ return status === "failed";
2621
+ }
2622
+ function defaultCollapsedIds(report) {
2623
+ const collapsed = /* @__PURE__ */ new Set();
2624
+ if (report.summary.failed === 0) return collapsed;
2625
+ for (const feature of report.features) {
2626
+ const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2627
+ if (!featureHasFailure) collapsed.add(feature.id);
2628
+ for (const scenario of feature.scenarios) {
2629
+ if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2630
+ }
2631
+ }
2632
+ return collapsed;
2633
+ }
2634
+ function failuresFirst(report) {
2635
+ if (report.summary.failed === 0) return report;
2636
+ const withFailures = [];
2637
+ const rest = [];
2638
+ for (const feature of report.features) {
2639
+ (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2640
+ }
2641
+ if (withFailures.length === 0) return report;
2642
+ return { ...report, features: [...withFailures, ...rest] };
2643
+ }
2644
+
2645
+ // src/interactive/use-deep-link-scroll.ts
2665
2646
  var import_react17 = require("react");
2647
+ function useDeepLinkScroll() {
2648
+ const lastAnchor = (0, import_react17.useRef)(null);
2649
+ (0, import_react17.useEffect)(() => {
2650
+ function scrollToHash() {
2651
+ const { anchor } = splitHash(window.location.hash);
2652
+ if (!anchor || anchor === lastAnchor.current) return;
2653
+ lastAnchor.current = anchor;
2654
+ requestAnimationFrame(() => {
2655
+ scrollToScenarioId(anchor, { updateHash: false });
2656
+ });
2657
+ }
2658
+ scrollToHash();
2659
+ window.addEventListener("hashchange", scrollToHash);
2660
+ return () => window.removeEventListener("hashchange", scrollToHash);
2661
+ }, []);
2662
+ }
2663
+
2664
+ // src/interactive/use-keyboard-shortcuts.ts
2665
+ var import_react18 = require("react");
2666
+ function isEditableTarget(target) {
2667
+ if (!(target instanceof HTMLElement)) return false;
2668
+ const tag = target.tagName;
2669
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2670
+ if (target.isContentEditable) return true;
2671
+ return false;
2672
+ }
2673
+ function useKeyboardShortcuts(handlers) {
2674
+ const ref = (0, import_react18.useRef)(handlers);
2675
+ ref.current = handlers;
2676
+ (0, import_react18.useEffect)(() => {
2677
+ function onKeyDown(e) {
2678
+ const h = ref.current;
2679
+ if (e.key === "Escape") {
2680
+ h.onEscape?.();
2681
+ return;
2682
+ }
2683
+ if (isEditableTarget(e.target)) return;
2684
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
2685
+ switch (e.key) {
2686
+ case "/":
2687
+ h.onFocusSearch?.();
2688
+ e.preventDefault();
2689
+ return;
2690
+ case "?":
2691
+ h.onToggleHelp?.();
2692
+ e.preventDefault();
2693
+ return;
2694
+ case "f":
2695
+ h.onNextFailure?.();
2696
+ e.preventDefault();
2697
+ return;
2698
+ case "F":
2699
+ h.onPrevFailure?.();
2700
+ e.preventDefault();
2701
+ return;
2702
+ case "e":
2703
+ h.onExpandAll?.();
2704
+ e.preventDefault();
2705
+ return;
2706
+ case "c":
2707
+ h.onCollapseAll?.();
2708
+ e.preventDefault();
2709
+ return;
2710
+ default:
2711
+ return;
2712
+ }
2713
+ }
2714
+ window.addEventListener("keydown", onKeyDown);
2715
+ return () => window.removeEventListener("keydown", onKeyDown);
2716
+ }, []);
2717
+ }
2718
+
2719
+ // src/interactive/use-theme.ts
2720
+ var import_react19 = require("react");
2666
2721
  var KEY = "es-theme";
2667
2722
  function systemDark() {
2668
2723
  return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
@@ -2683,13 +2738,13 @@ function initialPref() {
2683
2738
  return "system";
2684
2739
  }
2685
2740
  function useTheme() {
2686
- const [pref, setPrefState] = (0, import_react17.useState)("system");
2687
- (0, import_react17.useEffect)(() => {
2741
+ const [pref, setPrefState] = (0, import_react19.useState)("system");
2742
+ (0, import_react19.useEffect)(() => {
2688
2743
  const p = initialPref();
2689
2744
  setPrefState(p);
2690
2745
  applyTheme(p);
2691
2746
  }, []);
2692
- (0, import_react17.useEffect)(() => {
2747
+ (0, import_react19.useEffect)(() => {
2693
2748
  if (typeof matchMedia === "undefined") return;
2694
2749
  const mq = matchMedia("(prefers-color-scheme: dark)");
2695
2750
  const onChange = () => {
@@ -2698,7 +2753,7 @@ function useTheme() {
2698
2753
  mq.addEventListener("change", onChange);
2699
2754
  return () => mq.removeEventListener("change", onChange);
2700
2755
  }, [pref]);
2701
- const setPref = (0, import_react17.useCallback)((p) => {
2756
+ const setPref = (0, import_react19.useCallback)((p) => {
2702
2757
  setPrefState(p);
2703
2758
  applyTheme(p);
2704
2759
  try {
@@ -2710,19 +2765,19 @@ function useTheme() {
2710
2765
  }
2711
2766
 
2712
2767
  // src/interactive/use-url-state.ts
2713
- var import_react18 = require("react");
2768
+ var import_react20 = require("react");
2714
2769
  var WRITE_DELAY_MS = 250;
2715
2770
  function useUrlState() {
2716
- const [state, setState] = (0, import_react18.useState)(DEFAULT_URL_STATE);
2717
- const ready = (0, import_react18.useRef)(false);
2718
- (0, import_react18.useEffect)(() => {
2771
+ const [state, setState] = (0, import_react20.useState)(DEFAULT_URL_STATE);
2772
+ const ready = (0, import_react20.useRef)(false);
2773
+ (0, import_react20.useEffect)(() => {
2719
2774
  const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2720
2775
  read();
2721
2776
  ready.current = true;
2722
2777
  window.addEventListener("hashchange", read);
2723
2778
  return () => window.removeEventListener("hashchange", read);
2724
2779
  }, []);
2725
- (0, import_react18.useEffect)(() => {
2780
+ (0, import_react20.useEffect)(() => {
2726
2781
  if (!ready.current) return;
2727
2782
  const timer = setTimeout(
2728
2783
  () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
@@ -2730,7 +2785,7 @@ function useUrlState() {
2730
2785
  );
2731
2786
  return () => clearTimeout(timer);
2732
2787
  }, [state]);
2733
- const update = (0, import_react18.useCallback)(
2788
+ const update = (0, import_react20.useCallback)(
2734
2789
  (patch) => setState((prev) => ({ ...prev, ...patch })),
2735
2790
  []
2736
2791
  );
@@ -2738,22 +2793,33 @@ function useUrlState() {
2738
2793
  }
2739
2794
 
2740
2795
  // src/interactive/ReportInteractive.tsx
2741
- var import_jsx_runtime48 = require("react/jsx-runtime");
2796
+ var import_jsx_runtime49 = require("react/jsx-runtime");
2742
2797
  function ReportInteractive(props) {
2743
2798
  const { report, className, title, dataTheme } = props;
2744
2799
  const result = unwrapReport(report);
2745
2800
  if (!result.ok) {
2746
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportErrorShell, { error: result.error, className, title, dataTheme });
2801
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2802
+ ReportErrorShell,
2803
+ {
2804
+ error: result.error,
2805
+ className,
2806
+ title,
2807
+ dataTheme
2808
+ }
2809
+ );
2747
2810
  }
2748
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportInteractiveView, { ...props, report: result.data });
2811
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportInteractiveView, { ...props, report: result.data });
2749
2812
  }
2750
2813
  var THEME_OPTIONS = [
2751
2814
  { value: "light", label: "Light", Icon: import_lucide_react5.Sun },
2752
2815
  { value: "system", label: "System", Icon: import_lucide_react5.Monitor },
2753
2816
  { value: "dark", label: "Dark", Icon: import_lucide_react5.Moon }
2754
2817
  ];
2755
- function ThemeSegment({ pref, onPref }) {
2756
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
2818
+ function ThemeSegment({
2819
+ pref,
2820
+ onPref
2821
+ }) {
2822
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2757
2823
  ToggleGroup,
2758
2824
  {
2759
2825
  size: "sm",
@@ -2763,7 +2829,17 @@ function ThemeSegment({ pref, onPref }) {
2763
2829
  if (next) onPref(next);
2764
2830
  },
2765
2831
  "aria-label": "Color theme",
2766
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ToggleGroupItem, { value, "aria-label": label, title: label, className: "px-2", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(Icon, {}) }, value))
2832
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2833
+ ToggleGroupItem,
2834
+ {
2835
+ value,
2836
+ "aria-label": label,
2837
+ title: label,
2838
+ className: "px-2",
2839
+ children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Icon, {})
2840
+ },
2841
+ value
2842
+ ))
2767
2843
  }
2768
2844
  );
2769
2845
  }
@@ -2781,63 +2857,79 @@ function ReportInteractiveView({
2781
2857
  }) {
2782
2858
  const [urlState, setUrlState] = useUrlState();
2783
2859
  const { query, status: statusFilter, tags: activeTags, detail } = urlState;
2784
- const setQuery = (0, import_react19.useCallback)((next) => setUrlState({ query: next }), [setUrlState]);
2785
- const setStatusFilter = (0, import_react19.useCallback)(
2860
+ const setQuery = (0, import_react21.useCallback)(
2861
+ (next) => setUrlState({ query: next }),
2862
+ [setUrlState]
2863
+ );
2864
+ const setStatusFilter = (0, import_react21.useCallback)(
2786
2865
  (next) => setUrlState({ status: next }),
2787
2866
  [setUrlState]
2788
2867
  );
2789
- const setDetail = (0, import_react19.useCallback)(
2868
+ const setDetail = (0, import_react21.useCallback)(
2790
2869
  (next) => setUrlState({ detail: next }),
2791
2870
  [setUrlState]
2792
2871
  );
2793
- const [helpOpen, setHelpOpen] = (0, import_react19.useState)(false);
2872
+ const [helpOpen, setHelpOpen] = (0, import_react21.useState)(false);
2794
2873
  const { pref: themePref, setPref: setThemePref } = useTheme();
2795
- const searchRef = (0, import_react19.useRef)(null);
2796
- const expandId = (0, import_react19.useId)();
2797
- const docsId = (0, import_react19.useId)();
2798
- const deferredQuery = (0, import_react19.useDeferredValue)(query);
2799
- const failures = (0, import_react19.useMemo)(() => listFailures(report), [report]);
2800
- const filtered = (0, import_react19.useMemo)(
2874
+ const searchRef = (0, import_react21.useRef)(null);
2875
+ const expandId = (0, import_react21.useId)();
2876
+ const docsId = (0, import_react21.useId)();
2877
+ const deferredQuery = (0, import_react21.useDeferredValue)(query);
2878
+ const failures = (0, import_react21.useMemo)(() => listFailures(report), [report]);
2879
+ const filtered = (0, import_react21.useMemo)(
2801
2880
  // Features carrying failures float to the top so the first thing on screen
2802
2881
  // is the thing that needs attention; an all-green run keeps source order.
2803
- () => failuresFirst(filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags })),
2882
+ () => failuresFirst(
2883
+ filterReport(report, {
2884
+ query: deferredQuery,
2885
+ status: statusFilter,
2886
+ tags: activeTags
2887
+ })
2888
+ ),
2804
2889
  [report, deferredQuery, statusFilter, activeTags]
2805
2890
  );
2806
2891
  const isFiltering = query !== deferredQuery;
2807
- const statusOptions = (0, import_react19.useMemo)(() => {
2892
+ const statusOptions = (0, import_react21.useMemo)(() => {
2808
2893
  const s = report.summary;
2809
2894
  const opts = [
2810
2895
  { key: "all", label: "All", count: s.total }
2811
2896
  ];
2812
- if (s.passed) opts.push({ key: "passed", label: "Passed", count: s.passed });
2813
- if (s.failed) opts.push({ key: "failed", label: "Failed", count: s.failed });
2814
- if (s.skipped) opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2815
- if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
2897
+ if (s.passed)
2898
+ opts.push({ key: "passed", label: "Passed", count: s.passed });
2899
+ if (s.failed)
2900
+ opts.push({ key: "failed", label: "Failed", count: s.failed });
2901
+ if (s.skipped)
2902
+ opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2903
+ if (s.pending)
2904
+ opts.push({ key: "pending", label: "Pending", count: s.pending });
2816
2905
  return opts;
2817
2906
  }, [report]);
2818
- const tagOptions = (0, import_react19.useMemo)(() => allTags(report), [report]);
2819
- const toggleTag = (0, import_react19.useCallback)(
2907
+ const tagOptions = (0, import_react21.useMemo)(() => allTags(report), [report]);
2908
+ const toggleTag = (0, import_react21.useCallback)(
2820
2909
  (tag) => setUrlState({
2821
2910
  tags: activeTags.includes(tag) ? activeTags.filter((t) => t !== tag) : [...activeTags, tag]
2822
2911
  }),
2823
2912
  [activeTags, setUrlState]
2824
2913
  );
2825
- const [toast, setToast] = (0, import_react19.useState)(null);
2826
- const toastTimer = (0, import_react19.useRef)(null);
2827
- const showToast = (0, import_react19.useCallback)((msg) => {
2914
+ const [toast, setToast] = (0, import_react21.useState)(null);
2915
+ const toastTimer = (0, import_react21.useRef)(null);
2916
+ const showToast = (0, import_react21.useCallback)((msg) => {
2828
2917
  setToast(msg);
2829
2918
  if (toastTimer.current) clearTimeout(toastTimer.current);
2830
2919
  toastTimer.current = setTimeout(() => setToast(null), 1800);
2831
2920
  }, []);
2832
- const copy = (0, import_react19.useCallback)(
2921
+ const copy = (0, import_react21.useCallback)(
2833
2922
  (text, msg) => {
2834
2923
  if (typeof navigator !== "undefined" && navigator.clipboard) {
2835
- navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
2924
+ navigator.clipboard.writeText(text).then(
2925
+ () => showToast(msg),
2926
+ () => showToast("Copy failed")
2927
+ );
2836
2928
  }
2837
2929
  },
2838
2930
  [showToast]
2839
2931
  );
2840
- const scenarioActions = (0, import_react19.useMemo)(
2932
+ const scenarioActions = (0, import_react21.useMemo)(
2841
2933
  () => ({
2842
2934
  copyLink: (s) => copy(scenarioPermalink(s), "Link copied"),
2843
2935
  copyMarkdown: (s) => copy(scenarioToMarkdown(s), "Markdown copied"),
@@ -2845,15 +2937,27 @@ function ReportInteractiveView({
2845
2937
  }),
2846
2938
  [copy]
2847
2939
  );
2848
- const failureIndexRef = (0, import_react19.useRef)(0);
2849
- const allCollapsibleIds = (0, import_react19.useMemo)(
2940
+ const failureIndexRef = (0, import_react21.useRef)(0);
2941
+ const allCollapsibleIds = (0, import_react21.useMemo)(
2850
2942
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
2851
2943
  [report]
2852
2944
  );
2853
- const collapse = useCollapseState((0, import_react19.useCallback)(() => defaultCollapsedIds(report), [report]));
2854
- const collapseAll = (0, import_react19.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2855
- const [expandedAll, setExpandedAll] = (0, import_react19.useState)(() => report.summary.failed === 0);
2856
- const setExpanded = (0, import_react19.useCallback)(
2945
+ const collapse = useCollapseState(
2946
+ (0, import_react21.useCallback)(() => defaultCollapsedIds(report), [report])
2947
+ );
2948
+ const revealFilteredResults = deferredQuery.trim() !== "" || statusFilter !== "all" || activeTags.length > 0;
2949
+ const visibleCollapse = (0, import_react21.useMemo)(
2950
+ () => revealFilteredResults ? { ...collapse.api, isCollapsed: () => false } : collapse.api,
2951
+ [collapse.api, revealFilteredResults]
2952
+ );
2953
+ const collapseAll = (0, import_react21.useCallback)(
2954
+ () => collapse.collapseAll(allCollapsibleIds),
2955
+ [collapse, allCollapsibleIds]
2956
+ );
2957
+ const [expandedAll, setExpandedAll] = (0, import_react21.useState)(
2958
+ () => report.summary.failed === 0
2959
+ );
2960
+ const setExpanded = (0, import_react21.useCallback)(
2857
2961
  (expanded) => {
2858
2962
  if (expanded) collapse.expandAll();
2859
2963
  else collapseAll();
@@ -2861,10 +2965,10 @@ function ReportInteractiveView({
2861
2965
  },
2862
2966
  [collapse, collapseAll]
2863
2967
  );
2864
- const focusSearch = (0, import_react19.useCallback)(() => {
2968
+ const focusSearch = (0, import_react21.useCallback)(() => {
2865
2969
  searchRef.current?.focus();
2866
2970
  }, []);
2867
- const stepFailure = (0, import_react19.useCallback)(
2971
+ const stepFailure = (0, import_react21.useCallback)(
2868
2972
  (direction) => {
2869
2973
  if (failures.length === 0) return;
2870
2974
  failureIndexRef.current = (failureIndexRef.current + direction + failures.length) % failures.length;
@@ -2873,13 +2977,13 @@ function ReportInteractiveView({
2873
2977
  },
2874
2978
  [failures]
2875
2979
  );
2876
- const toggleHelp = (0, import_react19.useCallback)(() => {
2980
+ const toggleHelp = (0, import_react21.useCallback)(() => {
2877
2981
  setHelpOpen((v) => !v);
2878
2982
  }, []);
2879
- const escape = (0, import_react19.useCallback)(() => {
2983
+ const escape = (0, import_react21.useCallback)(() => {
2880
2984
  if (helpOpen) setHelpOpen(false);
2881
2985
  else if (query !== "") setQuery("");
2882
- }, [helpOpen, query]);
2986
+ }, [helpOpen, query, setQuery]);
2883
2987
  useKeyboardShortcuts({
2884
2988
  onFocusSearch: focusSearch,
2885
2989
  onNextFailure: () => stepFailure(1),
@@ -2894,13 +2998,13 @@ function ReportInteractiveView({
2894
2998
  const totalScenarios = report.summary.total;
2895
2999
  const matchedScenarios = filtered.summary.total;
2896
3000
  const showToc = !hideToc && totalScenarios > 3;
2897
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3001
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2898
3002
  ReportRoot,
2899
3003
  {
2900
3004
  report: filtered,
2901
3005
  customRenderers,
2902
3006
  renderers,
2903
- children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3007
+ children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
2904
3008
  "main",
2905
3009
  {
2906
3010
  className: cn("es-report", "es-report-interactive", className),
@@ -2909,8 +3013,8 @@ function ReportInteractiveView({
2909
3013
  "data-theme": dataTheme,
2910
3014
  "data-detail-level": detail,
2911
3015
  children: [
2912
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("header", { className: "es-report-header", children: [
2913
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3016
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("header", { className: "es-report-header", children: [
3017
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
2914
3018
  "div",
2915
3019
  {
2916
3020
  "data-es-navbar": true,
@@ -2919,9 +3023,9 @@ function ReportInteractiveView({
2919
3023
  hideHeader ? "justify-end" : "justify-between"
2920
3024
  ),
2921
3025
  children: [
2922
- hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2923
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
2924
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3026
+ hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("h1", { className: "min-w-0", children: title ?? "Story Report" }),
3027
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex items-center gap-2", children: [
3028
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2925
3029
  ReportSearch,
2926
3030
  {
2927
3031
  ref: searchRef,
@@ -2931,16 +3035,16 @@ function ReportInteractiveView({
2931
3035
  totalCount: totalScenarios
2932
3036
  }
2933
3037
  ),
2934
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ThemeSegment, { pref: themePref, onPref: setThemePref })
3038
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ThemeSegment, { pref: themePref, onPref: setThemePref })
2935
3039
  ] })
2936
3040
  ]
2937
3041
  }
2938
3042
  ),
2939
- hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_jsx_runtime48.Fragment, { children: [
2940
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportSummary, {}),
2941
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportMeta, {})
3043
+ hideHeader ? null : /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
3044
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportSummary, {}),
3045
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportMeta, {})
2942
3046
  ] }),
2943
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3047
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2944
3048
  ReportFreshness,
2945
3049
  {
2946
3050
  lastRunMs: reportLastRunMs(report),
@@ -2948,8 +3052,14 @@ function ReportInteractiveView({
2948
3052
  staleAfterDays
2949
3053
  }
2950
3054
  ),
2951
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportLastRunDelta, { history: scenarioHistory, report }),
2952
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3055
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3056
+ ReportLastRunDelta,
3057
+ {
3058
+ history: scenarioHistory,
3059
+ report
3060
+ }
3061
+ ),
3062
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2953
3063
  ReportFilters,
2954
3064
  {
2955
3065
  statuses: statusOptions,
@@ -2960,14 +3070,28 @@ function ReportInteractiveView({
2960
3070
  onToggleTag: toggleTag
2961
3071
  }
2962
3072
  ),
2963
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
2964
- showToc ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportTocDrawer, {}) : null,
2965
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
2966
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(Switch, { id: expandId, checked: expandedAll, onCheckedChange: setExpanded }),
2967
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("label", { htmlFor: expandId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Expand all" })
3073
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3074
+ showToc ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportTocDrawer, {}) : null,
3075
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex items-center gap-2", children: [
3076
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3077
+ Switch,
3078
+ {
3079
+ id: expandId,
3080
+ checked: expandedAll,
3081
+ onCheckedChange: setExpanded
3082
+ }
3083
+ ),
3084
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3085
+ "label",
3086
+ {
3087
+ htmlFor: expandId,
3088
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3089
+ children: "Expand all"
3090
+ }
3091
+ )
2968
3092
  ] }),
2969
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
2970
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3093
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex items-center gap-2", children: [
3094
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2971
3095
  Switch,
2972
3096
  {
2973
3097
  id: docsId,
@@ -2975,16 +3099,28 @@ function ReportInteractiveView({
2975
3099
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
2976
3100
  }
2977
3101
  ),
2978
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("label", { htmlFor: docsId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Show documentation" })
3102
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3103
+ "label",
3104
+ {
3105
+ htmlFor: docsId,
3106
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3107
+ children: "Show documentation"
3108
+ }
3109
+ )
2979
3110
  ] })
2980
3111
  ] })
2981
3112
  ] }),
2982
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportFailureBanner, { failures }),
2983
- hasContent ? showToc ? /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex gap-6", children: [
2984
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportToc, {}),
2985
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportFeatureList, {}) })
2986
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportFeatureList, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
2987
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3113
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportFailureBanner, { failures }),
3114
+ hasContent ? showToc ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "flex gap-6", children: [
3115
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportToc, {}),
3116
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportFeatureList, {}) })
3117
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ReportFeatureList, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3118
+ ReportEmpty,
3119
+ {
3120
+ message: query ? "No scenarios match the search." : void 0
3121
+ }
3122
+ ),
3123
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
2988
3124
  "button",
2989
3125
  {
2990
3126
  type: "button",
@@ -2995,8 +3131,14 @@ function ReportInteractiveView({
2995
3131
  children: "?"
2996
3132
  }
2997
3133
  ),
2998
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
2999
- toast ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3134
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3135
+ ReportShortcutsHelp,
3136
+ {
3137
+ open: helpOpen,
3138
+ onClose: () => setHelpOpen(false)
3139
+ }
3140
+ ),
3141
+ toast ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3000
3142
  "div",
3001
3143
  {
3002
3144
  role: "status",
@@ -3009,18 +3151,18 @@ function ReportInteractiveView({
3009
3151
  }
3010
3152
  )
3011
3153
  }
3012
- ) }) }) });
3154
+ ) }) }) }) });
3013
3155
  }
3014
3156
 
3015
3157
  // src/interactive/ReportInteractiveIsland.tsx
3016
- var import_react22 = require("react");
3158
+ var import_react24 = require("react");
3017
3159
 
3018
3160
  // src/interactive/island-renderers.tsx
3019
- var import_react21 = require("react");
3161
+ var import_react23 = require("react");
3020
3162
 
3021
3163
  // src/components/doc/MermaidView.tsx
3022
- var import_react20 = require("react");
3023
- var import_jsx_runtime49 = require("react/jsx-runtime");
3164
+ var import_react22 = require("react");
3165
+ var import_jsx_runtime50 = require("react/jsx-runtime");
3024
3166
  function prefersDark(el) {
3025
3167
  const themed = el?.closest("[data-theme]");
3026
3168
  if (themed?.dataset.theme === "dark") return true;
@@ -3028,10 +3170,10 @@ function prefersDark(el) {
3028
3170
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
3029
3171
  }
3030
3172
  function MermaidView({ entry, load }) {
3031
- const ref = (0, import_react20.useRef)(null);
3032
- const [svg, setSvg] = (0, import_react20.useState)(null);
3033
- const [error, setError] = (0, import_react20.useState)(null);
3034
- (0, import_react20.useEffect)(() => {
3173
+ const ref = (0, import_react22.useRef)(null);
3174
+ const [svg, setSvg] = (0, import_react22.useState)(null);
3175
+ const [error, setError] = (0, import_react22.useState)(null);
3176
+ (0, import_react22.useEffect)(() => {
3035
3177
  let cancelled = false;
3036
3178
  const draw = async () => {
3037
3179
  let mermaid;
@@ -3074,9 +3216,9 @@ function MermaidView({ entry, load }) {
3074
3216
  };
3075
3217
  }, [entry.code, load]);
3076
3218
  if (svg) {
3077
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("figure", { ref, className: "my-3", children: [
3078
- entry.title ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3079
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3219
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("figure", { ref, className: "my-3", children: [
3220
+ entry.title ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3221
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3080
3222
  "div",
3081
3223
  {
3082
3224
  role: "img",
@@ -3087,8 +3229,8 @@ function MermaidView({ entry, load }) {
3087
3229
  )
3088
3230
  ] });
3089
3231
  }
3090
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
3091
- error ? /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
3232
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
3233
+ error ? /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
3092
3234
  "p",
3093
3235
  {
3094
3236
  role: "alert",
@@ -3100,12 +3242,12 @@ function MermaidView({ entry, load }) {
3100
3242
  ]
3101
3243
  }
3102
3244
  ) : null,
3103
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(MermaidSource, { entry, ref })
3245
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(MermaidSource, { entry, ref })
3104
3246
  ] });
3105
3247
  }
3106
3248
 
3107
3249
  // src/interactive/island-renderers.tsx
3108
- var import_jsx_runtime50 = require("react/jsx-runtime");
3250
+ var import_jsx_runtime51 = require("react/jsx-runtime");
3109
3251
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
3110
3252
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
3111
3253
  function cachedCdnModule(url) {
@@ -3122,8 +3264,8 @@ function cachedCdnModule(url) {
3122
3264
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3123
3265
  var loadHljs = cachedCdnModule(HLJS_ESM);
3124
3266
  function CdnHighlightedCode({ entry }) {
3125
- const [html, setHtml] = (0, import_react21.useState)(null);
3126
- (0, import_react21.useEffect)(() => {
3267
+ const [html, setHtml] = (0, import_react23.useState)(null);
3268
+ (0, import_react23.useEffect)(() => {
3127
3269
  let cancelled = false;
3128
3270
  void (async () => {
3129
3271
  try {
@@ -3139,27 +3281,27 @@ function CdnHighlightedCode({ entry }) {
3139
3281
  cancelled = true;
3140
3282
  };
3141
3283
  }, [entry.content, entry.lang]);
3142
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
3284
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3143
3285
  "code",
3144
3286
  {
3145
3287
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
3146
3288
  dangerouslySetInnerHTML: { __html: html }
3147
3289
  }
3148
- ) : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3290
+ ) : /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3149
3291
  }
3150
3292
  function buildIslandRenderers(opts) {
3151
3293
  const renderers = {};
3152
3294
  if (opts.mermaid) {
3153
- renderers.mermaid = (entry) => (0, import_react21.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
3295
+ renderers.mermaid = (entry) => (0, import_react23.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
3154
3296
  }
3155
3297
  if (opts.syntaxHighlighting) {
3156
- renderers.code = (entry) => (0, import_react21.createElement)(CdnHighlightedCode, { entry });
3298
+ renderers.code = (entry) => (0, import_react23.createElement)(CdnHighlightedCode, { entry });
3157
3299
  }
3158
3300
  return renderers;
3159
3301
  }
3160
3302
 
3161
3303
  // src/interactive/ReportInteractiveIsland.tsx
3162
- var import_jsx_runtime51 = require("react/jsx-runtime");
3304
+ var import_jsx_runtime52 = require("react/jsx-runtime");
3163
3305
  function ReportInteractiveIsland({
3164
3306
  report,
3165
3307
  title,
@@ -3169,11 +3311,11 @@ function ReportInteractiveIsland({
3169
3311
  syntaxHighlighting = true,
3170
3312
  mermaid = true
3171
3313
  }) {
3172
- const renderers = (0, import_react22.useMemo)(
3314
+ const renderers = (0, import_react24.useMemo)(
3173
3315
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
3174
3316
  [syntaxHighlighting, mermaid]
3175
3317
  );
3176
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
3318
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
3177
3319
  ReportInteractive,
3178
3320
  {
3179
3321
  report,