executable-stories-react 0.10.0 → 0.11.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.
package/dist/ssr.js CHANGED
@@ -287,23 +287,11 @@ function useReport() {
287
287
  return ctx.report;
288
288
  }
289
289
 
290
- // ../executable-stories-core/dist/utils/duration.js
291
- function formatDuration(ms) {
292
- if (ms < 1e3) {
293
- return `${Math.round(ms)} ms`;
294
- }
295
- if (ms < 6e4) {
296
- return `${(ms / 1e3).toFixed(2)} s`;
297
- }
298
- const minutes = Math.floor(ms / 6e4);
299
- const seconds = Math.floor(ms % 6e4 / 1e3);
300
- if (minutes < 60) {
301
- return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
302
- }
303
- const hours = Math.floor(minutes / 60);
304
- const remainingMinutes = minutes % 60;
305
- return `${hours}h ${remainingMinutes}m`;
306
- }
290
+ // src/components/ReportScenario.tsx
291
+ import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
292
+
293
+ // src/components/ReportSteps.tsx
294
+ import { formatDuration } from "executable-stories-core/utils/duration";
307
295
 
308
296
  // src/components/doc/DocNote.tsx
309
297
  import { jsx as jsx3 } from "react/jsx-runtime";
@@ -392,36 +380,29 @@ function DocLink({ entry }) {
392
380
  );
393
381
  }
394
382
 
395
- // src/components/doc/DocSection.tsx
383
+ // src/lib/markdown.ts
396
384
  import { marked } from "marked";
397
385
 
398
- // ../executable-stories-core/dist/utils/url.js
399
- function safeUrl(value) {
400
- if (!value) return void 0;
401
- const trimmed = value.trim();
402
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
403
- if (scheme && !/^https?$/i.test(scheme[1] ?? "")) return void 0;
404
- return trimmed;
405
- }
406
- function safeImageUrl(value) {
407
- if (!value) return void 0;
408
- const trimmed = value.trim();
409
- if (/^data:image\//i.test(trimmed)) return trimmed;
410
- return safeUrl(trimmed);
411
- }
412
- function isLocalFsPath(value) {
413
- return /^(?:[/\\]|[A-Za-z]:[/\\])/.test(value);
414
- }
386
+ // src/lib/url.ts
387
+ import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
415
388
 
416
- // src/components/doc/DocSection.tsx
417
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
418
- function neutralizeUrl(value) {
419
- return safeUrl(value) ?? "#";
420
- }
389
+ // src/lib/markdown.ts
421
390
  function safeMarkdownHtml(markdown) {
422
391
  const raw = marked.parse(markdown, { async: false });
423
392
  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)}'`);
424
393
  }
