@waveso/docs 0.1.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/dist/frontmatter.d.ts +55 -0
  4. package/dist/frontmatter.js +80 -0
  5. package/dist/highlighter.d.ts +99 -0
  6. package/dist/highlighter.js +183 -0
  7. package/dist/meta.d.ts +75 -0
  8. package/dist/meta.js +183 -0
  9. package/dist/next.d.ts +256 -0
  10. package/dist/next.js +365 -0
  11. package/dist/plugins/rehype-capture-toc.d.ts +18 -0
  12. package/dist/plugins/rehype-capture-toc.js +69 -0
  13. package/dist/plugins/remark-doc-links.d.ts +63 -0
  14. package/dist/plugins/remark-doc-links.js +122 -0
  15. package/dist/plugins/remark-unwrap-images.d.ts +11 -0
  16. package/dist/plugins/remark-unwrap-images.js +25 -0
  17. package/dist/plugins/remark-youtube.d.ts +22 -0
  18. package/dist/plugins/remark-youtube.js +84 -0
  19. package/dist/react/callout.d.ts +37 -0
  20. package/dist/react/callout.js +113 -0
  21. package/dist/react/doc-content.d.ts +29 -0
  22. package/dist/react/doc-content.js +30 -0
  23. package/dist/react/markdown-components.d.ts +84 -0
  24. package/dist/react/markdown-components.js +122 -0
  25. package/dist/react/search-dialog.d.ts +41 -0
  26. package/dist/react/search-dialog.js +404 -0
  27. package/dist/react/sidebar.d.ts +29 -0
  28. package/dist/react/sidebar.js +196 -0
  29. package/dist/react/skip-link.d.ts +37 -0
  30. package/dist/react/skip-link.js +37 -0
  31. package/dist/react/toc.d.ts +35 -0
  32. package/dist/react/toc.js +87 -0
  33. package/dist/react/youtube.d.ts +27 -0
  34. package/dist/react/youtube.js +75 -0
  35. package/dist/render.d.ts +72 -0
  36. package/dist/render.js +279 -0
  37. package/dist/search-index.d.ts +51 -0
  38. package/dist/search-index.js +274 -0
  39. package/dist/search-options.d.ts +18 -0
  40. package/dist/search-options.js +40 -0
  41. package/dist/source.d.ts +67 -0
  42. package/dist/source.js +332 -0
  43. package/dist/styles.css +1033 -0
  44. package/dist/types.d.ts +334 -0
  45. package/dist/types.js +0 -0
  46. package/package.json +166 -0
