executable-stories-react 0.10.1 → 0.12.0

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