@waveso/docs 0.6.0 → 0.7.1
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/CHANGELOG.md +180 -1
- package/README.md +115 -38
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.js +7 -1
- package/dist/next.d.ts +34 -27
- package/dist/next.js +20 -11
- package/dist/react/hero.d.ts +19 -0
- package/dist/react/hero.js +44 -0
- package/dist/react/layout.d.ts +1 -3
- package/dist/react/layout.js +17 -54
- package/dist/react/nav.d.ts +11 -11
- package/dist/react/nav.js +106 -58
- package/dist/react/next-nav.d.ts +5 -1
- package/dist/react/next-nav.js +4 -2
- package/dist/react/shell-labels.d.ts +8 -2
- package/dist/react/toc.d.ts +13 -3
- package/dist/react/toc.js +17 -10
- package/dist/render.js +2 -1
- package/dist/source.js +1 -1
- package/dist/styles.css +1163 -238
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
package/dist/next.d.ts
CHANGED
|
@@ -96,27 +96,25 @@ interface DocsRouteOptions<TFrontmatter extends DocFrontmatter = DocFrontmatter>
|
|
|
96
96
|
/**
|
|
97
97
|
* Props for {@link DocsRoute.Layout}.
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
* turned out to be reachable already: an announcement banner renders
|
|
101
|
-
* `<docs.Layout>` in your own `layout.tsx`, because this does not own
|
|
102
|
-
* a content footer goes inside `children`; and sidebar links, social
|
|
103
|
-
* separators are `DocNavNode`s authored in `meta.json`.
|
|
104
|
-
*
|
|
99
|
+
* Three, and one of them is `children`. Everything else a docs shell is asked
|
|
100
|
+
* for turned out to be reachable already: an announcement banner renders
|
|
101
|
+
* *above* `<docs.Layout>` in your own `layout.tsx`, because this does not own
|
|
102
|
+
* `<body>`; a content footer goes inside `children`; and sidebar links, social
|
|
103
|
+
* icons and separators are `DocNavNode`s authored in `meta.json`. A theme
|
|
104
|
+
* toggle and a repository link go in the layout you write around this one — the
|
|
105
|
+
* host wraps `docs.Layout` exactly as it already wraps `<html>` and `<body>`,
|
|
106
|
+
* so there is no region only this package can reach.
|
|
105
107
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
+
* The one region a host cannot reach through `docs.Layout` is *inside* the
|
|
109
|
+
* sidebar, and the exported primitives are the answer for that: `DocsSidebar`,
|
|
110
|
+
* `DocsToc`, `DocContent` and `SkipLink` compose into a layout of your own.
|
|
111
|
+
*
|
|
112
|
+
* A `slots` map was the alternative, and shipping none is the reversible half —
|
|
113
|
+
* a map can be added the day something needs one, a map that shipped cannot be
|
|
114
|
+
* taken back.
|
|
108
115
|
*/
|
|
109
116
|
interface DocsLayoutProps {
|
|
110
117
|
children: ReactNode;
|
|
111
|
-
/**
|
|
112
|
-
* Brand at the header start. A string, or your own logo component.
|
|
113
|
-
*
|
|
114
|
-
* `ReactNode`, so it cannot also serve as the `<title>` or as the header's
|
|
115
|
-
* accessible name; the landmark carries a fixed label instead.
|
|
116
|
-
*/
|
|
117
|
-
title?: ReactNode;
|
|
118
|
-
/** Header end, after search: a theme toggle, a version switcher, a link. */
|
|
119
|
-
actions?: ReactNode;
|
|
120
118
|
/**
|
|
121
119
|
* The search trigger. Defaults to on, and the URL is always derived.
|
|
122
120
|
*
|
|
@@ -318,24 +316,33 @@ interface DocsRoute<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
|
|
|
318
316
|
* export default docs.Layout;
|
|
319
317
|
* ```
|
|
320
318
|
*
|
|
321
|
-
* Or, with your own chrome
|
|
319
|
+
* Or, with your own chrome *around* it — the same layout file `<html>` and
|
|
320
|
+
* `<body>` live in, and `SiteHeader` is yours:
|
|
322
321
|
*
|
|
323
322
|
* ```tsx
|
|
324
323
|
* export default function DocsLayout({ children }: { children: ReactNode }) {
|
|
325
324
|
* return (
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
325
|
+
* <>
|
|
326
|
+
* <SiteHeader />
|
|
327
|
+
* <docs.Layout search={{ placeholder: 'Search the docs' }}>
|
|
328
|
+
* {children}
|
|
329
|
+
* </docs.Layout>
|
|
330
|
+
* </>
|
|
329
331
|
* );
|
|
330
332
|
* }
|
|
331
333
|
* ```
|
|
332
334
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
335
|
+
* If that header of yours is sticky, say how tall it is once —
|
|
336
|
+
* `--wave-docs-chrome-offset: 4rem` — and our sticky columns start below it.
|
|
337
|
+
*
|
|
338
|
+
* It owns the skip link, the sidebar — one shell at every width, holding the
|
|
339
|
+
* navigation and the 44px strip that moves it — the search trigger and the
|
|
340
|
+
* grid.
|
|
341
|
+
* It reads `source.nav()` and `searchIndexUrl` itself, so there is no nav to
|
|
342
|
+
* fetch and no URL to pass. It does **not** own the table of contents: a Next
|
|
343
|
+
* layout receives `{children, params}` and cannot know which page is
|
|
344
|
+
* rendering, so `docs.Page` emits the TOC as its second child and the grid
|
|
345
|
+
* places it.
|
|
339
346
|
*
|
|
340
347
|
* Your `layout.tsx` stays a Server Component. The two pieces that need a
|
|
341
348
|
* client — the nav's `usePathname`, the search dialog — carry their own
|
package/dist/next.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mapPooled } from "./map-pooled.js";
|
|
|
6
6
|
import { findFunctionValuedOptions } from "./search-options.js";
|
|
7
7
|
import { createMarkdownComponents } from "./react/markdown-components.js";
|
|
8
8
|
import { DocContent } from "./react/doc-content.js";
|
|
9
|
+
import { DocsHero } from "./react/hero.js";
|
|
9
10
|
import { DocsToc } from "./react/toc.js";
|
|
10
11
|
import { wrapNextLink } from "./react/link-adapter.js";
|
|
11
12
|
import { createDocsRenderer } from "./render.js";
|
|
@@ -172,11 +173,15 @@ async function buildNextComponents(labels) {
|
|
|
172
173
|
const [linkMod, imageMod] = await Promise.all([importNext(() => import("next/link"), "next/link"), importNext(() => import("next/image"), "next/image")]);
|
|
173
174
|
const NextLink = readDefaultExport(linkMod, "next/link");
|
|
174
175
|
const NextImage = readDefaultExport(imageMod, "next/image");
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
const link = wrapNextLink(NextLink);
|
|
177
|
+
return {
|
|
178
|
+
components: createMarkdownComponents({
|
|
179
|
+
Link: link,
|
|
180
|
+
Image: wrapNextImage(NextImage),
|
|
181
|
+
...labels === void 0 ? {} : { labels }
|
|
182
|
+
}),
|
|
183
|
+
link
|
|
184
|
+
};
|
|
180
185
|
}
|
|
181
186
|
/**
|
|
182
187
|
* The named subset of `labels`, or `undefined` when none of it is set.
|
|
@@ -220,7 +225,7 @@ function pickLabels(labels, map) {
|
|
|
220
225
|
*/
|
|
221
226
|
function serializableSearchOptions(candidate) {
|
|
222
227
|
const functions = findFunctionValuedOptions(candidate);
|
|
223
|
-
if (functions.length > 0) throw docsError("invalid-config", `the search dialog cannot be given MiniSearch functions from a server component: ${functions.map((name) => `\`miniSearchOptions.${name}\``).join(", ")}. \`docs.Layout\` renders the dialog as a client component, so its props are serialised on the way across and React rejects a function with "Functions cannot be passed directly to Client Components" while prerendering. Keep the function on \`createDocsRoute\` so the index is still built with it, pass \`search={false}\` to \`docs.Layout\`, and render the dialog yourself from a \`'use client'\` module that imports the same function — \`<DocsSearch indexUrl={docs.searchIndexUrl} miniSearchOptions={{ processTerm }} />\`
|
|
228
|
+
if (functions.length > 0) throw docsError("invalid-config", `the search dialog cannot be given MiniSearch functions from a server component: ${functions.map((name) => `\`miniSearchOptions.${name}\``).join(", ")}. \`docs.Layout\` renders the dialog as a client component, so its props are serialised on the way across and React rejects a function with "Functions cannot be passed directly to Client Components" while prerendering. Keep the function on \`createDocsRoute\` so the index is still built with it, pass \`search={false}\` to \`docs.Layout\`, and render the dialog yourself from a \`'use client'\` module that imports the same function — \`<DocsSearch indexUrl={docs.searchIndexUrl} miniSearchOptions={{ processTerm }} />\` in your own layout. Serialisable overrides (\`storeFields\`, \`boost\`, \`searchOptions.fuzzy\`) need none of this and are forwarded as before.`);
|
|
224
229
|
return candidate;
|
|
225
230
|
}
|
|
226
231
|
/**
|
|
@@ -425,12 +430,18 @@ function createDocsRoute(options) {
|
|
|
425
430
|
async function renderRoute(segments) {
|
|
426
431
|
const doc = await getPage(segments);
|
|
427
432
|
if (doc === void 0) return (await loadNotFound())();
|
|
428
|
-
const components = await loadComponents();
|
|
433
|
+
const { components, link } = await loadComponents();
|
|
429
434
|
return createElement(Fragment, null, createElement("main", {
|
|
430
435
|
className: "wave-docs-layout__main",
|
|
431
436
|
id: DOCS_CONTENT_ID,
|
|
432
437
|
tabIndex: -1
|
|
433
|
-
}, createElement(
|
|
438
|
+
}, (doc.frontmatter.actions?.length ?? 0) === 0 ? null : createElement(DocsHero, {
|
|
439
|
+
title: doc.frontmatter.title,
|
|
440
|
+
...doc.frontmatter.description === void 0 ? {} : { description: doc.frontmatter.description },
|
|
441
|
+
...doc.frontmatter.actions === void 0 ? {} : { actions: doc.frontmatter.actions },
|
|
442
|
+
Link: link,
|
|
443
|
+
...routeLabels?.externalLink === void 0 ? {} : { externalLabel: routeLabels.externalLink }
|
|
444
|
+
}), createElement(DocContent, {
|
|
434
445
|
hast: doc.hast,
|
|
435
446
|
components: {
|
|
436
447
|
...components,
|
|
@@ -457,7 +468,7 @@ function createDocsRoute(options) {
|
|
|
457
468
|
async IndexPage() {
|
|
458
469
|
return renderRoute([]);
|
|
459
470
|
},
|
|
460
|
-
async Layout({ children,
|
|
471
|
+
async Layout({ children, search, labels }) {
|
|
461
472
|
const { DocsLayoutShell } = await import("./react/layout.js");
|
|
462
473
|
const host = search === true || search === void 0 || search === false ? void 0 : search;
|
|
463
474
|
const requestedOptions = host?.miniSearchOptions ?? options.miniSearchOptions;
|
|
@@ -474,8 +485,6 @@ function createDocsRoute(options) {
|
|
|
474
485
|
nav: await requestScopedSource.nav(),
|
|
475
486
|
searchIndexUrl,
|
|
476
487
|
search: searchProps,
|
|
477
|
-
...title === void 0 ? {} : { title },
|
|
478
|
-
...actions === void 0 ? {} : { actions },
|
|
479
488
|
...shellLabels === void 0 ? {} : { labels: shellLabels }
|
|
480
489
|
});
|
|
481
490
|
},
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DocAction } from "../types.js";
|
|
2
|
+
import { DocsLinkComponent } from "./markdown-components.js";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
//#region src/react/hero.d.ts
|
|
5
|
+
interface DocsHeroProps {
|
|
6
|
+
/** `frontmatter.title`, rendered as the page's `<h1>`. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** `frontmatter.description`. Omitted rather than rendered empty. */
|
|
9
|
+
description?: string | undefined;
|
|
10
|
+
/** `frontmatter.actions`. An empty list renders no `<nav>` at all. */
|
|
11
|
+
actions?: readonly DocAction[] | undefined;
|
|
12
|
+
/** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
|
|
13
|
+
Link?: DocsLinkComponent | undefined;
|
|
14
|
+
/** Screen-reader suffix on a link that opens elsewhere. */
|
|
15
|
+
externalLabel?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
declare function DocsHero({ title, description, actions, Link, externalLabel }: DocsHeroProps): ReactNode;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { DocsHero, DocsHeroProps };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { opensInNewTab } from "../safe-href.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/react/hero.tsx
|
|
4
|
+
function DocsHero({ title, description, actions, Link, externalLabel = "(opens in a new tab)" }) {
|
|
5
|
+
const Anchor = Link ?? "a";
|
|
6
|
+
return /* @__PURE__ */ jsx("header", {
|
|
7
|
+
className: "wave-docs-hero",
|
|
8
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
9
|
+
className: "wave-docs-hero__body",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ jsx("h1", {
|
|
12
|
+
className: "wave-docs-hero__title",
|
|
13
|
+
children: title
|
|
14
|
+
}),
|
|
15
|
+
description === void 0 || description === "" ? null : /* @__PURE__ */ jsx("p", {
|
|
16
|
+
className: "wave-docs-hero__tagline",
|
|
17
|
+
children: description
|
|
18
|
+
}),
|
|
19
|
+
actions === void 0 || actions.length === 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
20
|
+
className: "wave-docs-hero__actions",
|
|
21
|
+
children: actions.map((action, index) => {
|
|
22
|
+
const external = opensInNewTab(action.href);
|
|
23
|
+
const variant = action.variant ?? (index === 0 ? "primary" : "secondary");
|
|
24
|
+
return /* @__PURE__ */ jsxs(external ? "a" : Anchor, {
|
|
25
|
+
href: action.href,
|
|
26
|
+
className: "wave-docs-hero__action",
|
|
27
|
+
"data-variant": variant,
|
|
28
|
+
...external ? {
|
|
29
|
+
target: "_blank",
|
|
30
|
+
rel: "noreferrer"
|
|
31
|
+
} : {},
|
|
32
|
+
children: [action.label, external ? /* @__PURE__ */ jsx("span", {
|
|
33
|
+
className: "wave-docs-sr-only",
|
|
34
|
+
children: ` ${externalLabel}`
|
|
35
|
+
}) : null]
|
|
36
|
+
}, action.href);
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { DocsHero };
|
package/dist/react/layout.d.ts
CHANGED
|
@@ -27,8 +27,6 @@ interface DocsLayoutShellProps {
|
|
|
27
27
|
children: ReactNode;
|
|
28
28
|
nav: DocNavNode[];
|
|
29
29
|
searchIndexUrl: string;
|
|
30
|
-
title?: ReactNode;
|
|
31
|
-
actions?: ReactNode;
|
|
32
30
|
/**
|
|
33
31
|
* `false` to omit the trigger; an object to configure it.
|
|
34
32
|
*
|
|
@@ -56,6 +54,6 @@ interface DocsLayoutShellProps {
|
|
|
56
54
|
*/
|
|
57
55
|
labels?: DocsLabels | undefined;
|
|
58
56
|
}
|
|
59
|
-
declare function DocsLayoutShell({ children, nav, searchIndexUrl,
|
|
57
|
+
declare function DocsLayoutShell({ children, nav, searchIndexUrl, search, labels }: DocsLayoutShellProps): ReactNode;
|
|
60
58
|
//#endregion
|
|
61
59
|
export { DocsLayoutSearchProps, DocsLayoutShell, DocsLayoutShellProps };
|
package/dist/react/layout.js
CHANGED
|
@@ -1,68 +1,31 @@
|
|
|
1
1
|
import { DocsSearch } from "./next-search.js";
|
|
2
2
|
import { resolveLabels } from "./shell-labels.js";
|
|
3
|
-
import { DOCS_NAV_ID } from "./nav.js";
|
|
4
3
|
import { DocsNextNav } from "./next-nav.js";
|
|
5
4
|
import { SkipLink } from "./skip-link.js";
|
|
6
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
6
|
//#region src/react/layout.tsx
|
|
8
|
-
function DocsLayoutShell({ children, nav, searchIndexUrl,
|
|
7
|
+
function DocsLayoutShell({ children, nav, searchIndexUrl, search = true, labels }) {
|
|
9
8
|
const text = resolveLabels(labels);
|
|
10
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
11
|
-
|
|
12
|
-
/* @__PURE__ */
|
|
13
|
-
className: "wave-docs-layout__header",
|
|
14
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
15
|
-
className: "wave-docs-layout__header-inner",
|
|
16
|
-
children: [
|
|
17
|
-
/* @__PURE__ */ jsx("button", {
|
|
18
|
-
type: "button",
|
|
19
|
-
className: "wave-docs-layout__nav-trigger",
|
|
20
|
-
"aria-label": text.openNav,
|
|
21
|
-
command: "show-modal",
|
|
22
|
-
commandfor: DOCS_NAV_ID,
|
|
23
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
24
|
-
"aria-hidden": "true",
|
|
25
|
-
viewBox: "0 0 16 16",
|
|
26
|
-
width: "18",
|
|
27
|
-
height: "18",
|
|
28
|
-
fill: "none",
|
|
29
|
-
stroke: "currentColor",
|
|
30
|
-
strokeWidth: "1.5",
|
|
31
|
-
strokeLinecap: "round",
|
|
32
|
-
children: /* @__PURE__ */ jsx("path", { d: "M2.5 4h11M2.5 8h11M2.5 12h11" })
|
|
33
|
-
})
|
|
34
|
-
}),
|
|
35
|
-
title === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
36
|
-
className: "wave-docs-layout__title",
|
|
37
|
-
children: title
|
|
38
|
-
}),
|
|
39
|
-
search === false ? null : /* @__PURE__ */ jsx(DocsSearch, {
|
|
40
|
-
indexUrl: searchIndexUrl,
|
|
41
|
-
...search === true ? {} : search,
|
|
42
|
-
className: ["wave-docs-layout__search", search === true ? void 0 : search?.className].filter(Boolean).join(" ")
|
|
43
|
-
}),
|
|
44
|
-
actions === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
45
|
-
className: "wave-docs-layout__actions",
|
|
46
|
-
children: actions
|
|
47
|
-
})
|
|
48
|
-
]
|
|
49
|
-
})
|
|
50
|
-
}),
|
|
51
|
-
/* @__PURE__ */ jsxs("div", {
|
|
9
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SkipLink, { children: text.skipToContent }), /* @__PURE__ */ jsx("div", {
|
|
10
|
+
className: "wave-docs-shell",
|
|
11
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
52
12
|
className: "wave-docs-layout",
|
|
53
|
-
children: [/* @__PURE__ */ jsx(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
13
|
+
children: [/* @__PURE__ */ jsx(DocsNextNav, {
|
|
14
|
+
nav,
|
|
15
|
+
label: text.nav,
|
|
16
|
+
closeLabel: text.closeNav,
|
|
17
|
+
openLabel: text.openNav,
|
|
18
|
+
...labels?.expandGroup === void 0 ? {} : { expandGroup: labels.expandGroup },
|
|
19
|
+
...labels?.collapseGroup === void 0 ? {} : { collapseGroup: labels.collapseGroup },
|
|
20
|
+
...labels?.externalLink === void 0 ? {} : { externalLink: labels.externalLink },
|
|
21
|
+
children: search === false ? null : /* @__PURE__ */ jsx(DocsSearch, {
|
|
22
|
+
indexUrl: searchIndexUrl,
|
|
23
|
+
...search === true ? {} : search,
|
|
24
|
+
className: ["wave-docs-layout__search", search === true ? void 0 : search?.className].filter(Boolean).join(" ")
|
|
62
25
|
})
|
|
63
26
|
}), children]
|
|
64
27
|
})
|
|
65
|
-
] });
|
|
28
|
+
})] });
|
|
66
29
|
}
|
|
67
30
|
//#endregion
|
|
68
31
|
export { DocsLayoutShell };
|
package/dist/react/nav.d.ts
CHANGED
|
@@ -2,14 +2,7 @@ import { DocNavNode } from "../types.js";
|
|
|
2
2
|
import { DocsLinkComponent } from "./markdown-components.js";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
//#region src/react/nav.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* The drawer's `id`, and the header trigger's `commandfor`.
|
|
7
|
-
*
|
|
8
|
-
* A constant rather than a `useId`, for two reasons that both matter: the
|
|
9
|
-
* trigger is rendered on the server in a different subtree and cannot see a
|
|
10
|
-
* hook's value, and `command`/`commandfor` must agree before React hydrates or
|
|
11
|
-
* the button does nothing on the first tap.
|
|
12
|
-
*/
|
|
5
|
+
/** The navigation's `id`, and the trigger's `aria-controls`. */
|
|
13
6
|
declare const DOCS_NAV_ID = "wave-docs-nav";
|
|
14
7
|
interface DocsNavProps {
|
|
15
8
|
/** The tree from `docs.source.nav()`. */
|
|
@@ -18,15 +11,22 @@ interface DocsNavProps {
|
|
|
18
11
|
pathname: string;
|
|
19
12
|
/** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
|
|
20
13
|
Link?: DocsLinkComponent | undefined;
|
|
21
|
-
/** Accessible name for the nav landmark
|
|
14
|
+
/** Accessible name for the nav landmark. */
|
|
22
15
|
label?: string | undefined;
|
|
23
|
-
/** Accessible name for the
|
|
16
|
+
/** Accessible name for the trigger while the sidebar is open. */
|
|
24
17
|
closeLabel?: string | undefined;
|
|
18
|
+
/** Accessible name for the trigger while the sidebar is closed. */
|
|
19
|
+
openLabel?: string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Rendered above the tree. The search trigger goes here — it belongs to the
|
|
22
|
+
* navigation, so it moves with it and is never a second thing to place.
|
|
23
|
+
*/
|
|
24
|
+
children?: ReactNode;
|
|
25
25
|
/** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
|
|
26
26
|
expandGroup?: string | undefined;
|
|
27
27
|
collapseGroup?: string | undefined;
|
|
28
28
|
externalLink?: string | undefined;
|
|
29
29
|
}
|
|
30
|
-
declare function DocsNav({ nav, pathname, Link, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNavProps): ReactNode;
|
|
30
|
+
declare function DocsNav({ nav, pathname, Link, label, children, closeLabel, openLabel, expandGroup, collapseGroup, externalLink }: DocsNavProps): ReactNode;
|
|
31
31
|
//#endregion
|
|
32
32
|
export { DOCS_NAV_ID, DocsNav, DocsNavProps };
|
package/dist/react/nav.js
CHANGED
|
@@ -1,73 +1,121 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { DocsSidebar } from "./sidebar.js";
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/react/nav.tsx
|
|
6
|
-
/**
|
|
7
|
-
* The drawer's `id`, and the header trigger's `commandfor`.
|
|
8
|
-
*
|
|
9
|
-
* A constant rather than a `useId`, for two reasons that both matter: the
|
|
10
|
-
* trigger is rendered on the server in a different subtree and cannot see a
|
|
11
|
-
* hook's value, and `command`/`commandfor` must agree before React hydrates or
|
|
12
|
-
* the button does nothing on the first tap.
|
|
13
|
-
*/
|
|
6
|
+
/** The navigation's `id`, and the trigger's `aria-controls`. */
|
|
14
7
|
const DOCS_NAV_ID = "wave-docs-nav";
|
|
15
|
-
function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "Close navigation", expandGroup, collapseGroup, externalLink }) {
|
|
16
|
-
const
|
|
8
|
+
function DocsNav({ nav, pathname, Link, label = "Documentation", children, closeLabel = "Close navigation", openLabel = "Open navigation", expandGroup, collapseGroup, externalLink }) {
|
|
9
|
+
const shellRef = useRef(null);
|
|
10
|
+
const navRef = useRef(null);
|
|
11
|
+
const returnFocusRef = useRef(null);
|
|
12
|
+
/**
|
|
13
|
+
* ⚠️ THREE STATES, AND THE THIRD ONE IS WHY THERE IS NO FLASH.
|
|
14
|
+
*
|
|
15
|
+
* `null` is "nobody has chosen yet". The server renders it, no `data-state`
|
|
16
|
+
* reaches the DOM, and the stylesheet decides per mode — closed where the
|
|
17
|
+
* navigation would cover the article, open where it would sit beside it. So
|
|
18
|
+
* the first paint is already right at both shapes, with no JavaScript and
|
|
19
|
+
* nothing to correct afterwards.
|
|
20
|
+
*
|
|
21
|
+
* A boolean would have to be picked before the container's width is known,
|
|
22
|
+
* which is either a flash on a phone or a flash on a desktop.
|
|
23
|
+
*/
|
|
24
|
+
const [choice, setChoice] = useState(null);
|
|
25
|
+
const [mode, setMode] = useState(null);
|
|
26
|
+
const [ready, setReady] = useState(false);
|
|
27
|
+
const open = choice ?? mode === "push";
|
|
28
|
+
const covering = mode === "cover" && open;
|
|
17
29
|
useEffect(() => {
|
|
18
|
-
|
|
30
|
+
const shell = shellRef.current;
|
|
31
|
+
const layout = shell?.parentElement;
|
|
32
|
+
if (shell == null || layout == null) return;
|
|
33
|
+
const read = () => {
|
|
34
|
+
const value = getComputedStyle(shell).getPropertyValue("--wave-docs-sidebar-mode").trim();
|
|
35
|
+
setMode(value === "push" ? "push" : "cover");
|
|
36
|
+
};
|
|
37
|
+
read();
|
|
38
|
+
const frame = requestAnimationFrame(() => {
|
|
39
|
+
setReady(true);
|
|
40
|
+
});
|
|
41
|
+
const observer = new ResizeObserver(read);
|
|
42
|
+
observer.observe(layout);
|
|
43
|
+
return () => {
|
|
44
|
+
cancelAnimationFrame(frame);
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
const close = useCallback(() => {
|
|
49
|
+
setChoice(false);
|
|
50
|
+
}, []);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (covering) setChoice(false);
|
|
19
53
|
}, [pathname]);
|
|
20
54
|
useEffect(() => {
|
|
21
|
-
if (
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (button === null) return;
|
|
27
|
-
if (button.getAttribute("commandfor") !== "wave-docs-nav") return;
|
|
28
|
-
const dialog = ref.current;
|
|
29
|
-
if (dialog === null) return;
|
|
30
|
-
if (button.getAttribute("command") === "close") dialog.close();
|
|
31
|
-
else dialog.showModal();
|
|
55
|
+
if (!covering) return;
|
|
56
|
+
const onKeyDown = (event) => {
|
|
57
|
+
if (event.key !== "Escape") return;
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
close();
|
|
32
60
|
};
|
|
33
|
-
document.addEventListener("
|
|
61
|
+
document.addEventListener("keydown", onKeyDown);
|
|
34
62
|
return () => {
|
|
35
|
-
document.removeEventListener("
|
|
63
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
36
64
|
};
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
}, [covering, close]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const shell = shellRef.current;
|
|
68
|
+
const layout = shell?.parentElement;
|
|
69
|
+
if (shell == null || layout == null) return;
|
|
70
|
+
const outside = [...layout.children].filter((child) => child !== shell && !child.classList.contains(SCRIM_CLASS));
|
|
71
|
+
if (!covering) {
|
|
72
|
+
for (const element of outside) element.removeAttribute("inert");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
76
|
+
for (const element of outside) element.setAttribute("inert", "");
|
|
77
|
+
navRef.current?.focus();
|
|
78
|
+
return () => {
|
|
79
|
+
for (const element of outside) element.removeAttribute("inert");
|
|
80
|
+
returnFocusRef.current?.focus({ preventScroll: true });
|
|
81
|
+
};
|
|
82
|
+
}, [covering]);
|
|
83
|
+
const state = choice === null && mode === null ? void 0 : open;
|
|
84
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
85
|
+
ref: shellRef,
|
|
86
|
+
className: "wave-docs-layout__sidebar",
|
|
87
|
+
...state === void 0 ? {} : { "data-state": state ? "open" : "closed" },
|
|
88
|
+
...ready ? { "data-ready": "" } : {},
|
|
89
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
90
|
+
ref: navRef,
|
|
91
|
+
id: DOCS_NAV_ID,
|
|
92
|
+
className: "wave-docs-layout__sidebar-nav",
|
|
93
|
+
tabIndex: -1,
|
|
94
|
+
children: [children, /* @__PURE__ */ jsx(DocsSidebar, {
|
|
95
|
+
nav,
|
|
96
|
+
pathname,
|
|
97
|
+
label,
|
|
98
|
+
Link,
|
|
99
|
+
...expandGroup === void 0 ? {} : { expandGroup },
|
|
100
|
+
...collapseGroup === void 0 ? {} : { collapseGroup },
|
|
101
|
+
...externalLink === void 0 ? {} : { externalLink }
|
|
102
|
+
})]
|
|
103
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
45
104
|
type: "button",
|
|
46
|
-
className: "wave-docs-
|
|
47
|
-
"aria-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
width: "16",
|
|
54
|
-
height: "16",
|
|
55
|
-
fill: "none",
|
|
56
|
-
stroke: "currentColor",
|
|
57
|
-
strokeWidth: "1.5",
|
|
58
|
-
strokeLinecap: "round",
|
|
59
|
-
children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" })
|
|
60
|
-
})
|
|
61
|
-
}), /* @__PURE__ */ jsx(DocsSidebar, {
|
|
62
|
-
nav,
|
|
63
|
-
pathname,
|
|
64
|
-
label,
|
|
65
|
-
Link,
|
|
66
|
-
...expandGroup === void 0 ? {} : { expandGroup },
|
|
67
|
-
...collapseGroup === void 0 ? {} : { collapseGroup },
|
|
68
|
-
...externalLink === void 0 ? {} : { externalLink }
|
|
105
|
+
className: "wave-docs-layout__sidebar-trigger",
|
|
106
|
+
"aria-controls": DOCS_NAV_ID,
|
|
107
|
+
"aria-label": open ? closeLabel : openLabel,
|
|
108
|
+
...state === void 0 ? {} : { "aria-expanded": state },
|
|
109
|
+
onClick: () => {
|
|
110
|
+
setChoice(!open);
|
|
111
|
+
}
|
|
69
112
|
})]
|
|
70
|
-
})
|
|
113
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
114
|
+
className: SCRIM_CLASS,
|
|
115
|
+
"aria-hidden": "true",
|
|
116
|
+
onClick: close
|
|
117
|
+
})] });
|
|
71
118
|
}
|
|
119
|
+
const SCRIM_CLASS = "wave-docs-layout__sidebar-scrim";
|
|
72
120
|
//#endregion
|
|
73
121
|
export { DOCS_NAV_ID, DocsNav };
|
package/dist/react/next-nav.d.ts
CHANGED
|
@@ -5,11 +5,15 @@ interface DocsNextNavProps {
|
|
|
5
5
|
nav: DocNavNode[];
|
|
6
6
|
label?: string | undefined;
|
|
7
7
|
closeLabel?: string | undefined;
|
|
8
|
+
/** Accessible name for the trigger while the sidebar is closed. */
|
|
9
|
+
openLabel?: string | undefined;
|
|
10
|
+
/** Rendered above the tree. See `DocsNavProps.children`. */
|
|
11
|
+
children?: ReactNode;
|
|
8
12
|
/** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
|
|
9
13
|
expandGroup?: string | undefined;
|
|
10
14
|
collapseGroup?: string | undefined;
|
|
11
15
|
externalLink?: string | undefined;
|
|
12
16
|
}
|
|
13
|
-
declare function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNextNavProps): ReactNode;
|
|
17
|
+
declare function DocsNextNav({ nav, label, closeLabel, openLabel, children, expandGroup, collapseGroup, externalLink }: DocsNextNavProps): ReactNode;
|
|
14
18
|
//#endregion
|
|
15
19
|
export { DocsNextNav, DocsNextNavProps };
|
package/dist/react/next-nav.js
CHANGED
|
@@ -19,16 +19,18 @@ import { usePathname } from "next/navigation";
|
|
|
19
19
|
*/
|
|
20
20
|
/** Module scope: a fresh identity here remounts every nav link on every render. */
|
|
21
21
|
const Link = wrapNextLink(NextLink);
|
|
22
|
-
function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }) {
|
|
22
|
+
function DocsNextNav({ nav, label, closeLabel, openLabel, children, expandGroup, collapseGroup, externalLink }) {
|
|
23
23
|
return /* @__PURE__ */ jsx(DocsNav, {
|
|
24
24
|
nav,
|
|
25
25
|
pathname: usePathname(),
|
|
26
26
|
Link,
|
|
27
27
|
...label === void 0 ? {} : { label },
|
|
28
28
|
...closeLabel === void 0 ? {} : { closeLabel },
|
|
29
|
+
...openLabel === void 0 ? {} : { openLabel },
|
|
29
30
|
...expandGroup === void 0 ? {} : { expandGroup },
|
|
30
31
|
...collapseGroup === void 0 ? {} : { collapseGroup },
|
|
31
|
-
...externalLink === void 0 ? {} : { externalLink }
|
|
32
|
+
...externalLink === void 0 ? {} : { externalLink },
|
|
33
|
+
children
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
//#endregion
|
|
@@ -44,9 +44,15 @@
|
|
|
44
44
|
interface DocsLabels {
|
|
45
45
|
/** The navigation landmark's accessible name. Default `'Documentation'`. */
|
|
46
46
|
nav?: string | undefined;
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The sidebar's trigger, named for what pressing it does next. One button
|
|
49
|
+
* carries both strings: `openNav` while the sidebar is closed, `closeNav`
|
|
50
|
+
* while it is open.
|
|
51
|
+
*
|
|
52
|
+
* Default `'Open navigation'`.
|
|
53
|
+
*/
|
|
48
54
|
openNav?: string | undefined;
|
|
49
|
-
/** The button
|
|
55
|
+
/** The same button, while the sidebar is open. Default `'Close navigation'`. */
|
|
50
56
|
closeNav?: string | undefined;
|
|
51
57
|
/** The skip link's visible text. Default `'Skip to content'`. */
|
|
52
58
|
skipToContent?: string | undefined;
|
package/dist/react/toc.d.ts
CHANGED
|
@@ -8,9 +8,19 @@ interface DocsTocProps {
|
|
|
8
8
|
label?: string | undefined;
|
|
9
9
|
/**
|
|
10
10
|
* Region of the viewport that counts as "current", as an
|
|
11
|
-
* `IntersectionObserver` root margin. The default
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* `IntersectionObserver` root margin. The default ignores the bottom 60% of
|
|
12
|
+
* the screen, so the active entry tracks what you are reading rather than
|
|
13
|
+
* what has scrolled into view.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ IT USED TO RESERVE 80px FOR A STICKY HEADER, AND TWO THINGS WERE WRONG
|
|
16
|
+
* WITH THAT. There is no header — above 80rem, where this component
|
|
17
|
+
* is rendered at all, nothing of ours overlays the content. And 80px never
|
|
18
|
+
* matched the header anyway: the token was `3.5rem`, which is 56px, so the
|
|
19
|
+
* figure was never derived from the thing it claimed to reserve for.
|
|
20
|
+
*
|
|
21
|
+
* This component is exported standalone, so its default cannot assume our
|
|
22
|
+
* shell. It assumes nothing overlays the content; a host whose own chrome
|
|
23
|
+
* does sets this prop, which is what the prop is for.
|
|
14
24
|
*
|
|
15
25
|
* Only `px` and `%` are legal here — `IntersectionObserver` throws on any
|
|
16
26
|
* other unit, `rem` included.
|