@suzumiyaaoba/mdxr 0.3.0 → 0.4.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.
Files changed (78) hide show
  1. package/dist/cli.mjs +719 -214
  2. package/dist/components.d.mts +6 -6
  3. package/dist/components.mjs +1 -1
  4. package/dist/{config-DhBFpSdB.mjs → config-DQeZaek7.mjs} +1 -1
  5. package/dist/index.mjs +2 -2
  6. package/dist/{ui-DOxuV3n8.mjs → ui-D89i0Cq-.mjs} +394 -299
  7. package/package.json +1 -1
  8. package/src/ascii/ast.ts +129 -92
  9. package/src/ascii/charts.ts +2 -3
  10. package/src/ascii/document.ts +2 -4
  11. package/src/ascii/extended.ts +7 -4
  12. package/src/ascii/glyphs.ts +3 -1
  13. package/src/ascii/investigation.ts +38 -36
  14. package/src/ascii/output.ts +12 -4
  15. package/src/ascii/parts.ts +12 -1
  16. package/src/ascii/planning.ts +151 -87
  17. package/src/ascii/reports.ts +29 -23
  18. package/src/ascii/shadcn.ts +26 -32
  19. package/src/catalog.ts +60 -44
  20. package/src/client/doc-events.ts +12 -13
  21. package/src/components/ui/calendar.tsx +1 -0
  22. package/src/components/ui/carousel.tsx +39 -17
  23. package/src/components/ui/chart.tsx +2 -1
  24. package/src/components/ui/pagination.tsx +0 -1
  25. package/src/components/ui/slider.tsx +3 -6
  26. package/src/components/ui/tabs.tsx +1 -0
  27. package/src/components/ui/toc.tsx +560 -0
  28. package/src/components/ui/toggle-group.tsx +6 -3
  29. package/src/extended/data.ts +6 -4
  30. package/src/extended/diagrams.ts +3 -2
  31. package/src/extended/differences.ts +13 -9
  32. package/src/extended/plots.ts +36 -16
  33. package/src/extended/profiles.ts +3 -1
  34. package/src/extended/reports.ts +10 -5
  35. package/src/html.ts +20 -0
  36. package/src/hydrate/import-scan.ts +9 -6
  37. package/src/hydrate/runtime-module.ts +1 -0
  38. package/src/hydrate.ts +5 -0
  39. package/src/lines.ts +5 -1
  40. package/src/mdx.ts +4 -0
  41. package/src/page-toc-js.ts +46 -0
  42. package/src/page-toc-runtime.ts +22 -0
  43. package/src/remark/headings.ts +23 -1
  44. package/src/render.ts +51 -0
  45. package/src/ui/attrs.ts +5 -2
  46. package/src/ui/bar-chart.tsx +11 -18
  47. package/src/ui/bridge.tsx +15 -15
  48. package/src/ui/chart.ts +113 -34
  49. package/src/ui/children.ts +21 -1
  50. package/src/ui/comments.tsx +9 -8
  51. package/src/ui/data-context.ts +2 -1
  52. package/src/ui/data-table.tsx +3 -2
  53. package/src/ui/data-view.tsx +2 -1
  54. package/src/ui/diff-parse.ts +19 -1
  55. package/src/ui/diffstat.tsx +6 -5
  56. package/src/ui/document-inputs.tsx +13 -10
  57. package/src/ui/document-media.tsx +3 -2
  58. package/src/ui/document-tabs.tsx +4 -3
  59. package/src/ui/file-icon.ts +1 -1
  60. package/src/ui/funnel.tsx +12 -12
  61. package/src/ui/gantt.tsx +24 -9
  62. package/src/ui/graph.tsx +1 -1
  63. package/src/ui/line-chart.tsx +14 -17
  64. package/src/ui/matrix.tsx +16 -7
  65. package/src/ui/page-toc.tsx +54 -0
  66. package/src/ui/pie-chart.tsx +12 -12
  67. package/src/ui/props.tsx +15 -13
  68. package/src/ui/quadrant.tsx +8 -16
  69. package/src/ui/radar.tsx +9 -12
  70. package/src/ui/release.tsx +11 -5
  71. package/src/ui/sankey.tsx +4 -4
  72. package/src/ui/scatter.tsx +14 -17
  73. package/src/ui/series.tsx +2 -3
  74. package/src/ui/spark.tsx +2 -13
  75. package/src/ui/summary.tsx +1 -1
  76. package/src/ui/table-controls.tsx +9 -8
  77. package/src/ui/tests.tsx +4 -4
  78. package/src/ui/treemap.tsx +17 -17
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { Ap as splitPathLines, Cp as takeUsedIcons, Dp as fenceFilename, Fp as isRecord, Gd as reportModel, Ip as nonEmpty, Jf as isStatus, Kd as comparisonModel, Kf as STATUSES, Lp as own, Np as langForPath, Op as fenceLang, Pp as isComponent, Rp as safeHref, V as shadcnModules, Vd as calculate, Wd as REPORT_SPECS, _l as PLOT_NAMES, af as display, ap as textList, cf as parseJson, dc as queryPlanModel, df as tableRecords, fc as diagramModel, gp as numOf, hp as attrTrue, if as columnsOf, jl as PlanHeader, jp as SKIP_LANGS, kp as parseLineRange, lc as bundleModel, lf as readRecords, of as isDataRecord, pc as flameModel, qd as schemaRows, sf as numberValue, t as builtinComponents, uc as dependencyMatrixModel, uf as records, vl as plotModel, wp as DocContext, xf as parseDiff, zp as urlScheme } from "./ui-DOxuV3n8.mjs";
3
- import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-DhBFpSdB.mjs";
2
+ import { Ap as parseLineRange, Bp as urlScheme, Fp as isComponent, Gd as reportModel, Ip as isRecord, Jf as isStatus, Kd as comparisonModel, Kf as STATUSES, Lp as nonEmpty, Mp as SKIP_LANGS, Op as fenceFilename, Pp as langForPath, Rp as own, Tp as DocContext, V as shadcnModules, Vd as calculate, Wd as REPORT_SPECS, _l as PLOT_NAMES, _p as numOf, af as display, ap as numList, cf as parseJson, dc as queryPlanModel, df as tableRecords, fc as diagramModel, gp as attrTrue, if as columnsOf, jl as PlanHeader, jp as splitPathLines, kp as fenceLang, lc as bundleModel, lf as readRecords, of as isDataRecord, op as textList, pc as flameModel, qd as schemaRows, sf as numberValue, t as builtinComponents, uc as dependencyMatrixModel, uf as records, vl as plotModel, wp as takeUsedIcons, xf as parseDiff, zp as safeHref } from "./ui-D89i0Cq-.mjs";
3
+ import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-DQeZaek7.mjs";
4
4
  import module from "node:module";
5
5
  import fsp, { access, readFile, readdir, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
@@ -23,15 +23,18 @@ import { visit } from "unist-util-visit";
23
23
  import fs, { existsSync, readFileSync, realpathSync } from "node:fs";
24
24
  import { visitParents } from "unist-util-visit-parents";
25
25
  import * as valibot from "valibot";
26
- import { Fragment, createElement } from "react";
26
+ import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
27
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
27
28
  import { icons } from "@iconify-json/lucide";
28
29
  import { icons as icons$1 } from "@iconify-json/vscode-icons";
30
+ import { differenceInCalendarDays, isValid, parseISO } from "date-fns";
31
+ import { cn } from "cn";
29
32
  import { build } from "esbuild";
30
33
  import os from "node:os";
31
34
  import { spawn } from "node:child_process";
32
35
  import { once } from "node:events";
33
- import { compile } from "@mdx-js/mdx";
34
36
  import { renderToStaticMarkup, renderToString } from "react-dom/server";
37
+ import { compile } from "@mdx-js/mdx";
35
38
  import rehypeKatex from "rehype-katex";
36
39
  import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerRemoveNotationEscape } from "@shikijs/transformers";
37
40
  import { createHighlighter } from "shiki";
