executable-stories-react 0.10.1 → 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/index.js CHANGED
@@ -379,21 +379,29 @@ function DocLink({ entry }) {
379
379
  );
380
380
  }
381
381
 
382
- // src/components/doc/DocSection.tsx
382
+ // src/lib/markdown.ts
383
383
  import { marked } from "marked";
384
384
 
385
385
  // src/lib/url.ts
386
386
  import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
387
387
 
388
- // src/components/doc/DocSection.tsx
389
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
390
- function neutralizeUrl(value) {
391
- return safeUrl(value) ?? "#";
392
- }
388
+ // src/lib/markdown.ts
393
389
  function safeMarkdownHtml(markdown) {
394
390
  const raw = marked.parse(markdown, { async: false });
395
391
  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)}'`);
396
392
  }
393
+ function dedent(text) {
394
+ const lines = text.replace(/\t/g, " ").split("\n");
395
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
396
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
397
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
398
+ }
399
+ function neutralizeUrl(value) {
400
+ return safeUrl(value) ?? "#";
401
+ }
402
+
403
+ // src/components/doc/DocSection.tsx
404
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
397
405
  function DocSection({ entry }) {
398
406
  const renderers = useBuiltinRenderers();
399
407
  if (renderers.section) {
@@ -1270,8 +1278,35 @@ function ReportScenarioList({ feature }) {
1270
1278
  return /* @__PURE__ */ jsx27(Fragment8, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx27(ReportScenario, { scenario }, scenario.id)) });
1271
1279
  }
1272
1280
 
1281
+ // src/components/ReportFeatureIntro.tsx
1282
+ import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1283
+ function ReportFeatureIntro({ feature }) {
1284
+ const hasNarrative = Boolean(feature.narrative?.trim());
1285
+ const hasGlossary = Boolean(feature.glossary?.length);
1286
+ if (!hasNarrative && !hasGlossary) return null;
1287
+ return /* @__PURE__ */ jsxs18("div", { className: "flex flex-col gap-3 border-l-2 border-border pl-4 text-sm", children: [
1288
+ hasNarrative ? /* @__PURE__ */ jsx28(
1289
+ "div",
1290
+ {
1291
+ className: "es-doc-prose prose prose-sm max-w-none text-muted-foreground",
1292
+ dangerouslySetInnerHTML: { __html: safeMarkdownHtml(dedent(feature.narrative)) }
1293
+ }
1294
+ ) : null,
1295
+ 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: [
1296
+ /* @__PURE__ */ jsx28("dt", { className: "font-medium text-foreground", children: entry.term }),
1297
+ /* @__PURE__ */ jsx28("dd", { className: "flex-1 text-muted-foreground", children: entry.definition })
1298
+ ] }, entry.term)) }) : null
1299
+ ] });
1300
+ }
1301
+ function featureKindLabel(kind) {
1302
+ if (kind === "ability") return "Ability";
1303
+ if (kind === "business-need") return "Business need";
1304
+ if (kind === "feature") return "Feature";
1305
+ return null;
1306
+ }
1307
+
1273
1308
  // src/components/ReportFeature.tsx
