@waveso/docs 0.9.1 → 0.11.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/CHANGELOG.md +539 -0
- package/README.md +99 -3
- package/dist/code-frame.d.ts +28 -1
- package/dist/code-frame.js +36 -1
- package/dist/frontmatter.d.ts +5 -0
- package/dist/frontmatter.js +5 -0
- package/dist/nav-order.d.ts +43 -0
- package/dist/nav-order.js +72 -0
- package/dist/next.d.ts +10 -0
- package/dist/next.js +33 -0
- package/dist/plugins/rehype-code-frame.js +41 -11
- package/dist/react/explore.d.ts +53 -0
- package/dist/react/explore.js +86 -0
- package/dist/react/markdown-components.js +14 -0
- package/dist/react/pager.d.ts +42 -0
- package/dist/react/pager.js +91 -0
- package/dist/react/shell-labels.d.ts +25 -1
- package/dist/react/shell-labels.js +4 -0
- package/dist/react/toc.js +34 -5
- package/dist/styles.css +944 -146
- package/dist/types.d.ts +31 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -69,16 +69,16 @@ Every figure below is a **ceiling**, and `pnpm size` fails the build if the meas
|
|
|
69
69
|
|
|
70
70
|
| | At most |
|
|
71
71
|
| --- | --- |
|
|
72
|
-
| Everything the quick start ships, gzipped | 14.
|
|
72
|
+
| Everything the quick start ships, gzipped | 14.6 KB |
|
|
73
73
|
| Search dialog and router wiring | 9.5 KB |
|
|
74
74
|
| Navigation: one sidebar, open and closed | 3.1 KB |
|
|
75
|
-
| Table of contents |
|
|
75
|
+
| Table of contents | 1 KB |
|
|
76
76
|
| Copy-button runtime | 1.1 KB |
|
|
77
77
|
| hast over the wire vs HTML, prose page | 1.20× |
|
|
78
78
|
| hast over the wire vs HTML, code and tables | 1.12× |
|
|
79
79
|
| Highlighting vs no highlighting | 2.00× |
|
|
80
80
|
|
|
81
|
-
The first row is the honest total: a reader who lands on a page of your documentation downloads under 14.
|
|
81
|
+
The first row is the honest total: a reader who lands on a page of your documentation downloads under 14.6 KB gzipped of JavaScript from this package, and that is the whole of it. No markdown parser and no syntax highlighter reach the browser at all — those run in Node at build time. Drop the search dialog and it is under 4 KB.
|
|
82
82
|
|
|
83
83
|
The one real cost is the middle pair: shipping a tree instead of a string is about 20% more brotli on a prose page, and about 12% on a page with code and tables, where Shiki's token spans dominate both representations equally. That is the price of never handing markup to `dangerouslySetInnerHTML`, and it is the first number a skeptical reviewer should ask for.
|
|
84
84
|
|
|
@@ -203,6 +203,8 @@ Every component takes data as props, and every module that imports from `next/*`
|
|
|
203
203
|
| `DocContent` | `react/doc-content` | Renders a hast tree, inside `.wave-docs-prose`. Server Component |
|
|
204
204
|
| `DocsHero` | `react/hero` | A landing page's header. `title`, `description`, `actions`, `Link`, `externalLabel`. Server Component |
|
|
205
205
|
| `DocsSidebar` | `react/sidebar` | Takes `pathname` as a prop, not from `next/navigation`. `icons` controls the marker column — see [Sidebar icons](#sidebar-icons) |
|
|
206
|
+
| `DocsExplore` | `react/explore` | "Where to go next": a question per row and the page that answers it. `docs.Page` renders it when a page declares `explore` |
|
|
207
|
+
| `DocsPager` | `react/pager` | Links to the pages either side of this one. `docs.Page` renders it; `pager: false` on the route omits it |
|
|
206
208
|
| `DocsToc` | `react/toc` | Scrollspy via `IntersectionObserver`. `label`, `topLabel`, `rootMargin`, `className` |
|
|
207
209
|
| `DocsSearch` | `react/next-search` | `SearchDialog`, wired to Next's router. What you want |
|
|
208
210
|
| `DocsLink` | `react/next-link` | `next/link`, adapted — pass it as `Link` when composing by hand |
|
|
@@ -214,6 +216,58 @@ Every component takes data as props, and every module that imports from `next/*`
|
|
|
214
216
|
|
|
215
217
|
`DocsToc`'s `rootMargin` is the `IntersectionObserver` margin that decides how far above the viewport a heading counts as current; the default keeps the highlight on the section you are reading rather than the one about to arrive. `topLabel` is the back-to-top link at the end — it fades in once the reader is about a third of a screen down and fades out again on the way back, on a scroll timeline rather than a scroll listener, so the component ships no extra bytes to do it. Where that timeline cannot run — an engine without scroll-driven animations, a page too short to scroll, or a host that scrolls an inner pane rather than the document — the link is simply always there.
|
|
216
218
|
|
|
219
|
+
### Where to go next
|
|
220
|
+
|
|
221
|
+
A sidebar is a structure; this is a router. It says *why* a reader would go somewhere, which no tree of titles can. A page opts in from its frontmatter:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
---
|
|
225
|
+
title: How it fits together
|
|
226
|
+
explore:
|
|
227
|
+
- question: How a person is recognised across servers
|
|
228
|
+
href: ./identity.md
|
|
229
|
+
- question: What happens when the network fails
|
|
230
|
+
href: ./delivery.md
|
|
231
|
+
- question: Where the source lives
|
|
232
|
+
href: https://github.com/example/repo
|
|
233
|
+
title: GitHub
|
|
234
|
+
---
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Field | Type | What it is |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `question` | `string` | What the reader might want to know. The row's left half |
|
|
240
|
+
| `href` | `string` | Where the answer is. Checked against the same allowlist as every other link |
|
|
241
|
+
| `title` | `string` | The link's text. Defaults to the destination's title **in the navigation** |
|
|
242
|
+
|
|
243
|
+
**The link text comes from the navigation**, so renaming a page updates every block pointing at it — the same source the sidebar and the pager read. Name a `title` only where the tree cannot answer: an external link, or a page kept out of the navigation. An `href` that resolves to neither stops the build and says which of the two fixes to reach for, rather than rendering a URL where a sentence should be.
|
|
244
|
+
|
|
245
|
+
It renders above the pager: this is the semantic answer, the pager is the linear one.
|
|
246
|
+
|
|
247
|
+
It wears **the panel** — a framed block with a header and an inset surface, styled by `.wave-docs-panel`, `.wave-docs-panel__header`, `.wave-docs-panel__title`, `.wave-docs-panel__actions` and `.wave-docs-panel__body`. That is a shared primitive rather than this component's furniture, so a code frame can wear it next without a second copy of the same rules. The two radii are not independent: the inner one is the outer minus the panel's padding, or the corners run at different curvatures and the surface reads as pasted onto the frame instead of set into it. `--wave-docs-radius-lg` is picked so the arithmetic lands on `--wave-docs-radius`.
|
|
248
|
+
|
|
249
|
+
The panel also exports `--wave-docs-panel-inset` for anything placed inside `__body`: the header's title sits at the frame's padding, but body content sits at that padding *plus the body's own border*, so the two columns miss each other by a pixel per border unless the inset is used.
|
|
250
|
+
|
|
251
|
+
`DocsExplore` takes `steps` (each with `question`, `href` and a resolved `title`), plus `heading`, `Link`, `externalLabel` and `className`. The heading defaults to `'Where to go next'` and is `explore` in `labels`. Under a 40rem container the question and its answer stack instead of sharing a row.
|
|
252
|
+
|
|
253
|
+
### The pager
|
|
254
|
+
|
|
255
|
+
`docs.Page` renders it under every page, so most sites never touch it. The order is the navigation's — flattened from the same tree `DocsSidebar` renders — so a pager that disagrees with the sidebar beside it is impossible. Nothing is authored: a page gets one by being in the tree, and a page outside it (a draft, or a route you render yourself) gets none.
|
|
256
|
+
|
|
257
|
+
Separators and external links are not stops. A separator is a label with nowhere to go, and a "next page" that lands on npm has ended the sequence rather than continued it. A directory with an `index.md` contributes its own page before its children, which is the order its rows appear in.
|
|
258
|
+
|
|
259
|
+
| Prop | Type | Default | What it is |
|
|
260
|
+
| --- | --- | --- | --- |
|
|
261
|
+
| `previous` | `NavStop` | — | The stop before this page. Omit at the beginning |
|
|
262
|
+
| `next` | `NavStop` | — | The stop after it. Omit at the end |
|
|
263
|
+
| `Link` | `DocsLinkComponent` | `<a>` | Client-side router link |
|
|
264
|
+
| `previousLabel` | `string` | `'Previous'` | Above the previous page's title |
|
|
265
|
+
| `nextLabel` | `string` | `'Next'` | Above the next page's title |
|
|
266
|
+
| `label` | `string` | `'Pagination'` | Accessible name for the landmark |
|
|
267
|
+
| `className` | `string` | — | Extra classes |
|
|
268
|
+
|
|
269
|
+
With neither neighbour it renders nothing at all, rather than an empty landmark. Set `pager: false` on `createDocsRoute` to omit it everywhere, and the three strings through `labels` — `previousPage`, `nextPage`, `pagination`.
|
|
270
|
+
|
|
217
271
|
### Sidebar icons
|
|
218
272
|
|
|
219
273
|
Every row in the sidebar carries a marker at its head: a folder on a group, a page on a page, an arrow on a link that leaves your site. Weight and a chevron were the only difference before, and where categories and pages interleave that is not enough to scan.
|
|
@@ -667,6 +721,48 @@ your own `:root`, after the import:
|
|
|
667
721
|
}
|
|
668
722
|
```
|
|
669
723
|
|
|
724
|
+
### Corners
|
|
725
|
+
|
|
726
|
+
Three tiers, all derived from one root, so a box's radius is decided by what
|
|
727
|
+
*kind* of box it is rather than by how big it happens to be.
|
|
728
|
+
|
|
729
|
+
| Token | What takes it |
|
|
730
|
+
| --- | --- |
|
|
731
|
+
| `--wave-docs-radius-sm` | Inline chips, small controls, and focus rings drawn on those |
|
|
732
|
+
| `--wave-docs-radius` | Controls, overlays, and the panel's inset surface |
|
|
733
|
+
| `--wave-docs-radius-lg` | Every block in the reading flow, and the panel's outer edge |
|
|
734
|
+
|
|
735
|
+
**Retune all three from one line.** `--wave-docs-radius-base` is the root and
|
|
736
|
+
the other three are `calc()` off it, so a host already running `@waveso/ui`
|
|
737
|
+
points this package at their scale and every corner follows — including any
|
|
738
|
+
theme that moves it:
|
|
739
|
+
|
|
740
|
+
```css
|
|
741
|
+
:root {
|
|
742
|
+
--wave-docs-radius-base: var(--radius);
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
The numbers are `@waveso/ui`'s to begin with. A page running both should not
|
|
747
|
+
show two radius scales a few pixels apart, and taking theirs is how that is
|
|
748
|
+
guaranteed rather than kept in step by hand.
|
|
749
|
+
|
|
750
|
+
⚠️ **`--wave-docs-radius-step` is not decoration.** The panel's inset surface
|
|
751
|
+
takes the base radius and its frame takes `--wave-docs-radius-lg`, which is the
|
|
752
|
+
base plus one step — so the two corners are concentric only while the frame's
|
|
753
|
+
*padding* is that same step, which is why it is paid out of the token rather
|
|
754
|
+
than written as `4px`. Move the root and both stay true; hard-code the padding
|
|
755
|
+
and they drift the first time anyone retunes the scale.
|
|
756
|
+
|
|
757
|
+
Where the browser supports `corner-shape`, every corner this package draws
|
|
758
|
+
becomes a squircle and the root moves up, because a squircle reads tighter than
|
|
759
|
+
a circular arc at the same radius. `@waveso/ui` makes the same move to the same
|
|
760
|
+
value. Elsewhere it is an ordinary rounded corner at the original scale — an
|
|
761
|
+
enhancement, never a dependency. The shaping is scoped to elements this package
|
|
762
|
+
owns rather than applied with `*`: this stylesheet is mounted inside somebody
|
|
763
|
+
else's page, and reshaping the host's corners is the same trespass as claiming
|
|
764
|
+
`html`.
|
|
765
|
+
|
|
670
766
|
That works because **everything this stylesheet declares lives in a `@layer`** —
|
|
671
767
|
`theme` for the tokens, `base` for element resets, `components` for the classes
|
|
672
768
|
— and unlayered CSS outranks every layer regardless of specificity.
|
package/dist/code-frame.d.ts
CHANGED
|
@@ -16,6 +16,33 @@ declare const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
|
|
|
16
16
|
* tab stop where a control should be.
|
|
17
17
|
*/
|
|
18
18
|
declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
19
|
+
/**
|
|
20
|
+
* The copy button's three icons, as Lucide paths.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ PATHS FROM THE SAME SET AS EVERY OTHER ICON HERE, AND THEY WERE FONT
|
|
23
|
+
* GLYPHS. The button rendered `⧉` and swapped in `✓` and `×` through CSS
|
|
24
|
+
* `content` — three characters drawn by whatever font resolved, at whatever
|
|
25
|
+
* weight and baseline that font has, beside a sidebar, a pager and a search
|
|
26
|
+
* dialog that are all Lucide at `stroke-width: 2`. It read as a different
|
|
27
|
+
* icon set because it was one.
|
|
28
|
+
*
|
|
29
|
+
* `copy`, `check` and `x`, on the 24×24 grid the rest of the package uses.
|
|
30
|
+
* Lucide draws its `copy` as a `<rect>` plus a `<path>`; the rect is written
|
|
31
|
+
* here as the path it is, so a frame's icons are one shape of node and the
|
|
32
|
+
* builder stays a list of `d` strings — the same shape `NAV_ICON_PATHS` has.
|
|
33
|
+
*/
|
|
34
|
+
declare const CODE_ICON_PATHS: Record<'copy' | 'check' | 'x', string[]>;
|
|
35
|
+
/**
|
|
36
|
+
* The three states the button draws, in the order they are emitted.
|
|
37
|
+
*
|
|
38
|
+
* `idle` is what a reader sees; the runtime writes `data-copied` on the button
|
|
39
|
+
* and the stylesheet swaps which of the three is displayed. `display`, not
|
|
40
|
+
* `visibility`: the button is `visibility: hidden` until the runtime attaches,
|
|
41
|
+
* and `visibility` inherits — a child setting it back to `visible` would show
|
|
42
|
+
* an icon inside a button that is meant to be invisible and out of the tab
|
|
43
|
+
* order.
|
|
44
|
+
*/
|
|
45
|
+
declare const CODE_ICON_STATES: readonly [readonly ["idle", "copy"], readonly ["copied", "check"], readonly ["failed", "x"]];
|
|
19
46
|
/**
|
|
20
47
|
* Whether a tree contains a code frame.
|
|
21
48
|
*
|
|
@@ -26,4 +53,4 @@ declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
|
26
53
|
*/
|
|
27
54
|
declare function hasCodeFrame(tree: Root): boolean;
|
|
28
55
|
//#endregion
|
|
29
|
-
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
|
56
|
+
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
package/dist/code-frame.js
CHANGED
|
@@ -16,6 +16,41 @@ const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
|
|
|
16
16
|
*/
|
|
17
17
|
const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
18
18
|
/**
|
|
19
|
+
* The copy button's three icons, as Lucide paths.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ PATHS FROM THE SAME SET AS EVERY OTHER ICON HERE, AND THEY WERE FONT
|
|
22
|
+
* GLYPHS. The button rendered `⧉` and swapped in `✓` and `×` through CSS
|
|
23
|
+
* `content` — three characters drawn by whatever font resolved, at whatever
|
|
24
|
+
* weight and baseline that font has, beside a sidebar, a pager and a search
|
|
25
|
+
* dialog that are all Lucide at `stroke-width: 2`. It read as a different
|
|
26
|
+
* icon set because it was one.
|
|
27
|
+
*
|
|
28
|
+
* `copy`, `check` and `x`, on the 24×24 grid the rest of the package uses.
|
|
29
|
+
* Lucide draws its `copy` as a `<rect>` plus a `<path>`; the rect is written
|
|
30
|
+
* here as the path it is, so a frame's icons are one shape of node and the
|
|
31
|
+
* builder stays a list of `d` strings — the same shape `NAV_ICON_PATHS` has.
|
|
32
|
+
*/
|
|
33
|
+
const CODE_ICON_PATHS = {
|
|
34
|
+
copy: ["M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2z", "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"],
|
|
35
|
+
check: ["M20 6 9 17l-5-5"],
|
|
36
|
+
x: ["M18 6 6 18", "m6 6 12 12"]
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The three states the button draws, in the order they are emitted.
|
|
40
|
+
*
|
|
41
|
+
* `idle` is what a reader sees; the runtime writes `data-copied` on the button
|
|
42
|
+
* and the stylesheet swaps which of the three is displayed. `display`, not
|
|
43
|
+
* `visibility`: the button is `visibility: hidden` until the runtime attaches,
|
|
44
|
+
* and `visibility` inherits — a child setting it back to `visible` would show
|
|
45
|
+
* an icon inside a button that is meant to be invisible and out of the tab
|
|
46
|
+
* order.
|
|
47
|
+
*/
|
|
48
|
+
const CODE_ICON_STATES = [
|
|
49
|
+
["idle", "copy"],
|
|
50
|
+
["copied", "check"],
|
|
51
|
+
["failed", "x"]
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
19
54
|
* Whether a tree contains a code frame.
|
|
20
55
|
*
|
|
21
56
|
* `DocContent` asks before rendering the runtime, so a page with no fences
|
|
@@ -38,4 +73,4 @@ function isCodeFrame(node) {
|
|
|
38
73
|
return node.properties[CODE_FRAME_ATTRIBUTE] !== void 0;
|
|
39
74
|
}
|
|
40
75
|
//#endregion
|
|
41
|
-
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
|
76
|
+
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
package/dist/frontmatter.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
33
33
|
aliases: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
|
|
34
34
|
order: z.ZodExactOptional<z.ZodNumber>;
|
|
35
35
|
icon: z.ZodExactOptional<z.ZodString>;
|
|
36
|
+
explore: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
|
|
37
|
+
question: z.ZodString;
|
|
38
|
+
href: z.ZodString;
|
|
39
|
+
title: z.ZodExactOptional<z.ZodString>;
|
|
40
|
+
}, z.core.$strip>>>;
|
|
36
41
|
actions: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
|
|
37
42
|
label: z.ZodString;
|
|
38
43
|
href: z.ZodString;
|
package/dist/frontmatter.js
CHANGED
|
@@ -33,6 +33,11 @@ const docFrontmatterSchema = z.object({
|
|
|
33
33
|
aliases: z.array(z.string()).exactOptional(),
|
|
34
34
|
order: z.number().exactOptional(),
|
|
35
35
|
icon: z.string().min(1).exactOptional(),
|
|
36
|
+
explore: z.array(z.object({
|
|
37
|
+
question: z.string().min(1),
|
|
38
|
+
href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
|
|
39
|
+
title: z.string().min(1).exactOptional()
|
|
40
|
+
})).exactOptional(),
|
|
36
41
|
actions: z.array(z.object({
|
|
37
42
|
label: z.string().min(1),
|
|
38
43
|
href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DocNavNode } from "./types.js";
|
|
2
|
+
//#region src/nav-order.d.ts
|
|
3
|
+
/** A destination in the reading order. */
|
|
4
|
+
interface NavStop {
|
|
5
|
+
title: string;
|
|
6
|
+
href: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every page in the tree, in the order a reader meets them.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ SEPARATORS AND EXTERNAL LINKS ARE NOT STOPS. A separator is a label with
|
|
12
|
+
* nowhere to go, and an external link leaves the documentation entirely — a
|
|
13
|
+
* "next page" that lands on npm has ended the sequence rather than continued
|
|
14
|
+
* it. Internal `link` entries *are* stops: they are hand-written entries
|
|
15
|
+
* pointing at pages of this site, and a reader clicking down the sidebar hits
|
|
16
|
+
* them like any other row.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ A GROUP CONTRIBUTES ITS OWN PAGE FIRST, WHEN IT HAS ONE. A directory with
|
|
19
|
+
* an `index.md` renders as a link *and* a disclosure, so the reading order is
|
|
20
|
+
* the group's page and then its children — which is the order the rows appear
|
|
21
|
+
* in, and the order someone reading the section would take them.
|
|
22
|
+
*/
|
|
23
|
+
declare function readingOrder(nodes: DocNavNode[], into?: NavStop[]): NavStop[];
|
|
24
|
+
/** The stops either side of `href`, or `undefined` at each end of the sequence. */
|
|
25
|
+
declare function neighbours(nodes: DocNavNode[], href: string): {
|
|
26
|
+
previous?: NavStop;
|
|
27
|
+
next?: NavStop;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A step's link text: the one it named, or the navigation's name for the page.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ A THROW, NOT A FALLBACK TO THE HREF. A row reading "Where this runs and
|
|
33
|
+
* what that buys → /docs/infrastructure" is a URL where a sentence should be,
|
|
34
|
+
* and it renders perfectly — nothing else in the pipeline would notice. The
|
|
35
|
+
* frontmatter is authored and the author is right there, so the build stops and
|
|
36
|
+
* says which page and which of the two fixes to reach for.
|
|
37
|
+
*/
|
|
38
|
+
declare function stepTitle(stops: NavStop[], step: {
|
|
39
|
+
href: string;
|
|
40
|
+
title?: string | undefined;
|
|
41
|
+
}): string | undefined;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { NavStop, neighbours, readingOrder, stepTitle };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/nav-order.ts
|
|
2
|
+
/** Trailing slashes are a routing detail, not a difference in identity. */
|
|
3
|
+
function normalize(href) {
|
|
4
|
+
return href.length > 1 ? href.replace(/\/+$/, "") : href;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Every page in the tree, in the order a reader meets them.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ SEPARATORS AND EXTERNAL LINKS ARE NOT STOPS. A separator is a label with
|
|
10
|
+
* nowhere to go, and an external link leaves the documentation entirely — a
|
|
11
|
+
* "next page" that lands on npm has ended the sequence rather than continued
|
|
12
|
+
* it. Internal `link` entries *are* stops: they are hand-written entries
|
|
13
|
+
* pointing at pages of this site, and a reader clicking down the sidebar hits
|
|
14
|
+
* them like any other row.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ A GROUP CONTRIBUTES ITS OWN PAGE FIRST, WHEN IT HAS ONE. A directory with
|
|
17
|
+
* an `index.md` renders as a link *and* a disclosure, so the reading order is
|
|
18
|
+
* the group's page and then its children — which is the order the rows appear
|
|
19
|
+
* in, and the order someone reading the section would take them.
|
|
20
|
+
*/
|
|
21
|
+
function readingOrder(nodes, into = []) {
|
|
22
|
+
for (const node of nodes) switch (node.type) {
|
|
23
|
+
case "page":
|
|
24
|
+
into.push({
|
|
25
|
+
title: node.title,
|
|
26
|
+
href: node.href
|
|
27
|
+
});
|
|
28
|
+
break;
|
|
29
|
+
case "link":
|
|
30
|
+
if (!node.external) into.push({
|
|
31
|
+
title: node.title,
|
|
32
|
+
href: node.href
|
|
33
|
+
});
|
|
34
|
+
break;
|
|
35
|
+
case "group":
|
|
36
|
+
if (node.href !== void 0) into.push({
|
|
37
|
+
title: node.title,
|
|
38
|
+
href: node.href
|
|
39
|
+
});
|
|
40
|
+
readingOrder(node.children, into);
|
|
41
|
+
}
|
|
42
|
+
return into;
|
|
43
|
+
}
|
|
44
|
+
/** The stops either side of `href`, or `undefined` at each end of the sequence. */
|
|
45
|
+
function neighbours(nodes, href) {
|
|
46
|
+
const stops = readingOrder(nodes);
|
|
47
|
+
const here = normalize(href);
|
|
48
|
+
const at = stops.findIndex((stop) => normalize(stop.href) === here);
|
|
49
|
+
if (at === -1) return {};
|
|
50
|
+
const previous = stops[at - 1];
|
|
51
|
+
const next = stops[at + 1];
|
|
52
|
+
return {
|
|
53
|
+
...previous === void 0 ? {} : { previous },
|
|
54
|
+
...next === void 0 ? {} : { next }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A step's link text: the one it named, or the navigation's name for the page.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ A THROW, NOT A FALLBACK TO THE HREF. A row reading "Where this runs and
|
|
61
|
+
* what that buys → /docs/infrastructure" is a URL where a sentence should be,
|
|
62
|
+
* and it renders perfectly — nothing else in the pipeline would notice. The
|
|
63
|
+
* frontmatter is authored and the author is right there, so the build stops and
|
|
64
|
+
* says which page and which of the two fixes to reach for.
|
|
65
|
+
*/
|
|
66
|
+
function stepTitle(stops, step) {
|
|
67
|
+
if (step.title !== void 0) return step.title;
|
|
68
|
+
const here = normalize(step.href);
|
|
69
|
+
return stops.find((stop) => normalize(stop.href) === here)?.title;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { neighbours, readingOrder, stepTitle };
|
package/dist/next.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ import { Options } from "minisearch";
|
|
|
13
13
|
interface DocsRouteOptions<TFrontmatter extends DocFrontmatter = DocFrontmatter> extends DocsConfig<TFrontmatter> {
|
|
14
14
|
/** Overrides merged over the Next-flavoured defaults (`next/link` + `next/image`). */
|
|
15
15
|
components?: MarkdownComponents | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Links to the pages either side of this one, under every page. Default on.
|
|
18
|
+
*
|
|
19
|
+
* The order is the navigation's, so it cannot disagree with the sidebar —
|
|
20
|
+
* see `nav-order.ts`. Nothing is authored: a page gets a pager by being in
|
|
21
|
+
* the tree, and a page outside it gets none.
|
|
22
|
+
*
|
|
23
|
+
* `false` omits it, for a host whose own layout already ends a page.
|
|
24
|
+
*/
|
|
25
|
+
pager?: boolean | undefined;
|
|
16
26
|
/** Reuse an existing Shiki highlighter. */
|
|
17
27
|
highlighter?: DocsHighlighter | Promise<DocsHighlighter> | undefined;
|
|
18
28
|
/** Grammars to load, when building the default highlighter. */
|
package/dist/next.js
CHANGED
|
@@ -3,10 +3,13 @@ import { docsError } from "./docs-error.js";
|
|
|
3
3
|
import { DOCS_CONTENT_ID } from "./docs-content-id.js";
|
|
4
4
|
import { describeSuggestion } from "./link-suggestion.js";
|
|
5
5
|
import { mapPooled } from "./map-pooled.js";
|
|
6
|
+
import { neighbours, readingOrder, stepTitle } from "./nav-order.js";
|
|
6
7
|
import { findFunctionValuedOptions } from "./search-options.js";
|
|
7
8
|
import { createMarkdownComponents } from "./react/markdown-components.js";
|
|
8
9
|
import { DocContent } from "./react/doc-content.js";
|
|
9
10
|
import { DocsHero } from "./react/hero.js";
|
|
11
|
+
import { DocsPager } from "./react/pager.js";
|
|
12
|
+
import { DocsExplore } from "./react/explore.js";
|
|
10
13
|
import { DocsToc } from "./react/toc.js";
|
|
11
14
|
import { wrapNextLink } from "./react/link-adapter.js";
|
|
12
15
|
import { createDocsRenderer } from "./render.js";
|
|
@@ -427,6 +430,25 @@ function createDocsRoute(options) {
|
|
|
427
430
|
etag: `"${createHash("sha1").update(json).digest("hex")}"`
|
|
428
431
|
} });
|
|
429
432
|
};
|
|
433
|
+
/**
|
|
434
|
+
* A page's `next` rows with their link text filled in.
|
|
435
|
+
*
|
|
436
|
+
* ⚠️ THE ERROR IS THE POINT. Falling back to the href renders a URL where a
|
|
437
|
+
* sentence should be, on a page that builds cleanly — so an unresolvable row
|
|
438
|
+
* stops the build and names both fixes.
|
|
439
|
+
*/
|
|
440
|
+
async function resolveExplore(doc) {
|
|
441
|
+
const stops = readingOrder(await requestScopedSource.nav());
|
|
442
|
+
return (doc.frontmatter.explore ?? []).map((step) => {
|
|
443
|
+
const title = stepTitle(stops, step);
|
|
444
|
+
if (title === void 0) throw docsError("invalid-frontmatter", `${doc.href}: the "explore" entry pointing at "${step.href}" has no title, and no page in the navigation owns that route. Point it at a page in the tree, or give the entry its own \`title\` — which is what an external link or a page kept out of the navigation needs.`);
|
|
445
|
+
return {
|
|
446
|
+
question: step.question,
|
|
447
|
+
href: step.href,
|
|
448
|
+
title
|
|
449
|
+
};
|
|
450
|
+
});
|
|
451
|
+
}
|
|
430
452
|
async function renderRoute(segments) {
|
|
431
453
|
const doc = await getPage(segments);
|
|
432
454
|
if (doc === void 0) return (await loadNotFound())();
|
|
@@ -448,6 +470,17 @@ function createDocsRoute(options) {
|
|
|
448
470
|
...options.components
|
|
449
471
|
},
|
|
450
472
|
...copyLabels === void 0 ? {} : { labels: copyLabels }
|
|
473
|
+
}), (doc.frontmatter.explore?.length ?? 0) === 0 ? null : createElement(DocsExplore, {
|
|
474
|
+
links: await resolveExplore(doc),
|
|
475
|
+
Link: link,
|
|
476
|
+
...routeLabels?.explore === void 0 ? {} : { heading: routeLabels.explore },
|
|
477
|
+
...routeLabels?.externalLink === void 0 ? {} : { externalLabel: routeLabels.externalLink }
|
|
478
|
+
}), options.pager === false ? null : createElement(DocsPager, {
|
|
479
|
+
...neighbours(await requestScopedSource.nav(), doc.href),
|
|
480
|
+
Link: link,
|
|
481
|
+
...routeLabels?.previousPage === void 0 ? {} : { previousLabel: routeLabels.previousPage },
|
|
482
|
+
...routeLabels?.nextPage === void 0 ? {} : { nextLabel: routeLabels.nextPage },
|
|
483
|
+
...routeLabels?.pagination === void 0 ? {} : { label: routeLabels.pagination }
|
|
451
484
|
})), doc.toc.length === 0 ? null : createElement("aside", { className: "wave-docs-layout__toc" }, createElement(DocsToc, {
|
|
452
485
|
entries: doc.toc,
|
|
453
486
|
...routeLabels?.toc === void 0 ? {} : { label: routeLabels.toc },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE } from "../code-frame.js";
|
|
1
|
+
import { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES } from "../code-frame.js";
|
|
2
2
|
import { parseCodeMeta } from "../code-meta.js";
|
|
3
3
|
import { CONTINUE, SKIP, visit } from "unist-util-visit";
|
|
4
4
|
//#region src/plugins/rehype-code-frame.ts
|
|
@@ -28,12 +28,17 @@ const rehypeCodeFrame = (options = {}) => {
|
|
|
28
28
|
value: title
|
|
29
29
|
}]
|
|
30
30
|
});
|
|
31
|
-
children.push(copyButton(title === void 0 ? copyLabel : copyFromLabel.replace("{title}", title)),
|
|
31
|
+
children.push(copyButton(title === void 0 ? copyLabel : copyFromLabel.replace("{title}", title)), {
|
|
32
|
+
type: "element",
|
|
33
|
+
tagName: "div",
|
|
34
|
+
properties: { className: ["wave-docs-panel__body", "wave-docs-code__body"] },
|
|
35
|
+
children: [node]
|
|
36
|
+
});
|
|
32
37
|
parent.children[index] = {
|
|
33
38
|
type: "element",
|
|
34
39
|
tagName: "figure",
|
|
35
40
|
properties: {
|
|
36
|
-
className: ["wave-docs-code"],
|
|
41
|
+
className: ["wave-docs-panel", "wave-docs-code"],
|
|
37
42
|
[CODE_FRAME_ATTRIBUTE]: "",
|
|
38
43
|
...language === void 0 ? {} : { "data-lang": language }
|
|
39
44
|
},
|
|
@@ -60,15 +65,40 @@ function copyButton(label) {
|
|
|
60
65
|
[CODE_COPY_ATTRIBUTE]: "",
|
|
61
66
|
"aria-label": label
|
|
62
67
|
},
|
|
63
|
-
children: [
|
|
68
|
+
children: CODE_ICON_STATES.map(([state, name]) => stateIcon(state, name))
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One Lucide glyph, as hast.
|
|
73
|
+
*
|
|
74
|
+
* Decoration, so it is out of the accessibility tree — the button reads as its
|
|
75
|
+
* `aria-label` — and out of the search index by the same rule that drops the
|
|
76
|
+
* heading anchor icons.
|
|
77
|
+
*/
|
|
78
|
+
function stateIcon(state, name) {
|
|
79
|
+
return {
|
|
80
|
+
type: "element",
|
|
81
|
+
tagName: "svg",
|
|
82
|
+
properties: {
|
|
83
|
+
className: ["wave-docs-code__copy-icon"],
|
|
84
|
+
"data-state": state,
|
|
85
|
+
"aria-hidden": "true",
|
|
86
|
+
focusable: "false",
|
|
87
|
+
viewBox: "0 0 24 24",
|
|
88
|
+
width: "16",
|
|
89
|
+
height: "16",
|
|
90
|
+
fill: "none",
|
|
91
|
+
stroke: "currentColor",
|
|
92
|
+
strokeWidth: "2",
|
|
93
|
+
strokeLinecap: "round",
|
|
94
|
+
strokeLinejoin: "round"
|
|
95
|
+
},
|
|
96
|
+
children: CODE_ICON_PATHS[name].map((d) => ({
|
|
64
97
|
type: "element",
|
|
65
|
-
tagName: "
|
|
66
|
-
properties: {
|
|
67
|
-
children: [
|
|
68
|
-
|
|
69
|
-
value: "⧉"
|
|
70
|
-
}]
|
|
71
|
-
}]
|
|
98
|
+
tagName: "path",
|
|
99
|
+
properties: { d },
|
|
100
|
+
children: []
|
|
101
|
+
}))
|
|
72
102
|
};
|
|
73
103
|
}
|
|
74
104
|
/** `code.data.meta` — read, never written. */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DocsLinkComponent } from "./markdown-components.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/react/explore.d.ts
|
|
4
|
+
/** One row: a question, and the page that answers it. */
|
|
5
|
+
interface DocsExploreLink {
|
|
6
|
+
question: string;
|
|
7
|
+
href: string;
|
|
8
|
+
/** Resolved by the caller — from the navigation, or from the frontmatter. */
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
interface DocsExploreProps {
|
|
12
|
+
links: DocsExploreLink[];
|
|
13
|
+
/** The block's heading. Defaults to `'Where to go next'`. */
|
|
14
|
+
heading?: string | undefined;
|
|
15
|
+
/** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
|
|
16
|
+
Link?: DocsLinkComponent | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Screen-reader suffix on a row whose answer is off-site. Defaults to
|
|
19
|
+
* `'(opens in a new tab)'`.
|
|
20
|
+
*/
|
|
21
|
+
externalLabel?: string | undefined;
|
|
22
|
+
className?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* "Where to go next" — a question per row, and the page that answers it.
|
|
26
|
+
*
|
|
27
|
+
* The sidebar is a structure and this is a router: it says *why* a reader would
|
|
28
|
+
* go somewhere, which no tree of titles can. A page opts in by declaring
|
|
29
|
+
* `explore` in its frontmatter, so the same component is a landing page's
|
|
30
|
+
* onboarding and an ordinary page's footnote.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ `explore`, NOT `next` — AND NOT `steps` EITHER.
|
|
33
|
+
*
|
|
34
|
+
* `next` is unusable in this package: `src/next.ts` is the Next.js adapter, so
|
|
35
|
+
* two files one directory apart would carry the same name for entirely
|
|
36
|
+
* different things, and `doc.frontmatter.next` would read like a routing hook.
|
|
37
|
+
*
|
|
38
|
+
* `steps` is wrong for a second reason: these rows are a *branch*, not a
|
|
39
|
+
* sequence. A reader picks one and ignores the rest; none of them is first. A
|
|
40
|
+
* numbered "1 → 2 → 3" component is a real and separate thing worth building,
|
|
41
|
+
* and `steps` is the name it will need.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ A LIST, NOT A TABLE — which is what the markdown this replaces had to be.
|
|
44
|
+
* A screen reader announces "table, 2 columns, 7 rows" for what is a list of
|
|
45
|
+
* links with descriptions, and asks the reader to navigate it by cell. Two
|
|
46
|
+
* columns of sentence-length questions are also cramped on a phone, where this
|
|
47
|
+
* stacks instead.
|
|
48
|
+
*
|
|
49
|
+
* No client JavaScript: a heading and a list of links, rendered on the server.
|
|
50
|
+
*/
|
|
51
|
+
declare function DocsExplore({ links, heading, Link, externalLabel, className }: DocsExploreProps): ReactNode;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { DocsExplore, DocsExploreLink, DocsExploreProps };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { opensInNewTab } from "../safe-href.js";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/react/explore.tsx
|
|
4
|
+
/**
|
|
5
|
+
* "Where to go next" — a question per row, and the page that answers it.
|
|
6
|
+
*
|
|
7
|
+
* The sidebar is a structure and this is a router: it says *why* a reader would
|
|
8
|
+
* go somewhere, which no tree of titles can. A page opts in by declaring
|
|
9
|
+
* `explore` in its frontmatter, so the same component is a landing page's
|
|
10
|
+
* onboarding and an ordinary page's footnote.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ `explore`, NOT `next` — AND NOT `steps` EITHER.
|
|
13
|
+
*
|
|
14
|
+
* `next` is unusable in this package: `src/next.ts` is the Next.js adapter, so
|
|
15
|
+
* two files one directory apart would carry the same name for entirely
|
|
16
|
+
* different things, and `doc.frontmatter.next` would read like a routing hook.
|
|
17
|
+
*
|
|
18
|
+
* `steps` is wrong for a second reason: these rows are a *branch*, not a
|
|
19
|
+
* sequence. A reader picks one and ignores the rest; none of them is first. A
|
|
20
|
+
* numbered "1 → 2 → 3" component is a real and separate thing worth building,
|
|
21
|
+
* and `steps` is the name it will need.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ A LIST, NOT A TABLE — which is what the markdown this replaces had to be.
|
|
24
|
+
* A screen reader announces "table, 2 columns, 7 rows" for what is a list of
|
|
25
|
+
* links with descriptions, and asks the reader to navigate it by cell. Two
|
|
26
|
+
* columns of sentence-length questions are also cramped on a phone, where this
|
|
27
|
+
* stacks instead.
|
|
28
|
+
*
|
|
29
|
+
* No client JavaScript: a heading and a list of links, rendered on the server.
|
|
30
|
+
*/
|
|
31
|
+
function DocsExplore({ links, heading = "Where to go next", Link, externalLabel = "(opens in a new tab)", className }) {
|
|
32
|
+
if (links.length === 0) return null;
|
|
33
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
34
|
+
className: [
|
|
35
|
+
"wave-docs-panel",
|
|
36
|
+
"wave-docs-explore",
|
|
37
|
+
className
|
|
38
|
+
].filter(Boolean).join(" "),
|
|
39
|
+
"aria-labelledby": "wave-docs-explore-heading",
|
|
40
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
41
|
+
className: "wave-docs-panel__header",
|
|
42
|
+
children: /* @__PURE__ */ jsx("h2", {
|
|
43
|
+
className: "wave-docs-panel__title",
|
|
44
|
+
id: "wave-docs-explore-heading",
|
|
45
|
+
children: heading
|
|
46
|
+
})
|
|
47
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
48
|
+
className: "wave-docs-panel__body wave-docs-explore__list",
|
|
49
|
+
children: links.map((row) => /* @__PURE__ */ jsxs("li", {
|
|
50
|
+
className: "wave-docs-explore__item",
|
|
51
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
52
|
+
className: "wave-docs-explore__question",
|
|
53
|
+
children: row.question
|
|
54
|
+
}), /* @__PURE__ */ jsx(ExploreAnswer, {
|
|
55
|
+
row,
|
|
56
|
+
Link,
|
|
57
|
+
externalLabel
|
|
58
|
+
})]
|
|
59
|
+
}, row.href))
|
|
60
|
+
})]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function ExploreAnswer({ row, Link, externalLabel }) {
|
|
64
|
+
const className = "wave-docs-explore__answer";
|
|
65
|
+
if (opensInNewTab(row.href)) return /* @__PURE__ */ jsxs("a", {
|
|
66
|
+
className,
|
|
67
|
+
href: row.href,
|
|
68
|
+
target: "_blank",
|
|
69
|
+
rel: "noopener noreferrer",
|
|
70
|
+
children: [row.title, /* @__PURE__ */ jsxs("span", {
|
|
71
|
+
className: "wave-docs-sr-only",
|
|
72
|
+
children: [" ", externalLabel]
|
|
73
|
+
})]
|
|
74
|
+
});
|
|
75
|
+
return Link === void 0 ? /* @__PURE__ */ jsx("a", {
|
|
76
|
+
className,
|
|
77
|
+
href: row.href,
|
|
78
|
+
children: row.title
|
|
79
|
+
}) : /* @__PURE__ */ jsx(Link, {
|
|
80
|
+
className,
|
|
81
|
+
href: row.href,
|
|
82
|
+
children: row.title
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { DocsExplore };
|