wiki-formant 0.13.0 → 0.15.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 (137) hide show
  1. package/README.md +12 -25
  2. package/bin/check-classes.mjs +2 -3
  3. package/dist/analytics.d.ts +0 -1
  4. package/dist/analytics.js +0 -1
  5. package/dist/block-views.d.ts +97 -0
  6. package/dist/block-views.js +119 -0
  7. package/dist/blocks.d.ts +0 -1
  8. package/dist/blocks.js +1 -2
  9. package/dist/combobox.d.ts +0 -1
  10. package/dist/combobox.js +0 -1
  11. package/dist/conformance.d.ts +0 -3
  12. package/dist/conformance.js +40 -38
  13. package/dist/crawlers.d.ts +0 -1
  14. package/dist/crawlers.js +0 -1
  15. package/dist/dom.d.ts +0 -1
  16. package/dist/dom.js +0 -1
  17. package/dist/editor.d.ts +101 -0
  18. package/dist/editor.js +218 -0
  19. package/dist/feed.d.ts +0 -1
  20. package/dist/feed.js +0 -1
  21. package/dist/freshness.d.ts +0 -1
  22. package/dist/freshness.js +2 -2
  23. package/dist/headings.d.ts +0 -1
  24. package/dist/headings.js +1 -3
  25. package/dist/html.d.ts +16 -0
  26. package/dist/html.js +23 -0
  27. package/dist/http.d.ts +20 -1
  28. package/dist/http.js +23 -4
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +2 -2
  31. package/dist/license.d.ts +0 -1
  32. package/dist/license.js +0 -1
  33. package/dist/link-check.d.ts +92 -0
  34. package/dist/link-check.js +249 -0
  35. package/dist/links.d.ts +0 -1
  36. package/dist/links.js +1 -6
  37. package/dist/maps.d.ts +0 -1
  38. package/dist/maps.js +2 -2
  39. package/dist/markdown.d.ts +1 -1
  40. package/dist/markdown.js +34 -3
  41. package/dist/mcp.d.ts +17 -7
  42. package/dist/mcp.js +8 -8
  43. package/dist/pagination.d.ts +19 -1
  44. package/dist/pagination.js +21 -1
  45. package/dist/rate-limit.d.ts +18 -1
  46. package/dist/rate-limit.js +22 -1
  47. package/dist/react-server.d.ts +91 -9
  48. package/dist/react-server.js +68 -16
  49. package/dist/react.d.ts +37 -48
  50. package/dist/react.js +44 -23
  51. package/dist/revisions.d.ts +0 -1
  52. package/dist/revisions.js +0 -1
  53. package/dist/rola.d.ts +0 -1
  54. package/dist/rola.js +0 -1
  55. package/dist/seeded.d.ts +11 -0
  56. package/dist/seeded.js +32 -0
  57. package/dist/sidebar.d.ts +0 -1
  58. package/dist/sidebar.js +0 -1
  59. package/dist/taxonomy.d.ts +0 -1
  60. package/dist/taxonomy.js +0 -1
  61. package/dist/text.d.ts +14 -1
  62. package/dist/text.js +15 -2
  63. package/dist/tiptap.d.ts +1 -1
  64. package/dist/tiptap.js +1 -2
  65. package/dist/validation.d.ts +2 -3
  66. package/dist/validation.js +4 -5
  67. package/dist/versioning.d.ts +0 -1
  68. package/dist/versioning.js +0 -1
  69. package/dist/well-known.d.ts +19 -2
  70. package/dist/well-known.js +28 -2
  71. package/dist/x402.d.ts +2 -11
  72. package/dist/x402.js +4 -16
  73. package/package.json +52 -27
  74. package/dist/analytics.d.ts.map +0 -1
  75. package/dist/analytics.js.map +0 -1
  76. package/dist/blocks.d.ts.map +0 -1
  77. package/dist/blocks.js.map +0 -1
  78. package/dist/combobox.d.ts.map +0 -1
  79. package/dist/combobox.js.map +0 -1
  80. package/dist/conformance.d.ts.map +0 -1
  81. package/dist/conformance.js.map +0 -1
  82. package/dist/crawlers.d.ts.map +0 -1
  83. package/dist/crawlers.js.map +0 -1
  84. package/dist/dom.d.ts.map +0 -1
  85. package/dist/dom.js.map +0 -1
  86. package/dist/entities.d.ts +0 -3
  87. package/dist/entities.d.ts.map +0 -1
  88. package/dist/entities.js +0 -30
  89. package/dist/entities.js.map +0 -1
  90. package/dist/feed.d.ts.map +0 -1
  91. package/dist/feed.js.map +0 -1
  92. package/dist/freshness.d.ts.map +0 -1
  93. package/dist/freshness.js.map +0 -1
  94. package/dist/headings.d.ts.map +0 -1
  95. package/dist/headings.js.map +0 -1
  96. package/dist/http.d.ts.map +0 -1
  97. package/dist/http.js.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.js.map +0 -1
  100. package/dist/license.d.ts.map +0 -1
  101. package/dist/license.js.map +0 -1
  102. package/dist/links.d.ts.map +0 -1
  103. package/dist/links.js.map +0 -1
  104. package/dist/maps.d.ts.map +0 -1
  105. package/dist/maps.js.map +0 -1
  106. package/dist/markdown.d.ts.map +0 -1
  107. package/dist/markdown.js.map +0 -1
  108. package/dist/mcp.d.ts.map +0 -1
  109. package/dist/mcp.js.map +0 -1
  110. package/dist/pagination.d.ts.map +0 -1
  111. package/dist/pagination.js.map +0 -1
  112. package/dist/rate-limit.d.ts.map +0 -1
  113. package/dist/rate-limit.js.map +0 -1
  114. package/dist/react-server.d.ts.map +0 -1
  115. package/dist/react-server.js.map +0 -1
  116. package/dist/react.d.ts.map +0 -1
  117. package/dist/react.js.map +0 -1
  118. package/dist/revisions.d.ts.map +0 -1
  119. package/dist/revisions.js.map +0 -1
  120. package/dist/rola.d.ts.map +0 -1
  121. package/dist/rola.js.map +0 -1
  122. package/dist/sidebar.d.ts.map +0 -1
  123. package/dist/sidebar.js.map +0 -1
  124. package/dist/taxonomy.d.ts.map +0 -1
  125. package/dist/taxonomy.js.map +0 -1
  126. package/dist/text.d.ts.map +0 -1
  127. package/dist/text.js.map +0 -1
  128. package/dist/tiptap.d.ts.map +0 -1
  129. package/dist/tiptap.js.map +0 -1
  130. package/dist/validation.d.ts.map +0 -1
  131. package/dist/validation.js.map +0 -1
  132. package/dist/versioning.d.ts.map +0 -1
  133. package/dist/versioning.js.map +0 -1
  134. package/dist/well-known.d.ts.map +0 -1
  135. package/dist/well-known.js.map +0 -1
  136. package/dist/x402.d.ts.map +0 -1
  137. package/dist/x402.js.map +0 -1
