@waveso/docs 0.10.0 → 0.12.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 +655 -0
- package/README.md +80 -3
- 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/nav.js +14 -11
- package/dist/react/search-dialog.js +87 -15
- package/dist/react/shell-labels.d.ts +8 -1
- package/dist/react/shell-labels.js +1 -0
- package/dist/styles.css +1144 -168
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,660 @@
|
|
|
1
1
|
# @waveso/docs
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3551951: **The search results and the sidebar say "there is more this way" the same way
|
|
8
|
+
the table does, and hide their scrollbars where they can.**
|
|
9
|
+
|
|
10
|
+
Four gradients on the search list, exactly as `.wave-docs-table-scroll` does it:
|
|
11
|
+
the two `local` covers are painted in the surface colour and travel _with_ the
|
|
12
|
+
rows, so each sits over its shadow only while that edge is at rest, and the two
|
|
13
|
+
`scroll` shadows are pinned to the box. A grey edge appears on precisely the side
|
|
14
|
+
that has rows off-screen, with no listener, no state and no hydration.
|
|
15
|
+
|
|
16
|
+
⚠️ IT REPLACED A MASK, WHICH WAS THE WRONG TOOL TWICE OVER. A mask fades content
|
|
17
|
+
to _transparent_, so what showed through was the dialog's own white — a hole
|
|
18
|
+
rather than a shadow. And it cannot be conditional: CSS has no way to ask whether
|
|
19
|
+
there is anything above to scroll to, so the first and last rows were softened
|
|
20
|
+
even at rest.
|
|
21
|
+
|
|
22
|
+
## The sidebar needs its shadow above the content, not behind it
|
|
23
|
+
|
|
24
|
+
⚠️ `.wave-docs-sidebar` PAINTS ITS OWN GROUND, AND MUST. It is in the theme's
|
|
25
|
+
opt-in rule — the one that installs a ground wherever it installs a foreground
|
|
26
|
+
ramp — so a consumer mounting the tree alone gets one, and `styles.browser.test`
|
|
27
|
+
requires the shell to be a single surface. An element's background is painted
|
|
28
|
+
_below_ its children, so the four-gradient trick was covered by a child 480px
|
|
29
|
+
tall: measured 255/255 at the top edge with the nav scrolled — declared,
|
|
30
|
+
computed, and invisible.
|
|
31
|
+
|
|
32
|
+
So the navigation's overflow moved to an inner box, `.wave-docs-layout__sidebar-scroll`,
|
|
33
|
+
and the panel keeps only its edges.
|
|
34
|
+
|
|
35
|
+
⚠️ THAT SPLIT IS THE FIX, AND EVERY VERSION BEFORE IT FOUGHT THE SCROLLER. An
|
|
36
|
+
absolutely positioned child of a scroll container is laid out against that
|
|
37
|
+
container's padding box and _joins its scrollable overflow_ — so nothing placed
|
|
38
|
+
inside a scroller is ever pinned to it. `position: sticky` only clamps, and a
|
|
39
|
+
clamp is still a thing that travels until it catches. With the scrolling on an
|
|
40
|
+
inner element the panel is an ordinary positioned box: `top: 0` and `bottom: 0`
|
|
41
|
+
are its own edges, and the bands have no scroll to have a position within.
|
|
42
|
+
|
|
43
|
+
The timeline is declared on the scroller and `timeline-scope`d up to the panel,
|
|
44
|
+
because a `scroll-timeline` name is visible to the declaring element's
|
|
45
|
+
descendants and these pseudo-elements belong to its parent.
|
|
46
|
+
|
|
47
|
+
⚠️ AND THE SHADOW BELONGS TO THE PANEL RATHER THAN THE TREE, because
|
|
48
|
+
`.wave-docs-sidebar` paints an opaque ground — it is in the theme's opt-in rule,
|
|
49
|
+
and the shell has to be one surface — so a background on the scroller would be
|
|
50
|
+
painted underneath it. Measured 255/255 when it was.
|
|
51
|
+
|
|
52
|
+
⚠️ AND THE NAV'S FADE IS A LENGTH, NOT A PERCENTAGE OF THE SCROLL. The table's
|
|
53
|
+
keyframes shape their fade in percentages, which are percentages of the
|
|
54
|
+
container's _total_ scroll range — fine on a table, wrong on a navigation. A nav
|
|
55
|
+
with 1000px of scroll turns the same `2%…8%` into 20px…80px, so the shadow spends
|
|
56
|
+
eighty pixels of scrolling arriving at full strength, which reads exactly like it
|
|
57
|
+
is moving with the content rather than pinned to the edge. `animation-range: 0
|
|
58
|
+
1rem` makes it the same short distance whatever the nav's height, identical on a
|
|
59
|
+
six-page site and a three-hundred-page one, with plain fades for keyframes
|
|
60
|
+
because the shaping now lives in the range.
|
|
61
|
+
|
|
62
|
+
⚠️ THE SCROLLBAR IS HIDDEN ONLY WHERE THE SHADOW EXISTS. Both live in the same
|
|
63
|
+
`@supports (animation-timeline: scroll())`, so Firefox — where scroll-driven
|
|
64
|
+
animations have not shipped — keeps the thin bar rather than losing the bar and
|
|
65
|
+
the shadow together. A scrollbar is the one cue that a column has more below it,
|
|
66
|
+
and taking it away before something replaces it trades a slab for nothing.
|
|
67
|
+
|
|
68
|
+
⚠️ AND A SECOND TEST LEARNED THAT `getAnimations()` IS NOT "TRANSITIONS". A
|
|
69
|
+
scroll-driven animation's `finished` promise resolves when the _scroll position_
|
|
70
|
+
reaches the end, which is to say never on a panel sitting at the top — so
|
|
71
|
+
awaiting the whole list hung `sidebar.browser.test.tsx` for its full 15s timeout
|
|
72
|
+
the moment this shadow existed. `nav.browser.test.tsx` had already been bitten by
|
|
73
|
+
the table's; this is the second place the assumption lived.
|
|
74
|
+
|
|
75
|
+
- 70649b8: **Search results get a glyph, a breadcrumb, and no scrollbar.**
|
|
76
|
+
|
|
77
|
+
**A page or a section, as an icon.** Which one is read off the `href` rather
|
|
78
|
+
than a new index field — a record with an anchor is a section within a page and
|
|
79
|
+
one without is the page itself, which is what `buildSearchIndex` means by its
|
|
80
|
+
lead record — so the records did not grow by a byte.
|
|
81
|
+
|
|
82
|
+
⚠️ AND THE ICON IS NOT DECORATION, IT IS THE ALIGNMENT. With no icon a result's
|
|
83
|
+
heading sat on the same column as the input's _magnifier_ while the input's own
|
|
84
|
+
text sat 24px right of it: three left edges where a reader expects two, which is
|
|
85
|
+
what made the dialog read as stacked levels rather than as a list under a field.
|
|
86
|
+
The icon takes the magnifier's column and the text takes the input's, so the two
|
|
87
|
+
rows are the same shape. Measured: 18px and 42px, both rows, plus the footer's
|
|
88
|
+
key caps on the 18.
|
|
89
|
+
|
|
90
|
+
**The second line is a breadcrumb.** The segments were always a trail from the
|
|
91
|
+
site's root to the page; slashes made them read as a URL to parse and `›` makes
|
|
92
|
+
them read as what they are. ⚠️ SAFE ONLY BECAUSE THAT LINE IS `aria-hidden` —
|
|
93
|
+
more than one screen reader pronounces the character, which is exactly why
|
|
94
|
+
`spokenName` joins with commas instead. The two lines carry the same fact in the
|
|
95
|
+
form each audience can use.
|
|
96
|
+
|
|
97
|
+
⚠️ THE SEPARATOR IS THE PACKAGE'S OWN CHEVRON, NOT A CHARACTER, AND TWO
|
|
98
|
+
CHARACTERS WERE MEASURED AND REJECTED FIRST. `›` (U+203A) inks 4.93px tall in
|
|
99
|
+
the shipped mono face at 11px, against 6.32 for an `s` and 8.51 for a `b` — it
|
|
100
|
+
sat visibly below the words it separated. `❯` (U+276F) inks 8.03 and fixed the
|
|
101
|
+
height _on one machine_: `ui-monospace` resolves to SF Mono here, Consolas on
|
|
102
|
+
Windows, Liberation Mono on Linux, and a glyph missing from one of those falls
|
|
103
|
+
back to another face at another width. U+3009 already demonstrated exactly that,
|
|
104
|
+
measuring 11.24px against the 6.62 cell.
|
|
105
|
+
|
|
106
|
+
An SVG has no font to be missing from — and it is the same Lucide chevron the
|
|
107
|
+
sidebar and the pager draw, which is the real argument: everything else here is
|
|
108
|
+
that one set.
|
|
109
|
+
|
|
110
|
+
⚠️ AND THE TRAIL IS SET IN THE SANS, NOT IN THE MONO IT STARTED IN — WHICH IS
|
|
111
|
+
WHAT MADE THE SEPARATOR RELIABLE. A monospace face advances every glyph one
|
|
112
|
+
cell, so the segments marched on a rigid grid an icon could not join, and its
|
|
113
|
+
metrics are whatever the machine resolves: SF Mono here, Consolas on Windows,
|
|
114
|
+
Liberation Mono on Linux. The cap height an inline glyph is sized against
|
|
115
|
+
therefore moved from reader to reader, which is the same portability problem
|
|
116
|
+
that ruled out `›` and `❯` as characters — arriving a second time through the
|
|
117
|
+
face rather than through the glyph. Proportional sans has ordinary spacing and
|
|
118
|
+
one set of metrics to match. `font-size` goes up a notch with it, because a
|
|
119
|
+
mono face reads larger at the same size and holding the number would have
|
|
120
|
+
shrunk the line.
|
|
121
|
+
|
|
122
|
+
⚠️ AND THE `viewBox` IS CROPPED TO THE STROKE, WHICH IS WHAT RETIRED FOUR
|
|
123
|
+
CORRECTIONS. On Lucide's full 24 grid the chevron occupies x 9–15 and y 6–18,
|
|
124
|
+
so a square box around it was five sixths air across and half air down — and
|
|
125
|
+
every one of those gaps had to be subtracted back by hand: a `vertical-align`,
|
|
126
|
+
a negative `margin-inline`, a negative `margin-block`, and a size chosen to
|
|
127
|
+
make the surplus come out right. `8 5 8 14` is the painted extent, caps and
|
|
128
|
+
joins included. The box is the glyph, so it sits on the baseline the way a
|
|
129
|
+
letter does with nothing declared, and the air beside it is one positive
|
|
130
|
+
`margin-inline`.
|
|
131
|
+
|
|
132
|
+
⚠️ AND IT CARRIES `overflow: visible`, WITHOUT WHICH THE POINT IS FLAT. An
|
|
133
|
+
`svg` clips to its viewport by default and this `viewBox` is exactly the
|
|
134
|
+
stroke's extent, so the round join at the tip lands on the box's own edge and
|
|
135
|
+
loses its outermost anti-aliased pixel. Nothing overlaps anything: what spills
|
|
136
|
+
is a fraction of a pixel.
|
|
137
|
+
|
|
138
|
+
## The scrollbar is hidden, and it cannot be conditional
|
|
139
|
+
|
|
140
|
+
⚠️ THERE IS NO CSS WAY TO SHOW A SCROLLBAR ONLY WHILE SCROLLING. That behaviour
|
|
141
|
+
is the platform's: macOS draws overlay scrollbars that fade in on scroll and out
|
|
142
|
+
after it, and this list gets it for free there. Windows and Linux draw a classic
|
|
143
|
+
one that is always present, and the only ways to make it come and go are a
|
|
144
|
+
JavaScript timer toggling a class or a scrollbar drawn from scratch in script —
|
|
145
|
+
which is what a `ScrollArea` component is, and neither belongs in a package whose
|
|
146
|
+
whole argument is what it does _not_ ship to a reader.
|
|
147
|
+
|
|
148
|
+
So it is hidden, as `@waveso/app` does with `.scrollbar-none`. What replaces it
|
|
149
|
+
is the keyboard rather than a fade: the footer says `↑ ↓ Select`, the list is
|
|
150
|
+
driven by `aria-activedescendant`, and arrowing past the last visible row scrolls
|
|
151
|
+
it and loads the next page.
|
|
152
|
+
|
|
153
|
+
⚠️ AND `pageSize` STAYS A WINDOW, NOT A CAP. It was a hard ceiling of 8 once, and
|
|
154
|
+
that was removed with measurements: on a six-page site "docs" matches 18, so
|
|
155
|
+
results were unreachable and the live region announced "8 results" — not a
|
|
156
|
+
smaller truth but a false one. Rendering every row of a 300-page corpus costs
|
|
157
|
+
40ms, 128ms at 4x throttle, which is what paging exists to avoid; the ceiling
|
|
158
|
+
was never what made it fast.
|
|
159
|
+
|
|
160
|
+
The size budget moves with the glyphs, and the figures the README and the
|
|
161
|
+
installation page publish move with it — a document here may not understate what
|
|
162
|
+
this package costs.
|
|
163
|
+
|
|
164
|
+
- 78053a6: **Nothing in the search dialog is selected until the reader selects it.**
|
|
165
|
+
|
|
166
|
+
Typing lit the first result the instant it arrived, so a row changed under a
|
|
167
|
+
reader for something they had not chosen — and on this package's accent that
|
|
168
|
+
reads as a decision already made. `activeIndex` starts at `-1` now: an arrow key
|
|
169
|
+
or the pointer moving over a row is what selects.
|
|
170
|
+
|
|
171
|
+
⚠️ AND EVERY GUARD FOR IT ALREADY EXISTED, WHICH IS WHY THIS IS ONE NUMBER.
|
|
172
|
+
`hits[-1]` is `undefined`, so no `aria-activedescendant` is written and Enter
|
|
173
|
+
returns without opening anything. The one place that needed teaching is the
|
|
174
|
+
arrow keys: they wrap modulo the list, and `(-1 - 1 + n) % n` is the _second to
|
|
175
|
+
last_, so Up from a fresh query landed one short of the end.
|
|
176
|
+
|
|
177
|
+
**The active row is the table header's grey**, easing on the same 150ms the
|
|
178
|
+
sidebar's rows use. Moving the pointer over a result activates it, so hovering
|
|
179
|
+
_is_ this state; there is no second rule for it.
|
|
180
|
+
|
|
181
|
+
It has been four things: a 2px accent ring, which read as a component borrowed
|
|
182
|
+
from somewhere else and came and went as a reader arrowed; the trigger's border
|
|
183
|
+
pair, which meant bordering _every_ row to make one edge legible and turned a
|
|
184
|
+
list into a stack of cards; an accent tint, which was the same colour family as
|
|
185
|
+
the field above it. A row is a list item and its state is a colour — the field is
|
|
186
|
+
a control, and that is what wears the trigger's border.
|
|
187
|
+
|
|
188
|
+
⚠️ `bg-subtle` ON `bg` IS ABOUT 1.02:1, SO THE TINT IS A HINT AND NOT AN
|
|
189
|
+
INDICATOR. The marker going `fg-subtle` to `fg` is the part that carries the
|
|
190
|
+
state — text contrast rather than non-text, and the sidebar's own hover. What
|
|
191
|
+
must not happen is the tint being left to carry it alone.
|
|
192
|
+
|
|
193
|
+
## The field stops drawing a ring it could never put down
|
|
194
|
+
|
|
195
|
+
⚠️ A TEXT INPUT MATCHES `:focus-visible` WHENEVER IT IS FOCUSED, HOWEVER FOCUS
|
|
196
|
+
ARRIVED — that is the spec, not a heuristic — and this dialog focuses its input
|
|
197
|
+
the moment it opens. So the accent ring was not a state, it was the field's
|
|
198
|
+
permanent appearance, and it stopped the field looking like the bordered grey
|
|
199
|
+
control the reader clicked to get there.
|
|
200
|
+
|
|
201
|
+
The edge darkens to `--wave-docs-border-strong` instead, which is what the
|
|
202
|
+
trigger does under the pointer, so the field is the trigger in both of its
|
|
203
|
+
states.
|
|
204
|
+
|
|
205
|
+
⚠️ AND THE INDICATOR IS NOT LOST WITH IT, BECAUSE A TEXT FIELD HAS ONE OF ITS
|
|
206
|
+
OWN. The caret is the platform's focus indication for a text box, it is in this
|
|
207
|
+
field the whole time the dialog is open, and it is what a reader looks for to see
|
|
208
|
+
where typing goes. That is a different argument from the one this rule used to
|
|
209
|
+
reject: "the dialog frame is the indicator" was a _static_ border that looked
|
|
210
|
+
identical focused and unfocused, and indicated nothing. This edge changes.
|
|
211
|
+
|
|
212
|
+
## The list fades at both edges
|
|
213
|
+
|
|
214
|
+
A mask on the scrollport, so the row being cut off is the one that softens. It
|
|
215
|
+
does not travel with the content: a mask paints against the element's own box, so
|
|
216
|
+
the two stops stay at the top and bottom of the _port_ while the rows move under
|
|
217
|
+
them.
|
|
218
|
+
|
|
219
|
+
⚠️ IT IS UNCONDITIONAL, WHICH IS THE ONE THING TO KNOW. CSS cannot ask "is there
|
|
220
|
+
anything above this to scroll to" — the table's shadow answers that with a
|
|
221
|
+
scroll-driven animation whose timeline goes inactive when nothing overflows, and
|
|
222
|
+
a mask has no equivalent. So the first and last rows carry a little of it at
|
|
223
|
+
rest. At `1.25rem` against a row nearer three times that, it costs the top pixel
|
|
224
|
+
or two of a heading and buys never guillotining one.
|
|
225
|
+
|
|
226
|
+
## The field, and the gap under it
|
|
227
|
+
|
|
228
|
+
**The field is the trigger, expanded.** Same border, same fill, same radius. It
|
|
229
|
+
was a border with no fill — two frames a few pixels apart — and briefly a fill
|
|
230
|
+
with no border, which is a tinted band rather than a control. The trigger has
|
|
231
|
+
always been both, and both together are what reads as a field.
|
|
232
|
+
|
|
233
|
+
⚠️ THE PADDING IS NOT COPIED WITH THEM. The trigger pays `calc(0.5rem - 1px)`
|
|
234
|
+
because it is a compact control in a sidebar; the field pays
|
|
235
|
+
`calc(0.75rem - 1px)` because its glyph has to land on the column the results
|
|
236
|
+
and the footer sit on. The `- 1px` is the border either way — content inside a
|
|
237
|
+
bordered box starts a border further in.
|
|
238
|
+
|
|
239
|
+
The field takes the base radius and the result rows take `-sm`, which is the tier
|
|
240
|
+
system doing what it says: both are controls, and a result row is a list item.
|
|
241
|
+
|
|
242
|
+
⚠️ AND THE GAP UNDER THE FIELD WAS PAID TWICE. The field's margin and the result
|
|
243
|
+
list's top padding both contributed, so the space between the input and the first
|
|
244
|
+
result was double the space above the input — a doubled gap in the one place a
|
|
245
|
+
reader's eye travels on every keystroke. The list drops its top padding; the
|
|
246
|
+
field keeps its margin, because an empty result list is `display: none` and a gap
|
|
247
|
+
paid from there would vanish on the query that matches nothing.
|
|
248
|
+
|
|
249
|
+
### Patch Changes
|
|
250
|
+
|
|
251
|
+
- 688a84c: **The search dialog lines up on one column, and its input is a field rather
|
|
252
|
+
than a band.**
|
|
253
|
+
|
|
254
|
+
The input row was flush to the dialog's frame, so it could only round the two
|
|
255
|
+
corners it shared with it and needed a rule underneath to separate it from the
|
|
256
|
+
results. Inset by the same margin the results list uses, it is a box like they
|
|
257
|
+
are — all four corners rounded, and the gap does the separating.
|
|
258
|
+
|
|
259
|
+
⚠️ AND ITS RADIUS IS THE DIALOG'S MINUS THE GAP, NOT THE DIALOG'S. A rounded box
|
|
260
|
+
inset inside a rounded box is concentric only at `outer - gap`; equal radii run
|
|
261
|
+
the two corners at different curvatures six pixels apart and the field reads as
|
|
262
|
+
pasted onto the dialog rather than set into it.
|
|
263
|
+
|
|
264
|
+
**One column, measured from the bottom up.** The footer's key caps are the
|
|
265
|
+
anchor — the one row whose left edge is a drawn object — and the results' text
|
|
266
|
+
and the input's magnifier are measured to it. All three now start 18px from the
|
|
267
|
+
dialog's inner edge.
|
|
268
|
+
|
|
269
|
+
⚠️ BOXES, NOT INK. A cap's arrow sits its own border and `0.4em` of padding
|
|
270
|
+
inside the cap, so aligning the _glyphs_ would put every other row on a column
|
|
271
|
+
that moves whenever the footer's font size does.
|
|
272
|
+
|
|
273
|
+
⚠️ AND THE INPUT ROW PAYS `calc(0.75rem - 1px)` BECAUSE IT IS THE ONE ROW WITH A
|
|
274
|
+
BORDER. Content inside a bordered box starts a border further in than content
|
|
275
|
+
inside an unbordered one, so equal padding misses by exactly that — the same
|
|
276
|
+
subtraction `--wave-docs-panel-inset` exists for.
|
|
277
|
+
|
|
278
|
+
## 0.11.0
|
|
279
|
+
|
|
280
|
+
### Minor Changes
|
|
281
|
+
|
|
282
|
+
- 5730af3: **The code frame wears the panel.** A fence is now an outer card holding a
|
|
283
|
+
header row — the filename or the language on the left, the copy button on the
|
|
284
|
+
right — and an inset surface with the code on it, dressed by the same
|
|
285
|
+
`.wave-docs-panel` rules as "where to go next".
|
|
286
|
+
|
|
287
|
+
⚠️ THE `<figcaption>` STAYS A DIRECT CHILD OF THE `<figure>`, WHICH IS WHY THIS
|
|
288
|
+
HAS NO HEADER WRAPPER. A `figcaption` has to be the first or last child of its
|
|
289
|
+
figure; inside the `.wave-docs-panel__header` `<div>` that "where to go next"
|
|
290
|
+
uses it captions nothing, the markup is invalid, and a titled block loses the
|
|
291
|
+
accessible name it had. So the frame lays its header out on a grid and shares
|
|
292
|
+
the primitive's _insets_ rather than its header element. The button stays out of
|
|
293
|
+
the caption for the matching reason: a `<button>` inside a `<figcaption>`
|
|
294
|
+
contributes its accessible name to the figure's, so `swap.ts` would announce as
|
|
295
|
+
"swap.ts Copy code from swap.ts".
|
|
296
|
+
|
|
297
|
+
⚠️ AND A TITLE IS WHAT DECIDES WHETHER THERE IS A FRAME AT ALL. With one, the
|
|
298
|
+
figure is a panel: a band carrying the filename and the copy button, and the
|
|
299
|
+
code set into a card below it. With none there is nothing to put in a band, so
|
|
300
|
+
the frame flattens away, the surface becomes the block, and the button sits on
|
|
301
|
+
the code — which is what Mintlify does, and what stops an unnamed fence from
|
|
302
|
+
carrying a reserved slot with nothing in it.
|
|
303
|
+
|
|
304
|
+
A language is not a title for this purpose. A fence declaring `ts` and no
|
|
305
|
+
filename is still an untitled fence, and a band holding a two-letter badge is
|
|
306
|
+
the same empty header with a word in it. `data-lang` stays on the figure for
|
|
307
|
+
anyone selecting on it.
|
|
308
|
+
|
|
309
|
+
One shape of markup, switched in the stylesheet rather than in the pipeline: two
|
|
310
|
+
markup paths mean two fixtures, and the one that is not on screen is the one
|
|
311
|
+
that rots.
|
|
312
|
+
|
|
313
|
+
**The copy button no longer hides until you hover.** It faded in on `:hover` or
|
|
314
|
+
`:focus-within` because it was positioned over the code and had nowhere of its
|
|
315
|
+
own to be. It has a slot in the header row now, and a reserved slot that stays
|
|
316
|
+
empty until you point at it reads as a rendering fault — so the reveal, the
|
|
317
|
+
`@media (hover: none)` exception that existed because a hover-only control does
|
|
318
|
+
not exist on a phone, and the reduced-motion guard on its transition all went
|
|
319
|
+
with it. It is still `visibility: hidden` until the runtime attaches, which is
|
|
320
|
+
the structural promise and is unchanged.
|
|
321
|
+
|
|
322
|
+
## Three things the restructure moved, each of which was a defect
|
|
323
|
+
|
|
324
|
+
⚠️ THE `<pre>` DRAWS NO FRAME OF ITS OWN ANY MORE. It carried the border, the
|
|
325
|
+
radius and the background; inside a `.wave-docs-panel__body` that carries all
|
|
326
|
+
three, that draws the frame twice one pixel apart. `pre:not(.shiki)` keeps its
|
|
327
|
+
own, because an excluded fence is never wrapped.
|
|
328
|
+
|
|
329
|
+
⚠️ AND ITS INLINE PADDING IS `1rem` BECAUSE THAT IS `--wave-docs-panel-inset`,
|
|
330
|
+
not because it is a round number. The label sits at that inset plus the
|
|
331
|
+
surface's border; the code sits at the surface's border plus this. The
|
|
332
|
+
`1.125rem` it was put the first character 2px right of the filename above it —
|
|
333
|
+
visible, and attributable to nothing.
|
|
334
|
+
|
|
335
|
+
⚠️ AND THE FOCUS RING ON THE `<pre>` IS INSET NOW. Shiki gives it
|
|
336
|
+
`tabindex="0"` so a keyboard reader can scroll a wide block, and the surface
|
|
337
|
+
around it is `overflow: hidden` so a square corner cannot poke through the
|
|
338
|
+
frame's rounded one — which clipped a `+2px` outline away to nothing.
|
|
339
|
+
`styles.test.ts` reads rules as text and would have gone on passing.
|
|
340
|
+
|
|
341
|
+
## The panel grew two properties, both to settle a cascade rather than a taste
|
|
342
|
+
|
|
343
|
+
`--wave-docs-panel-surface` is the inset surface's ground, and its default lives
|
|
344
|
+
in a `var()` fallback rather than in a declaration. `.wave-docs-code__body` and
|
|
345
|
+
`.wave-docs-panel__body` are both one class, so source order decides and the
|
|
346
|
+
panel is declared later: a code frame asking for the darker code ground got the
|
|
347
|
+
panel's white. Moving the ground to a property did not fix it either — a frame
|
|
348
|
+
wears `.wave-docs-panel` _and_ `.wave-docs-code`, so both rules set that property
|
|
349
|
+
on the same element at the same specificity, and source order handed it back.
|
|
350
|
+
Measured twice as `oklch(1 0 0)` where `oklch(0.975 0.003 262)` was written. In
|
|
351
|
+
the fallback there is no declaration to lose to.
|
|
352
|
+
|
|
353
|
+
`--wave-docs-panel-header-row` floors the header. With a label the row is the
|
|
354
|
+
label's height and with none it is the button's, and a page mixing titled and
|
|
355
|
+
untitled fences showed two header heights.
|
|
356
|
+
|
|
357
|
+
The frame's markup now has one home — `codeFrameMarkup` — that the browser tier
|
|
358
|
+
mounts and the plugin tier asserts the pipeline agrees with. Written out
|
|
359
|
+
separately in both, a hand-written fixture goes on describing a frame the
|
|
360
|
+
pipeline stopped emitting while every assertion measuring it stays green.
|
|
361
|
+
|
|
362
|
+
- 793d0f7: **"Where to go next": a question per row, and the page that answers it.** A page
|
|
363
|
+
opts in from its frontmatter:
|
|
364
|
+
|
|
365
|
+
```yaml
|
|
366
|
+
explore:
|
|
367
|
+
- question: How a person is recognised across servers
|
|
368
|
+
href: ./identity.md
|
|
369
|
+
- question: What happens when the network fails
|
|
370
|
+
href: ./delivery.md
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
A sidebar is a structure; this is a router. It says _why_ a reader would go
|
|
374
|
+
somewhere, which no tree of titles can — so the same component is a landing
|
|
375
|
+
page's onboarding and an ordinary page's footnote.
|
|
376
|
+
|
|
377
|
+
⚠️ THE LINK TEXT COMES FROM THE NAVIGATION. An `href` pointing at a page in the
|
|
378
|
+
tree takes that page's own name, so renaming it updates every block that points
|
|
379
|
+
at it — the same tree the sidebar and the pager read. Name a `title` only where
|
|
380
|
+
the tree cannot answer: an external link, or a page kept out of the navigation.
|
|
381
|
+
|
|
382
|
+
⚠️ AND AN HREF THAT RESOLVES TO NEITHER STOPS THE BUILD. Falling back to the
|
|
383
|
+
href renders a URL where a sentence should be — "Where this runs and what that
|
|
384
|
+
buys → /docs/infrastructure" — on a page that builds cleanly, and nothing else
|
|
385
|
+
in the pipeline would notice. The frontmatter is authored and the author is
|
|
386
|
+
right there, so the error names the page and both fixes.
|
|
387
|
+
|
|
388
|
+
⚠️ A LIST, NOT A TABLE, WHICH IS WHAT THE MARKDOWN IT REPLACES HAD TO BE. A
|
|
389
|
+
screen reader announces "table, 2 columns, 7 rows" for what is a list of links
|
|
390
|
+
with descriptions, and asks the reader to navigate it by cell. Two columns of
|
|
391
|
+
sentence-length questions are also cramped in a narrow box, where the rows stack
|
|
392
|
+
instead — a `@container` query, because a host can hand this a 500px panel on a
|
|
393
|
+
1920px monitor.
|
|
394
|
+
|
|
395
|
+
One frame with hairlines between the rows rather than a stack of cards: they are
|
|
396
|
+
the same question asked several ways, and separate boxes say several unrelated
|
|
397
|
+
things. The rule is a _top_ border on every row but the first — `:last-child`
|
|
398
|
+
leaves a doubled line the moment anything is appended to the list.
|
|
399
|
+
|
|
400
|
+
## And the frame is a primitive, not this component's furniture
|
|
401
|
+
|
|
402
|
+
`.wave-docs-panel` is a framed block with a header and an inset surface — the
|
|
403
|
+
outer card names the thing and carries its controls, the inner one holds the
|
|
404
|
+
content. "Where to go next" is the first thing to wear it; a code frame is the
|
|
405
|
+
obvious next, and two copies of the same three rules is how they drift apart.
|
|
406
|
+
|
|
407
|
+
⚠️ THE TWO RADII ARE NOT INDEPENDENT NUMBERS. A rounded box inside a rounded box
|
|
408
|
+
only looks right when the inner radius is the outer one minus the gap between
|
|
409
|
+
them; anything else runs the corners at different curvatures and the inner box
|
|
410
|
+
reads as _pasted onto_ the frame rather than set into it. The new
|
|
411
|
+
`--wave-docs-radius-lg` is chosen so the arithmetic lands on an existing token:
|
|
412
|
+
`1rem` outer minus `0.5rem` of padding is `--wave-docs-radius`.
|
|
413
|
+
|
|
414
|
+
The header sits in the frame's padding and draws no rule of its own — the inset
|
|
415
|
+
surface below already draws the line, and a border there is a second one a pixel
|
|
416
|
+
away from the first.
|
|
417
|
+
|
|
418
|
+
⚠️ AND THE PANEL EXPORTS `--wave-docs-panel-inset`, WHICH IS NOT THE SAME NUMBER
|
|
419
|
+
AS ITS PADDING. The title sits at the frame's padding; anything inside the body
|
|
420
|
+
sits at that padding _plus the body's own border_, so the two columns miss each
|
|
421
|
+
other by a pixel per border. Measured before it existed: the rows started 9px
|
|
422
|
+
right of the heading above them — a number that appears in no rule and reads as
|
|
423
|
+
a design decision. Exported rather than repeated, because the next component to
|
|
424
|
+
wear the panel has to make the same subtraction and will not think to.
|
|
425
|
+
|
|
426
|
+
It renders above the pager. The two answer different questions and both belong
|
|
427
|
+
there: this is the semantic answer, the pager the linear one.
|
|
428
|
+
|
|
429
|
+
A real `<h2>` names it, not an `aria-label` — a reader moving by heading would
|
|
430
|
+
pass straight over a region named only by an attribute.
|
|
431
|
+
|
|
432
|
+
## The key is `explore`, and neither `next` nor `steps` would do
|
|
433
|
+
|
|
434
|
+
`next` is unusable in this package. `src/next.ts` is the Next.js adapter, so
|
|
435
|
+
two files one directory apart would carry the same name for entirely different
|
|
436
|
+
things, and `doc.frontmatter.next` would read like a routing hook rather than
|
|
437
|
+
a block of prose.
|
|
438
|
+
|
|
439
|
+
`steps` is wrong for a different reason: these rows are a _branch_, not a
|
|
440
|
+
sequence. A reader picks one and ignores the rest, and none of them is first.
|
|
441
|
+
A numbered "1 -> 2 -> 3" component is a real and separate thing worth building
|
|
442
|
+
later, and `steps` is the name it will need.
|
|
443
|
+
|
|
444
|
+
The rendered heading is still "Where to go next" — `next` in prose is fine,
|
|
445
|
+
it is only the identifier that had to move.
|
|
446
|
+
|
|
447
|
+
No client JavaScript. New: `DocsExplore` at `@waveso/docs/react/explore`,
|
|
448
|
+
`explore` in frontmatter, and `explore` in `labels`.
|
|
449
|
+
|
|
450
|
+
- c871d2f: **One radius scale, taken from `@waveso/ui`, retunable from a single line.**
|
|
451
|
+
|
|
452
|
+
Three tiers, all `calc()` off one root, so which corner a box gets is decided by
|
|
453
|
+
what _kind_ of box it is rather than by how big it happens to be:
|
|
454
|
+
|
|
455
|
+
| Token | What takes it |
|
|
456
|
+
| ----------------------- | ----------------------------------------------------------- |
|
|
457
|
+
| `--wave-docs-radius-sm` | inline chips, small controls, focus rings on those |
|
|
458
|
+
| `--wave-docs-radius` | controls, overlays, and the panel's inset surface |
|
|
459
|
+
| `--wave-docs-radius-lg` | every block in the reading flow, and the panel's outer edge |
|
|
460
|
+
|
|
461
|
+
⚠️ THE BLOCK TIER WAS SPLIT. Callouts, images and video embeds sat at the base
|
|
462
|
+
radius while a code frame and a table sat at 19px, so two blocks a paragraph
|
|
463
|
+
apart disagreed by eleven pixels — and a table read as aggressively round next
|
|
464
|
+
to the callout above it. 19 was measured off a reference site, which is a fine
|
|
465
|
+
way to pick a number and a bad way to pick a _system_.
|
|
466
|
+
|
|
467
|
+
⚠️ AND THE NUMBERS ARE `@waveso/ui`'s, NOT NEW ONES. A page running both this
|
|
468
|
+
package and the component library should not show two radius scales a few pixels
|
|
469
|
+
apart, and taking theirs is how that is guaranteed rather than kept in step by
|
|
470
|
+
hand. `--wave-docs-radius-base` is the override point: a host writes
|
|
471
|
+
`--wave-docs-radius-base: var(--radius)` and every corner here follows their app,
|
|
472
|
+
including any theme that moves it. Overriding three tokens separately would be
|
|
473
|
+
three chances to break the arithmetic below.
|
|
474
|
+
|
|
475
|
+
⚠️ `--wave-docs-radius-step` IS LOAD-BEARING. The panel's inset surface takes the
|
|
476
|
+
base radius and its frame takes `-lg`, which is the base plus one step — so the
|
|
477
|
+
two corners are concentric only while the frame's _padding_ is that same step.
|
|
478
|
+
It is paid out of the token rather than written as `4px`, so moving the root
|
|
479
|
+
keeps both true. `--wave-docs-radius-panel` is gone with it: a token whose only
|
|
480
|
+
job is to be another token minus a constant is a number that can drift from its
|
|
481
|
+
own definition.
|
|
482
|
+
|
|
483
|
+
## Squircles, where the browser has them
|
|
484
|
+
|
|
485
|
+
`corner-shape` renders every `border-radius` as a continuous superellipse rather
|
|
486
|
+
than a circular arc. `@waveso/ui` ships it and this now matches, including the
|
|
487
|
+
root bump both make under the same `@supports` — a squircle reads tighter at the
|
|
488
|
+
same radius, so the scale moves up to restore the roundness the numbers were
|
|
489
|
+
chosen for. Only the root moves; every tier and the panel's padding follow.
|
|
490
|
+
|
|
491
|
+
⚠️ SCOPED TO ELEMENTS THIS PACKAGE OWNS, AND NOT `*`. `@waveso/ui` can say `*`
|
|
492
|
+
because it is the application's own stylesheet. This one is mounted inside
|
|
493
|
+
somebody else's page, and a bare `*` would reshape every corner the host drew —
|
|
494
|
+
the same trespass as claiming `html` or `body`, which this file already refuses.
|
|
495
|
+
Pills and dots opt back out, because a squircled pill is a lozenge.
|
|
496
|
+
|
|
497
|
+
⚠️ AND THE SOURCE TEST FOR THE CONCENTRIC ARITHMETIC WENT VACUOUS ON THE WAY.
|
|
498
|
+
It regexed three `rem` literals out of the token block; with the tiers as
|
|
499
|
+
`calc()` the regex matched nothing, both sides defaulted to zero, and `0 - 0`
|
|
500
|
+
passed while asserting nothing. It is measured in computed pixels now, which is
|
|
501
|
+
also the only tier that can see the squircle bump. Two browser assertions that
|
|
502
|
+
built an expected radius out of `getPropertyValue` were the same mistake in a
|
|
503
|
+
different shape — a custom property is not computed to pixels, so it hands back
|
|
504
|
+
the `calc()` as written. They compare two computed corners to each other now,
|
|
505
|
+
which is the uniformity claim anyway.
|
|
506
|
+
|
|
507
|
+
### Patch Changes
|
|
508
|
+
|
|
509
|
+
- 04b7de7: **A blockquote is a box, not a rule down one edge.**
|
|
510
|
+
|
|
511
|
+
Every other block set apart from the prose here — a callout, a code frame, a
|
|
512
|
+
table, an embed — is a bordered box. A single 3px edge made the quote the one
|
|
513
|
+
exception, so next to a callout two paragraphs away it read as a _different kind
|
|
514
|
+
of thing_ rather than as a quieter one.
|
|
515
|
+
|
|
516
|
+
It is the callout's box now, minus the hue: the same padding so their text lines
|
|
517
|
+
up, the same block radius, a plain border instead of a tinted one, and no accent
|
|
518
|
+
edge. That is the relationship — **a callout is a quote with a colour** — and it
|
|
519
|
+
finally looks like it.
|
|
520
|
+
|
|
521
|
+
Still not italic. The box and the muted colour already say "quotation", a long
|
|
522
|
+
italic passage is measurably slower to read, and markdown authors use
|
|
523
|
+
blockquotes for asides and notes rather than only for speech.
|
|
524
|
+
|
|
525
|
+
⚠️ AND IT HAD TO JOIN THE SQUIRCLE LIST BY NAME. Corner shaping is scoped to
|
|
526
|
+
elements this package owns, matched by our own class prefix — and a `blockquote`
|
|
527
|
+
is the markdown author's tag, with no class of ours on it. It would have been
|
|
528
|
+
the one block on the page still drawing a circular arc.
|
|
529
|
+
|
|
530
|
+
- 975acba: **A callout draws one uniform border, not a 3px rule down its inline start.**
|
|
531
|
+
|
|
532
|
+
The stripe made a callout the exception among blocks set apart from the prose —
|
|
533
|
+
a code frame, a table, an embed and now a blockquote are all boxes with one
|
|
534
|
+
border all the way round — so beside any of them it read as a different kind of
|
|
535
|
+
thing rather than as a coloured one. A thick rule on one side also fights the
|
|
536
|
+
corner it runs into once the box is a squircle.
|
|
537
|
+
|
|
538
|
+
⚠️ THE TYPE IS STILL NOT CONVEYED BY COLOUR ALONE. The stripe was never what
|
|
539
|
+
carried it: the icon and the label — "Note", "Warning" — are the non-colour
|
|
540
|
+
signals, and they are unchanged. The tinted border and ground stay as
|
|
541
|
+
reinforcement rather than as the whole message.
|
|
542
|
+
|
|
543
|
+
The table-of-contents rail keeps its 2px inline-start border. That is a
|
|
544
|
+
continuous line an active marker slides along, which is a navigation affordance
|
|
545
|
+
rather than the edge of a box, and a source guard names the two blocks it does
|
|
546
|
+
apply to.
|
|
547
|
+
|
|
548
|
+
- cd21631: **The copy button draws Lucide icons, like everything else here.**
|
|
549
|
+
|
|
550
|
+
It rendered `⧉`, and swapped in `✓` and `×` through CSS `content` — three
|
|
551
|
+
characters drawn by whatever font resolved, at whatever weight and baseline
|
|
552
|
+
that font has, beside a sidebar, a pager, a callout and a search dialog that
|
|
553
|
+
are all Lucide paths at `stroke-width: 2`. It read as a different icon set
|
|
554
|
+
because it was one. Now `copy`, `check` and `x` on the same 24×24 grid.
|
|
555
|
+
|
|
556
|
+
⚠️ THE SWAP IS `display` AND NOT `visibility`, AND THAT IS NOT A PREFERENCE.
|
|
557
|
+
All three icons ship in the markup and the stylesheet picks one — there is no
|
|
558
|
+
component owning this button, so there is no state to re-render. But the button
|
|
559
|
+
is `visibility: hidden` until the runtime attaches, which is what keeps a reader
|
|
560
|
+
with no JavaScript from meeting a control that does nothing and keeps it out of
|
|
561
|
+
the tab order — and `visibility` inherits. An icon rule setting it back to
|
|
562
|
+
`visible` would draw a glyph inside a button that is meant to be invisible.
|
|
563
|
+
`display` does not inherit, so the button's own rule still governs all three.
|
|
564
|
+
|
|
565
|
+
Three icons per fence is fifty on a page with fifty of them, and the repeat is
|
|
566
|
+
why that is affordable: byte-identical every time, which is the case gzip
|
|
567
|
+
handles best. The quick start's gzipped payload did not move, and the
|
|
568
|
+
hast-over-the-wire ratio for code and tables went from 1.11× to 1.09×.
|
|
569
|
+
|
|
570
|
+
## And it stops drawing a box around itself
|
|
571
|
+
|
|
572
|
+
No border and no ground, in any state. A bordered, filled 2rem box sitting on
|
|
573
|
+
the frame's own band is a third framed rectangle inside a frame that already has
|
|
574
|
+
two, for a control secondary to everything around it. The glyph is the whole
|
|
575
|
+
control, and hover moves its ink to the accent rather than putting a box behind
|
|
576
|
+
it — the same signal every other interactive surface here gives. A ground also
|
|
577
|
+
had nowhere to come from: the subtle ramp _is_ the frame's colour, so the hover
|
|
578
|
+
state it used to have was the colour the button was already sitting on.
|
|
579
|
+
|
|
580
|
+
⚠️ AND IT HAD NO FOCUS INDICATOR AT ALL, WHICH THE BORDER WAS COVERING FOR. A
|
|
581
|
+
1px box is not a focus indicator — it is there whether the control is focused or
|
|
582
|
+
not — so a keyboard reader tabbing onto this button got a `color` change and
|
|
583
|
+
nothing else, and it never appeared in the package's own inventory of focusable
|
|
584
|
+
surfaces. Taking the border away made that visible; it did not create it. There
|
|
585
|
+
is a real `:focus-visible` outline now, and a test that keeps it.
|
|
586
|
+
|
|
587
|
+
⚠️ AND ONE TEST WAS PASSING BY ACCIDENT. `render.test.ts` asserted that a
|
|
588
|
+
GitHub alert produces no octicons by checking the _whole document_ for `<svg>`,
|
|
589
|
+
which was only ever true because nothing else in the pipeline emitted one. It
|
|
590
|
+
now checks the callout's own subtree, which is what it meant.
|
|
591
|
+
|
|
592
|
+
- d7333cb: **The site's pages stop hand-writing their own "Next" link.** Nineteen of them
|
|
593
|
+
ended with `Next: [Page](./page.md).`, which the pager has rendered from the
|
|
594
|
+
navigation tree since it landed — the same link, twice, a few hundred pixels
|
|
595
|
+
apart, and the hand-written one silently wrong the moment a page moved in the
|
|
596
|
+
tree.
|
|
597
|
+
|
|
598
|
+
Documentation-only: no package change.
|
|
599
|
+
|
|
600
|
+
- 6f0afb7: **A panel title is the size of a column header.**
|
|
601
|
+
|
|
602
|
+
"Where to go next" and `Enforced by` are the same kind of thing — the label a
|
|
603
|
+
reader's eye lands on before the content under it — and a page shows both. At
|
|
604
|
+
`0.875rem` against the table's `0.9375rem` they read as two levels rather than
|
|
605
|
+
as one.
|
|
606
|
+
|
|
607
|
+
The weight does not follow: a panel title names the block, a column header names
|
|
608
|
+
a column inside one, so the title stays a step heavier. A source guard pins the
|
|
609
|
+
pair, because two literals that have to agree are two literals that drift.
|
|
610
|
+
|
|
611
|
+
- 8c59155: The navigation stops being lopsided.
|
|
612
|
+
|
|
613
|
+
Its rows sat 16px from the inline start and 31px from the end. The 15px was
|
|
614
|
+
`scrollbar-gutter: stable`, which reserves the scrollbar's width whether or not
|
|
615
|
+
it is showing — and that reservation sits _inside_ the padding.
|
|
616
|
+
|
|
617
|
+
The jump it prevents is one 15px shift, the first time a nav grows past a
|
|
618
|
+
screen. What it cost was permanent and on every page.
|
|
619
|
+
|
|
620
|
+
⚠️ AND IT IS A NO-OP ON macOS, WHICH IS WHY IT SURVIVED BEING LOOKED AT.
|
|
621
|
+
`scrollbar-gutter` does nothing where overlay scrollbars are the default, so the
|
|
622
|
+
asymmetry was invisible to everyone who built this and plain on every Windows
|
|
623
|
+
and Linux machine that opened it.
|
|
624
|
+
|
|
625
|
+
`scrollbar-width: thin` in its place, with `scrollbar-color`, so the scrollbar
|
|
626
|
+
that does appear is narrow and coloured rather than a UA slab against the
|
|
627
|
+
panel's edge. A browser test measures both insets and fails at 15px.
|
|
628
|
+
|
|
629
|
+
- e94bb67: **A table keeps one frame, at the panel's radius.**
|
|
630
|
+
|
|
631
|
+
It wore `.wave-docs-panel` for a while — outer frame, header band, inset card —
|
|
632
|
+
and that was wrong for a reason worth writing down rather than just undoing.
|
|
633
|
+
|
|
634
|
+
⚠️ THE PANEL SEPARATES _CHROME_ FROM _CONTENT_, AND A TABLE'S HEADER ROW IS
|
|
635
|
+
CONTENT. "Where to go next" and a code frame both have chrome to put in the
|
|
636
|
+
band: a title, a language, a copy button. A table's `<thead>` is data. Setting
|
|
637
|
+
the body into a card away from its own header cost three vertical rules down
|
|
638
|
+
each side, stopped the row dividers short of the box and narrowed the reading
|
|
639
|
+
width — on the densest element on a page, for nothing gained. Full-width
|
|
640
|
+
dividers are what let an eye track a row across.
|
|
641
|
+
|
|
642
|
+
What is kept is the outer radius: `--wave-docs-radius-lg` rather than
|
|
643
|
+
`--wave-docs-radius`, so a table and a code block read as two of one family
|
|
644
|
+
without the table pretending to chrome it has not got.
|
|
645
|
+
|
|
646
|
+
⚠️ AND AN EMPTY HEADER ROW NOW DRAWS NO BAND, WHICH GFM PRODUCES ROUTINELY. A
|
|
647
|
+
GFM table always has a `<thead>` — the delimiter row is what makes it a table —
|
|
648
|
+
so an author who wants a plain two-column list of facts writes `| | |` and gets
|
|
649
|
+
a header of empty `<th>`s, which rendered as a tinted strip with nothing in it.
|
|
650
|
+
`:empty` and not a text check: GFM emits `<th></th>` with no whitespace inside.
|
|
651
|
+
A header with even one named column keeps its band.
|
|
652
|
+
|
|
653
|
+
**And a fence with neither a title nor a language gets no band either.** The
|
|
654
|
+
header row is floored so titled and untitled fences match, which is right when
|
|
655
|
+
there is a label and wrong when there is not: a bare fence had only the copy
|
|
656
|
+
button, adrift in a strip of empty ground. The row is the button now.
|
|
657
|
+
|
|
3
658
|
## 0.10.0
|
|
4
659
|
|
|
5
660
|
### Minor Changes
|