394
+ function dedent(text) {
395
+ const lines = text.replace(/\t/g, " ").split("\n");
396
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
397
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
398
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
399
+ }
400
+ function neutralizeUrl(value) {
401
+ return safeUrl(value) ?? "#";
402
+ }
403
+
404
+ // src/components/doc/DocSection.tsx
405
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
425
406
  function DocSection({ entry }) {
426
407
  const renderers = useBuiltinRenderers();
427
408
  if (renderers.section) {
@@ -704,125 +685,10 @@ function ReportStepItem({ step }) {
704
685
  );
705
686
  }
706
687
 
707
- // ../executable-stories-core/dist/state-diff.js
708
- var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
709
- function deepEqual(a, b) {
710
- if (Object.is(a, b)) return true;
711
- if (Array.isArray(a) && Array.isArray(b)) {
712
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
713
- }
714
- if (isPlainObject(a) && isPlainObject(b)) {
715
- const ak = Object.keys(a);
716
- const bk = Object.keys(b);
717
- return ak.length === bk.length && ak.every((k) => k in b && deepEqual(a[k], b[k]));
718
- }
719
- return false;
720
- }
721
- var joinKey = (path, key) => path === "" ? key : `${path}.${key}`;
722
- function walk(path, before, after, out) {
723
- if (Object.is(before, after)) return;
724
- if (Array.isArray(before) && Array.isArray(after)) {
725
- const shared = Math.min(before.length, after.length);
726
- for (let i = 0; i < shared; i++) walk(`${path}[${i}]`, before[i], after[i], out);
727
- for (let i = shared; i < after.length; i++) out.push({ path: `${path}[${i}]`, kind: "added", after: after[i] });
728
- for (let i = shared; i < before.length; i++) out.push({ path: `${path}[${i}]`, kind: "removed", before: before[i] });
729
- return;
730
- }
731
- if (isPlainObject(before) && isPlainObject(after)) {
732
- for (const key of Object.keys(after)) {
733
- const p = joinKey(path, key);
734
- if (key in before) walk(p, before[key], after[key], out);
735
- else out.push({ path: p, kind: "added", after: after[key] });
736
- }
737
- for (const key of Object.keys(before)) {
738
- if (!(key in after)) out.push({ path: joinKey(path, key), kind: "removed", before: before[key] });
739
- }
740
- return;
741
- }
742
- if (!deepEqual(before, after)) out.push({ path, kind: "changed", before, after });
743
- }
744
- function diffStateValues(before, after) {
745
- const out = [];
746
- walk("", before, after, out);
747
- return out;
748
- }
749
- function formatStateValue(value, maxLen = 60) {
750
- const s = value === void 0 ? "undefined" : JSON.stringify(value);
751
- return s.length > maxLen ? `${s.slice(0, maxLen - 1)}\u2026` : s;
752
- }
753
- function summarizeStateChanges(changes) {
754
- return changes.map((c) => {
755
- const at = c.path === "" ? "(root)" : c.path;
756
- switch (c.kind) {
757
- case "added":
758
- return `+ ${at}: ${formatStateValue(c.after)}`;
759
- case "removed":
760
- return `\u2212 ${at}`;
761
- case "changed":
762
- return `${at}: ${formatStateValue(c.before)} \u2192 ${formatStateValue(c.after)}`;
763
- }
764
- });
765
- }
766
-
767
- // ../executable-stories-core/dist/storyboard.js
768
- function findScreenshot(entries) {
769
- for (const entry of entries) {
770
- if (entry.kind === "screenshot") return entry;
771
- if (entry.children) {
772
- const nested = findScreenshot(entry.children);
773
- if (nested) return nested;
774
- }
775
- }
776
- return void 0;
777
- }
778
- function findStates(entries, out = []) {
779
- for (const entry of entries) {
780
- if (entry.kind === "state") out.push(entry);
781
- if (entry.children) findStates(entry.children, out);
782
- }
783
- return out;
784
- }
785
- var laneOf = (label) => label ?? "";
786
- function extractStoryboardFrames(scenario) {
787
- const frames = [];
788
- const laneOrder = [];
789
- const previous = /* @__PURE__ */ new Map();
790
- for (const step of scenario.steps) {
791
- const shot = findScreenshot(step.docEntries);
792
- const stateDocs = findStates(step.docEntries);
793
- if (!shot && stateDocs.length === 0) continue;
794
- const byLane = /* @__PURE__ */ new Map();
795
- for (const doc of stateDocs) {
796
- const lane = laneOf(doc.label);
797
- if (!byLane.has(lane) && !laneOrder.includes(lane)) laneOrder.push(lane);
798
- byLane.set(lane, doc);
799
- }
800
- const states = [];
801
- for (const lane of laneOrder) {
802
- const doc = byLane.get(lane);
803
- if (!doc) continue;
804
- const card = {
805
- ...doc.label !== void 0 && { label: doc.label },
806
- value: doc.value
807
- };
808
- if (previous.has(lane)) card.changes = diffStateValues(previous.get(lane), doc.value);
809
- previous.set(lane, doc.value);
810
- states.push(card);
811
- }
812
- frames.push({
813
- stepId: step.id,
814
- stepIndex: step.index,
815
- keyword: step.keyword,
816
- text: step.text,
817
- status: step.status,
818
- ...shot && { screenshot: { path: shot.path, ...shot.alt !== void 0 && { alt: shot.alt } } },
819
- states
820
- });
821
- }
822
- return frames;
823
- }
824
-
825
688
  // src/components/ReportStoryboard.tsx
689
+ import { extractStoryboardFrames } from "executable-stories-core/storyboard";
690
+ import { summarizeStateChanges } from "executable-stories-core/state-diff";
691
+ import { isLocalFsPath as isLocalFsPath2, safeImageUrl as safeImageUrl2 } from "executable-stories-core/utils/url";
826
692
  import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
827
693
  var FRAME_GLYPH = {
828
694
  passed: "\u2713",
@@ -871,7 +737,7 @@ function StoryboardFrameItem({ frame }) {
871
737
  }
872
738
  function FrameScreenshot({ frame, label }) {
873
739
  const path = frame.screenshot?.path ?? "";
874
- const src = isLocalFsPath(path) ? void 0 : safeImageUrl(path);
740
+ const src = isLocalFsPath2(path) ? void 0 : safeImageUrl2(path);
875
741
  return /* @__PURE__ */ jsx20(
876
742
  "a",
877
743
  {
@@ -977,6 +843,9 @@ function ReportAttachments({ attachments }) {
977
843
  );
978
844
  }
979
845
 
846
+ // src/components/ReportTrace.tsx
847
+ import { formatDuration as formatDuration2 } from "executable-stories-core/utils/duration";
848
+
980
849
  // src/lib/trace.ts
981
850
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
982
851
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -1029,15 +898,15 @@ function toTraceRows(spans) {
1029
898
  roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
1030
899
  const rows = [];
1031
900
  const seen = /* @__PURE__ */ new Set();
1032
- const walk2 = (node, depth) => {
901
+ const walk = (node, depth) => {
1033
902
  if (seen.has(node.span.spanId)) return;
1034
903
  seen.add(node.span.spanId);
1035
904
  rows.push({ ...node.span, depth });
1036
- for (const child of node.children) walk2(child, depth + 1);
905
+ for (const child of node.children) walk(child, depth + 1);
1037
906
  };
1038
- for (const root of roots) walk2(root, 0);
907
+ for (const root of roots) walk(root, 0);
1039
908
  for (const node of byId.values()) {
1040
- if (!seen.has(node.span.spanId)) walk2(node, 0);
909
+ if (!seen.has(node.span.spanId)) walk(node, 0);
1041
910
  }
1042
911
  return rows;
1043
912
  }
@@ -1071,7 +940,7 @@ function ReportTrace({ spans }) {
1071
940
  " span",
1072
941
  rows.length === 1 ? "" : "s",
1073
942
  ", ",
1074
- formatDuration(totalMs),
943
+ formatDuration2(totalMs),
1075
944
  ")"
1076
945
  ] }),
1077
946
  /* @__PURE__ */ jsx22("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -1094,7 +963,7 @@ function ReportTrace({ spans }) {
1094
963
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
1095
964
  }
1096
965
  ) }),
