@waveso/docs 0.10.0 → 0.12.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 +655 -0
- package/README.md +80 -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 +14 -1
- package/dist/nav-order.js +15 -1
- package/dist/next.js +26 -1
- 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/nav.js +14 -11
- package/dist/react/search-dialog.js +87 -15
- package/dist/react/shell-labels.d.ts +8 -1
- package/dist/react/shell-labels.js +1 -0
- package/dist/styles.css +1144 -168
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -69,8 +69,8 @@ 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.
|
|
73
|
-
| Search dialog and router wiring | 9.
|
|
72
|
+
| Everything the quick start ships, gzipped | 14.9 KB |
|
|
73
|
+
| Search dialog and router wiring | 9.8 KB |
|
|
74
74
|
| Navigation: one sidebar, open and closed | 3.1 KB |
|
|
75
75
|
| Table of contents | 1 KB |
|
|
76
76
|
| Copy-button runtime | 1.1 KB |
|
|
@@ -78,7 +78,7 @@ Every figure below is a **ceiling**, and `pnpm size` fails the build if the meas
|
|
|
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.9 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,7 @@ 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` |
|
|
206
207
|
| `DocsPager` | `react/pager` | Links to the pages either side of this one. `docs.Page` renders it; `pager: false` on the route omits it |
|
|
207
208
|
| `DocsToc` | `react/toc` | Scrollspy via `IntersectionObserver`. `label`, `topLabel`, `rootMargin`, `className` |
|
|
208
209
|
| `DocsSearch` | `react/next-search` | `SearchDialog`, wired to Next's router. What you want |
|
|
@@ -215,6 +216,40 @@ Every component takes data as props, and every module that imports from `next/*`
|
|
|
215
216
|
|
|
216
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.
|
|
217
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
|
+
|
|
218
253
|
### The pager
|
|
219
254
|
|
|
220
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.
|
|
@@ -686,6 +721,48 @@ your own `:root`, after the import:
|
|
|
686
721
|
}
|
|
687
722
|
```
|
|
688
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
|
+
|
|
689
766
|
That works because **everything this stylesheet declares lives in a `@layer`** —
|
|
690
767
|
`theme` for the tokens, `base` for element resets, `components` for the classes
|
|
691
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:)"),
|
package/dist/nav-order.d.ts
CHANGED
|
@@ -26,5 +26,18 @@ declare function neighbours(nodes: DocNavNode[], href: string): {
|
|
|
26
26
|
previous?: NavStop;
|
|
27
27
|
next?: NavStop;
|
|
28
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;
|
|
29
42
|
//#endregion
|
|
30
|
-
export { NavStop, neighbours, readingOrder };
|
|
43
|
+
export { NavStop, neighbours, readingOrder, stepTitle };
|
package/dist/nav-order.js
CHANGED
|
@@ -54,5 +54,19 @@ function neighbours(nodes, href) {
|
|
|
54
54
|
...next === void 0 ? {} : { next }
|
|
55
55
|
};
|
|
56
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
|
+
}
|
|
57
71
|
//#endregion
|
|
58
|
-
export { neighbours, readingOrder };
|
|
72
|
+
export { neighbours, readingOrder, stepTitle };
|
package/dist/next.js
CHANGED
|
@@ -3,12 +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 } from "./nav-order.js";
|
|
6
|
+
import { neighbours, readingOrder, stepTitle } from "./nav-order.js";
|
|
7
7
|
import { findFunctionValuedOptions } from "./search-options.js";
|
|
8
8
|
import { createMarkdownComponents } from "./react/markdown-components.js";
|
|
9
9
|
import { DocContent } from "./react/doc-content.js";
|
|
10
10
|
import { DocsHero } from "./react/hero.js";
|
|
11
11
|
import { DocsPager } from "./react/pager.js";
|
|
12
|
+
import { DocsExplore } from "./react/explore.js";
|
|
12
13
|
import { DocsToc } from "./react/toc.js";
|
|
13
14
|
import { wrapNextLink } from "./react/link-adapter.js";
|
|
14
15
|
import { createDocsRenderer } from "./render.js";
|
|
@@ -429,6 +430,25 @@ function createDocsRoute(options) {
|
|
|
429
430
|
etag: `"${createHash("sha1").update(json).digest("hex")}"`
|
|
430
431
|
} });
|
|
431
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
|
+
}
|
|
432
452
|
async function renderRoute(segments) {
|
|
433
453
|
const doc = await getPage(segments);
|
|
434
454
|
if (doc === void 0) return (await loadNotFound())();
|
|
@@ -450,6 +470,11 @@ function createDocsRoute(options) {
|
|
|
450
470
|
...options.components
|
|
451
471
|
},
|
|
452
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 }
|
|
453
478
|
}), options.pager === false ? null : createElement(DocsPager, {
|
|
454
479
|
...neighbours(await requestScopedSource.nav(), doc.href),
|
|
455
480
|
Link: link,
|
|
@@ -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 };
|
|
@@ -110,6 +110,20 @@ function createImage(Image) {
|
|
|
110
110
|
* "no tabindex on non-interactive elements", not a violation of it. A labelled
|
|
111
111
|
* `<section>` is a `region` landmark, so the tab stop announces itself instead
|
|
112
112
|
* of being a mystery stop in the tab order.
|
|
113
|
+
*
|
|
114
|
+
* ⚠️ ONE FRAME, AND IT WORE `.wave-docs-panel` FOR A WHILE.
|
|
115
|
+
*
|
|
116
|
+
* The panel — an outer frame, a header band, an inset card — exists to separate
|
|
117
|
+
* *chrome* from *content*. "Where to go next" and a code frame both have chrome
|
|
118
|
+
* to put in that band: a title, a language, a copy button. A table's header row
|
|
119
|
+
* is not chrome, it is data, and setting the body into a card away from its own
|
|
120
|
+
* header cost three vertical rules down each side, stopped the row dividers
|
|
121
|
+
* short of the box, and narrowed the reading width — on the densest element on
|
|
122
|
+
* a page, for nothing gained. Full-width dividers are what let an eye track a
|
|
123
|
+
* row across.
|
|
124
|
+
*
|
|
125
|
+
* It keeps the panel's outer radius, so a table and a code block still read as
|
|
126
|
+
* two of one family without the table pretending to have chrome it has not got.
|
|
113
127
|
*/
|
|
114
128
|
function createTable(label) {
|
|
115
129
|
return function MarkdownTable({ className, ...rest }) {
|
package/dist/react/nav.js
CHANGED
|
@@ -86,21 +86,24 @@ function DocsNav({ nav, pathname, Link, label = "Documentation", children, close
|
|
|
86
86
|
className: "wave-docs-layout__sidebar",
|
|
87
87
|
...state === void 0 ? {} : { "data-state": state ? "open" : "closed" },
|
|
88
88
|
...ready ? { "data-ready": "" } : {},
|
|
89
|
-
children: [/* @__PURE__ */
|
|
89
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
90
90
|
ref: navRef,
|
|
91
91
|
id: DOCS_NAV_ID,
|
|
92
92
|
className: "wave-docs-layout__sidebar-nav",
|
|
93
93
|
tabIndex: -1,
|
|
94
|
-
children:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
95
|
+
className: "wave-docs-layout__sidebar-scroll",
|
|
96
|
+
children: [children, /* @__PURE__ */ jsx(DocsSidebar, {
|
|
97
|
+
nav,
|
|
98
|
+
pathname,
|
|
99
|
+
label,
|
|
100
|
+
Link,
|
|
101
|
+
...expandGroup === void 0 ? {} : { expandGroup },
|
|
102
|
+
...collapseGroup === void 0 ? {} : { collapseGroup },
|
|
103
|
+
...externalLink === void 0 ? {} : { externalLink },
|
|
104
|
+
...icons === void 0 ? {} : { icons }
|
|
105
|
+
})]
|
|
106
|
+
})
|
|
104
107
|
}), /* @__PURE__ */ jsx("button", {
|
|
105
108
|
type: "button",
|
|
106
109
|
className: "wave-docs-layout__sidebar-trigger",
|