package/README.md CHANGED
@@ -264,7 +264,9 @@ are unit-tested without a DOM.
264
264
 
265
265
  `FacetBar` renders the rows `createTaxonomy` already produces. This is why the taxonomy exports a rows model rather than markup: the rows could always cross the boundary and, until this subpath existed, the markup could not, so all three wikis hand-rendered it and two put `aria-pressed` on an `<a>`. `Breadcrumbs` renders the trail and its `BreadcrumbList` JSON-LD together, because a trail whose structured data is written somewhere else is a trail that will one day disagree with its own markup — which is the case Google penalises. It takes a `base` origin: structured-data URLs must be absolute and a package cannot know the site.
266
266
 
267
- `WikiRail` stays in `wiki-formant/react`, because it calls `useSidebar` and genuinely is a client component. It renders the whole `wiki-rail__*` tree — scroll wrapper, both labelled `<nav>`s, the table of contents between them — where three wikis had drifted on which of those they had.
267
+ `PageNav` is the previous/next pair at the foot of an article — the sequential read the infobox rail's lateral links do not cover. Ordering is the caller's, because it is the one part that is never portable: a wiki's sequence is its section's configured sort, a knowledge base's is a taxonomy walk. Pair it with `adjacentPages` from `wiki-formant/pagination` over a list you already hold — neither wiki needs a query for it, and the two indexed lookups the neighbours used to cost were the reason one of them dropped the control.
268
+
269
+ `RailShell` stays in `wiki-formant/react`, because it calls `useSidebar` and genuinely is a client component. It renders the rail's landmark, its scroll wrapper and the three collapse states — including the `--instant` class that keeps a remembered-closed rail from animating shut on first paint, and the close-on-tap that a mobile rail needs. Compose your own rail inside it and mark the active link with `isRailLinkActive`; all three wikis do, and the pre-composed component that used to sit here had one consumer and had already lost both of those behaviours.
268
270
 
269
271
  All three take the router's link component as a prop:
270
272
 
@@ -419,30 +421,15 @@ they name a gap in the corpus in the reader's own words.
419
421
 
420
422
  ## API
421
423
 
422
- | Export | From |
423
- |---|---|
424
- | `createTaxonomy`, `defaultHref`, `firstLetter`, `toggleFilter` | `wiki-formant/taxonomy` |
425
- | `injectHeadingIds`, `headingsFrom`, `slugifyHeading` | `wiki-formant/headings` |
426
- | `mcpResponse`, `mcpGet`, `mcpOptions`, `handleMcp`, `withMcpCors`, `McpToolError`, `MCP_CORS`, `MCP_PROTOCOL_VERSION` | `wiki-formant/mcp` |
427
- | `htmlToMarkdown`, `inlineToMarkdown`, `tableToMarkdown`, `frontmatter`, `markdownDocument`, `decodeEntities` | `wiki-formant/markdown` |
428
- | `corpusEtag`, `notModified`, `textHeaders`, `markdownHeaders`, `descriptorHeaders`, `descriptorResponse`, `cleanSnippet`, `pageLine` | `wiki-formant/http` |
429
- | `parsePagination`, `paginatedResponse`, `toOffset` | `wiki-formant/pagination` |
430
- | `mapBlockTree`, `mapBlockTreeAsync`, `someBlock`, `renderBlockTree` | `wiki-formant/blocks` |
431
- | `parseVersion`, `formatVersion`, `incrementVersion`, `bump`, `compareVersions` | `wiki-formant/versioning` |
432
- | `plausibleEvent`, `mcpCallProps`, `searchQueryProps`, `plausibleDomain` | `wiki-formant/analytics` |
433
- | `comboboxAria`, `listId`, `optionId` | `wiki-formant/combobox` |
434
- | `computeRevisionDiff`, `diffBlocks`, `extractBlocks`, `classifyChanges`, `changeSummary` | `wiki-formant/revisions` |
435
- | `renderFeed`, `renderItem`, `escXml`, `cdata`, `clampWords`, `absolutise`, `FEED_HEADERS` | `wiki-formant/feed` |
436
- | `ccBy40`, `licenseBlock`, `licenseLines`, `licenseNote` | `wiki-formant/license` |
437
- | `AI_CRAWLERS`, `detectAiBot`, `aiCrawlerTokens`, `aiCrawlerRules` | `wiki-formant/crawlers` |
438
- | `useCollapsibleSidebar`, `SidebarProvider`, `useSidebar`, `TableOfContents`, `useTypeahead`, `useLinkPreview`, `useClickOutside`, `useTableSort`, `useCopy`, `ErrorBoundary`, `WikiRail` | `wiki-formant/react` |
439
- | `FacetBar`, `Breadcrumbs` | `wiki-formant/react-server` |
440
- | `gatePaidCalls` | `wiki-formant/x402` |
441
- | `resolveSidebarOpen`, `sidebarBootScript`, `SIDEBAR_ATTRIBUTE` | `wiki-formant/sidebar` |
442
- | `addCopyButtons`, `activateTabGroups`, `tweetEmbedSrc`, `onTweetResize`, `hydrateTweetEmbeds`, `sizeTweetEmbeds`, `TWITTER_ORIGIN` | `wiki-formant/dom` |
443
- | `Iframe`, `YouTube`, `TwitterEmbed`, `createMapEmbed`, `createCodeBlock`, `createTabs` | `wiki-formant/tiptap` |
444
-
445
- Everything above `wiki-formant/react` is also re-exported from the package root. The React, sidebar, DOM, tiptap, react-server and x402 subpaths are not: they carry `'use client'`, reach for a browser global, or need a peer the root must not assume, and the root has to stay importable from a route handler.
424
+ Every module has a subpath — `wiki-formant/taxonomy`, `wiki-formant/mcp`, and so on.
425
+ The emitted `.d.ts` files are the reference. There is no hand-maintained symbol list
426
+ here, because the one that used to be here drifted from them.
427
+
428
+ The package root re-exports the runtime modules that need no peer dependency and no
429
+ client boundary, so `import … from 'wiki-formant'` stays importable from a route
430
+ handler with nothing else installed. Everything that reaches for React, a browser
431
+ global, tiptap or a wallet — and the tooling modules, which no route imports — is
432
+ subpath-only.
446
433
 
