executable-stories-react 0.14.0 → 0.16.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.d.cts CHANGED
@@ -87,6 +87,10 @@ interface RenderReportToHtmlOptions {
87
87
  * static (non-island) output.
88
88
  */
89
89
  scenarioHistory?: ScenarioHistoryMap;
90
+ /** Show the Share button in the interactive header (default false). `--html-share` turns it on. */
91
+ share?: boolean;
92
+ /** Command the share dialog hands over, e.g. `npx executable-stories share reports/`. */
93
+ shareCommand?: string;
90
94
  }
91
95
  /**
92
96
  * Render a StoryReport (or a parse Result) to a complete, self-contained HTML
package/dist/ssr.d.ts CHANGED
@@ -87,6 +87,10 @@ interface RenderReportToHtmlOptions {
87
87
  * static (non-island) output.
88
88
  */
89
89
  scenarioHistory?: ScenarioHistoryMap;
90
+ /** Show the Share button in the interactive header (default false). `--html-share` turns it on. */
91
+ share?: boolean;
92
+ /** Command the share dialog hands over, e.g. `npx executable-stories share reports/`. */
93
+ shareCommand?: string;
90
94
  }
91
95
  /**
92
96
  * Render a StoryReport (or a parse Result) to a complete, self-contained HTML
package/dist/ssr.js CHANGED
@@ -523,12 +523,29 @@ function DocVideo({ entry }) {
523
523
  }
524
524
 
525
525
  // src/components/doc/DocHtml.tsx
526
+ import { useEffect, useRef, useState } from "react";
526
527
  import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
528
+ var MEASURE_SCRIPT = "<script>(function(){if(parent===window)return;var last=0,queued=false;var measure=function(){var kids=document.body.children,h=0;for(var i=0;i<kids.length;i++)h=Math.max(h,kids[i].getBoundingClientRect().bottom);return Math.ceil(h||document.body.getBoundingClientRect().bottom)};var post=function(){queued=false;var h=measure();if(h===last)return;last=h;parent.postMessage({__esHtmlHeight:h},'*')};var schedule=function(){if(queued)return;queued=true;requestAnimationFrame(post)};var start=function(){new ResizeObserver(schedule).observe(document.body);new MutationObserver(schedule).observe(document.body,{subtree:true,childList:true,attributes:true,characterData:true});addEventListener('load',schedule);post()};if(document.body)start();else addEventListener('DOMContentLoaded',start)})();</script>";
529
+ var MAX_MEASURED_HEIGHT = 5e3;
527
530
  function DocHtml({ entry }) {
528
- const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
531
+ const declaredHeight = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
529
532
  const title = entry.title ?? "Embedded HTML";
530
533
  const safeHref = safeUrl(entry.url ?? entry.path);
531
- const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
534
+ const frameRef = useRef(null);
535
+ const [measuredHeight, setMeasuredHeight] = useState();
536
+ useEffect(() => {
537
+ if (entry.content === void 0) return;
538
+ const onMessage = (event) => {
539
+ if (event.source !== frameRef.current?.contentWindow) return;
540
+ const reported = event.data?.__esHtmlHeight;
541
+ if (typeof reported !== "number" || !Number.isFinite(reported) || reported <= 0) return;
542
+ setMeasuredHeight(Math.min(reported, MAX_MEASURED_HEIGHT));
543
+ };
544
+ window.addEventListener("message", onMessage);
545
+ return () => window.removeEventListener("message", onMessage);
546
+ }, [entry.content]);
547
+ const height = measuredHeight === void 0 ? declaredHeight : `${measuredHeight}px`;
548
+ const frameProps = entry.content !== void 0 ? { srcDoc: entry.content + MEASURE_SCRIPT } : { src: safeHref ?? "" };
532
549
  return /* @__PURE__ */ jsxs9("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
533
550
  /* @__PURE__ */ jsxs9("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
534
551
  /* @__PURE__ */ jsx14("span", { children: entry.title ?? "HTML" }),
@@ -548,6 +565,7 @@ function DocHtml({ entry }) {
548
565
  /* @__PURE__ */ jsx14(
549
566
  "iframe",
550
567
  {
568
+ ref: frameRef,
551
569
  ...frameProps,
552
570
  sandbox: "allow-scripts",
553
571
  loading: "lazy",
@@ -1096,7 +1114,7 @@ function ScenarioRunHistory({ scenarioId }) {
1096
1114
  }
1097
1115
 
1098
1116
  // src/components/ScenarioStaleness.tsx
1099
- import { useEffect, useState } from "react";
1117
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1100
1118
 
1101
1119
  // src/lib/provenance.ts
1102
1120
  function formatRelativeAge(thenMs, nowMs) {
@@ -1155,8 +1173,8 @@ import { jsxs as jsxs16 } from "react/jsx-runtime";
1155
1173
  function ScenarioStaleness({ scenario }) {
1156
1174
  const freshness = useScenarioFreshness();
1157
1175
  const fixedNow = freshness?.nowMs;
1158
- const [now, setNow] = useState(fixedNow);
1159
- useEffect(() => {
1176
+ const [now, setNow] = useState2(fixedNow);
1177
+ useEffect2(() => {
1160
1178
  if (fixedNow === void 0) setNow(Date.now());
1161
1179
  }, [fixedNow]);
1162
1180
  if (!freshness || now === void 0) return null;
@@ -1170,7 +1188,7 @@ function ScenarioStaleness({ scenario }) {
1170
1188
  }
1171
1189
 
1172
1190
  // src/interactive/collapse-context.ts
1173
- import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState2 } from "react";
1191
+ import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState3 } from "react";
1174
1192
  var CollapseContext = createContext4(null);
1175
1193
  var CollapseProvider = CollapseContext.Provider;
1176
1194
  function useCollapse() {
@@ -1744,23 +1762,85 @@ function ReportErrorShell({
1744
1762
  return /* @__PURE__ */ jsx36("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx36(ReportSchemaError, { error }) });
1745
1763
  }
1746
1764
 
1747
- // src/components/Report.tsx
1765
+ // src/components/ReportSpanGraph.tsx
1766
+ import { deriveSpanGraph, spanGraphNodeMark, spanGraphToMermaid } from "executable-stories-core/span-graph";
1767
+ import { useMemo as useMemo2 } from "react";
1748
1768
  import { jsx as jsx37, jsxs as jsxs25 } from "react/jsx-runtime";
1769
+ var LANE_LABEL = {
1770
+ edge: "Edge",
1771
+ service: "Service",
1772
+ queue: "Queue",
1773
+ datastore: "Data"
1774
+ };
1775
+ var MARK_LABEL = {
1776
+ failing: "Failing",
1777
+ added: "New",
1778
+ changed: "Changed"
1779
+ };
1780
+ var MARK_CLASS = {
1781
+ failing: "text-fail",
1782
+ added: "text-pass",
1783
+ changed: "text-pend"
1784
+ };
1785
+ function MarkBadge({ node }) {
1786
+ const mark = spanGraphNodeMark(node);
1787
+ if (!mark) return null;
1788
+ return /* @__PURE__ */ jsx37("span", { className: cn("ml-2 text-xs font-medium", MARK_CLASS[mark]), children: MARK_LABEL[mark] });
1789
+ }
1790
+ function ReportSpanGraph() {
1791
+ const report = useReport();
1792
+ const { code, graph, titleById } = useMemo2(() => {
1793
+ const scenarios = report.features.flatMap((feature) => feature.scenarios);
1794
+ const derived = deriveSpanGraph(scenarios);
1795
+ return {
1796
+ graph: derived,
1797
+ code: spanGraphToMermaid(derived),
1798
+ titleById: new Map(scenarios.map((scenario) => [scenario.id, scenario.title]))
1799
+ };
1800
+ }, [report]);
1801
+ if (code === "") return null;
1802
+ return /* @__PURE__ */ jsxs25("section", { className: "es-span-graph mt-6", "aria-labelledby": "es-span-graph-heading", children: [
1803
+ /* @__PURE__ */ jsx37("h2", { id: "es-span-graph-heading", className: "text-base font-semibold text-foreground", children: "Architecture, as it ran" }),
1804
+ /* @__PURE__ */ jsx37("p", { className: "mt-1 text-sm text-muted-foreground", children: "Derived from this run\u2019s traces. Every component was named by a span while a scenario ran, so nothing here is inferred from the code. Instrumented, exercised paths only: a component no scenario reaches does not appear." }),
1805
+ /* @__PURE__ */ jsx37(DocMermaid, { entry: { kind: "mermaid", phase: "static", code } }),
1806
+ /* @__PURE__ */ jsxs25("table", { className: "mt-2 w-full text-left text-sm", children: [
1807
+ /* @__PURE__ */ jsx37("caption", { className: "sr-only", children: "Each component and the scenarios that exercised it" }),
1808
+ /* @__PURE__ */ jsx37("thead", { children: /* @__PURE__ */ jsxs25("tr", { className: "text-xs text-muted-foreground", children: [
1809
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "py-1 pr-4 font-medium", children: "Component" }),
1810
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "py-1 pr-4 font-medium", children: "Lane" }),
1811
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "py-1 font-medium", children: "Covered by" })
1812
+ ] }) }),
1813
+ /* @__PURE__ */ jsx37("tbody", { children: graph.nodes.map((node) => /* @__PURE__ */ jsxs25("tr", { className: "border-t border-border align-top", children: [
1814
+ /* @__PURE__ */ jsxs25("th", { scope: "row", className: "py-1.5 pr-4 font-mono text-xs font-normal", children: [
1815
+ node.id,
1816
+ /* @__PURE__ */ jsx37(MarkBadge, { node })
1817
+ ] }),
1818
+ /* @__PURE__ */ jsx37("td", { className: "py-1.5 pr-4 text-xs text-muted-foreground", children: LANE_LABEL[node.kind] }),
1819
+ /* @__PURE__ */ jsx37("td", { className: "py-1.5", children: /* @__PURE__ */ jsx37("ul", { className: "flex flex-col gap-0.5", children: node.scenarioIds.map((id) => /* @__PURE__ */ jsx37("li", { children: /* @__PURE__ */ jsx37("a", { href: `#${id}`, className: "text-xs underline underline-offset-2", children: titleById.get(id) ?? id }) }, id)) }) })
1820
+ ] }, node.id)) })
1821
+ ] })
1822
+ ] });
1823
+ }
1824
+
1825
+ // src/components/Report.tsx
1826
+ import { jsx as jsx38, jsxs as jsxs26 } from "react/jsx-runtime";
1749
1827
  function Report(props) {
1750
1828
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1751
1829
  const result = unwrapReport(report);
1752
1830
  if (!result.ok) {
1753
- return /* @__PURE__ */ jsx37(ReportErrorShell, { error: result.error, className, title, dataTheme });
1831
+ return /* @__PURE__ */ jsx38(ReportErrorShell, { error: result.error, className, title, dataTheme });
1754
1832
  }
1755
1833
  const data = result.data;
1756
1834
  const hasContent = data.features.length > 0;
1757
- return /* @__PURE__ */ jsx37(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs25("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1758
- /* @__PURE__ */ jsx37("header", { className: "es-report-header", children: /* @__PURE__ */ jsx37(ReportTitleBlock, { title }) }),
1759
- hasContent ? /* @__PURE__ */ jsx37(ReportFeatureList, {}) : /* @__PURE__ */ jsx37(ReportEmpty, {})
1835
+ return /* @__PURE__ */ jsx38(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs26("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1836
+ /* @__PURE__ */ jsx38("header", { className: "es-report-header", children: /* @__PURE__ */ jsx38(ReportTitleBlock, { title }) }),
1837
+ /* @__PURE__ */ jsx38(ReportSpanGraph, {}),
1838
+ hasContent ? /* @__PURE__ */ jsx38(ReportFeatureList, {}) : /* @__PURE__ */ jsx38(ReportEmpty, {})
1760
1839
  ] }) });
1761
1840
  }
1762
1841
 
1763
1842
  // src/ssr-entry.ts
1843
+ import { REPORT_FAVICON_LINK } from "executable-stories-core/utils/report-favicon";
1764
1844
  var ROOT_ID = "es-report-root";
1765
1845
  var DATA_ID = "es-report-data";
1766
1846
  var HISTORY_ID = "es-report-history";
@@ -1814,7 +1894,9 @@ function renderReportToHtml(report, options = {}) {
1814
1894
  mermaid = true,
1815
1895
  islandScript = "",
1816
1896
  staleAfterDays = 7,
1817
- scenarioHistory
1897
+ scenarioHistory,
1898
+ share = false,
1899
+ shareCommand = ""
1818
1900
  } = options;
1819
1901
  const markup = renderToStaticMarkup(
1820
1902
  createElement(Report, { report, title })
@@ -1827,7 +1909,8 @@ function renderReportToHtml(report, options = {}) {
1827
1909
  const historyScript = interactive && scenarioHistory && Object.keys(scenarioHistory).length > 0 ? `<script type="application/json" id="${HISTORY_ID}">${escapeJsonForScript(JSON.stringify(scenarioHistory))}</script>` : "";
1828
1910
  const islandTag = interactive ? `<script>${islandScript}</script>` : "";
1829
1911
  const cdnBody = interactive ? "" : cdn.body;
1830
- const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}" data-es-stale-days="${Number.isFinite(staleAfterDays) && staleAfterDays >= 0 ? staleAfterDays : 7}"` : "";
1912
+ const shareAttrs = share ? ` data-es-share="true"${shareCommand ? ` data-es-share-cmd="${escapeHtml(shareCommand)}"` : ""}` : "";
1913
+ const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}" data-es-stale-days="${Number.isFinite(staleAfterDays) && staleAfterDays >= 0 ? staleAfterDays : 7}"${shareAttrs}` : "";
1831
1914
  const rootAttrs = interactive ? ` id="${ROOT_ID}" data-title="${escapeHtml(title)}"${islandConfigAttrs}` : "";
1832
1915
  return `<!DOCTYPE html>
1833
1916
  <html lang="${escapeHtml(lang)}" data-theme="${theme === "dark" ? "dark" : "light"}">
@@ -1835,6 +1918,7 @@ function renderReportToHtml(report, options = {}) {
1835
1918
  <meta charset="UTF-8">
1836
1919
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1837
1920
  <meta name="color-scheme" content="light dark">
1921
+ ${REPORT_FAVICON_LINK}
1838
1922
  <title>${escapeHtml(title)}</title>
1839
1923
  ${cdn.head}
1840
1924
  ${headExtra}