1097
- /* @__PURE__ */ jsx22("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
966
+ /* @__PURE__ */ jsx22("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration2(row.durationMs) })
1098
967
  ] }, `${row.spanId}-${i}`);
1099
968
  }) })
1100
969
  ] });
@@ -1205,6 +1074,7 @@ function useCollapse() {
1205
1074
 
1206
1075
  // src/interactive/scenario-actions.tsx
1207
1076
  import { createContext as createContext4, useContext as useContext5 } from "react";
1077
+ import { scenarioToMarkdown as coreScenarioToMarkdown } from "executable-stories-core/utils/scenario-markdown";
1208
1078
  var ScenarioActionsContext = createContext4(null);
1209
1079
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
1210
1080
  function useScenarioActions() {
@@ -1350,7 +1220,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1350
1220
  ] }),
1351
1221
  /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1352
1222
  /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1353
- scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
1223
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1354
1224
  meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1355
1225
  actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
1356
1226
  /* @__PURE__ */ jsx26(
@@ -1409,8 +1279,35 @@ function ReportScenarioList({ feature }) {
1409
1279
  return /* @__PURE__ */ jsx27(Fragment8, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx27(ReportScenario, { scenario }, scenario.id)) });
1410
1280
  }
1411
1281
 
1282
+ // src/components/ReportFeatureIntro.tsx
1283
+ import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1284
+ function ReportFeatureIntro({ feature }) {
1285
+ const hasNarrative = Boolean(feature.narrative?.trim());
1286
+ const hasGlossary = Boolean(feature.glossary?.length);
1287
+ if (!hasNarrative && !hasGlossary) return null;
1288
+ return /* @__PURE__ */ jsxs18("div", { className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-sm", children: [
1289
+ hasNarrative ? /* @__PURE__ */ jsx28(
1290
+ "div",
1291
+ {
1292
+ className: "es-doc-prose prose prose-sm max-w-none text-muted-foreground",
1293
+ dangerouslySetInnerHTML: { __html: safeMarkdownHtml(dedent(feature.narrative)) }
1294
+ }
1295
+ ) : null,
1296
+ hasGlossary ? /* @__PURE__ */ jsx28("dl", { className: "grid gap-1.5", "aria-label": "Glossary", children: feature.glossary.map((entry) => /* @__PURE__ */ jsxs18("div", { className: "flex flex-wrap gap-x-2 text-xs", children: [
1297
+ /* @__PURE__ */ jsx28("dt", { className: "font-medium text-foreground", children: entry.term }),
1298
+ /* @__PURE__ */ jsx28("dd", { className: "flex-1 text-muted-foreground", children: entry.definition })
1299
+ ] }, entry.term)) }) : null
1300
+ ] });
1301
+ }
1302
+ function featureKindLabel(kind) {
1303
+ if (kind === "ability") return "Ability";
1304
+ if (kind === "business-need") return "Business need";
1305
+ if (kind === "feature") return "Feature";
1306
+ return null;
1307
+ }
1308
+
1412
1309
  // src/components/ReportFeature.tsx
