@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 +380 -0
- package/README.md +77 -0
- package/dist/code-frame.d.ts +28 -1
- package/dist/code-frame.js +36 -1
- package/dist/frontmatter.d.ts +5 -0
- package/dist/frontmatter.js +5 -0
- package/dist/nav-order.d.ts +14 -1
- package/dist/nav-order.js +15 -1
- package/dist/next.js +26 -1
- package/dist/plugins/rehype-code-frame.js +41 -11
- package/dist/react/explore.d.ts +53 -0
- package/dist/react/explore.js +86 -0
- package/dist/react/markdown-components.js +14 -0
- package/dist/react/shell-labels.d.ts +8 -1
- package/dist/react/shell-labels.js +1 -0
- package/dist/styles.css +688 -130
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
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.
|
package/dist/code-frame.d.ts
CHANGED
|
@@ -16,6 +16,33 @@ declare const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
|
|
|
16
16
|
* tab stop where a control should be.
|
|
17
17
|
*/
|
|
18
18
|
declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
19
|
+
/**
|
|
20
|
+
* The copy button's three icons, as Lucide paths.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ PATHS FROM THE SAME SET AS EVERY OTHER ICON HERE, AND THEY WERE FONT
|
|
23
|
+
* GLYPHS. The button rendered `⧉` and swapped in `✓` and `×` through CSS
|
|
24
|
+
* `content` — three characters drawn by whatever font resolved, at whatever
|
|
25
|
+
* weight and baseline that font has, beside a sidebar, a pager and a search
|
|
26
|
+
* dialog that are all Lucide at `stroke-width: 2`. It read as a different
|
|
27
|
+
* icon set because it was one.
|
|
28
|
+
*
|
|
29
|
+
* `copy`, `check` and `x`, on the 24×24 grid the rest of the package uses.
|
|
30
|
+
* Lucide draws its `copy` as a `<rect>` plus a `<path>`; the rect is written
|
|
31
|
+
* here as the path it is, so a frame's icons are one shape of node and the
|
|
32
|
+
* builder stays a list of `d` strings — the same shape `NAV_ICON_PATHS` has.
|
|
33
|
+
*/
|
|
34
|
+
declare const CODE_ICON_PATHS: Record<'copy' | 'check' | 'x', string[]>;
|
|
35
|
+
/**
|
|
36
|
+
* The three states the button draws, in the order they are emitted.
|
|
37
|
+
*
|
|
38
|
+
* `idle` is what a reader sees; the runtime writes `data-copied` on the button
|
|
39
|
+
* and the stylesheet swaps which of the three is displayed. `display`, not
|
|
40
|
+
* `visibility`: the button is `visibility: hidden` until the runtime attaches,
|
|
41
|
+
* and `visibility` inherits — a child setting it back to `visible` would show
|
|
42
|
+
* an icon inside a button that is meant to be invisible and out of the tab
|
|
43
|
+
* order.
|
|
44
|
+
*/
|
|
45
|
+
declare const CODE_ICON_STATES: readonly [readonly ["idle", "copy"], readonly ["copied", "check"], readonly ["failed", "x"]];
|
|
19
46
|
/**
|
|
20
47
|
* Whether a tree contains a code frame.
|
|
21
48
|
*
|
|
@@ -26,4 +53,4 @@ declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
|
26
53
|
*/
|
|
27
54
|
declare function hasCodeFrame(tree: Root): boolean;
|
|
28
55
|
//#endregion
|
|
29
|
-
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
|
56
|
+
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
package/dist/code-frame.js
CHANGED
|
@@ -16,6 +16,41 @@ const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
|
|
|
16
16
|
*/
|
|
17
17
|
const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
18
18
|
/**
|
|
19
|
+
* The copy button's three icons, as Lucide paths.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ PATHS FROM THE SAME SET AS EVERY OTHER ICON HERE, AND THEY WERE FONT
|
|
22
|
+
* GLYPHS. The button rendered `⧉` and swapped in `✓` and `×` through CSS
|
|
23
|
+
* `content` — three characters drawn by whatever font resolved, at whatever
|
|
24
|
+
* weight and baseline that font has, beside a sidebar, a pager and a search
|
|
25
|
+
* dialog that are all Lucide at `stroke-width: 2`. It read as a different
|
|
26
|
+
* icon set because it was one.
|
|
27
|
+
*
|
|
28
|
+
* `copy`, `check` and `x`, on the 24×24 grid the rest of the package uses.
|
|
29
|
+
* Lucide draws its `copy` as a `<rect>` plus a `<path>`; the rect is written
|
|
30
|
+
* here as the path it is, so a frame's icons are one shape of node and the
|
|
31
|
+
* builder stays a list of `d` strings — the same shape `NAV_ICON_PATHS` has.
|
|
32
|
+
*/
|
|
33
|
+
const CODE_ICON_PATHS = {
|
|
34
|
+
copy: ["M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2z", "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"],
|
|
35
|
+
check: ["M20 6 9 17l-5-5"],
|
|
36
|
+
x: ["M18 6 6 18", "m6 6 12 12"]
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The three states the button draws, in the order they are emitted.
|
|
40
|
+
*
|
|
41
|
+
* `idle` is what a reader sees; the runtime writes `data-copied` on the button
|
|
42
|
+
* and the stylesheet swaps which of the three is displayed. `display`, not
|
|
43
|
+
* `visibility`: the button is `visibility: hidden` until the runtime attaches,
|
|
44
|
+
* and `visibility` inherits — a child setting it back to `visible` would show
|
|
45
|
+
* an icon inside a button that is meant to be invisible and out of the tab
|
|
46
|
+
* order.
|
|
47
|
+
*/
|
|
48
|
+
const CODE_ICON_STATES = [
|
|
49
|
+
["idle", "copy"],
|
|
50
|
+
["copied", "check"],
|
|
51
|
+
["failed", "x"]
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
19
54
|
* Whether a tree contains a code frame.
|
|
20
55
|
*
|
|
21
56
|
* `DocContent` asks before rendering the runtime, so a page with no fences
|
|
@@ -38,4 +73,4 @@ function isCodeFrame(node) {
|
|
|
38
73
|
return node.properties[CODE_FRAME_ATTRIBUTE] !== void 0;
|
|
39
74
|
}
|
|
40
75
|
//#endregion
|
|
41
|
-
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
|
76
|
+
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_ICON_PATHS, CODE_ICON_STATES, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
package/dist/frontmatter.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
33
33
|
aliases: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
|
|
34
34
|
order: z.ZodExactOptional<z.ZodNumber>;
|
|
35
35
|
icon: z.ZodExactOptional<z.ZodString>;
|
|
36
|
+
explore: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
|
|
37
|
+
question: z.ZodString;
|
|
38
|
+
href: z.ZodString;
|
|
39
|
+
title: z.ZodExactOptional<z.ZodString>;
|
|
40
|
+
}, z.core.$strip>>>;
|
|
36
41
|
actions: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
|
|
37
42
|
label: z.ZodString;
|
|
38
43
|
href: z.ZodString;
|
package/dist/frontmatter.js
CHANGED
|
@@ -33,6 +33,11 @@ const docFrontmatterSchema = z.object({
|
|
|
33
33
|
aliases: z.array(z.string()).exactOptional(),
|
|
34
34
|
order: z.number().exactOptional(),
|
|
35
35
|
icon: z.string().min(1).exactOptional(),
|
|
36
|
+
explore: z.array(z.object({
|
|
37
|
+
question: z.string().min(1),
|
|
38
|
+
href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
|
|
39
|
+
title: z.string().min(1).exactOptional()
|
|
40
|
+
})).exactOptional(),
|
|
36
41
|
actions: z.array(z.object({
|
|
37
42
|
label: z.string().min(1),
|
|
38
43
|
href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
|
package/dist/nav-order.d.ts
CHANGED
|
@@ -26,5 +26,18 @@ declare function neighbours(nodes: DocNavNode[], href: string): {
|
|
|
26
26
|
previous?: NavStop;
|
|
27
27
|
next?: NavStop;
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* A step's link text: the one it named, or the navigation's name for the page.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ A THROW, NOT A FALLBACK TO THE HREF. A row reading "Where this runs and
|
|
33
|
+
* what that buys → /docs/infrastructure" is a URL where a sentence should be,
|
|
34
|
+
* and it renders perfectly — nothing else in the pipeline would notice. The
|
|
35
|
+
* frontmatter is authored and the author is right there, so the build stops and
|
|
36
|
+
* says which page and which of the two fixes to reach for.
|
|
37
|
+
*/
|
|
38
|
+
declare function stepTitle(stops: NavStop[], step: {
|
|
39
|
+
href: string;
|
|
40
|
+
title?: string | undefined;
|
|
41
|
+
}): string | undefined;
|
|
29
42
|
//#endregion
|
|
30
|
-
export { NavStop, neighbours, readingOrder };
|
|
43
|
+
export { NavStop, neighbours, readingOrder, stepTitle };
|
package/dist/nav-order.js
CHANGED
|
@@ -54,5 +54,19 @@ function neighbours(nodes, href) {
|
|
|
54
54
|
...next === void 0 ? {} : { next }
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A step's link text: the one it named, or the navigation's name for the page.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ A THROW, NOT A FALLBACK TO THE HREF. A row reading "Where this runs and
|
|
61
|
+
* what that buys → /docs/infrastructure" is a URL where a sentence should be,
|
|
62
|
+
* and it renders perfectly — nothing else in the pipeline would notice. The
|
|
63
|
+
* frontmatter is authored and the author is right there, so the build stops and
|
|
64
|
+
* says which page and which of the two fixes to reach for.
|
|
65
|
+
*/
|
|
66
|
+
function stepTitle(stops, step) {
|
|
67
|
+
if (step.title !== void 0) return step.title;
|
|
68
|
+
const here = normalize(step.href);
|
|
69
|
+
return stops.find((stop) => normalize(stop.href) === here)?.title;
|
|
70
|
+
}
|
|
57
71
|
//#endregion
|
|
58
|
-
export { neighbours, readingOrder };
|
|
72
|
+
export { neighbours, readingOrder, stepTitle };
|