@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.
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/dist/frontmatter.d.ts +55 -0
- package/dist/frontmatter.js +80 -0
- package/dist/highlighter.d.ts +99 -0
- package/dist/highlighter.js +183 -0
- package/dist/meta.d.ts +75 -0
- package/dist/meta.js +183 -0
- package/dist/next.d.ts +256 -0
- package/dist/next.js +365 -0
- package/dist/plugins/rehype-capture-toc.d.ts +18 -0
- package/dist/plugins/rehype-capture-toc.js +69 -0
- package/dist/plugins/remark-doc-links.d.ts +63 -0
- package/dist/plugins/remark-doc-links.js +122 -0
- package/dist/plugins/remark-unwrap-images.d.ts +11 -0
- package/dist/plugins/remark-unwrap-images.js +25 -0
- package/dist/plugins/remark-youtube.d.ts +22 -0
- package/dist/plugins/remark-youtube.js +84 -0
- package/dist/react/callout.d.ts +37 -0
- package/dist/react/callout.js +113 -0
- package/dist/react/doc-content.d.ts +29 -0
- package/dist/react/doc-content.js +30 -0
- package/dist/react/markdown-components.d.ts +84 -0
- package/dist/react/markdown-components.js +122 -0
- package/dist/react/search-dialog.d.ts +41 -0
- package/dist/react/search-dialog.js +404 -0
- package/dist/react/sidebar.d.ts +29 -0
- package/dist/react/sidebar.js +196 -0
- package/dist/react/skip-link.d.ts +37 -0
- package/dist/react/skip-link.js +37 -0
- package/dist/react/toc.d.ts +35 -0
- package/dist/react/toc.js +87 -0
- package/dist/react/youtube.d.ts +27 -0
- package/dist/react/youtube.js +75 -0
- package/dist/render.d.ts +72 -0
- package/dist/render.js +279 -0
- package/dist/search-index.d.ts +51 -0
- package/dist/search-index.js +274 -0
- package/dist/search-options.d.ts +18 -0
- package/dist/search-options.js +40 -0
- package/dist/source.d.ts +67 -0
- package/dist/source.js +332 -0
- package/dist/styles.css +1033 -0
- package/dist/types.d.ts +334 -0
- package/dist/types.js +0 -0
- package/package.json +166 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useId, useRef, useState } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/sidebar.tsx
|
|
5
|
+
/** Trailing slashes are a routing detail, not a difference in identity. */
|
|
6
|
+
function normalizeHref(href) {
|
|
7
|
+
return href.length > 1 ? href.replace(/\/+$/, "") : href;
|
|
8
|
+
}
|
|
9
|
+
function isActiveHref(pathname, href) {
|
|
10
|
+
return normalizeHref(pathname) === normalizeHref(href);
|
|
11
|
+
}
|
|
12
|
+
/** Whether the active page lives anywhere under this node. */
|
|
13
|
+
function containsActive(node, pathname) {
|
|
14
|
+
switch (node.type) {
|
|
15
|
+
case "page": return isActiveHref(pathname, node.href);
|
|
16
|
+
case "group": return node.href !== void 0 && isActiveHref(pathname, node.href) || node.children.some((child) => containsActive(child, pathname));
|
|
17
|
+
case "link": return !node.external && isActiveHref(pathname, node.href);
|
|
18
|
+
default: return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The docs navigation tree.
|
|
23
|
+
*
|
|
24
|
+
* Prefetch is off by default on the injected link. A full-tree sidebar on a
|
|
25
|
+
* few hundred pages otherwise asks Next to prefetch every route in it —
|
|
26
|
+
* ~1.8 KB brotli each, all of it wasted on the routes nobody clicks.
|
|
27
|
+
*/
|
|
28
|
+
function DocsSidebar({ nav, pathname, Link, label = "Docs", className }) {
|
|
29
|
+
const baseId = useId();
|
|
30
|
+
const [toggled, setToggled] = useState({});
|
|
31
|
+
const lastPathname = useRef(pathname);
|
|
32
|
+
if (lastPathname.current !== pathname) {
|
|
33
|
+
lastPathname.current = pathname;
|
|
34
|
+
setToggled({});
|
|
35
|
+
}
|
|
36
|
+
const handleToggle = (key, isOpen) => {
|
|
37
|
+
setToggled((previous) => ({
|
|
38
|
+
...previous,
|
|
39
|
+
[key]: isOpen
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
42
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
43
|
+
"aria-label": label,
|
|
44
|
+
className: ["wave-docs-sidebar", className].filter(Boolean).join(" "),
|
|
45
|
+
children: /* @__PURE__ */ jsx(NavList, {
|
|
46
|
+
nodes: nav,
|
|
47
|
+
depth: 0,
|
|
48
|
+
keyPrefix: baseId,
|
|
49
|
+
pathname,
|
|
50
|
+
Link,
|
|
51
|
+
toggled,
|
|
52
|
+
onToggle: handleToggle
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function NavList({ nodes, depth, keyPrefix, pathname, Link, toggled, onToggle, id }) {
|
|
57
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
58
|
+
id,
|
|
59
|
+
className: "wave-docs-sidebar__list",
|
|
60
|
+
"data-depth": depth,
|
|
61
|
+
children: nodes.map((node, index) => {
|
|
62
|
+
const key = `${keyPrefix}-${index}`;
|
|
63
|
+
switch (node.type) {
|
|
64
|
+
case "separator": return /* @__PURE__ */ jsx("li", {
|
|
65
|
+
className: "wave-docs-sidebar__separator-item",
|
|
66
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
67
|
+
className: "wave-docs-sidebar__separator",
|
|
68
|
+
children: node.title
|
|
69
|
+
})
|
|
70
|
+
}, key);
|
|
71
|
+
case "link": return /* @__PURE__ */ jsx("li", {
|
|
72
|
+
className: "wave-docs-sidebar__item",
|
|
73
|
+
children: /* @__PURE__ */ jsx(NavLink, {
|
|
74
|
+
href: node.href,
|
|
75
|
+
isExternal: node.external,
|
|
76
|
+
isActive: !node.external && isActiveHref(pathname, node.href),
|
|
77
|
+
Link,
|
|
78
|
+
children: node.title
|
|
79
|
+
})
|
|
80
|
+
}, key);
|
|
81
|
+
case "page": return /* @__PURE__ */ jsx("li", {
|
|
82
|
+
className: "wave-docs-sidebar__item",
|
|
83
|
+
children: /* @__PURE__ */ jsx(NavLink, {
|
|
84
|
+
href: node.href,
|
|
85
|
+
isExternal: false,
|
|
86
|
+
isActive: isActiveHref(pathname, node.href),
|
|
87
|
+
Link,
|
|
88
|
+
children: node.title
|
|
89
|
+
})
|
|
90
|
+
}, key);
|
|
91
|
+
case "group": return /* @__PURE__ */ jsx(NavGroup, {
|
|
92
|
+
node,
|
|
93
|
+
itemKey: key,
|
|
94
|
+
depth,
|
|
95
|
+
pathname,
|
|
96
|
+
Link,
|
|
97
|
+
toggled,
|
|
98
|
+
onToggle
|
|
99
|
+
}, key);
|
|
100
|
+
default: return null;
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function NavGroup({ node, itemKey, depth, pathname, Link, toggled, onToggle }) {
|
|
106
|
+
const listId = `${itemKey}-list`;
|
|
107
|
+
const hasActive = containsActive(node, pathname);
|
|
108
|
+
const isOpen = toggled[itemKey] ?? hasActive;
|
|
109
|
+
const isGroupActive = node.href !== void 0 && isActiveHref(pathname, node.href);
|
|
110
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
111
|
+
className: "wave-docs-sidebar__item",
|
|
112
|
+
"data-open": isOpen ? "" : void 0,
|
|
113
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
114
|
+
className: "wave-docs-sidebar__group-header",
|
|
115
|
+
children: node.href === void 0 ? /* @__PURE__ */ jsxs("button", {
|
|
116
|
+
type: "button",
|
|
117
|
+
className: "wave-docs-sidebar__group-button",
|
|
118
|
+
"aria-expanded": isOpen,
|
|
119
|
+
"aria-controls": isOpen ? listId : void 0,
|
|
120
|
+
onClick: () => onToggle(itemKey, !isOpen),
|
|
121
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
122
|
+
className: "wave-docs-sidebar__group-title",
|
|
123
|
+
children: node.title
|
|
124
|
+
}), /* @__PURE__ */ jsx(Chevron, { isOpen })]
|
|
125
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(NavLink, {
|
|
126
|
+
href: node.href,
|
|
127
|
+
isExternal: false,
|
|
128
|
+
isActive: isGroupActive,
|
|
129
|
+
Link,
|
|
130
|
+
children: node.title
|
|
131
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
132
|
+
type: "button",
|
|
133
|
+
className: "wave-docs-sidebar__group-toggle",
|
|
134
|
+
"aria-expanded": isOpen,
|
|
135
|
+
"aria-controls": isOpen ? listId : void 0,
|
|
136
|
+
"aria-label": `${isOpen ? "Collapse" : "Expand"} ${node.title}`,
|
|
137
|
+
onClick: () => onToggle(itemKey, !isOpen),
|
|
138
|
+
children: /* @__PURE__ */ jsx(Chevron, { isOpen })
|
|
139
|
+
})] })
|
|
140
|
+
}), isOpen ? /* @__PURE__ */ jsx(NavList, {
|
|
141
|
+
id: listId,
|
|
142
|
+
nodes: node.children,
|
|
143
|
+
depth: depth + 1,
|
|
144
|
+
keyPrefix: itemKey,
|
|
145
|
+
pathname,
|
|
146
|
+
Link,
|
|
147
|
+
toggled,
|
|
148
|
+
onToggle
|
|
149
|
+
}) : null]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function NavLink({ href, isExternal, isActive, Link, children }) {
|
|
153
|
+
const className = "wave-docs-sidebar__link";
|
|
154
|
+
if (isExternal) return /* @__PURE__ */ jsxs("a", {
|
|
155
|
+
className,
|
|
156
|
+
href,
|
|
157
|
+
target: "_blank",
|
|
158
|
+
rel: "noopener noreferrer",
|
|
159
|
+
children: [children, /* @__PURE__ */ jsx("span", {
|
|
160
|
+
className: "wave-docs-sr-only",
|
|
161
|
+
children: " (opens in a new tab)"
|
|
162
|
+
})]
|
|
163
|
+
});
|
|
164
|
+
if (Link === void 0) return /* @__PURE__ */ jsx("a", {
|
|
165
|
+
className,
|
|
166
|
+
href,
|
|
167
|
+
"aria-current": isActive ? "page" : void 0,
|
|
168
|
+
children
|
|
169
|
+
});
|
|
170
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
171
|
+
className,
|
|
172
|
+
href,
|
|
173
|
+
prefetch: false,
|
|
174
|
+
"aria-current": isActive ? "page" : void 0,
|
|
175
|
+
children
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function Chevron({ isOpen }) {
|
|
179
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
180
|
+
className: "wave-docs-sidebar__chevron",
|
|
181
|
+
"data-open": isOpen ? "" : void 0,
|
|
182
|
+
viewBox: "0 0 24 24",
|
|
183
|
+
width: "16",
|
|
184
|
+
height: "16",
|
|
185
|
+
fill: "none",
|
|
186
|
+
stroke: "currentColor",
|
|
187
|
+
strokeWidth: "2",
|
|
188
|
+
strokeLinecap: "round",
|
|
189
|
+
strokeLinejoin: "round",
|
|
190
|
+
"aria-hidden": "true",
|
|
191
|
+
focusable: "false",
|
|
192
|
+
children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { DocsSidebar };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/react/skip-link.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The `id` this link targets, and the one `createDocsRoute` puts on its
|
|
5
|
+
* `<article>`.
|
|
6
|
+
*
|
|
7
|
+
* One constant for both halves: the two files spelled the string independently,
|
|
8
|
+
* and a skip link pointing at an id nothing carries scrolls nowhere and focuses
|
|
9
|
+
* nothing — a failure with no symptom until a keyboard user hits it.
|
|
10
|
+
*/
|
|
11
|
+
declare const DOCS_CONTENT_ID = "docs-content";
|
|
12
|
+
interface SkipLinkProps {
|
|
13
|
+
/** Fragment id of the main content region. */
|
|
14
|
+
href?: string | undefined;
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Skip-to-content link: invisible until focused, first in the tab order.
|
|
20
|
+
*
|
|
21
|
+
* A docs sidebar can be a hundred links deep, and without this every keyboard
|
|
22
|
+
* and switch user tabs through all of them on every page to reach the prose.
|
|
23
|
+
* It is three lines of markup and most docs sites still do not have it.
|
|
24
|
+
*
|
|
25
|
+
* Put it first inside `<body>`, and give the target `tabIndex={-1}` — browsers
|
|
26
|
+
* move the *scroll* position on a fragment link but not always the focus, so
|
|
27
|
+
* an unfocusable target leaves focus stranded at the top of the document.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <SkipLink href="#docs-content" />
|
|
32
|
+
* <main id="docs-content" tabIndex={-1}>{children}</main>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare function SkipLink({ href, className, children }: SkipLinkProps): ReactNode;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { DOCS_CONTENT_ID, SkipLink, SkipLinkProps };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
//#region src/react/skip-link.tsx
|
|
3
|
+
/**
|
|
4
|
+
* The `id` this link targets, and the one `createDocsRoute` puts on its
|
|
5
|
+
* `<article>`.
|
|
6
|
+
*
|
|
7
|
+
* One constant for both halves: the two files spelled the string independently,
|
|
8
|
+
* and a skip link pointing at an id nothing carries scrolls nowhere and focuses
|
|
9
|
+
* nothing — a failure with no symptom until a keyboard user hits it.
|
|
10
|
+
*/
|
|
11
|
+
const DOCS_CONTENT_ID = "docs-content";
|
|
12
|
+
/**
|
|
13
|
+
* Skip-to-content link: invisible until focused, first in the tab order.
|
|
14
|
+
*
|
|
15
|
+
* A docs sidebar can be a hundred links deep, and without this every keyboard
|
|
16
|
+
* and switch user tabs through all of them on every page to reach the prose.
|
|
17
|
+
* It is three lines of markup and most docs sites still do not have it.
|
|
18
|
+
*
|
|
19
|
+
* Put it first inside `<body>`, and give the target `tabIndex={-1}` — browsers
|
|
20
|
+
* move the *scroll* position on a fragment link but not always the focus, so
|
|
21
|
+
* an unfocusable target leaves focus stranded at the top of the document.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <SkipLink href="#docs-content" />
|
|
26
|
+
* <main id="docs-content" tabIndex={-1}>{children}</main>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function SkipLink({ href = `#${DOCS_CONTENT_ID}`, className, children = "Skip to content" }) {
|
|
30
|
+
return /* @__PURE__ */ jsx("a", {
|
|
31
|
+
href,
|
|
32
|
+
className: ["wave-docs-skip-link", className].filter(Boolean).join(" "),
|
|
33
|
+
children
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { DOCS_CONTENT_ID, SkipLink };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TocEntry } from "../types.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/react/toc.d.ts
|
|
4
|
+
interface DocsTocProps {
|
|
5
|
+
/** Headings from `@waveso/docs/render`, already nested by depth. */
|
|
6
|
+
entries: TocEntry[];
|
|
7
|
+
/** Accessible name for the landmark. */
|
|
8
|
+
label?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Region of the viewport that counts as "current", as an
|
|
11
|
+
* `IntersectionObserver` root margin. The default reserves 80px for a sticky
|
|
12
|
+
* header and ignores the bottom 60% of the screen, so the active entry
|
|
13
|
+
* tracks what you are reading rather than what has scrolled into view.
|
|
14
|
+
*
|
|
15
|
+
* Only `px` and `%` are legal here — `IntersectionObserver` throws on any
|
|
16
|
+
* other unit, `rem` included.
|
|
17
|
+
*/
|
|
18
|
+
rootMargin?: string | undefined;
|
|
19
|
+
className?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* On-this-page navigation with scrollspy.
|
|
23
|
+
*
|
|
24
|
+
* The ids come from the same `rehype-slug` pass that annotated the document, so
|
|
25
|
+
* `getElementById` matches by construction — no second slugging pass to drift
|
|
26
|
+
* out of sync on duplicate headings.
|
|
27
|
+
*
|
|
28
|
+
* Scrolling itself is left to the browser: the links are real anchors, and
|
|
29
|
+
* smooth scrolling is applied in CSS under
|
|
30
|
+
* `@media (prefers-reduced-motion: no-preference)`. Doing it in JavaScript
|
|
31
|
+
* means reimplementing that check, and getting it wrong makes people ill.
|
|
32
|
+
*/
|
|
33
|
+
declare function DocsToc({ entries, label, rootMargin, className }: DocsTocProps): ReactNode;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { DocsToc, DocsTocProps };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/toc.tsx
|
|
5
|
+
const DEFAULT_ROOT_MARGIN = "-80px 0px -60% 0px";
|
|
6
|
+
function flattenTocIds(entries) {
|
|
7
|
+
const ids = [];
|
|
8
|
+
const walk = (list) => {
|
|
9
|
+
for (const entry of list) {
|
|
10
|
+
ids.push(entry.id);
|
|
11
|
+
walk(entry.children);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
walk(entries);
|
|
15
|
+
return ids;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* On-this-page navigation with scrollspy.
|
|
19
|
+
*
|
|
20
|
+
* The ids come from the same `rehype-slug` pass that annotated the document, so
|
|
21
|
+
* `getElementById` matches by construction — no second slugging pass to drift
|
|
22
|
+
* out of sync on duplicate headings.
|
|
23
|
+
*
|
|
24
|
+
* Scrolling itself is left to the browser: the links are real anchors, and
|
|
25
|
+
* smooth scrolling is applied in CSS under
|
|
26
|
+
* `@media (prefers-reduced-motion: no-preference)`. Doing it in JavaScript
|
|
27
|
+
* means reimplementing that check, and getting it wrong makes people ill.
|
|
28
|
+
*/
|
|
29
|
+
function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MARGIN, className }) {
|
|
30
|
+
const ids = useMemo(() => flattenTocIds(entries), [entries]);
|
|
31
|
+
const [activeId, setActiveId] = useState(void 0);
|
|
32
|
+
const lastIds = useRef(ids);
|
|
33
|
+
if (lastIds.current !== ids) {
|
|
34
|
+
lastIds.current = ids;
|
|
35
|
+
setActiveId(void 0);
|
|
36
|
+
}
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (ids.length === 0 || typeof IntersectionObserver === "undefined") return;
|
|
39
|
+
const visible = /* @__PURE__ */ new Set();
|
|
40
|
+
const observer = new IntersectionObserver((records) => {
|
|
41
|
+
for (const record of records) if (record.isIntersecting) visible.add(record.target.id);
|
|
42
|
+
else visible.delete(record.target.id);
|
|
43
|
+
const next = ids.find((id) => visible.has(id));
|
|
44
|
+
if (next !== void 0) setActiveId(next);
|
|
45
|
+
}, {
|
|
46
|
+
rootMargin,
|
|
47
|
+
threshold: 0
|
|
48
|
+
});
|
|
49
|
+
for (const id of ids) {
|
|
50
|
+
const element = document.getElementById(id);
|
|
51
|
+
if (element !== null) observer.observe(element);
|
|
52
|
+
}
|
|
53
|
+
return () => observer.disconnect();
|
|
54
|
+
}, [ids, rootMargin]);
|
|
55
|
+
if (entries.length === 0) return null;
|
|
56
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
57
|
+
"aria-label": label,
|
|
58
|
+
className: ["wave-docs-toc", className].filter(Boolean).join(" "),
|
|
59
|
+
children: /* @__PURE__ */ jsx(TocList, {
|
|
60
|
+
entries,
|
|
61
|
+
activeId,
|
|
62
|
+
onSelect: setActiveId
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function TocList({ entries, activeId, onSelect }) {
|
|
67
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
68
|
+
className: "wave-docs-toc__list",
|
|
69
|
+
children: entries.map((entry) => /* @__PURE__ */ jsxs("li", {
|
|
70
|
+
className: "wave-docs-toc__item",
|
|
71
|
+
children: [/* @__PURE__ */ jsx("a", {
|
|
72
|
+
className: "wave-docs-toc__link",
|
|
73
|
+
href: `#${entry.id}`,
|
|
74
|
+
"data-depth": entry.depth,
|
|
75
|
+
"aria-current": entry.id === activeId ? "location" : void 0,
|
|
76
|
+
onClick: () => onSelect(entry.id),
|
|
77
|
+
children: entry.text
|
|
78
|
+
}), entry.children.length > 0 ? /* @__PURE__ */ jsx(TocList, {
|
|
79
|
+
entries: entry.children,
|
|
80
|
+
activeId,
|
|
81
|
+
onSelect
|
|
82
|
+
}) : null]
|
|
83
|
+
}, entry.id))
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { DocsToc };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/react/youtube.d.ts
|
|
3
|
+
interface YouTubeProps {
|
|
4
|
+
/** The 11-character video id, e.g. `dQw4w9WgXcQ`. */
|
|
5
|
+
id?: string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Accessible name for the player. Markdown carries no video title, so the
|
|
8
|
+
* fallback is generic — pass a real one where you have it.
|
|
9
|
+
*/
|
|
10
|
+
title?: string | undefined;
|
|
11
|
+
className?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Click-to-load YouTube embed.
|
|
15
|
+
*
|
|
16
|
+
* An eager `<iframe>` costs ~137 KB of embed document plus ~580 KB gzipped of
|
|
17
|
+
* player JavaScript, on every page view, whether or not anyone presses play.
|
|
18
|
+
* A facade costs one ~15 KB JPEG and loads the rest on demand. On a docs page
|
|
19
|
+
* with three videos that is the difference between a good Lighthouse score and
|
|
20
|
+
* a bad one.
|
|
21
|
+
*
|
|
22
|
+
* `hqdefault.jpg` rather than `maxresdefault.jpg` deliberately: maxres does not
|
|
23
|
+
* exist for uploads below 1280×720 and 404s to a broken image with no fallback.
|
|
24
|
+
*/
|
|
25
|
+
declare function YouTube({ id, title, className }: YouTubeProps): ReactNode;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { YouTube, YouTubeProps };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/youtube.tsx
|
|
5
|
+
const DEFAULT_TITLE = "YouTube video player";
|
|
6
|
+
/**
|
|
7
|
+
* Click-to-load YouTube embed.
|
|
8
|
+
*
|
|
9
|
+
* An eager `<iframe>` costs ~137 KB of embed document plus ~580 KB gzipped of
|
|
10
|
+
* player JavaScript, on every page view, whether or not anyone presses play.
|
|
11
|
+
* A facade costs one ~15 KB JPEG and loads the rest on demand. On a docs page
|
|
12
|
+
* with three videos that is the difference between a good Lighthouse score and
|
|
13
|
+
* a bad one.
|
|
14
|
+
*
|
|
15
|
+
* `hqdefault.jpg` rather than `maxresdefault.jpg` deliberately: maxres does not
|
|
16
|
+
* exist for uploads below 1280×720 and 404s to a broken image with no fallback.
|
|
17
|
+
*/
|
|
18
|
+
function YouTube({ id, title, className }) {
|
|
19
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
20
|
+
if (!id) return null;
|
|
21
|
+
const safeId = encodeURIComponent(id);
|
|
22
|
+
const label = title?.trim() || DEFAULT_TITLE;
|
|
23
|
+
const rootClassName = ["wave-docs-youtube", className].filter(Boolean).join(" ");
|
|
24
|
+
if (isPlaying) return /* @__PURE__ */ jsx("div", {
|
|
25
|
+
className: rootClassName,
|
|
26
|
+
children: /* @__PURE__ */ jsx("iframe", {
|
|
27
|
+
className: "wave-docs-youtube__frame",
|
|
28
|
+
src: `https://www.youtube-nocookie.com/embed/${safeId}?autoplay=1&rel=0`,
|
|
29
|
+
title: label,
|
|
30
|
+
loading: "lazy",
|
|
31
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
32
|
+
allowFullScreen: true,
|
|
33
|
+
ref: (node) => {
|
|
34
|
+
node?.focus();
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
return /* @__PURE__ */ jsx("div", {
|
|
39
|
+
className: rootClassName,
|
|
40
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
41
|
+
type: "button",
|
|
42
|
+
className: "wave-docs-youtube__facade",
|
|
43
|
+
onClick: () => setIsPlaying(true),
|
|
44
|
+
"aria-label": `Play video: ${label}`,
|
|
45
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
46
|
+
className: "wave-docs-youtube__thumbnail",
|
|
47
|
+
src: `https://i.ytimg.com/vi/${safeId}/hqdefault.jpg`,
|
|
48
|
+
alt: "",
|
|
49
|
+
width: 480,
|
|
50
|
+
height: 360,
|
|
51
|
+
loading: "lazy",
|
|
52
|
+
decoding: "async"
|
|
53
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
54
|
+
className: "wave-docs-youtube__play",
|
|
55
|
+
"aria-hidden": "true",
|
|
56
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
57
|
+
viewBox: "0 0 68 48",
|
|
58
|
+
width: "68",
|
|
59
|
+
height: "48",
|
|
60
|
+
"aria-hidden": "true",
|
|
61
|
+
focusable: "false",
|
|
62
|
+
children: [/* @__PURE__ */ jsx("path", {
|
|
63
|
+
className: "wave-docs-youtube__play-bg",
|
|
64
|
+
d: "M66.52 7.74a8 8 0 0 0-5.65-5.66C56.1.99 34 .99 34 .99s-22.1 0-26.87 1.09a8 8 0 0 0-5.65 5.66C.39 12.51.39 24 .39 24s0 11.49 1.09 16.26a8 8 0 0 0 5.65 5.66C11.9 47 34 47 34 47s22.1 0 26.87-1.08a8 8 0 0 0 5.65-5.66C67.61 35.49 67.61 24 67.61 24s0-11.49-1.09-16.26"
|
|
65
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
66
|
+
className: "wave-docs-youtube__play-arrow",
|
|
67
|
+
d: "M27 34V14l17 10z"
|
|
68
|
+
})]
|
|
69
|
+
})
|
|
70
|
+
})]
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { YouTube };
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { DocFile, DocFrontmatter, ImageResolver, LinkResolver, RenderedDoc, ResolvedDocsConfig } from "./types.js";
|
|
2
|
+
import { DocsHighlighter, DocsLang, DocsThemes } from "./highlighter.js";
|
|
3
|
+
//#region src/render.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The parts of {@link ResolvedDocsConfig} rendering actually depends on.
|
|
6
|
+
*
|
|
7
|
+
* Declared as a `Pick` so a full resolved config passes straight through: the
|
|
8
|
+
* host resolves configuration once, with `resolveDocsConfig`, and hands the
|
|
9
|
+
* same object to the source walk and to the renderer. Nothing here re-applies
|
|
10
|
+
* defaults, so the two cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
type DocsRendererConfig = Pick<ResolvedDocsConfig, 'basePath' | 'assertLinks'>;
|
|
13
|
+
interface DocsRendererOptions {
|
|
14
|
+
config: DocsRendererConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Reuse an existing highlighter — the escape hatch for grammars and themes
|
|
17
|
+
* outside the curated set. Defaults to {@link createDocsHighlighter}.
|
|
18
|
+
*/
|
|
19
|
+
highlighter?: DocsHighlighter | Promise<DocsHighlighter>;
|
|
20
|
+
/** Grammars to load, when building the default highlighter. */
|
|
21
|
+
langs?: readonly DocsLang[];
|
|
22
|
+
/** Theme pair. Defaults to {@link DEFAULT_DOCS_THEMES}. */
|
|
23
|
+
themes?: DocsThemes;
|
|
24
|
+
/**
|
|
25
|
+
* Prepend an `<h1>` built from `frontmatter.title` when the markdown body
|
|
26
|
+
* has none. Defaults to `true`.
|
|
27
|
+
*
|
|
28
|
+
* Turn it off only if your layout renders the page title itself: a document
|
|
29
|
+
* with no `h1` fails `page-has-heading-one` and leaves the heading outline
|
|
30
|
+
* starting at `h2`, and markdown that repeats the frontmatter title as `# `
|
|
31
|
+
* is a duplication authors forget to keep in step.
|
|
32
|
+
*/
|
|
33
|
+
titleHeading?: boolean;
|
|
34
|
+
/** Replaces the built-in markdown-link resolution. */
|
|
35
|
+
linkResolver?: LinkResolver;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves image `src` to a public URL and intrinsic dimensions, so
|
|
38
|
+
* `next/image` can render without `fill`. Images are left untouched when
|
|
39
|
+
* omitted, or when the resolver returns `undefined`.
|
|
40
|
+
*/
|
|
41
|
+
imageResolver?: ImageResolver;
|
|
42
|
+
/**
|
|
43
|
+
* Every route the site publishes, used by `assertLinks`. Read at render
|
|
44
|
+
* time, so a host may pass a set it populates during the source walk.
|
|
45
|
+
*
|
|
46
|
+
* Without it only unresolvable links can be caught; with it, links to pages
|
|
47
|
+
* that simply do not exist are caught too.
|
|
48
|
+
*/
|
|
49
|
+
knownRoutes?: ReadonlySet<string>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Renders {@link DocFile}s. Build one per process and reuse it.
|
|
53
|
+
*
|
|
54
|
+
* The frontmatter type parameter sits on `render`, not on the interface: the
|
|
55
|
+
* renderer reads only `frontmatter.title` and passes the rest through, so one
|
|
56
|
+
* renderer serves files parsed by any schema — which is what lets a host build
|
|
57
|
+
* the processor and the highlighter once. A renderer-level parameter would
|
|
58
|
+
* force a second highlighter per frontmatter shape and buy nothing.
|
|
59
|
+
*/
|
|
60
|
+
interface DocsRenderer {
|
|
61
|
+
render<TFrontmatter extends DocFrontmatter>(file: DocFile<TFrontmatter>): Promise<RenderedDoc<TFrontmatter>>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a renderer.
|
|
65
|
+
*
|
|
66
|
+
* The processor and the highlighter are built once, eagerly, and shared by
|
|
67
|
+
* every call to `render`. Constructing them per file is the difference between
|
|
68
|
+
* a docs build that takes a second and one that takes a minute.
|
|
69
|
+
*/
|
|
70
|
+
declare function createDocsRenderer(options: DocsRendererOptions): DocsRenderer;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { DocsRenderer, DocsRendererConfig, DocsRendererOptions, createDocsRenderer };
|