executable-stories-react 0.1.16 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ssr.js ADDED
@@ -0,0 +1,1210 @@
1
+ // src/ssr-entry.ts
2
+ import { createElement } from "react";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+
5
+ // src/result.ts
6
+ var ok = (data) => ({ ok: true, data });
7
+ function unwrapReport(value) {
8
+ if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
9
+ return value;
10
+ }
11
+ return ok(value);
12
+ }
13
+
14
+ // src/context/ReportRoot.tsx
15
+ import { useMemo } from "react";
16
+
17
+ // src/context/ReportContext.ts
18
+ import { createContext } from "react";
19
+ var ReportContext = createContext(null);
20
+
21
+ // src/context/ReportRoot.tsx
22
+ import { jsx } from "react/jsx-runtime";
23
+ var EMPTY_CUSTOM = {};
24
+ var EMPTY_RENDERERS = {};
25
+ function ReportRoot({
26
+ report,
27
+ customRenderers,
28
+ renderers,
29
+ children
30
+ }) {
31
+ const value = useMemo(
32
+ () => ({
33
+ report,
34
+ customRenderers: customRenderers ?? EMPTY_CUSTOM,
35
+ renderers: renderers ?? EMPTY_RENDERERS
36
+ }),
37
+ [report, customRenderers, renderers]
38
+ );
39
+ return /* @__PURE__ */ jsx(ReportContext.Provider, { value, children });
40
+ }
41
+
42
+ // src/hooks/useReport.ts
43
+ import { useContext } from "react";
44
+ function useReport() {
45
+ const ctx = useContext(ReportContext);
46
+ if (!ctx) {
47
+ throw new Error(
48
+ "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
49
+ );
50
+ }
51
+ return ctx.report;
52
+ }
53
+
54
+ // src/components/ReportScenario.tsx
55
+ import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
56
+
57
+ // src/components/ReportSteps.tsx
58
+ import { formatDuration } from "executable-stories-core/utils/duration";
59
+
60
+ // src/components/doc/DocNote.tsx
61
+ import { jsx as jsx2 } from "react/jsx-runtime";
62
+ function DocNote({ entry }) {
63
+ return /* @__PURE__ */ jsx2("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
64
+ }
65
+
66
+ // src/components/ui/badge.tsx
67
+ import "react";
68
+ import { cva } from "class-variance-authority";
69
+ import { useRender } from "@base-ui/react/use-render";
70
+
71
+ // src/lib/utils.ts
72
+ import { clsx } from "clsx";
73
+ import { twMerge } from "tailwind-merge";
74
+ function cn(...inputs) {
75
+ return twMerge(clsx(inputs));
76
+ }
77
+
78
+ // src/components/ui/badge.tsx
79
+ var badgeVariants = cva(
80
+ "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",
81
+ {
82
+ variants: {
83
+ variant: {
84
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
85
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
86
+ destructive: "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
87
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
88
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
89
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
90
+ passed: "border-pass-border bg-pass-bg text-pass font-mono uppercase",
91
+ failed: "border-fail-border bg-fail-bg text-fail font-mono uppercase",
92
+ skipped: "border-skip-border bg-skip-bg text-skip font-mono uppercase",
93
+ pending: "border-pend-border bg-pend-bg text-pend font-mono uppercase",
94
+ tag: "border-pass-border bg-pass-bg text-pass font-mono font-medium"
95
+ }
96
+ },
97
+ defaultVariants: {
98
+ variant: "default"
99
+ }
100
+ }
101
+ );
102
+ function Badge({
103
+ className,
104
+ variant = "default",
105
+ render,
106
+ ...props
107
+ }) {
108
+ return useRender({
109
+ defaultTagName: "span",
110
+ render,
111
+ props: {
112
+ "data-slot": "badge",
113
+ "data-variant": variant,
114
+ className: cn(badgeVariants({ variant }), className),
115
+ ...props
116
+ }
117
+ });
118
+ }
119
+
120
+ // src/components/doc/DocTag.tsx
121
+ import { jsx as jsx3 } from "react/jsx-runtime";
122
+ function DocTag({ entry }) {
123
+ return /* @__PURE__ */ jsx3("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3(Badge, { variant: "tag", children: n }) }, n)) });
124
+ }
125
+
126
+ // src/components/doc/DocKv.tsx
127
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
128
+ function formatValue(value) {
129
+ if (value === null) return "null";
130
+ if (typeof value === "string") return value;
131
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
132
+ try {
133
+ return JSON.stringify(value);
134
+ } catch {
135
+ return String(value);
136
+ }
137
+ }
138
+ function DocKv({ entry }) {
139
+ return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
140
+ /* @__PURE__ */ jsx4("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
141
+ /* @__PURE__ */ jsx4("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
142
+ ] });
143
+ }
144
+
145
+ // src/hooks/useRenderers.ts
146
+ import { useContext as useContext2 } from "react";
147
+ var EMPTY_CUSTOM2 = {};
148
+ var EMPTY_RENDERERS2 = {};
149
+ function useCustomRenderers() {
150
+ const ctx = useContext2(ReportContext);
151
+ return ctx?.customRenderers ?? EMPTY_CUSTOM2;
152
+ }
153
+ function useBuiltinRenderers() {
154
+ const ctx = useContext2(ReportContext);
155
+ return ctx?.renderers ?? EMPTY_RENDERERS2;
156
+ }
157
+
158
+ // src/components/doc/DocCode.tsx
159
+ import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
160
+ function CodeFigure({ label, children }) {
161
+ return /* @__PURE__ */ jsxs2("figure", { className: "my-3", children: [
162
+ label ? /* @__PURE__ */ jsx5("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
163
+ /* @__PURE__ */ jsx5("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
164
+ ] });
165
+ }
166
+ function DocCode({ entry }) {
167
+ const renderers = useBuiltinRenderers();
168
+ if (renderers.code) {
169
+ return /* @__PURE__ */ jsx5(Fragment, { children: renderers.code(entry) });
170
+ }
171
+ return /* @__PURE__ */ jsx5(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx5("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
172
+ }
173
+
174
+ // src/components/doc/DocTable.tsx
175
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
176
+ function DocTable({ entry }) {
177
+ return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
178
+ entry.label ? /* @__PURE__ */ jsx6("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
179
+ /* @__PURE__ */ jsx6("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs3("table", { className: "w-full border-collapse text-sm", children: [
180
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsx6("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx6("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
181
+ /* @__PURE__ */ jsx6("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx6("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx6("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
182
+ ] }) })
183
+ ] });
184
+ }
185
+
186
+ // src/components/doc/DocLink.tsx
187
+ import { jsx as jsx7 } from "react/jsx-runtime";
188
+ function DocLink({ entry }) {
189
+ return /* @__PURE__ */ jsx7(
190
+ "a",
191
+ {
192
+ className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
193
+ href: entry.url,
194
+ rel: "noreferrer noopener",
195
+ target: "_blank",
196
+ children: entry.label
197
+ }
198
+ );
199
+ }
200
+
201
+ // src/components/doc/DocSection.tsx
202
+ import { marked } from "marked";
203
+
204
+ // src/lib/url.ts
205
+ function safeUrl(value) {
206
+ if (!value) return void 0;
207
+ const trimmed = value.trim();
208
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
209
+ if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
210
+ return trimmed;
211
+ }
212
+ function safeImageUrl(value) {
213
+ if (!value) return void 0;
214
+ const trimmed = value.trim();
215
+ if (/^data:image\//i.test(trimmed)) return trimmed;
216
+ return safeUrl(trimmed);
217
+ }
218
+ function isLocalFsPath(value) {
219
+ return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
220
+ }
221
+
222
+ // src/components/doc/DocSection.tsx
223
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
224
+ function neutralizeUrl(value) {
225
+ return safeUrl(value) ?? "#";
226
+ }
227
+ function safeMarkdownHtml(markdown) {
228
+ const raw = marked.parse(markdown, { async: false });
229
+ 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)}'`);
230
+ }
231
+ function DocSection({ entry }) {
232
+ const renderers = useBuiltinRenderers();
233
+ if (renderers.section) {
234
+ return /* @__PURE__ */ jsx8(Fragment2, { children: renderers.section(entry) });
235
+ }
236
+ const html = safeMarkdownHtml(entry.markdown);
237
+ return /* @__PURE__ */ jsxs4("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
238
+ entry.title ? /* @__PURE__ */ jsx8("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
239
+ /* @__PURE__ */ jsx8(
240
+ "div",
241
+ {
242
+ className: "es-doc-prose text-muted-foreground",
243
+ dangerouslySetInnerHTML: { __html: html }
244
+ }
245
+ )
246
+ ] });
247
+ }
248
+
249
+ // src/components/doc/DocMermaid.tsx
250
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
251
+ function MermaidSource({ entry, ref }) {
252
+ return /* @__PURE__ */ jsxs5("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
253
+ entry.title ? /* @__PURE__ */ jsx9("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
254
+ /* @__PURE__ */ jsx9(
255
+ "pre",
256
+ {
257
+ "data-mermaid": true,
258
+ tabIndex: 0,
259
+ className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
260
+ children: entry.code
261
+ }
262
+ )
263
+ ] });
264
+ }
265
+ function DocMermaid({ entry }) {
266
+ const renderers = useBuiltinRenderers();
267
+ if (renderers.mermaid) {
268
+ return /* @__PURE__ */ jsx9(Fragment3, { children: renderers.mermaid(entry) });
269
+ }
270
+ return /* @__PURE__ */ jsx9(MermaidSource, { entry });
271
+ }
272
+
273
+ // src/components/doc/DocScreenshot.tsx
274
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
275
+ function DocScreenshot({ entry }) {
276
+ const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
277
+ if (!src) {
278
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
279
+ /* @__PURE__ */ jsx10("div", { className: "font-medium", children: "Screenshot unavailable" }),
280
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
281
+ entry.alt ? /* @__PURE__ */ jsx10("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
282
+ ] });
283
+ }
284
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
285
+ /* @__PURE__ */ jsx10(
286
+ "img",
287
+ {
288
+ src,
289
+ alt: entry.alt ?? "",
290
+ loading: "lazy",
291
+ className: "max-w-full rounded-md border border-border"
292
+ }
293
+ ),
294
+ entry.alt ? /* @__PURE__ */ jsx10("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
295
+ ] });
296
+ }
297
+
298
+ // src/components/doc/DocVideo.tsx
299
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
300
+ function DocVideo({ entry }) {
301
+ if (isLocalFsPath(entry.path)) {
302
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
303
+ /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Video unavailable" }),
304
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
305
+ entry.caption ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
306
+ ] });
307
+ }
308
+ const src = safeUrl(entry.path);
309
+ const poster = safeUrl(entry.poster);
310
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
311
+ /* @__PURE__ */ jsx11(
312
+ "video",
313
+ {
314
+ controls: true,
315
+ preload: "metadata",
316
+ ...src ? { src } : {},
317
+ ...poster ? { poster } : {},
318
+ className: "max-w-full rounded-md border border-border"
319
+ }
320
+ ),
321
+ entry.caption ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
322
+ ] });
323
+ }
324
+
325
+ // src/components/doc/DocHtml.tsx
326
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
327
+ function DocHtml({ entry }) {
328
+ const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
329
+ const title = entry.title ?? "Embedded HTML";
330
+ const safeHref = safeUrl(entry.url ?? entry.path);
331
+ const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
332
+ return /* @__PURE__ */ jsxs8("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
333
+ /* @__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: [
334
+ /* @__PURE__ */ jsx12("span", { children: entry.title ?? "HTML" }),
335
+ safeHref ? /* @__PURE__ */ jsx12(
336
+ "a",
337
+ {
338
+ href: safeHref,
339
+ target: "_blank",
340
+ rel: "noreferrer noopener",
341
+ "aria-label": "Open in new tab",
342
+ title: "Open in new tab",
343
+ className: "text-primary",
344
+ children: "\u2197"
345
+ }
346
+ ) : null
347
+ ] }),
348
+ /* @__PURE__ */ jsx12(
349
+ "iframe",
350
+ {
351
+ ...frameProps,
352
+ sandbox: "allow-scripts",
353
+ loading: "lazy",
354
+ title,
355
+ className: "block w-full border-0",
356
+ style: { height }
357
+ }
358
+ )
359
+ ] });
360
+ }
361
+
362
+ // src/components/doc/DocCustom.tsx
363
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
364
+ function DocCustom({ entry }) {
365
+ const renderers = useCustomRenderers();
366
+ const renderer = renderers[entry.type];
367
+ if (renderer) {
368
+ return /* @__PURE__ */ jsx13(Fragment4, { children: renderer(entry) });
369
+ }
370
+ return /* @__PURE__ */ jsxs9("div", { className: "my-2", "data-type": entry.type, children: [
371
+ /* @__PURE__ */ jsx13("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
372
+ /* @__PURE__ */ jsx13("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
373
+ ] });
374
+ }
375
+ function safeStringify(value) {
376
+ try {
377
+ return JSON.stringify(value, null, 2);
378
+ } catch {
379
+ return String(value);
380
+ }
381
+ }
382
+
383
+ // src/components/doc/DocEntry.tsx
384
+ import { jsx as jsx14 } from "react/jsx-runtime";
385
+ function DocEntry({ entry }) {
386
+ switch (entry.kind) {
387
+ case "note":
388
+ return /* @__PURE__ */ jsx14(DocNote, { entry });
389
+ case "tag":
390
+ return /* @__PURE__ */ jsx14(DocTag, { entry });
391
+ case "kv":
392
+ return /* @__PURE__ */ jsx14(DocKv, { entry });
393
+ case "code":
394
+ return /* @__PURE__ */ jsx14(DocCode, { entry });
395
+ case "table":
396
+ return /* @__PURE__ */ jsx14(DocTable, { entry });
397
+ case "link":
398
+ return /* @__PURE__ */ jsx14(DocLink, { entry });
399
+ case "section":
400
+ return /* @__PURE__ */ jsx14(DocSection, { entry });
401
+ case "mermaid":
402
+ return /* @__PURE__ */ jsx14(DocMermaid, { entry });
403
+ case "screenshot":
404
+ return /* @__PURE__ */ jsx14(DocScreenshot, { entry });
405
+ case "video":
406
+ return /* @__PURE__ */ jsx14(DocVideo, { entry });
407
+ case "html":
408
+ return /* @__PURE__ */ jsx14(DocHtml, { entry });
409
+ case "custom":
410
+ return /* @__PURE__ */ jsx14(DocCustom, { entry });
411
+ }
412
+ }
413
+
414
+ // src/components/ReportDocEntries.tsx
415
+ import { Fragment as Fragment5, jsx as jsx15 } from "react/jsx-runtime";
416
+ function ReportDocEntries({ entries }) {
417
+ if (entries.length === 0) return null;
418
+ return /* @__PURE__ */ jsx15(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx15(DocEntry, { entry }, i)) });
419
+ }
420
+
421
+ // src/lib/step-params.tsx
422
+ import { jsx as jsx16 } from "react/jsx-runtime";
423
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
424
+ function highlightStepParams(text) {
425
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
426
+ if (matches.length === 0) return text;
427
+ const nodes = [];
428
+ let lastIndex = 0;
429
+ matches.forEach((match, i) => {
430
+ const start = match.index;
431
+ const end = start + match[0].length;
432
+ if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
433
+ nodes.push(
434
+ /* @__PURE__ */ jsx16("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
435
+ );
436
+ lastIndex = end;
437
+ });
438
+ if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
439
+ return nodes;
440
+ }
441
+
442
+ // src/components/ReportSteps.tsx
443
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
444
+ var STEP_GLYPH = {
445
+ passed: "\u2713",
446
+ failed: "\u2717",
447
+ skipped: "\u25CB",
448
+ pending: "\u25CB"
449
+ };
450
+ var STEP_GLYPH_COLOR = {
451
+ passed: "text-pass",
452
+ failed: "text-fail",
453
+ skipped: "text-skip",
454
+ pending: "text-pend"
455
+ };
456
+ function ReportSteps({ scenario }) {
457
+ if (scenario.steps.length === 0) return null;
458
+ return /* @__PURE__ */ jsx17("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx17(ReportStepItem, { step }, step.id)) });
459
+ }
460
+ function ReportStepItem({ step }) {
461
+ const kw = step.keyword.toLowerCase();
462
+ const continuation = kw === "and" || kw === "but";
463
+ return /* @__PURE__ */ jsxs10(
464
+ "li",
465
+ {
466
+ id: step.id,
467
+ "data-status": step.status,
468
+ className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
469
+ children: [
470
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-baseline gap-2", children: [
471
+ /* @__PURE__ */ jsx17("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" }),
472
+ /* @__PURE__ */ jsx17(
473
+ "span",
474
+ {
475
+ className: cn(
476
+ "min-w-[52px] shrink-0 font-mono text-xs font-semibold capitalize",
477
+ continuation ? "font-medium text-muted-foreground" : "text-keyword"
478
+ ),
479
+ children: step.keyword
480
+ }
481
+ ),
482
+ /* @__PURE__ */ jsx17("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
483
+ step.durationMs > 0 ? /* @__PURE__ */ jsx17("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
484
+ ] }),
485
+ step.errorMessage ? /* @__PURE__ */ jsx17(
486
+ "pre",
487
+ {
488
+ role: "alert",
489
+ className: "mt-2.5 ml-6 overflow-x-auto rounded-md border border-fail-border bg-fail-bg px-4 py-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap text-fail",
490
+ children: step.errorMessage
491
+ }
492
+ ) : null,
493
+ step.docEntries.length > 0 ? /* @__PURE__ */ jsx17("div", { "data-es-docs": true, className: "mt-2 ml-6", children: /* @__PURE__ */ jsx17(ReportDocEntries, { entries: step.docEntries }) }) : null
494
+ ]
495
+ }
496
+ );
497
+ }
498
+
499
+ // src/components/ReportAttachments.tsx
500
+ import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
501
+ function ReportAttachments({ attachments }) {
502
+ if (attachments.length === 0) return null;
503
+ return (
504
+ // role="group" (not a labelled <section>) so multiple scenarios' attachment
505
+ // blocks don't create duplicate "Attachments" region landmarks.
506
+ /* @__PURE__ */ jsx18("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
507
+ const dataUri = att.contentEncoding === "BASE64" ? `data:${att.mediaType};base64,${att.body}` : `data:${att.mediaType},${encodeURIComponent(att.body)}`;
508
+ const isImage = att.mediaType.startsWith("image/");
509
+ return /* @__PURE__ */ jsx18("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
510
+ /* @__PURE__ */ jsx18(
511
+ "img",
512
+ {
513
+ src: dataUri,
514
+ alt: att.name,
515
+ loading: "lazy",
516
+ className: "max-w-full rounded-md border border-border"
517
+ }
518
+ ),
519
+ /* @__PURE__ */ jsx18("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
520
+ ] }) : /* @__PURE__ */ jsxs11(
521
+ "a",
522
+ {
523
+ href: dataUri,
524
+ download: att.name,
525
+ className: "text-xs text-link underline underline-offset-2",
526
+ children: [
527
+ att.name,
528
+ " ",
529
+ /* @__PURE__ */ jsxs11("span", { className: "text-muted-foreground", children: [
530
+ "(",
531
+ att.mediaType,
532
+ ")"
533
+ ] })
534
+ ]
535
+ }
536
+ ) }, `${att.name}-${i}`);
537
+ }) })
538
+ );
539
+ }
540
+
541
+ // src/components/ReportTrace.tsx
542
+ import { formatDuration as formatDuration2 } from "executable-stories-core/utils/duration";
543
+
544
+ // src/lib/trace.ts
545
+ var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
546
+ var safeStatus = (s) => VALID.has(s) ? s : "unset";
547
+ function normalizeSpans(spans) {
548
+ const out = [];
549
+ for (const span of spans) {
550
+ if (!span || typeof span.spanId !== "string" || typeof span.name !== "string") continue;
551
+ let startTimeMs;
552
+ let durationMs;
553
+ if (span.startTimeMs != null && span.durationMs != null) {
554
+ startTimeMs = span.startTimeMs;
555
+ durationMs = span.durationMs;
556
+ } else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
557
+ startTimeMs = span.startTimeUnixNano / 1e6;
558
+ durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
559
+ } else {
560
+ continue;
561
+ }
562
+ durationMs = Math.max(0, durationMs);
563
+ if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
564
+ out.push({
565
+ spanId: span.spanId,
566
+ parentSpanId: span.parentSpanId,
567
+ name: span.name,
568
+ startTimeMs,
569
+ durationMs,
570
+ status: safeStatus(span.status),
571
+ statusMessage: span.statusMessage
572
+ });
573
+ }
574
+ return out;
575
+ }
576
+ function toTraceRows(spans) {
577
+ const normalized = normalizeSpans(spans);
578
+ if (normalized.length === 0) return [];
579
+ const byId = /* @__PURE__ */ new Map();
580
+ for (const span of normalized) {
581
+ let key = span.spanId;
582
+ let suffix = 2;
583
+ while (byId.has(key)) key = `${span.spanId}__dup${suffix++}`;
584
+ byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
585
+ }
586
+ const roots = [];
587
+ for (const node of byId.values()) {
588
+ const parent = node.span.parentSpanId ? byId.get(node.span.parentSpanId) : void 0;
589
+ if (parent && parent !== node) parent.children.push(node);
590
+ else roots.push(node);
591
+ }
592
+ for (const node of byId.values()) node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
593
+ roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
594
+ const rows = [];
595
+ const seen = /* @__PURE__ */ new Set();
596
+ const walk = (node, depth) => {
597
+ if (seen.has(node.span.spanId)) return;
598
+ seen.add(node.span.spanId);
599
+ rows.push({ ...node.span, depth });
600
+ for (const child of node.children) walk(child, depth + 1);
601
+ };
602
+ for (const root of roots) walk(root, 0);
603
+ for (const node of byId.values()) {
604
+ if (!seen.has(node.span.spanId)) walk(node, 0);
605
+ }
606
+ return rows;
607
+ }
608
+ function traceWindow(rows) {
609
+ let min = Infinity;
610
+ let max = -Infinity;
611
+ for (const r of rows) {
612
+ min = Math.min(min, r.startTimeMs);
613
+ max = Math.max(max, r.startTimeMs + r.durationMs);
614
+ }
615
+ const total = max - min;
616
+ return { minStartMs: isFinite(min) ? min : 0, totalMs: total > 0 ? total : 1 };
617
+ }
618
+
619
+ // src/components/ReportTrace.tsx
620
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
621
+ var BAR_COLOR = {
622
+ ok: "bg-pass",
623
+ error: "bg-fail",
624
+ unset: "bg-skip"
625
+ };
626
+ function ReportTrace({ spans }) {
627
+ if (!spans || spans.length === 0) return null;
628
+ const rows = toTraceRows(spans);
629
+ if (rows.length === 0) return null;
630
+ const { minStartMs, totalMs } = traceWindow(rows);
631
+ return /* @__PURE__ */ jsxs12("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
632
+ /* @__PURE__ */ jsxs12("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
633
+ "Trace (",
634
+ rows.length,
635
+ " span",
636
+ rows.length === 1 ? "" : "s",
637
+ ", ",
638
+ formatDuration2(totalMs),
639
+ ")"
640
+ ] }),
641
+ /* @__PURE__ */ jsx19("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
642
+ const leftPct = (row.startTimeMs - minStartMs) / totalMs * 100;
643
+ const widthPct = Math.max(row.durationMs / totalMs * 100, 0.5);
644
+ return /* @__PURE__ */ jsxs12("li", { className: "flex items-center gap-2", children: [
645
+ /* @__PURE__ */ jsx19(
646
+ "span",
647
+ {
648
+ className: "min-w-0 flex-1 truncate text-foreground",
649
+ style: { paddingLeft: `${row.depth * 12}px` },
650
+ title: row.statusMessage ? `${row.name} \u2014 ${row.statusMessage}` : row.name,
651
+ children: row.name
652
+ }
653
+ ),
654
+ /* @__PURE__ */ jsx19("span", { className: "relative h-3 w-1/2 shrink-0 overflow-hidden rounded-sm bg-border/40", children: /* @__PURE__ */ jsx19(
655
+ "span",
656
+ {
657
+ className: cn("absolute top-0 h-full rounded-sm", BAR_COLOR[row.status]),
658
+ style: { left: `${leftPct}%`, width: `${widthPct}%` }
659
+ }
660
+ ) }),
661
+ /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration2(row.durationMs) })
662
+ ] }, `${row.spanId}-${i}`);
663
+ }) })
664
+ ] });
665
+ }
666
+
667
+ // src/interactive/collapse-context.ts
668
+ import { createContext as createContext2, useCallback, useContext as useContext3, useState } from "react";
669
+ var CollapseContext = createContext2(null);
670
+ var CollapseProvider = CollapseContext.Provider;
671
+ function useCollapse() {
672
+ return useContext3(CollapseContext);
673
+ }
674
+
675
+ // src/interactive/scenario-actions.tsx
676
+ import { createContext as createContext3, useContext as useContext4 } from "react";
677
+ var ScenarioActionsContext = createContext3(null);
678
+ var ScenarioActionsProvider = ScenarioActionsContext.Provider;
679
+ function useScenarioActions() {
680
+ return useContext4(ScenarioActionsContext);
681
+ }
682
+
683
+ // src/components/ui/card.tsx
684
+ import "react";
685
+ import { jsx as jsx20 } from "react/jsx-runtime";
686
+ function Card({ className, ...props }) {
687
+ return /* @__PURE__ */ jsx20(
688
+ "div",
689
+ {
690
+ "data-slot": "card",
691
+ className: cn(
692
+ "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
693
+ className
694
+ ),
695
+ ...props
696
+ }
697
+ );
698
+ }
699
+ function CardHeader({ className, ...props }) {
700
+ return /* @__PURE__ */ jsx20(
701
+ "div",
702
+ {
703
+ "data-slot": "card-header",
704
+ className: cn(
705
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
706
+ className
707
+ ),
708
+ ...props
709
+ }
710
+ );
711
+ }
712
+ function CardContent({ className, ...props }) {
713
+ return /* @__PURE__ */ jsx20(
714
+ "div",
715
+ {
716
+ "data-slot": "card-content",
717
+ className: cn("px-6", className),
718
+ ...props
719
+ }
720
+ );
721
+ }
722
+
723
+ // src/components/ReportScenario.tsx
724
+ import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
725
+ var STATUS_LABEL = {
726
+ passed: "Passed",
727
+ failed: "Failed",
728
+ skipped: "Skipped",
729
+ pending: "Pending"
730
+ };
731
+ var STATUS_GLYPH = {
732
+ passed: "\u2713",
733
+ failed: "\u2717",
734
+ skipped: "\u25CB",
735
+ pending: "\u25CB"
736
+ };
737
+ var STATUS_GLYPH_COLOR = {
738
+ passed: "text-pass",
739
+ failed: "text-fail",
740
+ skipped: "text-skip",
741
+ pending: "text-pend"
742
+ };
743
+ var STATUS_BADGE = {
744
+ passed: "passed",
745
+ failed: "failed",
746
+ skipped: "skipped",
747
+ pending: "pending"
748
+ };
749
+ function ReportScenario({ scenario, hideTitle = false }) {
750
+ const titleId = `${scenario.id}-title`;
751
+ const bodyId = `${scenario.id}-body`;
752
+ const label = STATUS_LABEL[scenario.status];
753
+ const collapse = useCollapse();
754
+ const collapsible = collapse !== null && !hideTitle;
755
+ const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
756
+ const actions = useScenarioActions();
757
+ return /* @__PURE__ */ jsxs13(
758
+ Card,
759
+ {
760
+ id: scenario.id,
761
+ "data-status": scenario.status,
762
+ ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
763
+ className: "gap-0 py-0",
764
+ children: [
765
+ /* @__PURE__ */ jsxs13(CardHeader, { className: "px-4 py-3", children: [
766
+ /* @__PURE__ */ jsxs13("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
767
+ hideTitle ? null : /* @__PURE__ */ jsxs13("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
768
+ collapsible ? /* @__PURE__ */ jsx21(
769
+ "button",
770
+ {
771
+ type: "button",
772
+ onClick: () => collapse.toggle(scenario.id),
773
+ "aria-expanded": !collapsed,
774
+ "aria-controls": bodyId,
775
+ "aria-label": `Toggle ${scenario.title}`,
776
+ className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
777
+ children: /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
778
+ }
779
+ ) : null,
780
+ /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
781
+ /* @__PURE__ */ jsx21("span", { children: scenario.title })
782
+ ] }),
783
+ /* @__PURE__ */ jsxs13("div", { className: "flex shrink-0 items-center gap-2", children: [
784
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx21("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
785
+ /* @__PURE__ */ jsx21(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
786
+ actions ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-0.5", children: [
787
+ /* @__PURE__ */ jsx21(
788
+ "button",
789
+ {
790
+ type: "button",
791
+ onClick: () => actions.copyLink(scenario),
792
+ "aria-label": `Copy link to ${scenario.title}`,
793
+ title: "Copy permalink",
794
+ className: "cursor-pointer rounded px-1 py-0.5 font-mono text-[0.625rem] text-muted-foreground hover:bg-muted hover:text-foreground",
795
+ children: "link"
796
+ }
797
+ ),
798
+ /* @__PURE__ */ jsx21(
799
+ "button",
800
+ {
801
+ type: "button",
802
+ onClick: () => actions.copyMarkdown(scenario),
803
+ "aria-label": `Copy ${scenario.title} as Markdown`,
804
+ title: "Copy as Markdown",
805
+ className: "cursor-pointer rounded px-1 py-0.5 font-mono text-[0.625rem] text-muted-foreground hover:bg-muted hover:text-foreground",
806
+ children: "md"
807
+ }
808
+ ),
809
+ /* @__PURE__ */ jsx21(
810
+ "button",
811
+ {
812
+ type: "button",
813
+ onClick: () => actions.copyPrompt(scenario),
814
+ "aria-label": `Copy ${scenario.title} as an AI investigation prompt`,
815
+ title: "Copy as AI prompt",
816
+ className: "cursor-pointer rounded px-1 py-0.5 font-mono text-[0.625rem] text-muted-foreground hover:bg-muted hover:text-foreground",
817
+ children: "ai"
818
+ }
819
+ )
820
+ ] }) : null
821
+ ] })
822
+ ] }),
823
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs13("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
824
+ scenario.tags.map((t) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
825
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx21("li", { children: ticket.url ? /* @__PURE__ */ jsx21("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
826
+ ] }) : null
827
+ ] }),
828
+ /* @__PURE__ */ jsxs13(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
829
+ scenario.errorMessage ? /* @__PURE__ */ jsx21(
830
+ "pre",
831
+ {
832
+ role: "alert",
833
+ className: "mb-1 overflow-x-auto rounded-md border border-fail-border bg-fail-bg px-4 py-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap text-fail",
834
+ children: scenario.errorMessage
835
+ }
836
+ ) : null,
837
+ /* @__PURE__ */ jsx21(ReportSteps, { scenario }),
838
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx21("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx21(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
839
+ /* @__PURE__ */ jsx21(ReportAttachments, { attachments: scenario.attachments }),
840
+ /* @__PURE__ */ jsx21(ReportTrace, { spans: scenario.otelSpans })
841
+ ] })
842
+ ]
843
+ }
844
+ );
845
+ }
846
+
847
+ // src/components/ReportScenarioList.tsx
848
+ import { Fragment as Fragment7, jsx as jsx22 } from "react/jsx-runtime";
849
+ function ReportScenarioList({ feature }) {
850
+ return /* @__PURE__ */ jsx22(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx22(ReportScenario, { scenario }, scenario.id)) });
851
+ }
852
+
853
+ // src/components/ReportFeature.tsx
854
+ import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
855
+ function ReportFeature({ feature }) {
856
+ const titleId = `${feature.id}-title`;
857
+ const bodyId = `${feature.id}-body`;
858
+ const s = feature.summary;
859
+ const skipped = s.skipped + s.pending;
860
+ const collapse = useCollapse();
861
+ const collapsed = collapse?.isCollapsed(feature.id) ?? false;
862
+ return /* @__PURE__ */ jsxs14(
863
+ "section",
864
+ {
865
+ id: feature.id,
866
+ "data-slot": "feature",
867
+ "aria-labelledby": titleId,
868
+ className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
869
+ children: [
870
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
871
+ /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 items-center gap-2", children: [
872
+ collapse ? /* @__PURE__ */ jsx23(
873
+ "button",
874
+ {
875
+ type: "button",
876
+ onClick: () => collapse.toggle(feature.id),
877
+ "aria-expanded": !collapsed,
878
+ "aria-controls": bodyId,
879
+ "aria-label": `Toggle ${feature.title}`,
880
+ className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
881
+ children: /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
882
+ }
883
+ ) : null,
884
+ /* @__PURE__ */ jsxs14("div", { className: "min-w-0", children: [
885
+ /* @__PURE__ */ jsx23("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
886
+ /* @__PURE__ */ jsx23("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
887
+ ] })
888
+ ] }),
889
+ /* @__PURE__ */ jsxs14(
890
+ "div",
891
+ {
892
+ role: "img",
893
+ className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
894
+ "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
895
+ children: [
896
+ /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-pass", children: [
897
+ "\u2713",
898
+ s.passed
899
+ ] }),
900
+ /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-fail", children: [
901
+ "\u2717",
902
+ s.failed
903
+ ] }),
904
+ /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-skip", children: [
905
+ "\u25CB",
906
+ skipped
907
+ ] })
908
+ ]
909
+ }
910
+ )
911
+ ] }),
912
+ /* @__PURE__ */ jsx23("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx23(ReportScenarioList, { feature }) })
913
+ ]
914
+ }
915
+ );
916
+ }
917
+
918
+ // src/components/ReportFeatureList.tsx
919
+ import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
920
+ function ReportFeatureList() {
921
+ const report = useReport();
922
+ return /* @__PURE__ */ jsx24(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx24(ReportFeature, { feature }, feature.id)) });
923
+ }
924
+
925
+ // src/components/ui/empty.tsx
926
+ import { cva as cva2 } from "class-variance-authority";
927
+ import { jsx as jsx25 } from "react/jsx-runtime";
928
+ function Empty({ className, ...props }) {
929
+ return /* @__PURE__ */ jsx25(
930
+ "div",
931
+ {
932
+ "data-slot": "empty",
933
+ className: cn(
934
+ "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",
935
+ className
936
+ ),
937
+ ...props
938
+ }
939
+ );
940
+ }
941
+ function EmptyHeader({ className, ...props }) {
942
+ return /* @__PURE__ */ jsx25(
943
+ "div",
944
+ {
945
+ "data-slot": "empty-header",
946
+ className: cn(
947
+ "flex max-w-sm flex-col items-center gap-2 text-center",
948
+ className
949
+ ),
950
+ ...props
951
+ }
952
+ );
953
+ }
954
+ var emptyMediaVariants = cva2(
955
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
956
+ {
957
+ variants: {
958
+ variant: {
959
+ default: "bg-transparent",
960
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
961
+ }
962
+ },
963
+ defaultVariants: {
964
+ variant: "default"
965
+ }
966
+ }
967
+ );
968
+ function EmptyTitle({ className, ...props }) {
969
+ return /* @__PURE__ */ jsx25(
970
+ "div",
971
+ {
972
+ "data-slot": "empty-title",
973
+ className: cn("text-lg font-medium tracking-tight", className),
974
+ ...props
975
+ }
976
+ );
977
+ }
978
+
979
+ // src/components/ReportEmpty.tsx
980
+ import { jsx as jsx26 } from "react/jsx-runtime";
981
+ function ReportEmpty({ message }) {
982
+ return /* @__PURE__ */ jsx26(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx26(EmptyHeader, { children: /* @__PURE__ */ jsx26(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
983
+ }
984
+
985
+ // src/components/ReportSummary.tsx
986
+ import { Fragment as Fragment9, jsx as jsx27, jsxs as jsxs15 } from "react/jsx-runtime";
987
+ function ReportSummary({ className }) {
988
+ const report = useReport();
989
+ return /* @__PURE__ */ jsx27(
990
+ ReportSummaryView,
991
+ {
992
+ summary: report.summary,
993
+ ...className !== void 0 && { className },
994
+ ariaLabel: "Run summary"
995
+ }
996
+ );
997
+ }
998
+ function ReportSummaryView({ summary, className, ariaLabel }) {
999
+ return /* @__PURE__ */ jsxs15("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1000
+ /* @__PURE__ */ jsxs15("span", { children: [
1001
+ /* @__PURE__ */ jsx27("strong", { className: "font-semibold text-foreground", children: summary.total }),
1002
+ " scenario",
1003
+ summary.total === 1 ? "" : "s"
1004
+ ] }),
1005
+ " \xB7 ",
1006
+ /* @__PURE__ */ jsxs15("span", { "data-status": "passed", className: "text-pass", children: [
1007
+ summary.passed,
1008
+ " passed"
1009
+ ] }),
1010
+ " \xB7 ",
1011
+ /* @__PURE__ */ jsxs15("span", { "data-status": "failed", className: "text-fail", children: [
1012
+ summary.failed,
1013
+ " failed"
1014
+ ] }),
1015
+ summary.skipped > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1016
+ " \xB7 ",
1017
+ /* @__PURE__ */ jsxs15("span", { "data-status": "skipped", className: "text-skip", children: [
1018
+ summary.skipped,
1019
+ " skipped"
1020
+ ] })
1021
+ ] }) : null,
1022
+ summary.pending > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1023
+ " \xB7 ",
1024
+ /* @__PURE__ */ jsxs15("span", { "data-status": "pending", className: "text-pend", children: [
1025
+ summary.pending,
1026
+ " pending"
1027
+ ] })
1028
+ ] }) : null
1029
+ ] });
1030
+ }
1031
+
1032
+ // src/components/ReportMeta.tsx
1033
+ import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1034
+ import { jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
1035
+ function ReportMeta() {
1036
+ const report = useReport();
1037
+ const items = [];
1038
+ if (report.startedAtMs) {
1039
+ items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1040
+ }
1041
+ if (report.durationMs) {
1042
+ items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1043
+ }
1044
+ if (report.packageVersion) {
1045
+ items.push({ label: "Version", value: report.packageVersion });
1046
+ }
1047
+ if (report.gitSha) {
1048
+ items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1049
+ }
1050
+ if (report.ci) {
1051
+ items.push({
1052
+ label: "CI",
1053
+ value: report.ci.url ? /* @__PURE__ */ jsxs16("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1054
+ report.ci.name,
1055
+ report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1056
+ ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1057
+ });
1058
+ }
1059
+ if (items.length === 0) return null;
1060
+ return /* @__PURE__ */ jsx28("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ jsxs16("div", { className: "flex gap-1.5", children: [
1061
+ /* @__PURE__ */ jsx28("dt", { className: "font-medium", children: item.label }),
1062
+ /* @__PURE__ */ jsx28("dd", { className: "font-mono text-foreground", children: item.value })
1063
+ ] }, item.label)) });
1064
+ }
1065
+
1066
+ // src/components/ReportSchemaError.tsx
1067
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1068
+ function ReportSchemaError({ error }) {
1069
+ return /* @__PURE__ */ jsxs17("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1070
+ /* @__PURE__ */ jsx29("p", { children: /* @__PURE__ */ jsx29("strong", { children: "Report could not be displayed." }) }),
1071
+ /* @__PURE__ */ jsx29("p", { children: error.message }),
1072
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs17("p", { children: [
1073
+ "The report bundle is newer than this version of ",
1074
+ /* @__PURE__ */ jsx29("code", { children: "executable-stories-react" }),
1075
+ ". Upgrade the package, or regenerate the report with an older formatters CLI."
1076
+ ] }) : null,
1077
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs17("details", { children: [
1078
+ /* @__PURE__ */ jsxs17("summary", { children: [
1079
+ error.issues.length,
1080
+ " validation issue",
1081
+ error.issues.length === 1 ? "" : "s"
1082
+ ] }),
1083
+ /* @__PURE__ */ jsx29("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1084
+ ] }) : null
1085
+ ] });
1086
+ }
1087
+
1088
+ // src/components/ReportShell.tsx
1089
+ import { Fragment as Fragment10, jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1090
+ function ReportTitleBlock({ title }) {
1091
+ return /* @__PURE__ */ jsxs18(Fragment10, { children: [
1092
+ /* @__PURE__ */ jsx30("h1", { children: title ?? "Story Report" }),
1093
+ /* @__PURE__ */ jsx30(ReportSummary, {}),
1094
+ /* @__PURE__ */ jsx30(ReportMeta, {})
1095
+ ] });
1096
+ }
1097
+ function ReportErrorShell({
1098
+ error,
1099
+ className,
1100
+ title,
1101
+ dataTheme
1102
+ }) {
1103
+ return /* @__PURE__ */ jsx30("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx30(ReportSchemaError, { error }) });
1104
+ }
1105
+
1106
+ // src/components/Report.tsx
1107
+ import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1108
+ function Report(props) {
1109
+ const { report, customRenderers, renderers, className, title, dataTheme } = props;
1110
+ const result = unwrapReport(report);
1111
+ if (!result.ok) {
1112
+ return /* @__PURE__ */ jsx31(ReportErrorShell, { error: result.error, className, title, dataTheme });
1113
+ }
1114
+ const data = result.data;
1115
+ const hasContent = data.features.length > 0;
1116
+ return /* @__PURE__ */ jsx31(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs19("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1117
+ /* @__PURE__ */ jsx31("header", { className: "es-report-header", children: /* @__PURE__ */ jsx31(ReportTitleBlock, { title }) }),
1118
+ hasContent ? /* @__PURE__ */ jsx31(ReportFeatureList, {}) : /* @__PURE__ */ jsx31(ReportEmpty, {})
1119
+ ] }) });
1120
+ }
1121
+
1122
+ // src/ssr-entry.ts
1123
+ var ROOT_ID = "es-report-root";
1124
+ var DATA_ID = "es-report-data";
1125
+ function escapeJsonForScript(json) {
1126
+ return json.replace(/[<\u2028\u2029]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
1127
+ }
1128
+ var HLJS_URL = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
1129
+ var HLJS_CSS_LIGHT = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css";
1130
+ var HLJS_CSS_DARK = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css";
1131
+ var MERMAID_URL = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
1132
+ function cdnAssets(syntaxHighlighting, mermaid) {
1133
+ const headParts = [];
1134
+ const imports = [];
1135
+ const init = [];
1136
+ if (syntaxHighlighting) {
1137
+ headParts.push(`<link rel="stylesheet" href="${HLJS_CSS_LIGHT}">`);
1138
+ headParts.push(`<link rel="stylesheet" href="${HLJS_CSS_DARK}" media="(prefers-color-scheme: dark)">`);
1139
+ imports.push(`import hljs from "${HLJS_URL}";`);
1140
+ init.push("document.querySelectorAll('pre code').forEach((el) => hljs.highlightElement(el));");
1141
+ }
1142
+ if (mermaid) {
1143
+ imports.push(`import mermaid from "${MERMAID_URL}";`);
1144
+ init.push("mermaid.initialize({ startOnLoad: false, theme: 'neutral' });");
1145
+ init.push('await mermaid.run({ querySelector: "pre[data-mermaid]" });');
1146
+ }
1147
+ const body = imports.length ? `<script type="module">
1148
+ ${imports.join("\n")}
1149
+ ${init.join("\n")}
1150
+ </script>` : "";
1151
+ return { head: headParts.join("\n"), body };
1152
+ }
1153
+ var HTML_ESCAPES = {
1154
+ "&": "&amp;",
1155
+ "<": "&lt;",
1156
+ ">": "&gt;",
1157
+ '"': "&quot;",
1158
+ "'": "&#39;"
1159
+ };
1160
+ function escapeHtml(value) {
1161
+ return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch] ?? ch);
1162
+ }
1163
+ function renderReportToHtml(report, options = {}) {
1164
+ const {
1165
+ title = "Story Report",
1166
+ css = "",
1167
+ theme = "light",
1168
+ headExtra = "",
1169
+ bodyExtra = "",
1170
+ lang = "en",
1171
+ syntaxHighlighting = true,
1172
+ mermaid = true,
1173
+ islandScript = ""
1174
+ } = options;
1175
+ const markup = renderToStaticMarkup(
1176
+ createElement(Report, { report, title })
1177
+ );
1178
+ const cdn = cdnAssets(syntaxHighlighting, mermaid);
1179
+ const parsed = unwrapReport(report);
1180
+ const rawReport = parsed.ok ? parsed.data : void 0;
1181
+ const interactive = islandScript.length > 0 && rawReport !== void 0;
1182
+ const dataScript = interactive ? `<script type="application/json" id="${DATA_ID}">${escapeJsonForScript(JSON.stringify(rawReport))}</script>` : "";
1183
+ const islandTag = interactive ? `<script>${islandScript}</script>` : "";
1184
+ const cdnBody = interactive ? "" : cdn.body;
1185
+ const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}"` : "";
1186
+ const rootAttrs = interactive ? ` id="${ROOT_ID}" data-title="${escapeHtml(title)}"${islandConfigAttrs}` : "";
1187
+ return `<!DOCTYPE html>
1188
+ <html lang="${escapeHtml(lang)}" data-theme="${theme === "dark" ? "dark" : "light"}">
1189
+ <head>
1190
+ <meta charset="UTF-8">
1191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1192
+ <meta name="color-scheme" content="light dark">
1193
+ <title>${escapeHtml(title)}</title>
1194
+ ${cdn.head}
1195
+ ${headExtra}
1196
+ <style>${css}</style>
1197
+ </head>
1198
+ <body>
1199
+ <div${rootAttrs} class="es-report-island font-sans text-foreground">${markup}</div>
1200
+ ${dataScript}
1201
+ ${bodyExtra}
1202
+ ${cdnBody}
1203
+ ${islandTag}
1204
+ </body>
1205
+ </html>`;
1206
+ }
1207
+ export {
1208
+ renderReportToHtml
1209
+ };
1210
+ //# sourceMappingURL=ssr.js.map