447
434
  ## A bin
448
435
 
@@ -113,7 +113,6 @@ for (const [tok, at] of [...dead].sort((a, b) => b[1].length - a[1].length)) {
113
113
  console.error(` ${tok.padEnd(24)} ${String(at.length).padStart(3)}x ${at.slice(0, 4).join(', ')}${at.length > 4 ? ', …' : ''}`);
114
114
  }
115
115
  // Derived, not hardcoded: this runs in every sibling project and their
116
- // globals.css does not sit at the same path.
117
- const globals = walk(SRC_DIR, '.css').find(f => f.endsWith('globals.css')) ?? 'your globals.css';
118
- console.error(`\nEither define it in ${globals} or delete the usage.`);
116
+ // globals.css does not sit at the same path. Found once, above.
117
+ console.error(`\nEither define it in ${globalsFile ?? 'your globals.css'} or delete the usage.`);
119
118
  process.exit(WARN_ONLY ? 0 : 1);
@@ -62,4 +62,3 @@ export interface SearchQueryInput {
62
62
  export declare function searchQueryProps({ query, results, surface, }: SearchQueryInput): Record<string, string> | null;
63
63
  /** The hostname of a site URL, for the `domain` a Plausible property is filed under. */
64
64
  export declare function plausibleDomain(siteUrl: string | undefined, fallback: string): string;
65
- //# sourceMappingURL=analytics.d.ts.map
package/dist/analytics.js CHANGED
@@ -88,4 +88,3 @@ export function plausibleDomain(siteUrl, fallback) {
88
88
  return fallback.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
89
89
  }
90
90
  }