1274
- import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1309
+ import { Fragment as Fragment9, jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
1275
1310
  function ReportFeature({ feature }) {
1276
1311
  const titleId = `${feature.id}-title`;
1277
1312
  const bodyId = `${feature.id}-body`;
@@ -1279,10 +1314,11 @@ function ReportFeature({ feature }) {
1279
1314
  const skipped = s.skipped + s.pending;
1280
1315
  const collapse = useCollapse();
1281
1316
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
1317
+ const kindLabel = featureKindLabel(feature.kind);
1282
1318
  return (
1283
1319
  // Suite = a section heading + a divider, NOT another bordered card. Drops a
1284
1320
  // layer of nesting so the scenario cards below read as the primary unit.
1285
- /* @__PURE__ */ jsxs18(
1321
+ /* @__PURE__ */ jsxs19(
1286
1322
  "section",
1287
1323
  {
1288
1324
  id: feature.id,
@@ -1290,9 +1326,9 @@ function ReportFeature({ feature }) {
1290
1326
  "aria-labelledby": titleId,
1291
1327
  className: "flex flex-col gap-3",
1292
1328
  children: [
1293
- /* @__PURE__ */ jsxs18("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1294
- /* @__PURE__ */ jsxs18("div", { className: "min-w-0", children: [
1295
- /* @__PURE__ */ jsx28("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ jsxs18(
1329
+ /* @__PURE__ */ jsxs19("div", { className: "flex items-end justify-between gap-4 border-b border-border pb-2", children: [
1330
+ /* @__PURE__ */ jsxs19("div", { className: "min-w-0", children: [
1331
+ /* @__PURE__ */ jsx29("h2", { id: titleId, className: "text-base font-semibold tracking-tight text-foreground", children: collapse ? /* @__PURE__ */ jsxs19(
1296
1332
  "button",
1297
1333
  {
1298
1334
  type: "button",
@@ -1301,31 +1337,34 @@ function ReportFeature({ feature }) {
1301
1337
  "aria-controls": bodyId,
1302
1338
  className: "flex w-full cursor-pointer items-center gap-2 text-left hover:text-foreground/80",
1303
1339
  children: [
1304
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1305
- /* @__PURE__ */ jsx28("span", { className: "truncate", children: feature.title })
1340
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, className: cn("shrink-0 text-xs text-muted-foreground transition-transform", !collapsed && "rotate-90"), children: "\u25B8" }),
1341
+ /* @__PURE__ */ jsx29("span", { className: "truncate", children: feature.title })
1306
1342
  ]
1307
1343
  }
1308
1344
  ) : feature.title }),
1309
- /* @__PURE__ */ jsx28("p", { className: cn("mt-0.5 truncate font-mono text-xs text-muted-foreground", collapse && "pl-5"), children: feature.sourceFile })
1345
+ /* @__PURE__ */ jsxs19("p", { className: cn("mt-0.5 flex items-center gap-2 text-xs text-muted-foreground", collapse && "pl-5"), children: [
1346
+ kindLabel ? /* @__PURE__ */ jsx29("span", { className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 font-medium text-foreground", children: kindLabel }) : null,
1347
+ /* @__PURE__ */ jsx29("span", { className: "truncate font-mono", children: feature.sourceFile })
1348
+ ] })
1310
1349
  ] }),
1311
- /* @__PURE__ */ jsxs18(
1350
+ /* @__PURE__ */ jsxs19(
1312
1351
  "p",
1313
1352
  {
1314
1353
  className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground",
1315
1354
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
1316
1355
  children: [
1317
- /* @__PURE__ */ jsxs18("span", { children: [
1356
+ /* @__PURE__ */ jsxs19("span", { children: [
1318
1357
  s.passed,
1319
1358
  " passed"
1320
1359
  ] }),
1321
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, children: "\xB7" }),
1322
- /* @__PURE__ */ jsxs18("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1360
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, children: "\xB7" }),
1361
+ /* @__PURE__ */ jsxs19("span", { className: cn(s.failed > 0 && "font-medium text-fail"), children: [
1323
1362
  s.failed,
1324
1363
  " failed"
1325
1364
  ] }),
1326
- skipped > 0 ? /* @__PURE__ */ jsxs18(Fragment9, { children: [
1327
- /* @__PURE__ */ jsx28("span", { "aria-hidden": true, children: "\xB7" }),
1328
- /* @__PURE__ */ jsxs18("span", { children: [
1365
+ skipped > 0 ? /* @__PURE__ */ jsxs19(Fragment9, { children: [
1366
+ /* @__PURE__ */ jsx29("span", { "aria-hidden": true, children: "\xB7" }),
1367
+ /* @__PURE__ */ jsxs19("span", { children: [
1329
1368
  skipped,
1330
1369
  " skipped"
1331
1370
  ] })
@@ -1334,7 +1373,10 @@ function ReportFeature({ feature }) {
1334
1373
  }
1335
1374
  )
1336
1375
  ] }),
1337
- /* @__PURE__ */ jsx28("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: /* @__PURE__ */ jsx28(ReportScenarioList, { feature }) })
1376
+ /* @__PURE__ */ jsxs19("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-3", children: [
1377
+ /* @__PURE__ */ jsx29(ReportFeatureIntro, { feature }),
1378
+ /* @__PURE__ */ jsx29(ReportScenarioList, { feature })
1379
+ ] })
1338
1380
  ]
1339
1381
  }
1340
1382
  )
@@ -1342,17 +1384,17 @@ function ReportFeature({ feature }) {
1342
1384
  }
1343
1385
 
1344
1386
  // src/components/ReportFeatureList.tsx
1345
- import { jsx as jsx29 } from "react/jsx-runtime";
1387
+ import { jsx as jsx30 } from "react/jsx-runtime";
1346
1388
  function ReportFeatureList() {
1347
1389
  const report = useReport();
1348
- return /* @__PURE__ */ jsx29("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx29(ReportFeature, { feature }, feature.id)) });
1390
+ return /* @__PURE__ */ jsx30("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx30(ReportFeature, { feature }, feature.id)) });
1349
1391
  }
1350
1392
 
1351
1393
  // src/components/ui/empty.tsx
1352
1394
  import { cva as cva2 } from "class-variance-authority";
1353
- import { jsx as jsx30 } from "react/jsx-runtime";
1395
+ import { jsx as jsx31 } from "react/jsx-runtime";
1354
1396
  function Empty({ className, ...props }) {
1355
- return /* @__PURE__ */ jsx30(
1397
+ return /* @__PURE__ */ jsx31(
1356
1398
  "div",
1357
1399
  {
1358
1400
  "data-slot": "empty",
@@ -1365,7 +1407,7 @@ function Empty({ className, ...props }) {
1365
1407
  );
1366
1408
  }
1367
1409
  function EmptyHeader({ className, ...props }) {
1368
- return /* @__PURE__ */ jsx30(
1410
+ return /* @__PURE__ */ jsx31(
1369
1411
  "div",
1370
1412
  {
1371
1413
  "data-slot": "empty-header",
@@ -1392,7 +1434,7 @@ var emptyMediaVariants = cva2(
1392
1434
  }
1393
1435
  );
1394
1436
  function EmptyTitle({ className, ...props }) {
1395
- return /* @__PURE__ */ jsx30(
1437
+ return /* @__PURE__ */ jsx31(
1396
1438
  "div",
1397
1439
  {
1398
1440
  "data-slot": "empty-title",
@@ -1403,16 +1445,16 @@ function EmptyTitle({ className, ...props }) {
1403
1445
  }
1404
1446
 
1405
1447
  // src/components/ReportEmpty.tsx
1406
- import { jsx as jsx31 } from "react/jsx-runtime";
1448
+ import { jsx as jsx32 } from "react/jsx-runtime";
1407
1449
  function ReportEmpty({ message }) {
1408
- return /* @__PURE__ */ jsx31(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx31(EmptyHeader, { children: /* @__PURE__ */ jsx31(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1450
+ return /* @__PURE__ */ jsx32(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx32(EmptyHeader, { children: /* @__PURE__ */ jsx32(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1409
1451
  }
1410
1452
 
1411
1453
  // src/components/ReportSummary.tsx
1412
- import { jsx as jsx32, jsxs as jsxs19 } from "react/jsx-runtime";
1454
+ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1413
1455
  function ReportSummary({ className }) {
1414
1456
  const report = useReport();
1415
- return /* @__PURE__ */ jsx32(
1457
+ return /* @__PURE__ */ jsx33(
1416
1458
  ReportSummaryView,
1417
1459
  {
1418
1460
  summary: report.summary,
@@ -1438,7 +1480,7 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1438
1480
  if (summary.pending > 0) {
1439
1481
  cards.push({ status: "pending", label: "Pending", value: summary.pending });
1440
1482
  }
1441
- return /* @__PURE__ */ jsx32(
1483
+ return /* @__PURE__ */ jsx33(
1442
1484
  "dl",
1443
1485
  {
1444
1486
  className: cn(
@@ -1451,17 +1493,17 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1451
1493
  "aria-label": ariaLabel,
1452
1494
  children: cards.map((c) => {
1453
1495
  const tone = TONE[c.status];
1454
- return /* @__PURE__ */ jsxs19(
1496
+ return /* @__PURE__ */ jsxs20(
1455
1497
  "div",
1456
1498
  {
1457
1499
  "data-status": c.status,
1458
1500
  className: "rounded-lg border bg-card px-4 py-3",
1459
1501
  children: [
1460
- /* @__PURE__ */ jsxs19("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1461
- /* @__PURE__ */ jsx32("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1502
+ /* @__PURE__ */ jsxs20("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1503
+ /* @__PURE__ */ jsx33("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1462
1504
  c.label
1463
1505
  ] }),
1464
- /* @__PURE__ */ jsx32("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1506
+ /* @__PURE__ */ jsx33("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1465
1507
  ]
1466
1508
  },
1467
1509
  c.status
@@ -1524,7 +1566,7 @@ function prUrl(ci) {
1524
1566
  }
1525
1567
 
1526
1568
  // src/components/ReportMeta.tsx
1527
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1569
+ import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1528
1570
  function ReportMeta() {
1529
1571
  const report = useReport();
1530
1572
  const items = [];
@@ -1547,13 +1589,13 @@ function ReportMeta() {
1547
1589
  const short = sha.slice(0, 8);
1548
1590
  items.push({
1549
1591
  label: "Commit",
1550
- value: url ? /* @__PURE__ */ jsx33("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1592
+ value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1551
1593
  });
1552
1594
  }
1553
1595
  if (report.ci) {
1554
1596
  items.push({
1555
1597
  label: "CI",
1556
- value: report.ci.url ? /* @__PURE__ */ jsxs20("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1598
+ value: report.ci.url ? /* @__PURE__ */ jsxs21("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1557
1599
  ciDisplayName(report.ci.name),
1558
1600
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1559
1601
  ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
@@ -1564,7 +1606,7 @@ function ReportMeta() {
1564
1606
  const label = `#${report.ci.prNumber}`;
1565
1607
  items.push({
1566
1608
  label: "PR",
1567
- value: url ? /* @__PURE__ */ jsx33("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1609
+ value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1568
1610
  });
1569
1611
  }
1570
1612
  if (items.length === 0) return null;
@@ -1572,48 +1614,48 @@ function ReportMeta() {
1572
1614
  // Run metadata is useful but secondary to "what failed" — tuck it into a
1573
1615
  // collapsed disclosure so it doesn't push the result down. Native <details>
1574
1616
  // so it works with no JS (the SSR/no-hydration report still toggles).
1575
- /* @__PURE__ */ jsxs20("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1576
- /* @__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: [
1577
- /* @__PURE__ */ jsx33("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1617
+ /* @__PURE__ */ jsxs21("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1618
+ /* @__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: [
1619
+ /* @__PURE__ */ jsx34("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1578
1620
  "Run details"
1579
1621
  ] }),
1580
- /* @__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: [
1581
- /* @__PURE__ */ jsx33("dt", { className: "font-medium", children: item.label }),
1582
- /* @__PURE__ */ jsx33("dd", { className: "font-mono text-foreground", children: item.value })
1622
+ /* @__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: [
1623
+ /* @__PURE__ */ jsx34("dt", { className: "font-medium", children: item.label }),
1624
+ /* @__PURE__ */ jsx34("dd", { className: "font-mono text-foreground", children: item.value })
1583
1625
  ] }, item.label)) })
1584
1626
  ] })
1585
1627
  );
1586
1628
  }
1587
1629
 
1588
1630
  // src/components/ReportSchemaError.tsx
1589
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1631
+ import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1590
1632
  function ReportSchemaError({ error }) {
1591
- return /* @__PURE__ */ jsxs21("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1592
- /* @__PURE__ */ jsx34("p", { children: /* @__PURE__ */ jsx34("strong", { children: "Report could not be displayed." }) }),
1593
- /* @__PURE__ */ jsx34("p", { children: error.message }),
1594
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs21("p", { children: [
1633
+ return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1634
+ /* @__PURE__ */ jsx35("p", { children: /* @__PURE__ */ jsx35("strong", { children: "Report could not be displayed." }) }),
1635
+ /* @__PURE__ */ jsx35("p", { children: error.message }),
1636
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1595
1637
  "The report bundle is newer than this version of ",
1596
- /* @__PURE__ */ jsx34("code", { children: "executable-stories-react" }),
1638
+ /* @__PURE__ */ jsx35("code", { children: "executable-stories-react" }),
1597
1639
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1598
1640
  ] }) : null,
1599
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs21("details", { children: [
1600
- /* @__PURE__ */ jsxs21("summary", { children: [
1641
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1642
+ /* @__PURE__ */ jsxs22("summary", { children: [
1601
1643
  error.issues.length,
1602
1644
  " validation issue",
1603
1645
  error.issues.length === 1 ? "" : "s"
1604
1646
  ] }),
1605
- /* @__PURE__ */ jsx34("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1647
+ /* @__PURE__ */ jsx35("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1606
1648
  ] }) : null
1607
1649
  ] });
1608
1650
  }
1609
1651
 
1610
1652
  // src/components/ReportShell.tsx
1611
- import { Fragment as Fragment10, jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1653
+ import { Fragment as Fragment10, jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1612
1654
  function ReportTitleBlock({ title }) {
1613
- return /* @__PURE__ */ jsxs22(Fragment10, { children: [
1614
- /* @__PURE__ */ jsx35("h1", { children: title ?? "Story Report" }),
1615
- /* @__PURE__ */ jsx35(ReportSummary, {}),
1616
- /* @__PURE__ */ jsx35(ReportMeta, {})
1655
+ return /* @__PURE__ */ jsxs23(Fragment10, { children: [
1656
+ /* @__PURE__ */ jsx36("h1", { children: title ?? "Story Report" }),
1657
+ /* @__PURE__ */ jsx36(ReportSummary, {}),
1658
+ /* @__PURE__ */ jsx36(ReportMeta, {})
1617
1659
  ] });
1618
1660
  }
1619
1661
  function ReportErrorShell({
@@ -1622,28 +1664,28 @@ function ReportErrorShell({
1622
1664
  title,
1623
1665
  dataTheme
1624
1666
  }) {
1625
- return /* @__PURE__ */ jsx35("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx35(ReportSchemaError, { error }) });
1667
+ return /* @__PURE__ */ jsx36("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx36(ReportSchemaError, { error }) });
1626
1668
  }
1627
1669
 
1628
1670
  // src/components/Report.tsx
1629
- import { jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1671
+ import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1630
1672
  function Report(props) {
1631
1673
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1632
1674
  const result = unwrapReport(report);
1633
1675
  if (!result.ok) {
1634
- return /* @__PURE__ */ jsx36(ReportErrorShell, { error: result.error, className, title, dataTheme });
1676
+ return /* @__PURE__ */ jsx37(ReportErrorShell, { error: result.error, className, title, dataTheme });
1635
1677
  }
1636
1678
  const data = result.data;
1637
1679
  const hasContent = data.features.length > 0;
1638
- 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: [
1639
- /* @__PURE__ */ jsx36("header", { className: "es-report-header", children: /* @__PURE__ */ jsx36(ReportTitleBlock, { title }) }),
1640
- hasContent ? /* @__PURE__ */ jsx36(ReportFeatureList, {}) : /* @__PURE__ */ jsx36(ReportEmpty, {})
1680
+ 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: [
1681
+ /* @__PURE__ */ jsx37("header", { className: "es-report-header", children: /* @__PURE__ */ jsx37(ReportTitleBlock, { title }) }),
1682
+ hasContent ? /* @__PURE__ */ jsx37(ReportFeatureList, {}) : /* @__PURE__ */ jsx37(ReportEmpty, {})
1641
1683
  ] }) });
1642
1684
  }
1643
1685
 
1644
1686
  // src/components/doc/MermaidView.tsx
1645
1687
  import { useEffect, useRef, useState as useState2 } from "react";
1646
- import { Fragment as Fragment11, jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1688
+ import { Fragment as Fragment11, jsx as jsx38, jsxs as jsxs25 } from "react/jsx-runtime";
1647
1689
  function prefersDark(el) {
1648
1690
  const themed = el?.closest("[data-theme]");
1649
1691
  if (themed?.dataset.theme === "dark") return true;
@@ -1697,9 +1739,9 @@ function MermaidView({ entry, load }) {
1697
1739
  };
1698
1740
  }, [entry.code, load]);
1699
1741
  if (svg) {
1700
- return /* @__PURE__ */ jsxs24("figure", { ref, className: "my-3", children: [
1701
- entry.title ? /* @__PURE__ */ jsx37("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1702
- /* @__PURE__ */ jsx37(
1742
+ return /* @__PURE__ */ jsxs25("figure", { ref, className: "my-3", children: [
1743
+ entry.title ? /* @__PURE__ */ jsx38("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
1744
+ /* @__PURE__ */ jsx38(
1703
1745
  "div",
1704
1746
  {
1705
1747
  role: "img",
@@ -1710,8 +1752,8 @@ function MermaidView({ entry, load }) {
1710
1752
  )
1711
1753
  ] });
1712
1754
  }
1713
- return /* @__PURE__ */ jsxs24(Fragment11, { children: [
1714
- error ? /* @__PURE__ */ jsxs24(
1755
+ return /* @__PURE__ */ jsxs25(Fragment11, { children: [
1756
+ error ? /* @__PURE__ */ jsxs25(
1715
1757
  "p",
1716
1758
  {
1717
1759
  role: "alert",
@@ -1723,19 +1765,19 @@ function MermaidView({ entry, load }) {
1723
1765
  ]
1724
1766
  }
1725
1767
  ) : null,
1726
- /* @__PURE__ */ jsx37(MermaidSource, { entry, ref })
1768
+ /* @__PURE__ */ jsx38(MermaidSource, { entry, ref })
1727
1769
  ] });
1728
1770
  }
1729
1771
 
1730
1772
  // src/components/doc/MermaidDiagram.tsx
1731
- import { jsx as jsx38 } from "react/jsx-runtime";
1773
+ import { jsx as jsx39 } from "react/jsx-runtime";
1732
1774
  var mermaidLoader;
1733
1775
  function loadMermaid() {
1734
1776
  mermaidLoader ??= import("mermaid").then((m) => m.default);
1735
1777
  return mermaidLoader;
1736
1778
  }
1737
1779
  function MermaidDiagram({ entry, load = loadMermaid }) {
1738
- return /* @__PURE__ */ jsx38(MermaidView, { entry, load });
1780
+ return /* @__PURE__ */ jsx39(MermaidView, { entry, load });
1739
1781
  }
1740
1782
 
1741
1783
  // src/components/ScenarioExplorer.tsx
@@ -1745,12 +1787,12 @@ import { useState as useState3 } from "react";
1745
1787
  import "react";
1746
1788
  import { Command as CommandPrimitive } from "cmdk";
1747
1789
  import { SearchIcon } from "lucide-react";
1748
- import { jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
1790
+ import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
1749
1791
  function Command({
1750
1792
  className,
1751
1793
  ...props
1752
1794
  }) {
1753
- return /* @__PURE__ */ jsx39(
1795
+ return /* @__PURE__ */ jsx40(
1754
1796
  CommandPrimitive,
1755
1797
  {
1756
1798
  "data-slot": "command",
@@ -1766,14 +1808,14 @@ function CommandInput({
1766
1808
  className,
1767
1809
  ...props
1768
1810
  }) {
1769
- return /* @__PURE__ */ jsxs25(
1811
+ return /* @__PURE__ */ jsxs26(
1770
1812
  "div",
1771
1813
  {
1772
1814
  "data-slot": "command-input-wrapper",
1773
1815
  className: "flex h-9 items-center gap-2 border-b px-3",
1774
1816
  children: [
1775
- /* @__PURE__ */ jsx39(SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
1776
- /* @__PURE__ */ jsx39(
1817
+ /* @__PURE__ */ jsx40(SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
1818
+ /* @__PURE__ */ jsx40(
1777
1819
  CommandPrimitive.Input,
1778
1820
  {
1779
1821
  "data-slot": "command-input",
@@ -1792,7 +1834,7 @@ function CommandList({
1792
1834
  className,
1793
1835
  ...props
1794
1836
  }) {
1795
- return /* @__PURE__ */ jsx39(
1837
+ return /* @__PURE__ */ jsx40(
1796
1838
  CommandPrimitive.List,
1797
1839
  {
1798
1840
  "data-slot": "command-list",
@@ -1807,7 +1849,7 @@ function CommandList({
1807
1849
  function CommandEmpty({
1808
1850
  ...props
1809
1851
  }) {
1810
- return /* @__PURE__ */ jsx39(
1852
+ return /* @__PURE__ */ jsx40(
1811
1853
  CommandPrimitive.Empty,
1812
1854
  {
1813
1855
  "data-slot": "command-empty",
@@ -1820,7 +1862,7 @@ function CommandGroup({
1820
1862
  className,
1821
1863
  ...props
1822
1864
  }) {
1823
- return /* @__PURE__ */ jsx39(
1865
+ return /* @__PURE__ */ jsx40(
1824
1866
  CommandPrimitive.Group,
1825
1867
  {
1826
1868
  "data-slot": "command-group",
@@ -1836,7 +1878,7 @@ function CommandItem({
1836
1878
  className,
1837
1879
  ...props
1838
1880
  }) {
1839
- return /* @__PURE__ */ jsx39(
1881
+ return /* @__PURE__ */ jsx40(
1840
1882
  CommandPrimitive.Item,
1841
1883
  {
1842
1884
  "data-slot": "command-item",
@@ -1850,7 +1892,7 @@ function CommandItem({
1850
1892
  }
1851
1893
 
1852
1894
  // src/components/ScenarioExplorer.tsx
1853
- import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
1895
+ import { jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
1854
1896
  var STATUS_FILTERS = [
1855
1897
  { value: "all", label: "All" },
1856
1898
  { value: "passed", label: "Passed" },
@@ -1861,22 +1903,22 @@ var STATUS_FILTERS = [
1861
1903
  function ScenarioExplorer({ scenarios, summary }) {
1862
1904
  const [status, setStatus] = useState3("all");
1863
1905
  const visible = status === "all" ? scenarios : scenarios.filter((s) => s.status === status);
1864
- return /* @__PURE__ */ jsxs26("div", { className: "font-sans text-foreground", children: [
1865
- summary ? /* @__PURE__ */ jsxs26("p", { className: "mb-3 text-sm text-muted-foreground", children: [
1906
+ return /* @__PURE__ */ jsxs27("div", { className: "font-sans text-foreground", children: [
1907
+ summary ? /* @__PURE__ */ jsxs27("p", { className: "mb-3 text-sm text-muted-foreground", children: [
1866
1908
  summary.total,
1867
1909
  " scenarios \xB7 ",
1868
- /* @__PURE__ */ jsxs26("span", { className: "text-pass", children: [
1910
+ /* @__PURE__ */ jsxs27("span", { className: "text-pass", children: [
1869
1911
  summary.passed,
1870
1912
  " passed"
1871
1913
  ] }),
1872
1914
  " \xB7",
1873
1915
  " ",
1874
- /* @__PURE__ */ jsxs26("span", { className: "text-fail", children: [
1916
+ /* @__PURE__ */ jsxs27("span", { className: "text-fail", children: [
1875
1917
  summary.failed,
1876
1918
  " failed"
1877
1919
  ] })
1878
1920
  ] }) : null,
1879
- /* @__PURE__ */ jsx40("div", { className: "mb-2 flex flex-wrap gap-1.5", role: "group", "aria-label": "Filter by status", children: STATUS_FILTERS.map((f) => /* @__PURE__ */ jsx40(
1921
+ /* @__PURE__ */ jsx41("div", { className: "mb-2 flex flex-wrap gap-1.5", role: "group", "aria-label": "Filter by status", children: STATUS_FILTERS.map((f) => /* @__PURE__ */ jsx41(
1880
1922
  "button",
1881
1923
  {
1882
1924
  type: "button",
@@ -1891,26 +1933,26 @@ function ScenarioExplorer({ scenarios, summary }) {
1891
1933
  },
1892
1934
  f.value
1893
1935
  )) }),
1894
- /* @__PURE__ */ jsxs26(
1936
+ /* @__PURE__ */ jsxs27(
1895
1937
  Command,
1896
1938
  {
1897
1939
  className: "rounded-lg border border-border bg-card",
1898
1940
  filter: (value, search) => value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0,
1899
1941
  children: [
1900
- /* @__PURE__ */ jsx40(CommandInput, { placeholder: "Search scenarios\u2026" }),
1901
- /* @__PURE__ */ jsxs26(CommandList, { className: "max-h-none", children: [
1902
- /* @__PURE__ */ jsx40(CommandEmpty, { children: "No scenarios match your filters." }),
1903
- /* @__PURE__ */ jsx40(CommandGroup, { children: visible.map((s) => /* @__PURE__ */ jsx40(
1942
+ /* @__PURE__ */ jsx41(CommandInput, { placeholder: "Search scenarios\u2026" }),
1943
+ /* @__PURE__ */ jsxs27(CommandList, { className: "max-h-none", children: [
1944
+ /* @__PURE__ */ jsx41(CommandEmpty, { children: "No scenarios match your filters." }),
1945
+ /* @__PURE__ */ jsx41(CommandGroup, { children: visible.map((s) => /* @__PURE__ */ jsx41(
1904
1946
  CommandItem,
1905
1947
  {
1906
1948
  value: `${s.title} ${s.feature} ${s.tags.join(" ")}`,
1907
1949
  asChild: true,
1908
1950
  className: "cursor-pointer",
1909
- children: /* @__PURE__ */ jsxs26("a", { href: s.href, className: "flex items-center gap-3", children: [
1910
- /* @__PURE__ */ jsx40(Badge, { variant: s.status, children: s.status }),
1911
- /* @__PURE__ */ jsx40("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
1912
- s.tags.slice(0, 3).map((t) => /* @__PURE__ */ jsx40(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
1913
- /* @__PURE__ */ jsx40("code", { className: "hidden font-mono text-xs text-muted-foreground md:inline", children: s.feature })
1951
+ children: /* @__PURE__ */ jsxs27("a", { href: s.href, className: "flex items-center gap-3", children: [
1952
+ /* @__PURE__ */ jsx41(Badge, { variant: s.status, children: s.status }),
1953
+ /* @__PURE__ */ jsx41("span", { className: "min-w-0 flex-1 truncate font-medium", children: s.title }),
1954
+ s.tags.slice(0, 3).map((t) => /* @__PURE__ */ jsx41(Badge, { variant: "tag", className: "hidden sm:inline-flex", children: t }, t)),
1955
+ /* @__PURE__ */ jsx41("code", { className: "hidden font-mono text-xs text-muted-foreground md:inline", children: s.feature })
1914
1956
  ] })
1915
1957
  },
1916
1958
  s.id
@@ -2057,7 +2099,7 @@ var story_report_v1_default = {
2057
2099
  title: {
2058
2100
  type: "string",
2059
2101
  minLength: 1,
2060
- description: "Display title. Derived from describe block when present, otherwise the file basename."
2102
+ description: "Display title. Comes from story.feature(...) when declared, otherwise the describe block or file basename."
2061
2103
  },
2062
2104
  sourceFile: {
2063
2105
  type: "string",
@@ -2073,6 +2115,26 @@ var story_report_v1_default = {
2073
2115
  $ref: "#/$defs/Scenario"
2074
2116
  },
2075
2117
  description: "Scenarios in this feature, in declaration order."
2118
+ },
2119
+ kind: {
2120
+ type: "string",
2121
+ enum: [
2122
+ "feature",
2123
+ "ability",
2124
+ "business-need"
2125
+ ],
2126
+ description: "How the feature was declared. Absent when the title was derived rather than declared."
2127
+ },
2128
+ narrative: {
2129
+ type: "string",
2130
+ description: "Markdown explaining why the feature exists and who it serves."
2131
+ },
2132
+ glossary: {
2133
+ type: "array",
2134
+ items: {
2135
+ $ref: "#/$defs/GlossaryTerm"
2136
+ },
2137
+ description: "Terms this feature defines, linked wherever they appear in scenario text."
2076
2138
  }
2077
2139
  },
2078
2140
  required: [
@@ -2884,6 +2946,25 @@ var story_report_v1_default = {
2884
2946
  }
2885
2947
  },
2886
2948
  additionalProperties: false
2949
+ },
2950
+ GlossaryTerm: {
2951
+ type: "object",
2952
+ description: "One glossary entry: a term the scenarios use and what it means.",
2953
+ properties: {
2954
+ term: {
2955
+ type: "string",
2956
+ minLength: 1
2957
+ },
2958
+ definition: {
2959
+ type: "string",
2960
+ minLength: 1
2961
+ }
2962
+ },
2963
+ required: [
2964
+ "term",
2965
+ "definition"
2966
+ ],
2967
+ additionalProperties: false
2887
2968
  }
2888
2969
  }
2889
2970
  };