executable-stories-react 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,45 +1,145 @@
1
1
  "use client";
2
2
 
3
3
  // src/interactive/ReportInteractive.tsx
4
+ import { Monitor, Moon, Sun } from "lucide-react";
4
5
  import {
5
6
  useCallback as useCallback5,
6
7
  useDeferredValue,
7
8
  useId as useId2,
8
9
  useMemo as useMemo3,
9
10
  useRef as useRef5,
10
- useState as useState8
11
+ useState as useState9
11
12
  } from "react";
12
13
 
13
- // src/result.ts
14
- var ok = (data) => ({ ok: true, data });
15
- function unwrapReport(value) {
16
- if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
17
- return value;
14
+ // src/components/ui/empty.tsx
15
+ import { cva } from "class-variance-authority";
16
+
17
+ // src/lib/utils.ts
18
+ import { clsx } from "clsx";
19
+ import { twMerge } from "tailwind-merge";
20
+ function cn(...inputs) {
21
+ return twMerge(clsx(inputs));
22
+ }
23
+
24
+ // src/components/ui/empty.tsx
25
+ import { jsx } from "react/jsx-runtime";
26
+ function Empty({ className, ...props }) {
27
+ return /* @__PURE__ */ jsx(
28
+ "div",
29
+ {
30
+ "data-slot": "empty",
31
+ className: cn(
32
+ "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",
33
+ className
34
+ ),
35
+ ...props
36
+ }
37
+ );
38
+ }
39
+ function EmptyHeader({ className, ...props }) {
40
+ return /* @__PURE__ */ jsx(
41
+ "div",
42
+ {
43
+ "data-slot": "empty-header",
44
+ className: cn(
45
+ "flex max-w-sm flex-col items-center gap-2 text-center",
46
+ className
47
+ ),
48
+ ...props
49
+ }
50
+ );
51
+ }
52
+ var emptyMediaVariants = cva(
53
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
54
+ {
55
+ variants: {
56
+ variant: {
57
+ default: "bg-transparent",
58
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
59
+ }
60
+ },
61
+ defaultVariants: {
62
+ variant: "default"
63
+ }
18
64
  }
19
- return ok(value);
65
+ );
66
+ function EmptyTitle({ className, ...props }) {
67
+ return /* @__PURE__ */ jsx(
68
+ "div",
69
+ {
70
+ "data-slot": "empty-title",
71
+ className: cn("text-lg font-medium tracking-tight", className),
72
+ ...props
73
+ }
74
+ );
20
75
  }
21
76
 
22
- // src/context/ReportRoot.tsx
23
- import { useMemo } from "react";
77
+ // src/components/ReportEmpty.tsx
78
+ import { jsx as jsx2 } from "react/jsx-runtime";
79
+ function ReportEmpty({ message }) {
80
+ return /* @__PURE__ */ jsx2(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx2(EmptyHeader, { children: /* @__PURE__ */ jsx2(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
81
+ }
82
+
83
+ // src/hooks/useReport.ts
84
+ import { useContext } from "react";
24
85
 
25
86
  // src/context/ReportContext.ts
26
87
  import { createContext } from "react";
27
88
  var ReportContext = createContext(null);
28
89
 
29
- // src/components/ui/badge.tsx
30
- import "react";
31
- import { cva } from "class-variance-authority";
32
- import { useRender } from "@base-ui/react/use-render";
90
+ // src/hooks/useReport.ts
91
+ function useReport() {
92
+ const ctx = useContext(ReportContext);
93
+ if (!ctx) {
94
+ throw new Error(
95
+ "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
96
+ );
97
+ }
98
+ return ctx.report;
99
+ }
33
100
 
34
- // src/lib/utils.ts
35
- import { clsx } from "clsx";
36
- import { twMerge } from "tailwind-merge";
37
- function cn(...inputs) {
38
- return twMerge(clsx(inputs));
101
+ // src/components/ReportScenario.tsx
102
+ import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
103
+
104
+ // src/lib/step-params.tsx
105
+ import { jsx as jsx3 } from "react/jsx-runtime";
106
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
107
+ function highlightStepParams(text) {
108
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
109
+ if (matches.length === 0) return text;
110
+ const nodes = [];
111
+ let lastIndex = 0;
112
+ matches.forEach((match, i) => {
113
+ const start = match.index;
114
+ const end = start + match[0].length;
115
+ if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
116
+ nodes.push(
117
+ /* @__PURE__ */ jsx3("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
118
+ );
119
+ lastIndex = end;
120
+ });
121
+ if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
122
+ return nodes;
123
+ }
124
+
125
+ // src/components/ReportSteps.tsx
126
+ import {
127
+ assertionState,
128
+ assertiveSteps
129
+ } from "executable-stories-core/utils/assertive-steps";
130
+ import { formatDuration } from "executable-stories-core/utils/duration";
131
+
132
+ // src/components/doc/DocNote.tsx
133
+ import { jsx as jsx4 } from "react/jsx-runtime";
134
+ function DocNote({ entry }) {
135
+ return /* @__PURE__ */ jsx4("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
39
136
  }
40
137
 
41
138
  // src/components/ui/badge.tsx
42
- var badgeVariants = cva(
139
+ import "react";
140
+ import { cva as cva2 } from "class-variance-authority";
141
+ import { useRender } from "@base-ui/react/use-render";
142
+ var badgeVariants = cva2(
43
143
  "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",
44
144
  {
45
145
  variants: {
@@ -84,342 +184,119 @@ function Badge({
84
184
  });
85
185
  }
86
186
 
87
- // src/components/doc/NarrativeBlocks.tsx
88
- import { jsx, jsxs } from "react/jsx-runtime";
89
- var FILE_TREE_TYPE = "file-tree";
90
- var DATA_MODEL_TYPE = "data-model";
91
- function isChangeKind(value) {
92
- return value === "added" || value === "modified" || value === "removed" || value === "renamed";
187
+ // src/components/doc/DocTag.tsx
188
+ import { jsx as jsx5 } from "react/jsx-runtime";
189
+ function DocTag({ entry }) {
190
+ return /* @__PURE__ */ jsx5("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(Badge, { variant: "tag", children: n }) }, n)) });
93
191
  }
94
- function isRecord(value) {
95
- return typeof value === "object" && value !== null && !Array.isArray(value);
192
+
193
+ // src/components/doc/DocKv.tsx
194
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
195
+ function formatValue(value) {
196
+ if (value === null) return "null";
197
+ if (typeof value === "string") return value;
198
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
199
+ try {
200
+ return JSON.stringify(value);
201
+ } catch {
202
+ return String(value);
203
+ }
96
204
  }
97
- function str(value) {
98
- return typeof value === "string" && value !== "" ? value : void 0;
205
+ function DocKv({ entry }) {
206
+ return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
207
+ /* @__PURE__ */ jsx6("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
208
+ /* @__PURE__ */ jsx6("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
209
+ ] });
99
210
  }
100
- function AuthoredMarker({ authored }) {
101
- if (authored !== "agent") return null;
102
- return /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
211
+
212
+ // src/hooks/useRenderers.ts
213
+ import { useContext as useContext2 } from "react";
214
+ var EMPTY_CUSTOM = {};
215
+ var EMPTY_RENDERERS = {};
216
+ function useCustomRenderers() {
217
+ const ctx = useContext2(ReportContext);
218
+ return ctx?.customRenderers ?? EMPTY_CUSTOM;
103
219
  }
104
- function BlockFrame({
105
- title,
106
- authored,
107
- children
108
- }) {
109
- return /* @__PURE__ */ jsxs("figure", { className: "my-3", children: [
110
- title || authored === "agent" ? /* @__PURE__ */ jsxs("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
111
- title ? /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
112
- /* @__PURE__ */ jsx(AuthoredMarker, { authored })
113
- ] }) : null,
114
- children
220
+ function useBuiltinRenderers() {
221
+ const ctx = useContext2(ReportContext);
222
+ return ctx?.renderers ?? EMPTY_RENDERERS;
223
+ }
224
+
225
+ // src/components/doc/DocCode.tsx
226
+ import { Fragment, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
227
+ function CodeFigure({ label, children }) {
228
+ return /* @__PURE__ */ jsxs2("figure", { className: "my-3", children: [
229
+ label ? /* @__PURE__ */ jsx7("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
230
+ /* @__PURE__ */ jsx7("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
115
231
  ] });
116
232
  }
117
- function ChangeBadge({ change }) {
118
- return /* @__PURE__ */ jsx(Badge, { variant: "tag", className: "text-[10px]", children: change });
233
+ function DocCode({ entry }) {
234
+ const renderers = useBuiltinRenderers();
235
+ if (renderers.code) {
236
+ return /* @__PURE__ */ jsx7(Fragment, { children: renderers.code(entry) });
237
+ }
238
+ return /* @__PURE__ */ jsx7(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx7("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
119
239
  }
120
- function parseFileTree(data) {
121
- if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
122
- const files = [];
123
- for (const raw of data.files) {
124
- if (typeof raw === "string") {
125
- if (raw) files.push({ path: raw });
126
- continue;
240
+
241
+ // src/components/doc/DocTable.tsx
242
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
243
+ function DocTable({ entry }) {
244
+ return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
245
+ entry.label ? /* @__PURE__ */ jsx8("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
246
+ /* @__PURE__ */ jsx8("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs3("table", { className: "w-full border-collapse text-sm", children: [
247
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsx8("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx8("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
248
+ /* @__PURE__ */ jsx8("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx8("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx8("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
249
+ ] }) })
250
+ ] });
251
+ }
252
+
253
+ // src/components/doc/DocLink.tsx
254
+ import { jsx as jsx9 } from "react/jsx-runtime";
255
+ function DocLink({ entry }) {
256
+ return /* @__PURE__ */ jsx9(
257
+ "a",
258
+ {
259
+ className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
260
+ href: entry.url,
261
+ rel: "noreferrer noopener",
262
+ target: "_blank",
263
+ children: entry.label
127
264
  }
128
- if (!isRecord(raw)) continue;
129
- const path = str(raw.path);
130
- if (!path) continue;
131
- files.push({
132
- path,
133
- change: isChangeKind(raw.change) ? raw.change : void 0,
134
- note: str(raw.note)
135
- });
136
- }
137
- if (files.length === 0) return void 0;
138
- return { title: str(data.title), authored: str(data.authored), files };
265
+ );
139
266
  }
140
- function buildTree(files) {
141
- const root = { name: "", children: /* @__PURE__ */ new Map() };
142
- for (const file of files) {
143
- const segments = file.path.split("/").filter(Boolean);
144
- let node = root;
145
- segments.forEach((segment, index) => {
146
- let child = node.children.get(segment);
147
- if (!child) {
148
- child = { name: segment, children: /* @__PURE__ */ new Map() };
149
- node.children.set(segment, child);
150
- }
151
- if (index === segments.length - 1) child.file = file;
152
- node = child;
153
- });
154
- }
155
- return root;
267
+
268
+ // src/lib/markdown.ts
269
+ import { marked } from "marked";
270
+
271
+ // src/lib/url.ts
272
+ import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
273
+
274
+ // src/lib/markdown.ts
275
+ function safeMarkdownHtml(markdown) {
276
+ const raw = marked.parse(markdown, { async: false });
277
+ 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)}'`);
156
278
  }
157
- function sortedChildren(node) {
158
- return [...node.children.values()].sort((a, b) => {
159
- const aDir = a.children.size > 0;
160
- const bDir = b.children.size > 0;
161
- if (aDir !== bDir) return aDir ? -1 : 1;
162
- return a.name.localeCompare(b.name);
163
- });
164
- }
165
- function TreeBranch({ node, depth }) {
166
- const children = sortedChildren(node);
167
- return /* @__PURE__ */ jsx("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
168
- const isDir = child.children.size > 0;
169
- return /* @__PURE__ */ jsxs("li", { children: [
170
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [
171
- /* @__PURE__ */ jsx(
172
- "span",
173
- {
174
- className: cn(
175
- "font-mono text-xs",
176
- isDir ? "text-muted-foreground" : "text-foreground"
177
- ),
178
- children: isDir ? `${child.name}/` : child.name
179
- }
180
- ),
181
- child.file?.change ? /* @__PURE__ */ jsx(ChangeBadge, { change: child.file.change }) : null,
182
- child.file?.note ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
183
- ] }),
184
- isDir ? /* @__PURE__ */ jsx(TreeBranch, { node: child, depth: depth + 1 }) : null
185
- ] }, child.name);
186
- }) });
187
- }
188
- function FileTreeBlock({ entry }) {
189
- const data = parseFileTree(entry.data);
190
- if (!data) return /* @__PURE__ */ jsx(UnreadableBlock, { entry });
191
- return /* @__PURE__ */ jsx(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ jsx("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ jsx(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
192
- }
193
- function parseDataModel(data) {
194
- if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
195
- const fields = [];
196
- for (const raw of data.fields) {
197
- if (!isRecord(raw)) continue;
198
- const name = str(raw.name);
199
- if (!name) continue;
200
- fields.push({
201
- name,
202
- type: str(raw.type),
203
- note: str(raw.note),
204
- change: isChangeKind(raw.change) ? raw.change : void 0
205
- });
206
- }
207
- if (fields.length === 0) return void 0;
208
- return {
209
- title: str(data.title),
210
- authored: str(data.authored),
211
- name: str(data.name),
212
- fields
213
- };
214
- }
215
- function DataModelBlock({ entry }) {
216
- const data = parseDataModel(entry.data);
217
- if (!data) return /* @__PURE__ */ jsx(UnreadableBlock, { entry });
218
- const hasNotes = data.fields.some((f) => f.note);
219
- return /* @__PURE__ */ jsx(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ jsx("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-sm", children: [
220
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-muted/50", children: [
221
- /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
222
- /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
223
- hasNotes ? /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
224
- ] }) }),
225
- /* @__PURE__ */ jsx("tbody", { children: data.fields.map((field) => /* @__PURE__ */ jsxs("tr", { children: [
226
- /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [
227
- /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-foreground", children: field.name }),
228
- field.change ? /* @__PURE__ */ jsx(ChangeBadge, { change: field.change }) : null
229
- ] }) }),
230
- /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
231
- hasNotes ? /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
232
- ] }, field.name)) })
233
- ] }) }) });
234
- }
235
- function UnreadableBlock({ entry }) {
236
- return /* @__PURE__ */ jsxs("div", { className: "my-2", "data-type": entry.type, children: [
237
- /* @__PURE__ */ jsxs("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
238
- entry.type,
239
- " (unrecognised shape)"
240
- ] }),
241
- /* @__PURE__ */ jsx(
242
- "pre",
243
- {
244
- tabIndex: 0,
245
- className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
246
- children: safeStringify(entry.data)
247
- }
248
- )
249
- ] });
250
- }
251
- function safeStringify(value) {
252
- try {
253
- return JSON.stringify(value, null, 2);
254
- } catch {
255
- return String(value);
256
- }
257
- }
258
- var narrativeBlockRenderers = {
259
- [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ jsx(FileTreeBlock, { entry }),
260
- [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ jsx(DataModelBlock, { entry })
261
- };
262
-
263
- // src/context/ReportRoot.tsx
264
- import { jsx as jsx2 } from "react/jsx-runtime";
265
- var EMPTY_RENDERERS = {};
266
- function ReportRoot({
267
- report,
268
- customRenderers,
269
- renderers,
270
- children
271
- }) {
272
- const value = useMemo(
273
- () => ({
274
- report,
275
- // Narrative blocks render everywhere the report does (static SSR, the
276
- // island, Astro) without every host wiring them up. A host renderer for
277
- // the same type still wins.
278
- customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
279
- renderers: renderers ?? EMPTY_RENDERERS
280
- }),
281
- [report, customRenderers, renderers]
282
- );
283
- return /* @__PURE__ */ jsx2(ReportContext.Provider, { value, children });
284
- }
285
-
286
- // src/hooks/useReport.ts
287
- import { useContext } from "react";
288
- function useReport() {
289
- const ctx = useContext(ReportContext);
290
- if (!ctx) {
291
- throw new Error(
292
- "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
293
- );
294
- }
295
- return ctx.report;
296
- }
297
-
298
- // src/components/ReportScenario.tsx
299
- import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
300
-
301
- // src/components/ReportSteps.tsx
302
- import { formatDuration } from "executable-stories-core/utils/duration";
303
-
304
- // src/components/doc/DocNote.tsx
305
- import { jsx as jsx3 } from "react/jsx-runtime";
306
- function DocNote({ entry }) {
307
- return /* @__PURE__ */ jsx3("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
308
- }
309
-
310
- // src/components/doc/DocTag.tsx
311
- import { jsx as jsx4 } from "react/jsx-runtime";
312
- function DocTag({ entry }) {
313
- return /* @__PURE__ */ jsx4("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(Badge, { variant: "tag", children: n }) }, n)) });
314
- }
315
-
316
- // src/components/doc/DocKv.tsx
317
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
318
- function formatValue(value) {
319
- if (value === null) return "null";
320
- if (typeof value === "string") return value;
321
- if (typeof value === "number" || typeof value === "boolean") return String(value);
322
- try {
323
- return JSON.stringify(value);
324
- } catch {
325
- return String(value);
326
- }
327
- }
328
- function DocKv({ entry }) {
329
- return /* @__PURE__ */ jsxs2("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
330
- /* @__PURE__ */ jsx5("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
331
- /* @__PURE__ */ jsx5("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
332
- ] });
333
- }
334
-
335
- // src/hooks/useRenderers.ts
336
- import { useContext as useContext2 } from "react";
337
- var EMPTY_CUSTOM = {};
338
- var EMPTY_RENDERERS2 = {};
339
- function useCustomRenderers() {
340
- const ctx = useContext2(ReportContext);
341
- return ctx?.customRenderers ?? EMPTY_CUSTOM;
342
- }
343
- function useBuiltinRenderers() {
344
- const ctx = useContext2(ReportContext);
345
- return ctx?.renderers ?? EMPTY_RENDERERS2;
346
- }
347
-
348
- // src/components/doc/DocCode.tsx
349
- import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
350
- function CodeFigure({ label, children }) {
351
- return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
352
- label ? /* @__PURE__ */ jsx6("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
353
- /* @__PURE__ */ jsx6("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
354
- ] });
355
- }
356
- function DocCode({ entry }) {
357
- const renderers = useBuiltinRenderers();
358
- if (renderers.code) {
359
- return /* @__PURE__ */ jsx6(Fragment, { children: renderers.code(entry) });
360
- }
361
- return /* @__PURE__ */ jsx6(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx6("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
362
- }
363
-
364
- // src/components/doc/DocTable.tsx
365
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
366
- function DocTable({ entry }) {
367
- return /* @__PURE__ */ jsxs4("figure", { className: "my-3", children: [
368
- entry.label ? /* @__PURE__ */ jsx7("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
369
- /* @__PURE__ */ jsx7("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs4("table", { className: "w-full border-collapse text-sm", children: [
370
- /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx7("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
371
- /* @__PURE__ */ jsx7("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx7("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
372
- ] }) })
373
- ] });
374
- }
375
-
376
- // src/components/doc/DocLink.tsx
377
- import { jsx as jsx8 } from "react/jsx-runtime";
378
- function DocLink({ entry }) {
379
- return /* @__PURE__ */ jsx8(
380
- "a",
381
- {
382
- className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
383
- href: entry.url,
384
- rel: "noreferrer noopener",
385
- target: "_blank",
386
- children: entry.label
387
- }
388
- );
389
- }
390
-
391
- // src/lib/markdown.ts
392
- import { marked } from "marked";
393
-
394
- // src/lib/url.ts
395
- import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
396
-
397
- // src/lib/markdown.ts
398
- function safeMarkdownHtml(markdown) {
399
- const raw = marked.parse(markdown, { async: false });
400
- 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)}'`);
401
- }
402
- function dedent(text) {
403
- const lines = text.replace(/\t/g, " ").split("\n");
404
- const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
405
- const shortest = indents.length > 0 ? Math.min(...indents) : 0;
406
- return lines.map((line) => line.slice(shortest)).join("\n").trim();
279
+ function dedent(text) {
280
+ const lines = text.replace(/\t/g, " ").split("\n");
281
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
282
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
283
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
407
284
  }
408
285
  function neutralizeUrl(value) {
409
286
  return safeUrl(value) ?? "#";
410
287
  }
411
288
 
412
289
  // src/components/doc/DocSection.tsx
413
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
290
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
414
291
  function DocSection({ entry }) {
415
292
  const renderers = useBuiltinRenderers();
416
293
  if (renderers.section) {
417
- return /* @__PURE__ */ jsx9(Fragment2, { children: renderers.section(entry) });
294
+ return /* @__PURE__ */ jsx10(Fragment2, { children: renderers.section(entry) });
418
295
  }
419
296
  const html = safeMarkdownHtml(entry.markdown);
420
- return /* @__PURE__ */ jsxs5("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
421
- entry.title ? /* @__PURE__ */ jsx9("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
422
- /* @__PURE__ */ jsx9(
297
+ return /* @__PURE__ */ jsxs4("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
298
+ entry.title ? /* @__PURE__ */ jsx10("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
299
+ /* @__PURE__ */ jsx10(
423
300
  "div",
424
301
  {
425
302
  className: "es-doc-prose prose prose-sm max-w-none",
@@ -430,11 +307,11 @@ function DocSection({ entry }) {
430
307
  }
431
308
 
432
309
  // src/components/doc/DocMermaid.tsx
433
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
310
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
434
311
  function MermaidSource({ entry, ref }) {
435
- return /* @__PURE__ */ jsxs6("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
436
- entry.title ? /* @__PURE__ */ jsx10("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
437
- /* @__PURE__ */ jsx10(
312
+ return /* @__PURE__ */ jsxs5("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
313
+ entry.title ? /* @__PURE__ */ jsx11("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
314
+ /* @__PURE__ */ jsx11(
438
315
  "pre",
439
316
  {
440
317
  "data-mermaid": true,
@@ -448,24 +325,24 @@ function MermaidSource({ entry, ref }) {
448
325
  function DocMermaid({ entry }) {
449
326
  const renderers = useBuiltinRenderers();
450
327
  if (renderers.mermaid) {
451
- return /* @__PURE__ */ jsx10(Fragment3, { children: renderers.mermaid(entry) });
328
+ return /* @__PURE__ */ jsx11(Fragment3, { children: renderers.mermaid(entry) });
452
329
  }
453
- return /* @__PURE__ */ jsx10(MermaidSource, { entry });
330
+ return /* @__PURE__ */ jsx11(MermaidSource, { entry });
454
331
  }
455
332
 
456
333
  // src/components/doc/DocScreenshot.tsx
457
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
334
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
458
335
  function DocScreenshot({ entry }) {
459
336
  const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
460
337
  if (!src) {
461
- return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
462
- /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Screenshot unavailable" }),
463
- /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
464
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
338
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
339
+ /* @__PURE__ */ jsx12("div", { className: "font-medium", children: "Screenshot unavailable" }),
340
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
341
+ entry.alt ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
465
342
  ] });
466
343
  }
467
- return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
468
- /* @__PURE__ */ jsx11(
344
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
345
+ /* @__PURE__ */ jsx12(
469
346
  "img",
470
347
  {
471
348
  src,
@@ -474,24 +351,24 @@ function DocScreenshot({ entry }) {
474
351
  className: "max-w-full rounded-md border border-border"
475
352
  }
476
353
  ),
477
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
354
+ entry.alt ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
478
355
  ] });
479
356
  }
480
357
 
481
358
  // src/components/doc/DocVideo.tsx
482
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
359
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
483
360
  function DocVideo({ entry }) {
484
361
  if (isLocalFsPath(entry.path)) {
485
- return /* @__PURE__ */ jsxs8("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
486
- /* @__PURE__ */ jsx12("div", { className: "font-medium", children: "Video unavailable" }),
487
- /* @__PURE__ */ jsx12("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
488
- entry.caption ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
362
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
363
+ /* @__PURE__ */ jsx13("div", { className: "font-medium", children: "Video unavailable" }),
364
+ /* @__PURE__ */ jsx13("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
365
+ entry.caption ? /* @__PURE__ */ jsx13("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
489
366
  ] });
490
367
  }
491
368
  const src = safeUrl(entry.path);
492
369
  const poster = safeUrl(entry.poster);
493
- return /* @__PURE__ */ jsxs8("figure", { className: "my-3", children: [
494
- /* @__PURE__ */ jsx12(
370
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
371
+ /* @__PURE__ */ jsx13(
495
372
  "video",
496
373
  {
497
374
  controls: true,
@@ -501,21 +378,21 @@ function DocVideo({ entry }) {
501
378
  className: "max-w-full rounded-md border border-border"
502
379
  }
503
380
  ),
504
- entry.caption ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
381
+ entry.caption ? /* @__PURE__ */ jsx13("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
505
382
  ] });
506
383
  }
507
384
 
508
385
  // src/components/doc/DocHtml.tsx
509
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
386
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
510
387
  function DocHtml({ entry }) {
511
388
  const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
512
389
  const title = entry.title ?? "Embedded HTML";
513
390
  const safeHref = safeUrl(entry.url ?? entry.path);
514
391
  const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
515
- return /* @__PURE__ */ jsxs9("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
516
- /* @__PURE__ */ jsxs9("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: [
517
- /* @__PURE__ */ jsx13("span", { children: entry.title ?? "HTML" }),
518
- safeHref ? /* @__PURE__ */ jsx13(
392
+ return /* @__PURE__ */ jsxs8("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
393
+ /* @__PURE__ */ jsxs8("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: [
394
+ /* @__PURE__ */ jsx14("span", { children: entry.title ?? "HTML" }),
395
+ safeHref ? /* @__PURE__ */ jsx14(
519
396
  "a",
520
397
  {
521
398
  href: safeHref,
@@ -528,7 +405,7 @@ function DocHtml({ entry }) {
528
405
  }
529
406
  ) : null
530
407
  ] }),
531
- /* @__PURE__ */ jsx13(
408
+ /* @__PURE__ */ jsx14(
532
409
  "iframe",
533
410
  {
534
411
  ...frameProps,
@@ -543,7 +420,7 @@ function DocHtml({ entry }) {
543
420
  }
544
421
 
545
422
  // src/components/doc/DocState.tsx
546
- import { jsx as jsx14 } from "react/jsx-runtime";
423
+ import { jsx as jsx15 } from "react/jsx-runtime";
547
424
  function DocState({ entry }) {
548
425
  let json;
549
426
  try {
@@ -551,23 +428,23 @@ function DocState({ entry }) {
551
428
  } catch {
552
429
  json = String(entry.value);
553
430
  }
554
- return /* @__PURE__ */ jsx14(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ jsx14("code", { className: "font-mono text-xs language-json", children: json }) });
431
+ return /* @__PURE__ */ jsx15(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ jsx15("code", { className: "font-mono text-xs language-json", children: json }) });
555
432
  }
556
433
 
557
434
  // src/components/doc/DocCustom.tsx
558
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
435
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
559
436
  function DocCustom({ entry }) {
560
437
  const renderers = useCustomRenderers();
561
438
  const renderer = renderers[entry.type];
562
439
  if (renderer) {
563
- return /* @__PURE__ */ jsx15(Fragment4, { children: renderer(entry) });
440
+ return /* @__PURE__ */ jsx16(Fragment4, { children: renderer(entry) });
564
441
  }
565
- return /* @__PURE__ */ jsxs10("div", { className: "my-2", "data-type": entry.type, children: [
566
- /* @__PURE__ */ jsx15("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
567
- /* @__PURE__ */ jsx15("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify2(entry.data) })
442
+ return /* @__PURE__ */ jsxs9("div", { className: "my-2", "data-type": entry.type, children: [
443
+ /* @__PURE__ */ jsx16("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
444
+ /* @__PURE__ */ jsx16("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
568
445
  ] });
569
446
  }
570
- function safeStringify2(value) {
447
+ function safeStringify(value) {
571
448
  try {
572
449
  return JSON.stringify(value, null, 2);
573
450
  } catch {
@@ -576,68 +453,47 @@ function safeStringify2(value) {
576
453
  }
577
454
 
578
455
  // src/components/doc/DocEntry.tsx
579
- import { jsx as jsx16 } from "react/jsx-runtime";
456
+ import { jsx as jsx17 } from "react/jsx-runtime";
580
457
  function DocEntry({ entry }) {
581
458
  switch (entry.kind) {
582
459
  case "note":
583
- return /* @__PURE__ */ jsx16(DocNote, { entry });
460
+ return /* @__PURE__ */ jsx17(DocNote, { entry });
584
461
  case "tag":
585
- return /* @__PURE__ */ jsx16(DocTag, { entry });
462
+ return /* @__PURE__ */ jsx17(DocTag, { entry });
586
463
  case "kv":
587
- return /* @__PURE__ */ jsx16(DocKv, { entry });
464
+ return /* @__PURE__ */ jsx17(DocKv, { entry });
588
465
  case "code":
589
- return /* @__PURE__ */ jsx16(DocCode, { entry });
466
+ return /* @__PURE__ */ jsx17(DocCode, { entry });
590
467
  case "table":
591
- return /* @__PURE__ */ jsx16(DocTable, { entry });
468
+ return /* @__PURE__ */ jsx17(DocTable, { entry });
592
469
  case "link":
593
- return /* @__PURE__ */ jsx16(DocLink, { entry });
470
+ return /* @__PURE__ */ jsx17(DocLink, { entry });
594
471
  case "section":
595
- return /* @__PURE__ */ jsx16(DocSection, { entry });
472
+ return /* @__PURE__ */ jsx17(DocSection, { entry });
596
473
  case "mermaid":
597
- return /* @__PURE__ */ jsx16(DocMermaid, { entry });
474
+ return /* @__PURE__ */ jsx17(DocMermaid, { entry });
598
475
  case "screenshot":
599
- return /* @__PURE__ */ jsx16(DocScreenshot, { entry });
476
+ return /* @__PURE__ */ jsx17(DocScreenshot, { entry });
600
477
  case "video":
601
- return /* @__PURE__ */ jsx16(DocVideo, { entry });
478
+ return /* @__PURE__ */ jsx17(DocVideo, { entry });
602
479
  case "html":
603
- return /* @__PURE__ */ jsx16(DocHtml, { entry });
480
+ return /* @__PURE__ */ jsx17(DocHtml, { entry });
604
481
  case "state":
605
- return /* @__PURE__ */ jsx16(DocState, { entry });
482
+ return /* @__PURE__ */ jsx17(DocState, { entry });
606
483
  case "custom":
607
- return /* @__PURE__ */ jsx16(DocCustom, { entry });
484
+ return /* @__PURE__ */ jsx17(DocCustom, { entry });
608
485
  }
609
486
  }
610
487
 
611
488
  // src/components/ReportDocEntries.tsx
612
- import { Fragment as Fragment5, jsx as jsx17 } from "react/jsx-runtime";
489
+ import { Fragment as Fragment5, jsx as jsx18 } from "react/jsx-runtime";
613
490
  function ReportDocEntries({ entries }) {
614
491
  if (entries.length === 0) return null;
615
- return /* @__PURE__ */ jsx17(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx17(DocEntry, { entry }, i)) });
616
- }
617
-
618
- // src/lib/step-params.tsx
619
- import { jsx as jsx18 } from "react/jsx-runtime";
620
- var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
621
- function highlightStepParams(text) {
622
- const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
623
- if (matches.length === 0) return text;
624
- const nodes = [];
625
- let lastIndex = 0;
626
- matches.forEach((match, i) => {
627
- const start = match.index;
628
- const end = start + match[0].length;
629
- if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
630
- nodes.push(
631
- /* @__PURE__ */ jsx18("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
632
- );
633
- lastIndex = end;
634
- });
635
- if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
636
- return nodes;
492
+ return /* @__PURE__ */ jsx18(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx18(DocEntry, { entry }, i)) });
637
493
  }
638
494
 
639
495
  // src/components/ReportSteps.tsx
640
- import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
496
+ import { jsx as jsx19, jsxs as jsxs10 } from "react/jsx-runtime";
641
497
  var STEP_GLYPH = {
642
498
  passed: "\u2713",
643
499
  failed: "\u2717",
@@ -652,20 +508,46 @@ var STEP_GLYPH_COLOR = {
652
508
  };
653
509
  function ReportSteps({ scenario }) {
654
510
  if (scenario.steps.length === 0) return null;
655
- return /* @__PURE__ */ jsx19("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx19(ReportStepItem, { step }, step.id)) });
511
+ const unasserted = new Set(
512
+ assertionState(scenario.steps) === "unasserted" ? assertiveSteps(scenario.steps).map((step) => step.id) : []
513
+ );
514
+ return /* @__PURE__ */ jsx19("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx19(
515
+ ReportStepItem,
516
+ {
517
+ step,
518
+ unasserted: unasserted.has(step.id)
519
+ },
520
+ step.id
521
+ )) });
656
522
  }
657
- function ReportStepItem({ step }) {
523
+ function ReportStepItem({
524
+ step,
525
+ unasserted = false
526
+ }) {
658
527
  const kw = step.keyword.toLowerCase();
659
528
  const continuation = kw === "and" || kw === "but";
660
- return /* @__PURE__ */ jsxs11(
529
+ return /* @__PURE__ */ jsxs10(
661
530
  "li",
662
531
  {
663
532
  id: step.id,
664
533
  "data-status": step.status,
665
- className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
534
+ className: cn(
535
+ "py-1.5 text-[0.8125rem] leading-normal",
536
+ continuation && "pl-5"
537
+ ),
666
538
  children: [
667
- /* @__PURE__ */ jsxs11("div", { className: "flex items-baseline gap-2", children: [
668
- /* @__PURE__ */ jsx19("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" }),
539
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-baseline gap-2", children: [
540
+ /* @__PURE__ */ jsx19(
541
+ "span",
542
+ {
543
+ "aria-hidden": true,
544
+ className: cn(
545
+ "w-4 shrink-0 text-center text-xs",
546
+ STEP_GLYPH_COLOR[step.status]
547
+ ),
548
+ children: STEP_GLYPH[step.status] ?? "\u25CB"
549
+ }
550
+ ),
669
551
  /* @__PURE__ */ jsx19(
670
552
  "span",
671
553
  {
@@ -677,6 +559,7 @@ function ReportStepItem({ step }) {
677
559
  }
678
560
  ),
679
561
  /* @__PURE__ */ jsx19("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
562
+ unasserted ? /* @__PURE__ */ jsx19("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,
680
563
  step.durationMs > 0 ? /* @__PURE__ */ jsx19("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
681
564
  ] }),
682
565
  step.errorMessage ? /* @__PURE__ */ jsx19(
@@ -697,7 +580,7 @@ function ReportStepItem({ step }) {
697
580
  import { extractStoryboardFrames } from "executable-stories-core/storyboard";
698
581
  import { summarizeStateChanges } from "executable-stories-core/state-diff";
699
582
  import { isLocalFsPath as isLocalFsPath2, safeImageUrl as safeImageUrl2 } from "executable-stories-core/utils/url";
700
- import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
583
+ import { jsx as jsx20, jsxs as jsxs11 } from "react/jsx-runtime";
701
584
  var FRAME_GLYPH = {
702
585
  passed: "\u2713",
703
586
  failed: "\u2717",
@@ -713,7 +596,7 @@ var FRAME_GLYPH_COLOR = {
713
596
  function ReportStoryboard({ scenario }) {
714
597
  const frames = extractStoryboardFrames(scenario);
715
598
  if (frames.length < 2) return null;
716
- return /* @__PURE__ */ jsxs12("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
599
+ return /* @__PURE__ */ jsxs11("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
717
600
  /* @__PURE__ */ jsx20("h4", { className: "mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Storyboard" }),
718
601
  /* @__PURE__ */ jsx20("ol", { className: "flex items-start gap-3 overflow-x-auto pb-2", children: frames.map((frame) => /* @__PURE__ */ jsx20(StoryboardFrameItem, { frame }, frame.stepId)) })
719
602
  ] });
@@ -721,9 +604,9 @@ function ReportStoryboard({ scenario }) {
721
604
  function StoryboardFrameItem({ frame }) {
722
605
  const label = `${frame.keyword} ${frame.text}`;
723
606
  const hasStates = frame.states.length > 0;
724
- const caption = /* @__PURE__ */ jsxs12("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
607
+ const caption = /* @__PURE__ */ jsxs11("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
725
608
  /* @__PURE__ */ jsx20("span", { "aria-hidden": true, className: cn("shrink-0", FRAME_GLYPH_COLOR[frame.status]), children: FRAME_GLYPH[frame.status] ?? "\u25CB" }),
726
- /* @__PURE__ */ jsxs12("span", { className: "min-w-0", children: [
609
+ /* @__PURE__ */ jsxs11("span", { className: "min-w-0", children: [
727
610
  /* @__PURE__ */ jsx20("span", { className: "font-mono font-semibold text-keyword", children: frame.keyword }),
728
611
  " ",
729
612
  /* @__PURE__ */ jsx20("span", { className: "text-muted-foreground", children: frame.screenshot?.alt ?? frame.text })
@@ -732,7 +615,7 @@ function StoryboardFrameItem({ frame }) {
732
615
  return (
733
616
  // State cards need room for diff rows; screenshot-only frames keep the
734
617
  // original narrow film-cell width (and DOM) unchanged.
735
- /* @__PURE__ */ jsxs12("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
618
+ /* @__PURE__ */ jsxs11("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
736
619
  frame.screenshot ? /* @__PURE__ */ jsx20(FrameScreenshot, { frame, label }) : null,
737
620
  hasStates ? /* @__PURE__ */ jsx20("div", { className: cn("flex flex-col gap-1.5", frame.screenshot && "mt-1.5"), children: frame.states.map((card, i) => /* @__PURE__ */ jsx20(StateCardView, { card }, card.label ?? i)) }) : null,
738
621
  frame.screenshot ? caption : (
@@ -779,8 +662,8 @@ function prettyJson(value) {
779
662
  function StateCardView({ card }) {
780
663
  const json = prettyJson(card.value);
781
664
  const firstCapture = card.changes === void 0;
782
- return /* @__PURE__ */ jsxs12("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
783
- /* @__PURE__ */ jsxs12("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
665
+ return /* @__PURE__ */ jsxs11("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
666
+ /* @__PURE__ */ jsxs11("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
784
667
  /* @__PURE__ */ jsx20("span", { className: "inline-flex rounded-full border border-border px-1.5 font-mono text-[0.625rem] font-medium text-muted-foreground", children: card.label ?? "State" }),
785
668
  firstCapture ? /* @__PURE__ */ jsx20("span", { className: "text-[0.625rem] text-muted-foreground italic", children: "first capture" }) : null
786
669
  ] }),
@@ -795,7 +678,7 @@ function StateCardView({ card }) {
795
678
  },
796
679
  i
797
680
  )) }) : /* @__PURE__ */ jsx20("p", { className: "mt-1.5 text-[0.625rem] text-muted-foreground", children: "No changes" })),
798
- /* @__PURE__ */ jsxs12(
681
+ /* @__PURE__ */ jsxs11(
799
682
  "details",
800
683
  {
801
684
  open: firstCapture && json.length <= 240,
@@ -810,7 +693,7 @@ function StateCardView({ card }) {
810
693
  }
811
694
 
812
695
  // src/components/ReportAttachments.tsx
813
- import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
696
+ import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
814
697
  function ReportAttachments({ attachments }) {
815
698
  if (attachments.length === 0) return null;
816
699
  return (
@@ -819,7 +702,7 @@ function ReportAttachments({ attachments }) {
819
702
  /* @__PURE__ */ jsx21("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
820
703
  const dataUri = att.contentEncoding === "BASE64" ? `data:${att.mediaType};base64,${att.body}` : `data:${att.mediaType},${encodeURIComponent(att.body)}`;
821
704
  const isImage = att.mediaType.startsWith("image/");
822
- return /* @__PURE__ */ jsx21("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs13(Fragment6, { children: [
705
+ return /* @__PURE__ */ jsx21("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs12(Fragment6, { children: [
823
706
  /* @__PURE__ */ jsx21(
824
707
  "img",
825
708
  {
@@ -830,7 +713,7 @@ function ReportAttachments({ attachments }) {
830
713
  }
831
714
  ),
832
715
  /* @__PURE__ */ jsx21("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
833
- ] }) : /* @__PURE__ */ jsxs13(
716
+ ] }) : /* @__PURE__ */ jsxs12(
834
717
  "a",
835
718
  {
836
719
  href: dataUri,
@@ -839,7 +722,7 @@ function ReportAttachments({ attachments }) {
839
722
  children: [
840
723
  att.name,
841
724
  " ",
842
- /* @__PURE__ */ jsxs13("span", { className: "text-muted-foreground", children: [
725
+ /* @__PURE__ */ jsxs12("span", { className: "text-muted-foreground", children: [
843
726
  "(",
844
727
  att.mediaType,
845
728
  ")"
@@ -930,7 +813,7 @@ function traceWindow(rows) {
930
813
  }
931
814
 
932
815
  // src/components/ReportTrace.tsx
933
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
816
+ import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
934
817
  var BAR_COLOR = {
935
818
  ok: "bg-pass",
936
819
  error: "bg-fail",
@@ -941,8 +824,8 @@ function ReportTrace({ spans }) {
941
824
  const rows = toTraceRows(spans);
942
825
  if (rows.length === 0) return null;
943
826
  const { minStartMs, totalMs } = traceWindow(rows);
944
- return /* @__PURE__ */ jsxs14("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
945
- /* @__PURE__ */ jsxs14("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
827
+ return /* @__PURE__ */ jsxs13("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
828
+ /* @__PURE__ */ jsxs13("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
946
829
  "Trace (",
947
830
  rows.length,
948
831
  " span",
@@ -954,7 +837,7 @@ function ReportTrace({ spans }) {
954
837
  /* @__PURE__ */ jsx22("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
955
838
  const leftPct = (row.startTimeMs - minStartMs) / totalMs * 100;
956
839
  const widthPct = Math.max(row.durationMs / totalMs * 100, 0.5);
957
- return /* @__PURE__ */ jsxs14("li", { className: "flex items-center gap-2", children: [
840
+ return /* @__PURE__ */ jsxs13("li", { className: "flex items-center gap-2", children: [
958
841
  /* @__PURE__ */ jsx22(
959
842
  "span",
960
843
  {
@@ -1040,7 +923,7 @@ function diffSinceLastRun(history, scenarios) {
1040
923
  }
1041
924
 
1042
925
  // src/components/ScenarioRunHistory.tsx
1043
- import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
926
+ import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1044
927
  var MAX_DOTS = 10;
1045
928
  var DOT_COLOR = {
1046
929
  passed: "bg-pass",
@@ -1060,7 +943,7 @@ function ScenarioRunHistory({ scenarioId }) {
1060
943
  const recent = entries.slice(-MAX_DOTS);
1061
944
  const flaky = flakinessOf(recent) === "flaky";
1062
945
  const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
1063
- return /* @__PURE__ */ jsxs15(Fragment7, { children: [
946
+ return /* @__PURE__ */ jsxs14(Fragment7, { children: [
1064
947
  flaky ? /* @__PURE__ */ jsx23(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
1065
948
  /* @__PURE__ */ jsx23(
1066
949
  "span",
@@ -1090,24 +973,103 @@ function ScenarioRunHistory({ scenarioId }) {
1090
973
  ] });
1091
974
  }
1092
975
 
1093
- // src/interactive/collapse-context.ts
1094
- import { createContext as createContext3, useCallback, useContext as useContext4, useState } from "react";
1095
- var CollapseContext = createContext3(null);
1096
- var CollapseProvider = CollapseContext.Provider;
1097
- function useCollapse() {
1098
- return useContext4(CollapseContext);
1099
- }
1100
- var STORAGE_KEY = "es-collapsed-ids";
1101
- function readPersisted() {
1102
- if (typeof localStorage === "undefined") return null;
1103
- try {
1104
- const raw = localStorage.getItem(STORAGE_KEY);
1105
- return raw ? new Set(JSON.parse(raw)) : null;
1106
- } catch {
1107
- return null;
1108
- }
976
+ // src/components/ScenarioStaleness.tsx
977
+ import { useEffect, useState } from "react";
978
+
979
+ // src/lib/provenance.ts
980
+ function reportLastRunMs(report) {
981
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
982
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
983
+ return void 0;
1109
984
  }
1110
- function persist(ids) {
985
+ function formatRelativeAge(thenMs, nowMs) {
986
+ const deltaMs = Math.max(0, nowMs - thenMs);
987
+ const minutes = Math.floor(deltaMs / 6e4);
988
+ if (minutes < 1) return "just now";
989
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
990
+ const hours = Math.floor(minutes / 60);
991
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
992
+ const days = Math.floor(hours / 24);
993
+ return `${days} day${days === 1 ? "" : "s"} ago`;
994
+ }
995
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
996
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
997
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
998
+ }
999
+ var CI_DISPLAY_NAMES = {
1000
+ github: "GitHub Actions",
1001
+ gitlab: "GitLab CI",
1002
+ circleci: "CircleCI",
1003
+ jenkins: "Jenkins",
1004
+ azure: "Azure DevOps",
1005
+ buildkite: "Buildkite",
1006
+ travis: "Travis CI",
1007
+ ci: "CI"
1008
+ };
1009
+ function ciDisplayName(name) {
1010
+ return CI_DISPLAY_NAMES[name] ?? name;
1011
+ }
1012
+ function commitUrl(ci, sha) {
1013
+ const commitSha = ci?.commitSha ?? sha;
1014
+ if (!ci?.url || !commitSha) return void 0;
1015
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1016
+ if (github) return `${github[1]}/commit/${commitSha}`;
1017
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1018
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1019
+ return void 0;
1020
+ }
1021
+ function prUrl(ci) {
1022
+ if (!ci?.url || !ci.prNumber) return void 0;
1023
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1024
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1025
+ return void 0;
1026
+ }
1027
+
1028
+ // src/interactive/scenario-freshness-context.tsx
1029
+ import { createContext as createContext3, useContext as useContext4 } from "react";
1030
+ var ScenarioFreshnessContext = createContext3(null);
1031
+ var ScenarioFreshnessProvider = ScenarioFreshnessContext.Provider;
1032
+ function useScenarioFreshness() {
1033
+ return useContext4(ScenarioFreshnessContext);
1034
+ }
1035
+
1036
+ // src/components/ScenarioStaleness.tsx
1037
+ import { jsxs as jsxs15 } from "react/jsx-runtime";
1038
+ function ScenarioStaleness({ scenario }) {
1039
+ const freshness = useScenarioFreshness();
1040
+ const fixedNow = freshness?.nowMs;
1041
+ const [now, setNow] = useState(fixedNow);
1042
+ useEffect(() => {
1043
+ if (fixedNow === void 0) setNow(Date.now());
1044
+ }, [fixedNow]);
1045
+ if (!freshness || now === void 0) return null;
1046
+ const lastRunAtMs = scenario.lastRunAtMs;
1047
+ if (!lastRunAtMs || lastRunAtMs <= 0) return null;
1048
+ if (!isReportStale(lastRunAtMs, freshness.staleAfterDays, now)) return null;
1049
+ return /* @__PURE__ */ jsxs15(Badge, { variant: "outline", "aria-label": "Scenario freshness", children: [
1050
+ "Last ran ",
1051
+ formatRelativeAge(lastRunAtMs, now)
1052
+ ] });
1053
+ }
1054
+
1055
+ // src/interactive/collapse-context.ts
1056
+ import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState2 } from "react";
1057
+ var CollapseContext = createContext4(null);
1058
+ var CollapseProvider = CollapseContext.Provider;
1059
+ function useCollapse() {
1060
+ return useContext5(CollapseContext);
1061
+ }
1062
+ var STORAGE_KEY = "es-collapsed-ids";
1063
+ function readPersisted() {
1064
+ if (typeof localStorage === "undefined") return null;
1065
+ try {
1066
+ const raw = localStorage.getItem(STORAGE_KEY);
1067
+ return raw ? new Set(JSON.parse(raw)) : null;
1068
+ } catch {
1069
+ return null;
1070
+ }
1071
+ }
1072
+ function persist(ids) {
1111
1073
  if (typeof localStorage === "undefined") return;
1112
1074
  try {
1113
1075
  localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]));
@@ -1115,7 +1077,7 @@ function persist(ids) {
1115
1077
  }
1116
1078
  }
1117
1079
  function useCollapseState(computeDefault) {
1118
- const [collapsed, setCollapsed] = useState(
1080
+ const [collapsed, setCollapsed] = useState2(
1119
1081
  () => readPersisted() ?? computeDefault?.() ?? /* @__PURE__ */ new Set()
1120
1082
  );
1121
1083
  const isCollapsed = useCallback((id) => collapsed.has(id), [collapsed]);
@@ -1146,12 +1108,12 @@ function useCollapseState(computeDefault) {
1146
1108
  }
1147
1109
 
1148
1110
  // src/interactive/scenario-actions.tsx
1149
- import { createContext as createContext4, useContext as useContext5 } from "react";
1111
+ import { createContext as createContext5, useContext as useContext6 } from "react";
1150
1112
  import { scenarioToMarkdown as coreScenarioToMarkdown } from "executable-stories-core/utils/scenario-markdown";
1151
- var ScenarioActionsContext = createContext4(null);
1113
+ var ScenarioActionsContext = createContext5(null);
1152
1114
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
1153
1115
  function useScenarioActions() {
1154
- return useContext5(ScenarioActionsContext);
1116
+ return useContext6(ScenarioActionsContext);
1155
1117
  }
1156
1118
  function scenarioToMarkdown(scenario) {
1157
1119
  return coreScenarioToMarkdown(scenario, { variant: "compact" });
@@ -1319,6 +1281,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1319
1281
  ] }),
1320
1282
  /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1321
1283
  /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1284
+ /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1322
1285
  scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1323
1286
  meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1324
1287
  actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
@@ -1490,71 +1453,74 @@ function ReportFeatureList() {
1490
1453
  return /* @__PURE__ */ jsx30("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx30(ReportFeature, { feature }, feature.id)) });
1491
1454
  }
1492
1455
 
1493
- // src/components/ui/empty.tsx
1494
- import { cva as cva2 } from "class-variance-authority";
1495
- import { jsx as jsx31 } from "react/jsx-runtime";
1496
- function Empty({ className, ...props }) {
1497
- return /* @__PURE__ */ jsx31(
1498
- "div",
1499
- {
1500
- "data-slot": "empty",
1501
- className: cn(
1502
- "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",
1503
- className
1504
- ),
1505
- ...props
1506
- }
1507
- );
1508
- }
1509
- function EmptyHeader({ className, ...props }) {
1510
- return /* @__PURE__ */ jsx31(
1511
- "div",
1512
- {
1513
- "data-slot": "empty-header",
1514
- className: cn(
1515
- "flex max-w-sm flex-col items-center gap-2 text-center",
1516
- className
1517
- ),
1518
- ...props
1519
- }
1520
- );
1521
- }
1522
- var emptyMediaVariants = cva2(
1523
- "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
1524
- {
1525
- variants: {
1526
- variant: {
1527
- default: "bg-transparent",
1528
- icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
1529
- }
1530
- },
1531
- defaultVariants: {
1532
- variant: "default"
1533
- }
1456
+ // src/components/ReportMeta.tsx
1457
+ import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1458
+ import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1459
+ function ReportMeta() {
1460
+ const report = useReport();
1461
+ const items = [];
1462
+ const linkClass = "text-primary underline underline-offset-2";
1463
+ if (report.startedAtMs) {
1464
+ items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1534
1465
  }
1535
- );
1536
- function EmptyTitle({ className, ...props }) {
1537
- return /* @__PURE__ */ jsx31(
1538
- "div",
1539
- {
1540
- "data-slot": "empty-title",
1541
- className: cn("text-lg font-medium tracking-tight", className),
1542
- ...props
1543
- }
1466
+ if (report.durationMs) {
1467
+ items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1468
+ }
1469
+ if (report.packageVersion) {
1470
+ items.push({ label: "Version", value: report.packageVersion });
1471
+ }
1472
+ if (report.ci?.branch) {
1473
+ items.push({ label: "Branch", value: report.ci.branch });
1474
+ }
1475
+ const sha = report.ci?.commitSha ?? report.gitSha;
1476
+ if (sha) {
1477
+ const url = commitUrl(report.ci, report.gitSha);
1478
+ const short = sha.slice(0, 8);
1479
+ items.push({
1480
+ label: "Commit",
1481
+ value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1482
+ });
1483
+ }
1484
+ if (report.ci) {
1485
+ items.push({
1486
+ label: "CI",
1487
+ value: report.ci.url ? /* @__PURE__ */ jsxs20("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1488
+ ciDisplayName(report.ci.name),
1489
+ report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1490
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1491
+ });
1492
+ }
1493
+ if (report.ci?.prNumber) {
1494
+ const url = prUrl(report.ci);
1495
+ const label = `#${report.ci.prNumber}`;
1496
+ items.push({
1497
+ label: "PR",
1498
+ value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1499
+ });
1500
+ }
1501
+ if (items.length === 0) return null;
1502
+ return (
1503
+ // Run metadata is useful but secondary to "what failed" — tuck it into a
1504
+ // collapsed disclosure so it doesn't push the result down. Native <details>
1505
+ // so it works with no JS (the SSR/no-hydration report still toggles).
1506
+ /* @__PURE__ */ jsxs20("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1507
+ /* @__PURE__ */ jsxs20("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: [
1508
+ /* @__PURE__ */ jsx31("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1509
+ "Run details"
1510
+ ] }),
1511
+ /* @__PURE__ */ jsx31("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__ */ jsxs20("div", { className: "flex items-baseline gap-1.5", children: [
1512
+ /* @__PURE__ */ jsx31("dt", { className: "font-medium", children: item.label }),
1513
+ /* @__PURE__ */ jsx31("dd", { className: "font-mono text-foreground", children: item.value })
1514
+ ] }, item.label)) })
1515
+ ] })
1544
1516
  );
1545
1517
  }
1546
1518
 
1547
- // src/components/ReportEmpty.tsx
1548
- import { jsx as jsx32 } from "react/jsx-runtime";
1549
- function ReportEmpty({ message }) {
1550
- return /* @__PURE__ */ jsx32(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx32(EmptyHeader, { children: /* @__PURE__ */ jsx32(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1551
- }
1552
-
1553
1519
  // src/components/ReportSummary.tsx
1554
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1520
+ import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
1555
1521
  function ReportSummary({ className }) {
1556
1522
  const report = useReport();
1557
- return /* @__PURE__ */ jsx33(
1523
+ return /* @__PURE__ */ jsx32(
1558
1524
  ReportSummaryView,
1559
1525
  {
1560
1526
  summary: report.summary,
@@ -1580,7 +1546,7 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1580
1546
  if (summary.pending > 0) {
1581
1547
  cards.push({ status: "pending", label: "Pending", value: summary.pending });
1582
1548
  }
1583
- return /* @__PURE__ */ jsx33(
1549
+ return /* @__PURE__ */ jsx32(
1584
1550
  "dl",
1585
1551
  {
1586
1552
  className: cn(
@@ -1593,17 +1559,17 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1593
1559
  "aria-label": ariaLabel,
1594
1560
  children: cards.map((c) => {
1595
1561
  const tone = TONE[c.status];
1596
- return /* @__PURE__ */ jsxs20(
1562
+ return /* @__PURE__ */ jsxs21(
1597
1563
  "div",
1598
1564
  {
1599
1565
  "data-status": c.status,
1600
1566
  className: "rounded-lg border bg-card px-4 py-3",
1601
1567
  children: [
1602
- /* @__PURE__ */ jsxs20("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1603
- /* @__PURE__ */ jsx33("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1568
+ /* @__PURE__ */ jsxs21("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1569
+ /* @__PURE__ */ jsx32("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1604
1570
  c.label
1605
1571
  ] }),
1606
- /* @__PURE__ */ jsx33("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1572
+ /* @__PURE__ */ jsx32("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1607
1573
  ]
1608
1574
  },
1609
1575
  c.status
@@ -1613,165 +1579,48 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1613
1579
  );
1614
1580
  }
1615
1581
 
1616
- // src/components/ReportMeta.tsx
1617
- import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1618
-
1619
- // src/lib/provenance.ts
1620
- function reportLastRunMs(report) {
1621
- if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1622
- if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1623
- return void 0;
1624
- }
1625
- function formatRelativeAge(thenMs, nowMs) {
1626
- const deltaMs = Math.max(0, nowMs - thenMs);
1627
- const minutes = Math.floor(deltaMs / 6e4);
1628
- if (minutes < 1) return "just now";
1629
- if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1630
- const hours = Math.floor(minutes / 60);
1631
- if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1632
- const days = Math.floor(hours / 24);
1633
- return `${days} day${days === 1 ? "" : "s"} ago`;
1634
- }
1635
- function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1636
- if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1637
- return nowMs - lastRunMs >= staleAfterDays * 864e5;
1638
- }
1639
- var CI_DISPLAY_NAMES = {
1640
- github: "GitHub Actions",
1641
- gitlab: "GitLab CI",
1642
- circleci: "CircleCI",
1643
- jenkins: "Jenkins",
1644
- azure: "Azure DevOps",
1645
- buildkite: "Buildkite",
1646
- travis: "Travis CI",
1647
- ci: "CI"
1648
- };
1649
- function ciDisplayName(name) {
1650
- return CI_DISPLAY_NAMES[name] ?? name;
1651
- }
1652
- function commitUrl(ci, sha) {
1653
- const commitSha = ci?.commitSha ?? sha;
1654
- if (!ci?.url || !commitSha) return void 0;
1655
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1656
- if (github) return `${github[1]}/commit/${commitSha}`;
1657
- const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1658
- if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1659
- return void 0;
1582
+ // src/components/ReportSchemaError.tsx
1583
+ import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
1584
+ function ReportSchemaError({ error }) {
1585
+ return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1586
+ /* @__PURE__ */ jsx33("p", { children: /* @__PURE__ */ jsx33("strong", { children: "Report could not be displayed." }) }),
1587
+ /* @__PURE__ */ jsx33("p", { children: error.message }),
1588
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1589
+ "The report bundle is newer than this version of ",
1590
+ /* @__PURE__ */ jsx33("code", { children: "executable-stories-react" }),
1591
+ ". Upgrade the package, or regenerate the report with an older formatters CLI."
1592
+ ] }) : null,
1593
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1594
+ /* @__PURE__ */ jsxs22("summary", { children: [
1595
+ error.issues.length,
1596
+ " validation issue",
1597
+ error.issues.length === 1 ? "" : "s"
1598
+ ] }),
1599
+ /* @__PURE__ */ jsx33("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1600
+ ] }) : null
1601
+ ] });
1660
1602
  }
1661
- function prUrl(ci) {
1662
- if (!ci?.url || !ci.prNumber) return void 0;
1663
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1664
- if (github) return `${github[1]}/pull/${ci.prNumber}`;
1665
- return void 0;
1603
+
1604
+ // src/components/ReportShell.tsx
1605
+ import { Fragment as Fragment10, jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
1606
+ function ReportErrorShell({
1607
+ error,
1608
+ className,
1609
+ title,
1610
+ dataTheme
1611
+ }) {
1612
+ return /* @__PURE__ */ jsx34("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx34(ReportSchemaError, { error }) });
1666
1613
  }
1667
1614
 
1668
- // src/components/ReportMeta.tsx
1669
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1670
- function ReportMeta() {
1671
- const report = useReport();
1672
- const items = [];
1673
- const linkClass = "text-primary underline underline-offset-2";
1674
- if (report.startedAtMs) {
1675
- items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1676
- }
1677
- if (report.durationMs) {
1678
- items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1679
- }
1680
- if (report.packageVersion) {
1681
- items.push({ label: "Version", value: report.packageVersion });
1682
- }
1683
- if (report.ci?.branch) {
1684
- items.push({ label: "Branch", value: report.ci.branch });
1685
- }
1686
- const sha = report.ci?.commitSha ?? report.gitSha;
1687
- if (sha) {
1688
- const url = commitUrl(report.ci, report.gitSha);
1689
- const short = sha.slice(0, 8);
1690
- items.push({
1691
- label: "Commit",
1692
- value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1693
- });
1694
- }
1695
- if (report.ci) {
1696
- items.push({
1697
- label: "CI",
1698
- value: report.ci.url ? /* @__PURE__ */ jsxs21("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1699
- ciDisplayName(report.ci.name),
1700
- report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1701
- ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1702
- });
1703
- }
1704
- if (report.ci?.prNumber) {
1705
- const url = prUrl(report.ci);
1706
- const label = `#${report.ci.prNumber}`;
1707
- items.push({
1708
- label: "PR",
1709
- value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1710
- });
1711
- }
1712
- if (items.length === 0) return null;
1713
- return (
1714
- // Run metadata is useful but secondary to "what failed" — tuck it into a
1715
- // collapsed disclosure so it doesn't push the result down. Native <details>
1716
- // so it works with no JS (the SSR/no-hydration report still toggles).
1717
- /* @__PURE__ */ jsxs21("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1718
- /* @__PURE__ */ jsxs21("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: [
1719
- /* @__PURE__ */ jsx34("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1720
- "Run details"
1721
- ] }),
1722
- /* @__PURE__ */ jsx34("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__ */ jsxs21("div", { className: "flex items-baseline gap-1.5", children: [
1723
- /* @__PURE__ */ jsx34("dt", { className: "font-medium", children: item.label }),
1724
- /* @__PURE__ */ jsx34("dd", { className: "font-mono text-foreground", children: item.value })
1725
- ] }, item.label)) })
1726
- ] })
1727
- );
1728
- }
1729
-
1730
- // src/components/ReportSchemaError.tsx
1731
- import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1732
- function ReportSchemaError({ error }) {
1733
- return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1734
- /* @__PURE__ */ jsx35("p", { children: /* @__PURE__ */ jsx35("strong", { children: "Report could not be displayed." }) }),
1735
- /* @__PURE__ */ jsx35("p", { children: error.message }),
1736
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1737
- "The report bundle is newer than this version of ",
1738
- /* @__PURE__ */ jsx35("code", { children: "executable-stories-react" }),
1739
- ". Upgrade the package, or regenerate the report with an older formatters CLI."
1740
- ] }) : null,
1741
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1742
- /* @__PURE__ */ jsxs22("summary", { children: [
1743
- error.issues.length,
1744
- " validation issue",
1745
- error.issues.length === 1 ? "" : "s"
1746
- ] }),
1747
- /* @__PURE__ */ jsx35("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1748
- ] }) : null
1749
- ] });
1750
- }
1751
-
1752
- // src/components/ReportShell.tsx
1753
- import { Fragment as Fragment10, jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1754
- function ReportErrorShell({
1755
- error,
1756
- className,
1757
- title,
1758
- dataTheme
1759
- }) {
1760
- return /* @__PURE__ */ jsx36("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx36(ReportSchemaError, { error }) });
1761
- }
1762
-
1763
- // src/interactive/ReportInteractive.tsx
1764
- import { Monitor, Moon, Sun } from "lucide-react";
1765
-
1766
1615
  // src/components/ui/switch.tsx
1767
1616
  import "react";
1768
1617
  import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
1769
- import { jsx as jsx37 } from "react/jsx-runtime";
1618
+ import { jsx as jsx35 } from "react/jsx-runtime";
1770
1619
  function Switch({
1771
1620
  className,
1772
1621
  ...props
1773
1622
  }) {
1774
- return /* @__PURE__ */ jsx37(
1623
+ return /* @__PURE__ */ jsx35(
1775
1624
  SwitchPrimitive.Root,
1776
1625
  {
1777
1626
  "data-slot": "switch",
@@ -1780,7 +1629,7 @@ function Switch({
1780
1629
  className
1781
1630
  ),
1782
1631
  ...props,
1783
- children: /* @__PURE__ */ jsx37(
1632
+ children: /* @__PURE__ */ jsx35(
1784
1633
  SwitchPrimitive.Thumb,
1785
1634
  {
1786
1635
  "data-slot": "switch-thumb",
@@ -1796,7 +1645,7 @@ import * as React5 from "react";
1796
1645
  import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
1797
1646
  import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
1798
1647
  import { cva as cva3 } from "class-variance-authority";
1799
- import { jsx as jsx38 } from "react/jsx-runtime";
1648
+ import { jsx as jsx36 } from "react/jsx-runtime";
1800
1649
  var toggleItemVariants = cva3(
1801
1650
  "inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap text-muted-foreground transition-[color,background-color,box-shadow] outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-background data-[pressed]:text-foreground data-[pressed]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1802
1651
  {
@@ -1818,13 +1667,13 @@ function ToggleGroup({
1818
1667
  children,
1819
1668
  ...props
1820
1669
  }) {
1821
- return /* @__PURE__ */ jsx38(
1670
+ return /* @__PURE__ */ jsx36(
1822
1671
  ToggleGroupPrimitive,
1823
1672
  {
1824
1673
  "data-slot": "toggle-group",
1825
1674
  className: cn("flex w-fit items-center gap-0.5 rounded-lg bg-muted p-0.5", className),
1826
1675
  ...props,
1827
- children: /* @__PURE__ */ jsx38(ToggleGroupContext.Provider, { value: { size }, children })
1676
+ children: /* @__PURE__ */ jsx36(ToggleGroupContext.Provider, { value: { size }, children })
1828
1677
  }
1829
1678
  );
1830
1679
  }
@@ -1835,7 +1684,7 @@ function ToggleGroupItem({
1835
1684
  ...props
1836
1685
  }) {
1837
1686
  const context = React5.useContext(ToggleGroupContext);
1838
- return /* @__PURE__ */ jsx38(
1687
+ return /* @__PURE__ */ jsx36(
1839
1688
  TogglePrimitive,
1840
1689
  {
1841
1690
  "data-slot": "toggle-group-item",
@@ -1846,173 +1695,382 @@ function ToggleGroupItem({
1846
1695
  );
1847
1696
  }
1848
1697
 
1849
- // src/interactive/ReportSearch.tsx
1850
- import {
1851
- forwardRef,
1852
- useId
1853
- } from "react";
1854
- import { Search } from "lucide-react";
1698
+ // src/context/ReportRoot.tsx
1699
+ import { useMemo } from "react";
1855
1700
 
1856
- // src/components/ui/input.tsx
1857
- import "react";
1858
- import { jsx as jsx39 } from "react/jsx-runtime";
1859
- function Input({ className, type, ...props }) {
1860
- return /* @__PURE__ */ jsx39(
1861
- "input",
1862
- {
1863
- type,
1864
- "data-slot": "input",
1865
- className: cn(
1866
- "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",
1867
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
1868
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
1869
- className
1870
- ),
1871
- ...props
1872
- }
1873
- );
1701
+ // src/components/doc/NarrativeBlocks.tsx
1702
+ import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1703
+ var FILE_TREE_TYPE = "file-tree";
1704
+ var DATA_MODEL_TYPE = "data-model";
1705
+ function isChangeKind(value) {
1706
+ return value === "added" || value === "modified" || value === "removed" || value === "renamed";
1874
1707
  }
1875
-
1876
- // src/interactive/ReportSearch.tsx
1877
- import { jsx as jsx40, jsxs as jsxs24 } from "react/jsx-runtime";
1878
- var ReportSearch = forwardRef(
1879
- function ReportSearch2(props, ref) {
1880
- const {
1881
- value,
1882
- onChange,
1883
- matchedCount,
1884
- totalCount,
1885
- placeholder = "Search scenarios or tags\u2026",
1886
- className
1887
- } = props;
1888
- const inputId = useId();
1889
- const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
1890
- function handleChange(e) {
1891
- onChange(e.target.value);
1892
- }
1893
- function handleKeyDown(e) {
1894
- if (e.key === "Escape" && value !== "") {
1895
- onChange("");
1896
- e.preventDefault();
1897
- }
1898
- }
1899
- return /* @__PURE__ */ jsxs24("div", { className: cn("es-search flex flex-col gap-1", className), children: [
1900
- /* @__PURE__ */ jsx40("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
1901
- /* @__PURE__ */ jsxs24("div", { className: "relative w-full sm:w-72", children: [
1902
- /* @__PURE__ */ jsx40(
1903
- Search,
1904
- {
1905
- "aria-hidden": true,
1906
- className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
1907
- }
1908
- ),
1909
- /* @__PURE__ */ jsx40(
1910
- Input,
1911
- {
1912
- ref,
1913
- id: inputId,
1914
- type: "search",
1915
- value,
1916
- onChange: handleChange,
1917
- onKeyDown: handleKeyDown,
1918
- placeholder,
1919
- autoComplete: "off",
1920
- spellCheck: false,
1921
- "aria-keyshortcuts": "/",
1922
- className: "pl-8"
1923
- }
1924
- )
1925
- ] }),
1926
- showCounts ? /* @__PURE__ */ jsxs24(
1927
- "span",
1928
- {
1929
- className: "text-xs tabular-nums text-muted-foreground",
1930
- "aria-live": "polite",
1931
- children: [
1932
- matchedCount,
1933
- " of ",
1934
- totalCount,
1935
- " scenarios"
1936
- ]
1937
- }
1938
- ) : null
1939
- ] });
1940
- }
1941
- );
1942
-
1943
- // src/interactive/ReportFailureBanner.tsx
1944
- import { useCallback as useCallback2 } from "react";
1945
-
1946
- // src/lib/hash-state.ts
1947
- var DEFAULT_URL_STATE = {
1948
- query: "",
1949
- status: "all",
1950
- tags: [],
1951
- detail: "full"
1952
- };
1953
- var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
1954
- function splitHash(hash) {
1955
- const raw = hash.replace(/^#/, "");
1956
- const split = raw.indexOf("?");
1957
- if (split === -1) return { anchor: raw, params: new URLSearchParams() };
1958
- return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
1708
+ function isRecord(value) {
1709
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1959
1710
  }
1960
- function decodeUrlState(params) {
1961
- const status = params.get("status");
1962
- const tags = params.get("tags");
1963
- return {
1964
- query: params.get("q") ?? DEFAULT_URL_STATE.query,
1965
- status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
1966
- tags: tags ? tags.split(",").filter(Boolean) : [],
1967
- detail: params.get("docs") === "0" ? "minimal" : "full"
1968
- };
1711
+ function str(value) {
1712
+ return typeof value === "string" && value !== "" ? value : void 0;
1969
1713
  }
1970
- function encodeUrlState(state) {
1971
- const params = new URLSearchParams();
1972
- if (state.query) params.set("q", state.query);
1973
- if (state.status !== "all") params.set("status", state.status);
1974
- if (state.tags.length > 0) params.set("tags", state.tags.join(","));
1975
- if (state.detail === "minimal") params.set("docs", "0");
1976
- return params.toString();
1714
+ function AuthoredMarker({ authored }) {
1715
+ if (authored !== "agent") return null;
1716
+ return /* @__PURE__ */ jsx37(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
1977
1717
  }
1978
- function formatHash(anchor, params) {
1979
- if (params) return `#${anchor}?${params}`;
1980
- return anchor ? `#${anchor}` : "";
1718
+ function BlockFrame({
1719
+ title,
1720
+ authored,
1721
+ children
1722
+ }) {
1723
+ return /* @__PURE__ */ jsxs24("figure", { className: "my-3", children: [
1724
+ title || authored === "agent" ? /* @__PURE__ */ jsxs24("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
1725
+ title ? /* @__PURE__ */ jsx37("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
1726
+ /* @__PURE__ */ jsx37(AuthoredMarker, { authored })
1727
+ ] }) : null,
1728
+ children
1729
+ ] });
1981
1730
  }
1982
- function writeHash(anchor, params) {
1983
- if (typeof window === "undefined") return;
1984
- const next = formatHash(anchor, params);
1985
- if (next === window.location.hash) return;
1986
- const url = next || window.location.href.split("#")[0];
1987
- try {
1988
- window.history.replaceState(window.history.state, "", url);
1989
- } catch {
1990
- window.location.hash = next.slice(1);
1731
+ function ChangeBadge({ change }) {
1732
+ return /* @__PURE__ */ jsx37(Badge, { variant: "tag", className: "text-[10px]", children: change });
1733
+ }
1734
+ function parseFileTree(data) {
1735
+ if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
1736
+ const files = [];
1737
+ for (const raw of data.files) {
1738
+ if (typeof raw === "string") {
1739
+ if (raw) files.push({ path: raw });
1740
+ continue;
1741
+ }
1742
+ if (!isRecord(raw)) continue;
1743
+ const path = str(raw.path);
1744
+ if (!path) continue;
1745
+ files.push({
1746
+ path,
1747
+ change: isChangeKind(raw.change) ? raw.change : void 0,
1748
+ note: str(raw.note)
1749
+ });
1991
1750
  }
1751
+ if (files.length === 0) return void 0;
1752
+ return { title: str(data.title), authored: str(data.authored), files };
1992
1753
  }
1993
-
1994
- // src/lib/scroll.ts
1995
- function prefersReducedMotion() {
1996
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1997
- return false;
1754
+ function buildTree(files) {
1755
+ const root = { name: "", children: /* @__PURE__ */ new Map() };
1756
+ for (const file of files) {
1757
+ const segments = file.path.split("/").filter(Boolean);
1758
+ let node = root;
1759
+ segments.forEach((segment, index) => {
1760
+ let child = node.children.get(segment);
1761
+ if (!child) {
1762
+ child = { name: segment, children: /* @__PURE__ */ new Map() };
1763
+ node.children.set(segment, child);
1764
+ }
1765
+ if (index === segments.length - 1) child.file = file;
1766
+ node = child;
1767
+ });
1998
1768
  }
1999
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1769
+ return root;
2000
1770
  }
2001
- function scrollToScenarioId(scenarioId, options = {}) {
2002
- if (typeof document === "undefined") return;
2003
- const el = document.getElementById(scenarioId);
2004
- if (!el) return;
2005
- el.scrollIntoView({
2006
- behavior: prefersReducedMotion() ? "auto" : "smooth",
2007
- block: "start"
1771
+ function sortedChildren(node) {
1772
+ return [...node.children.values()].sort((a, b) => {
1773
+ const aDir = a.children.size > 0;
1774
+ const bDir = b.children.size > 0;
1775
+ if (aDir !== bDir) return aDir ? -1 : 1;
1776
+ return a.name.localeCompare(b.name);
1777
+ });
1778
+ }
1779
+ function TreeBranch({ node, depth }) {
1780
+ const children = sortedChildren(node);
1781
+ return /* @__PURE__ */ jsx37("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
1782
+ const isDir = child.children.size > 0;
1783
+ return /* @__PURE__ */ jsxs24("li", { children: [
1784
+ /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-baseline gap-2", children: [
1785
+ /* @__PURE__ */ jsx37(
1786
+ "span",
1787
+ {
1788
+ className: cn(
1789
+ "font-mono text-xs",
1790
+ isDir ? "text-muted-foreground" : "text-foreground"
1791
+ ),
1792
+ children: isDir ? `${child.name}/` : child.name
1793
+ }
1794
+ ),
1795
+ child.file?.change ? /* @__PURE__ */ jsx37(ChangeBadge, { change: child.file.change }) : null,
1796
+ child.file?.note ? /* @__PURE__ */ jsx37("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
1797
+ ] }),
1798
+ isDir ? /* @__PURE__ */ jsx37(TreeBranch, { node: child, depth: depth + 1 }) : null
1799
+ ] }, child.name);
1800
+ }) });
1801
+ }
1802
+ function FileTreeBlock({ entry }) {
1803
+ const data = parseFileTree(entry.data);
1804
+ if (!data) return /* @__PURE__ */ jsx37(UnreadableBlock, { entry });
1805
+ return /* @__PURE__ */ jsx37(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ jsx37("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ jsx37(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
1806
+ }
1807
+ function parseDataModel(data) {
1808
+ if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
1809
+ const fields = [];
1810
+ for (const raw of data.fields) {
1811
+ if (!isRecord(raw)) continue;
1812
+ const name = str(raw.name);
1813
+ if (!name) continue;
1814
+ fields.push({
1815
+ name,
1816
+ type: str(raw.type),
1817
+ note: str(raw.note),
1818
+ change: isChangeKind(raw.change) ? raw.change : void 0
1819
+ });
1820
+ }
1821
+ if (fields.length === 0) return void 0;
1822
+ return {
1823
+ title: str(data.title),
1824
+ authored: str(data.authored),
1825
+ name: str(data.name),
1826
+ fields
1827
+ };
1828
+ }
1829
+ function DataModelBlock({ entry }) {
1830
+ const data = parseDataModel(entry.data);
1831
+ if (!data) return /* @__PURE__ */ jsx37(UnreadableBlock, { entry });
1832
+ const hasNotes = data.fields.some((f) => f.note);
1833
+ return /* @__PURE__ */ jsx37(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ jsx37("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs24("table", { className: "w-full border-collapse text-sm", children: [
1834
+ /* @__PURE__ */ jsx37("thead", { children: /* @__PURE__ */ jsxs24("tr", { className: "bg-muted/50", children: [
1835
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
1836
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
1837
+ hasNotes ? /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
1838
+ ] }) }),
1839
+ /* @__PURE__ */ jsx37("tbody", { children: data.fields.map((field) => /* @__PURE__ */ jsxs24("tr", { children: [
1840
+ /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs24("span", { className: "flex flex-wrap items-center gap-2", children: [
1841
+ /* @__PURE__ */ jsx37("span", { className: "font-mono text-xs text-foreground", children: field.name }),
1842
+ field.change ? /* @__PURE__ */ jsx37(ChangeBadge, { change: field.change }) : null
1843
+ ] }) }),
1844
+ /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
1845
+ hasNotes ? /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
1846
+ ] }, field.name)) })
1847
+ ] }) }) });
1848
+ }
1849
+ function UnreadableBlock({ entry }) {
1850
+ return /* @__PURE__ */ jsxs24("div", { className: "my-2", "data-type": entry.type, children: [
1851
+ /* @__PURE__ */ jsxs24("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
1852
+ entry.type,
1853
+ " (unrecognised shape)"
1854
+ ] }),
1855
+ /* @__PURE__ */ jsx37(
1856
+ "pre",
1857
+ {
1858
+ tabIndex: 0,
1859
+ className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
1860
+ children: safeStringify2(entry.data)
1861
+ }
1862
+ )
1863
+ ] });
1864
+ }
1865
+ function safeStringify2(value) {
1866
+ try {
1867
+ return JSON.stringify(value, null, 2);
1868
+ } catch {
1869
+ return String(value);
1870
+ }
1871
+ }
1872
+ var narrativeBlockRenderers = {
1873
+ [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ jsx37(FileTreeBlock, { entry }),
1874
+ [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ jsx37(DataModelBlock, { entry })
1875
+ };
1876
+
1877
+ // src/context/ReportRoot.tsx
1878
+ import { jsx as jsx38 } from "react/jsx-runtime";
1879
+ var EMPTY_RENDERERS2 = {};
1880
+ function ReportRoot({
1881
+ report,
1882
+ customRenderers,
1883
+ renderers,
1884
+ children
1885
+ }) {
1886
+ const value = useMemo(
1887
+ () => ({
1888
+ report,
1889
+ // Narrative blocks render everywhere the report does (static SSR, the
1890
+ // island, Astro) without every host wiring them up. A host renderer for
1891
+ // the same type still wins.
1892
+ customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
1893
+ renderers: renderers ?? EMPTY_RENDERERS2
1894
+ }),
1895
+ [report, customRenderers, renderers]
1896
+ );
1897
+ return /* @__PURE__ */ jsx38(ReportContext.Provider, { value, children });
1898
+ }
1899
+
1900
+ // src/lib/hash-state.ts
1901
+ var DEFAULT_URL_STATE = {
1902
+ query: "",
1903
+ status: "all",
1904
+ tags: [],
1905
+ detail: "full"
1906
+ };
1907
+ var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
1908
+ function splitHash(hash) {
1909
+ const raw = hash.replace(/^#/, "");
1910
+ const split = raw.indexOf("?");
1911
+ if (split === -1) return { anchor: raw, params: new URLSearchParams() };
1912
+ return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
1913
+ }
1914
+ function decodeUrlState(params) {
1915
+ const status = params.get("status");
1916
+ const tags = params.get("tags");
1917
+ return {
1918
+ query: params.get("q") ?? DEFAULT_URL_STATE.query,
1919
+ status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
1920
+ tags: tags ? tags.split(",").filter(Boolean) : [],
1921
+ detail: params.get("docs") === "0" ? "minimal" : "full"
1922
+ };
1923
+ }
1924
+ function encodeUrlState(state) {
1925
+ const params = new URLSearchParams();
1926
+ if (state.query) params.set("q", state.query);
1927
+ if (state.status !== "all") params.set("status", state.status);
1928
+ if (state.tags.length > 0) params.set("tags", state.tags.join(","));
1929
+ if (state.detail === "minimal") params.set("docs", "0");
1930
+ return params.toString();
1931
+ }
1932
+ function formatHash(anchor, params) {
1933
+ if (params) return `#${anchor}?${params}`;
1934
+ return anchor ? `#${anchor}` : "";
1935
+ }
1936
+ function writeHash(anchor, params) {
1937
+ if (typeof window === "undefined") return;
1938
+ const next = formatHash(anchor, params);
1939
+ if (next === window.location.hash) return;
1940
+ const url = next || window.location.href.split("#")[0];
1941
+ try {
1942
+ window.history.replaceState(window.history.state, "", url);
1943
+ } catch {
1944
+ window.location.hash = next.slice(1);
1945
+ }
1946
+ }
1947
+
1948
+ // src/lib/scroll.ts
1949
+ function prefersReducedMotion() {
1950
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1951
+ return false;
1952
+ }
1953
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1954
+ }
1955
+ function scrollToScenarioId(scenarioId, options = {}) {
1956
+ if (typeof document === "undefined") return;
1957
+ const el = document.getElementById(scenarioId);
1958
+ if (!el) return;
1959
+ el.scrollIntoView({
1960
+ behavior: prefersReducedMotion() ? "auto" : "smooth",
1961
+ block: "start"
2008
1962
  });
2009
1963
  if (options.updateHash !== false) {
2010
1964
  writeHash(scenarioId, splitHash(window.location.hash).params.toString());
2011
1965
  }
2012
1966
  }
2013
1967
 
1968
+ // src/result.ts
1969
+ var ok = (data) => ({ ok: true, data });
1970
+ function unwrapReport(value) {
1971
+ if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
1972
+ return value;
1973
+ }
1974
+ return ok(value);
1975
+ }
1976
+
1977
+ // src/interactive/filter.ts
1978
+ function normalizeQuery(query) {
1979
+ return query.trim().toLowerCase();
1980
+ }
1981
+ function allTags(report) {
1982
+ const seen = /* @__PURE__ */ new Set();
1983
+ for (const feature of report.features) {
1984
+ for (const scenario of feature.scenarios) {
1985
+ for (const tag of scenario.tags) seen.add(tag);
1986
+ }
1987
+ }
1988
+ return [...seen];
1989
+ }
1990
+ function queryMatches(scenario, q) {
1991
+ if (q === "") return true;
1992
+ if (scenario.title.toLowerCase().includes(q)) return true;
1993
+ for (const tag of scenario.tags) {
1994
+ if (tag.toLowerCase().includes(q)) return true;
1995
+ }
1996
+ for (const step of scenario.steps) {
1997
+ if (step.text.toLowerCase().includes(q)) return true;
1998
+ }
1999
+ return false;
2000
+ }
2001
+ function scenarioMatches(scenario, q, status, tags) {
2002
+ if (status !== "all" && scenario.status !== status) return false;
2003
+ if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2004
+ return queryMatches(scenario, q);
2005
+ }
2006
+ function summarizeScenarios(scenarios) {
2007
+ let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
2008
+ for (const s of scenarios) {
2009
+ total += 1;
2010
+ durationMs += s.durationMs;
2011
+ if (s.status === "passed") passed += 1;
2012
+ else if (s.status === "failed") failed += 1;
2013
+ else if (s.status === "skipped") skipped += 1;
2014
+ else pending += 1;
2015
+ }
2016
+ return { total, passed, failed, skipped, pending, durationMs };
2017
+ }
2018
+ function filterReport(report, criteria) {
2019
+ const c = typeof criteria === "string" ? { query: criteria } : criteria;
2020
+ const q = normalizeQuery(c.query ?? "");
2021
+ const status = c.status ?? "all";
2022
+ const tags = c.tags ?? [];
2023
+ if (q === "" && status === "all" && tags.length === 0) return report;
2024
+ const features = [];
2025
+ let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2026
+ for (const feature of report.features) {
2027
+ const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2028
+ if (matched.length === 0) continue;
2029
+ const summary = summarizeScenarios(matched);
2030
+ features.push({ ...feature, summary, scenarios: matched });
2031
+ topTotal += summary.total;
2032
+ topPassed += summary.passed;
2033
+ topFailed += summary.failed;
2034
+ topSkipped += summary.skipped;
2035
+ topPending += summary.pending;
2036
+ topDuration += summary.durationMs;
2037
+ }
2038
+ return {
2039
+ ...report,
2040
+ summary: {
2041
+ total: topTotal,
2042
+ passed: topPassed,
2043
+ failed: topFailed,
2044
+ skipped: topSkipped,
2045
+ pending: topPending,
2046
+ durationMs: topDuration
2047
+ },
2048
+ features
2049
+ };
2050
+ }
2051
+ function listFailures(report) {
2052
+ const out = [];
2053
+ for (const feature of report.features) {
2054
+ for (const scenario of feature.scenarios) {
2055
+ if (scenario.status === "failed") {
2056
+ const ref = {
2057
+ featureId: feature.id,
2058
+ scenarioId: scenario.id,
2059
+ scenarioTitle: scenario.title
2060
+ };
2061
+ if (scenario.errorMessage !== void 0) {
2062
+ ref.errorMessage = scenario.errorMessage;
2063
+ }
2064
+ out.push(ref);
2065
+ }
2066
+ }
2067
+ }
2068
+ return out;
2069
+ }
2070
+
2014
2071
  // src/interactive/ReportFailureBanner.tsx
2015
- import { jsx as jsx41, jsxs as jsxs25 } from "react/jsx-runtime";
2072
+ import { useCallback as useCallback2 } from "react";
2073
+ import { jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
2016
2074
  function ReportFailureBanner({ failures }) {
2017
2075
  const first = failures[0];
2018
2076
  const jumpToFirst = useCallback2(() => {
@@ -2029,12 +2087,12 @@ function ReportFailureBanner({ failures }) {
2029
2087
  "aria-label": "Failure summary",
2030
2088
  children: [
2031
2089
  /* @__PURE__ */ jsxs25("span", { className: "es-failure-banner-text", children: [
2032
- /* @__PURE__ */ jsx41("strong", { children: failures.length }),
2090
+ /* @__PURE__ */ jsx39("strong", { children: failures.length }),
2033
2091
  " ",
2034
2092
  "failure",
2035
2093
  failures.length === 1 ? "" : "s"
2036
2094
  ] }),
2037
- /* @__PURE__ */ jsx41(
2095
+ /* @__PURE__ */ jsx39(
2038
2096
  "button",
2039
2097
  {
2040
2098
  type: "button",
@@ -2049,17 +2107,91 @@ function ReportFailureBanner({ failures }) {
2049
2107
  );
2050
2108
  }
2051
2109
 
2110
+ // src/interactive/ReportFilters.tsx
2111
+ import { useState as useState3 } from "react";
2112
+ import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
2113
+ var TAG_LIMIT = 12;
2114
+ function ReportFilters({
2115
+ statuses,
2116
+ status,
2117
+ onStatus,
2118
+ tags,
2119
+ activeTags,
2120
+ onToggleTag
2121
+ }) {
2122
+ const [showAllTags, setShowAllTags] = useState3(false);
2123
+ if (statuses.length === 0 && tags.length === 0) return null;
2124
+ const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2125
+ const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2126
+ return /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-2", children: [
2127
+ statuses.length > 0 ? /* @__PURE__ */ jsx40("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2128
+ const active = status === opt.key;
2129
+ return /* @__PURE__ */ jsxs26(
2130
+ "button",
2131
+ {
2132
+ type: "button",
2133
+ "aria-pressed": active,
2134
+ onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
2135
+ className: cn(
2136
+ "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
2137
+ active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
2138
+ ),
2139
+ children: [
2140
+ opt.label,
2141
+ /* @__PURE__ */ jsx40("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2142
+ ]
2143
+ },
2144
+ opt.key
2145
+ );
2146
+ }) }) : null,
2147
+ tags.length > 0 ? /* @__PURE__ */ jsxs26("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2148
+ visibleTags.map((tag) => {
2149
+ const active = activeTags.includes(tag);
2150
+ return /* @__PURE__ */ jsx40("li", { children: /* @__PURE__ */ jsx40(
2151
+ "button",
2152
+ {
2153
+ type: "button",
2154
+ "aria-pressed": active,
2155
+ onClick: () => onToggleTag(tag),
2156
+ className: "cursor-pointer",
2157
+ children: /* @__PURE__ */ jsx40(
2158
+ Badge,
2159
+ {
2160
+ variant: "tag",
2161
+ className: cn(
2162
+ active && "border-primary bg-primary text-primary-foreground"
2163
+ ),
2164
+ children: tag
2165
+ }
2166
+ )
2167
+ }
2168
+ ) }, tag);
2169
+ }),
2170
+ tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx40("li", { children: /* @__PURE__ */ jsx40(
2171
+ "button",
2172
+ {
2173
+ type: "button",
2174
+ "aria-expanded": showAllTags,
2175
+ onClick: () => setShowAllTags((v) => !v),
2176
+ className: "cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2177
+ children: tagsCollapsed ? `+${tags.length - TAG_LIMIT} more` : "Show fewer"
2178
+ }
2179
+ ) }) : null
2180
+ ] }) : null
2181
+ ] });
2182
+ }
2183
+
2052
2184
  // src/interactive/ReportFreshness.tsx
2053
- import { useEffect, useState as useState2 } from "react";
2054
- import { Fragment as Fragment11, jsx as jsx42, jsxs as jsxs26 } from "react/jsx-runtime";
2185
+ import { useEffect as useEffect2, useState as useState4 } from "react";
2186
+ import { Fragment as Fragment11, jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
2055
2187
  function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2056
- const [now, setNow] = useState2(nowMs);
2057
- useEffect(() => {
2188
+ const [now, setNow] = useState4(nowMs);
2189
+ useEffect2(() => {
2058
2190
  if (nowMs === void 0) setNow(Date.now());
2059
2191
  }, [nowMs]);
2060
2192
  if (lastRunMs === void 0 || now === void 0) return null;
2061
2193
  const age = formatRelativeAge(lastRunMs, now);
2062
- const runLink = ciUrl ? /* @__PURE__ */ jsx42(
2194
+ const runLink = ciUrl ? /* @__PURE__ */ jsx41(
2063
2195
  "a",
2064
2196
  {
2065
2197
  href: ciUrl,
@@ -2070,9 +2202,9 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2070
2202
  }
2071
2203
  ) : null;
2072
2204
  if (isReportStale(lastRunMs, staleAfterDays, now)) {
2073
- return /* @__PURE__ */ jsxs26("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2074
- /* @__PURE__ */ jsxs26("span", { children: [
2075
- /* @__PURE__ */ jsxs26("strong", { children: [
2205
+ return /* @__PURE__ */ jsxs27("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2206
+ /* @__PURE__ */ jsxs27("span", { children: [
2207
+ /* @__PURE__ */ jsxs27("strong", { children: [
2076
2208
  "Last verified ",
2077
2209
  age,
2078
2210
  "."
@@ -2082,10 +2214,10 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2082
2214
  runLink
2083
2215
  ] });
2084
2216
  }
2085
- return /* @__PURE__ */ jsxs26("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2217
+ return /* @__PURE__ */ jsxs27("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2086
2218
  "Verified ",
2087
2219
  age,
2088
- runLink ? /* @__PURE__ */ jsxs26(Fragment11, { children: [
2220
+ runLink ? /* @__PURE__ */ jsxs27(Fragment11, { children: [
2089
2221
  " \xB7 ",
2090
2222
  runLink
2091
2223
  ] }) : null
@@ -2093,15 +2225,15 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2093
2225
  }
2094
2226
 
2095
2227
  // src/interactive/ReportLastRunDelta.tsx
2096
- import { Fragment as Fragment12, jsx as jsx43, jsxs as jsxs27 } from "react/jsx-runtime";
2228
+ import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs28 } from "react/jsx-runtime";
2097
2229
  var MAX_LINKS = 5;
2098
2230
  function ScenarioLinks({ scenarios, tone }) {
2099
- return /* @__PURE__ */ jsxs27(Fragment12, { children: [
2100
- scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs27("span", { children: [
2231
+ return /* @__PURE__ */ jsxs28(Fragment12, { children: [
2232
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs28("span", { children: [
2101
2233
  i > 0 ? ", " : "",
2102
- /* @__PURE__ */ jsx43("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2234
+ /* @__PURE__ */ jsx42("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2103
2235
  ] }, s.id)),
2104
- scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs27("span", { children: [
2236
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs28("span", { children: [
2105
2237
  " and ",
2106
2238
  scenarios.length - MAX_LINKS,
2107
2239
  " more"
@@ -2118,31 +2250,125 @@ function ReportLastRunDelta({
2118
2250
  if (!delta) return null;
2119
2251
  const { newlyFailing, newlyPassing, added } = delta;
2120
2252
  if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
2121
- return /* @__PURE__ */ jsx43("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2253
+ return /* @__PURE__ */ jsx42("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2254
+ }
2255
+ return /* @__PURE__ */ jsxs28("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2256
+ /* @__PURE__ */ jsx42("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2257
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2 text-fail", children: [
2258
+ newlyFailing.length,
2259
+ " newly failing (",
2260
+ /* @__PURE__ */ jsx42(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2261
+ ")"
2262
+ ] }) : null,
2263
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2 text-pass", children: [
2264
+ newlyPassing.length,
2265
+ " fixed (",
2266
+ /* @__PURE__ */ jsx42(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2267
+ ")"
2268
+ ] }) : null,
2269
+ added.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2", children: [
2270
+ added.length,
2271
+ " new"
2272
+ ] }) : null
2273
+ ] });
2274
+ }
2275
+
2276
+ // src/interactive/ReportSearch.tsx
2277
+ import {
2278
+ forwardRef,
2279
+ useId
2280
+ } from "react";
2281
+ import { Search } from "lucide-react";
2282
+
2283
+ // src/components/ui/input.tsx
2284
+ import "react";
2285
+ import { jsx as jsx43 } from "react/jsx-runtime";
2286
+ function Input({ className, type, ...props }) {
2287
+ return /* @__PURE__ */ jsx43(
2288
+ "input",
2289
+ {
2290
+ type,
2291
+ "data-slot": "input",
2292
+ className: cn(
2293
+ "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",
2294
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
2295
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
2296
+ className
2297
+ ),
2298
+ ...props
2299
+ }
2300
+ );
2301
+ }
2302
+
2303
+ // src/interactive/ReportSearch.tsx
2304
+ import { jsx as jsx44, jsxs as jsxs29 } from "react/jsx-runtime";
2305
+ var ReportSearch = forwardRef(
2306
+ function ReportSearch2(props, ref) {
2307
+ const {
2308
+ value,
2309
+ onChange,
2310
+ matchedCount,
2311
+ totalCount,
2312
+ placeholder = "Search scenarios or tags\u2026",
2313
+ className
2314
+ } = props;
2315
+ const inputId = useId();
2316
+ const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
2317
+ function handleChange(e) {
2318
+ onChange(e.target.value);
2319
+ }
2320
+ function handleKeyDown(e) {
2321
+ if (e.key === "Escape" && value !== "") {
2322
+ onChange("");
2323
+ e.preventDefault();
2324
+ }
2325
+ }
2326
+ return /* @__PURE__ */ jsxs29("div", { className: cn("es-search flex flex-col gap-1", className), children: [
2327
+ /* @__PURE__ */ jsx44("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
2328
+ /* @__PURE__ */ jsxs29("div", { className: "relative w-full sm:w-72", children: [
2329
+ /* @__PURE__ */ jsx44(
2330
+ Search,
2331
+ {
2332
+ "aria-hidden": true,
2333
+ className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
2334
+ }
2335
+ ),
2336
+ /* @__PURE__ */ jsx44(
2337
+ Input,
2338
+ {
2339
+ ref,
2340
+ id: inputId,
2341
+ type: "search",
2342
+ value,
2343
+ onChange: handleChange,
2344
+ onKeyDown: handleKeyDown,
2345
+ placeholder,
2346
+ autoComplete: "off",
2347
+ spellCheck: false,
2348
+ "aria-keyshortcuts": "/",
2349
+ className: "pl-8"
2350
+ }
2351
+ )
2352
+ ] }),
2353
+ showCounts ? /* @__PURE__ */ jsxs29(
2354
+ "span",
2355
+ {
2356
+ className: "text-xs tabular-nums text-muted-foreground",
2357
+ "aria-live": "polite",
2358
+ children: [
2359
+ matchedCount,
2360
+ " of ",
2361
+ totalCount,
2362
+ " scenarios"
2363
+ ]
2364
+ }
2365
+ ) : null
2366
+ ] });
2122
2367
  }
2123
- return /* @__PURE__ */ jsxs27("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2124
- /* @__PURE__ */ jsx43("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2125
- newlyFailing.length > 0 ? /* @__PURE__ */ jsxs27("span", { className: "ml-2 text-fail", children: [
2126
- newlyFailing.length,
2127
- " newly failing (",
2128
- /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2129
- ")"
2130
- ] }) : null,
2131
- newlyPassing.length > 0 ? /* @__PURE__ */ jsxs27("span", { className: "ml-2 text-pass", children: [
2132
- newlyPassing.length,
2133
- " fixed (",
2134
- /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2135
- ")"
2136
- ] }) : null,
2137
- added.length > 0 ? /* @__PURE__ */ jsxs27("span", { className: "ml-2", children: [
2138
- added.length,
2139
- " new"
2140
- ] }) : null
2141
- ] });
2142
- }
2368
+ );
2143
2369
 
2144
2370
  // src/interactive/ReportShortcutsHelp.tsx
2145
- import { useEffect as useEffect2, useRef } from "react";
2371
+ import { useEffect as useEffect3, useRef } from "react";
2146
2372
 
2147
2373
  // src/components/ui/button.tsx
2148
2374
  import "react";
@@ -2185,335 +2411,66 @@ function Button({
2185
2411
  ...props
2186
2412
  }) {
2187
2413
  return useRender2({
2188
- defaultTagName: "button",
2189
- render,
2190
- props: {
2191
- "data-slot": "button",
2192
- "data-variant": variant,
2193
- "data-size": size,
2194
- className: cn(buttonVariants({ variant, size, className })),
2195
- ...props
2196
- }
2197
- });
2198
- }
2199
-
2200
- // src/interactive/ReportShortcutsHelp.tsx
2201
- import { jsx as jsx44, jsxs as jsxs28 } from "react/jsx-runtime";
2202
- var SHORTCUTS = [
2203
- { keys: "/", description: "Focus search" },
2204
- { keys: "f", description: "Jump to next failure" },
2205
- { keys: "Shift F", description: "Jump to previous failure" },
2206
- { keys: "e", description: "Expand all" },
2207
- { keys: "c", description: "Collapse all" },
2208
- { keys: "?", description: "Toggle this help" },
2209
- { keys: "Esc", description: "Clear search / close" }
2210
- ];
2211
- function Keys({ keys }) {
2212
- const parts = keys.split(" ");
2213
- return /* @__PURE__ */ jsx44("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs28("span", { className: "inline-flex items-center gap-1", children: [
2214
- i > 0 ? /* @__PURE__ */ jsx44("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2215
- /* @__PURE__ */ jsx44("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 })
2216
- ] }, k)) });
2217
- }
2218
- function ReportShortcutsHelp({ open, onClose }) {
2219
- const dialogRef = useRef(null);
2220
- useEffect2(() => {
2221
- const dialog = dialogRef.current;
2222
- if (!dialog) return;
2223
- if (open && !dialog.open) dialog.showModal();
2224
- else if (!open && dialog.open) dialog.close();
2225
- }, [open]);
2226
- return /* @__PURE__ */ jsxs28(
2227
- "dialog",
2228
- {
2229
- ref: dialogRef,
2230
- 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",
2231
- "aria-label": "Keyboard shortcuts",
2232
- onClose,
2233
- children: [
2234
- /* @__PURE__ */ jsx44("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2235
- /* @__PURE__ */ jsx44("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs28("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2236
- /* @__PURE__ */ jsx44("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx44(Keys, { keys: s.keys }) }),
2237
- /* @__PURE__ */ jsx44("dd", { className: "text-sm text-muted-foreground", children: s.description })
2238
- ] }, s.keys)) }),
2239
- /* @__PURE__ */ jsx44("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx44(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2240
- ]
2241
- }
2242
- );
2243
- }
2244
-
2245
- // src/interactive/use-keyboard-shortcuts.ts
2246
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
2247
- function isEditableTarget(target) {
2248
- if (!(target instanceof HTMLElement)) return false;
2249
- const tag = target.tagName;
2250
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2251
- if (target.isContentEditable) return true;
2252
- return false;
2253
- }
2254
- function useKeyboardShortcuts(handlers) {
2255
- const ref = useRef2(handlers);
2256
- ref.current = handlers;
2257
- useEffect3(() => {
2258
- function onKeyDown(e) {
2259
- const h = ref.current;
2260
- if (e.key === "Escape") {
2261
- h.onEscape?.();
2262
- return;
2263
- }
2264
- if (isEditableTarget(e.target)) return;
2265
- if (e.metaKey || e.ctrlKey || e.altKey) return;
2266
- switch (e.key) {
2267
- case "/":
2268
- h.onFocusSearch?.();
2269
- e.preventDefault();
2270
- return;
2271
- case "?":
2272
- h.onToggleHelp?.();
2273
- e.preventDefault();
2274
- return;
2275
- case "f":
2276
- h.onNextFailure?.();
2277
- e.preventDefault();
2278
- return;
2279
- case "F":
2280
- h.onPrevFailure?.();
2281
- e.preventDefault();
2282
- return;
2283
- case "e":
2284
- h.onExpandAll?.();
2285
- e.preventDefault();
2286
- return;
2287
- case "c":
2288
- h.onCollapseAll?.();
2289
- e.preventDefault();
2290
- return;
2291
- default:
2292
- return;
2293
- }
2294
- }
2295
- window.addEventListener("keydown", onKeyDown);
2296
- return () => window.removeEventListener("keydown", onKeyDown);
2297
- }, []);
2298
- }
2299
-
2300
- // src/interactive/use-deep-link-scroll.ts
2301
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
2302
- function useDeepLinkScroll() {
2303
- const lastAnchor = useRef3(null);
2304
- useEffect4(() => {
2305
- function scrollToHash() {
2306
- const { anchor } = splitHash(window.location.hash);
2307
- if (!anchor || anchor === lastAnchor.current) return;
2308
- lastAnchor.current = anchor;
2309
- requestAnimationFrame(() => {
2310
- scrollToScenarioId(anchor, { updateHash: false });
2311
- });
2312
- }
2313
- scrollToHash();
2314
- window.addEventListener("hashchange", scrollToHash);
2315
- return () => window.removeEventListener("hashchange", scrollToHash);
2316
- }, []);
2317
- }
2318
-
2319
- // src/interactive/filter.ts
2320
- function normalizeQuery(query) {
2321
- return query.trim().toLowerCase();
2322
- }
2323
- function allTags(report) {
2324
- const seen = /* @__PURE__ */ new Set();
2325
- for (const feature of report.features) {
2326
- for (const scenario of feature.scenarios) {
2327
- for (const tag of scenario.tags) seen.add(tag);
2328
- }
2329
- }
2330
- return [...seen];
2331
- }
2332
- function queryMatches(scenario, q) {
2333
- if (q === "") return true;
2334
- if (scenario.title.toLowerCase().includes(q)) return true;
2335
- for (const tag of scenario.tags) {
2336
- if (tag.toLowerCase().includes(q)) return true;
2337
- }
2338
- for (const step of scenario.steps) {
2339
- if (step.text.toLowerCase().includes(q)) return true;
2340
- }
2341
- return false;
2342
- }
2343
- function scenarioMatches(scenario, q, status, tags) {
2344
- if (status !== "all" && scenario.status !== status) return false;
2345
- if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2346
- return queryMatches(scenario, q);
2347
- }
2348
- function summarizeScenarios(scenarios) {
2349
- let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
2350
- for (const s of scenarios) {
2351
- total += 1;
2352
- durationMs += s.durationMs;
2353
- if (s.status === "passed") passed += 1;
2354
- else if (s.status === "failed") failed += 1;
2355
- else if (s.status === "skipped") skipped += 1;
2356
- else pending += 1;
2357
- }
2358
- return { total, passed, failed, skipped, pending, durationMs };
2359
- }
2360
- function filterReport(report, criteria) {
2361
- const c = typeof criteria === "string" ? { query: criteria } : criteria;
2362
- const q = normalizeQuery(c.query ?? "");
2363
- const status = c.status ?? "all";
2364
- const tags = c.tags ?? [];
2365
- if (q === "" && status === "all" && tags.length === 0) return report;
2366
- const features = [];
2367
- let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2368
- for (const feature of report.features) {
2369
- const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2370
- if (matched.length === 0) continue;
2371
- const summary = summarizeScenarios(matched);
2372
- features.push({ ...feature, summary, scenarios: matched });
2373
- topTotal += summary.total;
2374
- topPassed += summary.passed;
2375
- topFailed += summary.failed;
2376
- topSkipped += summary.skipped;
2377
- topPending += summary.pending;
2378
- topDuration += summary.durationMs;
2379
- }
2380
- return {
2381
- ...report,
2382
- summary: {
2383
- total: topTotal,
2384
- passed: topPassed,
2385
- failed: topFailed,
2386
- skipped: topSkipped,
2387
- pending: topPending,
2388
- durationMs: topDuration
2389
- },
2390
- features
2391
- };
2392
- }
2393
- function listFailures(report) {
2394
- const out = [];
2395
- for (const feature of report.features) {
2396
- for (const scenario of feature.scenarios) {
2397
- if (scenario.status === "failed") {
2398
- const ref = {
2399
- featureId: feature.id,
2400
- scenarioId: scenario.id,
2401
- scenarioTitle: scenario.title
2402
- };
2403
- if (scenario.errorMessage !== void 0) {
2404
- ref.errorMessage = scenario.errorMessage;
2405
- }
2406
- out.push(ref);
2407
- }
2408
- }
2409
- }
2410
- return out;
2411
- }
2412
-
2413
- // src/interactive/triage-default.ts
2414
- function needsAttention(status) {
2415
- return status === "failed";
2416
- }
2417
- function defaultCollapsedIds(report) {
2418
- const collapsed = /* @__PURE__ */ new Set();
2419
- if (report.summary.failed === 0) return collapsed;
2420
- for (const feature of report.features) {
2421
- const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2422
- if (!featureHasFailure) collapsed.add(feature.id);
2423
- for (const scenario of feature.scenarios) {
2424
- if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2425
- }
2426
- }
2427
- return collapsed;
2428
- }
2429
- function failuresFirst(report) {
2430
- if (report.summary.failed === 0) return report;
2431
- const withFailures = [];
2432
- const rest = [];
2433
- for (const feature of report.features) {
2434
- (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2435
- }
2436
- if (withFailures.length === 0) return report;
2437
- return { ...report, features: [...withFailures, ...rest] };
2414
+ defaultTagName: "button",
2415
+ render,
2416
+ props: {
2417
+ "data-slot": "button",
2418
+ "data-variant": variant,
2419
+ "data-size": size,
2420
+ className: cn(buttonVariants({ variant, size, className })),
2421
+ ...props
2422
+ }
2423
+ });
2438
2424
  }
2439
2425
 
2440
- // src/interactive/ReportFilters.tsx
2441
- import { useState as useState3 } from "react";
2442
- import { jsx as jsx45, jsxs as jsxs29 } from "react/jsx-runtime";
2443
- var TAG_LIMIT = 12;
2444
- function ReportFilters({
2445
- statuses,
2446
- status,
2447
- onStatus,
2448
- tags,
2449
- activeTags,
2450
- onToggleTag
2451
- }) {
2452
- const [showAllTags, setShowAllTags] = useState3(false);
2453
- if (statuses.length === 0 && tags.length === 0) return null;
2454
- const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2455
- const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2456
- return /* @__PURE__ */ jsxs29("div", { className: "flex flex-col gap-2", children: [
2457
- statuses.length > 0 ? /* @__PURE__ */ jsx45("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2458
- const active = status === opt.key;
2459
- return /* @__PURE__ */ jsxs29(
2460
- "button",
2461
- {
2462
- type: "button",
2463
- "aria-pressed": active,
2464
- onClick: () => onStatus(active && opt.key !== "all" ? "all" : opt.key),
2465
- className: cn(
2466
- "cursor-pointer rounded-md border px-2 py-1 text-xs transition-colors",
2467
- active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
2468
- ),
2469
- children: [
2470
- opt.label,
2471
- /* @__PURE__ */ jsx45("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2472
- ]
2473
- },
2474
- opt.key
2475
- );
2476
- }) }) : null,
2477
- tags.length > 0 ? /* @__PURE__ */ jsxs29("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2478
- visibleTags.map((tag) => {
2479
- const active = activeTags.includes(tag);
2480
- return /* @__PURE__ */ jsx45("li", { children: /* @__PURE__ */ jsx45(
2481
- "button",
2482
- {
2483
- type: "button",
2484
- "aria-pressed": active,
2485
- onClick: () => onToggleTag(tag),
2486
- className: "cursor-pointer",
2487
- children: /* @__PURE__ */ jsx45(
2488
- Badge,
2489
- {
2490
- variant: "tag",
2491
- className: cn(
2492
- active && "border-primary bg-primary text-primary-foreground"
2493
- ),
2494
- children: tag
2495
- }
2496
- )
2497
- }
2498
- ) }, tag);
2499
- }),
2500
- tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx45("li", { children: /* @__PURE__ */ jsx45(
2501
- "button",
2502
- {
2503
- type: "button",
2504
- "aria-expanded": showAllTags,
2505
- onClick: () => setShowAllTags((v) => !v),
2506
- className: "cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground",
2507
- children: tagsCollapsed ? `+${tags.length - TAG_LIMIT} more` : "Show fewer"
2508
- }
2509
- ) }) : null
2510
- ] }) : null
2511
- ] });
2426
+ // src/interactive/ReportShortcutsHelp.tsx
2427
+ import { jsx as jsx45, jsxs as jsxs30 } from "react/jsx-runtime";
2428
+ var SHORTCUTS = [
2429
+ { keys: "/", description: "Focus search" },
2430
+ { keys: "f", description: "Jump to next failure" },
2431
+ { keys: "Shift F", description: "Jump to previous failure" },
2432
+ { keys: "e", description: "Expand all" },
2433
+ { keys: "c", description: "Collapse all" },
2434
+ { keys: "?", description: "Toggle this help" },
2435
+ { keys: "Esc", description: "Clear search / close" }
2436
+ ];
2437
+ function Keys({ keys }) {
2438
+ const parts = keys.split(" ");
2439
+ return /* @__PURE__ */ jsx45("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs30("span", { className: "inline-flex items-center gap-1", children: [
2440
+ i > 0 ? /* @__PURE__ */ jsx45("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2441
+ /* @__PURE__ */ jsx45("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 })
2442
+ ] }, k)) });
2443
+ }
2444
+ function ReportShortcutsHelp({ open, onClose }) {
2445
+ const dialogRef = useRef(null);
2446
+ useEffect3(() => {
2447
+ const dialog = dialogRef.current;
2448
+ if (!dialog) return;
2449
+ if (open && !dialog.open) dialog.showModal();
2450
+ else if (!open && dialog.open) dialog.close();
2451
+ }, [open]);
2452
+ return /* @__PURE__ */ jsxs30(
2453
+ "dialog",
2454
+ {
2455
+ ref: dialogRef,
2456
+ 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",
2457
+ "aria-label": "Keyboard shortcuts",
2458
+ onClose,
2459
+ children: [
2460
+ /* @__PURE__ */ jsx45("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2461
+ /* @__PURE__ */ jsx45("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs30("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2462
+ /* @__PURE__ */ jsx45("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx45(Keys, { keys: s.keys }) }),
2463
+ /* @__PURE__ */ jsx45("dd", { className: "text-sm text-muted-foreground", children: s.description })
2464
+ ] }, s.keys)) }),
2465
+ /* @__PURE__ */ jsx45("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx45(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2466
+ ]
2467
+ }
2468
+ );
2512
2469
  }
2513
2470
 
2514
2471
  // src/interactive/ReportToc.tsx
2515
- import { useEffect as useEffect5, useMemo as useMemo2, useState as useState4 } from "react";
2516
- import { jsx as jsx46, jsxs as jsxs30 } from "react/jsx-runtime";
2472
+ import { useEffect as useEffect4, useMemo as useMemo2, useState as useState5 } from "react";
2473
+ import { jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2517
2474
  var DOT = {
2518
2475
  passed: "bg-pass",
2519
2476
  failed: "bg-fail",
@@ -2522,12 +2479,12 @@ var DOT = {
2522
2479
  };
2523
2480
  function TocContent({ onNavigate }) {
2524
2481
  const report = useReport();
2525
- const [activeId, setActiveId] = useState4(null);
2482
+ const [activeId, setActiveId] = useState5(null);
2526
2483
  const scenarioIds = useMemo2(
2527
2484
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
2528
2485
  [report]
2529
2486
  );
2530
- useEffect5(() => {
2487
+ useEffect4(() => {
2531
2488
  if (typeof IntersectionObserver === "undefined") return;
2532
2489
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
2533
2490
  if (els.length === 0) return;
@@ -2542,11 +2499,11 @@ function TocContent({ onNavigate }) {
2542
2499
  return () => observer.disconnect();
2543
2500
  }, [scenarioIds]);
2544
2501
  if (report.features.length === 0) return null;
2545
- return /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs30("li", { children: [
2502
+ return /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs31("li", { children: [
2546
2503
  /* @__PURE__ */ jsx46("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2547
2504
  /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2548
2505
  const active = activeId === scenario.id;
2549
- return /* @__PURE__ */ jsx46("li", { children: /* @__PURE__ */ jsxs30(
2506
+ return /* @__PURE__ */ jsx46("li", { children: /* @__PURE__ */ jsxs31(
2550
2507
  "a",
2551
2508
  {
2552
2509
  href: `#${scenario.id}`,
@@ -2578,7 +2535,7 @@ function TocContent({ onNavigate }) {
2578
2535
  function ReportToc() {
2579
2536
  const report = useReport();
2580
2537
  if (report.features.length === 0) return null;
2581
- return /* @__PURE__ */ jsxs30(
2538
+ return /* @__PURE__ */ jsxs31(
2582
2539
  "nav",
2583
2540
  {
2584
2541
  "aria-label": "Table of contents",
@@ -2592,23 +2549,23 @@ function ReportToc() {
2592
2549
  }
2593
2550
 
2594
2551
  // src/interactive/ReportTocDrawer.tsx
2595
- import { useState as useState5 } from "react";
2552
+ import { useState as useState6 } from "react";
2596
2553
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2597
2554
  import { ListIcon, XIcon } from "lucide-react";
2598
- import { jsx as jsx47, jsxs as jsxs31 } from "react/jsx-runtime";
2555
+ import { jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2599
2556
  function ReportTocDrawer() {
2600
2557
  const report = useReport();
2601
- const [open, setOpen] = useState5(false);
2558
+ const [open, setOpen] = useState6(false);
2602
2559
  if (report.features.length === 0) return null;
2603
- return /* @__PURE__ */ jsxs31(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2604
- /* @__PURE__ */ jsxs31(DialogPrimitive.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: [
2560
+ return /* @__PURE__ */ jsxs32(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2561
+ /* @__PURE__ */ jsxs32(DialogPrimitive.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: [
2605
2562
  /* @__PURE__ */ jsx47(ListIcon, { "aria-hidden": true }),
2606
2563
  " Contents"
2607
2564
  ] }),
2608
- /* @__PURE__ */ jsxs31(DialogPrimitive.Portal, { children: [
2565
+ /* @__PURE__ */ jsxs32(DialogPrimitive.Portal, { children: [
2609
2566
  /* @__PURE__ */ jsx47(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2610
- /* @__PURE__ */ jsxs31(DialogPrimitive.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: [
2611
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center justify-between", children: [
2567
+ /* @__PURE__ */ jsxs32(DialogPrimitive.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: [
2568
+ /* @__PURE__ */ jsxs32("div", { className: "flex items-center justify-between", children: [
2612
2569
  /* @__PURE__ */ jsx47(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2613
2570
  /* @__PURE__ */ jsx47(
2614
2571
  DialogPrimitive.Close,
@@ -2625,8 +2582,109 @@ function ReportTocDrawer() {
2625
2582
  ] });
2626
2583
  }
2627
2584
 
2585
+ // src/interactive/triage-default.ts
2586
+ function needsAttention(status) {
2587
+ return status === "failed";
2588
+ }
2589
+ function defaultCollapsedIds(report) {
2590
+ const collapsed = /* @__PURE__ */ new Set();
2591
+ if (report.summary.failed === 0) return collapsed;
2592
+ for (const feature of report.features) {
2593
+ const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2594
+ if (!featureHasFailure) collapsed.add(feature.id);
2595
+ for (const scenario of feature.scenarios) {
2596
+ if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2597
+ }
2598
+ }
2599
+ return collapsed;
2600
+ }
2601
+ function failuresFirst(report) {
2602
+ if (report.summary.failed === 0) return report;
2603
+ const withFailures = [];
2604
+ const rest = [];
2605
+ for (const feature of report.features) {
2606
+ (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2607
+ }
2608
+ if (withFailures.length === 0) return report;
2609
+ return { ...report, features: [...withFailures, ...rest] };
2610
+ }
2611
+
2612
+ // src/interactive/use-deep-link-scroll.ts
2613
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
2614
+ function useDeepLinkScroll() {
2615
+ const lastAnchor = useRef2(null);
2616
+ useEffect5(() => {
2617
+ function scrollToHash() {
2618
+ const { anchor } = splitHash(window.location.hash);
2619
+ if (!anchor || anchor === lastAnchor.current) return;
2620
+ lastAnchor.current = anchor;
2621
+ requestAnimationFrame(() => {
2622
+ scrollToScenarioId(anchor, { updateHash: false });
2623
+ });
2624
+ }
2625
+ scrollToHash();
2626
+ window.addEventListener("hashchange", scrollToHash);
2627
+ return () => window.removeEventListener("hashchange", scrollToHash);
2628
+ }, []);
2629
+ }
2630
+
2631
+ // src/interactive/use-keyboard-shortcuts.ts
2632
+ import { useEffect as useEffect6, useRef as useRef3 } from "react";
2633
+ function isEditableTarget(target) {
2634
+ if (!(target instanceof HTMLElement)) return false;
2635
+ const tag = target.tagName;
2636
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2637
+ if (target.isContentEditable) return true;
2638
+ return false;
2639
+ }
2640
+ function useKeyboardShortcuts(handlers) {
2641
+ const ref = useRef3(handlers);
2642
+ ref.current = handlers;
2643
+ useEffect6(() => {
2644
+ function onKeyDown(e) {
2645
+ const h = ref.current;
2646
+ if (e.key === "Escape") {
2647
+ h.onEscape?.();
2648
+ return;
2649
+ }
2650
+ if (isEditableTarget(e.target)) return;
2651
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
2652
+ switch (e.key) {
2653
+ case "/":
2654
+ h.onFocusSearch?.();
2655
+ e.preventDefault();
2656
+ return;
2657
+ case "?":
2658
+ h.onToggleHelp?.();
2659
+ e.preventDefault();
2660
+ return;
2661
+ case "f":
2662
+ h.onNextFailure?.();
2663
+ e.preventDefault();
2664
+ return;
2665
+ case "F":
2666
+ h.onPrevFailure?.();
2667
+ e.preventDefault();
2668
+ return;
2669
+ case "e":
2670
+ h.onExpandAll?.();
2671
+ e.preventDefault();
2672
+ return;
2673
+ case "c":
2674
+ h.onCollapseAll?.();
2675
+ e.preventDefault();
2676
+ return;
2677
+ default:
2678
+ return;
2679
+ }
2680
+ }
2681
+ window.addEventListener("keydown", onKeyDown);
2682
+ return () => window.removeEventListener("keydown", onKeyDown);
2683
+ }, []);
2684
+ }
2685
+
2628
2686
  // src/interactive/use-theme.ts
2629
- import { useCallback as useCallback3, useEffect as useEffect6, useState as useState6 } from "react";
2687
+ import { useCallback as useCallback3, useEffect as useEffect7, useState as useState7 } from "react";
2630
2688
  var KEY = "es-theme";
2631
2689
  function systemDark() {
2632
2690
  return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
@@ -2647,13 +2705,13 @@ function initialPref() {
2647
2705
  return "system";
2648
2706
  }
2649
2707
  function useTheme() {
2650
- const [pref, setPrefState] = useState6("system");
2651
- useEffect6(() => {
2708
+ const [pref, setPrefState] = useState7("system");
2709
+ useEffect7(() => {
2652
2710
  const p = initialPref();
2653
2711
  setPrefState(p);
2654
2712
  applyTheme(p);
2655
2713
  }, []);
2656
- useEffect6(() => {
2714
+ useEffect7(() => {
2657
2715
  if (typeof matchMedia === "undefined") return;
2658
2716
  const mq = matchMedia("(prefers-color-scheme: dark)");
2659
2717
  const onChange = () => {
@@ -2674,19 +2732,19 @@ function useTheme() {
2674
2732
  }
2675
2733
 
2676
2734
  // src/interactive/use-url-state.ts
2677
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef4, useState as useState7 } from "react";
2735
+ import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef4, useState as useState8 } from "react";
2678
2736
  var WRITE_DELAY_MS = 250;
2679
2737
  function useUrlState() {
2680
- const [state, setState] = useState7(DEFAULT_URL_STATE);
2738
+ const [state, setState] = useState8(DEFAULT_URL_STATE);
2681
2739
  const ready = useRef4(false);
2682
- useEffect7(() => {
2740
+ useEffect8(() => {
2683
2741
  const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2684
2742
  read();
2685
2743
  ready.current = true;
2686
2744
  window.addEventListener("hashchange", read);
2687
2745
  return () => window.removeEventListener("hashchange", read);
2688
2746
  }, []);
2689
- useEffect7(() => {
2747
+ useEffect8(() => {
2690
2748
  if (!ready.current) return;
2691
2749
  const timer = setTimeout(
2692
2750
  () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
@@ -2702,12 +2760,20 @@ function useUrlState() {
2702
2760
  }
2703
2761
 
2704
2762
  // src/interactive/ReportInteractive.tsx
2705
- import { Fragment as Fragment13, jsx as jsx48, jsxs as jsxs32 } from "react/jsx-runtime";
2763
+ import { Fragment as Fragment13, jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
2706
2764
  function ReportInteractive(props) {
2707
2765
  const { report, className, title, dataTheme } = props;
2708
2766
  const result = unwrapReport(report);
2709
2767
  if (!result.ok) {
2710
- return /* @__PURE__ */ jsx48(ReportErrorShell, { error: result.error, className, title, dataTheme });
2768
+ return /* @__PURE__ */ jsx48(
2769
+ ReportErrorShell,
2770
+ {
2771
+ error: result.error,
2772
+ className,
2773
+ title,
2774
+ dataTheme
2775
+ }
2776
+ );
2711
2777
  }
2712
2778
  return /* @__PURE__ */ jsx48(ReportInteractiveView, { ...props, report: result.data });
2713
2779
  }
@@ -2716,7 +2782,10 @@ var THEME_OPTIONS = [
2716
2782
  { value: "system", label: "System", Icon: Monitor },
2717
2783
  { value: "dark", label: "Dark", Icon: Moon }
2718
2784
  ];
2719
- function ThemeSegment({ pref, onPref }) {
2785
+ function ThemeSegment({
2786
+ pref,
2787
+ onPref
2788
+ }) {
2720
2789
  return /* @__PURE__ */ jsx48(
2721
2790
  ToggleGroup,
2722
2791
  {
@@ -2727,7 +2796,17 @@ function ThemeSegment({ pref, onPref }) {
2727
2796
  if (next) onPref(next);
2728
2797
  },
2729
2798
  "aria-label": "Color theme",
2730
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx48(ToggleGroupItem, { value, "aria-label": label, title: label, className: "px-2", children: /* @__PURE__ */ jsx48(Icon, {}) }, value))
2799
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx48(
2800
+ ToggleGroupItem,
2801
+ {
2802
+ value,
2803
+ "aria-label": label,
2804
+ title: label,
2805
+ className: "px-2",
2806
+ children: /* @__PURE__ */ jsx48(Icon, {})
2807
+ },
2808
+ value
2809
+ ))
2731
2810
  }
2732
2811
  );
2733
2812
  }
@@ -2745,7 +2824,10 @@ function ReportInteractiveView({
2745
2824
  }) {
2746
2825
  const [urlState, setUrlState] = useUrlState();
2747
2826
  const { query, status: statusFilter, tags: activeTags, detail } = urlState;
2748
- const setQuery = useCallback5((next) => setUrlState({ query: next }), [setUrlState]);
2827
+ const setQuery = useCallback5(
2828
+ (next) => setUrlState({ query: next }),
2829
+ [setUrlState]
2830
+ );
2749
2831
  const setStatusFilter = useCallback5(
2750
2832
  (next) => setUrlState({ status: next }),
2751
2833
  [setUrlState]
@@ -2754,7 +2836,7 @@ function ReportInteractiveView({
2754
2836
  (next) => setUrlState({ detail: next }),
2755
2837
  [setUrlState]
2756
2838
  );
2757
- const [helpOpen, setHelpOpen] = useState8(false);
2839
+ const [helpOpen, setHelpOpen] = useState9(false);
2758
2840
  const { pref: themePref, setPref: setThemePref } = useTheme();
2759
2841
  const searchRef = useRef5(null);
2760
2842
  const expandId = useId2();
@@ -2764,7 +2846,13 @@ function ReportInteractiveView({
2764
2846
  const filtered = useMemo3(
2765
2847
  // Features carrying failures float to the top so the first thing on screen
2766
2848
  // is the thing that needs attention; an all-green run keeps source order.
2767
- () => failuresFirst(filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags })),
2849
+ () => failuresFirst(
2850
+ filterReport(report, {
2851
+ query: deferredQuery,
2852
+ status: statusFilter,
2853
+ tags: activeTags
2854
+ })
2855
+ ),
2768
2856
  [report, deferredQuery, statusFilter, activeTags]
2769
2857
  );
2770
2858
  const isFiltering = query !== deferredQuery;
@@ -2773,10 +2861,14 @@ function ReportInteractiveView({
2773
2861
  const opts = [
2774
2862
  { key: "all", label: "All", count: s.total }
2775
2863
  ];
2776
- if (s.passed) opts.push({ key: "passed", label: "Passed", count: s.passed });
2777
- if (s.failed) opts.push({ key: "failed", label: "Failed", count: s.failed });
2778
- if (s.skipped) opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2779
- if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
2864
+ if (s.passed)
2865
+ opts.push({ key: "passed", label: "Passed", count: s.passed });
2866
+ if (s.failed)
2867
+ opts.push({ key: "failed", label: "Failed", count: s.failed });
2868
+ if (s.skipped)
2869
+ opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2870
+ if (s.pending)
2871
+ opts.push({ key: "pending", label: "Pending", count: s.pending });
2780
2872
  return opts;
2781
2873
  }, [report]);
2782
2874
  const tagOptions = useMemo3(() => allTags(report), [report]);
@@ -2786,7 +2878,7 @@ function ReportInteractiveView({
2786
2878
  }),
2787
2879
  [activeTags, setUrlState]
2788
2880
  );
2789
- const [toast, setToast] = useState8(null);
2881
+ const [toast, setToast] = useState9(null);
2790
2882
  const toastTimer = useRef5(null);
2791
2883
  const showToast = useCallback5((msg) => {
2792
2884
  setToast(msg);
@@ -2796,7 +2888,10 @@ function ReportInteractiveView({
2796
2888
  const copy = useCallback5(
2797
2889
  (text, msg) => {
2798
2890
  if (typeof navigator !== "undefined" && navigator.clipboard) {
2799
- navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
2891
+ navigator.clipboard.writeText(text).then(
2892
+ () => showToast(msg),
2893
+ () => showToast("Copy failed")
2894
+ );
2800
2895
  }
2801
2896
  },
2802
2897
  [showToast]
@@ -2814,9 +2909,21 @@ function ReportInteractiveView({
2814
2909
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
2815
2910
  [report]
2816
2911
  );
2817
- const collapse = useCollapseState(useCallback5(() => defaultCollapsedIds(report), [report]));
2818
- const collapseAll = useCallback5(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2819
- const [expandedAll, setExpandedAll] = useState8(() => report.summary.failed === 0);
2912
+ const collapse = useCollapseState(
2913
+ useCallback5(() => defaultCollapsedIds(report), [report])
2914
+ );
2915
+ const revealFilteredResults = deferredQuery.trim() !== "" || statusFilter !== "all" || activeTags.length > 0;
2916
+ const visibleCollapse = useMemo3(
2917
+ () => revealFilteredResults ? { ...collapse.api, isCollapsed: () => false } : collapse.api,
2918
+ [collapse.api, revealFilteredResults]
2919
+ );
2920
+ const collapseAll = useCallback5(
2921
+ () => collapse.collapseAll(allCollapsibleIds),
2922
+ [collapse, allCollapsibleIds]
2923
+ );
2924
+ const [expandedAll, setExpandedAll] = useState9(
2925
+ () => report.summary.failed === 0
2926
+ );
2820
2927
  const setExpanded = useCallback5(
2821
2928
  (expanded) => {
2822
2929
  if (expanded) collapse.expandAll();
@@ -2843,7 +2950,7 @@ function ReportInteractiveView({
2843
2950
  const escape = useCallback5(() => {
2844
2951
  if (helpOpen) setHelpOpen(false);
2845
2952
  else if (query !== "") setQuery("");
2846
- }, [helpOpen, query]);
2953
+ }, [helpOpen, query, setQuery]);
2847
2954
  useKeyboardShortcuts({
2848
2955
  onFocusSearch: focusSearch,
2849
2956
  onNextFailure: () => stepFailure(1),
@@ -2858,13 +2965,13 @@ function ReportInteractiveView({
2858
2965
  const totalScenarios = report.summary.total;
2859
2966
  const matchedScenarios = filtered.summary.total;
2860
2967
  const showToc = !hideToc && totalScenarios > 3;
2861
- return /* @__PURE__ */ jsx48(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ jsx48(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx48(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx48(
2968
+ return /* @__PURE__ */ jsx48(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx48(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx48(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx48(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx48(
2862
2969
  ReportRoot,
2863
2970
  {
2864
2971
  report: filtered,
2865
2972
  customRenderers,
2866
2973
  renderers,
2867
- children: /* @__PURE__ */ jsxs32(
2974
+ children: /* @__PURE__ */ jsxs33(
2868
2975
  "main",
2869
2976
  {
2870
2977
  className: cn("es-report", "es-report-interactive", className),
@@ -2873,8 +2980,8 @@ function ReportInteractiveView({
2873
2980
  "data-theme": dataTheme,
2874
2981
  "data-detail-level": detail,
2875
2982
  children: [
2876
- /* @__PURE__ */ jsxs32("header", { className: "es-report-header", children: [
2877
- /* @__PURE__ */ jsxs32(
2983
+ /* @__PURE__ */ jsxs33("header", { className: "es-report-header", children: [
2984
+ /* @__PURE__ */ jsxs33(
2878
2985
  "div",
2879
2986
  {
2880
2987
  "data-es-navbar": true,
@@ -2884,7 +2991,7 @@ function ReportInteractiveView({
2884
2991
  ),
2885
2992
  children: [
2886
2993
  hideHeader ? null : /* @__PURE__ */ jsx48("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2887
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-2", children: [
2994
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
2888
2995
  /* @__PURE__ */ jsx48(
2889
2996
  ReportSearch,
2890
2997
  {
@@ -2900,7 +3007,7 @@ function ReportInteractiveView({
2900
3007
  ]
2901
3008
  }
2902
3009
  ),
2903
- hideHeader ? null : /* @__PURE__ */ jsxs32(Fragment13, { children: [
3010
+ hideHeader ? null : /* @__PURE__ */ jsxs33(Fragment13, { children: [
2904
3011
  /* @__PURE__ */ jsx48(ReportSummary, {}),
2905
3012
  /* @__PURE__ */ jsx48(ReportMeta, {})
2906
3013
  ] }),
@@ -2912,7 +3019,13 @@ function ReportInteractiveView({
2912
3019
  staleAfterDays
2913
3020
  }
2914
3021
  ),
2915
- /* @__PURE__ */ jsx48(ReportLastRunDelta, { history: scenarioHistory, report }),
3022
+ /* @__PURE__ */ jsx48(
3023
+ ReportLastRunDelta,
3024
+ {
3025
+ history: scenarioHistory,
3026
+ report
3027
+ }
3028
+ ),
2916
3029
  /* @__PURE__ */ jsx48(
2917
3030
  ReportFilters,
2918
3031
  {
@@ -2924,13 +3037,27 @@ function ReportInteractiveView({
2924
3037
  onToggleTag: toggleTag
2925
3038
  }
2926
3039
  ),
2927
- /* @__PURE__ */ jsxs32("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3040
+ /* @__PURE__ */ jsxs33("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
2928
3041
  showToc ? /* @__PURE__ */ jsx48(ReportTocDrawer, {}) : null,
2929
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-2", children: [
2930
- /* @__PURE__ */ jsx48(Switch, { id: expandId, checked: expandedAll, onCheckedChange: setExpanded }),
2931
- /* @__PURE__ */ jsx48("label", { htmlFor: expandId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Expand all" })
3042
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
3043
+ /* @__PURE__ */ jsx48(
3044
+ Switch,
3045
+ {
3046
+ id: expandId,
3047
+ checked: expandedAll,
3048
+ onCheckedChange: setExpanded
3049
+ }
3050
+ ),
3051
+ /* @__PURE__ */ jsx48(
3052
+ "label",
3053
+ {
3054
+ htmlFor: expandId,
3055
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3056
+ children: "Expand all"
3057
+ }
3058
+ )
2932
3059
  ] }),
2933
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-2", children: [
3060
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
2934
3061
  /* @__PURE__ */ jsx48(
2935
3062
  Switch,
2936
3063
  {
@@ -2939,15 +3066,27 @@ function ReportInteractiveView({
2939
3066
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
2940
3067
  }
2941
3068
  ),
2942
- /* @__PURE__ */ jsx48("label", { htmlFor: docsId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Show documentation" })
3069
+ /* @__PURE__ */ jsx48(
3070
+ "label",
3071
+ {
3072
+ htmlFor: docsId,
3073
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3074
+ children: "Show documentation"
3075
+ }
3076
+ )
2943
3077
  ] })
2944
3078
  ] })
2945
3079
  ] }),
2946
3080
  /* @__PURE__ */ jsx48(ReportFailureBanner, { failures }),
2947
- hasContent ? showToc ? /* @__PURE__ */ jsxs32("div", { className: "flex gap-6", children: [
3081
+ hasContent ? showToc ? /* @__PURE__ */ jsxs33("div", { className: "flex gap-6", children: [
2948
3082
  /* @__PURE__ */ jsx48(ReportToc, {}),
2949
3083
  /* @__PURE__ */ jsx48("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) })
2950
- ] }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx48(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
3084
+ ] }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx48(
3085
+ ReportEmpty,
3086
+ {
3087
+ message: query ? "No scenarios match the search." : void 0
3088
+ }
3089
+ ),
2951
3090
  /* @__PURE__ */ jsx48(
2952
3091
  "button",
2953
3092
  {
@@ -2959,7 +3098,13 @@ function ReportInteractiveView({
2959
3098
  children: "?"
2960
3099
  }
2961
3100
  ),
2962
- /* @__PURE__ */ jsx48(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
3101
+ /* @__PURE__ */ jsx48(
3102
+ ReportShortcutsHelp,
3103
+ {
3104
+ open: helpOpen,
3105
+ onClose: () => setHelpOpen(false)
3106
+ }
3107
+ ),
2963
3108
  toast ? /* @__PURE__ */ jsx48(
2964
3109
  "div",
2965
3110
  {
@@ -2973,18 +3118,18 @@ function ReportInteractiveView({
2973
3118
  }
2974
3119
  )
2975
3120
  }
2976
- ) }) }) });
3121
+ ) }) }) }) });
2977
3122
  }
2978
3123
 
2979
3124
  // src/interactive/ReportInteractiveIsland.tsx
2980
3125
  import { useMemo as useMemo4 } from "react";
2981
3126
 
2982
3127
  // src/interactive/island-renderers.tsx
2983
- import { createElement, useEffect as useEffect9, useState as useState10 } from "react";
3128
+ import { createElement, useEffect as useEffect10, useState as useState11 } from "react";
2984
3129
 
2985
3130
  // src/components/doc/MermaidView.tsx
2986
- import { useEffect as useEffect8, useRef as useRef6, useState as useState9 } from "react";
2987
- import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs33 } from "react/jsx-runtime";
3131
+ import { useEffect as useEffect9, useRef as useRef6, useState as useState10 } from "react";
3132
+ import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
2988
3133
  function prefersDark(el) {
2989
3134
  const themed = el?.closest("[data-theme]");
2990
3135
  if (themed?.dataset.theme === "dark") return true;
@@ -2993,9 +3138,9 @@ function prefersDark(el) {
2993
3138
  }
2994
3139
  function MermaidView({ entry, load }) {
2995
3140
  const ref = useRef6(null);
2996
- const [svg, setSvg] = useState9(null);
2997
- const [error, setError] = useState9(null);
2998
- useEffect8(() => {
3141
+ const [svg, setSvg] = useState10(null);
3142
+ const [error, setError] = useState10(null);
3143
+ useEffect9(() => {
2999
3144
  let cancelled = false;
3000
3145
  const draw = async () => {
3001
3146
  let mermaid;
@@ -3038,7 +3183,7 @@ function MermaidView({ entry, load }) {
3038
3183
  };
3039
3184
  }, [entry.code, load]);
3040
3185
  if (svg) {
3041
- return /* @__PURE__ */ jsxs33("figure", { ref, className: "my-3", children: [
3186
+ return /* @__PURE__ */ jsxs34("figure", { ref, className: "my-3", children: [
3042
3187
  entry.title ? /* @__PURE__ */ jsx49("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3043
3188
  /* @__PURE__ */ jsx49(
3044
3189
  "div",
@@ -3051,8 +3196,8 @@ function MermaidView({ entry, load }) {
3051
3196
  )
3052
3197
  ] });
3053
3198
  }
3054
- return /* @__PURE__ */ jsxs33(Fragment14, { children: [
3055
- error ? /* @__PURE__ */ jsxs33(
3199
+ return /* @__PURE__ */ jsxs34(Fragment14, { children: [
3200
+ error ? /* @__PURE__ */ jsxs34(
3056
3201
  "p",
3057
3202
  {
3058
3203
  role: "alert",
@@ -3086,8 +3231,8 @@ function cachedCdnModule(url) {
3086
3231
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3087
3232
  var loadHljs = cachedCdnModule(HLJS_ESM);
3088
3233
  function CdnHighlightedCode({ entry }) {
3089
- const [html, setHtml] = useState10(null);
3090
- useEffect9(() => {
3234
+ const [html, setHtml] = useState11(null);
3235
+ useEffect10(() => {
3091
3236
  let cancelled = false;
3092
3237
  void (async () => {
3093
3238
  try {