91
- //# sourceMappingURL=analytics.js.map
@@ -0,0 +1,97 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { CodeTab, LinkGridGroup, ReferenceItem, StatItem } from './blocks.js';
3
+ import type { WikiLinkComponent } from './react-server.js';
4
+ /**
5
+ * Tabbed code samples. Every tab's body stays mounted and the inactive ones are
6
+ * hidden rather than unmounted: the panels carry pre-highlighted markup, and
7
+ * remounting one would re-run whatever the consumer's highlighter attached to
8
+ * it on every tab press.
9
+ */
10
+ export declare function CodeTabsView({ tabs }: {
11
+ tabs: readonly CodeTab[];
12
+ }): import("react").JSX.Element | null;
13
+ export interface ColumnsViewProps<B extends {
14
+ id: string;
15
+ }> {
16
+ columns: ReadonlyArray<{
17
+ id: string;
18
+ blocks?: readonly B[];
19
+ }>;
20
+ gap?: 'sm' | 'md' | 'lg';
21
+ align?: 'start' | 'center' | 'end' | 'stretch';
22
+ /** The caller's own dispatch, because the block union is the caller's. */
23
+ render: (block: B) => ReactNode;
24
+ }
25
+ /**
26
+ * A row of columns, each holding blocks the caller renders.
27
+ *
28
+ * Children are wrapped in a `Fragment`, not a `<div>`. One repo used a div and
29
+ * it was a layout bug rather than a style: `.column-view` is the flow context
30
+ * its stylesheet spaces children in, and an extra element between them means
31
+ * every gap rule inside a column silently matches nothing.
32
+ */
33
+ export declare function ColumnsView<B extends {
34
+ id: string;
35
+ }>({ columns, gap, align, render, }: ColumnsViewProps<B>): import("react").JSX.Element;
36
+ /**
37
+ * Banded groups of link pills.
38
+ *
39
+ * A link whose href fails `safeLinkHref` is DROPPED, not rendered inert. These
40
+ * hrefs are author-supplied, and a `javascript:` URL fails the http test above
41
+ * — so without the guard it falls through to the router's link component, which
42
+ * renders it looking like an ordinary pill and executes it on click.
43
+ */
44
+ export declare function LinkGridView({ groups, intro, link: Link, }: {
45
+ groups?: readonly (LinkGridGroup & {
46
+ id?: string;
47
+ })[];
48
+ intro?: string;
49
+ link?: WikiLinkComponent;
50
+ }): import("react").JSX.Element;
51
+ /**
52
+ * The numbered citation list, with the `#cite-n` back-links the inline markers
53
+ * point at. `processHtml` is a prop because only one wiki runs reference text
54
+ * through its HTML pipeline, and running it in the other would rewrite anchors
55
+ * that were minted under different rules.
56
+ */
57
+ export declare function ReferencesView({ items, title, processHtml, }: {
58
+ items?: readonly (ReferenceItem & {
59
+ id?: string;
60
+ })[];
61
+ title?: string;
62
+ processHtml?: (html: string) => string;
63
+ }): import("react").JSX.Element | null;
64
+ /**
65
+ * A row of measured figures.
66
+ *
67
+ * The column count is a class (`stat-grid-4`), never an inline
68
+ * `gridTemplateColumns`. One repo did it inline, which put a layout decision
69
+ * outside the stylesheet that owns every other one — invisible to the dead-class
70
+ * check, unreachable from a media query, and so a four-up row that could not
71
+ * become a two-up row on a phone.
72
+ */
73
+ export declare function StatsView({ items, columns, }: {
74
+ items?: readonly (StatItem & {
75
+ id?: string;
76
+ })[];
77
+ columns?: number;
78
+ }): import("react").JSX.Element | null;
79
+ /**
80
+ * A maintenance notice.
81
+ *
82
+ * The label and the fallback message arrive as `meta` because they are this
83
+ * wiki's editorial voice — "You can help RADIX Wiki by expanding it" has a name
84
+ * in it. The markup, the `role="note"` and the variant class are shared, and
85
+ * were identical in both repos. `icon` is optional: one wiki sets one, and a
86
+ * component library that hardcoded an icon set would make its consumers install
87
+ * that icon set.
88
+ */
89
+ export declare function BannerView({ variant, text, meta, icon, }: {
90
+ variant: string;
91
+ text?: string | null;
92
+ meta: {
93
+ label: string;
94
+ message: string;
95
+ };
96
+ icon?: ReactNode;
97
+ }): import("react").JSX.Element;
@@ -0,0 +1,119 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // block-views.tsx — the block renderers both wikis had written twice.
4
+ //
5
+ // Behind its own subpath for the reason `react.tsx` is: React is an OPTIONAL
6
+ // peer, so a consumer that only wants the taxonomy or the MCP transport still
7
+ // installs a package with no runtime dependencies.
8
+ //
9
+ // WHY THESE FIVE AND NOT THE SWITCH. The block union is per-repo and staying
10
+ // that way (see the Block Model note in the workspace CLAUDE.md) — a closed
11
+ // union is what makes `switch (block.type)` exhaustive, so a new block type is
12
+ // a compile error rather than a silent blank. What is NOT per-repo is what a
13
+ // codeTabs or a linkGrid LOOKS like once you have dispatched to it: those were
14
+ // byte-identical in both repos, down to the class names. So the dispatch stays
15
+ // with the caller and the leaves move here.
16
+ //
17
+ // The class names are NOT props. Like the rail's `wiki-rail__*` tree they are
18
+ // the shared convention both stylesheets already implement, and making them
19
+ // configurable would only let that convention fork again. Everything that
20
+ // genuinely differs — the prose of a banner, whether references run through an
21
+ // HTML processor, the router's link — arrives as a prop.
22
+ import { Fragment, useState } from 'react';
23
+ import { Anchor } from './react-server.js';
24
+ import { safeLinkHref } from './validation.js';
25
+ import { cx } from './html.js';
26
+ // ---- codeTabs ---------------------------------------------------------------
27
+ /**
28
+ * Tabbed code samples. Every tab's body stays mounted and the inactive ones are
29
+ * hidden rather than unmounted: the panels carry pre-highlighted markup, and
30
+ * remounting one would re-run whatever the consumer's highlighter attached to
31
+ * it on every tab press.
32
+ */
33
+ export function CodeTabsView({ tabs }) {
34
+ const [activeTab, setActiveTab] = useState(0);
35
+ if (!tabs.length)
36
+ return null;
37
+ return (_jsxs("div", { className: "code-tabs", children: [_jsx("div", { className: "code-tabs-list", children: tabs.map((tab, i) => (_jsx("button", { type: "button", className: cx('code-tabs-btn', i === activeTab && 'code-tabs-btn-active'), onClick: () => setActiveTab(i), children: tab.label }, i))) }), tabs.map((tab, i) => (_jsx("div", { className: i === activeTab ? 'block' : 'hidden', dangerouslySetInnerHTML: { __html: tab.code } }, i)))] }));
38
+ }
39
+ /**
40
+ * A row of columns, each holding blocks the caller renders.
41
+ *
42
+ * Children are wrapped in a `Fragment`, not a `<div>`. One repo used a div and
43
+ * it was a layout bug rather than a style: `.column-view` is the flow context
44
+ * its stylesheet spaces children in, and an extra element between them means
45
+ * every gap rule inside a column silently matches nothing.
46
+ */
47
+ export function ColumnsView({ columns, gap = 'md', align = 'start', render, }) {
48
+ const gapClass = { sm: 'gap-2', md: 'gap-4', lg: 'gap-6' }[gap];
49
+ const alignClass = {
50
+ start: 'items-start',
51
+ center: 'items-center',
52
+ end: 'items-end',
53
+ stretch: 'items-stretch',
54
+ }[align];
55
+ return (_jsx("div", { className: cx('columns-layout', gapClass, alignClass), children: columns.map(col => (_jsx("div", { className: "column-view", children: (col.blocks ?? []).map(bl => (_jsx(Fragment, { children: render(bl) }, bl.id))) }, col.id))) }));
56
+ }
57
+ // ---- linkGrid ---------------------------------------------------------------
58
+ /**
59
+ * Banded groups of link pills.
60
+ *
61
+ * A link whose href fails `safeLinkHref` is DROPPED, not rendered inert. These
62
+ * hrefs are author-supplied, and a `javascript:` URL fails the http test above
63
+ * — so without the guard it falls through to the router's link component, which
64
+ * renders it looking like an ordinary pill and executes it on click.
65
+ */
66
+ export function LinkGridView({ groups, intro, link: Link = Anchor, }) {
67
+ return (_jsxs("div", { className: "link-grid", children: [intro && _jsx("p", { children: intro }), (groups ?? []).map((group, gi) => (_jsxs("section", { className: "link-grid-group", children: [_jsx("h3", { children: group.heading }), group.description && (_jsx("div", { className: "link-grid-group-description", dangerouslySetInnerHTML: { __html: group.description } })), _jsx("div", { className: "link-grid-pills", children: (group.links ?? []).map((item, i) => {
68
+ const href = safeLinkHref(item.href);
69
+ if (!href)
70
+ return null;
71
+ return /^https?:\/\//.test(href) ? (_jsx("a", { href: href, target: "_blank", rel: "noopener", children: item.label }, i)) : (_jsx(Link, { href: href, children: item.label }, i));
72
+ }) })] }, group.id ?? gi)))] }));
73
+ }
74
+ // ---- references -------------------------------------------------------------
75
+ /**
76
+ * The numbered citation list, with the `#cite-n` back-links the inline markers
77
+ * point at. `processHtml` is a prop because only one wiki runs reference text
78
+ * through its HTML pipeline, and running it in the other would rewrite anchors
79
+ * that were minted under different rules.
80
+ */
81
+ export function ReferencesView({ items, title = 'References', processHtml = (html) => html, }) {
82
+ const rows = items ?? [];
83
+ if (!rows.length)
84
+ return null;
85
+ return (_jsxs("section", { className: "references-block", "aria-labelledby": "references-heading", children: [_jsx("h2", { id: "references-heading", children: title }), _jsx("ol", { className: "references-list", children: rows.map((item, i) => {
86
+ const url = safeLinkHref(item.url);
87
+ return (_jsxs("li", { id: `ref-${i + 1}`, className: "reference-item", children: [_jsx("a", { href: `#cite-${i + 1}`, className: "ref-backlink", "aria-label": "Back to citation", children: "\u2191" }), ' ', _jsx("span", { dangerouslySetInnerHTML: { __html: processHtml(item.text) } }), url && (_jsxs(_Fragment, { children: [' ', _jsx("a", { href: url, target: "_blank", rel: "noopener", className: "reference-link", "aria-label": "Open source", children: "\u2197" })] }))] }, item.id ?? i));
88
+ }) })] }));
89
+ }
90
+ // ---- stats ------------------------------------------------------------------
91
+ /**
92
+ * A row of measured figures.
93
+ *
94
+ * The column count is a class (`stat-grid-4`), never an inline
95
+ * `gridTemplateColumns`. One repo did it inline, which put a layout decision
96
+ * outside the stylesheet that owns every other one — invisible to the dead-class
97
+ * check, unreachable from a media query, and so a four-up row that could not
98
+ * become a two-up row on a phone.
99
+ */
100
+ export function StatsView({ items, columns = 4, }) {
101
+ const rows = items ?? [];
102
+ if (!rows.length)
103
+ return null;
104
+ return (_jsx("div", { className: cx('stat-grid', `stat-grid-${columns}`), children: rows.map((item, i) => (_jsxs("div", { className: "stat-card", children: [_jsxs("span", { className: "stat-value", children: [item.value, item.suffix && _jsx("span", { className: "stat-suffix", children: item.suffix })] }), _jsx("span", { className: "stat-label", children: item.label })] }, item.id ?? i))) }));
105
+ }
106
+ // ---- banner -----------------------------------------------------------------
107
+ /**
108
+ * A maintenance notice.
109
+ *
110
+ * The label and the fallback message arrive as `meta` because they are this
111
+ * wiki's editorial voice — "You can help RADIX Wiki by expanding it" has a name
112
+ * in it. The markup, the `role="note"` and the variant class are shared, and
113
+ * were identical in both repos. `icon` is optional: one wiki sets one, and a
114
+ * component library that hardcoded an icon set would make its consumers install
115
+ * that icon set.
116
+ */
117
+ export function BannerView({ variant, text, meta, icon, }) {
118
+ return (_jsxs("div", { className: cx('editorial-banner', `editorial-banner-${variant}`), role: "note", children: [icon, _jsxs("p", { className: "editorial-banner-body", children: [_jsxs("strong", { children: [meta.label, "."] }), " ", text?.trim() || meta.message] })] }));
119
+ }
package/dist/blocks.d.ts CHANGED
@@ -94,4 +94,3 @@ export declare function statsToMarkdown(items: readonly StatItem[]): string;
94
94
  export declare function linkGridToMarkdown(groups: readonly LinkGridGroup[], intro?: string | null): string;