@@ -1177,8 +1180,11 @@ const toList = (items) => ({
1177
1180
  * 2. `<Toc>` elements (`:::toc` included) get a nested link list injected as
1178
1181
  * children — `depth`/`min` attributes bound the heading levels included
1179
1182
  * (defaults: h2–h3, i.e. min=2 depth=3).
1183
+ *
1184
+ * The flat heading list is also left on `file.data.mdxrHeadings` for the
1185
+ * page-level sidebar ToC.
1180
1186
  */
1181
- const remarkMdxrHeadings = () => (tree) => {
1187
+ const remarkMdxrHeadings = () => (tree, file) => {
1182
1188
  const counts = /* @__PURE__ */ new Map();
1183
1189
  const used = /* @__PURE__ */ new Set();
1184
1190
  const headings = [];
@@ -1209,6 +1215,11 @@ const remarkMdxrHeadings = () => (tree) => {
1209
1215
  const items = headings.filter((h) => h.depth >= minDepth && h.depth <= maxDepth && h.text !== "");
1210
1216
  node.children = items.length === 0 ? [] : [toList(nest(items))];
1211
1217
  });
1218
+ file.data.mdxrHeadings = headings.filter((h) => h.text !== "").map(({ depth, slug, text }) => ({
1219
+ depth,
1220
+ slug,
1221
+ text
1222
+ }));
1212
1223
  };
1213
1224
  //#endregion
1214
1225
  //#region src/remark/no-js.ts
@@ -1483,6 +1494,8 @@ const flag = (node, name) => {
1483
1494
  for (const a of node.attributes) if (isRecord(a) && a.name === name) return a.value === null || a.value === true || attrTrue(a.value);
1484
1495
  return false;
1485
1496
  };
1497
+ /** Attribute explicitly set to the string `"false"` (open="false" opt-outs). */
1498
+ const flagOff = (node, name) => attr(node, name) === "false";
1486
1499
  /** Numeric part of an attribute — `"120ms"` → 120. */
1487
1500
  const num = (node, name) => numOf(attr(node, name));
1488
1501
  /** Comma-separated attribute list (`labels="a,b"`). */
@@ -1501,36 +1514,29 @@ const hoistable = (c) => c.type === "paragraph" && isParent(c) && c.children.eve
1501
1514
  * elements hoisted out of element-only paragraphs (see `hoistable`).
1502
1515
  */
1503
1516
  const els = (node, name) => {
1504
- const out = [];
1505
- for (const c of node.children ?? []) {
1506
- if (isMdxEl(c)) {
1507
- if (name === void 0 || c.name === name) out.push(c);
1508
- continue;
1509
- }
1510
- if (hoistable(c)) {
1511
- for (const cc of c.children) if (isMdxEl(cc) && (name === void 0 || cc.name === name)) out.push(cc);
1512
- }
1513
- }
1514
- return out;
1517
+ return (node.children ?? []).flatMap((c) => hoistable(c) ? c.children : [c]).filter((c) => isMdxEl(c) && (name === void 0 || c.name === name));
1515
1518
  };
1516
1519
  /**
1517
1520
  * `node` minus the named JSX children — direct elements removed, matching
1518
1521
  * elements stripped out of paragraphs (now-empty paragraphs dropped).
1519
1522
  */
1520
- const withoutEls = (node, names) => ({
1521
- ...node,
1522
- children: (node.children ?? []).flatMap((c) => {
1523
- if (isMdxEl(c)) return names.includes(c.name ?? "") ? [] : [c];
1524
- if (c.type === "paragraph" && isParent(c)) {
1525
- const kids = c.children.filter((cc) => !(isMdxEl(cc) && names.includes(cc.name ?? "")));
1526
- return kids.every((cc) => cc.type === "text" && "value" in cc && typeof cc.value === "string" && cc.value.trim() === "") ? [] : [{
1527
- ...c,
1528
- children: kids
1529
- }];
1530
- }
1531
- return [c];
1532
- })
1533
- });
1523
+ const withoutEls = (node, names) => {
1524
+ const removed = new Set(names);
1525
+ return {
1526
+ ...node,
1527
+ children: (node.children ?? []).flatMap((c) => {
1528
+ if (isMdxEl(c)) return removed.has(c.name ?? "") ? [] : [c];
1529
+ if (hoistable(c)) {
1530
+ const kids = c.children.filter((cc) => !(isMdxEl(cc) && removed.has(cc.name ?? "")));
1531
+ return kids.every((cc) => cc.type === "text" && "value" in cc && typeof cc.value === "string" && cc.value.trim() === "") ? [] : [{
1532
+ ...c,
1533
+ children: kids
1534
+ }];
1535
+ }
1536
+ return [c];
1537
+ })
1538
+ };
1539
+ };
1534
1540
  /** All literal text under `node` (text + inlineCode values, tree order). */
1535
1541
  const textOf = (node) => Array.isArray(node) ? node.map(textContent$1).join("") : textContent$1(node);
1536
1542
  const txt = (value) => ({
@@ -1663,6 +1669,53 @@ const phrasing = (nodes) => nodes.flatMap((n) => {
1663
1669
  const t = deepText([n]).trim();
1664
1670
  return t === "" ? [] : [txt(t)];
1665
1671
  });
1672
+ const boldHtml = (node, ctx) => [strong(ctx.inline(node))];
1673
+ const emphasisHtml = (node, ctx) => [em(ctx.inline(node))];
1674
+ const deleteHtml = (node, ctx) => [del(ctx.inline(node))];
1675
+ const codeHtml = (node) => [icode(deepText(node.children ?? []))];
1676
+ const HTML_INLINE = {
1677
+ a: (node, ctx) => {
1678
+ const href = attr(node, "href");
1679
+ const inner = ctx.inline(node);
1680
+ return href === void 0 ? inner : [link(href, inner)];
1681
+ },
1682
+ b: boldHtml,
1683
+ br: () => [brk()],
1684
+ code: codeHtml,
1685
+ del: deleteHtml,
1686
+ em: emphasisHtml,
1687
+ i: emphasisHtml,
1688
+ img: (node) => {
1689
+ return [{
1690
+ alt: attr(node, "alt"),
1691
+ type: "image",
1692
+ url: attr(node, "src") ?? ""
1693
+ }];
1694
+ },
1695
+ kbd: codeHtml,
1696
+ s: deleteHtml,
1697
+ samp: codeHtml,
1698
+ strike: deleteHtml,
1699
+ strong: boldHtml
1700
+ };
1701
+ /** Wrap adjacent inline nodes while retaining nested block structure. */
1702
+ const blocksOf = (nodes) => {
1703
+ const blocks = [];
1704
+ let inline = [];
1705
+ for (const node of nodes) {
1706
+ if (PHRASING.has(node.type)) {
1707
+ inline.push(...phrasing([node]));
1708
+ continue;
1709
+ }
1710
+ if (inline.length > 0) {
1711
+ blocks.push(para(inline));
1712
+ inline = [];
1713
+ }
1714
+ blocks.push(node);
1715
+ }
1716
+ if (inline.length > 0) blocks.push(para(inline));
1717
+ return blocks;
1718
+ };
1666
1719
  /**
1667
1720
  * Raw HTML elements written into a document are mdxJsx elements too —
1668
1721
  * give the meaningful ones a markdown form (`<a>` → link, `<code>` →
@@ -1672,49 +1725,52 @@ const htmlEl = (node, ctx, inline) => {
1672
1725
  const name = node.name ?? "";
1673
1726
  const kids = () => phrasing(ctx.children(node));
1674
1727
  const blocks = () => ctx.children(node);
1675
- if (name === "a") {
1676
- const href = attr(node, "href");
1677
- const inner = kids();
1678
- return href === void 0 ? inner : [link(href, inner)];
1679
- }
1680
- if (name === "img") return [{
1681
- alt: attr(node, "alt"),
1682
- type: "image",
1683
- url: attr(node, "src") ?? ""
1684
- }];
1685
- if (name === "br") return [brk()];
1686
- if (name === "strong" || name === "b") return [strong(kids())];
1687
- if (name === "em" || name === "i") return [em(kids())];
1688
- if (name === "del" || name === "s" || name === "strike") return [del(kids())];
1689
- if (name === "code" || name === "kbd" || name === "samp") return [icode(deepText(node.children ?? []))];
1728
+ const inlineRenderer = own(HTML_INLINE, name);
1729
+ if (inlineRenderer !== void 0) return inlineRenderer(node, ctx);
1690
1730
  if (name === "hr") return [thematic()];
1691
1731
  if (name === "p") return [para(kids())];
1692
1732
  if (/^h[1-6]$/u.test(name)) return [heading(Number(name[1]), kids())];
1693
1733
  if (name === "blockquote") return [quote(blocks())];
1694
1734
  if (name === "pre") return [pre(deepText(node.children ?? []))];
1695
1735
  if (name === "ul" || name === "ol") {
1696
- const items = els(node, "li").map((li) => item([para(phrasing(ctx.children(li)))]));
1736
+ const items = els(node, "li").map((li) => item(blocksOf(ctx.children(li))));
1697
1737
  return items.length === 0 ? blocks() : [list(items, name === "ol")];
1698
1738
  }
1699
1739
  return inline ? kids() : blocks();
1700
1740
  };
1741
+ const renderMdxElement = (node, ctx, registry) => {
1742
+ const name = node.name ?? "";
1743
+ const entry = own(registry, name);
1744
+ if (entry === void 0) {
1745
+ if (/^[a-z]/u.test(name)) return htmlEl(node, ctx, node.type === "mdxJsxTextElement");
1746
+ ctx.warn(name);
1747
+ return ctx.children(node);
1748
+ }
1749
+ if (node.type === "mdxJsxTextElement") return entry.text?.(node, ctx) ?? phrasing(entry.flow?.(node, ctx) ?? ctx.children(node));
1750
+ if (entry.flow !== void 0) return entry.flow(node, ctx);
1751
+ const inlineOut = entry.text?.(node, ctx);
1752
+ return inlineOut === void 0 ? ctx.children(node) : [para(inlineOut)];
1753
+ };
1701
1754
  /** Replace every mdxJsx element under `root` with its ASCII representation. */
1702
1755
  const transformAscii = (root, registry, serialize, warn) => {
1703
1756
  const transformElement = (n, ctx) => {
1704
- if (isMdxEl(n)) {
1705
- const name = n.name ?? "";
1706
- const entry = own(registry, name);
1707
- if (entry === void 0) {
1708
- if (/^[a-z]/u.test(name)) return htmlEl(n, ctx, n.type === "mdxJsxTextElement");
1709
- warn(name);
1710
- return ctx.children(n);
1757
+ if (isMdxEl(n)) return renderMdxElement(n, ctx, registry);
1758
+ if (isParent(n)) {
1759
+ if (hoistable(n)) {
1760
+ const children = n.children.filter(isMdxEl).map((child) => ({
1761
+ ...child,
1762
+ type: "mdxJsxFlowElement"
1763
+ }));
1764
+ return blocksOf(ctx.children({
1765
+ ...n,
1766
+ children
1767
+ }));
1711
1768
  }
1712
- if (n.type === "mdxJsxTextElement") return entry.text?.(n, ctx) ?? phrasing(entry.flow?.(n, ctx) ?? ctx.children(n));
1713
- if (entry.flow !== void 0) return entry.flow(n, ctx);
1714
- const inlineOut = entry.text?.(n, ctx);
1715
- return inlineOut === void 0 ? ctx.children(n) : [para(inlineOut)];
1769
+ return [{
1770
+ ...n,
1771
+ children: ctx.children(n)
1772
+ }];
1716
1773
  }
1717
- if (isParent(n)) n.children = ctx.children(n);
1718
1774
  return [n];
1719
1775
  };
1720
1776
  const transformNodes = (nodes, ctx) => nodes.flatMap((node) => {
@@ -1784,7 +1840,7 @@ const STATUS_ICON = {
1784
1840
  unverified: "?",
1785
1841
  up: "✓"
1786
1842
  };
1787
- const statusIcon = (status) => status === void 0 ? "•" : STATUS_ICON[status.toLowerCase()] ?? "•";
1843
+ const statusIcon = (status) => status === void 0 ? "•" : own(STATUS_ICON, status.toLowerCase()) ?? "•";
1788
1844
  /** Left-align `s` in a `width`-wide column (no padding past the width). */
1789
1845
  const pad = (s, width) => s.length >= width ? s : s + " ".repeat(width - s.length);
1790
1846
  //#endregion
@@ -1796,6 +1852,11 @@ const suffix = (parts) => {
1796
1852
  };
1797
1853
  /** Bare `a · b · c` joining (no leading separator). */
1798
1854
  const joined = (parts) => parts.filter(nonEmpty).join(" · ");
1855
+ /** A disclosure with literal summary text and an already serialized body. */
1856
+ const detailsBlock = (summary, body) => {
1857
+ const escaped = summary.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1858
+ return html(`<details>\n<summary>${escaped}</summary>\n\n${body.trim()}\n\n</details>`);
1859
+ };
1799
1860
  /**
1800
1861
  * `**Title — extra**` caption paragraph, emitted only when something is
1801
1862
  * there. A title-less caption drops a leading ` — `/space so `extra` alone
@@ -1824,7 +1885,7 @@ const tally$1 = (entries, key) => {
1824
1885
  //#endregion
1825
1886
  //#region src/ascii/charts.ts
1826
1887
  /** Attribute → number list (`values="12, 8, 4"`). */
1827
- const nums = (node, key) => csv(node, key).map(Number).filter((v) => !Number.isNaN(v));
1888
+ const nums = (node, key) => numList(attr(node, key));
1828
1889
  /** `label ██████░░ 42` fence rows from name/value pairs. */
1829
1890
  const barRows = (rows, max) => {
1830
1891
  const W = 24;
@@ -2190,7 +2251,7 @@ const term = (node, ctx) => [para([strong([txt(attr(node, "name") ?? "")]), ...c
2190
2251
  const details = (node, ctx) => {
2191
2252
  const summary = attr(node, "summary") ?? "Details";
2192
2253
  const body = ctx.serialize(ctx.children(node)).trim();
2193
- return [html(`<details>\n<summary>${summary}</summary>\n\n${body}\n\n</details>`)];
2254
+ return [detailsBlock(summary, body)];
2194
2255
  };
2195
2256
  const documentRenderers = {
2196
2257
  Callout: { flow: (n, ctx) => {
@@ -2286,7 +2347,7 @@ const nodeRows = (node) => {
2286
2347
  if (grid && isParent(grid)) return tableRecords(grid.children.map((row) => isParent(row) ? row.children.map(textOf) : []));
2287
2348
  return els(node, "RecordItem").map(nodeAttributes);
2288
2349
  };
2289
- const recordsTable = (rows, cols) => rows.length ? [table(cols ?? columnsOf(rows), rows.map((row) => (cols ?? columnsOf(rows)).map((key) => display(row[key]) || "—")))] : [para([txt("No data")])];
2350
+ const recordsTable = (rows, cols) => rows.length ? [table(cols ?? columnsOf(rows), rows.map((row) => (cols ?? columnsOf(rows)).map((key) => display(own(row, key)) || "—")))] : [para([txt("No data")])];
2290
2351
  const narrative = (node, ctx) => {
2291
2352
  if (attr(node, "data") !== void 0) return ctx.children(node);
2292
2353
  const children = node.children?.filter((child) => {
@@ -2411,14 +2472,14 @@ Object.assign(extendedRenderers, {
2411
2472
  "pnpm",
2412
2473
  "yarn",
2413
2474
  "bun"
2414
- ].map((manager) => `${manager} ${manager === "npm" ? "install" : "add"}${attr(node, "dev") === "true" ? " -D" : ""} ${attr(node, "packages") ?? ""}`).join("\n"), "sh")] },
2475
+ ].map((manager) => `${manager} ${manager === "npm" ? "install" : "add"}${flag(node, "dev") ? " -D" : ""} ${attr(node, "packages") ?? ""}`).join("\n"), "sh")] },
2415
2476
  PageBreak: { flow: () => [{ type: "thematicBreak" }] },
2416
2477
  PdfPreview: { flow: (node) => sourceLink(node, "src", `${attr(node, "title") ?? "PDF document"} (page ${attr(node, "page") ?? "1"})`) },
2417
2478
  PrintLayout: { flow: (node, ctx) => ctx.children(node) },
2418
2479
  PromptTemplate: { flow: (node) => {
2419
2480
  const rows = nodeRows(node);
2420
2481
  const defaults = Object.fromEntries(rows.map((row) => [display(row.name), display(row.value)]));
2421
- const prompt = (attr(node, "template") ?? "").replaceAll(/\{\{\s*(?<variable>[\w.-]+)\s*\}\}/gu, (match, name) => defaults[name] ?? match);
2482
+ const prompt = (attr(node, "template") ?? "").replaceAll(/\{\{\s*(?<variable>[\w.-]+)\s*\}\}/gu, (match, name) => own(defaults, name) ?? match);
2422
2483
  return [
2423
2484
  ...caption(attr(node, "title") ?? "Prompt template"),
2424
2485
  ...recordsTable(rows),
@@ -2534,28 +2595,20 @@ const numbered = (entries, statusKey, ctx) => list(entries.map((e) => item([para
2534
2595
  ])])), true);
2535
2596
  const NOTE_RE = /\s+(?:—|#)\s+/u;
2536
2597
  const PLACEHOLDER_RE = /^(?:\.{3}|…)$/u;
2537
- const treeEntries = (nodes) => {
2538
- const out = [];
2539
- const walk = (n) => {
2540
- if (n.type === "list" && isParent(n)) {
2541
- for (const li of n.children) {
2542
- if (li.type !== "listItem" || !isParent(li)) continue;
2543
- const nested = li.children.filter((c) => c.type === "list");
2544
- const labelText = li.children.filter((c) => c.type !== "list").map((c) => textOf(c)).join("").trim();
2545
- const m = NOTE_RE.exec(labelText);
2546
- out.push({
2547
- children: treeEntries(nested),
2548
- name: (m === null ? labelText : labelText.slice(0, m.index)).trim(),
2549
- ...m === null ? {} : { note: labelText.slice(m.index + m[0].length).trim() }
2550
- });
2551
- }
2552
- return;
2553
- }
2554
- if (isParent(n)) for (const c of n.children) walk(c);
2598
+ const treeEntry = (node, children) => {
2599
+ const label = node.children.filter((child) => child.type !== "list").map(textOf).join("").trim();
2600
+ const note = NOTE_RE.exec(label);
2601
+ return {
2602
+ children,
2603
+ name: (note === null ? label : label.slice(0, note.index)).trim(),
2604
+ ...note === null ? {} : { note: label.slice(note.index + note[0].length).trim() }
2555
2605
  };
2556
- for (const c of nodes) walk(c);
2557
- return out;
2558
2606
  };
2607
+ const treeEntries = (nodes) => nodes.flatMap((node) => {
2608
+ if (!isParent(node)) return [];
2609
+ if (node.type === "list") return node.children.filter((child) => child.type === "listItem" && isParent(child)).map((child) => treeEntry(child, treeEntries(child.children.filter((nested) => nested.type === "list"))));
2610
+ return treeEntries(node.children);
2611
+ });
2559
2612
  const treeLines = (entries, prefix) => entries.flatMap((e, i) => {
2560
2613
  const last = i === entries.length - 1;
2561
2614
  const name = PLACEHOLDER_RE.test(e.name) ? "…" : e.name;
@@ -2652,14 +2705,18 @@ const investigationRenderers = {
2652
2705
  ...nonEmpty(attr(n, "path")) ? [txt(" — "), icode(pathAt(n))] : [],
2653
2706
  ...ctx.inline(n).length === 0 ? [] : [txt(": "), ...ctx.inline(n)]
2654
2707
  ])] },
2655
- Hypotheses: { flow: (n, ctx) => [...caption(attr(n, "title")), list(els(n, "Hypothesis").map((h) => {
2656
- const status = (attr(h, "status") ?? "untested").toLowerCase();
2657
- return item([para([
2658
- txt(`${statusIcon(status)} `),
2659
- ...nonEmpty(attr(h, "title")) ? [strong([txt(attr(h, "title") ?? "")])] : [],
2660
- ...ctx.inline(h).length === 0 ? [] : [txt(" — "), ...ctx.inline(h)]
2661
- ]), ...ctx.children(withoutEls(n, ["Hypothesis"]))], status === "supported" ? true : void 0);
2662
- }))] },
2708
+ Hypotheses: { flow: (n, ctx) => [
2709
+ ...caption(attr(n, "title")),
2710
+ list(els(n, "Hypothesis").map((h) => {
2711
+ const status = (attr(h, "status") ?? "untested").toLowerCase();
2712
+ return item([para([
2713
+ txt(`${statusIcon(status)} `),
2714
+ ...nonEmpty(attr(h, "title")) ? [strong([txt(attr(h, "title") ?? "")])] : [],
2715
+ ...ctx.inline(h).length === 0 ? [] : [txt(" — "), ...ctx.inline(h)]
2716
+ ])], status === "supported" ? true : void 0);
2717
+ })),
2718
+ ...ctx.children(withoutEls(n, ["Hypothesis"]))
2719
+ ] },
2663
2720
  Hypothesis: { flow: (n, ctx) => [para([txt(`${statusIcon(attr(n, "status"))} `), ...nonEmpty(attr(n, "title")) ? [strong([txt(attr(n, "title") ?? "")])] : []]), ...ctx.children(n)] },
2664
2721
  Prop: { flow: (n, ctx) => [para([
2665
2722
  icode(attr(n, "name") ?? ""),
@@ -2784,12 +2841,13 @@ const comment = (node, ctx) => {
2784
2841
  const graph = (node, ctx) => {
2785
2842
  const nodes = els(node, "Node");
2786
2843
  const edges = els(node, "Edge");
2844
+ const connected = new Set(edges.flatMap((edge) => [attr(edge, "from"), attr(edge, "to")]));
2787
2845
  const nodeNotes = nodes.flatMap((nd) => {
2788
2846
  const label = attr(nd, "label");
2789
2847
  const note = attr(nd, "note");
2790
2848
  const path = attr(nd, "path");
2791
2849
  const status = attr(nd, "status");
2792
- if ((label === void 0 || label === attr(nd, "id")) && note === void 0 && path === void 0 && status === void 0) return [];
2850
+ if ((label === void 0 || label === attr(nd, "id")) && note === void 0 && path === void 0 && status === void 0 && connected.has(attr(nd, "id"))) return [];
2793
2851
  const head = [
2794
2852
  icode(attr(nd, "id") ?? ""),
2795
2853
  txt(" = "),
@@ -2912,9 +2970,10 @@ const waterfall = (node, ctx) => {
2912
2970
  const title = attr(node, "title");
2913
2971
  if (nonEmpty(title)) lines.push(`${title} (0 → ${total}${unit})`);
2914
2972
  for (const s of spans) {
2915
- const from = Math.round(s.start / Math.max(1, total) * W);
2916
- const len = Math.max(1, Math.round(s.duration / Math.max(1, total) * W));
2917
- const cells = `${" ".repeat(Math.min(W, from))}${"█".repeat(Math.min(len, W - from))}`;
2973
+ const scale = Math.max(1, total);
2974
+ const from = Math.max(0, Math.min(W, Math.round(s.start / scale * W)));
2975
+ const to = Math.max(from, Math.min(W, Math.round((s.start + Math.max(0, s.duration)) / scale * W)));
2976
+ const cells = `${" ".repeat(from)}${"█".repeat(to - from)}`;
2918
2977
  lines.push(`${pad(s.name, nameW)} ${cells} ${s.durationRaw || `${s.duration}${unit}`}${nonEmpty(s.note) ? ` ${s.note}` : ""}`);
2919
2978
  }
2920
2979
  return [pre(lines.join("\n")), ...ctx.children(withoutEls(node, ["Span"]))];
@@ -2970,6 +3029,7 @@ const outputRenderers = {
2970
3029
  };
2971
3030
  //#endregion
2972
3031
  //#region src/ascii/planning.ts
3032
+ /** ASCII renderers for planning & status components. */
2973
3033
  /** `owner` attr → a normalized `@name` chip (leading `@`s collapsed). */
2974
3034
  const ownerChip = (node) => {
2975
3035
  const owner = attr(node, "owner");
@@ -3009,8 +3069,8 @@ const approvalStatus = (s) => {
3009
3069
  const DAY_MS = 864e5;
3010
3070
  const dayOf = (iso) => {
3011
3071
  if (iso === void 0) return;
3012
- const t = Date.parse(iso);
3013
- return Number.isNaN(t) ? void 0 : Math.floor(t / DAY_MS);
3072
+ const date = parseISO(iso);
3073
+ return isValid(date) ? differenceInCalendarDays(date, new Date(1970, 0, 1)) : void 0;
3014
3074
  };
3015
3075
  const TASK_CHAR = {
3016
3076
  blocked: "▒",
@@ -3018,56 +3078,75 @@ const TASK_CHAR = {
3018
3078
  done: "█",
3019
3079
  todo: "░"
3020
3080
  };
3081
+ const GANTT_WIDTH = 40;
3082
+ const ganttTasks = (node) => els(node, "Task").flatMap((el) => {
3083
+ const start = dayOf(attr(el, "start"));
3084
+ if (start === void 0) return [];
3085
+ return [{
3086
+ el,
3087
+ end: Math.max(start, dayOf(attr(el, "end")) ?? start),
3088
+ name: attr(el, "name") ?? "",
3089
+ progress: num(el, "progress"),
3090
+ start,
3091
+ status: (attr(el, "status") ?? "todo").toLowerCase()
3092
+ }];
3093
+ });
3094
+ const ganttMilestones = (node) => els(node, "Milestone").flatMap((el) => {
3095
+ const date = dayOf(attr(el, "date"));
3096
+ return date === void 0 ? [] : [{
3097
+ date,
3098
+ el,
3099
+ name: attr(el, "name") ?? ""
3100
+ }];
3101
+ });
3102
+ const ganttRange = (node, tasks, milestones) => {
3103
+ const start = dayOf(attr(node, "start"));
3104
+ const end = dayOf(attr(node, "end"));
3105
+ let first = start ?? Math.min(...tasks.map((task) => task.start), ...milestones.map((milestone) => milestone.date));
3106
+ let last = end ?? Math.max(...tasks.map((task) => task.end), ...milestones.map((milestone) => milestone.date));
3107
+ if (last < first) {
3108
+ if (end !== void 0 && start === void 0) first = last;
3109
+ else last = first;
3110
+ }
3111
+ return {
3112
+ first,
3113
+ last
3114
+ };
3115
+ };
3116
+ const ganttColumn = (day, range) => Math.max(0, Math.min(GANTT_WIDTH, Math.round((day - range.first) / (range.last - range.first + 1) * GANTT_WIDTH)));
3117
+ const ganttTaskLine = (task, range, nameWidth) => {
3118
+ const from = ganttColumn(task.start, range);
3119
+ const to = Math.max(from + 1, ganttColumn(task.end + 1, range));
3120
+ const outside = task.end < range.first || task.start > range.last;
3121
+ const glyph = own(TASK_CHAR, task.status) ?? "░";
3122
+ const filled = (to - from) * Math.min(100, Math.max(0, task.progress ?? 0)) / 100;
3123
+ const cells = Array.from({ length: GANTT_WIDTH }, (_, i) => {
3124
+ if (outside || i < from || i >= to) return " ";
3125
+ return i - from < filled ? "█" : glyph;
3126
+ }).join("");
3127
+ const tail = joined([ownerChip(task.el), attr(task.el, "note")]);
3128
+ return `${pad(task.name, nameWidth)} ${cells}${tail === "" ? "" : ` ${tail}`}`;
3129
+ };
3130
+ const ganttMilestoneLine = (milestone, range, nameWidth) => {
3131
+ const position = milestone.date < range.first || milestone.date > range.last ? -1 : Math.min(39, ganttColumn(milestone.date, range));
3132
+ const cells = Array.from({ length: GANTT_WIDTH }, (_, i) => i === position ? "◆" : " ").join("");
3133
+ const tail = joined([attr(milestone.el, "status"), attr(milestone.el, "note")]);
3134
+ return `${pad(milestone.name, nameWidth)} ${cells}${tail === "" ? "" : ` ${tail}`}`;
3135
+ };
3021
3136
  const gantt = (node, ctx) => {
3022
- const tasks = els(node, "Task").flatMap((t) => {
3023
- const start = dayOf(attr(t, "start"));
3024
- return start === void 0 ? [] : [{
3025
- el: t,
3026
- end: dayOf(attr(t, "end")) ?? start,
3027
- name: attr(t, "name") ?? "",
3028
- progress: num(t, "progress"),
3029
- start,
3030
- status: (attr(t, "status") ?? "todo").toLowerCase()
3031
- }];
3032
- });
3033
- const milestones = els(node, "Milestone").flatMap((m) => {
3034
- const date = dayOf(attr(m, "date"));
3035
- return date === void 0 ? [] : [{
3036
- date,
3037
- el: m,
3038
- name: attr(m, "name") ?? ""
3039
- }];
3040
- });
3137
+ const tasks = ganttTasks(node);
3138
+ const milestones = ganttMilestones(node);
3041
3139
  if (tasks.length === 0 && milestones.length === 0) return ctx.children(node);
3042
- const lo = dayOf(attr(node, "start")) ?? Math.min(...tasks.map((t) => t.start), ...milestones.map((m) => m.date));
3043
- const hi = dayOf(attr(node, "end")) ?? Math.max(...tasks.map((t) => t.end + 1), ...milestones.map((m) => m.date + 1));
3044
- const span = Math.max(1, hi - lo);
3045
- const W = 40;
3046
- const at = (day) => Math.max(0, Math.min(W, Math.round((day - lo) / span * W)));
3047
- const nameW = Math.min(24, Math.max(8, ...tasks.map((t) => t.name.length), ...milestones.map((m) => m.name.length)));
3048
- const iso = (d) => (/* @__PURE__ */ new Date(d * DAY_MS)).toISOString().slice(0, 10);
3049
- const lines = [];
3140
+ const range = ganttRange(node, tasks, milestones);
3141
+ const nameWidth = Math.min(24, Math.max(8, ...tasks.map((task) => task.name.length), ...milestones.map((milestone) => milestone.name.length)));
3142
+ const iso = (day) => (/* @__PURE__ */ new Date(day * DAY_MS)).toISOString().slice(0, 10);
3050
3143
  const title = attr(node, "title");
3051
- if (nonEmpty(title)) lines.push(title);
3052
- lines.push(`${pad("", nameW)} ${iso(lo)} ${iso(hi)}`);
3053
- for (const t of tasks) {
3054
- const from = at(t.start);
3055
- const to = Math.max(from + 1, at(t.end + 1));
3056
- const cells = Array.from({ length: W }, (_, i) => {
3057
- if (i < from || i >= to) return " ";
3058
- const ch = TASK_CHAR[t.status] ?? "░";
3059
- if (t.progress === void 0 || t.progress <= 0) return ch;
3060
- return i - from < (to - from) * Math.min(100, t.progress) / 100 ? "█" : ch;
3061
- }).join("");
3062
- const owner = attr(t.el, "owner");
3063
- const tail = joined([nonEmpty(owner) ? `@${owner.replace(/^@+/u, "")}` : void 0, attr(t.el, "note")]);
3064
- lines.push(`${pad(t.name, nameW)} ${cells}${tail === "" ? "" : ` ${tail}`}`);
3065
- }
3066
- for (const m of milestones) {
3067
- const cells = Array.from({ length: W }, (_, i) => i === at(m.date) ? "◆" : " ").join("");
3068
- const tail = joined([attr(m.el, "status"), attr(m.el, "note")]);
3069
- lines.push(`${pad(m.name, nameW)} ${cells}${tail === "" ? "" : ` ${tail}`}`);
3070
- }
3144
+ const lines = [
3145
+ ...nonEmpty(title) ? [title] : [],
3146
+ `${pad("", nameWidth)} ${iso(range.first)} → ${iso(range.last)}`,
3147
+ ...tasks.map((task) => ganttTaskLine(task, range, nameWidth)),
3148
+ ...milestones.map((milestone) => ganttMilestoneLine(milestone, range, nameWidth))
3149
+ ];
3071
3150
  return [pre(lines.join("\n")), ...ctx.children(withoutEls(node, ["Task", "Milestone"]))];
3072
3151
  };
3073
3152
  const CELL_GLYPH = {
@@ -3103,7 +3182,7 @@ const CELL_GLYPH = {
3103
3182
  };
3104
3183
  const matrixCell = (cell) => {
3105
3184
  const key = cell.trim().toLowerCase();
3106
- return key === "" ? "—" : CELL_GLYPH[key] ?? cell.trim();
3185
+ return key === "" ? "—" : own(CELL_GLYPH, key) ?? cell.trim();
3107
3186
  };
3108
3187
  /** `- label | a | b` list items → matrix rows. */
3109
3188
  const matrixRows = (node) => {
@@ -3135,12 +3214,16 @@ const planningRenderers = {
3135
3214
  attr(a, "role"),
3136
3215
  attr(a, "status"),
3137
3216
  attr(a, "date")
3138
- ])], []))), ...ctx.children(withoutEls(n, ["Approval"]))] },
3217
+ ])], ctx.children(a)))), ...ctx.children(withoutEls(n, ["Approval"]))] },
3139
3218
  Board: { flow: (n, ctx) => [
3140
3219
  ...caption(attr(n, "title")),
3141
3220
  ...els(n, "Lane").flatMap((lane) => {
3142
3221
  const cards = els(lane, "BoardCard");
3143
- return [para([txt(`${statusIcon(attr(lane, "status"))} `), strong([txt(`${attr(lane, "title") ?? ""} (${cards.length})`)])]), list(cards.map((c) => boardCard(c, ctx)))];
3222
+ return [
3223
+ para([txt(`${statusIcon(attr(lane, "status"))} `), strong([txt(`${attr(lane, "title") ?? ""} (${cards.length})`)])]),
3224
+ list(cards.map((c) => boardCard(c, ctx))),
3225
+ ...ctx.children(withoutEls(lane, ["BoardCard"]))
3226
+ ];
3144
3227
  }),
3145
3228
  ...ctx.children(withoutEls(n, ["Lane"]))
3146
3229
  ] },
@@ -3202,11 +3285,11 @@ const planningRenderers = {
3202
3285
  ])],
3203
3286
  text: (n) => [strong([txt(attr(n, "value") ?? "")]), txt(` ${attr(n, "label") ?? ""}`)]
3204
3287
  },
3205
- Stats: { flow: (n) => [list(els(n, "Stat").map((s) => item([para([
3288
+ Stats: { flow: (n, ctx) => [list(els(n, "Stat").map((s) => item([para([
3206
3289
  strong([txt(attr(s, "value") ?? "")]),
3207
3290
  txt(` ${attr(s, "label") ?? ""}`),
3208
3291
  ...nonEmpty(attr(s, "delta")) ? [txt(` (${attr(s, "delta")})`)] : []
3209
- ])])))] },
3292
+ ])]))), ...ctx.children(withoutEls(n, ["Stat"]))] },
3210
3293
  StatusBadge: { text: (n) => [icode(attr(n, "status") ?? "")] },
3211
3294
  Step: { flow: (n, ctx) => [para([
3212
3295
  txt(`${statusIcon(attr(n, "status"))} `),
@@ -3269,7 +3352,7 @@ const VERDICT_LABEL = {
3269
3352
  pass: "Passed",
3270
3353
  warn: "Warning"
3271
3354
  };
3272
- const verdictLabel = (s) => s === void 0 ? void 0 : VERDICT_LABEL[s.toLowerCase()] ?? s;
3355
+ const verdictLabel = (s) => s === void 0 ? void 0 : own(VERDICT_LABEL, s.toLowerCase()) ?? s;
3273
3356
  const DURATION_MS = {
3274
3357
  h: 36e5,
3275
3358
  m: 6e4,
@@ -3318,9 +3401,14 @@ const DAY_CELL = {
3318
3401
  const uptimeStrip = (node) => {
3319
3402
  const days = els(node, "Day");
3320
3403
  if (days.length === 0) return "";
3321
- return days.map((d) => DAY_CELL[(attr(d, "status") ?? "up").toLowerCase()] ?? "█").join("");
3404
+ return days.map((d) => own(DAY_CELL, (attr(d, "status") ?? "up").toLowerCase()) ?? "█").join("");
3405
+ };
3406
+ const percentText = (value) => nonEmpty(value) && !value.endsWith("%") ? `${value}%` : value;
3407
+ const serviceRow = (node, ctx) => statusItem$1(attr(node, "status"), [txt(attr(node, "name") ?? ""), ...suffix([attr(node, "status"), percentText(attr(node, "uptime"))])], ctx.children(node));
3408
+ const releaseVersion = (node) => {
3409
+ const version = attr(node, "version") ?? "";
3410
+ return version.startsWith("v") ? version : `v${version}`;
3322
3411
  };
3323
- const serviceRow = (node) => statusItem$1(attr(node, "status"), [txt(attr(node, "name") ?? ""), ...suffix([attr(node, "status"), nonEmpty(attr(node, "uptime")) ? `${attr(node, "uptime")}%` : void 0])], []);
3324
3412
  const entryRow = (node, ctx) => {
3325
3413
  const scope = attr(node, "scope");
3326
3414
  return item([para([
@@ -3368,7 +3456,7 @@ const reportRenderers = {
3368
3456
  attr(b, "before") ?? "",
3369
3457
  attr(b, "after") ?? "",
3370
3458
  d,
3371
- attr(b, "note") ?? ctx.text(b)
3459
+ joined([attr(b, "note"), ctx.text(b)])
3372
3460
  ];
3373
3461
  })),
3374
3462
  ...ctx.children(withoutEls(n, ["Bench"]))
@@ -3439,7 +3527,8 @@ const reportRenderers = {
3439
3527
  "field",
3440
3528
  "type",
3441
3529
  "flags",
3442
- "default"
3530
+ "default",
3531
+ "description"
3443
3532
  ], els(n, "DbField").map((f) => {
3444
3533
  const flags = [
3445
3534
  flag(f, "pk") ? "pk" : void 0,
@@ -3451,7 +3540,8 @@ const reportRenderers = {
3451
3540
  [icode(attr(f, "name") ?? "")],
3452
3541
  [icode(attr(f, "type") ?? "")],
3453
3542
  flags,
3454
- attr(f, "default") ?? ""
3543
+ attr(f, "default") ?? "",
3544
+ ctx.text(f)
3455
3545
  ];
3456
3546
  })),
3457
3547
  ...ctx.children(withoutEls(n, ["DbField"]))
@@ -3464,7 +3554,7 @@ const reportRenderers = {
3464
3554
  EnvVar: { flow: (n, ctx) => [para([
3465
3555
  icode(attr(n, "name") ?? ""),
3466
3556
  ...flag(n, "required") ? [txt(" "), icode("required")] : [],
3467
- ...suffix([flag(n, "secret") ? "secret" : attr(n, "value"), nonEmpty(attr(n, "default")) ? `default ${attr(n, "default")}` : void 0]),
3557
+ ...suffix([flag(n, "secret") ? "secret" : attr(n, "value"), !flag(n, "secret") && nonEmpty(attr(n, "default")) ? `default ${attr(n, "default")}` : void 0]),
3468
3558
  ...ctx.inline(n).length === 0 ? [] : [txt(" — "), ...ctx.inline(n)]
3469
3559
  ])] },
3470
3560
  EnvVars: { flow: (n, ctx) => [
@@ -3479,7 +3569,7 @@ const reportRenderers = {
3479
3569
  [icode(attr(e, "name") ?? "")],
3480
3570
  flag(e, "required") ? "yes" : "",
3481
3571
  flag(e, "secret") ? "•••" : attr(e, "value") ?? "",
3482
- attr(e, "default") ?? "",
3572
+ flag(e, "secret") ? "•••" : attr(e, "default") ?? "",
3483
3573
  ctx.text(e)
3484
3574
  ])),
3485
3575
  ...ctx.children(withoutEls(n, ["EnvVar"]))
@@ -3553,7 +3643,7 @@ const reportRenderers = {
3553
3643
  attr(p, "version") ?? "",
3554
3644
  attr(p, "kind") ?? "",
3555
3645
  attr(p, "license") ?? "",
3556
- ctx.text(p)
3646
+ joined([attr(p, "note"), ctx.text(p)])
3557
3647
  ])),
3558
3648
  ...ctx.children(withoutEls(n, ["Package"]))
3559
3649
  ] },
@@ -3563,17 +3653,17 @@ const reportRenderers = {
3563
3653
  ...ctx.children(withoutEls(n, ["Stop"]))
3564
3654
  ] },
3565
3655
  Release: { flow: (n, ctx) => [
3566
- heading(3, [...nonEmpty(attr(n, "href")) ? [link(attr(n, "href") ?? "", [txt(`v${attr(n, "version") ?? ""}`)])] : [icode(`v${attr(n, "version") ?? ""}`)], ...nonEmpty(attr(n, "title")) ? [txt(` — ${attr(n, "title")}`)] : []]),
3656
+ heading(3, [...nonEmpty(attr(n, "href")) ? [link(attr(n, "href") ?? "", [txt(releaseVersion(n))])] : [icode(releaseVersion(n))], ...nonEmpty(attr(n, "title")) ? [txt(` — ${attr(n, "title")}`)] : []]),
3567
3657
  ...nonEmpty(attr(n, "date")) ? [para([em([txt(attr(n, "date") ?? "")])])] : [],
3568
3658
  ...els(n, "Entry").length === 0 ? ctx.children(n) : [list(els(n, "Entry").map((e) => entryRow(e, ctx))), ...ctx.children(withoutEls(n, ["Entry"]))]
3569
3659
  ] },
3570
3660
  Review: { flow: (n, ctx) => [...caption(attr(n, "title"), `${nonEmpty(attr(n, "verdict")) ? ` — ${verdictLabel(attr(n, "verdict"))}` : ""}${tally$1(els(n, "Comment"), "severity")}`), ...ctx.children(n)] },
3571
3661
  Schema: { flow: (n, ctx) => [...caption(attr(n, "title"), nonEmpty(attr(n, "engine")) ? ` (${attr(n, "engine")})` : ""), ...ctx.children(n)] },
3572
- Service: { flow: (n) => [para([
3662
+ Service: { flow: (n, ctx) => [para([
3573
3663
  txt(`${statusIcon(attr(n, "status"))} `),
3574
3664
  txt(attr(n, "name") ?? ""),
3575
- ...suffix([attr(n, "status"), nonEmpty(attr(n, "uptime")) ? `${attr(n, "uptime")}%` : void 0])
3576
- ])] },
3665
+ ...suffix([attr(n, "status"), percentText(attr(n, "uptime"))])
3666
+ ]), ...ctx.children(n)] },
3577
3667
  Severity: { text: (n) => [icode((attr(n, "level") ?? "").toUpperCase())] },
3578
3668
  StatusPage: { flow: (n, ctx) => {
3579
3669
  const services = els(n, "Service");
@@ -3588,7 +3678,7 @@ const reportRenderers = {
3588
3678
  ...suffix([[attr(u, "from"), attr(u, "to")].filter(nonEmpty).join(" → ")])
3589
3679
  ]), ...strip === "" ? [] : [pre(strip)]];
3590
3680
  }),
3591
- ...services.length === 0 ? [] : [list(services.map(serviceRow))],
3681
+ ...services.length === 0 ? [] : [list(services.map((service) => serviceRow(service, ctx)))],
3592
3682
  ...ctx.children(withoutEls(n, ["Service", "Uptime"]))
3593
3683
  ];
3594
3684
  } },
@@ -3661,33 +3751,39 @@ const menuItem = {
3661
3751
  text: (n, ctx) => ctx.inline(n)
3662
3752
  };
3663
3753
  /** `[x]`/`[ ]`/`( )` control from a checked-ish attribute. */
3754
+ const controlOn = (n) => {
3755
+ if (flag(n, "checked") || flagOff(n, "checked")) return flag(n, "checked");
3756
+ return flag(n, "defaultChecked") || flag(n, "pressed");
3757
+ };
3664
3758
  const control = (mark) => ({
3665
3759
  flow: (n, ctx) => {
3666
3760
  const inner = ctx.inline(n);
3667
3761
  const label = attr(n, "label");
3668
3762
  return [para([
3669
- icode(mark(flag(n, "checked") || flag(n, "defaultChecked") || flag(n, "pressed"))),
3763
+ icode(mark(controlOn(n))),
3670
3764
  ...inner.length === 0 ? [] : [txt(" "), ...inner],
3671
3765
  ...nonEmpty(label) ? [txt(` ${label}`)] : []
3672
3766
  ])];
3673
3767
  },
3674
- text: (n, ctx) => [icode(mark(flag(n, "checked") || flag(n, "defaultChecked") || flag(n, "pressed"))), ...ctx.inline(n).length === 0 ? [] : [txt(" "), ...ctx.inline(n)]]
3768
+ text: (n, ctx) => [icode(mark(controlOn(n))), ...ctx.inline(n).length === 0 ? [] : [txt(" "), ...ctx.inline(n)]]
3675
3769
  });
3676
3770
  /** `` `[____]` ``-style empty input stand-in. */
3771
+ const inputText = (n) => {
3772
+ const value = attr(n, "value") ?? attr(n, "defaultValue");
3773
+ if (value !== void 0) return attr(n, "type") === "password" ? "••••" : value;
3774
+ return attr(n, "placeholder") ?? attr(n, "label") ?? "____";
3775
+ };
3677
3776
  const input = {
3678
- flow: (n) => [pre(`[ ${attr(n, "placeholder") ?? attr(n, "value") ?? attr(n, "label") ?? "____"} ]`)],
3679
- text: (n) => [icode(`[${attr(n, "placeholder") ?? attr(n, "value") ?? attr(n, "label") ?? "____"}]`)]
3777
+ flow: (n) => [pre(`[ ${inputText(n)} ]`)],
3778
+ text: (n) => [icode(`[${inputText(n)}]`)]
3680
3779
  };
3681
3780
  /** `<details>` disclosure — summary from AccordionTrigger/collapsible label. */
3682
3781
  const disclosure = (trigger, fallback) => ({ flow: (n, ctx) => {
3683
3782
  const [head] = els(n, trigger);
3684
3783
  const summary = head === void 0 ? attr(n, "label") ?? fallback : ctx.text(head);
3685
- const rest = ctx.children({
3686
- ...n,
3687
- children: (n.children ?? []).filter((c) => !named(c, trigger))
3688
- });
3784
+ const rest = ctx.children(withoutEls(n, [trigger]));
3689
3785
  const body = ctx.serialize(rest).trim();
3690
- return [html(`<details>\n<summary>${summary}</summary>\n\n${body}\n\n</details>`)];
3786
+ return [detailsBlock(summary, body)];
3691
3787
  } });
3692
3788
  /** `<Tabs>` → `**tab**` headings + content sections. */
3693
3789
  const tabs = { flow: (n, ctx) => {
@@ -3762,7 +3858,7 @@ const frac = (n) => {
3762
3858
  const max = num(n, "max") ?? 100;
3763
3859
  return {
3764
3860
  text: `${attr(n, "value") ?? Math.round(value)}${nonEmpty(attr(n, "max")) ? `/${attr(n, "max")}` : "%"}`,
3765
- v: max === 0 ? 0 : Math.min(1, value / max)
3861
+ v: max <= 0 ? 0 : Math.max(0, Math.min(1, value / max))
3766
3862
  };
3767
3863
  };
3768
3864
  const progress = {
@@ -3813,7 +3909,7 @@ const otp = {
3813
3909
  return [pre(`[ ${"_ ".repeat(count).trim()} ]`)];
3814
3910
  },
3815
3911
  text: (n) => {
3816
- const count = num(n, "maxLength") ?? 6;
3912
+ const count = Math.max(0, num(n, "maxLength") ?? 6);
3817
3913
  return [icode(`[${"_".repeat(count)}]`)];
3818
3914
  }
3819
3915
  };
@@ -4091,50 +4187,54 @@ const mdxToAscii = async (source, filePath = "document.mdx") => {
4091
4187
  };
4092
4188
  //#endregion
4093
4189
  //#region src/catalog.ts
4190
+ const OPTIONAL_TYPES = /* @__PURE__ */ new Set([
4191
+ "exact_optional",
4192
+ "nullish",
4193
+ "optional"
4194
+ ]);
4195
+ const describeLiteral = (value) => {
4196
+ if (typeof value === "bigint") return `${value}n`;
4197
+ return JSON.stringify(value) ?? String(value);
4198
+ };
4199
+ const pipeConstraint = (schema) => {
4200
+ if (!Array.isArray(schema.pipe)) return;
4201
+ return schema.pipe.find((s) => isRecord(s) && (s.type === "picklist" || s.type === "union" || s.type === "literal"));
4202
+ };
4094
4203
  /** Structural view of a valibot schema — enough for catalog introspection. */
4095
4204
  const describeSchema = (schema) => {
4096
4205
  if (!isRecord(schema)) return "unknown";
4097
- if (schema.type === "array") return `${describeSchema(schema.item)}[]`;
4098
- if (schema.type === "literal") return JSON.stringify(schema.literal);
4099
- if (schema.type === "picklist" && Array.isArray(schema.options)) return schema.options.map((o) => JSON.stringify(o)).join(" | ");
4100
- if (schema.type === "union" && Array.isArray(schema.options)) return schema.options.map(describeSchema).join(" | ");
4101
- if (Array.isArray(schema.pipe)) {
4102
- const constrained = schema.pipe.find((s) => isRecord(s) && (s.type === "picklist" || s.type === "union" || s.type === "literal"));
4103
- if (constrained !== void 0) return describeSchema(constrained);
4206
+ switch (schema.type) {
4207
+ case "nullable":
4208
+ case "nullish": return `${describeSchema(schema.wrapped)} | null`;
4209
+ case "optional":
4210
+ case "exact_optional": return describeSchema(schema.wrapped);
4211
+ case "array": {
4212
+ const item = describeSchema(schema.item);
4213
+ return item.includes(" | ") ? `(${item})[]` : `${item}[]`;
4214
+ }
4215
+ case "literal": return describeLiteral(schema.literal);
4216
+ case "picklist":
4217
+ case "union": if (Array.isArray(schema.options)) {
4218
+ const describe = schema.type === "picklist" ? describeLiteral : describeSchema;
4219
+ return schema.options.map(describe).join(" | ");
4220
+ }
4104
4221
  }
4222
+ const constrained = pipeConstraint(schema);
4223
+ if (constrained !== void 0) return describeSchema(constrained);
4105
4224
  return typeof schema.type === "string" ? schema.type : "unknown";
4106
4225
  };
4107
- const WRAPPER_TYPES = /* @__PURE__ */ new Set([
4108
- "nullable",
4109
- "nullish",
4110
- "optional"
4111
- ]);
4112
- const unwrapProp = (raw) => {
4113
- let s = isRecord(raw) ? raw : {};
4114
- let required = true;
4115
- let def;
4116
- while (typeof s.type === "string" && WRAPPER_TYPES.has(s.type)) {
4117
- required = false;
4118
- def ??= s.default;
4119
- s = isRecord(s.wrapped) ? s.wrapped : {};
4120
- }
4121
- return {
4122
- def,
4123
- required,
4124
- s
4125
- };
4126
- };
4127
4226
  const propsOf = (schema) => {
4128
4227
  if (!isRecord(schema) || !isRecord(schema.entries)) return {};
4129
4228
  const out = {};
4130
4229
  for (const [key, raw] of Object.entries(schema.entries)) {
4131
- const { def, required, s } = unwrapProp(raw);
4230
+ const optional = isRecord(raw) && typeof raw.type === "string" && OPTIONAL_TYPES.has(raw.type);
4231
+ const def = optional ? raw.default : void 0;
4132
4232
  Object.defineProperty(out, key, {
4133
4233
  configurable: true,
4134
4234
  enumerable: true,
4135
4235
  value: {
4136
- required,
4137
- type: describeSchema(s),
4236
+ required: !optional,
4237
+ type: describeSchema(raw),
4138
4238
  ...def === void 0 ? {} : { default: def }
4139
4239
  },
4140
4240
  writable: true
@@ -4451,7 +4551,7 @@ const openInBrowser = async (target) => {
4451
4551
  };
4452
4552
  //#endregion
4453
4553
  //#region src/client-js.ts
4454
- let cache;
4554
+ let cache$1;
4455
4555
  const buildClientJs = async () => {
4456
4556
  return (await build({
4457
4557
  absWorkingDir: pkgRoot,
@@ -4479,11 +4579,11 @@ const buildClientJs = async () => {
4479
4579
  * bundle is identical for every render in a process.
4480
4580
  */
4481
4581
  const clientJs = async () => {
4482
- cache ??= buildClientJs();
4582
+ cache$1 ??= buildClientJs();
4483
4583
  try {
4484
- return await cache;
4584
+ return await cache$1;
4485
4585
  } catch (error) {
4486
- cache = void 0;
4586
+ cache$1 = void 0;
4487
4587
  throw error;
4488
4588
  }
4489
4589
  };
@@ -4577,6 +4677,338 @@ addEventListener('DOMContentLoaded', function () {
4577
4677
  });
4578
4678
  `;
4579
4679
  //#endregion
4680
+ //#region src/components/ui/toc.tsx
4681
+ /**
4682
+ * Table of contents with active-anchor tracking and depth-aware line styling.
4683
+ * Adapted from the Crux UI / ABUI registry (`@abui/toc`), itself derived from
4684
+ * fumadocs' TOC. Two list variants: `TOCItems` (straight line) and
4685
+ * `ClerkTOCItems` (line that follows the heading hierarchy).
4686
+ */
4687
+ const ActiveAnchorContext = createContext([]);
4688
+ const ScrollContext = createContext({ current: null });
4689
+ const TOCContext = createContext([]);
4690
+ /** The ids of visible anchors. */
4691
+ const useActiveAnchors = () => useContext(ActiveAnchorContext);
4692
+ const useTOCItems = () => useContext(TOCContext);
4693
+ const mergeRefs = (...refs) => (value) => {
4694
+ for (const ref of refs) if (typeof ref === "function") ref(value);
4695
+ else if (ref !== null && ref !== void 0) ref.current = value;
4696
+ };
4697
+ /** Track which of the `watch` heading ids are on screen. */
4698
+ const useAnchorObserver = (watch, single) => {
4699
+ const observerRef = useRef(null);
4700
+ const visibleRef = useRef(/* @__PURE__ */ new Set());
4701
+ const [activeAnchor, setActiveAnchor] = useState([]);
4702
+ const onChange = useCallback((entries) => {
4703
+ const visible = visibleRef.current;
4704
+ for (const entry of entries) if (entry.isIntersecting) visible.add(entry.target.id);
4705
+ else visible.delete(entry.target.id);
4706
+ if (visible.size > 0) {
4707
+ const items = watch.filter((item) => visible.has(item));
4708
+ setActiveAnchor(single ? items.slice(0, 1) : items);
4709
+ return;
4710
+ }
4711
+ const viewTop = entries[0]?.rootBounds?.top ?? 0;
4712
+ let fallback;
4713
+ let min = -1;
4714
+ for (const id of watch) {
4715
+ const element = document.getElementById(id);
4716
+ if (element === null) continue;
4717
+ const d = Math.abs(viewTop - element.getBoundingClientRect().top);
4718
+ if (min === -1 || d < min) {
4719
+ fallback = element;
4720
+ min = d;
4721
+ }
4722
+ }
4723
+ setActiveAnchor(fallback === void 0 ? [] : [fallback.id]);
4724
+ }, [watch, single]);
4725
+ useEffect(() => {
4726
+ if (observerRef.current !== null) return;
4727
+ observerRef.current = new IntersectionObserver(onChange, {
4728
+ rootMargin: "0px",
4729
+ threshold: .98
4730
+ });
4731
+ return () => {
4732
+ observerRef.current?.disconnect();
4733
+ observerRef.current = null;
4734
+ };
4735
+ }, [onChange]);
4736
+ useEffect(() => {
4737
+ const observer = observerRef.current;
4738
+ if (observer === null) return;
4739
+ const elements = watch.flatMap((heading) => document.getElementById(heading) ?? []);
4740
+ for (const element of elements) observer.observe(element);
4741
+ return () => {
4742
+ for (const element of elements) observer.unobserve(element);
4743
+ };
4744
+ }, [watch]);
4745
+ return activeAnchor;
4746
+ };
4747
+ const TOCProvider = ({ toc, single = false, children }) => {
4748
+ const headings = useMemo(() => toc.map((item) => item.url.split("#")[1] ?? ""), [toc]);
4749
+ const activeAnchors = useAnchorObserver(headings, single);
4750
+ return /* @__PURE__ */ jsx(TOCContext.Provider, {
4751
+ value: toc,
4752
+ children: /* @__PURE__ */ jsx(ActiveAnchorContext.Provider, {
4753
+ value: activeAnchors,
4754
+ children
4755
+ })
4756
+ });
4757
+ };
4758
+ /** Scrollable container; the first active item is kept in view. */
4759
+ const TOCScrollArea = ({ ref, className, children, ...props }) => {
4760
+ const viewRef = useRef(null);
4761
+ return /* @__PURE__ */ jsx("div", {
4762
+ className: cn("relative ms-px min-h-0 [scrollbar-width:none] overflow-auto [mask-image:linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3 text-sm", className),
4763
+ ref: mergeRefs(viewRef, ref),
4764
+ ...props,
4765
+ children: /* @__PURE__ */ jsx(ScrollContext.Provider, {
4766
+ value: viewRef,
4767
+ children
4768
+ })
4769
+ });
4770
+ };
4771
+ const calcThumb = (container, active) => {
4772
+ if (active.length === 0 || container.clientHeight === 0) return [0, 0];
4773
+ let upper = Number.MAX_VALUE;
4774
+ let lower = 0;
4775
+ for (const item of active) {
4776
+ const element = container.querySelector(`a[href="#${CSS.escape(item)}"]`);
4777
+ if (element === null) continue;
4778
+ const styles = getComputedStyle(element);
4779
+ upper = Math.min(upper, element.offsetTop + Number.parseFloat(styles.paddingTop));
4780
+ lower = Math.max(lower, element.offsetTop + element.clientHeight - Number.parseFloat(styles.paddingBottom));
4781
+ }
4782
+ return [upper, lower - upper];
4783
+ };
4784
+ const updateThumb = (element, [top, height]) => {
4785
+ element.style.setProperty("--toc-top", `${top}px`);
4786
+ element.style.setProperty("--toc-height", `${height}px`);
4787
+ };
4788
+ /** Active indicator that slides alongside the visible entries. */
4789
+ const TocThumb = ({ containerRef, ...props }) => {
4790
+ const thumbRef = useRef(null);
4791
+ const active = useActiveAnchors();
4792
+ useEffect(() => {
4793
+ const container = containerRef.current;
4794
+ if (container === null) return;
4795
+ const onUpdate = () => {
4796
+ if (thumbRef.current !== null) updateThumb(thumbRef.current, calcThumb(container, active));
4797
+ };
4798
+ const observer = new ResizeObserver(onUpdate);
4799
+ observer.observe(container);
4800
+ onUpdate();
4801
+ return () => {
4802
+ observer.disconnect();
4803
+ };
4804
+ }, [containerRef, active]);
4805
+ return /* @__PURE__ */ jsx("div", {
4806
+ ref: thumbRef,
4807
+ role: "none",
4808
+ ...props
4809
+ });
4810
+ };
4811
+ const TOCItem = ({ ref, onActiveChange, children, ...props }) => {
4812
+ const containerRef = useContext(ScrollContext);
4813
+ const anchorRef = useRef(null);
4814
+ const activeOrder = useActiveAnchors().indexOf(props.href.slice(1));
4815
+ const isActive = activeOrder !== -1;
4816
+ const shouldScroll = activeOrder === 0;
4817
+ useLayoutEffect(() => {
4818
+ const anchor = anchorRef.current;
4819
+ const container = containerRef.current;
4820
+ if (container === null || anchor === null || !shouldScroll || !anchor.isConnected) return;
4821
+ const containerRect = container.getBoundingClientRect();
4822
+ const anchorRect = anchor.getBoundingClientRect();
4823
+ if (!(anchorRect.top < containerRect.top || anchorRect.bottom > containerRect.bottom)) return;
4824
+ const anchorCenter = anchor.offsetTop - container.offsetTop + anchor.offsetHeight / 2;
4825
+ container.scrollTo({
4826
+ behavior: "smooth",
4827
+ top: Math.max(0, anchorCenter - container.clientHeight / 2)
4828
+ });
4829
+ }, [containerRef, shouldScroll]);
4830
+ useEffect(() => {
4831
+ onActiveChange?.(isActive);
4832
+ }, [isActive, onActiveChange]);
4833
+ return /* @__PURE__ */ jsx("a", {
4834
+ "data-active": isActive,
4835
+ ref: mergeRefs(anchorRef, ref),
4836
+ ...props,
4837
+ children
4838
+ });
4839
+ };
4840
+ const EMPTY_CLS = "rounded-lg border bg-card p-3 text-xs text-muted-foreground";
4841
+ const ITEM_CLS = "py-1.5 text-sm text-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 hover:text-accent-foreground data-[active=true]:text-primary";
4842
+ const SimpleTOCItem = ({ item }) => /* @__PURE__ */ jsx(TOCItem, {
4843
+ className: cn(ITEM_CLS, item.depth <= 2 && "ps-3", item.depth === 3 && "ps-6", item.depth >= 4 && "ps-8"),
4844
+ href: item.url,
4845
+ children: item.title
4846
+ });
4847
+ /** TOC list with a straight border line. */
4848
+ const TOCItems = ({ ref, className, emptyText = "No Headings", ...props }) => {
4849
+ const containerRef = useRef(null);
4850
+ const items = useTOCItems();
4851
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
4852
+ className: EMPTY_CLS,
4853
+ children: emptyText
4854
+ });
4855
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(TocThumb, {
4856
+ className: "bg-primary absolute top-(--toc-top) h-(--toc-height) w-px transition-all",
4857
+ containerRef
4858
+ }), /* @__PURE__ */ jsx("div", {
4859
+ className: cn("border-foreground/10 flex flex-col border-s", className),
4860
+ ref: mergeRefs(ref, containerRef),
4861
+ ...props,
4862
+ children: items.map((item) => /* @__PURE__ */ jsx(SimpleTOCItem, { item }, item.url))
4863
+ })] });
4864
+ };
4865
+ const getItemOffset = (depth) => {
4866
+ if (depth <= 2) return 14;
4867
+ return depth === 3 ? 26 : 36;
4868
+ };
4869
+ const getLineOffset = (depth) => depth >= 3 ? 10 : 0;
4870
+ /** One vertical segment per entry, shifted right for nested headings. */
4871
+ const clerkLine = (container, items) => {
4872
+ let width = 0;
4873
+ let height = 0;
4874
+ const d = [];
4875
+ for (const [i, item] of items.entries()) {
4876
+ const element = container.querySelector(`a[href="${CSS.escape(item.url)}"]`);
4877
+ if (element === null) continue;
4878
+ const styles = getComputedStyle(element);
4879
+ const offset = getLineOffset(item.depth) + 1;
4880
+ const top = element.offsetTop + Number.parseFloat(styles.paddingTop);
4881
+ const bottom = element.offsetTop + element.clientHeight - Number.parseFloat(styles.paddingBottom);
4882
+ width = Math.max(offset, width);
4883
+ height = Math.max(height, bottom);
4884
+ d.push(`${i === 0 ? "M" : "L"}${offset} ${top}`, `L${offset} ${bottom}`);
4885
+ }
4886
+ return {
4887
+ height,
4888
+ path: d.join(" "),
4889
+ width: width + 1
4890
+ };
4891
+ };
4892
+ const ClerkTOCItemElement = ({ item, upper = item.depth, lower = item.depth }) => {
4893
+ const offset = getLineOffset(item.depth);
4894
+ const upperOffset = getLineOffset(upper);
4895
+ const lowerOffset = getLineOffset(lower);
4896
+ return /* @__PURE__ */ jsxs(TOCItem, {
4897
+ className: cn("relative", ITEM_CLS),
4898
+ href: item.url,
4899
+ style: { paddingInlineStart: getItemOffset(item.depth) },
4900
+ children: [
4901
+ offset === upperOffset ? null : /* @__PURE__ */ jsx("svg", {
4902
+ "aria-hidden": "true",
4903
+ className: "absolute start-0 -top-1.5 size-4 rtl:-scale-x-100",
4904
+ viewBox: "0 0 16 16",
4905
+ xmlns: "http://www.w3.org/2000/svg",
4906
+ children: /* @__PURE__ */ jsx("line", {
4907
+ className: "stroke-foreground/10",
4908
+ strokeWidth: "1",
4909
+ x1: upperOffset,
4910
+ x2: offset,
4911
+ y1: "0",
4912
+ y2: "12"
4913
+ })
4914
+ }),
4915
+ /* @__PURE__ */ jsx("div", {
4916
+ className: cn("bg-foreground/10 absolute inset-y-0 w-px", offset !== upperOffset && "top-1.5", offset !== lowerOffset && "bottom-1.5"),
4917
+ style: { insetInlineStart: offset }
4918
+ }),
4919
+ item.title
4920
+ ]
4921
+ });
4922
+ };
4923
+ /** TOC list whose line bends to follow the heading hierarchy. */
4924
+ const ClerkTOCItems = ({ ref, className, emptyText = "No Headings", ...props }) => {
4925
+ const containerRef = useRef(null);
4926
+ const items = useTOCItems();
4927
+ const [svg, setSvg] = useState();
4928
+ useEffect(() => {
4929
+ const container = containerRef.current;
4930
+ if (container === null) return;
4931
+ const onResize = () => {
4932
+ if (container.clientHeight > 0) setSvg(clerkLine(container, items));
4933
+ };
4934
+ const observer = new ResizeObserver(onResize);
4935
+ onResize();
4936
+ observer.observe(container);
4937
+ return () => {
4938
+ observer.disconnect();
4939
+ };
4940
+ }, [items]);
4941
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
4942
+ className: EMPTY_CLS,
4943
+ children: emptyText
4944
+ });
4945
+ const mask = svg && {
4946
+ height: svg.height,
4947
+ maskImage: `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`)}")`,
4948
+ width: svg.width
4949
+ };
4950
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [mask ? /* @__PURE__ */ jsx("div", {
4951
+ className: "absolute start-0 top-0 rtl:-scale-x-100",
4952
+ style: mask,
4953
+ children: /* @__PURE__ */ jsx(TocThumb, {
4954
+ className: "bg-primary mt-(--toc-top) h-(--toc-height) transition-all",
4955
+ containerRef
4956
+ })
4957
+ }) : null, /* @__PURE__ */ jsx("div", {
4958
+ className: cn("flex flex-col", className),
4959
+ ref: mergeRefs(containerRef, ref),
4960
+ ...props,
4961
+ children: items.map((item, i) => /* @__PURE__ */ jsx(ClerkTOCItemElement, {
4962
+ item,
4963
+ lower: items[i + 1]?.depth,
4964
+ upper: items[i - 1]?.depth
4965
+ }, item.url))
4966
+ })] });
4967
+ };
4968
+ const PageTOC = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
4969
+ className: cn("flex flex-col gap-3", className),
4970
+ ...props
4971
+ });
4972
+ const PageTOCItems = ({ variant = "default", ...props }) => /* @__PURE__ */ jsx(TOCScrollArea, { children: variant === "clerk" ? /* @__PURE__ */ jsx(ClerkTOCItems, { ...props }) : /* @__PURE__ */ jsx(TOCItems, { ...props }) });
4973
+ //#endregion
4974
+ //#region src/ui/page-toc.tsx
4975
+ /** `<aside>` holding the sidebar and the JSON island with its headings. */
4976
+ const PAGE_TOC_ID = "mdxr-page-toc";
4977
+ const PAGE_TOC_DATA_ID = "mdxr-page-toc-data";
4978
+ /** Heading levels listed in the sidebar (h2–h3, like `:::toc`). */
4979
+ const MIN_DEPTH = 2;
4980
+ const MAX_DEPTH = 3;
4981
+ /** A single entry isn't worth a sidebar. */
4982
+ const MIN_ENTRIES = 2;
4983
+ /** The headings the sidebar lists — empty when the page gets no sidebar. */
4984
+ const pageTocHeadings = (headings) => {
4985
+ const items = headings.filter((h) => h.depth >= MIN_DEPTH && h.depth <= MAX_DEPTH);
4986
+ return items.length < MIN_ENTRIES ? [] : items;
4987
+ };
4988
+ /**
4989
+ * Page-level "On this page" sidebar built on the Crux UI ToC: a clerk-style
4990
+ * outline whose thumb follows the headings currently on screen. Rendered next
4991
+ * to `<main>` by `htmlDocument` (not part of the MDX tree) and hydrated by its
4992
+ * own root, so it works with or without the document hydration bundle.
4993
+ */
4994
+ const PageToc = ({ headings }) => {
4995
+ const toc = useMemo(() => headings.map((h) => ({
4996
+ depth: h.depth,
4997
+ title: h.text,
4998
+ url: `#${h.slug}`
4999
+ })), [headings]);
5000
+ return /* @__PURE__ */ jsx(TOCProvider, {
5001
+ toc,
5002
+ children: /* @__PURE__ */ jsxs(PageTOC, {
5003
+ className: "min-h-0",
5004
+ children: [/* @__PURE__ */ jsx("p", {
5005
+ className: "text-muted-foreground m-0 text-xs font-semibold",
5006
+ children: "On this page"
5007
+ }), /* @__PURE__ */ jsx(PageTOCItems, { variant: "clerk" })]
5008
+ })
5009
+ });
5010
+ };
5011
+ //#endregion
4580
5012
  //#region src/html.ts
4581
5013
  /**
4582
5014
  * JS destined for an inline `<script>` element: neutralize the two byte
@@ -4612,6 +5044,10 @@ const iconSvg = (name) => {
4612
5044
  * `handleDocEvent`); THEME_JS restores the stored choice before paint.
4613
5045
  */
4614
5046
  const THEME_TOGGLE_HTML = `<button type="button" class="mdxr-theme" data-mdxr-theme data-mode="auto" title="Theme: auto" aria-label="Switch theme (current: auto)"><span class="mdxr-theme-i mdxr-theme-i-auto">${iconSvg("sun-moon")}</span><span class="mdxr-theme-i mdxr-theme-i-light">${iconSvg("sun")}</span><span class="mdxr-theme-i mdxr-theme-i-dark">${iconSvg("moon")}</span></button>`;
5047
+ /** JSON for an inline `<script type="application/json">` island. */
5048
+ const jsonIsland = (data) => JSON.stringify(data).replaceAll("<", String.raw`\u003c`);
5049
+ const pageTocHtml = (toc) => toc === void 0 ? "" : `<aside id="${PAGE_TOC_ID}" aria-label="On this page" class="fixed top-20 left-[calc(50%+25.5rem)] hidden max-h-[calc(100vh-7rem)] w-52 flex-col xl:flex print:hidden">${toc.html}</aside>
5050
+ <script type="application/json" id="${PAGE_TOC_DATA_ID}">${jsonIsland(toc.headings)}<\/script>`;
4615
5051
  const htmlDocument = (o) => `<!doctype html>
4616
5052
  <html lang="en">
4617
5053
  <head>
@@ -4626,10 +5062,12 @@ ${o.needsKatex === true ? `<link rel="stylesheet" href="${KATEX_CDN_URL}">` : ""
4626
5062
  <body class="bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
4627
5063
  ${THEME_TOGGLE_HTML}
4628
5064
  <main id="mdxr-root" class="prose prose-neutral dark:prose-invert mx-auto max-w-3xl px-6 py-10">${o.body}</main>
5065
+ ${pageTocHtml(o.pageToc)}
4629
5066
  <script>${inlineScript(o.clientJs)}<\/script>
4630
5067
  ${o.needsMermaid ? `<script type="module">${inlineScript(MERMAID_JS)}<\/script>` : ""}
4631
5068
  ${o.liveReload === true ? `<script>${inlineScript(LIVE_RELOAD_JS)}<\/script>` : ""}
4632
5069
  ${o.hydrateJs === void 0 ? "" : `<script>${inlineScript(o.hydrateJs)}<\/script>`}
5070
+ ${o.pageTocJs === void 0 ? "" : `<script>${inlineScript(o.pageTocJs)}<\/script>`}
4633
5071
  </body>
4634
5072
  </html>
4635
5073
  `;
@@ -4883,14 +5321,16 @@ const iconSets = (usedIcons) => ({
4883
5321
  */
4884
5322
  const scanVProps = (stripped, alias) => {
4885
5323
  const esc = alias.replaceAll(/[$()*+.?[\\\]^{|}]/gu, "\\$&");
4886
- const access = new RegExp(`\\b${esc}\\s*\\?\\.\\s*(\\w+)|\\b${esc}\\s*\\.\\s*(\\w+)`, "gu");
5324
+ const identifierPart = String.raw`[\p{ID_Continue}$\u200C\u200D]`;
5325
+ const start = `(?<!${identifierPart})${esc}`;
5326
+ const access = new RegExp(`${start}\\s*\\??\\.\\s*(\\w+)`, "gu");
4887
5327
  const props = /* @__PURE__ */ new Set();
4888
5328
  for (const use of stripped.matchAll(access)) {
4889
- const prop = use[1] ?? use[2];
5329
+ const [, prop] = use;
4890
5330
  if (prop !== void 0) props.add(prop);
4891
5331
  }
4892
5332
  const leftover = stripped.replaceAll(access, "");
4893
- return new RegExp(`\\b${esc}\\b`, "u").test(leftover) ? "all" : props;
5333
+ return new RegExp(`${start}(?!${identifierPart})`, "u").test(leftover) ? "all" : props;
4894
5334
  };
4895
5335
  /**
4896
5336
  * `/* … *\/` and `// …` are legal inside a multiline import clause, and a
@@ -4988,7 +5428,8 @@ const scanMdxrImports = async (importer) => {
4988
5428
  const EXTRA_MODULES = {
4989
5429
  builtinComponents: path.join(srcDir, "ui/index.js"),
4990
5430
  defineConfig: path.join(srcDir, "config.js"),
4991
- mountDocument: path.join(srcDir, "hydrate-runtime.js")
5431
+ mountDocument: path.join(srcDir, "hydrate-runtime.js"),
5432
+ mountPageToc: path.join(srcDir, "page-toc-runtime.js")
4992
5433
  };
4993
5434
  /**
4994
5435
  * The virtual module's `v` export. `export * as v` would materialize the
@@ -5086,6 +5527,7 @@ const buildHydrateScript = async (spec) => {
5086
5527
  };
5087
5528
  const mountDocument = bind("mountDocument");
5088
5529
  const planHeader = spec.header === void 0 ? "undefined" : bind("PlanHeader");
5530
+ const mountPageToc = spec.pageToc === true ? `${bind("mountPageToc")}();` : "";
5089
5531
  for (const name of spec.usedComponents) {
5090
5532
  if (!Object.hasOwn(builtinComponents, name)) continue;
5091
5533
  const exported = name === "pre" ? "Pre" : name;
@@ -5104,6 +5546,7 @@ ${mountDocument}({
5104
5546
  planHeader: ${planHeader},
5105
5547
  userModule: ${spec.componentsPath === void 0 ? "undefined" : "__mdxrUser"},
5106
5548
  });
5549
+ ${mountPageToc}
5107
5550
  `;
5108
5551
  const result = await build({
5109
5552
  absWorkingDir: pkgRoot,
@@ -5605,6 +6048,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
5605
6048
  context,
5606
6049
  fileLinks: Object.fromEntries(fileLinks),
5607
6050
  frontmatter,
6051
+ headings: compiled.data.mdxrHeadings ?? [],
5608
6052
  hydrated,
5609
6053
  renderWithHeader: (header) => {
5610
6054
  try {
@@ -5624,6 +6068,44 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
5624
6068
  };
5625
6069
  };
5626
6070
  //#endregion
6071
+ //#region src/page-toc-js.ts
6072
+ let cache;
6073
+ const buildPageTocJs = async () => {
6074
+ return (await build({
6075
+ absWorkingDir: pkgRoot,
6076
+ bundle: true,
6077
+ define: { "process.env.NODE_ENV": "\"production\"" },
6078
+ format: "iife",
6079
+ jsx: "automatic",
6080
+ jsxImportSource: "react",
6081
+ logLevel: "silent",
6082
+ minify: true,
6083
+ platform: "browser",
6084
+ stdin: {
6085
+ contents: "import { mountPageToc } from \"./page-toc-runtime.js\"; mountPageToc();",
6086
+ loader: "js",
6087
+ resolveDir: path.join(pkgRoot, "src"),
6088
+ sourcefile: "mdxr-page-toc.js"
6089
+ },
6090
+ target: "es2022",
6091
+ write: false
6092
+ })).outputFiles[0]?.text ?? "";
6093
+ };
6094
+ /**
6095
+ * Standalone bundle that hydrates the sidebar ToC — used only when the
6096
+ * document has no hydration bundle of its own (that one mounts the ToC with
6097
+ * the React copy it already ships). Memoized: identical for every render.
6098
+ */
6099
+ const pageTocJs = async () => {
6100
+ cache ??= buildPageTocJs();
6101
+ try {
6102
+ return await cache;
6103
+ } catch (error) {
6104
+ cache = void 0;
6105
+ throw error;
6106
+ }
6107
+ };
6108
+ //#endregion
5627
6109
  //#region src/assets/css.ts
5628
6110
  /** Base CSS appended after Tailwind utilities (covers what utilities can't). */
5629
6111
  const BASE_CSS = `
@@ -6131,6 +6613,7 @@ const buildHydrateBundle = async (args) => {
6131
6613
  fileLinks: args.fileLinks,
6132
6614
  header: args.headerProps,
6133
6615
  now: args.now,
6616
+ pageToc: args.pageToc,
6134
6617
  usedComponents: args.usedComponents,
6135
6618
  usedIcons: args.usedIcons
6136
6619
  });
@@ -6167,6 +6650,23 @@ const frontmatterHeader = (fm, body) => {
6167
6650
  version: fm("version")
6168
6651
  };
6169
6652
  };
6653
+ /** Sidebar ToC entries — frontmatter `toc: false` opts the page out. */
6654
+ const sidebarHeadings = (frontmatter, headings) => frontmatter.toc === false || frontmatter.toc === "false" ? [] : pageTocHeadings(headings);
6655
+ /**
6656
+ * SSR markup for the sidebar ToC plus the script that hydrates it. The
6657
+ * document bundle mounts the ToC itself when there is one; otherwise the
6658
+ * standalone bundle does. Without JS the sidebar stays plain anchor links.
6659
+ */
6660
+ const renderPageToc = async (headings, interactive, hasDocumentBundle) => {
6661
+ if (headings.length === 0) return {};
6662
+ return {
6663
+ pageToc: {
6664
+ headings,
6665
+ html: (interactive ? renderToString : renderToStaticMarkup)(createElement(PageToc, { headings }))
6666
+ },
6667
+ pageTocJs: interactive && !hasDocumentBundle ? await pageTocJs() : void 0
6668
+ };
6669
+ };
6170
6670
  /** Render MDX source text to a standalone HTML document. */
6171
6671
  const render = async (source, opts = {}) => {
6172
6672
  const dir = path.resolve(opts.dir ?? process.cwd());
@@ -6179,7 +6679,7 @@ const render = async (source, opts = {}) => {
6179
6679
  ...builtinComponents,
6180
6680
  ...user.components
6181
6681
  };
6182
- const { body, code, fileLinks, frontmatter, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
6682
+ const { body, code, fileLinks, frontmatter, headings, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
6183
6683
  editor: config.editor,
6184
6684
  hydrate: opts.hydrate
6185
6685
  });
@@ -6228,6 +6728,7 @@ const render = async (source, opts = {}) => {
6228
6728
  ];
6229
6729
  const { css, dependencies } = await buildCss(sources, config.themePath);
6230
6730
  opts.onDependencies?.(dependencies);
6731
+ const tocHeadings = sidebarHeadings(frontmatter, headings);
6231
6732
  const [js, hydrateJs] = await Promise.all([clientJs(), buildHydrateBundle({
6232
6733
  code,
6233
6734
  config,
@@ -6235,9 +6736,11 @@ const render = async (source, opts = {}) => {
6235
6736
  headerProps,
6236
6737
  hydrate: opts.hydrate,
6237
6738
  now: renderedAt,
6739
+ pageToc: tocHeadings.length > 0,
6238
6740
  usedComponents,
6239
6741
  usedIcons: docIcons
6240
6742
  })]);
6743
+ const { pageToc, pageTocJs: tocJs } = await renderPageToc(tocHeadings, opts.hydrate ?? true, hydrateJs !== void 0);
6241
6744
  return htmlDocument({
6242
6745
  body: docBody,
6243
6746
  clientJs: js,
@@ -6246,6 +6749,8 @@ const render = async (source, opts = {}) => {
6246
6749
  liveReload: opts.liveReload,
6247
6750
  needsKatex: /class="[^"]*katex/u.test(docBody),
6248
6751
  needsMermaid: /class="[^"]*mermaid/u.test(docBody),
6752
+ pageToc,
6753
+ pageTocJs: tocJs,
6249
6754
  title
6250
6755
  });
6251
6756
  };