@@ -0,0 +1,69 @@
1
+ import { SKIP, visit } from "unist-util-visit";
2
+ import { toString } from "hast-util-to-string";
3
+ //#region src/plugins/rehype-capture-toc.ts
4
+ /** `h2`–`h6`. `h1` is the page title and never appears in a TOC. */
5
+ const HEADING = /^h([2-6])$/;
6
+ /**
7
+ * Drop the permalink anchor `rehype-autolink-headings` appends.
8
+ *
9
+ * This plugin is ordered before that one, so in practice there is nothing to
10
+ * drop — but the check costs nothing and the alternative, if the order ever
11
+ * changes, is every TOC entry silently gaining a trailing `#`.
12
+ */
13
+ function isPermalink(child) {
14
+ if (child.type !== "element" || child.tagName !== "a") return false;
15
+ const className = child.properties.className;
16
+ const ariaHidden = child.properties.ariaHidden;
17
+ return ariaHidden === true || ariaHidden === "true" || Array.isArray(className) && className.includes("heading-anchor");
18
+ }
19
+ /**
20
+ * The GFM footnote block `mdast-util-to-hast` appends.
21
+ *
22
+ * It carries a generated `<h2 id="footnote-label">Footnotes</h2>` that is
23
+ * machinery rather than a section of the page — and is visually hidden, so a
24
+ * TOC entry for it points the reader at nothing they can see. `search-index.ts`
25
+ * skips the same subtree; the two must agree about which sections exist.
26
+ */
27
+ function isFootnotes(node) {
28
+ return node.properties.dataFootnotes !== void 0;
29
+ }
30
+ function headingText(node) {
31
+ return node.children.filter((child) => !isPermalink(child)).map((child) => toString(child)).join("").trim();
32
+ }
33
+ /**
34
+ * rehype plugin. Must run after `rehype-slug`; ordering is enforced by the
35
+ * renderer rather than checked here, because a heading legitimately may have
36
+ * been given an id by hand.
37
+ */
38
+ const rehypeCaptureToc = () => {
39
+ return (tree, file) => {
40
+ const toc = [];
41
+ /** Open ancestors, outermost first. */
42
+ const stack = [];
43
+ visit(tree, "element", (node) => {
44
+ if (isFootnotes(node)) return SKIP;
45
+ const level = HEADING.exec(node.tagName)?.[1];
46
+ if (level === void 0) return;
47
+ const id = node.properties.id;
48
+ if (typeof id !== "string" || id === "") return;
49
+ const entry = {
50
+ id,
51
+ text: headingText(node),
52
+ depth: Number(level),
53
+ children: []
54
+ };
55
+ while (stack.length > 0) {
56
+ const top = stack[stack.length - 1];
57
+ if (top !== void 0 && top.depth < entry.depth) break;
58
+ stack.pop();
59
+ }
60
+ const parent = stack[stack.length - 1];
61
+ if (parent === void 0) toc.push(entry);
62
+ else parent.children.push(entry);
63
+ stack.push(entry);
64
+ });
65
+ file.data.toc = toc;
66
+ };
67
+ };
68
+ //#endregion
69
+ export { rehypeCaptureToc };
@@ -0,0 +1,63 @@
1
+ import { DocLinkContext, LinkResolver } from "../types.js";
2
+ import { Plugin } from "unified";
3
+ import { Root } from "mdast";
4
+ //#region src/plugins/remark-doc-links.d.ts
5
+ /**
6
+ * Where a link was found and what it resolved to.
7
+ *
8
+ * `href` is `undefined` when resolution failed — an unknown resolver result,
9
+ * or a `../` chain that climbs out of the content root. That is a distinct
10
+ * condition from "resolved but the page does not exist", and the caller
11
+ * reports them differently.
12
+ */
13
+ interface DocLinkRef {
14
+ /** The href exactly as authored, e.g. `'./api/auth.md'`. */
15
+ raw: string;
16
+ /** Resolved route including `?query` and `#anchor`, or `undefined`. */
17
+ href: string | undefined;
18
+ /** 1-based line in the source markdown, when the parser recorded one. */
19
+ line?: number;
20
+ }
21
+ declare module 'vfile' {
22
+ interface DataMap {
23
+ /** Set by the caller before running the processor. */
24
+ docLinkContext: DocLinkContext;
25
+ /** Appended to by {@link remarkDocLinks}. */
26
+ docLinks: DocLinkRef[];
27
+ }
28
+ }
29
+ interface RemarkDocLinksOptions {
30
+ /** URL prefix the docs are mounted at, e.g. `'/docs'`. */
31
+ basePath: string;
32
+ /** Overrides the built-in resolution entirely, for every relative link. */
33
+ resolve?: LinkResolver;
34
+ }
35
+ /**
36
+ * Fold `.` and `..` against a starting directory.
37
+ *
38
+ * Hand-rolled rather than `path.resolve` because these are URL paths, not
39
+ * filesystem paths: they must use `/` on Windows and must not pick up the
40
+ * process working directory. Returns `undefined` when the chain climbs above
41
+ * the content root, which is always an authoring error worth surfacing.
42
+ *
43
+ * EXPORTED as part of the wrap-don't-replace surface. Getting `../` right is
44
+ * the fiddly half of writing a {@link LinkResolver}, and a host that reuses
45
+ * this cannot disagree with the built-in resolver about where a link points.
46
+ * It is also what `render.ts` folds image sources with, so links and images
47
+ * are contained by one implementation rather than two.
48
+ */
49
+ declare function foldSegments(from: readonly string[], path: string): string[] | undefined;
50
+ /**
51
+ * The built-in {@link LinkResolver}: markdown file path in, route out.
52
+ *
53
+ * Exported for reuse by hosts that want to wrap rather than replace it.
54
+ */
55
+ declare function resolveMarkdownLink(href: string, fromDir: readonly string[], basePath: string): string | undefined;
56
+ /**
57
+ * remark plugin. Requires `file.data.docLinkContext` to be set; without it the
58
+ * containing document is unknown and every relative link would resolve against
59
+ * the content root, which is worse than leaving them alone.
60
+ */
61
+ declare const remarkDocLinks: Plugin<[RemarkDocLinksOptions], Root>;
62
+ //#endregion
63
+ export { type DocLinkContext, DocLinkRef, RemarkDocLinksOptions, foldSegments, remarkDocLinks, resolveMarkdownLink };
@@ -0,0 +1,122 @@
1
+ import { visit } from "unist-util-visit";
2
+ //#region src/plugins/remark-doc-links.ts
3
+ /** `scheme:` — matches `https:`, `mailto:`, `tel:`, `data:`. */
4
+ const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
5
+ /** Splits `path?query#hash`; every group is optional. */
6
+ const HREF_PARTS = /^([^?#]*)(\?[^#]*)?(#.*)?$/;
7
+ const MARKDOWN_EXTENSION = /\.mdx?$/i;
8
+ /** A dot-extension on the final segment, e.g. `.png` in `img/logo.png`. */
9
+ const FILE_EXTENSION = /\.[^./]+$/;
10
+ /**
11
+ * Does this href point at another page in the same docs tree?
12
+ *
13
+ * Absolute paths are already routes, in-page anchors are already correct, and
14
+ * anything with a scheme belongs to someone else. A path-less href — `?tab=json`
15
+ * — addresses the current page and is left alone for the same reason `#anchor`
16
+ * is: there is nothing to resolve, and resolving it to `undefined` would fail
17
+ * the build with advice the author cannot act on.
18
+ */
19
+ function isRelativeLink(href) {
20
+ return href !== "" && !href.startsWith("#") && !href.startsWith("?") && !href.startsWith("/") && !HAS_SCHEME.test(href);
21
+ }
22
+ /**
23
+ * Fold `.` and `..` against a starting directory.
24
+ *
25
+ * Hand-rolled rather than `path.resolve` because these are URL paths, not
26
+ * filesystem paths: they must use `/` on Windows and must not pick up the
27
+ * process working directory. Returns `undefined` when the chain climbs above
28
+ * the content root, which is always an authoring error worth surfacing.
29
+ *
30
+ * EXPORTED as part of the wrap-don't-replace surface. Getting `../` right is
31
+ * the fiddly half of writing a {@link LinkResolver}, and a host that reuses
32
+ * this cannot disagree with the built-in resolver about where a link points.
33
+ * It is also what `render.ts` folds image sources with, so links and images
34
+ * are contained by one implementation rather than two.
35
+ */
36
+ function foldSegments(from, path) {
37
+ const out = [...from];
38
+ for (const part of path.split("/")) {
39
+ if (part === "" || part === ".") continue;
40
+ if (part === "..") {
41
+ if (out.length === 0) return;
42
+ out.pop();
43
+ continue;
44
+ }
45
+ out.push(part);
46
+ }
47
+ return out;
48
+ }
49
+ /** Join route segments onto the base path, e.g. `('/docs', ['api'])`. */
50
+ function toRoute(basePath, segments) {
51
+ const base = basePath.replace(/\/+$/, "");
52
+ const path = segments.join("/");
53
+ if (path === "") return base === "" ? "/" : base;
54
+ return `${base}/${path}`;
55
+ }
56
+ /**
57
+ * The built-in {@link LinkResolver}: markdown file path in, route out.
58
+ *
59
+ * Exported for reuse by hosts that want to wrap rather than replace it.
60
+ */
61
+ function resolveMarkdownLink(href, fromDir, basePath) {
62
+ const parts = HREF_PARTS.exec(href);
63
+ const path = parts?.[1] ?? "";
64
+ const query = parts?.[2] ?? "";
65
+ const hash = parts?.[3] ?? "";
66
+ if (path === "") return;
67
+ const segments = foldSegments(fromDir, path);
68
+ if (segments === void 0) return;
69
+ const last = segments.at(-1);
70
+ if (last !== void 0) {
71
+ const stripped = last.replace(MARKDOWN_EXTENSION, "");
72
+ if (stripped === "index") segments.pop();
73
+ else segments[segments.length - 1] = stripped;
74
+ }
75
+ return `${toRoute(basePath, segments)}${query}${hash}`;
76
+ }
77
+ /**
78
+ * Is this relative href pointing at an asset rather than a page?
79
+ *
80
+ * A link to `./diagram.svg` or `./schema.sql` is a download, not a route, and
81
+ * rewriting it would break it. Markdown extensions are pages; any other
82
+ * extension on the final segment is an asset; no extension is a page.
83
+ * The false positive is a page literally named `v2.0`, which is rare enough
84
+ * to accept and is fixed by writing `v2.0.md`.
85
+ */
86
+ function isAssetLink(href) {
87
+ const last = (HREF_PARTS.exec(href)?.[1] ?? "").split("/").at(-1) ?? "";
88
+ return FILE_EXTENSION.test(last) && !MARKDOWN_EXTENSION.test(last);
89
+ }
90
+ /**
91
+ * remark plugin. Requires `file.data.docLinkContext` to be set; without it the
92
+ * containing document is unknown and every relative link would resolve against
93
+ * the content root, which is worse than leaving them alone.
94
+ */
95
+ const remarkDocLinks = (options) => {
96
+ const { basePath, resolve } = options;
97
+ return (tree, file) => {
98
+ const context = file.data.docLinkContext;
99
+ if (context === void 0) throw new Error(`@waveso/docs: remarkDocLinks ran without file.data.docLinkContext${file.path === void 0 ? "" : ` (file: ${file.path})`}. Set it before running the processor.`);
100
+ const refs = file.data.docLinks ?? [];
101
+ file.data.docLinks = refs;
102
+ visit(tree, ["link", "definition"], (node) => {
103
+ if (node.type !== "link" && node.type !== "definition") return;
104
+ const raw = node.url;
105
+ if (!isRelativeLink(raw)) return;
106
+ if (resolve === void 0 && isAssetLink(raw)) return;
107
+ const href = resolve ? resolve(raw, context) : resolveMarkdownLink(raw, context.dirSegments, basePath);
108
+ const line = node.position?.start.line;
109
+ refs.push(line === void 0 ? {
110
+ raw,
111
+ href
112
+ } : {
113
+ raw,
114
+ href,
115
+ line
116
+ });
117
+ if (href !== void 0) node.url = href;
118
+ });
119
+ };
120
+ };
121
+ //#endregion
122
+ export { foldSegments, remarkDocLinks, resolveMarkdownLink };
@@ -0,0 +1,11 @@
1
+ import { Plugin } from "unified";
2
+ import { Root } from "mdast";
3
+ //#region src/plugins/remark-unwrap-images.d.ts
4
+ /**
5
+ * remark plugin. Only unwraps paragraphs whose sole meaningful child is an
6
+ * image — a paragraph with a caption beside the image is prose, and prose
7
+ * belongs in a paragraph.
8
+ */
9
+ declare const remarkUnwrapImages: Plugin<[], Root>;
10
+ //#endregion
11
+ export { remarkUnwrapImages };
@@ -0,0 +1,25 @@
1
+ import { SKIP, visit } from "unist-util-visit";
2
+ //#region src/plugins/remark-unwrap-images.ts
3
+ /** Whitespace-only text is what separates `![a](a.png)\n![b](b.png)`. */
4
+ function isIgnorable(node) {
5
+ return node.type === "text" && node.value.trim() === "";
6
+ }
7
+ /**
8
+ * remark plugin. Only unwraps paragraphs whose sole meaningful child is an
9
+ * image — a paragraph with a caption beside the image is prose, and prose
10
+ * belongs in a paragraph.
11
+ */
12
+ const remarkUnwrapImages = () => {
13
+ return (tree) => {
14
+ visit(tree, "paragraph", (node, index, parent) => {
15
+ if (parent === void 0 || index === void 0) return;
16
+ const meaningful = node.children.filter((child) => !isIgnorable(child));
17
+ const only = meaningful[0];
18
+ if (meaningful.length !== 1 || only === void 0 || only.type !== "image" && only.type !== "imageReference") return;
19
+ parent.children[index] = only;
20
+ return [SKIP, index + 1];
21
+ });
22
+ };
23
+ };
24
+ //#endregion
25
+ export { remarkUnwrapImages };
@@ -0,0 +1,22 @@
1
+ import { Plugin } from "unified";
2
+ import { Root } from "mdast";
3
+ //#region src/plugins/remark-youtube.d.ts
4
+ /**
5
+ * Extract a video id from a YouTube watch/short/embed URL.
6
+ *
7
+ * Returns `undefined` for anything else, including YouTube URLs that are not a
8
+ * single video (channels, playlists) — those stay ordinary links.
9
+ */
10
+ declare function parseYouTubeId(href: string): string | undefined;
11
+ /**
12
+ * remark plugin. Replaces the whole PARAGRAPH, not the link inside it — which
13
+ * is the point: leaving the paragraph is what nested a block element in
14
+ * phrasing content.
15
+ *
16
+ * `data.hName` / `data.hProperties` is how `mdast-util-to-hast` is told to
17
+ * serialise a node as something other than its default, so the output is a
18
+ * plain hast element and the React layer maps it like any other.
19
+ */
20
+ declare const remarkYouTube: Plugin<[], Root>;
21
+ //#endregion
22
+ export { parseYouTubeId, remarkYouTube };
@@ -0,0 +1,84 @@
1
+ import { SKIP, visit } from "unist-util-visit";
2
+ //#region src/plugins/remark-youtube.ts
3
+ /** YouTube ids are exactly eleven URL-safe characters. */
4
+ const VIDEO_ID = /^[A-Za-z0-9_-]{11}$/;
5
+ /** `https://` or `http://`, for comparing a label against its own href. */
6
+ const HTTP_SCHEME = /^https?:\/\//i;
7
+ /**
8
+ * Extract a video id from a YouTube watch/short/embed URL.
9
+ *
10
+ * Returns `undefined` for anything else, including YouTube URLs that are not a
11
+ * single video (channels, playlists) — those stay ordinary links.
12
+ */
13
+ function parseYouTubeId(href) {
14
+ let url;
15
+ try {
16
+ url = new URL(href, "https://example.invalid");
17
+ } catch {
18
+ return;
19
+ }
20
+ const host = url.hostname.replace(/^(www|m)\./, "");
21
+ const segments = url.pathname.split("/").filter(Boolean);
22
+ if (host === "youtu.be") {
23
+ const [id] = segments;
24
+ return id !== void 0 && VIDEO_ID.test(id) ? id : void 0;
25
+ }
26
+ if (host !== "youtube.com" && host !== "youtube-nocookie.com") return;
27
+ if (url.pathname === "/watch") {
28
+ const id = url.searchParams.get("v");
29
+ return id !== null && VIDEO_ID.test(id) ? id : void 0;
30
+ }
31
+ const [prefix, id] = segments;
32
+ if ((prefix === "embed" || prefix === "shorts") && id !== void 0) return VIDEO_ID.test(id) ? id : void 0;
33
+ }
34
+ /** Whitespace-only text is what separates two links on consecutive lines. */
35
+ function isIgnorable(node) {
36
+ return node.type === "text" && node.value.trim() === "";
37
+ }
38
+ /**
39
+ * Is this link a BARE URL rather than a labelled one?
40
+ *
41
+ * `[the intro](https://youtu.be/x)` keeps its label and stays a link; only a
42
+ * link whose visible text IS its own href becomes a video. Compared with the
43
+ * scheme and any trailing slash removed, because a markdown autolink often
44
+ * drops the scheme from the label.
45
+ */
46
+ function isBareUrl(node) {
47
+ if (node.type !== "link" || node.children.length !== 1) return false;
48
+ const [only] = node.children;
49
+ if (only === void 0 || only.type !== "text") return false;
50
+ const strip = (value) => value.replace(HTTP_SCHEME, "").replace(/\/$/, "");
51
+ return strip(only.value) === strip(node.url);
52
+ }
53
+ /**
54
+ * remark plugin. Replaces the whole PARAGRAPH, not the link inside it — which
55
+ * is the point: leaving the paragraph is what nested a block element in
56
+ * phrasing content.
57
+ *
58
+ * `data.hName` / `data.hProperties` is how `mdast-util-to-hast` is told to
59
+ * serialise a node as something other than its default, so the output is a
60
+ * plain hast element and the React layer maps it like any other.
61
+ */
62
+ const remarkYouTube = () => {
63
+ return (tree) => {
64
+ visit(tree, "paragraph", (node, index, parent) => {
65
+ if (parent === void 0 || index === void 0) return;
66
+ const meaningful = node.children.filter((child) => !isIgnorable(child));
67
+ const only = meaningful[0];
68
+ if (meaningful.length !== 1 || only === void 0 || !isBareUrl(only)) return;
69
+ const id = only.type === "link" ? parseYouTubeId(only.url) : void 0;
70
+ if (id === void 0) return;
71
+ parent.children[index] = {
72
+ type: "paragraph",
73
+ children: [],
74
+ data: {
75
+ hName: "youtube",
76
+ hProperties: { id }
77
+ }
78
+ };
79
+ return [SKIP, index + 1];
80
+ });
81
+ };
82
+ };
83
+ //#endregion
84
+ export { parseYouTubeId, remarkYouTube };
@@ -0,0 +1,37 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/react/callout.d.ts
3
+ /**
4
+ * The five alert kinds GitHub understands, which is also what
5
+ * `rehype-github-alerts` produces from `> [!NOTE]` and friends.
6
+ */
7
+ declare const CALLOUT_TYPES: readonly ["note", "tip", "important", "warning", "caution"];
8
+ /** One of {@link CALLOUT_TYPES}. */
9
+ type CalloutType = (typeof CALLOUT_TYPES)[number];
10
+ interface CalloutProps {
11
+ /**
12
+ * Kind of callout. Typed as a plain string because it arrives as an
13
+ * unvalidated hast attribute; anything unrecognised falls back to `note`
14
+ * rather than rendering an unstyled box.
15
+ */
16
+ type?: string | undefined;
17
+ /** Overrides the default label ("Note", "Warning", …). */
18
+ title?: string | undefined;
19
+ className?: string | undefined;
20
+ children?: ReactNode;
21
+ }
22
+ /**
23
+ * A note/tip/important/warning/caution block.
24
+ *
25
+ * Rendered as an `<aside role="note">`: `aside` is the honest element, and the
26
+ * explicit role keeps a callout in the middle of an article from showing up in
27
+ * every screen reader's landmark list. The type is carried on `aria-label`
28
+ * rather than left to the coloured border, which conveys nothing to a screen
29
+ * reader and nothing to the 8% of men who cannot separate the red one from the
30
+ * green one.
31
+ *
32
+ * All styling lives in `@waveso/docs/styles.css` under `.wave-docs-callout`,
33
+ * so consumers can restyle it without forking the component.
34
+ */
35
+ declare function Callout({ type, title, className, children }: CalloutProps): ReactNode;
36
+ //#endregion
37
+ export { CALLOUT_TYPES, Callout, CalloutProps, CalloutType };
@@ -0,0 +1,113 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ //#region src/react/callout.tsx
3
+ /**
4
+ * The five alert kinds GitHub understands, which is also what
5
+ * `rehype-github-alerts` produces from `> [!NOTE]` and friends.
6
+ */
7
+ const CALLOUT_TYPES = [
8
+ "note",
9
+ "tip",
10
+ "important",
11
+ "warning",
12
+ "caution"
13
+ ];
14
+ const CALLOUT_LABELS = {
15
+ note: "Note",
16
+ tip: "Tip",
17
+ important: "Important",
18
+ warning: "Warning",
19
+ caution: "Caution"
20
+ };
21
+ /**
22
+ * Lucide-derived glyph paths, drawn at 24×24 with a 2px stroke.
23
+ *
24
+ * Inlined rather than imported so the package takes no icon dependency and
25
+ * ships no icon bytes for callouts a given page does not use.
26
+ */
27
+ const CALLOUT_ICON_PATHS = {
28
+ note: ["M12 16v-4", "M12 8h.01"],
29
+ tip: [
30
+ "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5a6 6 0 0 0-12 0c0 1.3.5 2.6 1.5 3.5.8.8 1.3 1.5 1.5 2.5",
31
+ "M9 18h6",
32
+ "M10 22h4"
33
+ ],
34
+ important: [
35
+ "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",
36
+ "M12 7v4",
37
+ "M12 15h.01"
38
+ ],
39
+ warning: [
40
+ "m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",
41
+ "M12 9v4",
42
+ "M12 17h.01"
43
+ ],
44
+ caution: [
45
+ "M15.31 2a2 2 0 0 1 1.42.59l4.68 4.68A2 2 0 0 1 22 8.69v6.62a2 2 0 0 1-.59 1.42l-4.68 4.68a2 2 0 0 1-1.42.59H8.69a2 2 0 0 1-1.42-.59l-4.68-4.68A2 2 0 0 1 2 15.31V8.69a2 2 0 0 1 .59-1.42l4.68-4.68A2 2 0 0 1 8.69 2z",
46
+ "M12 8v4",
47
+ "M12 16h.01"
48
+ ]
49
+ };
50
+ /**
51
+ * `note` is the only glyph whose enclosing shape is not already one of its
52
+ * paths, so it gets a circle drawn before the strokes.
53
+ */
54
+ const CALLOUT_ICON_CIRCLE = { note: true };
55
+ function normalizeCalloutType(value) {
56
+ const candidate = value?.toLowerCase();
57
+ return CALLOUT_TYPES.find((type) => type === candidate) ?? "note";
58
+ }
59
+ /**
60
+ * A note/tip/important/warning/caution block.
61
+ *
62
+ * Rendered as an `<aside role="note">`: `aside` is the honest element, and the
63
+ * explicit role keeps a callout in the middle of an article from showing up in
64
+ * every screen reader's landmark list. The type is carried on `aria-label`
65
+ * rather than left to the coloured border, which conveys nothing to a screen
66
+ * reader and nothing to the 8% of men who cannot separate the red one from the
67
+ * green one.
68
+ *
69
+ * All styling lives in `@waveso/docs/styles.css` under `.wave-docs-callout`,
70
+ * so consumers can restyle it without forking the component.
71
+ */
72
+ function Callout({ type, title, className, children }) {
73
+ const kind = normalizeCalloutType(type);
74
+ const label = title?.trim() || CALLOUT_LABELS[kind];
75
+ return /* @__PURE__ */ jsxs("aside", {
76
+ role: "note",
77
+ "aria-label": label,
78
+ className: [
79
+ "wave-docs-callout",
80
+ `wave-docs-callout--${kind}`,
81
+ className
82
+ ].filter(Boolean).join(" "),
83
+ children: [/* @__PURE__ */ jsxs("p", {
84
+ className: "wave-docs-callout__label",
85
+ children: [/* @__PURE__ */ jsx(CalloutIcon, { type: kind }), label]
86
+ }), /* @__PURE__ */ jsx("div", {
87
+ className: "wave-docs-callout__body",
88
+ children
89
+ })]
90
+ });
91
+ }
92
+ function CalloutIcon({ type }) {
93
+ return /* @__PURE__ */ jsxs("svg", {
94
+ className: "wave-docs-callout__icon",
95
+ viewBox: "0 0 24 24",
96
+ width: "16",
97
+ height: "16",
98
+ fill: "none",
99
+ stroke: "currentColor",
100
+ strokeWidth: "2",
101
+ strokeLinecap: "round",
102
+ strokeLinejoin: "round",
103
+ "aria-hidden": "true",
104
+ focusable: "false",
105
+ children: [CALLOUT_ICON_CIRCLE[type] ? /* @__PURE__ */ jsx("circle", {
106
+ cx: "12",
107
+ cy: "12",
108
+ r: "10"
109
+ }) : null, CALLOUT_ICON_PATHS[type].map((d) => /* @__PURE__ */ jsx("path", { d }, d))]
110
+ });
111
+ }
112
+ //#endregion
113
+ export { CALLOUT_TYPES, Callout };
@@ -0,0 +1,29 @@
1
+ import { MarkdownComponents } from "./markdown-components.js";
2
+ import { ReactNode } from "react";
3
+ import { Root } from "hast";
4
+ //#region src/react/doc-content.d.ts
5
+ interface DocContentProps {
6
+ /**
7
+ * The tree from `@waveso/docs/render`. Plain serialisable JSON, so it
8
+ * crosses the RSC boundary and survives any build-time artifact intact.
9
+ */
10
+ hast: Root;
11
+ /** Overrides, merged over {@link defaultMarkdownComponents}. */
12
+ components?: MarkdownComponents | undefined;
13
+ }
14
+ /**
15
+ * Render a hast tree as React elements.
16
+ *
17
+ * Not a client component, and it must stay that way: the markdown parser and
18
+ * Shiki ran in Node at build time, and this component only walks the resulting
19
+ * tree. Nothing here pulls unified, remark or a highlighter into the browser.
20
+ *
21
+ * `passNode` is left off (the default). `react-markdown` hardcodes it *on* with
22
+ * no opt-out, so any mapped component that spreads its props renders
23
+ * `node="[object Object]"` into production HTML — with no type error to warn
24
+ * you, because `node` is a legal prop on the component and an unknown attribute
25
+ * on the element.
26
+ */
27
+ declare function DocContent({ hast, components }: DocContentProps): ReactNode;
28
+ //#endregion
29
+ export { DocContent, DocContentProps };
@@ -0,0 +1,30 @@
1
+ import { defaultMarkdownComponents } from "./markdown-components.js";
2
+ import { toJsxRuntime } from "hast-util-to-jsx-runtime";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/react/doc-content.tsx
5
+ /**
6
+ * Render a hast tree as React elements.
7
+ *
8
+ * Not a client component, and it must stay that way: the markdown parser and
9
+ * Shiki ran in Node at build time, and this component only walks the resulting
10
+ * tree. Nothing here pulls unified, remark or a highlighter into the browser.
11
+ *
12
+ * `passNode` is left off (the default). `react-markdown` hardcodes it *on* with
13
+ * no opt-out, so any mapped component that spreads its props renders
14
+ * `node="[object Object]"` into production HTML — with no type error to warn
15
+ * you, because `node` is a legal prop on the component and an unknown attribute
16
+ * on the element.
17
+ */
18
+ function DocContent({ hast, components }) {
19
+ return toJsxRuntime(hast, {
20
+ Fragment,
21
+ jsx,
22
+ jsxs,
23
+ components: {
24
+ ...defaultMarkdownComponents,
25
+ ...components
26
+ }
27
+ });
28
+ }
29
+ //#endregion
30
+ export { DocContent };