95
95
  /** A flat bullet list of links — resolved page lists, feed items, link rails. */
96
96
  export declare function linkList(items: readonly LinkGridLink[]): string;
97
- //# sourceMappingURL=blocks.d.ts.map
package/dist/blocks.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // new block type is a compile error until it is handled) and the bodies come
11
11
  // from here. Each renderer takes the block's DATA rather than the block, which
12
12
  // keeps this file free of any one repo's type union.
13
- import { decodeEntities } from './entities.js';
13
+ import { decodeEntities } from './markdown.js';
14
14
  import { htmlToMarkdown, inlineToMarkdown } from './markdown.js';
15
15
  /**
16
16
  * The whole tree as markdown. Containers flatten in document order, because a
@@ -121,4 +121,3 @@ export function linkGridToMarkdown(groups, intro) {
121
121
  export function linkList(items) {
122
122
  return items.map(l => `- [${l.label}](${l.href})`).join('\n');
123
123
  }
124
- //# sourceMappingURL=blocks.js.map
@@ -57,4 +57,3 @@ export declare function listId(baseId: string, listKey?: string): string;
57
57
  * have an active option.
58
58
  */
59
59
  export declare function comboboxAria({ baseId, listKey, count, highlight, open }: ComboboxInput): ComboboxAria;
60
- //# sourceMappingURL=combobox.d.ts.map
package/dist/combobox.js CHANGED
@@ -56,4 +56,3 @@ export function comboboxAria({ baseId, listKey, count, highlight, open }) {
56
56
  }),
57
57
  };
58
58
  }
59
- //# sourceMappingURL=combobox.js.map
@@ -91,8 +91,6 @@ export declare function transportChecks(t: Tester, clientName: string,
91
91
  * business, so it is a parameter rather than one surface's set hardcoded here.
92
92
  */
93
93
  expectedCapabilities?: readonly string[]): Promise<Rpc>;
94
- /** The newest protocol revision `wiki-formant/mcp` speaks. */
95
- export declare const CURRENT_PROTOCOL = "2025-06-18";
96
94
  /**
97
95
  * One service, many descriptors — server.json, the two agent-card paths, the
98
96
  * OpenAPI document, the MCP server card, and `initialize` — should never
@@ -161,4 +159,3 @@ export declare function payloadBudget(t: Tester, calls: ReadonlyArray<{
161
159
  * every unnamed crawler is telling two different stories to the same caller.
162
160
  */
