@waveso/docs 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,544 @@
1
1
  # @waveso/docs
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5730af3: **The code frame wears the panel.** A fence is now an outer card holding a
8
+ header row — the filename or the language on the left, the copy button on the
9
+ right — and an inset surface with the code on it, dressed by the same
10
+ `.wave-docs-panel` rules as "where to go next".
11
+
12
+ ⚠️ THE `<figcaption>` STAYS A DIRECT CHILD OF THE `<figure>`, WHICH IS WHY THIS
13
+ HAS NO HEADER WRAPPER. A `figcaption` has to be the first or last child of its
14
+ figure; inside the `.wave-docs-panel__header` `<div>` that "where to go next"
15
+ uses it captions nothing, the markup is invalid, and a titled block loses the
16
+ accessible name it had. So the frame lays its header out on a grid and shares
17
+ the primitive's _insets_ rather than its header element. The button stays out of
18
+ the caption for the matching reason: a `<button>` inside a `<figcaption>`
19
+ contributes its accessible name to the figure's, so `swap.ts` would announce as
20
+ "swap.ts Copy code from swap.ts".
21
+
22
+ ⚠️ AND A TITLE IS WHAT DECIDES WHETHER THERE IS A FRAME AT ALL. With one, the
23
+ figure is a panel: a band carrying the filename and the copy button, and the
24
+ code set into a card below it. With none there is nothing to put in a band, so
25
+ the frame flattens away, the surface becomes the block, and the button sits on
26
+ the code — which is what Mintlify does, and what stops an unnamed fence from
27
+ carrying a reserved slot with nothing in it.
28
+
29
+ A language is not a title for this purpose. A fence declaring `ts` and no
30
+ filename is still an untitled fence, and a band holding a two-letter badge is
31
+ the same empty header with a word in it. `data-lang` stays on the figure for
32
+ anyone selecting on it.
33
+
34
+ One shape of markup, switched in the stylesheet rather than in the pipeline: two
35
+ markup paths mean two fixtures, and the one that is not on screen is the one
36
+ that rots.
37
+
38
+ **The copy button no longer hides until you hover.** It faded in on `:hover` or
39
+ `:focus-within` because it was positioned over the code and had nowhere of its
40
+ own to be. It has a slot in the header row now, and a reserved slot that stays
41
+ empty until you point at it reads as a rendering fault — so the reveal, the
42
+ `@media (hover: none)` exception that existed because a hover-only control does
43
+ not exist on a phone, and the reduced-motion guard on its transition all went
44
+ with it. It is still `visibility: hidden` until the runtime attaches, which is
45
+ the structural promise and is unchanged.
46
+
47
+ ## Three things the restructure moved, each of which was a defect
48
+
49
+ ⚠️ THE `<pre>` DRAWS NO FRAME OF ITS OWN ANY MORE. It carried the border, the
50
+ radius and the background; inside a `.wave-docs-panel__body` that carries all
51
+ three, that draws the frame twice one pixel apart. `pre:not(.shiki)` keeps its
52
+ own, because an excluded fence is never wrapped.
53
+
54
+ ⚠️ AND ITS INLINE PADDING IS `1rem` BECAUSE THAT IS `--wave-docs-panel-inset`,
55
+ not because it is a round number. The label sits at that inset plus the
56
+ surface's border; the code sits at the surface's border plus this. The
57
+ `1.125rem` it was put the first character 2px right of the filename above it —
58
+ visible, and attributable to nothing.
59
+
60
+ ⚠️ AND THE FOCUS RING ON THE `<pre>` IS INSET NOW. Shiki gives it
61
+ `tabindex="0"` so a keyboard reader can scroll a wide block, and the surface
62
+ around it is `overflow: hidden` so a square corner cannot poke through the
63
+ frame's rounded one — which clipped a `+2px` outline away to nothing.
64
+ `styles.test.ts` reads rules as text and would have gone on passing.
65
+
66
+ ## The panel grew two properties, both to settle a cascade rather than a taste
67
+
68
+ `--wave-docs-panel-surface` is the inset surface's ground, and its default lives
69
+ in a `var()` fallback rather than in a declaration. `.wave-docs-code__body` and
70
+ `.wave-docs-panel__body` are both one class, so source order decides and the
71
+ panel is declared later: a code frame asking for the darker code ground got the
72
+ panel's white. Moving the ground to a property did not fix it either — a frame
73
+ wears `.wave-docs-panel` _and_ `.wave-docs-code`, so both rules set that property
74
+ on the same element at the same specificity, and source order handed it back.
75
+ Measured twice as `oklch(1 0 0)` where `oklch(0.975 0.003 262)` was written. In
76
+ the fallback there is no declaration to lose to.
77
+
78
+ `--wave-docs-panel-header-row` floors the header. With a label the row is the
79
+ label's height and with none it is the button's, and a page mixing titled and
80
+ untitled fences showed two header heights.
81
+
82
+ The frame's markup now has one home — `codeFrameMarkup` — that the browser tier
83
+ mounts and the plugin tier asserts the pipeline agrees with. Written out
84
+ separately in both, a hand-written fixture goes on describing a frame the
85
+ pipeline stopped emitting while every assertion measuring it stays green.
86
+
87
+ - 793d0f7: **"Where to go next": a question per row, and the page that answers it.** A page
88
+ opts in from its frontmatter:
89
+
90
+ ```yaml
91
+ explore:
92
+ - question: How a person is recognised across servers
93
+ href: ./identity.md
94
+ - question: What happens when the network fails
95
+ href: ./delivery.md
96
+ ```
97
+
98
+ A sidebar is a structure; this is a router. It says _why_ a reader would go
99
+ somewhere, which no tree of titles can — so the same component is a landing
100
+ page's onboarding and an ordinary page's footnote.
101
+
102
+ ⚠️ THE LINK TEXT COMES FROM THE NAVIGATION. An `href` pointing at a page in the
103
+ tree takes that page's own name, so renaming it updates every block that points
104
+ at it — the same tree the sidebar and the pager read. Name a `title` only where
105
+ the tree cannot answer: an external link, or a page kept out of the navigation.
106
+
107
+ ⚠️ AND AN HREF THAT RESOLVES TO NEITHER STOPS THE BUILD. Falling back to the
108
+ href renders a URL where a sentence should be — "Where this runs and what that
109
+ buys → /docs/infrastructure" — on a page that builds cleanly, and nothing else
110
+ in the pipeline would notice. The frontmatter is authored and the author is
111
+ right there, so the error names the page and both fixes.
112
+
113
+ ⚠️ A LIST, NOT A TABLE, WHICH IS WHAT THE MARKDOWN IT REPLACES HAD TO BE. A
114
+ screen reader announces "table, 2 columns, 7 rows" for what is a list of links
115
+ with descriptions, and asks the reader to navigate it by cell. Two columns of
116
+ sentence-length questions are also cramped in a narrow box, where the rows stack
117
+ instead — a `@container` query, because a host can hand this a 500px panel on a
118
+ 1920px monitor.
119
+
120
+ One frame with hairlines between the rows rather than a stack of cards: they are
121
+ the same question asked several ways, and separate boxes say several unrelated
122
+ things. The rule is a _top_ border on every row but the first — `:last-child`
123
+ leaves a doubled line the moment anything is appended to the list.
124
+
125
+ ## And the frame is a primitive, not this component's furniture
126
+
127
+ `.wave-docs-panel` is a framed block with a header and an inset surface — the
128
+ outer card names the thing and carries its controls, the inner one holds the
129
+ content. "Where to go next" is the first thing to wear it; a code frame is the
130
+ obvious next, and two copies of the same three rules is how they drift apart.
131
+
132
+ ⚠️ THE TWO RADII ARE NOT INDEPENDENT NUMBERS. A rounded box inside a rounded box
133
+ only looks right when the inner radius is the outer one minus the gap between
134
+ them; anything else runs the corners at different curvatures and the inner box
135
+ reads as _pasted onto_ the frame rather than set into it. The new
136
+ `--wave-docs-radius-lg` is chosen so the arithmetic lands on an existing token:
137
+ `1rem` outer minus `0.5rem` of padding is `--wave-docs-radius`.
138
+
139
+ The header sits in the frame's padding and draws no rule of its own — the inset
140
+ surface below already draws the line, and a border there is a second one a pixel
141
+ away from the first.
142
+
143
+ ⚠️ AND THE PANEL EXPORTS `--wave-docs-panel-inset`, WHICH IS NOT THE SAME NUMBER
144
+ AS ITS PADDING. The title sits at the frame's padding; anything inside the body
145
+ sits at that padding _plus the body's own border_, so the two columns miss each
146
+ other by a pixel per border. Measured before it existed: the rows started 9px
147
+ right of the heading above them — a number that appears in no rule and reads as
148
+ a design decision. Exported rather than repeated, because the next component to
149
+ wear the panel has to make the same subtraction and will not think to.
150
+
151
+ It renders above the pager. The two answer different questions and both belong
152
+ there: this is the semantic answer, the pager the linear one.
153
+
154
+ A real `<h2>` names it, not an `aria-label` — a reader moving by heading would
155
+ pass straight over a region named only by an attribute.
156
+
157
+ ## The key is `explore`, and neither `next` nor `steps` would do
158
+
159
+ `next` is unusable in this package. `src/next.ts` is the Next.js adapter, so
160
+ two files one directory apart would carry the same name for entirely different
161
+ things, and `doc.frontmatter.next` would read like a routing hook rather than
162
+ a block of prose.
163
+
164
+ `steps` is wrong for a different reason: these rows are a _branch_, not a
165
+ sequence. A reader picks one and ignores the rest, and none of them is first.
166
+ A numbered "1 -> 2 -> 3" component is a real and separate thing worth building
167
+ later, and `steps` is the name it will need.
168
+
169
+ The rendered heading is still "Where to go next" — `next` in prose is fine,
170
+ it is only the identifier that had to move.
171
+
172
+ No client JavaScript. New: `DocsExplore` at `@waveso/docs/react/explore`,
173
+ `explore` in frontmatter, and `explore` in `labels`.
174
+
175
+ - c871d2f: **One radius scale, taken from `@waveso/ui`, retunable from a single line.**
176
+
177
+ Three tiers, all `calc()` off one root, so which corner a box gets is decided by
178
+ what _kind_ of box it is rather than by how big it happens to be:
179
+
180
+ | Token | What takes it |
181
+ | ----------------------- | ----------------------------------------------------------- |
182
+ | `--wave-docs-radius-sm` | inline chips, small controls, focus rings on those |
183
+ | `--wave-docs-radius` | controls, overlays, and the panel's inset surface |
184
+ | `--wave-docs-radius-lg` | every block in the reading flow, and the panel's outer edge |
185
+
186
+ ⚠️ THE BLOCK TIER WAS SPLIT. Callouts, images and video embeds sat at the base
187
+ radius while a code frame and a table sat at 19px, so two blocks a paragraph
188
+ apart disagreed by eleven pixels — and a table read as aggressively round next
189
+ to the callout above it. 19 was measured off a reference site, which is a fine
190
+ way to pick a number and a bad way to pick a _system_.
191
+
192
+ ⚠️ AND THE NUMBERS ARE `@waveso/ui`'s, NOT NEW ONES. A page running both this
193
+ package and the component library should not show two radius scales a few pixels
194
+ apart, and taking theirs is how that is guaranteed rather than kept in step by
195
+ hand. `--wave-docs-radius-base` is the override point: a host writes
196
+ `--wave-docs-radius-base: var(--radius)` and every corner here follows their app,
197
+ including any theme that moves it. Overriding three tokens separately would be
198
+ three chances to break the arithmetic below.
199
+
200
+ ⚠️ `--wave-docs-radius-step` IS LOAD-BEARING. The panel's inset surface takes the
201
+ base radius and its frame takes `-lg`, which is the base plus one step — so the
202
+ two corners are concentric only while the frame's _padding_ is that same step.
203
+ It is paid out of the token rather than written as `4px`, so moving the root
204
+ keeps both true. `--wave-docs-radius-panel` is gone with it: a token whose only
205
+ job is to be another token minus a constant is a number that can drift from its
206
+ own definition.
207
+
208
+ ## Squircles, where the browser has them
209
+
210
+ `corner-shape` renders every `border-radius` as a continuous superellipse rather
211
+ than a circular arc. `@waveso/ui` ships it and this now matches, including the
212
+ root bump both make under the same `@supports` — a squircle reads tighter at the
213
+ same radius, so the scale moves up to restore the roundness the numbers were
214
+ chosen for. Only the root moves; every tier and the panel's padding follow.
215
+
216
+ ⚠️ SCOPED TO ELEMENTS THIS PACKAGE OWNS, AND NOT `*`. `@waveso/ui` can say `*`
217
+ because it is the application's own stylesheet. This one is mounted inside
218
+ somebody else's page, and a bare `*` would reshape every corner the host drew —
219
+ the same trespass as claiming `html` or `body`, which this file already refuses.
220
+ Pills and dots opt back out, because a squircled pill is a lozenge.
221
+
222
+ ⚠️ AND THE SOURCE TEST FOR THE CONCENTRIC ARITHMETIC WENT VACUOUS ON THE WAY.
223
+ It regexed three `rem` literals out of the token block; with the tiers as
224
+ `calc()` the regex matched nothing, both sides defaulted to zero, and `0 - 0`
225
+ passed while asserting nothing. It is measured in computed pixels now, which is
226
+ also the only tier that can see the squircle bump. Two browser assertions that
227
+ built an expected radius out of `getPropertyValue` were the same mistake in a
228
+ different shape — a custom property is not computed to pixels, so it hands back
229
+ the `calc()` as written. They compare two computed corners to each other now,
230
+ which is the uniformity claim anyway.
231
+
232
+ ### Patch Changes
233
+
234
+ - 04b7de7: **A blockquote is a box, not a rule down one edge.**
235
+
236
+ Every other block set apart from the prose here — a callout, a code frame, a
237
+ table, an embed — is a bordered box. A single 3px edge made the quote the one
238
+ exception, so next to a callout two paragraphs away it read as a _different kind
239
+ of thing_ rather than as a quieter one.
240
+
241
+ It is the callout's box now, minus the hue: the same padding so their text lines
242
+ up, the same block radius, a plain border instead of a tinted one, and no accent
243
+ edge. That is the relationship — **a callout is a quote with a colour** — and it
244
+ finally looks like it.
245
+
246
+ Still not italic. The box and the muted colour already say "quotation", a long
247
+ italic passage is measurably slower to read, and markdown authors use
248
+ blockquotes for asides and notes rather than only for speech.
249
+
250
+ ⚠️ AND IT HAD TO JOIN THE SQUIRCLE LIST BY NAME. Corner shaping is scoped to
251
+ elements this package owns, matched by our own class prefix — and a `blockquote`
252
+ is the markdown author's tag, with no class of ours on it. It would have been
253
+ the one block on the page still drawing a circular arc.
254
+
255
+ - 975acba: **A callout draws one uniform border, not a 3px rule down its inline start.**
256
+
257
+ The stripe made a callout the exception among blocks set apart from the prose —
258
+ a code frame, a table, an embed and now a blockquote are all boxes with one
259
+ border all the way round — so beside any of them it read as a different kind of
260
+ thing rather than as a coloured one. A thick rule on one side also fights the
261
+ corner it runs into once the box is a squircle.
262
+
263
+ ⚠️ THE TYPE IS STILL NOT CONVEYED BY COLOUR ALONE. The stripe was never what
264
+ carried it: the icon and the label — "Note", "Warning" — are the non-colour
265
+ signals, and they are unchanged. The tinted border and ground stay as
266
+ reinforcement rather than as the whole message.
267
+
268
+ The table-of-contents rail keeps its 2px inline-start border. That is a
269
+ continuous line an active marker slides along, which is a navigation affordance
270
+ rather than the edge of a box, and a source guard names the two blocks it does
271
+ apply to.
272
+
273
+ - cd21631: **The copy button draws Lucide icons, like everything else here.**
274
+
275
+ It rendered `⧉`, and swapped in `✓` and `×` through CSS `content` — three
276
+ characters drawn by whatever font resolved, at whatever weight and baseline
277
+ that font has, beside a sidebar, a pager, a callout and a search dialog that
278
+ are all Lucide paths at `stroke-width: 2`. It read as a different icon set
279
+ because it was one. Now `copy`, `check` and `x` on the same 24×24 grid.
280
+
281
+ ⚠️ THE SWAP IS `display` AND NOT `visibility`, AND THAT IS NOT A PREFERENCE.
282
+ All three icons ship in the markup and the stylesheet picks one — there is no
283
+ component owning this button, so there is no state to re-render. But the button
284
+ is `visibility: hidden` until the runtime attaches, which is what keeps a reader
285
+ with no JavaScript from meeting a control that does nothing and keeps it out of
286
+ the tab order — and `visibility` inherits. An icon rule setting it back to
287
+ `visible` would draw a glyph inside a button that is meant to be invisible.
288
+ `display` does not inherit, so the button's own rule still governs all three.
289
+
290
+ Three icons per fence is fifty on a page with fifty of them, and the repeat is
291
+ why that is affordable: byte-identical every time, which is the case gzip
292
+ handles best. The quick start's gzipped payload did not move, and the
293
+ hast-over-the-wire ratio for code and tables went from 1.11× to 1.09×.
294
+
295
+ ## And it stops drawing a box around itself
296
+
297
+ No border and no ground, in any state. A bordered, filled 2rem box sitting on
298
+ the frame's own band is a third framed rectangle inside a frame that already has
299
+ two, for a control secondary to everything around it. The glyph is the whole
300
+ control, and hover moves its ink to the accent rather than putting a box behind
301
+ it — the same signal every other interactive surface here gives. A ground also
302
+ had nowhere to come from: the subtle ramp _is_ the frame's colour, so the hover
303
+ state it used to have was the colour the button was already sitting on.
304
+
305
+ ⚠️ AND IT HAD NO FOCUS INDICATOR AT ALL, WHICH THE BORDER WAS COVERING FOR. A
306
+ 1px box is not a focus indicator — it is there whether the control is focused or
307
+ not — so a keyboard reader tabbing onto this button got a `color` change and
308
+ nothing else, and it never appeared in the package's own inventory of focusable
309
+ surfaces. Taking the border away made that visible; it did not create it. There
310
+ is a real `:focus-visible` outline now, and a test that keeps it.
311
+
312
+ ⚠️ AND ONE TEST WAS PASSING BY ACCIDENT. `render.test.ts` asserted that a
313
+ GitHub alert produces no octicons by checking the _whole document_ for `<svg>`,
314
+ which was only ever true because nothing else in the pipeline emitted one. It
315
+ now checks the callout's own subtree, which is what it meant.
316
+
317
+ - d7333cb: **The site's pages stop hand-writing their own "Next" link.** Nineteen of them
318
+ ended with `Next: [Page](./page.md).`, which the pager has rendered from the
319
+ navigation tree since it landed — the same link, twice, a few hundred pixels
320
+ apart, and the hand-written one silently wrong the moment a page moved in the
321
+ tree.
322
+
323
+ Documentation-only: no package change.
324
+
325
+ - 6f0afb7: **A panel title is the size of a column header.**
326
+
327
+ "Where to go next" and `Enforced by` are the same kind of thing — the label a
328
+ reader's eye lands on before the content under it — and a page shows both. At
329
+ `0.875rem` against the table's `0.9375rem` they read as two levels rather than
330
+ as one.
331
+
332
+ The weight does not follow: a panel title names the block, a column header names
333
+ a column inside one, so the title stays a step heavier. A source guard pins the
334
+ pair, because two literals that have to agree are two literals that drift.
335
+
336
+ - 8c59155: The navigation stops being lopsided.
337
+
338
+ Its rows sat 16px from the inline start and 31px from the end. The 15px was
339
+ `scrollbar-gutter: stable`, which reserves the scrollbar's width whether or not
340
+ it is showing — and that reservation sits _inside_ the padding.
341
+
342
+ The jump it prevents is one 15px shift, the first time a nav grows past a
343
+ screen. What it cost was permanent and on every page.
344
+
345
+ ⚠️ AND IT IS A NO-OP ON macOS, WHICH IS WHY IT SURVIVED BEING LOOKED AT.
346
+ `scrollbar-gutter` does nothing where overlay scrollbars are the default, so the
347
+ asymmetry was invisible to everyone who built this and plain on every Windows
348
+ and Linux machine that opened it.
349
+
350
+ `scrollbar-width: thin` in its place, with `scrollbar-color`, so the scrollbar
351
+ that does appear is narrow and coloured rather than a UA slab against the
352
+ panel's edge. A browser test measures both insets and fails at 15px.
353
+
354
+ - e94bb67: **A table keeps one frame, at the panel's radius.**
355
+
356
+ It wore `.wave-docs-panel` for a while — outer frame, header band, inset card —
357
+ and that was wrong for a reason worth writing down rather than just undoing.
358
+
359
+ ⚠️ THE PANEL SEPARATES _CHROME_ FROM _CONTENT_, AND A TABLE'S HEADER ROW IS
360
+ CONTENT. "Where to go next" and a code frame both have chrome to put in the
361
+ band: a title, a language, a copy button. A table's `<thead>` is data. Setting
362
+ the body into a card away from its own header cost three vertical rules down
363
+ each side, stopped the row dividers short of the box and narrowed the reading
364
+ width — on the densest element on a page, for nothing gained. Full-width
365
+ dividers are what let an eye track a row across.
366
+
367
+ What is kept is the outer radius: `--wave-docs-radius-lg` rather than
368
+ `--wave-docs-radius`, so a table and a code block read as two of one family
369
+ without the table pretending to chrome it has not got.
370
+
371
+ ⚠️ AND AN EMPTY HEADER ROW NOW DRAWS NO BAND, WHICH GFM PRODUCES ROUTINELY. A
372
+ GFM table always has a `<thead>` — the delimiter row is what makes it a table —
373
+ so an author who wants a plain two-column list of facts writes `| | |` and gets
374
+ a header of empty `<th>`s, which rendered as a tinted strip with nothing in it.
375
+ `:empty` and not a text check: GFM emits `<th></th>` with no whitespace inside.
376
+ A header with even one named column keeps its band.
377
+
378
+ **And a fence with neither a title nor a language gets no band either.** The
379
+ header row is floored so titled and untitled fences match, which is right when
380
+ there is a label and wrong when there is not: a bare fence had only the copy
381
+ button, adrift in a strip of empty ground. The row is the button now.
382
+
383
+ ## 0.10.0
384
+
385
+ ### Minor Changes
386
+
387
+ - f52cb99: **Every page links to the ones either side of it.** `docs.Page` renders a pager
388
+ under the prose. Nothing is authored: a page gets one by being in the
389
+ navigation.
390
+
391
+ ⚠️ THE ORDER IS THE SIDEBAR'S, NOT THE SLUG LIST'S. `generateStaticParams` has
392
+ every route in it and no opinion about their order; `meta.json` is where the
393
+ author said what comes next, and it is what the reader is looking at. Two
394
+ orderings of the same pages is two answers to one question, and they drift the
395
+ first time a `meta.json` moves — so the pager reads the same tree `DocsSidebar`
396
+ renders and flattens it. A pager that disagrees with the column beside it is
397
+ impossible by construction.
398
+
399
+ Separators and external links are not stops: a separator is a label with
400
+ nowhere to go, and a "next page" that lands on npm has ended the sequence
401
+ rather than continued it. A group with an `index.md` contributes its own page
402
+ before its children, which is the order its rows appear in.
403
+
404
+ ⚠️ A PAGE OUTSIDE THE TREE GETS NO PAGER, RATHER THAN THE FIRST ONE. `-1` from
405
+ `findIndex` reads as "just before the beginning", so an unguarded lookup hands
406
+ every draft and every route rendered outside the navigation the same first page
407
+ as its "next" — confidently wrong on exactly the pages nobody checks.
408
+
409
+ ⚠️ AND AN EMPTY CELL AT EACH END, NOT A MISSING ONE. The two links share a grid
410
+ row; drop the absent side and the survivor slides into the first track, so the
411
+ first page of a site puts "Next" on the left and every other page puts it on
412
+ the right. The one page where the position moves is the one a reader sees
413
+ first.
414
+
415
+ Nothing renders at all when there is no neighbour either side — a one-page site
416
+ would otherwise get a navigation landmark containing nothing.
417
+
418
+ A chevron on the outer edge of each link points the way it goes.
419
+
420
+ ⚠️ AND "OUTWARD" MIRRORS. Under `dir="rtl"` the grid's first track is on the
421
+ right, so the _previous_ link moves there and its arrow has to point right —
422
+ the reverse of the rule that draws it. Same trap as the sidebar's chevron, in a
423
+ component built after it, and `[dir='rtl']` again rather than `:dir(rtl)`.
424
+
425
+ No client JavaScript: two links, two captions and two glyphs, rendered on the
426
+ server. Each
427
+ link is named by direction _and_ destination — "Previous: Installation" — since
428
+ a link announced as a bare title says nothing about which way it goes. The
429
+ landmark is named too, because a page now carries three of them.
430
+
431
+ New: `DocsPager` at `@waveso/docs/react/pager`, `pager: false` on
432
+ `createDocsRoute` to omit it, and `previousPage`, `nextPage` and `pagination`
433
+ in `labels`.
434
+
435
+ ### Patch Changes
436
+
437
+ - 09a9369: The sidebar grip says whether it has anything to do.
438
+
439
+ Blue when the navigation is hidden or the pointer is on it; grey when the
440
+ sidebar is open and untouched. One rule at every width — a closed sidebar is
441
+ the same request for attention on a phone as on a desktop.
442
+
443
+ ⚠️ `:not([data-state='open'])`, NOT `[data-state='closed']`. The attribute is
444
+ absent until the reader chooses, and a server-rendered page has none — so
445
+ matching only the explicit value leaves every first paint below 64rem showing a
446
+ grey grip in front of hidden navigation, which is the one moment the cue is
447
+ for. Above 64rem the default inverts, and a second rule says so.
448
+
449
+ ⚠️ AND THE COLOUR IS A CUSTOM PROPERTY, NOT A SELECTOR FIGHT. Three things want
450
+ to set it — the resting style, the pointer, and the sidebar's state — and the
451
+ state lives on an _ancestor_, so `.sidebar[data-state] .trigger::before`
452
+ outranks `.trigger:hover::before` by a whole class. Written as backgrounds that
453
+ is a rule which silently kills hover on the one state that still needs it.
454
+ Properties settle it by inheritance: the state sets them on the sidebar, the
455
+ trigger sets them on itself under `:hover`, and a value on the element always
456
+ beats one it inherited. No specificity ladder, no `!important`.
457
+
458
+ The grip is also a full pill now rather than a rounded rectangle.
459
+
460
+ ⚠️ TESTED AS MARKUP, BECAUSE A MOUNTED COMPONENT CANNOT SHOW THE UNTOUCHED
461
+ STATE. `DocsNav` resolves the mode on mount and writes `data-state`
462
+ immediately, so every React fixture is already explicit. Two wrong versions
463
+ passed the whole suite against that — matching the explicit value, and dropping
464
+ the wide-layout reset — and both are caught now by a server-shaped fixture with
465
+ no attribute at all.
466
+
467
+ Also fixed here: the focus-indicator guard looked up one rule per selector, and
468
+ a selector may legitimately appear in several. The trigger now has one rule
469
+ setting properties on focus and another drawing the ring; taking "the first" or
470
+ "the last" is a coin flip on file order, so it reads every rule and asks for
471
+ one to declare an outline.
472
+
473
+ ## 0.9.2
474
+
475
+ ### Patch Changes
476
+
477
+ - 6b84354: A separator now rules off the block above it.
478
+
479
+ It ends one section as much as it names the next, and 1rem of margin was not
480
+ saying so — the gap read as "these two lists are a bit far apart" rather than as
481
+ a division.
482
+
483
+ ⚠️ THE RULE AND THE LABEL SHARE THE LIST'S OWN EDGE, WHICH IS ALSO THE ROWS'. A
484
+ row is full-bleed — its hover surface spans the whole column, and so does the
485
+ search field above it — so a rule on that edge divides the column, while an
486
+ inset one floats inside it. The label sits on the same line, because a heading
487
+ and the rule above it reading as one object is the whole reason the rule exists.
488
+
489
+ The label keeps the rows' own content edge — the marker column when there is
490
+ one, the words when `icons={false}` removes it. Those are the same number: a
491
+ row's `padding-inline` is what both modes have in common, so matching it lands
492
+ on whichever is there, with no query and nothing threaded to the stylesheet.
493
+
494
+ ⚠️ AND NOT ABOVE THE FIRST CHILD. A `meta.json` may open with
495
+ `"---Reference---"`, and on that tree the very first thing in the navigation
496
+ would otherwise be a hairline above nothing.
497
+
498
+ The label also drops from `font-weight: 650` to `500`. At 650 it was heavier
499
+ than the group titles it sits under — a divider out-shouting the navigation it
500
+ divides. Weight rather than colour, because there is no lighter colour to
501
+ reach for: `--wave-docs-fg-subtle` is already the lightest text token at 5.05:1
502
+ against WCAG 1.4.3's 4.5:1 floor, and `--wave-docs-border` — the rule's own
503
+ colour — measures 1.31:1 and is a line colour, not a text one.
504
+
505
+ - 7e67c5a: The table of contents marks the last section when you reach it.
506
+
507
+ Scroll to the foot of a page whose final section is short and nothing happened:
508
+ the entry stayed on the section _above_, and the last one could only be
509
+ highlighted by clicking its own link.
510
+
511
+ ⚠️ NO `rootMargin` FIXES THIS, WHICH IS WHY IT LOOKED LIKE A TUNING PROBLEM.
512
+ The default makes the top 40% of the viewport the region that counts as
513
+ current, and that is right while there is document left to scroll — a heading
514
+ rises into the band and takes the highlight. At the end there is none. A short
515
+ trailing section sits on screen, fully readable, below a band it can never
516
+ enter, while the heading above it is still _inside_ that band. The observer was
517
+ giving a correct answer to the wrong question. Any band smaller than the
518
+ viewport has this hole; a bigger one only trades it for a highlight that jumps
519
+ early.
520
+
521
+ So the end of the document is handled as what it is — a place where scrolling
522
+ stops answering — and the last heading takes the highlight there. Scroll up and
523
+ the band has it straight back, without waiting for a heading to cross.
524
+
525
+ ⚠️ AND BOTH INPUTS GO THROUGH ONE RESOLVER, WHICH IS THE HALF THAT IS EASY TO
526
+ MISS. Left as two `setActiveId` calls they race, and the observer wins — it
527
+ fires last and it still likes the heading above. The first version of this fix
528
+ was measured doing exactly nothing for that reason.
529
+
530
+ ⚠️ ONLY WHEN THE DOCUMENT ACTUALLY SCROLLS. On a page that fits, "scrolled to
531
+ the bottom" is true at rest, and the last section would be current before the
532
+ reader had read a word of the first.
533
+
534
+ The listener is `passive` and reads two numbers — no `getBoundingClientRect`,
535
+ no layout flush. Like every other scroll reader here it watches the document; a
536
+ host that scrolls an inner pane keeps the observer's behaviour and loses only
537
+ this tail case.
538
+
539
+ `toc` grows 0.88 → 1 KB, and the published total 14.5 → 14.6 KB. It is still
540
+ the smallest client component in the package.
541
+
3
542
  ## 0.9.1
4
543
 
5
544
  ### Patch Changes