1413
- import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1310
+ import { Fragment as Fragment9, jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
1414
1311
  function ReportFeature({ feature }) {
1415
1312
  const titleId = `${feature.id}-title`;
1416
1313
  const bodyId = `${feature.id}-body`;
@@ -1418,10 +1315,11 @@ function ReportFeature({ feature }) {
1418
1315
  const skipped = s.skipped + s.pending;
1419
1316
  const collapse = useCollapse();
1420
1317
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
1318
+ const kindLabel = featureKindLabel(feature.kind);
1421
1319
  return (
1422
1320
  // Suite = a section heading + a divider, NOT another bordered card. Drops a
1423
1321
  // layer of nesting so the scenario cards below read as the primary unit.
1424
- /* @__PURE__ */ jsxs18(
1322
+ /* @__PURE__ */ jsxs19(
1425
1323
  "section",
1426
1324
  {
1427
1325
  id: feature.id,
@@ -1429,9 +1327,9 @@ function ReportFeature({ feature }) {
1429
1327
  "aria-labelledby": titleId,
1430
1328
  className: "flex flex-col gap-3",
1431
1329
  children: [
1432
- /* @__PURE__ */ jsxs18("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1433
- /* @__PURE__ */ jsxs18("div", { className: "min-w-0", children: [
1434
- /* @__PURE__ */ jsx28("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ jsxs18(
1330
+ /* @__PURE__ */ jsxs19("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1331
+ /* @__PURE__ */ jsxs19("div", { className: "min-w-0", children: [
1332
+ /* @__PURE__ */ jsx29("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ jsxs19(
1435
1333
  "button",
1436
1334
  {
1437
1335
  type: "button",
@@ -1440,31 +1338,34 @@ function ReportFeature({ feature }) {
1440
1338
  "aria-controls": bodyId,
1441
1339
  className: "flex w-full cursor-pointer items-center gap-2 text-left hover:text-foreground/80",
1442
1340
  children: [
1443
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1444
- /* @__PURE__ */ jsx28("span", { className: "truncate", children: feature.title })
1341
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1342
+ /* @__PURE__ */ jsx29("span", { className: "truncate", children: feature.title })
1445
1343
  ]
1446
1344
  }
1447
1345
  ) : feature.title }),
1448
- /* @__PURE__ */ jsx28("p", { className: cn("mt-0.5 truncate font-mono text-xs text-muted-foreground", collapse && "pl-5"), children: feature.sourceFile })
1346
+ /* @__PURE__ */ jsxs19("p", { className: cn("mt-0.5 flex items-center gap-2 text-xs text-muted-foreground", collapse && "pl-5"), children: [
1347
+ kindLabel ? /* @__PURE__ */ jsx29("span", { className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 font-medium text-foreground", children: kindLabel }) : null,
1348
+ /* @__PURE__ */ jsx29("span", { className: "truncate font-mono", children: feature.sourceFile })
1349
+ ] })
1449
1350
  ] }),
1450
- /* @__PURE__ */ jsxs18(
1351
+ /* @__PURE__ */ jsxs19(
1451
1352
  "p",
1452
1353
  {
1453
1354
  className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground",
1454
1355
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
1455
1356
  children: [
1456
- /* @__PURE__ */ jsxs18("span", { children: [
1357
+ /* @__PURE__ */ jsxs19("span", { children: [
1457
1358
  s.passed,
1458
1359
  " passed"
1459
1360
  ] }),
1460
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, children: "\xB7" }),
1461
- /* @__PURE__ */ jsxs18("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1361
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, children: "\xB7" }),
1362
+ /* @__PURE__ */ jsxs19("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1462
1363
  s.failed,
1463
1364
  " failed"
1464
1365
  ] }),
1465
- skipped > 0 ? /* @__PURE__ */ jsxs18(Fragment9, { children: [
1466
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, children: "\xB7" }),
1467
- /* @__PURE__ */ jsxs18("span", { children: [
1366
+ skipped > 0 ? /* @__PURE__ */ jsxs19(Fragment9, { children: [
1367
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, children: "\xB7" }),
1368
+ /* @__PURE__ */ jsxs19("span", { children: [
1468
1369
  skipped,
1469
1370
  " skipped"
1470
1371
  ] })
@@ -1473,7 +1374,10 @@ function ReportFeature({ feature }) {
1473
1374
  }
1474
1375
  )
1475
1376
  ] }),
1476
- /* @__PURE__ */ jsx28("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: /* @__PURE__ */ jsx28(ReportScenarioList, { feature }) })
1377
+ /* @__PURE__ */ jsxs19("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: [
1378
+ /* @__PURE__ */ jsx29(ReportFeatureIntro, { feature }),
1379
+ /* @__PURE__ */ jsx29(ReportScenarioList, { feature })
1380
+ ] })
1477
1381
  ]
1478
1382
  }
1479
1383
  )
@@ -1481,17 +1385,17 @@ function ReportFeature({ feature }) {
1481
1385
  }
1482
1386
 
1483
1387
  // src/components/ReportFeatureList.tsx
1484
- import { jsx as jsx29 } from "react/jsx-runtime";
1388
+ import { jsx as jsx30 } from "react/jsx-runtime";
1485
1389
  function ReportFeatureList() {
1486
1390
  const report = useReport();
1487
- return /* @__PURE__ */ jsx29("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx29(ReportFeature, { feature }, feature.id)) });
1391
+ return /* @__PURE__ */ jsx30("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx30(ReportFeature, { feature }, feature.id)) });
1488
1392
  }
1489
1393
 
1490
1394
  // src/components/ui/empty.tsx
1491
1395
  import { cva as cva2 } from "class-variance-authority";
1492
- import { jsx as jsx30 } from "react/jsx-runtime";
1396
+ import { jsx as jsx31 } from "react/jsx-runtime";
1493
1397
  function Empty({ className, ...props }) {
1494
- return /* @__PURE__ */ jsx30(
1398
+ return /* @__PURE__ */ jsx31(
1495
1399
  "div",
1496
1400
  {
1497
1401
  "data-slot": "empty",
@@ -1504,7 +1408,7 @@ function Empty({ className, ...props }) {
1504
1408
  );
1505
1409
  }
1506
1410
  function EmptyHeader({ className, ...props }) {
1507
- return /* @__PURE__ */ jsx30(
1411
+ return /* @__PURE__ */ jsx31(
1508
1412
  "div",
1509
1413
  {
1510
1414
  "data-slot": "empty-header",
@@ -1531,7 +1435,7 @@ var emptyMediaVariants = cva2(
1531
1435
  }
1532
1436
  );
1533
1437
  function EmptyTitle({ className, ...props }) {
1534
- return /* @__PURE__ */ jsx30(
1438
+ return /* @__PURE__ */ jsx31(
1535
1439
  "div",
1536
1440
  {
1537
1441
  "data-slot": "empty-title",
@@ -1542,16 +1446,16 @@ function EmptyTitle({ className, ...props }) {
1542
1446
  }
1543
1447
 
1544
1448
  // src/components/ReportEmpty.tsx
1545
- import { jsx as jsx31 } from "react/jsx-runtime";
1449
+ import { jsx as jsx32 } from "react/jsx-runtime";
1546
1450
  function ReportEmpty({ message }) {
1547
- return /* @__PURE__ */ jsx31(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx31(EmptyHeader, { children: /* @__PURE__ */ jsx31(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1451
+ return /* @__PURE__ */ jsx32(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx32(EmptyHeader, { children: /* @__PURE__ */ jsx32(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1548
1452
  }
1549
1453
 
1550
1454
  // src/components/ReportSummary.tsx
1551
- import { jsx as jsx32, jsxs as jsxs19 } from "react/jsx-runtime";
1455
+ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1552
1456
  function ReportSummary({ className }) {
1553
1457
  const report = useReport();
1554
- return /* @__PURE__ */ jsx32(
1458
+ return /* @__PURE__ */ jsx33(
1555
1459
  ReportSummaryView,
1556
1460
  {
1557
1461
  summary: report.summary,
@@ -1577,7 +1481,7 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1577
1481
  if (summary.pending > 0) {
1578
1482
  cards.push({ status: "pending", label: "Pending", value: summary.pending });
1579
1483
  }
1580
- return /* @__PURE__ */ jsx32(
1484
+ return /* @__PURE__ */ jsx33(
1581
1485
  "dl",
1582
1486
  {
1583
1487
  className: cn(
@@ -1590,17 +1494,17 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1590
1494
  "aria-label": ariaLabel,
1591
1495
  children: cards.map((c) => {
1592
1496
  const tone = TONE[c.status];
1593
- return /* @__PURE__ */ jsxs19(
1497
+ return /* @__PURE__ */ jsxs20(
1594
1498
  "div",
1595
1499
  {
1596
1500
  "data-status": c.status,
1597
1501
  className: "rounded-lg border bg-card px-4 py-3",
1598
1502
  children: [
1599
- /* @__PURE__ */ jsxs19("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1600
- /* @__PURE__ */ jsx32("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1503
+ /* @__PURE__ */ jsxs20("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1504
+ /* @__PURE__ */ jsx33("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1601
1505
  c.label
1602
1506
  ] }),
1603
- /* @__PURE__ */ jsx32("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1507
+ /* @__PURE__ */ jsx33("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1604
1508
  ]
1605
1509
  },
1606
1510
  c.status
@@ -1610,6 +1514,9 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1610
1514
  );
1611
1515
  }
1612
1516
 
1517
+ // src/components/ReportMeta.tsx
1518
+ import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1519
+
1613
1520
  // src/lib/provenance.ts
1614
1521
  var CI_DISPLAY_NAMES = {
1615
1522
  github: "GitHub Actions",
@@ -1641,7 +1548,7 @@ function prUrl(ci) {
1641
1548
  }
1642
1549
 
1643
1550
  // src/components/ReportMeta.tsx
1644
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1551
+ import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1645
1552
  function ReportMeta() {
1646
1553
  const report = useReport();
1647
1554
  const items = [];
@@ -1650,7 +1557,7 @@ function ReportMeta() {
1650
1557
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1651
1558
  }
1652
1559
  if (report.durationMs) {
1653
- items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1560
+ items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1654
1561
  }
1655
1562
  if (report.packageVersion) {
1656
1563
  items.push({ label: "Version", value: report.packageVersion });
@@ -1664,13 +1571,13 @@ function ReportMeta() {
1664
1571
  const short = sha.slice(0, 8);
1665
1572
  items.push({
1666
1573
  label: "Commit",
1667
- value: url ? /* @__PURE__ */ jsx33("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1574
+ value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1668
1575
  });
1669
1576
  }
1670
1577
  if (report.ci) {
1671
1578
  items.push({
1672
1579
  label: "CI",
1673
- value: report.ci.url ? /* @__PURE__ */ jsxs20("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1580
+ value: report.ci.url ? /* @__PURE__ */ jsxs21("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1674
1581
  ciDisplayName(report.ci.name),
1675
1582
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1676
1583
  ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
@@ -1681,7 +1588,7 @@ function ReportMeta() {
1681
1588
  const label = `#${report.ci.prNumber}`;
1682
1589
  items.push({
1683
1590
  label: "PR",
1684
- value: url ? /* @__PURE__ */ jsx33("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1591
+ value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1685
1592
  });
1686
1593
  }
1687
1594
  if (items.length === 0) return null;
@@ -1689,48 +1596,48 @@ function ReportMeta() {
1689
1596
  // Run metadata is useful but secondary to "what failed" — tuck it into a
1690
1597
  // collapsed disclosure so it doesn't push the result down. Native <details>
1691
1598
  // so it works with no JS (the SSR/no-hydration report still toggles).
1692
- /* @__PURE__ */ jsxs20("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1693
- /* @__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: [
1694
- /* @__PURE__ */ jsx33("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1599
+ /* @__PURE__ */ jsxs21("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1600
+ /* @__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: [
1601
+ /* @__PURE__ */ jsx34("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1695
1602
  "Run details"
1696
1603
  ] }),
1697
- /* @__PURE__ */ jsx33("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: [
1698
- /* @__PURE__ */ jsx33("dt", { className: "font-medium", children: item.label }),
1699
- /* @__PURE__ */ jsx33("dd", { className: "font-mono text-foreground", children: item.value })
1604
+ /* @__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: [
1605
+ /* @__PURE__ */ jsx34("dt", { className: "font-medium", children: item.label }),
1606
+ /* @__PURE__ */ jsx34("dd", { className: "font-mono text-foreground", children: item.value })
1700
1607
  ] }, item.label)) })
1701
1608
  ] })
1702
1609
  );
1703
1610
  }
1704
1611
 
1705
1612
  // src/components/ReportSchemaError.tsx
1706
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1613
+ import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1707
1614
  function ReportSchemaError({ error }) {
1708
- return /* @__PURE__ */ jsxs21("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1709
- /* @__PURE__ */ jsx34("p", { children: /* @__PURE__ */ jsx34("strong", { children: "Report could not be displayed." }) }),
1710
- /* @__PURE__ */ jsx34("p", { children: error.message }),
1711
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs21("p", { children: [
1615
+ return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1616
+ /* @__PURE__ */ jsx35("p", { children: /* @__PURE__ */ jsx35("strong", { children: "Report could not be displayed." }) }),
1617
+ /* @__PURE__ */ jsx35("p", { children: error.message }),
1618
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1712
1619
  "The report bundle is newer than this version of ",
1713
- /* @__PURE__ */ jsx34("code", { children: "executable-stories-react" }),
1620
+ /* @__PURE__ */ jsx35("code", { children: "executable-stories-react" }),
1714
1621
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1715
1622
  ] }) : null,
1716
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs21("details", { children: [
1717
- /* @__PURE__ */ jsxs21("summary", { children: [
1623
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1624
+ /* @__PURE__ */ jsxs22("summary", { children: [
1718
1625
  error.issues.length,
1719
1626
  " validation issue",
1720
1627
  error.issues.length === 1 ? "" : "s"
1721
1628
  ] }),
1722
- /* @__PURE__ */ jsx34("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1629
+ /* @__PURE__ */ jsx35("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1723
1630
  ] }) : null
1724
1631
  ] });
1725
1632
  }
1726
1633
 
1727
1634
  // src/components/ReportShell.tsx
1728
- import { Fragment as Fragment10, jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1635
+ import { Fragment as Fragment10, jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1729
1636
  function ReportTitleBlock({ title }) {
1730
- return /* @__PURE__ */ jsxs22(Fragment10, { children: [
1731
- /* @__PURE__ */ jsx35("h1", { children: title ?? "Story Report" }),
1732
- /* @__PURE__ */ jsx35(ReportSummary, {}),
1733
- /* @__PURE__ */ jsx35(ReportMeta, {})
1637
+ return /* @__PURE__ */ jsxs23(Fragment10, { children: [
1638
+ /* @__PURE__ */ jsx36("h1", { children: title ?? "Story Report" }),
1639
+ /* @__PURE__ */ jsx36(ReportSummary, {}),
1640
+ /* @__PURE__ */ jsx36(ReportMeta, {})
1734
1641
  ] });
1735
1642
  }
1736
1643
  function ReportErrorShell({
@@ -1739,22 +1646,22 @@ function ReportErrorShell({
1739
1646
  title,
1740
1647
  dataTheme
1741
1648
  }) {
1742
- return /* @__PURE__ */ jsx35("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx35(ReportSchemaError, { error }) });
1649
+ return /* @__PURE__ */ jsx36("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx36(ReportSchemaError, { error }) });
1743
1650
  }
1744
1651
 
1745
1652
  // src/components/Report.tsx
1746
- import { jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1653
+ import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1747
1654
  function Report(props) {
1748
1655
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1749
1656
  const result = unwrapReport(report);
1750
1657
  if (!result.ok) {
1751
- return /* @__PURE__ */ jsx36(ReportErrorShell, { error: result.error, className, title, dataTheme });
1658
+ return /* @__PURE__ */ jsx37(ReportErrorShell, { error: result.error, className, title, dataTheme });
1752
1659
  }
1753
1660
  const data = result.data;
1754
1661
  const hasContent = data.features.length > 0;
1755
- return /* @__PURE__ */ jsx36(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs23("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1756
- /* @__PURE__ */ jsx36("header", { className: "es-report-header", children: /* @__PURE__ */ jsx36(ReportTitleBlock, { title }) }),
1757
- hasContent ? /* @__PURE__ */ jsx36(ReportFeatureList, {}) : /* @__PURE__ */ jsx36(ReportEmpty, {})
1662
+ return /* @__PURE__ */ jsx37(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs24("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1663
+ /* @__PURE__ */ jsx37("header", { className: "es-report-header", children: /* @__PURE__ */ jsx37(ReportTitleBlock, { title }) }),
1664
+ hasContent ? /* @__PURE__ */ jsx37(ReportFeatureList, {}) : /* @__PURE__ */ jsx37(ReportEmpty, {})
1758
1665
  ] }) });
1759
1666
  }
1760
1667