@waveso/docs 0.10.0 → 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,385 @@
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
+
3
383
  ## 0.10.0
4
384
 
5
385
  ### Minor Changes
package/README.md CHANGED
@@ -203,6 +203,7 @@ Every component takes data as props, and every module that imports from `next/*`
203
203
  | `DocContent` | `react/doc-content` | Renders a hast tree, inside `.wave-docs-prose`. Server Component |
204
204
  | `DocsHero` | `react/hero` | A landing page's header. `title`, `description`, `actions`, `Link`, `externalLabel`. Server Component |
205
205
  | `DocsSidebar` | `react/sidebar` | Takes `pathname` as a prop, not from `next/navigation`. `icons` controls the marker column — see [Sidebar icons](#sidebar-icons) |
206
+ | `DocsExplore` | `react/explore` | "Where to go next": a question per row and the page that answers it. `docs.Page` renders it when a page declares `explore` |
206
207
  | `DocsPager` | `react/pager` | Links to the pages either side of this one. `docs.Page` renders it; `pager: false` on the route omits it |
207
208
  | `DocsToc` | `react/toc` | Scrollspy via `IntersectionObserver`. `label`, `topLabel`, `rootMargin`, `className` |
208
209
  | `DocsSearch` | `react/next-search` | `SearchDialog`, wired to Next's router. What you want |
@@ -215,6 +216,40 @@ Every component takes data as props, and every module that imports from `next/*`
215
216
 
216
217
  `DocsToc`'s `rootMargin` is the `IntersectionObserver` margin that decides how far above the viewport a heading counts as current; the default keeps the highlight on the section you are reading rather than the one about to arrive. `topLabel` is the back-to-top link at the end — it fades in once the reader is about a third of a screen down and fades out again on the way back, on a scroll timeline rather than a scroll listener, so the component ships no extra bytes to do it. Where that timeline cannot run — an engine without scroll-driven animations, a page too short to scroll, or a host that scrolls an inner pane rather than the document — the link is simply always there.
217
218
 
219
+ ### Where to go next
220
+
221
+ A sidebar is a structure; this is a router. It says *why* a reader would go somewhere, which no tree of titles can. A page opts in from its frontmatter:
222
+
223
+ ```yaml
224
+ ---
225
+ title: How it fits together
226
+ explore:
227
+ - question: How a person is recognised across servers
228
+ href: ./identity.md
229
+ - question: What happens when the network fails
230
+ href: ./delivery.md
231
+ - question: Where the source lives
232
+ href: https://github.com/example/repo
233
+ title: GitHub
234
+ ---
235
+ ```
236
+
237
+ | Field | Type | What it is |
238
+ | --- | --- | --- |
239
+ | `question` | `string` | What the reader might want to know. The row's left half |
240
+ | `href` | `string` | Where the answer is. Checked against the same allowlist as every other link |
241
+ | `title` | `string` | The link's text. Defaults to the destination's title **in the navigation** |
242
+
243
+ **The link text comes from the navigation**, so renaming a page updates every block pointing at it — the same source the sidebar and the pager read. Name a `title` only where the tree cannot answer: an external link, or a page kept out of the navigation. An `href` that resolves to neither stops the build and says which of the two fixes to reach for, rather than rendering a URL where a sentence should be.
244
+
245
+ It renders above the pager: this is the semantic answer, the pager is the linear one.
246
+
247
+ It wears **the panel** — a framed block with a header and an inset surface, styled by `.wave-docs-panel`, `.wave-docs-panel__header`, `.wave-docs-panel__title`, `.wave-docs-panel__actions` and `.wave-docs-panel__body`. That is a shared primitive rather than this component's furniture, so a code frame can wear it next without a second copy of the same rules. The two radii are not independent: the inner one is the outer minus the panel's padding, or the corners run at different curvatures and the surface reads as pasted onto the frame instead of set into it. `--wave-docs-radius-lg` is picked so the arithmetic lands on `--wave-docs-radius`.
248
+
249
+ The panel also exports `--wave-docs-panel-inset` for anything placed inside `__body`: the header's title sits at the frame's padding, but body content sits at that padding *plus the body's own border*, so the two columns miss each other by a pixel per border unless the inset is used.
250
+
251
+ `DocsExplore` takes `steps` (each with `question`, `href` and a resolved `title`), plus `heading`, `Link`, `externalLabel` and `className`. The heading defaults to `'Where to go next'` and is `explore` in `labels`. Under a 40rem container the question and its answer stack instead of sharing a row.
252
+
218
253
  ### The pager
219
254
 
220
255
  `docs.Page` renders it under every page, so most sites never touch it. The order is the navigation's — flattened from the same tree `DocsSidebar` renders — so a pager that disagrees with the sidebar beside it is impossible. Nothing is authored: a page gets one by being in the tree, and a page outside it (a draft, or a route you render yourself) gets none.
@@ -686,6 +721,48 @@ your own `:root`, after the import:
686
721
  }
