@waveso/docs 0.9.0 → 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 +226 -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 +359 -20
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,231 @@
|
|
|
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
|
+
|
|
162
|
+
## 0.9.1
|
|
163
|
+
|
|
164
|
+
### Patch Changes
|
|
165
|
+
|
|
166
|
+
- 80d3db4: The sidebar's handle and the search shortcut stop competing with the page.
|
|
167
|
+
|
|
168
|
+
**The handle is 16 × 56 and faded.** It was 20 × 80 at full strength — a solid
|
|
169
|
+
slab beside the reading column, for a control nobody looks at while reading. It
|
|
170
|
+
now sits at 40% until a pointer or a caret reaches it, the same treatment as the
|
|
171
|
+
tree's markers.
|
|
172
|
+
|
|
173
|
+
⚠️ BOTH MARKS FADE, NOT THE PILL ALONE. Fading `::before` by itself leaves crisp
|
|
174
|
+
dots on a washed-out slab, which reads louder than the solid grip it replaced.
|
|
175
|
+
|
|
176
|
+
⚠️ AND NOT ON THE `<button>`, which would take the focus ring down with it —
|
|
177
|
+
`opacity` applies to the whole element, outline included, so a keyboard reader
|
|
178
|
+
would get a 40% indicator on the control they had just moved to. It is on the
|
|
179
|
+
two pseudo-elements, and `:focus-visible` restores both.
|
|
180
|
+
|
|
181
|
+
⚠️ THE TAP TARGET IS UNCHANGED BY ANY OF IT. The button is the whole strip and
|
|
182
|
+
runs the height of the column; the pill is paint. At 16px plus 4px of padding a
|
|
183
|
+
side the strip is 24px wide — WCAG 2.5.8's minimum to the pixel, and a test now
|
|
184
|
+
says so, because the next narrowing is the one that fails it.
|
|
185
|
+
|
|
186
|
+
⚠️ THE DOTS DO NOT SCALE WITH THE PILL. They are `box-shadow` offsets on one
|
|
187
|
+
element, so they span 18px whatever the pill does. Found by shortening it to
|
|
188
|
+
16px tall and watching the outer two render outside it.
|
|
189
|
+
|
|
190
|
+
**`⌘K` is levelled with `Search`.** Equal `font-size` in two families is not
|
|
191
|
+
equal type: the label is `ui-sans-serif` and the badge `ui-monospace`, which
|
|
192
|
+
draws 0.7292px of cap per px. `1.012em` is what puts the `K`'s cap on the `S`'s.
|
|
193
|
+
|
|
194
|
+
⚠️ AND THE SYMBOL IS `1.369em`, THE MEASURED INK RATIO — NOT A HAIR MORE. At
|
|
195
|
+
`1.45em` the `⌘` stood 6% above the cap line, and the badge read as _bigger
|
|
196
|
+
type_ than the label: 88 device px of ink against the word's 85, its top three
|
|
197
|
+
higher, its centre 1.5 out. Same letter height, louder cluster. At the ratio,
|
|
198
|
+
`Search` and `⌘K` measure 85 and 85 and share a centre to the device pixel.
|
|
199
|
+
|
|
200
|
+
⚠️ AND `line-height: 0` ON THE SYMBOL, WHICH IS WHAT MADE THE TWO CENTRE. A line
|
|
201
|
+
box is as tall as the tallest inline box in it, so the 20.5px glyph made the
|
|
202
|
+
`<kbd>` 20.55px against the label's 17 — and flex centres them by their _boxes_,
|
|
203
|
+
so the `K` rode 1.9px high inside a box the symbol had stretched.
|
|
204
|
+
|
|
205
|
+
Also fixed: the focus-indicator test looked its selectors up with `indexOf`, so
|
|
206
|
+
`…:focus-visible` matched inside `…:focus-visible::before` — a different rule,
|
|
207
|
+
about pseudo-elements, with no business declaring an outline. It reported the
|
|
208
|
+
trigger as having no focus indicator while the trigger's own rule sat further
|
|
209
|
+
down the file declaring one.
|
|
210
|
+
|
|
211
|
+
- 27f63ec: Sidebar separators keep the case their author wrote them in.
|
|
212
|
+
|
|
213
|
+
⚠️ `text-transform: uppercase` ON A STRING THIS PACKAGE DOES NOT OWN. A
|
|
214
|
+
separator's text comes from a consumer's `meta.json` — `"---Reference---"` —
|
|
215
|
+
and restyling it is this package rewriting words in a language it cannot read.
|
|
216
|
+
Portuguese `Referência` shipped as `REFERÊNCIA`. Turkish trades its dotted and
|
|
217
|
+
dotless `i` for each other under a naive uppercase. No CJK script has a case to
|
|
218
|
+
transform at all, so those authors got the `letter-spacing` and none of the
|
|
219
|
+
effect it existed to rescue.
|
|
220
|
+
|
|
221
|
+
The string was already a prop. Its shape was not, and there was no way to turn
|
|
222
|
+
this off short of overriding the rule.
|
|
223
|
+
|
|
224
|
+
Reading as a divider rather than as another row is done by size, weight and a
|
|
225
|
+
subtle colour — none of which touch a character. `letter-spacing` goes with the
|
|
226
|
+
caps: it is there to make uppercase legible, and on sentence case it only reads
|
|
227
|
+
as loose.
|
|
228
|
+
|
|
3
229
|
## 0.9.0
|
|
4
230
|
|
|
5
231
|
### Minor 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
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
* of padding on every side — so setting this is the whole of resizing the
|
|
154
154
|
* control, and the hit area grows with it rather than being a second number
|
|
155
155
|
* to keep in step. */
|
|
156
|
-
--wave-docs-trigger-width:
|
|
156
|
+
--wave-docs-trigger-width: 1rem;
|
|
157
157
|
--wave-docs-toc-width: 15rem;
|
|
158
158
|
|
|
159
159
|
/*
|
|
@@ -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,18 +1771,85 @@
|
|
|
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
|
+
|
|
1805
|
+
/*
|
|
1806
|
+
* ⚠️ NO `text-transform: uppercase`, AND IT USED TO HAVE ONE.
|
|
1807
|
+
*
|
|
1808
|
+
* A separator's text is authored — `"---Reference---"` in someone's
|
|
1809
|
+
* `meta.json` — so uppercasing it is this package rewriting words it does not
|
|
1810
|
+
* own, in a language it cannot read. Portuguese `Referência` came out as
|
|
1811
|
+
* `REFERÊNCIA`; Turkish loses the dotted and dotless `i` to each other, and
|
|
1812
|
+
* no CJK script has a case to transform at all, so those authors got the
|
|
1813
|
+
* `letter-spacing` and none of the effect it was there to rescue. And it
|
|
1814
|
+
* could not be turned off — the string is a prop, but its shape was ours.
|
|
1815
|
+
*
|
|
1816
|
+
* The job the caps were doing — read as a divider, not as a row — is already
|
|
1817
|
+
* done by the size, the weight and the subtle colour, none of which touch a
|
|
1818
|
+
* single character. `letter-spacing` went with them: it exists to make caps
|
|
1819
|
+
* legible, and on sentence case it only reads as loose.
|
|
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
|
+
*/
|
|
1771
1834
|
.wave-docs-sidebar__separator {
|
|
1772
1835
|
display: block;
|
|
1773
|
-
padding-inline: 0.5rem;
|
|
1836
|
+
padding-inline-start: 0.5rem;
|
|
1774
1837
|
color: var(--wave-docs-fg-subtle);
|
|
1775
1838
|
font-size: 0.75rem;
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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;
|
|
1779
1853
|
}
|
|
1780
1854
|
|
|
1781
1855
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -1814,6 +1888,133 @@
|
|
|
1814
1888
|
}
|
|
1815
1889
|
}
|
|
1816
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
|
+
|
|
1817
2018
|
/* ---------------------------------------------------------------------------
|
|
1818
2019
|
* Table of contents
|
|
1819
2020
|
* ------------------------------------------------------------------------ */
|
|
@@ -2560,6 +2761,47 @@
|
|
|
2560
2761
|
* 20px transparent border for a while to answer to 44 while painting 24, and
|
|
2561
2762
|
* a box the eye cannot see is a box the next person deletes.
|
|
2562
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
|
+
|
|
2563
2805
|
.wave-docs-layout__sidebar-trigger {
|
|
2564
2806
|
flex: none;
|
|
2565
2807
|
/*
|
|
@@ -2589,7 +2831,7 @@
|
|
|
2589
2831
|
padding: 4px;
|
|
2590
2832
|
border: 0;
|
|
2591
2833
|
background: none;
|
|
2592
|
-
color: var(--wave-docs-
|
|
2834
|
+
color: var(--wave-docs-trigger-ink);
|
|
2593
2835
|
cursor: pointer;
|
|
2594
2836
|
}
|
|
2595
2837
|
|
|
@@ -2607,9 +2849,16 @@
|
|
|
2607
2849
|
.wave-docs-layout__sidebar-trigger::before {
|
|
2608
2850
|
content: '';
|
|
2609
2851
|
width: var(--wave-docs-trigger-width);
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2852
|
+
/*
|
|
2853
|
+
* 16 x 56 — a 3.5:1 grip. It was 20 x 80, which at the old width read as a
|
|
2854
|
+
* slab; narrowing it made the length obvious. The floor is 18px, the span
|
|
2855
|
+
* of the three dots, and nothing here scales with the pill: see `::after`.
|
|
2856
|
+
*/
|
|
2857
|
+
height: 3.5rem;
|
|
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);
|
|
2613
2862
|
}
|
|
2614
2863
|
|
|
2615
2864
|
/*
|
|
@@ -2631,11 +2880,49 @@
|
|
|
2631
2880
|
translate: -50% -50%;
|
|
2632
2881
|
border-radius: 50%;
|
|
2633
2882
|
background: currentcolor;
|
|
2883
|
+
/*
|
|
2884
|
+
* ⚠️ THE DOTS DO NOT SCALE WITH THE PILL. They are `box-shadow` offsets on
|
|
2885
|
+
* one element, so ±7px is 18px of ink whatever `--wave-docs-trigger-width`
|
|
2886
|
+
* or the pill's height happen to be. Shorten the pill below 18px and the
|
|
2887
|
+
* outer two sit outside it, top and bottom — measured, not guessed.
|
|
2888
|
+
*/
|
|
2634
2889
|
box-shadow:
|
|
2635
2890
|
0 -7px 0 currentcolor,
|
|
2636
2891
|
0 7px 0 currentcolor;
|
|
2637
2892
|
}
|
|
2638
2893
|
|
|
2894
|
+
/*
|
|
2895
|
+
* Faded at rest, full strength under a pointer or a caret — the same
|
|
2896
|
+
* treatment as the tree's markers, and for the same reason: this is a handle,
|
|
2897
|
+
* not a thing to look at while reading.
|
|
2898
|
+
*
|
|
2899
|
+
* ⚠️ BOTH MARKS, NOT THE PILL ALONE. Fading `::before` by itself leaves the
|
|
2900
|
+
* dots at full strength on a washed-out slab, which reads louder than the
|
|
2901
|
+
* solid grip it replaced — the opposite of the point.
|
|
2902
|
+
*
|
|
2903
|
+
* ⚠️ AND NOT ON THE BUTTON ITSELF, which would take the focus ring down with
|
|
2904
|
+
* it: `opacity` applies to the whole element, outline included, so a keyboard
|
|
2905
|
+
* reader would get a 40% indicator on a control they had just moved to.
|
|
2906
|
+
*
|
|
2907
|
+
* ⚠️ THE TAP TARGET IS UNTOUCHED BY ANY OF THIS. The `<button>` is the entire
|
|
2908
|
+
* strip and runs the height of the column; the pill is paint. Its 80px was
|
|
2909
|
+
* only ever ink, which is why 32px costs nothing a finger can feel.
|
|
2910
|
+
*/
|
|
2911
|
+
.wave-docs-layout__sidebar-trigger::before,
|
|
2912
|
+
.wave-docs-layout__sidebar-trigger::after {
|
|
2913
|
+
opacity: var(--wave-docs-trigger-opacity);
|
|
2914
|
+
}
|
|
2915
|
+
|
|
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;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2639
2926
|
/*
|
|
2640
2927
|
* ⚠️ THE STRIP'S HOVER LIGHTS THE BUTTON TOO, AND THAT IS WHAT MAKES IT ONE
|
|
2641
2928
|
* CONTROL. Hovering 44px of strip while the button inside it stays grey reads
|
|
@@ -2648,11 +2935,6 @@
|
|
|
2648
2935
|
var(--wave-docs-accent) 30%,
|
|
2649
2936
|
transparent
|
|
2650
2937
|
);
|
|
2651
|
-
color: var(--wave-docs-accent-fg);
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
.wave-docs-layout__sidebar-trigger:hover::before {
|
|
2655
|
-
background-color: var(--wave-docs-accent);
|
|
2656
2938
|
}
|
|
2657
2939
|
|
|
2658
2940
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -2667,7 +2949,8 @@
|
|
|
2667
2949
|
*/
|
|
2668
2950
|
transition:
|
|
2669
2951
|
background-color 120ms ease-out,
|
|
2670
|
-
color 120ms ease-out
|
|
2952
|
+
color 120ms ease-out,
|
|
2953
|
+
opacity 120ms ease-out;
|
|
2671
2954
|
}
|
|
2672
2955
|
}
|
|
2673
2956
|
|
|
@@ -2775,6 +3058,18 @@
|
|
|
2775
3058
|
margin-inline-end: 0;
|
|
2776
3059
|
}
|
|
2777
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
|
+
|
|
2778
3073
|
/* Nothing is covered, so nothing is scrimmed. */
|
|
2779
3074
|
.wave-docs-layout__sidebar-scrim {
|
|
2780
3075
|
display: none;
|
|
@@ -2945,20 +3240,64 @@
|
|
|
2945
3240
|
border-radius: var(--wave-docs-radius-sm);
|
|
2946
3241
|
}
|
|
2947
3242
|
|
|
3243
|
+
/*
|
|
3244
|
+
* ⚠️ THE LABEL'S SIZE, AND IT IS THE CAPS THAT HAD TO MATCH — NOT THE NUMBER.
|
|
3245
|
+
*
|
|
3246
|
+
* `Search` is `ui-sans-serif` and this is `ui-monospace`, and the same
|
|
3247
|
+
* `font-size` in two families does not give you the same letter. Measured at
|
|
3248
|
+
* 14px: the label's `S` carries 10.33px of ink and the mono `K` carries
|
|
3249
|
+
* 10.21px. A tenth of a pixel — invisible until you look, and then it is the
|
|
3250
|
+
* only thing you can see.
|
|
3251
|
+
*
|
|
3252
|
+
* `1.012em` is that tenth. Mono here draws 0.7292px of cap per px of
|
|
3253
|
+
* `font-size`, so 10.33px of cap needs 14.17px, which is 1.012 x the 14px it
|
|
3254
|
+
* sits beside. `em` rather than a `rem` constant so it tracks the label if
|
|
3255
|
+
* the trigger's size ever moves.
|
|
3256
|
+
*
|
|
3257
|
+
* ⚠️ IT IS A MEASURED CONSTANT AND IT BELONGS TO THESE TWO FAMILIES. Change
|
|
3258
|
+
* `--wave-docs-font-mono` or `--wave-docs-font-sans` and it is wrong — not
|
|
3259
|
+
* broken, just quietly a tenth of a pixel out again. Re-measure the caps, do
|
|
3260
|
+
* not re-derive from the declaration.
|
|
3261
|
+
*
|
|
3262
|
+
* Not in the shared rule above: the footer's caps sit in a 12px footer and
|
|
3263
|
+
* belong at 12px. This is the only `<kbd>` standing beside body-sized text.
|
|
3264
|
+
*/
|
|
3265
|
+
.wave-docs-search-trigger-kbd {
|
|
3266
|
+
font-size: 1.012em;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
2948
3269
|
/*
|
|
2949
3270
|
* ⚠️ THE SYMBOL IS SCALED AND THE WORD IS NOT — which is why this is a
|
|
2950
3271
|
* separate element rather than one string in the `<kbd>`.
|
|
2951
3272
|
*
|
|
2952
|
-
* Measured in the shipped mono stack
|
|
2953
|
-
* `K
|
|
2954
|
-
*
|
|
3273
|
+
* Measured in the shipped mono stack: `⌘` carries 6.39px of ink per 8.75px of
|
|
3274
|
+
* `K`, so at a shared `font-size` it sits a third short of the letter next to
|
|
3275
|
+
* it. `1.369em` is exactly that ratio, and it makes the two the same height.
|
|
3276
|
+
*
|
|
3277
|
+
* ⚠️ NOT A HAIR TALLER, WHICH IS WHERE THIS FIRST LANDED. `1.45em` gave the
|
|
3278
|
+
* symbol 6% on the `K` — invisible at 12px, and at this size 0.6px of ink
|
|
3279
|
+
* standing above the cap line. It made the whole badge read as *bigger type*
|
|
3280
|
+
* than the label beside it: measured against the word `Search`, 88 device px
|
|
3281
|
+
* of ink against 85, its top three device px higher and its centre 1.5 out.
|
|
3282
|
+
* Same letter height, louder cluster. The eye compares the clusters.
|
|
2955
3283
|
*
|
|
2956
3284
|
* `Ctrl` gets none of it: it is a word set in the same face as the `K`, and
|
|
2957
3285
|
* scaling it would make the hint shout.
|
|
2958
3286
|
*/
|
|
2959
3287
|
.wave-docs-search-trigger-mod[data-symbol] {
|
|
2960
|
-
font-size: 1.
|
|
2961
|
-
|
|
3288
|
+
font-size: 1.369em;
|
|
3289
|
+
/*
|
|
3290
|
+
* ⚠️ `0`, NOT `1` — THIS GLYPH MUST NOT SIZE THE LINE IT SITS ON. A line
|
|
3291
|
+
* box is as tall as the tallest inline box in it, so at `line-height: 1`
|
|
3292
|
+
* this 20.5px symbol made the `<kbd>` 20.55px tall against the label's 17,
|
|
3293
|
+
* and the two were centred as flex items by their *boxes*. Same cap height,
|
|
3294
|
+
* 1.9px apart on screen — measured off an 8x render, invisible in the CSS.
|
|
3295
|
+
*
|
|
3296
|
+
* At `0` the symbol contributes no height, the `<kbd>` keeps the line box
|
|
3297
|
+
* its own font gives it, and the `K` centres with the `S` beside it. The
|
|
3298
|
+
* glyph still paints in full: `line-height` bounds the line, not the ink.
|
|
3299
|
+
*/
|
|
3300
|
+
line-height: 0;
|
|
2962
3301
|
/*
|
|
2963
3302
|
* ⚠️ AND IT HAS TO BE MOVED DOWN, WHICH `font-size` ALONE WILL NEVER DO.
|
|
2964
3303
|
*
|
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": [
|