@waveso/docs 0.9.1 → 0.10.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 +159 -0
- package/README.md +22 -3
- package/dist/nav-order.d.ts +30 -0
- package/dist/nav-order.js +58 -0
- package/dist/next.d.ts +10 -0
- package/dist/next.js +8 -0
- package/dist/react/pager.d.ts +42 -0
- package/dist/react/pager.js +91 -0
- package/dist/react/shell-labels.d.ts +18 -1
- package/dist/react/shell-labels.js +3 -0
- package/dist/react/toc.js +34 -5
- package/dist/styles.css +256 -16
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,164 @@
|
|
|
1
1
|
# @waveso/docs
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f52cb99: **Every page links to the ones either side of it.** `docs.Page` renders a pager
|
|
8
|
+
under the prose. Nothing is authored: a page gets one by being in the
|
|
9
|
+
navigation.
|
|
10
|
+
|
|
11
|
+
⚠️ THE ORDER IS THE SIDEBAR'S, NOT THE SLUG LIST'S. `generateStaticParams` has
|
|
12
|
+
every route in it and no opinion about their order; `meta.json` is where the
|
|
13
|
+
author said what comes next, and it is what the reader is looking at. Two
|
|
14
|
+
orderings of the same pages is two answers to one question, and they drift the
|
|
15
|
+
first time a `meta.json` moves — so the pager reads the same tree `DocsSidebar`
|
|
16
|
+
renders and flattens it. A pager that disagrees with the column beside it is
|
|
17
|
+
impossible by construction.
|
|
18
|
+
|
|
19
|
+
Separators and external links are not stops: a separator is a label with
|
|
20
|
+
nowhere to go, and a "next page" that lands on npm has ended the sequence
|
|
21
|
+
rather than continued it. A group with an `index.md` contributes its own page
|
|
22
|
+
before its children, which is the order its rows appear in.
|
|
23
|
+
|
|
24
|
+
⚠️ A PAGE OUTSIDE THE TREE GETS NO PAGER, RATHER THAN THE FIRST ONE. `-1` from
|
|
25
|
+
`findIndex` reads as "just before the beginning", so an unguarded lookup hands
|
|
26
|
+
every draft and every route rendered outside the navigation the same first page
|
|
27
|
+
as its "next" — confidently wrong on exactly the pages nobody checks.
|
|
28
|
+
|
|
29
|
+
⚠️ AND AN EMPTY CELL AT EACH END, NOT A MISSING ONE. The two links share a grid
|
|
30
|
+
row; drop the absent side and the survivor slides into the first track, so the
|
|
31
|
+
first page of a site puts "Next" on the left and every other page puts it on
|
|
32
|
+
the right. The one page where the position moves is the one a reader sees
|
|
33
|
+
first.
|
|
34
|
+
|
|
35
|
+
Nothing renders at all when there is no neighbour either side — a one-page site
|
|
36
|
+
would otherwise get a navigation landmark containing nothing.
|
|
37
|
+
|
|
38
|
+
A chevron on the outer edge of each link points the way it goes.
|
|
39
|
+
|
|
40
|
+
⚠️ AND "OUTWARD" MIRRORS. Under `dir="rtl"` the grid's first track is on the
|
|
41
|
+
right, so the _previous_ link moves there and its arrow has to point right —
|
|
42
|
+
the reverse of the rule that draws it. Same trap as the sidebar's chevron, in a
|
|
43
|
+
component built after it, and `[dir='rtl']` again rather than `:dir(rtl)`.
|
|
44
|
+
|
|
45
|
+
No client JavaScript: two links, two captions and two glyphs, rendered on the
|
|
46
|
+
server. Each
|
|
47
|
+
link is named by direction _and_ destination — "Previous: Installation" — since
|
|
48
|
+
a link announced as a bare title says nothing about which way it goes. The
|
|
49
|
+
landmark is named too, because a page now carries three of them.
|
|
50
|
+
|
|
51
|
+
New: `DocsPager` at `@waveso/docs/react/pager`, `pager: false` on
|
|
52
|
+
`createDocsRoute` to omit it, and `previousPage`, `nextPage` and `pagination`
|
|
53
|
+
in `labels`.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- 09a9369: The sidebar grip says whether it has anything to do.
|
|
58
|
+
|
|
59
|
+
Blue when the navigation is hidden or the pointer is on it; grey when the
|
|
60
|
+
sidebar is open and untouched. One rule at every width — a closed sidebar is
|
|
61
|
+
the same request for attention on a phone as on a desktop.
|
|
62
|
+
|
|
63
|
+
⚠️ `:not([data-state='open'])`, NOT `[data-state='closed']`. The attribute is
|
|
64
|
+
absent until the reader chooses, and a server-rendered page has none — so
|
|
65
|
+
matching only the explicit value leaves every first paint below 64rem showing a
|
|
66
|
+
grey grip in front of hidden navigation, which is the one moment the cue is
|
|
67
|
+
for. Above 64rem the default inverts, and a second rule says so.
|
|
68
|
+
|
|
69
|
+
⚠️ AND THE COLOUR IS A CUSTOM PROPERTY, NOT A SELECTOR FIGHT. Three things want
|
|
70
|
+
to set it — the resting style, the pointer, and the sidebar's state — and the
|
|
71
|
+
state lives on an _ancestor_, so `.sidebar[data-state] .trigger::before`
|
|
72
|
+
outranks `.trigger:hover::before` by a whole class. Written as backgrounds that
|
|
73
|
+
is a rule which silently kills hover on the one state that still needs it.
|
|
74
|
+
Properties settle it by inheritance: the state sets them on the sidebar, the
|
|
75
|
+
trigger sets them on itself under `:hover`, and a value on the element always
|
|
76
|
+
beats one it inherited. No specificity ladder, no `!important`.
|
|
77
|
+
|
|
78
|
+
The grip is also a full pill now rather than a rounded rectangle.
|
|
79
|
+
|
|
80
|
+
⚠️ TESTED AS MARKUP, BECAUSE A MOUNTED COMPONENT CANNOT SHOW THE UNTOUCHED
|
|
81
|
+
STATE. `DocsNav` resolves the mode on mount and writes `data-state`
|
|
82
|
+
immediately, so every React fixture is already explicit. Two wrong versions
|
|
83
|
+
passed the whole suite against that — matching the explicit value, and dropping
|
|
84
|
+
the wide-layout reset — and both are caught now by a server-shaped fixture with
|
|
85
|
+
no attribute at all.
|
|
86
|
+
|
|
87
|
+
Also fixed here: the focus-indicator guard looked up one rule per selector, and
|
|
88
|
+
a selector may legitimately appear in several. The trigger now has one rule
|
|
89
|
+
setting properties on focus and another drawing the ring; taking "the first" or
|
|
90
|
+
"the last" is a coin flip on file order, so it reads every rule and asks for
|
|
91
|
+
one to declare an outline.
|
|
92
|
+
|
|
93
|
+
## 0.9.2
|
|
94
|
+
|
|
95
|
+
### Patch Changes
|
|
96
|
+
|
|
97
|
+
- 6b84354: A separator now rules off the block above it.
|
|
98
|
+
|
|
99
|
+
It ends one section as much as it names the next, and 1rem of margin was not
|
|
100
|
+
saying so — the gap read as "these two lists are a bit far apart" rather than as
|
|
101
|
+
a division.
|
|
102
|
+
|
|
103
|
+
⚠️ THE RULE AND THE LABEL SHARE THE LIST'S OWN EDGE, WHICH IS ALSO THE ROWS'. A
|
|
104
|
+
row is full-bleed — its hover surface spans the whole column, and so does the
|
|
105
|
+
search field above it — so a rule on that edge divides the column, while an
|
|
106
|
+
inset one floats inside it. The label sits on the same line, because a heading
|
|
107
|
+
and the rule above it reading as one object is the whole reason the rule exists.
|
|
108
|
+
|
|
109
|
+
The label keeps the rows' own content edge — the marker column when there is
|
|
110
|
+
one, the words when `icons={false}` removes it. Those are the same number: a
|
|
111
|
+
row's `padding-inline` is what both modes have in common, so matching it lands
|
|
112
|
+
on whichever is there, with no query and nothing threaded to the stylesheet.
|
|
113
|
+
|
|
114
|
+
⚠️ AND NOT ABOVE THE FIRST CHILD. A `meta.json` may open with
|
|
115
|
+
`"---Reference---"`, and on that tree the very first thing in the navigation
|
|
116
|
+
would otherwise be a hairline above nothing.
|
|
117
|
+
|
|
118
|
+
The label also drops from `font-weight: 650` to `500`. At 650 it was heavier
|
|
119
|
+
than the group titles it sits under — a divider out-shouting the navigation it
|
|
120
|
+
divides. Weight rather than colour, because there is no lighter colour to
|
|
121
|
+
reach for: `--wave-docs-fg-subtle` is already the lightest text token at 5.05:1
|
|
122
|
+
against WCAG 1.4.3's 4.5:1 floor, and `--wave-docs-border` — the rule's own
|
|
123
|
+
colour — measures 1.31:1 and is a line colour, not a text one.
|
|
124
|
+
|
|
125
|
+
- 7e67c5a: The table of contents marks the last section when you reach it.
|
|
126
|
+
|
|
127
|
+
Scroll to the foot of a page whose final section is short and nothing happened:
|
|
128
|
+
the entry stayed on the section _above_, and the last one could only be
|
|
129
|
+
highlighted by clicking its own link.
|
|
130
|
+
|
|
131
|
+
⚠️ NO `rootMargin` FIXES THIS, WHICH IS WHY IT LOOKED LIKE A TUNING PROBLEM.
|
|
132
|
+
The default makes the top 40% of the viewport the region that counts as
|
|
133
|
+
current, and that is right while there is document left to scroll — a heading
|
|
134
|
+
rises into the band and takes the highlight. At the end there is none. A short
|
|
135
|
+
trailing section sits on screen, fully readable, below a band it can never
|
|
136
|
+
enter, while the heading above it is still _inside_ that band. The observer was
|
|
137
|
+
giving a correct answer to the wrong question. Any band smaller than the
|
|
138
|
+
viewport has this hole; a bigger one only trades it for a highlight that jumps
|
|
139
|
+
early.
|
|
140
|
+
|
|
141
|
+
So the end of the document is handled as what it is — a place where scrolling
|
|
142
|
+
stops answering — and the last heading takes the highlight there. Scroll up and
|
|
143
|
+
the band has it straight back, without waiting for a heading to cross.
|
|
144
|
+
|
|
145
|
+
⚠️ AND BOTH INPUTS GO THROUGH ONE RESOLVER, WHICH IS THE HALF THAT IS EASY TO
|
|
146
|
+
MISS. Left as two `setActiveId` calls they race, and the observer wins — it
|
|
147
|
+
fires last and it still likes the heading above. The first version of this fix
|
|
148
|
+
was measured doing exactly nothing for that reason.
|
|
149
|
+
|
|
150
|
+
⚠️ ONLY WHEN THE DOCUMENT ACTUALLY SCROLLS. On a page that fits, "scrolled to
|
|
151
|
+
the bottom" is true at rest, and the last section would be current before the
|
|
152
|
+
reader had read a word of the first.
|
|
153
|
+
|
|
154
|
+
The listener is `passive` and reads two numbers — no `getBoundingClientRect`,
|
|
155
|
+
no layout flush. Like every other scroll reader here it watches the document; a
|
|
156
|
+
host that scrolls an inner pane keeps the observer's behaviour and loses only
|
|
157
|
+
this tail case.
|
|
158
|
+
|
|
159
|
+
`toc` grows 0.88 → 1 KB, and the published total 14.5 → 14.6 KB. It is still
|
|
160
|
+
the smallest client component in the package.
|
|
161
|
+
|
|
3
162
|
## 0.9.1
|
|
4
163
|
|
|
5
164
|
### Patch Changes
|
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,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
|
+
| `DocsPager` | `react/pager` | Links to the pages either side of this one. `docs.Page` renders it; `pager: false` on the route omits it |
|
|
206
207
|
| `DocsToc` | `react/toc` | Scrollspy via `IntersectionObserver`. `label`, `topLabel`, `rootMargin`, `className` |
|
|
207
208
|
| `DocsSearch` | `react/next-search` | `SearchDialog`, wired to Next's router. What you want |
|
|
208
209
|
| `DocsLink` | `react/next-link` | `next/link`, adapted — pass it as `Link` when composing by hand |
|
|
@@ -214,6 +215,24 @@ Every component takes data as props, and every module that imports from `next/*`
|
|
|
214
215
|
|
|
215
216
|
`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
217
|
|
|
218
|
+
### The pager
|
|
219
|
+
|
|
220
|
+
`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.
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
| Prop | Type | Default | What it is |
|
|
225
|
+
| --- | --- | --- | --- |
|
|
226
|
+
| `previous` | `NavStop` | — | The stop before this page. Omit at the beginning |
|
|
227
|
+
| `next` | `NavStop` | — | The stop after it. Omit at the end |
|
|
228
|
+
| `Link` | `DocsLinkComponent` | `<a>` | Client-side router link |
|
|
229
|
+
| `previousLabel` | `string` | `'Previous'` | Above the previous page's title |
|
|
230
|
+
| `nextLabel` | `string` | `'Next'` | Above the next page's title |
|
|
231
|
+
| `label` | `string` | `'Pagination'` | Accessible name for the landmark |
|
|
232
|
+
| `className` | `string` | — | Extra classes |
|
|
233
|
+
|
|
234
|
+
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`.
|
|
235
|
+
|
|
217
236
|
### Sidebar icons
|
|
218
237
|
|
|
219
238
|
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.
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
//#endregion
|
|
30
|
+
export { NavStop, neighbours, readingOrder };
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
//#endregion
|
|
58
|
+
export { neighbours, readingOrder };
|
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,12 @@ 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
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";
|
|
10
12
|
import { DocsToc } from "./react/toc.js";
|
|
11
13
|
import { wrapNextLink } from "./react/link-adapter.js";
|
|
12
14
|
import { createDocsRenderer } from "./render.js";
|
|
@@ -448,6 +450,12 @@ function createDocsRoute(options) {
|
|
|
448
450
|
...options.components
|
|
449
451
|
},
|
|
450
452
|
...copyLabels === void 0 ? {} : { labels: copyLabels }
|
|
453
|
+
}), options.pager === false ? null : createElement(DocsPager, {
|
|
454
|
+
...neighbours(await requestScopedSource.nav(), doc.href),
|
|
455
|
+
Link: link,
|
|
456
|
+
...routeLabels?.previousPage === void 0 ? {} : { previousLabel: routeLabels.previousPage },
|
|
457
|
+
...routeLabels?.nextPage === void 0 ? {} : { nextLabel: routeLabels.nextPage },
|
|
458
|
+
...routeLabels?.pagination === void 0 ? {} : { label: routeLabels.pagination }
|
|
451
459
|
})), doc.toc.length === 0 ? null : createElement("aside", { className: "wave-docs-layout__toc" }, createElement(DocsToc, {
|
|
452
460
|
entries: doc.toc,
|
|
453
461
|
...routeLabels?.toc === void 0 ? {} : { label: routeLabels.toc },
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NavStop } from "../nav-order.js";
|
|
2
|
+
import { DocsLinkComponent } from "./markdown-components.js";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
//#region src/react/pager.d.ts
|
|
5
|
+
interface DocsPagerProps {
|
|
6
|
+
/** The stop before this page in the reading order. Omit at the beginning. */
|
|
7
|
+
previous?: NavStop | undefined;
|
|
8
|
+
/** The stop after it. Omit at the end. */
|
|
9
|
+
next?: NavStop | undefined;
|
|
10
|
+
/** Client-side router link, e.g. `next/link`. Falls back to `<a>`. */
|
|
11
|
+
Link?: DocsLinkComponent | undefined;
|
|
12
|
+
/** Above the previous page's title. Defaults to `'Previous'`. */
|
|
13
|
+
previousLabel?: string | undefined;
|
|
14
|
+
/** Above the next page's title. Defaults to `'Next'`. */
|
|
15
|
+
nextLabel?: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Accessible name for the landmark. Defaults to `'Pagination'`.
|
|
18
|
+
*
|
|
19
|
+
* Named because a page can hold more than one navigation landmark — this,
|
|
20
|
+
* the sidebar and the table of contents — and "navigation" three times is
|
|
21
|
+
* not a list anyone can steer by.
|
|
22
|
+
*/
|
|
23
|
+
label?: string | undefined;
|
|
24
|
+
className?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Links to the pages either side of this one in the reading order.
|
|
28
|
+
*
|
|
29
|
+
* Derived from the same tree `DocsSidebar` renders, so it cannot disagree with
|
|
30
|
+
* the column beside it — see `nav-order.ts`. Nothing here is authored: a page
|
|
31
|
+
* gets a pager by being in the navigation.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ NOTHING RENDERS AT ALL WHEN THERE IS NO NEIGHBOUR EITHER SIDE. A single
|
|
34
|
+
* page site, or a route outside the tree, would otherwise get an empty
|
|
35
|
+
* landmark — announced by a screen reader as a navigation region containing
|
|
36
|
+
* nothing, which is worse than the absence it is standing in for.
|
|
37
|
+
*
|
|
38
|
+
* No client JavaScript: two links and a heading, rendered on the server.
|
|
39
|
+
*/
|
|
40
|
+
declare function DocsPager({ previous, next, Link, previousLabel, nextLabel, label, className }: DocsPagerProps): ReactNode;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { DocsPager, DocsPagerProps };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/react/pager.tsx
|
|
3
|
+
/**
|
|
4
|
+
* Links to the pages either side of this one in the reading order.
|
|
5
|
+
*
|
|
6
|
+
* Derived from the same tree `DocsSidebar` renders, so it cannot disagree with
|
|
7
|
+
* the column beside it — see `nav-order.ts`. Nothing here is authored: a page
|
|
8
|
+
* gets a pager by being in the navigation.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ NOTHING RENDERS AT ALL WHEN THERE IS NO NEIGHBOUR EITHER SIDE. A single
|
|
11
|
+
* page site, or a route outside the tree, would otherwise get an empty
|
|
12
|
+
* landmark — announced by a screen reader as a navigation region containing
|
|
13
|
+
* nothing, which is worse than the absence it is standing in for.
|
|
14
|
+
*
|
|
15
|
+
* No client JavaScript: two links and a heading, rendered on the server.
|
|
16
|
+
*/
|
|
17
|
+
function DocsPager({ previous, next, Link, previousLabel = "Previous", nextLabel = "Next", label = "Pagination", className }) {
|
|
18
|
+
if (previous === void 0 && next === void 0) return null;
|
|
19
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
20
|
+
"aria-label": label,
|
|
21
|
+
className: ["wave-docs-pager", className].filter(Boolean).join(" "),
|
|
22
|
+
children: [/* @__PURE__ */ jsx(PagerLink, {
|
|
23
|
+
stop: previous,
|
|
24
|
+
direction: "previous",
|
|
25
|
+
caption: previousLabel,
|
|
26
|
+
Link
|
|
27
|
+
}), /* @__PURE__ */ jsx(PagerLink, {
|
|
28
|
+
stop: next,
|
|
29
|
+
direction: "next",
|
|
30
|
+
caption: nextLabel,
|
|
31
|
+
Link
|
|
32
|
+
})]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function PagerLink({ stop, direction, caption, Link }) {
|
|
36
|
+
if (stop === void 0) return /* @__PURE__ */ jsx("div", { className: "wave-docs-pager__gap" });
|
|
37
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
38
|
+
className: "wave-docs-pager__caption",
|
|
39
|
+
children: [
|
|
40
|
+
direction === "previous" ? /* @__PURE__ */ jsx(PagerChevron, {}) : null,
|
|
41
|
+
caption,
|
|
42
|
+
direction === "next" ? /* @__PURE__ */ jsx(PagerChevron, {}) : null
|
|
43
|
+
]
|
|
44
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
45
|
+
className: "wave-docs-pager__title",
|
|
46
|
+
children: stop.title
|
|
47
|
+
})] });
|
|
48
|
+
const props = {
|
|
49
|
+
className: "wave-docs-pager__link",
|
|
50
|
+
href: stop.href,
|
|
51
|
+
"data-direction": direction,
|
|
52
|
+
"aria-label": `${caption}: ${stop.title}`
|
|
53
|
+
};
|
|
54
|
+
return Link === void 0 ? /* @__PURE__ */ jsx("a", {
|
|
55
|
+
...props,
|
|
56
|
+
children: body
|
|
57
|
+
}) : /* @__PURE__ */ jsx(Link, {
|
|
58
|
+
...props,
|
|
59
|
+
children: body
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The arrow on the outer edge of each link, pointing the way it goes.
|
|
64
|
+
*
|
|
65
|
+
* Decorative: the link is named "Previous: Installation" by `aria-label`, so
|
|
66
|
+
* this would only repeat a word already in the name — and `⌘`-style symbols
|
|
67
|
+
* read badly when they reach a screen reader at all.
|
|
68
|
+
*
|
|
69
|
+
* Which way it points is the stylesheet's, not this component's: it is a
|
|
70
|
+
* physical direction, and it mirrors under `dir="rtl"` where "previous" is on
|
|
71
|
+
* the right. Rotating it here would put that decision somewhere CSS cannot
|
|
72
|
+
* correct it.
|
|
73
|
+
*/
|
|
74
|
+
function PagerChevron() {
|
|
75
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
76
|
+
className: "wave-docs-pager__chevron",
|
|
77
|
+
"aria-hidden": "true",
|
|
78
|
+
focusable: "false",
|
|
79
|
+
viewBox: "0 0 24 24",
|
|
80
|
+
width: "16",
|
|
81
|
+
height: "16",
|
|
82
|
+
fill: "none",
|
|
83
|
+
stroke: "currentColor",
|
|
84
|
+
strokeWidth: "2",
|
|
85
|
+
strokeLinecap: "round",
|
|
86
|
+
strokeLinejoin: "round",
|
|
87
|
+
children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
export { DocsPager };
|
|
@@ -70,6 +70,23 @@ interface DocsLabels {
|
|
|
70
70
|
toc?: string | undefined;
|
|
71
71
|
/** The link at the end of the TOC. Default `'Back to top'`. */
|
|
72
72
|
backToTop?: string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Above the previous page's title in the pager. Defaults to `'Previous'`.
|
|
75
|
+
*
|
|
76
|
+
* The direction, not the destination: the page's own title is the name, and
|
|
77
|
+
* these two words are what say which way it lies.
|
|
78
|
+
*/
|
|
79
|
+
previousPage?: string | undefined;
|
|
80
|
+
/** The same for the next page. Defaults to `'Next'`. */
|
|
81
|
+
nextPage?: string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Accessible name for the pager landmark. Defaults to `'Pagination'`.
|
|
84
|
+
*
|
|
85
|
+
* A page carries three navigation landmarks — the sidebar, the table of
|
|
86
|
+
* contents and this — and "navigation" three times is not a list anyone can
|
|
87
|
+
* steer by.
|
|
88
|
+
*/
|
|
89
|
+
pagination?: string | undefined;
|
|
73
90
|
/**
|
|
74
91
|
* Screen-reader suffix on a link that opens a new tab.
|
|
75
92
|
* Default `'(opens in a new tab)'`.
|
|
@@ -137,7 +154,7 @@ interface DocsLabels {
|
|
|
137
154
|
* failure being guarded against is a key that is declared, documented and never
|
|
138
155
|
* read, which type-checks perfectly.
|
|
139
156
|
*/
|
|
140
|
-
declare const DOCS_LABEL_KEYS: readonly ["nav", "openNav", "closeNav", "skipToContent", "expandGroup", "collapseGroup", "toc", "backToTop", "externalLink", "table", "calloutNote", "calloutTip", "calloutImportant", "calloutWarning", "calloutCaution", "youtubeTitle", "youtubePlay", "youtubeHide", "copyCode", "copyCodeFrom", "copied", "copyFailed"];
|
|
157
|
+
declare const DOCS_LABEL_KEYS: readonly ["nav", "openNav", "closeNav", "skipToContent", "expandGroup", "collapseGroup", "toc", "backToTop", "externalLink", "table", "calloutNote", "calloutTip", "calloutImportant", "calloutWarning", "calloutCaution", "youtubeTitle", "youtubePlay", "youtubeHide", "copyCode", "copyCodeFrom", "previousPage", "nextPage", "pagination", "copied", "copyFailed"];
|
|
141
158
|
/** The four the shell renders itself, resolved centrally. */
|
|
142
159
|
type ShellLabelKey = 'nav' | 'openNav' | 'closeNav' | 'skipToContent';
|
|
143
160
|
/**
|
package/dist/react/toc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { DOCS_CONTENT_ID } from "../docs-content-id.js";
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/react/toc.tsx
|
|
6
6
|
const DEFAULT_ROOT_MARGIN = "0px 0px -60% 0px";
|
|
@@ -48,14 +48,26 @@ function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MA
|
|
|
48
48
|
lastIds.current = ids;
|
|
49
49
|
setActiveId(void 0);
|
|
50
50
|
}
|
|
51
|
+
const inBand = useRef(/* @__PURE__ */ new Set());
|
|
52
|
+
const atBottom = useRef(false);
|
|
53
|
+
const resolveActive = useCallback(() => {
|
|
54
|
+
if (atBottom.current) {
|
|
55
|
+
const last = ids[ids.length - 1];
|
|
56
|
+
if (last !== void 0) {
|
|
57
|
+
setActiveId(last);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const next = ids.find((id) => inBand.current.has(id));
|
|
62
|
+
if (next !== void 0) setActiveId(next);
|
|
63
|
+
}, [ids]);
|
|
51
64
|
useEffect(() => {
|
|
52
65
|
if (ids.length === 0 || typeof IntersectionObserver === "undefined") return;
|
|
53
|
-
const visible =
|
|
66
|
+
const visible = inBand.current;
|
|
54
67
|
const observer = new IntersectionObserver((records) => {
|
|
55
68
|
for (const record of records) if (record.isIntersecting) visible.add(record.target.id);
|
|
56
69
|
else visible.delete(record.target.id);
|
|
57
|
-
|
|
58
|
-
if (next !== void 0) setActiveId(next);
|
|
70
|
+
resolveActive();
|
|
59
71
|
}, {
|
|
60
72
|
rootMargin,
|
|
61
73
|
threshold: 0
|
|
@@ -81,7 +93,24 @@ function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MA
|
|
|
81
93
|
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
82
94
|
observer.disconnect();
|
|
83
95
|
};
|
|
84
|
-
}, [
|
|
96
|
+
}, [
|
|
97
|
+
ids,
|
|
98
|
+
rootMargin,
|
|
99
|
+
resolveActive
|
|
100
|
+
]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (ids[ids.length - 1] === void 0 || typeof window === "undefined") return;
|
|
103
|
+
const onScroll = () => {
|
|
104
|
+
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
|
105
|
+
const bottom = scrollable > 2 && window.scrollY >= scrollable - 2;
|
|
106
|
+
if (bottom === atBottom.current) return;
|
|
107
|
+
atBottom.current = bottom;
|
|
108
|
+
resolveActive();
|
|
109
|
+
};
|
|
110
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
111
|
+
onScroll();
|
|
112
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
113
|
+
}, [ids, resolveActive]);
|
|
85
114
|
if (entries.length === 0) return null;
|
|
86
115
|
return /* @__PURE__ */ jsxs("nav", {
|
|
87
116
|
"aria-label": label,
|
package/dist/styles.css
CHANGED
|
@@ -382,12 +382,19 @@
|
|
|
382
382
|
* Declared at all because inheriting means a host that never set a family
|
|
383
383
|
* renders its documentation in the UA serif. `--wave-docs-font-sans: inherit`
|
|
384
384
|
* gives the host's typeface back in one line.
|
|
385
|
+
*
|
|
386
|
+
* ⚠️ EVERY NEW ROOT HAS TO BE ADDED HERE, AND ONE ALREADY WAS NOT. The pager
|
|
387
|
+
* sits inside `<main>` but outside `.wave-docs-prose` — `DocContent` owns
|
|
388
|
+
* that class — so it matched nothing in this list and shipped its page titles
|
|
389
|
+
* in Times. Caught in a screenshot, not by a test: nothing here fails when a
|
|
390
|
+
* root is missing, it just renders wrong.
|
|
385
391
|
*/
|
|
386
392
|
.wave-docs-prose,
|
|
387
393
|
.wave-docs-sidebar,
|
|
388
394
|
.wave-docs-toc,
|
|
389
395
|
.wave-docs-skip-link,
|
|
390
396
|
.wave-docs-hero,
|
|
397
|
+
.wave-docs-pager,
|
|
391
398
|
.wave-docs-layout__sidebar,
|
|
392
399
|
.wave-docs-search-trigger,
|
|
393
400
|
.wave-docs-search-dialog {
|
|
@@ -1764,10 +1771,37 @@
|
|
|
1764
1771
|
rotate: 90deg;
|
|
1765
1772
|
}
|
|
1766
1773
|
|
|
1774
|
+
/*
|
|
1775
|
+
* A separator ends the block above it as much as it names the block below,
|
|
1776
|
+
* and a rule says that in a way whitespace alone cannot: at 1rem of margin
|
|
1777
|
+
* the gap read as "these two lists are a bit far apart" rather than as a
|
|
1778
|
+
* division.
|
|
1779
|
+
*
|
|
1780
|
+
* ⚠️ THE RULE AND THE LABEL SHARE THE LIST'S OWN EDGE, WHICH IS ALSO THE
|
|
1781
|
+
* ROWS'. A row is full-bleed — its hover surface spans the whole column, and
|
|
1782
|
+
* so does the search field above it — so a rule on that edge divides the
|
|
1783
|
+
* column, while an inset one floats inside it. The label sits on the same
|
|
1784
|
+
* line, because a heading and the rule above it reading as one object is the
|
|
1785
|
+
* whole reason the rule is there.
|
|
1786
|
+
*
|
|
1787
|
+
* Measured before this: the label sat at 24px, the rows' *icon* column. It
|
|
1788
|
+
* headed a column of decoration, a step in from the words it names and a step
|
|
1789
|
+
* out from the edge — aligned with neither.
|
|
1790
|
+
*
|
|
1791
|
+
* ⚠️ AND NO RULE ON THE FIRST CHILD, which would draw a line above nothing. A
|
|
1792
|
+
* `meta.json` may open with `"---Reference---"`, and on that tree the very
|
|
1793
|
+
* first thing in the navigation would be a hairline.
|
|
1794
|
+
*/
|
|
1767
1795
|
.wave-docs-sidebar__separator-item {
|
|
1768
1796
|
margin-block: 1rem 0.25rem;
|
|
1769
1797
|
}
|
|
1770
1798
|
|
|
1799
|
+
.wave-docs-sidebar__separator-item:not(:first-child) {
|
|
1800
|
+
margin-block-start: 1rem;
|
|
1801
|
+
padding-block-start: 1rem;
|
|
1802
|
+
border-block-start: 1px solid var(--wave-docs-border);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1771
1805
|
/*
|
|
1772
1806
|
* ⚠️ NO `text-transform: uppercase`, AND IT USED TO HAVE ONE.
|
|
1773
1807
|
*
|
|
@@ -1784,12 +1818,38 @@
|
|
|
1784
1818
|
* single character. `letter-spacing` went with them: it exists to make caps
|
|
1785
1819
|
* legible, and on sentence case it only reads as loose.
|
|
1786
1820
|
*/
|
|
1821
|
+
/*
|
|
1822
|
+
* The label starts where a row's content starts — the marker column when
|
|
1823
|
+
* there is one, the words when there is not.
|
|
1824
|
+
*
|
|
1825
|
+
* ⚠️ THAT IS ONE NUMBER, NOT TWO, AND CHASING IT WITH `:has()` WAS THE WRONG
|
|
1826
|
+
* SHAPE. A row's own `padding-inline` is what both modes have in common: with
|
|
1827
|
+
* markers the icon sits on it, with `icons={false}` the text does. Matching
|
|
1828
|
+
* the row's padding lands on whichever is there, in both modes, with no
|
|
1829
|
+
* query and nothing threaded.
|
|
1830
|
+
*
|
|
1831
|
+
* The *rule* keeps the column's edge instead — it divides the column rather
|
|
1832
|
+
* than naming anything. See `__separator-item`.
|
|
1833
|
+
*/
|
|
1787
1834
|
.wave-docs-sidebar__separator {
|
|
1788
1835
|
display: block;
|
|
1789
|
-
padding-inline: 0.5rem;
|
|
1836
|
+
padding-inline-start: 0.5rem;
|
|
1790
1837
|
color: var(--wave-docs-fg-subtle);
|
|
1791
1838
|
font-size: 0.75rem;
|
|
1792
|
-
|
|
1839
|
+
/*
|
|
1840
|
+
* ⚠️ THE WEIGHT IS THE LEVER HERE, NOT THE COLOUR. At 650 this label was
|
|
1841
|
+
* heavier than the group titles it sits under and read as the loudest thing
|
|
1842
|
+
* in the column — a divider out-shouting the navigation it divides.
|
|
1843
|
+
*
|
|
1844
|
+
* The obvious fix is a lighter colour, and there is none to reach for:
|
|
1845
|
+
* `--wave-docs-fg-subtle` is already the lightest text token at 5.05:1 on
|
|
1846
|
+
* the page, against WCAG 1.4.3's 4.5:1 floor for text this size — 0.55 of
|
|
1847
|
+
* slack. `--wave-docs-border`, which is what the rule above it is painted
|
|
1848
|
+
* with, measures 1.31:1 and is a line colour, not a text one.
|
|
1849
|
+
*
|
|
1850
|
+
* 500 drops it below the rows without touching the contrast at all.
|
|
1851
|
+
*/
|
|
1852
|
+
font-weight: 500;
|
|
1793
1853
|
}
|
|
1794
1854
|
|
|
1795
1855
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -1828,6 +1888,133 @@
|
|
|
1828
1888
|
}
|
|
1829
1889
|
}
|
|
1830
1890
|
|
|
1891
|
+
/* ---------------------------------------------------------------------------
|
|
1892
|
+
* Pager
|
|
1893
|
+
*
|
|
1894
|
+
* Links to the pages either side of this one, emitted by `docs.Page` at the
|
|
1895
|
+
* foot of the article.
|
|
1896
|
+
* ------------------------------------------------------------------------ */
|
|
1897
|
+
|
|
1898
|
+
@layer components {
|
|
1899
|
+
/*
|
|
1900
|
+
* ⚠️ TWO TRACKS, ALWAYS — WHICH IS WHY THE COMPONENT RENDERS AN EMPTY CELL
|
|
1901
|
+
* RATHER THAN NOTHING AT EACH END OF THE SEQUENCE. With `auto-fit` or a
|
|
1902
|
+
* plain flex row, the first page of a site puts its only link — `Next` — on
|
|
1903
|
+
* the left, and every other page puts it on the right. The reader learns the
|
|
1904
|
+
* position, and the one page where it moves is the one they see first.
|
|
1905
|
+
*
|
|
1906
|
+
* ⚠️ THE MEASURE *AND* THE CENTRING, WHICH IS TWO DECLARATIONS AND I SHIPPED
|
|
1907
|
+
* ONE. `.wave-docs-prose` is capped and centred inside `__main`, so a
|
|
1908
|
+
* `max-width` alone left this the right width in the wrong place: the
|
|
1909
|
+
* paragraphs began at 440px and the pager at 304px, a 136px step directly
|
|
1910
|
+
* under them. It belongs to the prose, so it takes both halves of what puts
|
|
1911
|
+
* the prose where it is.
|
|
1912
|
+
*/
|
|
1913
|
+
.wave-docs-pager {
|
|
1914
|
+
display: grid;
|
|
1915
|
+
margin-inline: auto;
|
|
1916
|
+
/* `minmax(0, 1fr)`, never a bare `1fr`: that floors each track at its
|
|
1917
|
+
* content's min-content width, so a long page title pushes the pair past
|
|
1918
|
+
* the measure instead of wrapping inside it. */
|
|
1919
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
1920
|
+
gap: 0.75rem;
|
|
1921
|
+
max-width: var(--wave-docs-measure);
|
|
1922
|
+
margin-block-start: 3rem;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
.wave-docs-pager__link {
|
|
1926
|
+
display: flex;
|
|
1927
|
+
flex-direction: column;
|
|
1928
|
+
gap: 0.25rem;
|
|
1929
|
+
padding: 0.75rem 1rem;
|
|
1930
|
+
border: 1px solid var(--wave-docs-border);
|
|
1931
|
+
border-radius: var(--wave-docs-radius);
|
|
1932
|
+
color: var(--wave-docs-fg);
|
|
1933
|
+
text-decoration: none;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/*
|
|
1937
|
+
* The next page is set to its own edge, so the pair reads outward from the
|
|
1938
|
+
* middle — the direction each one takes you. `text-align` and not
|
|
1939
|
+
* `align-items`, so a title that wraps to two lines stays aligned with
|
|
1940
|
+
* itself rather than ragging against the box.
|
|
1941
|
+
*/
|
|
1942
|
+
.wave-docs-pager__link[data-direction='next'] {
|
|
1943
|
+
text-align: end;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/*
|
|
1947
|
+
* ⚠️ THE ARROW POINTS OUTWARD, AND "OUTWARD" MIRRORS. Under `dir="rtl"` the
|
|
1948
|
+
* grid's first track is on the right, so the *previous* link moves there and
|
|
1949
|
+
* its arrow has to point right — the reverse of both rules above it. Same
|
|
1950
|
+
* shape as the sidebar's chevron, and the same reason it is `[dir='rtl']`
|
|
1951
|
+
* rather than `:dir(rtl)`: lightningcss rewrites the latter into a list of
|
|
1952
|
+
* right-to-left *languages*, which is not the same question.
|
|
1953
|
+
*
|
|
1954
|
+
* Source order is load-bearing in the middle pair: `[dir='rtl'] .chevron` and
|
|
1955
|
+
* `[data-direction='next'] .chevron` have the same specificity, so the
|
|
1956
|
+
* mirror only wins by being written second.
|
|
1957
|
+
*/
|
|
1958
|
+
.wave-docs-pager__chevron {
|
|
1959
|
+
flex: none;
|
|
1960
|
+
/* Sized down from the 16 it is drawn at: beside 12px type a full-size
|
|
1961
|
+
* chevron is the loudest thing in the card. */
|
|
1962
|
+
inline-size: 0.875rem;
|
|
1963
|
+
block-size: 0.875rem;
|
|
1964
|
+
rotate: 180deg;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
.wave-docs-pager__link[data-direction='next'] .wave-docs-pager__chevron {
|
|
1968
|
+
rotate: 0deg;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
[dir='rtl'] .wave-docs-pager__chevron {
|
|
1972
|
+
rotate: 0deg;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
[dir='rtl']
|
|
1976
|
+
.wave-docs-pager__link[data-direction='next']
|
|
1977
|
+
.wave-docs-pager__chevron {
|
|
1978
|
+
rotate: 180deg;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/* The arrow shares the caption's line, so the two read as one label and the
|
|
1982
|
+
* title below starts at the card's own edge. */
|
|
1983
|
+
.wave-docs-pager__caption {
|
|
1984
|
+
display: flex;
|
|
1985
|
+
align-items: center;
|
|
1986
|
+
gap: 0.25rem;
|
|
1987
|
+
color: var(--wave-docs-fg-subtle);
|
|
1988
|
+
font-size: 0.75rem;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.wave-docs-pager__link[data-direction='next'] .wave-docs-pager__caption {
|
|
1992
|
+
justify-content: flex-end;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
.wave-docs-pager__title {
|
|
1996
|
+
font-weight: 600;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
.wave-docs-pager__link:hover {
|
|
2000
|
+
border-color: var(--wave-docs-border-strong);
|
|
2001
|
+
background: var(--wave-docs-bg-subtle);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
.wave-docs-pager__link:focus-visible {
|
|
2005
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
2006
|
+
outline-offset: 2px;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2010
|
+
.wave-docs-pager__link {
|
|
2011
|
+
transition:
|
|
2012
|
+
background-color 150ms ease-out,
|
|
2013
|
+
border-color 150ms ease-out;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1831
2018
|
/* ---------------------------------------------------------------------------
|
|
1832
2019
|
* Table of contents
|
|
1833
2020
|
* ------------------------------------------------------------------------ */
|
|
@@ -2574,6 +2761,47 @@
|
|
|
2574
2761
|
* 20px transparent border for a while to answer to 44 while painting 24, and
|
|
2575
2762
|
* a box the eye cannot see is a box the next person deletes.
|
|
2576
2763
|
*/
|
|
2764
|
+
/*
|
|
2765
|
+
* ⚠️ THE GRIP'S COLOUR IS A CUSTOM PROPERTY, NOT A SELECTOR FIGHT.
|
|
2766
|
+
*
|
|
2767
|
+
* Three things want to set it: the resting style, the reader's pointer, and
|
|
2768
|
+
* the sidebar's own state — and the state lives on an *ancestor*, so
|
|
2769
|
+
* `.sidebar[data-state] .trigger::before` outranks `.trigger:hover::before`
|
|
2770
|
+
* by a whole class. Written as backgrounds that is a rule that silently kills
|
|
2771
|
+
* hover on the one state that still needs it.
|
|
2772
|
+
*
|
|
2773
|
+
* Custom properties settle it by inheritance instead: the state sets them on
|
|
2774
|
+
* the sidebar, the trigger sets them on itself under `:hover`, and a value on
|
|
2775
|
+
* the element always beats one it inherited. No specificity, no `!important`,
|
|
2776
|
+
* and the hover rule stays two lines long.
|
|
2777
|
+
*/
|
|
2778
|
+
.wave-docs-layout__sidebar {
|
|
2779
|
+
--wave-docs-trigger-fill: var(--wave-docs-border-strong);
|
|
2780
|
+
--wave-docs-trigger-ink: var(--wave-docs-fg-muted);
|
|
2781
|
+
--wave-docs-trigger-opacity: 0.4;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
/*
|
|
2785
|
+
* Blue means the grip is the thing to press; grey means it is at rest.
|
|
2786
|
+
*
|
|
2787
|
+
* The navigation is hidden and this is the way back to it — so a closed
|
|
2788
|
+
* sidebar lights the grip, exactly as the pointer does. Open, the grip has
|
|
2789
|
+
* nothing to offer that the visible column does not, and it goes quiet.
|
|
2790
|
+
*
|
|
2791
|
+
* ⚠️ `:not([data-state='open'])`, NOT `[data-state='closed']`. Below 64rem
|
|
2792
|
+
* the sidebar starts closed and the attribute is absent until the reader
|
|
2793
|
+
* chooses — a server-rendered page has no state at all. Matching only the
|
|
2794
|
+
* explicit value leaves every first paint on a phone showing a grey grip in
|
|
2795
|
+
* front of hidden navigation, which is the one moment the cue is for.
|
|
2796
|
+
*
|
|
2797
|
+
* The wide layout inverts the default, and overrides this below.
|
|
2798
|
+
*/
|
|
2799
|
+
.wave-docs-layout__sidebar:not([data-state='open']) {
|
|
2800
|
+
--wave-docs-trigger-fill: var(--wave-docs-accent);
|
|
2801
|
+
--wave-docs-trigger-ink: var(--wave-docs-accent-fg);
|
|
2802
|
+
--wave-docs-trigger-opacity: 1;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2577
2805
|
.wave-docs-layout__sidebar-trigger {
|
|
2578
2806
|
flex: none;
|
|
2579
2807
|
/*
|
|
@@ -2603,7 +2831,7 @@
|
|
|
2603
2831
|
padding: 4px;
|
|
2604
2832
|
border: 0;
|
|
2605
2833
|
background: none;
|
|
2606
|
-
color: var(--wave-docs-
|
|
2834
|
+
color: var(--wave-docs-trigger-ink);
|
|
2607
2835
|
cursor: pointer;
|
|
2608
2836
|
}
|
|
2609
2837
|
|
|
@@ -2627,8 +2855,10 @@
|
|
|
2627
2855
|
* of the three dots, and nothing here scales with the pill: see `::after`.
|
|
2628
2856
|
*/
|
|
2629
2857
|
height: 3.5rem;
|
|
2630
|
-
|
|
2631
|
-
|
|
2858
|
+
/* A full pill: the radius is half the *short* side, so `9999px` clamps to
|
|
2859
|
+
* exactly that whatever the two dimensions become. */
|
|
2860
|
+
border-radius: 9999px;
|
|
2861
|
+
background-color: var(--wave-docs-trigger-fill);
|
|
2632
2862
|
}
|
|
2633
2863
|
|
|
2634
2864
|
/*
|
|
@@ -2680,14 +2910,17 @@
|
|
|
2680
2910
|
*/
|
|
2681
2911
|
.wave-docs-layout__sidebar-trigger::before,
|
|
2682
2912
|
.wave-docs-layout__sidebar-trigger::after {
|
|
2683
|
-
opacity:
|
|
2913
|
+
opacity: var(--wave-docs-trigger-opacity);
|
|
2684
2914
|
}
|
|
2685
2915
|
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
.wave-docs-layout__sidebar-trigger:
|
|
2690
|
-
|
|
2916
|
+
/* On the element, not on the pseudo-elements: a custom property set here is
|
|
2917
|
+
* inherited by both, and beats the sidebar's regardless of how specific the
|
|
2918
|
+
* state selector that set it was. */
|
|
2919
|
+
.wave-docs-layout__sidebar-trigger:hover,
|
|
2920
|
+
.wave-docs-layout__sidebar-trigger:focus-visible {
|
|
2921
|
+
--wave-docs-trigger-fill: var(--wave-docs-accent);
|
|
2922
|
+
--wave-docs-trigger-ink: var(--wave-docs-accent-fg);
|
|
2923
|
+
--wave-docs-trigger-opacity: 1;
|
|
2691
2924
|
}
|
|
2692
2925
|
|
|
2693
2926
|
/*
|
|
@@ -2702,11 +2935,6 @@
|
|
|
2702
2935
|
var(--wave-docs-accent) 30%,
|
|
2703
2936
|
transparent
|
|
2704
2937
|
);
|
|
2705
|
-
color: var(--wave-docs-accent-fg);
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
.wave-docs-layout__sidebar-trigger:hover::before {
|
|
2709
|
-
background-color: var(--wave-docs-accent);
|
|
2710
2938
|
}
|
|
2711
2939
|
|
|
2712
2940
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -2830,6 +3058,18 @@
|
|
|
2830
3058
|
margin-inline-end: 0;
|
|
2831
3059
|
}
|
|
2832
3060
|
|
|
3061
|
+
/*
|
|
3062
|
+
* Unchosen means *open* up here, so the grip is at rest by default — the
|
|
3063
|
+
* mirror of the rule above, and the reason that one is written as
|
|
3064
|
+
* "not open" rather than "closed". A closed sidebar keeps the blue it
|
|
3065
|
+
* inherits from there; nothing needs restating.
|
|
3066
|
+
*/
|
|
3067
|
+
.wave-docs-layout__sidebar:not([data-state='closed']) {
|
|
3068
|
+
--wave-docs-trigger-fill: var(--wave-docs-border-strong);
|
|
3069
|
+
--wave-docs-trigger-ink: var(--wave-docs-fg-muted);
|
|
3070
|
+
--wave-docs-trigger-opacity: 0.4;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2833
3073
|
/* Nothing is covered, so nothing is scrimmed. */
|
|
2834
3074
|
.wave-docs-layout__sidebar-scrim {
|
|
2835
3075
|
display: none;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveso/docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Zero parser bytes in the browser: markdown docs for Next.js, built to hast in Node and rendered as your components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -107,6 +107,10 @@
|
|
|
107
107
|
"./react/youtube": {
|
|
108
108
|
"types": "./dist/react/youtube.d.ts",
|
|
109
109
|
"default": "./dist/react/youtube.js"
|
|
110
|
+
},
|
|
111
|
+
"./react/pager": {
|
|
112
|
+
"types": "./dist/react/pager.d.ts",
|
|
113
|
+
"default": "./dist/react/pager.js"
|
|
110
114
|
}
|
|
111
115
|
},
|
|
112
116
|
"files": [
|