687
722
  ```
688
723
 
724
+ ### Corners
725
+
726
+ Three tiers, all derived from one root, so a box's radius is decided by what
727
+ *kind* of box it is rather than by how big it happens to be.
728
+
729
+ | Token | What takes it |
730
+ | --- | --- |
731
+ | `--wave-docs-radius-sm` | Inline chips, small controls, and focus rings drawn on those |
732
+ | `--wave-docs-radius` | Controls, overlays, and the panel's inset surface |
733
+ | `--wave-docs-radius-lg` | Every block in the reading flow, and the panel's outer edge |
734
+
735
+ **Retune all three from one line.** `--wave-docs-radius-base` is the root and
736
+ the other three are `calc()` off it, so a host already running `@waveso/ui`
737
+ points this package at their scale and every corner follows — including any
738
+ theme that moves it:
739
+
740
+ ```css
741
+ :root {
742
+ --wave-docs-radius-base: var(--radius);
743
+ }
744
+ ```
745
+
746
+ The numbers are `@waveso/ui`'s to begin with. A page running both should not
747
+ show two radius scales a few pixels apart, and taking theirs is how that is
748
+ guaranteed rather than kept in step by hand.
749
+
750
+ ⚠️ **`--wave-docs-radius-step` is not decoration.** The panel's inset surface
751
+ takes the base radius and its frame takes `--wave-docs-radius-lg`, which is the
752
+ base plus one step — so the two corners are concentric only while the frame's
753
+ *padding* is that same step, which is why it is paid out of the token rather
754
+ than written as `4px`. Move the root and both stay true; hard-code the padding
755
+ and they drift the first time anyone retunes the scale.
756
+
757
+ Where the browser supports `corner-shape`, every corner this package draws
758
+ becomes a squircle and the root moves up, because a squircle reads tighter than
759
+ a circular arc at the same radius. `@waveso/ui` makes the same move to the same
760
+ value. Elsewhere it is an ordinary rounded corner at the original scale — an
761
+ enhancement, never a dependency. The shaping is scoped to elements this package
762
+ owns rather than applied with `*`: this stylesheet is mounted inside somebody
763
+ else's page, and reshaping the host's corners is the same trespass as claiming
764
+ `html`.
765
+
689
766
  That works because **everything this stylesheet declares lives in a `@layer`** —
690
767
  `theme` for the tokens, `base` for element resets, `components` for the classes
691
768
  — and unlayered CSS outranks every layer regardless of specificity.
@@ -16,6 +16,33 @@ declare const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
16
16
  * tab stop where a control should be.
17
17
  */
18
18
  declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
19
+ /**
20
+ * The copy button's three icons, as Lucide paths.
21
+ *
22
+ * ⚠️ PATHS FROM THE SAME SET AS EVERY OTHER ICON HERE, AND THEY WERE FONT
23
+ * GLYPHS. The button rendered `⧉` and swapped in `✓` and `×` through CSS
24
+ * `content` — three characters drawn by whatever font resolved, at whatever
25
+ * weight and baseline that font has, beside a sidebar, a pager and a search
26
+ * dialog that are all Lucide at `stroke-width: 2`. It read as a different
27
+ * icon set because it was one.
28
+ *
29
+ * `copy`, `check` and `x`, on the 24×24 grid the rest of the package uses.
30
+ * Lucide draws its `copy` as a `<rect>` plus a `<path>`; the rect is written
31
+ * here as the path it is, so a frame's icons are one shape of node and the
32
+ * builder stays a list of `d` strings — the same shape `NAV_ICON_PATHS` has.
33
+ */
34
+ declare const CODE_ICON_PATHS: Record<'copy' | 'check' | 'x', string[]>;
35
+ /**
36
+ * The three states the button draws, in the order they are emitted.
37
+ *
38
+ * `idle` is what a reader sees; the runtime writes `data-copied` on the button
39
+ * and the stylesheet swaps which of the three is displayed. `display`, not
40
+ * `visibility`: the button is `visibility: hidden` until the runtime attaches,
41
+ * and `visibility` inherits — a child setting it back to `visible` would show
42
+ * an icon inside a button that is meant to be invisible and out of the tab
43
+ * order.
44
+ */
45
+ declare const CODE_ICON_STATES: readonly [readonly ["idle", "copy"], readonly ["copied", "check"], readonly ["failed", "x"]];
19
46
  /**
20
47
  * Whether a tree contains a code frame.
21
48
  *
@@ -26,4 +53,4 @@ declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
26
53
  */
27
54
  declare function hasCodeFrame(tree: Root): boolean;
28
55
  //#endregion
29
- export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
56
+ export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES, CODE_READY_ATTRIBUTE, hasCodeFrame };
@@ -16,6 +16,41 @@ const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
16
16
  */
17
17
  const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
18
18
  /**
19
+ * The copy button's three icons, as Lucide paths.
20
+ *
21
+ * ⚠️ PATHS FROM THE SAME SET AS EVERY OTHER ICON HERE, AND THEY WERE FONT
22
+ * GLYPHS. The button rendered `⧉` and swapped in `✓` and `×` through CSS
23
+ * `content` — three characters drawn by whatever font resolved, at whatever
24
+ * weight and baseline that font has, beside a sidebar, a pager and a search
25
+ * dialog that are all Lucide at `stroke-width: 2`. It read as a different
26
+ * icon set because it was one.
27
+ *
28
+ * `copy`, `check` and `x`, on the 24×24 grid the rest of the package uses.
29
+ * Lucide draws its `copy` as a `<rect>` plus a `<path>`; the rect is written
30
+ * here as the path it is, so a frame's icons are one shape of node and the
31
+ * builder stays a list of `d` strings — the same shape `NAV_ICON_PATHS` has.
32
+ */
33
+ const CODE_ICON_PATHS = {
34
+ copy: ["M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2z", "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"],
35
+ check: ["M20 6 9 17l-5-5"],
36
+ x: ["M18 6 6 18", "m6 6 12 12"]
37
+ };
38
+ /**
39
+ * The three states the button draws, in the order they are emitted.
40
+ *
41
+ * `idle` is what a reader sees; the runtime writes `data-copied` on the button
42
+ * and the stylesheet swaps which of the three is displayed. `display`, not
43
+ * `visibility`: the button is `visibility: hidden` until the runtime attaches,
44
+ * and `visibility` inherits — a child setting it back to `visible` would show
45
+ * an icon inside a button that is meant to be invisible and out of the tab
46
+ * order.
47
+ */
48
+ const CODE_ICON_STATES = [
49
+ ["idle", "copy"],
50
+ ["copied", "check"],
51
+ ["failed", "x"]
52
+ ];
53
+ /**
19
54
  * Whether a tree contains a code frame.
20
55
  *
21
56
  * `DocContent` asks before rendering the runtime, so a page with no fences
@@ -38,4 +73,4 @@ function isCodeFrame(node) {
38
73
  return node.properties[CODE_FRAME_ATTRIBUTE] !== void 0;
39
74
  }
40
75
  //#endregion
41
- export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
76
+ export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES, CODE_READY_ATTRIBUTE, hasCodeFrame };
@@ -33,6 +33,11 @@ declare const docFrontmatterSchema: z.ZodObject<{
33
33
  aliases: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
34
34
  order: z.ZodExactOptional<z.ZodNumber>;
35
35
  icon: z.ZodExactOptional<z.ZodString>;
36
+ explore: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
37
+ question: z.ZodString;
38
+ href: z.ZodString;
39
+ title: z.ZodExactOptional<z.ZodString>;
40
+ }, z.core.$strip>>>;
36
41
  actions: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
37
42
  label: z.ZodString;
38
43
  href: z.ZodString;
@@ -33,6 +33,11 @@ const docFrontmatterSchema = z.object({
33
33
  aliases: z.array(z.string()).exactOptional(),
34
34
  order: z.number().exactOptional(),
35
35
  icon: z.string().min(1).exactOptional(),
36
+ explore: z.array(z.object({
37
+ question: z.string().min(1),
38
+ href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
39
+ title: z.string().min(1).exactOptional()
40
+ })).exactOptional(),
36
41
  actions: z.array(z.object({
37
42
  label: z.string().min(1),
38
43
  href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
@@ -26,5 +26,18 @@ declare function neighbours(nodes: DocNavNode[], href: string): {
26
26
  previous?: NavStop;
27
27
  next?: NavStop;
28
28
  };
29
+ /**
30
+ * A step's link text: the one it named, or the navigation's name for the page.
31
+ *
32
+ * ⚠️ A THROW, NOT A FALLBACK TO THE HREF. A row reading "Where this runs and
33
+ * what that buys → /docs/infrastructure" is a URL where a sentence should be,
34
+ * and it renders perfectly — nothing else in the pipeline would notice. The
35
+ * frontmatter is authored and the author is right there, so the build stops and
36
+ * says which page and which of the two fixes to reach for.
37
+ */
38
+ declare function stepTitle(stops: NavStop[], step: {
39
+ href: string;
40
+ title?: string | undefined;
41
+ }): string | undefined;
29
42
  //#endregion
30
- export { NavStop, neighbours, readingOrder };
43
+ export { NavStop, neighbours, readingOrder, stepTitle };
package/dist/nav-order.js CHANGED
@@ -54,5 +54,19 @@ function neighbours(nodes, href) {
54
54
  ...next === void 0 ? {} : { next }
55
55
  };
56
56
  }
57
+ /**
58
+ * A step's link text: the one it named, or the navigation's name for the page.
59
+ *
60
+ * ⚠️ A THROW, NOT A FALLBACK TO THE HREF. A row reading "Where this runs and
61
+ * what that buys → /docs/infrastructure" is a URL where a sentence should be,
62
+ * and it renders perfectly — nothing else in the pipeline would notice. The
63
+ * frontmatter is authored and the author is right there, so the build stops and
64
+ * says which page and which of the two fixes to reach for.
65
+ */
66
+ function stepTitle(stops, step) {
67
+ if (step.title !== void 0) return step.title;
68
+ const here = normalize(step.href);
69
+ return stops.find((stop) => normalize(stop.href) === here)?.title;
70
+ }
57
71
  //#endregion
58
- export { neighbours, readingOrder };
72
+ export { neighbours, readingOrder, stepTitle };