163
161
  export declare function robotsChecks(t: Tester, paths: readonly string[]): Promise<void>;
164
- //# sourceMappingURL=conformance.d.ts.map
@@ -11,6 +11,31 @@
11
11
  // same package — so the suite that checks it belongs next to it. What each app
12
12
  // keeps is its own fixtures: which tools it expects, what a good answer from each
13
13
  // looks like, and which text surfaces it publishes.
14
+ //
15
+ // Every value the transport assertions compare against is imported from `mcp.ts`,
16
+ // not restated here. A suite that carries its own copy of the contract stops
17
+ // testing the boundary the moment the contract moves and says nothing about it.
18
+ import { DEFAULT_MAX_BATCH, MCP_CORS, MCP_PROTOCOL_VERSION } from './mcp.js';
19
+ /**
20
+ * Does the live preflight carry every token `MCP_CORS` declares for this header?
21
+ *
22
+ * Derived rather than listed: the assertion used to name three headers by hand,
23
+ * so a fourth added to `MCP_CORS` was tested by nobody.
24
+ */
25
+ const corsCovers = (res, header) => {
26
+ const live = res.headers.get(header) ?? '';
27
+ return (MCP_CORS[header] ?? '').split(',').every(tok => live.includes(tok.trim()));
28
+ };
29
+ /**
30
+ * A POST that `Tester.rpc` cannot make: one whose raw status and headers are the
31
+ * thing under test, and which must not take `rpc`'s 429 retry. A string body is
32
+ * sent as-is, so the malformed-JSON case uses the same door as the rest.
33
+ */
34
+ const rawPost = (endpoint, body, headers = {}) => fetch(endpoint, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json', ...headers },
37
+ body: typeof body === 'string' ? body : JSON.stringify(body),
38
+ });
14
39
  export function createTester(opts) {
15
40
  const base = opts.base.replace(/\/$/, '');
16
41
  const endpoint = opts.endpoint ?? `${base}/api/mcp`;
@@ -87,30 +112,26 @@ expectedCapabilities = ['tools']) {
87
112
  // 2025-03-26 — forfeiting structured output, tool titles and `_meta` — while
88
113
  // every suite reported green.
89
114
  const echoed = await t.rpc('initialize', {
90
- protocolVersion: CURRENT_PROTOCOL,
115
+ protocolVersion: MCP_PROTOCOL_VERSION,
91
116
  capabilities: {},
92
117
  clientInfo: { name: clientName, version: '1' },
93
118
  });
94
- t.check('protocol negotiated', echoed.result?.protocolVersion === CURRENT_PROTOCOL, `asked ${CURRENT_PROTOCOL}, got ${echoed.result?.protocolVersion}`);
119
+ t.check('protocol negotiated', echoed.result?.protocolVersion === MCP_PROTOCOL_VERSION, `asked ${MCP_PROTOCOL_VERSION}, got ${echoed.result?.protocolVersion}`);
95
120
  t.check('downgrades gracefully', (init.result?.protocolVersion ?? '').length > 0, `asked 2024-11-05, got ${init.result?.protocolVersion}`);
96
121
  console.log(`\n=== transport ===`);
97
122
  const opt = await fetch(t.endpoint, { method: 'OPTIONS' });
98
123
  t.check('OPTIONS preflight', opt.status === 204 &&
99
124
  opt.headers.get('access-control-allow-origin') === '*' &&
100
- (opt.headers.get('access-control-allow-headers') ?? '').includes('Mcp-Protocol-Version'), `${opt.status} ACAO=${opt.headers.get('access-control-allow-origin')}`);
125
+ corsCovers(opt, 'Access-Control-Allow-Headers'), `${opt.status} ACAO=${opt.headers.get('access-control-allow-origin')}`);
101
126
  // Allow-Headers governs what a browser may send; Expose-Headers what it may
102
127
  // read. Without the second, a browser client cannot see `Retry-After` on a
103
128
  // 429 and a rate limit reads to it as a hang.
104
- t.check('CORS exposes response headers', ['Mcp-Protocol-Version', 'Retry-After', 'RateLimit-Remaining'].every(h => (opt.headers.get('access-control-expose-headers') ?? '').includes(h)), `expose=${opt.headers.get('access-control-expose-headers')}`);
129
+ t.check('CORS exposes response headers', corsCovers(opt, 'Access-Control-Expose-Headers'), `expose=${opt.headers.get('access-control-expose-headers')}`);
105
130
  // A budget stated only in the 429 can be discovered only by exceeding it —
106
131
  // the one moment an agent is least able to act on it. This suite is the
107
132
  // proof: with no header to read, its own client blind-sleeps five seconds on
108
133
  // a 429 and hopes.
109
- const headroom = await fetch(t.endpoint, {
110
- method: 'POST',
111
- headers: { 'Content-Type': 'application/json' },
112
- body: JSON.stringify({ jsonrpc: '2.0', id: 'headroom', method: 'ping' }),
113
- });
134
+ const headroom = await rawPost(t.endpoint, { jsonrpc: '2.0', id: 'headroom', method: 'ping' });
114
135
  t.recordCall();
115
136
  const remaining = headroom.headers.get('ratelimit-remaining');
116
137
  t.check('rate limit states headroom', !!headroom.headers.get('ratelimit-limit') && remaining !== null, `RateLimit-Limit=${headroom.headers.get('ratelimit-limit')} Remaining=${remaining} Reset=${headroom.headers.get('ratelimit-reset')}`);
@@ -118,23 +139,15 @@ expectedCapabilities = ['tools']) {
118
139
  t.check('GET→405', get.status === 405 && get.headers.get('access-control-allow-origin') === '*', `${get.status} Allow=${get.headers.get('allow')} ACAO=${get.headers.get('access-control-allow-origin')}`);
119
140
  // A notification has no id, so it must be acknowledged with no body at all —
120
141
  // a JSON-RPC response to one is a protocol error.
121
- const notif = await fetch(t.endpoint, {
122
- method: 'POST',
123
- headers: { 'Content-Type': 'application/json' },
124
- body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
125
- });
142
+ const notif = await rawPost(t.endpoint, { jsonrpc: '2.0', method: 'notifications/initialized' });
126
143
  const notifBody = await notif.text();
127
144
  t.check('notification→202', notif.status === 202 && notifBody === '', `${notif.status} body="${notifBody}"`);
128
- const bad = await fetch(t.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: 'not json' });
145
+ const bad = await rawPost(t.endpoint, 'not json');
129
146
  const badJson = (await bad.json());
