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