130
147
  t.check('parse error→400', bad.status === 400 && badJson.error?.code === -32700, `${bad.status} code=${badJson.error?.code}`);
131
- const over = await fetch(t.endpoint, {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify(Array.from({ length: 21 }, (_, i) => ({ jsonrpc: '2.0', id: i, method: 'ping' }))),
135
- });
148
+ const over = await rawPost(t.endpoint, Array.from({ length: DEFAULT_MAX_BATCH + 1 }, (_, i) => ({ jsonrpc: '2.0', id: i, method: 'ping' })));
136
149
  const overJson = (await over.json());
137
- t.check('batch cap (21)', overJson.error?.code === -32600, `code=${overJson.error?.code}: ${(overJson.error?.message ?? '').slice(0, 80)}`);
150
+ t.check(`batch cap (${DEFAULT_MAX_BATCH + 1})`, overJson.error?.code === -32600, `code=${overJson.error?.code}: ${(overJson.error?.message ?? '').slice(0, 80)}`);
138
151
  const caps = init.result?.capabilities ?? {};
139
152
  t.check('capabilities honest', expectedCapabilities.every(k => k in caps), `${JSON.stringify(caps)} expected=[${expectedCapabilities.join(', ')}]`);
140
153
  // Declaring `resources` makes a client ask for templates. -32601 to a method
@@ -143,26 +156,20 @@ expectedCapabilities = ['tools']) {
143
156
  const templates = await t.rpc('resources/templates/list');
144
157
  t.check('resources/templates/list', Array.isArray(templates.result?.resourceTemplates), templates.error ? `-${templates.error.code}` : `${templates.result?.resourceTemplates?.length} templates`);
145
158
  }
146
- const versioned = await fetch(t.endpoint, {
147
- method: 'POST',
148
- headers: { 'Content-Type': 'application/json', 'MCP-Protocol-Version': CURRENT_PROTOCOL },
149
- body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' }),
159
+ const versioned = await rawPost(t.endpoint, { jsonrpc: '2.0', id: 1, method: 'ping' }, {
160
+ 'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
150
161
  });
151
162
  t.recordCall();
152
- t.check('MCP-Protocol-Version echoed', versioned.headers.get('mcp-protocol-version') === CURRENT_PROTOCOL, `sent ${CURRENT_PROTOCOL}, got ${versioned.headers.get('mcp-protocol-version')}`);
163
+ t.check('MCP-Protocol-Version echoed', versioned.headers.get('mcp-protocol-version') === MCP_PROTOCOL_VERSION, `sent ${MCP_PROTOCOL_VERSION}, got ${versioned.headers.get('mcp-protocol-version')}`);
153
164
  // Batching was removed in 2025-06-18. A server that keeps honouring it under
154
165
  // a version that forbids it is telling the client something untrue.
155
- const batched = await fetch(t.endpoint, {
156
- method: 'POST',
157
- headers: { 'Content-Type': 'application/json', 'MCP-Protocol-Version': CURRENT_PROTOCOL },
158
- body: JSON.stringify([{ jsonrpc: '2.0', id: 1, method: 'ping' }]),
166
+ const batched = await rawPost(t.endpoint, [{ jsonrpc: '2.0', id: 1, method: 'ping' }], {
167
+ 'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
159
168
  });
160
169
  t.recordCall();
161
170
  t.check('batch refused at 2025-06-18', batched.status === 400, `${batched.status}`);
162
171
  return init;
163
172
  }
164
- /** The newest protocol revision `wiki-formant/mcp` speaks. */
165
- export const CURRENT_PROTOCOL = '2025-06-18';
166
173
  /**
167
174
  * One service, many descriptors — server.json, the two agent-card paths, the
168
175
  * OpenAPI document, the MCP server card, and `initialize` — should never
@@ -212,11 +219,7 @@ export async function agentCardParity(t) {
212
219
  // `url` is where a client sends its first call. Pointed at the homepage it
213
220
  // gets HTML back, which is the failure that looks like a broken agent.
214
221
  const endpoint = String(a.url ?? '');
215
- const probe = await fetch(endpoint, {
216
- method: 'POST',
217
- headers: { 'Content-Type': 'application/json' },
218
- body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' }),
219
- });
222
+ const probe = await rawPost(endpoint, { jsonrpc: '2.0', id: 1, method: 'ping' });
220
223
  t.recordCall();
221
224
  t.check('agent card url is callable', (probe.headers.get('content-type') ?? '').includes('json'), `POST ${endpoint} -> ${probe.status} ${probe.headers.get('content-type')}`);
222
225
  }
@@ -372,4 +375,3 @@ export async function robotsChecks(t, paths) {
372
375
  t.check(`robots allows ${path}`, !match || match.allow, match ? `${match.allow ? 'Allow' : 'Disallow'}: ${match.path}` : 'no matching rule');
373
376
  }
374
377
  }
375
- //# sourceMappingURL=conformance.js.map
@@ -42,4 +42,3 @@ export declare function aiCrawlerRules(opts: {
42
42
  disallow: string | string[];
43
43
  aiAllow: string | string[];
44
44
  }): RobotsGroup[];
45
- //# sourceMappingURL=crawlers.d.ts.map
package/dist/crawlers.js CHANGED
@@ -84,4 +84,3 @@ export function aiCrawlerRules(opts) {
84
84
  })),
85
85
  ];
86
86
  }
87
- //# sourceMappingURL=crawlers.js.map
package/dist/dom.d.ts CHANGED
@@ -66,4 +66,3 @@ export interface TabGroupClassNames {
66
66
  * silently reset it to the first tab.
67
67
  */
68
68
  export declare function activateTabGroups(root: ParentNode, classNames?: TabGroupClassNames): void;
69
- //# sourceMappingURL=dom.d.ts.map
package/dist/dom.js CHANGED
@@ -164,4 +164,3 @@ export function activateTabGroups(root, classNames = {}) {
164
164
  group.appendChild(tabPanels);
165
165
  }
166
166
  }
167
- //# sourceMappingURL=dom.js.map
@@ -0,0 +1,101 @@
1
+ import type { AnyExtension, Editor } from '@tiptap/core';
2
+ import { type ChangeEvent, type RefObject } from 'react';
3
+ /**
4
+ * Re-exported so the COMMAND AUGMENTATIONS reach consumers.
5
+ *
6
+ * Each of these packages ships a `declare module '@tiptap/core'` block that adds
7
+ * its commands to `ChainedCommands` — `toggleBold`, `insertTable`, `setLink`,
8
+ * `setImage`. A consumer that imported the extensions itself picked those up as
9
+ * a side effect of the import; now that this module owns them, it has to carry
10
+ * the augmentation across the package boundary, and a re-exported type is what
11
+ * makes the emitted `.d.ts` load the module that declares it. Without these
12
+ * four lines every `editor.chain().focus().toggleBold()` in every consumer
13
+ * stops type-checking.
14
+ */
15
+ export type { StarterKitOptions } from '@tiptap/starter-kit';
16
+ export type { LinkOptions } from '@tiptap/extension-link';
17
+ export type { ImageOptions } from '@tiptap/extension-image';
18
+ export type { TableOptions } from '@tiptap/extension-table';
19
+ /**
20
+ * Reduce pasted HTML to structure plus the three attributes that carry meaning.
21
+ *
22
+ * A paste from a word processor or a web page arrives carrying its whole
23
+ * stylesheet inline. Keeping any of it means the wiki's own typography loses to
24
+ * whatever the author copied from, per paragraph, invisibly — and `style` on a
25
+ * pasted node is also the cheapest way to smuggle a full-bleed overlay into a
26
+ * page body.
27
+ *
28
+ * Browser-only: it parses with `DOMParser`. Called from `transformPastedHTML`,
29
+ * which only ever runs in response to a paste.
30
+ */
31
+ export declare function cleanPastedHtml(html: string): string;
32
+ export interface WikiEditorExtensionOptions {
33
+ /** Empty-document prompt. */
34
+ placeholder?: string;
35
+ /**
36
+ * The custom nodes, built by the consumer.
37
+ *
38
+ * They stay the consumer's because `createCodeBlock`, `createTabs` and
39
+ * `createMapEmbed` all take that design system's class names and icons — and
40
+ * `createMapEmbed` needs its API route to follow a shortened link. What is
41
+ * shared is the twelve entries around them and how each is configured.
42
+ */
43
+ nodes?: readonly AnyExtension[];
44
+ }
45
+ /**
46
+ * The editor's extension set.
47
+ *
48
+ * `codeBlock: false` on StarterKit is load-bearing: the consumer registers its
49
+ * own via `createCodeBlock`, and leaving StarterKit's in place would give the
50
+ * schema two nodes claiming the same name. Headings stop at h2 — the page title
51
+ * is the only h1 a wiki page has.
52
+ */
53
+ export declare function wikiEditorExtensions({ placeholder, nodes, }?: WikiEditorExtensionOptions): AnyExtension[];
54
+ /**
55
+ * Turn a pasted URL into the richest node that fits it, falling back to a bare
56
+ * iframe. Order matters: a YouTube URL is also a valid iframe source, so the
57
+ * specific cases have to be tried before the general one.
58
+ *
59
+ * A shortened map link inserts IMMEDIATELY with `about:blank` and swaps its
60
+ * `src` once the redirect resolves. Pasting must not block on a network hop,
61
+ * and the node has to already exist for the reader to see anything happen. The
62
+ * swap re-finds the node by its `url` attribute rather than caching a position,
63
+ * because every keystroke between the paste and the resolve moves it.
64
+ */
65
+ export declare function insertEmbed(editor: Editor, url: string, { resolveMapUrl }: {
66
+ resolveMapUrl: (url: string) => Promise<string | null>;
67
+ }): void;
68
+ /** The table controls, shown only while the selection is inside a table. */
69
+ export declare const TABLE_ACTIONS: ReadonlyArray<[command: string, label: string, danger?: boolean]>;
70
+ /** `editor.isActive` as a toolbar button's `active` descriptor expresses it. */
71
+ export type ActiveDescriptor = string | [string, Record<string, unknown>];
72
+ export interface WikiEditorOptions {
73
+ value: string;
74
+ onChange: (html: string) => void;
75
+ placeholder?: string;
76
+ nodes?: readonly AnyExtension[];
77
+ /** The prose class the editor body renders under. */
78
+ proseClass: string;
79
+ /** Upload a chosen file, returning its URL or null. The route is the app's. */
80
+ uploadImage: (file: File) => Promise<string | null>;
81
+ /** Debounce before `onChange` fires, in ms. */
82
+ debounceMs?: number;
83
+ }
84
+ export interface WikiEditorState {
85
+ editor: Editor | null;
86
+ fileInputRef: RefObject<HTMLInputElement | null>;
87
+ isUploading: boolean;
88
+ handleFileChange: (e: ChangeEvent<HTMLInputElement>) => Promise<void>;
89
+ /** Open the file picker — what an upload toolbar button calls. */
90
+ triggerUpload: () => void;
91
+ isActive: (a?: ActiveDescriptor) => boolean;
92
+ }
93
+ /**
94
+ * The editor, its upload plumbing and the state a toolbar reads.
95
+ *
96
+ * The toolbar itself stays with the consumer: the two wikis differ on which
97
+ * buttons exist, which icon set draws them, and whether a link is entered in a
98
+ * `window.prompt` or an inline field. Those are real differences. What they had
99
+ * no business differing on is everything below.
100
+ */
101
+ export declare function useWikiEditor({ value, onChange, placeholder, nodes, proseClass, uploadImage, debounceMs, }: WikiEditorOptions): WikiEditorState;