@waveso/docs 0.6.0 → 0.7.1
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 +180 -1
- package/README.md +115 -38
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.js +7 -1
- package/dist/next.d.ts +34 -27
- package/dist/next.js +20 -11
- package/dist/react/hero.d.ts +19 -0
- package/dist/react/hero.js +44 -0
- package/dist/react/layout.d.ts +1 -3
- package/dist/react/layout.js +17 -54
- package/dist/react/nav.d.ts +11 -11
- package/dist/react/nav.js +106 -58
- package/dist/react/next-nav.d.ts +5 -1
- package/dist/react/next-nav.js +4 -2
- package/dist/react/shell-labels.d.ts +8 -2
- package/dist/react/toc.d.ts +13 -3
- package/dist/react/toc.js +17 -10
- package/dist/render.js +2 -1
- package/dist/source.js +1 -1
- package/dist/styles.css +1163 -238
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
package/dist/styles.css
CHANGED
|
@@ -80,6 +80,25 @@
|
|
|
80
80
|
--wave-docs-fg-subtle: oklch(0.54 0.015 262);
|
|
81
81
|
--wave-docs-border: oklch(0.91 0.006 262);
|
|
82
82
|
--wave-docs-border-strong: oklch(0.84 0.008 262);
|
|
83
|
+
/* The hero's grid, and the one place this package paints the brand ramp
|
|
84
|
+
* rather than a neutral.
|
|
85
|
+
*
|
|
86
|
+
* Wave 200 and Wave 300 from `@waveso/ui` verbatim — #AEDDFF and #7BC3FF.
|
|
87
|
+
*
|
|
88
|
+
* ⚠️ THE RIGHT STEP ON THE RAMP DEPENDS ON THE MASK, AND THE MASK CHANGED.
|
|
89
|
+
* It walked up to 300/400 while a radial gradient was holding the middle of
|
|
90
|
+
* the header at a few per cent — the numbers were chosen against a picture
|
|
91
|
+
* that was mostly suppressed. The radial is gone, so the same tokens paint
|
|
92
|
+
* at full strength across the whole header and read far heavier than they
|
|
93
|
+
* did. One step back is the same apparent weight.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ NOT `--wave-docs-border`. The grid is decoration behind a radial mask
|
|
96
|
+
* that leaves it near-invisible where the words are; a border is a boundary
|
|
97
|
+
* a reader has to be able to see. Sharing one token would tie a decorative
|
|
98
|
+
* tint to a functional contrast ratio, and the next change to either would
|
|
99
|
+
* be wrong for the other. */
|
|
100
|
+
--wave-docs-hero-grid: oklch(0.876 0.068 240);
|
|
101
|
+
--wave-docs-hero-grid-strong: oklch(0.793 0.112 245);
|
|
83
102
|
|
|
84
103
|
/* Wave Blue #0074DE, darkened to #006EC8: the brand value clears 4.5:1 on
|
|
85
104
|
* white but only 4.26:1 on `accent-subtle`, which is what the active sidebar
|
|
@@ -125,23 +144,47 @@
|
|
|
125
144
|
|
|
126
145
|
--wave-docs-radius: 0.5rem;
|
|
127
146
|
--wave-docs-radius-sm: 0.375rem;
|
|
128
|
-
/* The shell
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
--wave-docs-header-height: 3.5rem;
|
|
147
|
+
/* The shell. These five plus `--wave-docs-measure` are the whole settable
|
|
148
|
+
* layout surface; the gutter and the drawer width are literals, because each
|
|
149
|
+
* appears once and an ordinary override is already the cleanest tool for
|
|
150
|
+
* them. */
|
|
133
151
|
--wave-docs-sidebar-width: 16rem;
|
|
152
|
+
/* The button the reader sees. The strip around it is `auto` — this plus 4px
|
|
153
|
+
* of padding on every side — so setting this is the whole of resizing the
|
|
154
|
+
* control, and the hit area grows with it rather than being a second number
|
|
155
|
+
* to keep in step. */
|
|
156
|
+
--wave-docs-trigger-width: 1.25rem;
|
|
134
157
|
--wave-docs-toc-width: 15rem;
|
|
135
|
-
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* Where our sticky chrome starts, and the one thing only the host knows.
|
|
161
|
+
*
|
|
162
|
+
* A documentation section inside an application sits under that
|
|
163
|
+
* application's own bar. If theirs is sticky, ours has to start below it —
|
|
164
|
+
* and nothing in this package can measure that. This is the whole
|
|
165
|
+
* mechanical fix for it. A `standalone`/`embedded` mode flag was considered
|
|
166
|
+
* and rejected: it would have been an alias for this one line, which the
|
|
167
|
+
* host has to know the value of either way.
|
|
168
|
+
*
|
|
169
|
+
* ⚠️ `0rem`, NOT `0`, AND THE UNIT IS LOAD-BEARING. It is read inside
|
|
170
|
+
* `calc(100dvh - …)` on both sticky columns, and `calc(100dvh - 0)` is
|
|
171
|
+
* invalid at computed-value time — a unitless zero would not resolve to "no
|
|
172
|
+
* change", it would kill the `max-height` outright.
|
|
173
|
+
*/
|
|
174
|
+
--wave-docs-chrome-offset: 0rem;
|
|
136
175
|
|
|
137
176
|
/* How far below the top of the scrollport an anchored heading parks.
|
|
138
177
|
*
|
|
139
|
-
* Derived, not set
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
178
|
+
* Derived, not set, and one derivation rather than two. It carried a
|
|
179
|
+
* `--wave-docs-bar-height` term as well, because the sidebar used to take a
|
|
180
|
+
* strip shape *above* the content below 64rem and a heading had to clear
|
|
181
|
+
* that too. The sidebar is beside the content at every width now, so
|
|
182
|
+
* nothing of ours is ever over a heading.
|
|
183
|
+
*
|
|
184
|
+
* ⚠️ THE HOST TERM IS NOT OPTIONAL. An earlier draft made the desktop value
|
|
185
|
+
* a flat `1rem`, which parks a heading underneath the host bar of the very
|
|
186
|
+
* persona `--wave-docs-chrome-offset` exists for. */
|
|
187
|
+
--wave-docs-scroll-padding: calc(var(--wave-docs-chrome-offset) + 1rem);
|
|
145
188
|
--wave-docs-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
|
|
146
189
|
Consolas, "Liberation Mono", monospace;
|
|
147
190
|
|
|
@@ -168,8 +211,8 @@
|
|
|
168
211
|
* all, a 1440px viewport produced a ~140-character line.
|
|
169
212
|
*
|
|
170
213
|
* `--wave-docs-measure: none` opts out entirely, for a host whose own
|
|
171
|
-
* shell already constrains the column.
|
|
172
|
-
*
|
|
214
|
+
* shell already constrains the column. One of the six settable layout
|
|
215
|
+
* tokens. */
|
|
173
216
|
--wave-docs-measure: 46rem;
|
|
174
217
|
}
|
|
175
218
|
|
|
@@ -185,6 +228,11 @@
|
|
|
185
228
|
--wave-docs-fg-subtle: oklch(0.665 0.015 265);
|
|
186
229
|
--wave-docs-border: oklch(0.31 0.014 265);
|
|
187
230
|
--wave-docs-border-strong: oklch(0.4 0.016 265);
|
|
231
|
+
/* Wave 900 and Wave 800. The light pair would be a white grid on a dark
|
|
232
|
+
* page — a ramp is not a colour, and the tint has to come from the end
|
|
233
|
+
* that belongs on this ground. */
|
|
234
|
+
--wave-docs-hero-grid: oklch(0.284 0.088 255);
|
|
235
|
+
--wave-docs-hero-grid-strong: oklch(0.351 0.118 255);
|
|
188
236
|
|
|
189
237
|
--wave-docs-accent: oklch(0.72 0.14 248);
|
|
190
238
|
--wave-docs-accent-hover: oklch(0.8 0.12 248);
|
|
@@ -228,6 +276,8 @@
|
|
|
228
276
|
--wave-docs-fg-subtle: oklch(0.665 0.015 265);
|
|
229
277
|
--wave-docs-border: oklch(0.31 0.014 265);
|
|
230
278
|
--wave-docs-border-strong: oklch(0.4 0.016 265);
|
|
279
|
+
--wave-docs-hero-grid: oklch(0.284 0.088 255);
|
|
280
|
+
--wave-docs-hero-grid-strong: oklch(0.351 0.118 255);
|
|
231
281
|
|
|
232
282
|
--wave-docs-accent: oklch(0.72 0.14 248);
|
|
233
283
|
--wave-docs-accent-hover: oklch(0.8 0.12 248);
|
|
@@ -259,6 +309,28 @@
|
|
|
259
309
|
* ------------------------------------------------------------------------ */
|
|
260
310
|
|
|
261
311
|
@layer base {
|
|
312
|
+
/*
|
|
313
|
+
* ⚠️ THE SHELL DEPENDED ON THE HOST SHIPPING A RESET, AND MOST HOSTS DO.
|
|
314
|
+
*
|
|
315
|
+
* `.wave-docs-sidebar__link` is `width: 100%` with `0.5rem` of inline
|
|
316
|
+
* padding. Under `content-box` that is a 272px box in a 256px track, so the
|
|
317
|
+
* external-link icon `justify-content: space-between` pins to the far end
|
|
318
|
+
* lands 8px outside the sidebar and is clipped in half — measured, on the
|
|
319
|
+
* site in `site/`. Tailwind's preflight and every normalize-style reset set
|
|
320
|
+
* `border-box` globally, so the overflow was invisible in every project that
|
|
321
|
+
* has one, and visible on the only site here that ships no CSS at all.
|
|
322
|
+
*
|
|
323
|
+
* Scoped to this package's own namespace rather than to `*`. The prose
|
|
324
|
+
* renders a consumer's components as well as ours, and a global reset
|
|
325
|
+
* arriving inside a stylesheet they imported for their documentation is not
|
|
326
|
+
* ours to write — but every element carrying a `wave-docs-` class is one this
|
|
327
|
+
* package emitted and sized.
|
|
328
|
+
*/
|
|
329
|
+
[class^='wave-docs-'],
|
|
330
|
+
[class*=' wave-docs-'] {
|
|
331
|
+
box-sizing: border-box;
|
|
332
|
+
}
|
|
333
|
+
|
|
262
334
|
/*
|
|
263
335
|
* The ground the ramp is composed against.
|
|
264
336
|
*
|
|
@@ -315,6 +387,8 @@
|
|
|
315
387
|
.wave-docs-sidebar,
|
|
316
388
|
.wave-docs-toc,
|
|
317
389
|
.wave-docs-skip-link,
|
|
390
|
+
.wave-docs-hero,
|
|
391
|
+
.wave-docs-layout__sidebar,
|
|
318
392
|
.wave-docs-search-trigger,
|
|
319
393
|
.wave-docs-search-dialog {
|
|
320
394
|
font-family: var(--wave-docs-font-sans);
|
|
@@ -415,6 +489,13 @@
|
|
|
415
489
|
* shipped, so in practice every consumer's first override was the same
|
|
416
490
|
* container CSS — a default the package should have carried itself. */
|
|
417
491
|
max-width: var(--wave-docs-measure);
|
|
492
|
+
/* ⚠️ AND CENTRED IN ITS TRACK, WHICH IS WHERE THE SHELL'S OLD CAP WENT.
|
|
493
|
+
* The shell used to be capped and centred, which pushed the sidebar off the
|
|
494
|
+
* page's inline start edge and left the closed navigation visible in the
|
|
495
|
+
* centring margin. The reading column is the thing that should not stretch,
|
|
496
|
+
* so it is the thing that is capped — and centring it here is what keeps a
|
|
497
|
+
* 2560px display from pinning the text against the navigation. */
|
|
498
|
+
margin-inline: auto;
|
|
418
499
|
}
|
|
419
500
|
|
|
420
501
|
.wave-docs-prose > * + * {
|
|
@@ -627,6 +708,33 @@
|
|
|
627
708
|
|
|
628
709
|
@layer components {
|
|
629
710
|
.wave-docs-table-scroll {
|
|
711
|
+
/*
|
|
712
|
+
* One cell, three things in it: the table and the two edge overlays below.
|
|
713
|
+
* `grid` rather than `relative` + `absolute` because an absolutely
|
|
714
|
+
* positioned child of a scroll container scrolls with the content, and
|
|
715
|
+
* these have to hold the scrollport's edges.
|
|
716
|
+
*/
|
|
717
|
+
display: grid;
|
|
718
|
+
/*
|
|
719
|
+
* ⚠️ `max-content`, NOT `1fr`, AND THE DIFFERENCE IS WHETHER THE END EDGE
|
|
720
|
+
* EXISTS. A `1fr` track resolves against the scroll *port*, so the cell is
|
|
721
|
+
* narrower than the table it holds — and a sticky element can only travel
|
|
722
|
+
* inside its containing block, so the end overlay scrolled out of view and
|
|
723
|
+
* the shadow measured 0/255 at every scroll position. `max-content` makes
|
|
724
|
+
* the cell as wide as the table, which is the span both edges have to reach
|
|
725
|
+
* across.
|
|
726
|
+
*
|
|
727
|
+
* ⚠️ AND `max-content` IS THE *MIN*, WHICH IS THE ONLY SPELLING THAT DOES
|
|
728
|
+
* BOTH JOBS. A bare `max-content` track is exactly the table's width, so
|
|
729
|
+
* `width: 100%` resolved against that and a table narrower than the reading
|
|
730
|
+
* column stopped filling it — a bordered box with empty space down its
|
|
731
|
+
* inline end. `minmax(100%, max-content)` does not fix it either: a track
|
|
732
|
+
* only grows toward its max when there is free space, and inside a scroll
|
|
733
|
+
* container there is none, so it stayed at the port's width and the end
|
|
734
|
+
* overlay lost its containing block again. As the *floor*, `max-content`
|
|
735
|
+
* wins when the table is wider and `1fr` takes over when it is not.
|
|
736
|
+
*/
|
|
737
|
+
grid-template-columns: minmax(max-content, 1fr);
|
|
630
738
|
overflow-x: auto;
|
|
631
739
|
border: 1px solid var(--wave-docs-border);
|
|
632
740
|
border-radius: var(--wave-docs-radius);
|
|
@@ -667,7 +775,159 @@
|
|
|
667
775
|
outline-offset: 2px;
|
|
668
776
|
}
|
|
669
777
|
|
|
778
|
+
/*
|
|
779
|
+
* ⚠️ THE SAME AFFORDANCE, PAINTED ABOVE THE CONTENT INSTEAD OF BEHIND IT.
|
|
780
|
+
*
|
|
781
|
+
* The gradients above are a *background*, and an element's background is by
|
|
782
|
+
* definition below everything inside it — so the sticky `thead th`, which
|
|
783
|
+
* carries its own opaque fill, and every inline `<code>` chip, which carries
|
|
784
|
+
* another, punched holes in the shadow exactly where a wide table is most
|
|
785
|
+
* likely to be cut off. Found in a screenshot; no test could see it, because
|
|
786
|
+
* every assertion about the shadow was about declarations rather than pixels.
|
|
787
|
+
*
|
|
788
|
+
* These two overlay it. They share the table's grid cell, stick to the
|
|
789
|
+
* scrollport edges, and sit at `z-index: 2` — above `thead th`, which is 1,
|
|
790
|
+
* and above the chips, which are in flow.
|
|
791
|
+
*
|
|
792
|
+
* `opacity: 0` is the base state and it is load-bearing: see the `@supports`
|
|
793
|
+
* block below.
|
|
794
|
+
*/
|
|
795
|
+
.wave-docs-table-scroll::before,
|
|
796
|
+
.wave-docs-table-scroll::after {
|
|
797
|
+
content: '';
|
|
798
|
+
grid-area: 1 / 1;
|
|
799
|
+
position: sticky;
|
|
800
|
+
z-index: 2;
|
|
801
|
+
width: 0.75rem;
|
|
802
|
+
opacity: 0;
|
|
803
|
+
pointer-events: none;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/*
|
|
807
|
+
* ⚠️ THE TAPER IS A MASK, AND THE SHAPE WAS HALF THE ORIGINAL BUG.
|
|
808
|
+
*
|
|
809
|
+
* The background version used `radial-gradient(farthest-side at 0 50%)`,
|
|
810
|
+
* which reads well — soft at the top and bottom, strongest through the middle
|
|
811
|
+
* — and whose falloff is *proportional to the box*. On a tall table that puts
|
|
812
|
+
* the strength at the vertical centre and nothing at the ends: sampled on a
|
|
813
|
+
* real one, 5–9/255 of darkening beside the first body rows and 1/255 beside
|
|
814
|
+
* the header, which is where a reader looks first.
|
|
815
|
+
*
|
|
816
|
+
* So the horizontal falloff is a linear gradient, even down the whole height,
|
|
817
|
+
* and the taper is a mask with a *bounded* ramp. Proportional while the table
|
|
818
|
+
* is short — a quarter of the height at each end, which is the oval the
|
|
819
|
+
* original had — and capped at 5rem once it is tall, so a long table softens
|
|
820
|
+
* over its first and last 80px instead of over its first and last third.
|
|
821
|
+
* Measured at the cap: 0/255 at the very top edge, ~14 across the header row,
|
|
822
|
+
* 24 through the body. Soft where a shadow should be soft, present where a
|
|
823
|
+
* reader is looking.
|
|
824
|
+
*/
|
|
825
|
+
.wave-docs-table-scroll::before,
|
|
826
|
+
.wave-docs-table-scroll::after {
|
|
827
|
+
mask-image: linear-gradient(
|
|
828
|
+
to bottom,
|
|
829
|
+
transparent 0,
|
|
830
|
+
rgb(0 0 0 / 0.55) min(8%, 1.25rem),
|
|
831
|
+
#000 min(24%, 5rem),
|
|
832
|
+
#000 calc(100% - min(24%, 5rem)),
|
|
833
|
+
rgb(0 0 0 / 0.55) calc(100% - min(8%, 1.25rem)),
|
|
834
|
+
transparent 100%
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.wave-docs-table-scroll::before {
|
|
839
|
+
justify-self: start;
|
|
840
|
+
inset-inline-start: 0;
|
|
841
|
+
background: linear-gradient(
|
|
842
|
+
to right,
|
|
843
|
+
var(--wave-docs-scroll-shadow),
|
|
844
|
+
transparent
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.wave-docs-table-scroll::after {
|
|
849
|
+
justify-self: end;
|
|
850
|
+
inset-inline-end: 0;
|
|
851
|
+
background: linear-gradient(
|
|
852
|
+
to left,
|
|
853
|
+
var(--wave-docs-scroll-shadow),
|
|
854
|
+
transparent
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/*
|
|
859
|
+
* ⚠️ AN INACTIVE TIMELINE IS WHAT MAKES THIS CORRECT, NOT JUST CLEVER.
|
|
860
|
+
*
|
|
861
|
+
* A scroll timeline is *inactive* when its scroll container has no scrollable
|
|
862
|
+
* overflow in the axis, and an animation with an inactive timeline does not
|
|
863
|
+
* apply — so the base `opacity: 0` wins and a table that fits shows no shadow
|
|
864
|
+
* at all. No query asks whether it scrolls; the answer falls out of whether
|
|
865
|
+
* there is anything to scroll.
|
|
866
|
+
*
|
|
867
|
+
* ⚠️ `nearest`, NOT `self`. `self` names the element the animation is on, and
|
|
868
|
+
* the element here is a pseudo-element *inside* the scroll container rather
|
|
869
|
+
* than the container itself — so `scroll(self inline)` was inactive at every
|
|
870
|
+
* width, on every table, and the shadow simply never appeared. It looked
|
|
871
|
+
* exactly like the feature being unsupported: the `@supports` matched, both
|
|
872
|
+
* animations were listed on the pseudo, and the computed opacity was 0.
|
|
873
|
+
*
|
|
874
|
+
* Where it does overflow, scroll position alone drives the fade: no listener,
|
|
875
|
+
* no state, no hydration, on a table a reader may well meet before JS lands.
|
|
876
|
+
*
|
|
877
|
+
* Firefox skips this block — the feature is still behind
|
|
878
|
+
* `layout.css.scroll-driven-animations.enabled` as of 152, and is an Interop
|
|
879
|
+
* 2026 priority — and keeps the background gradients above, which is exactly
|
|
880
|
+
* what it renders today. The two are mutually exclusive on purpose: the
|
|
881
|
+
* `background: none` here is what stops a supporting browser darkening each
|
|
882
|
+
* edge twice.
|
|
883
|
+
*/
|
|
884
|
+
@supports (animation-timeline: scroll()) {
|
|
885
|
+
.wave-docs-table-scroll {
|
|
886
|
+
background: none;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.wave-docs-table-scroll::before,
|
|
890
|
+
.wave-docs-table-scroll::after {
|
|
891
|
+
animation-timeline: scroll(nearest inline);
|
|
892
|
+
animation-timing-function: linear;
|
|
893
|
+
animation-fill-mode: both;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.wave-docs-table-scroll::before {
|
|
897
|
+
animation-name: wave-docs-scroll-shadow-start;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.wave-docs-table-scroll::after {
|
|
901
|
+
animation-name: wave-docs-scroll-shadow-end;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/* Absent while the start edge is at rest, present once it is not. */
|
|
906
|
+
@keyframes wave-docs-scroll-shadow-start {
|
|
907
|
+
0%,
|
|
908
|
+
2% {
|
|
909
|
+
opacity: 0;
|
|
910
|
+
}
|
|
911
|
+
8%,
|
|
912
|
+
100% {
|
|
913
|
+
opacity: 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/* Present until the end edge is reached. */
|
|
918
|
+
@keyframes wave-docs-scroll-shadow-end {
|
|
919
|
+
0%,
|
|
920
|
+
92% {
|
|
921
|
+
opacity: 1;
|
|
922
|
+
}
|
|
923
|
+
98%,
|
|
924
|
+
100% {
|
|
925
|
+
opacity: 0;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
670
929
|
.wave-docs-table {
|
|
930
|
+
grid-area: 1 / 1;
|
|
671
931
|
width: 100%;
|
|
672
932
|
/*
|
|
673
933
|
* A FLOOR, NOT A SIZING MODE — and `width: 100%` is not one.
|
|
@@ -732,7 +992,20 @@
|
|
|
732
992
|
white-space: nowrap;
|
|
733
993
|
}
|
|
734
994
|
|
|
735
|
-
|
|
995
|
+
/*
|
|
996
|
+
* ⚠️ `tbody tr + tr` ONLY. `thead` WAS IN THIS SELECTOR AND DOUBLED THE FRAME.
|
|
997
|
+
*
|
|
998
|
+
* The header is the first thing in the table, so its `border-block-start`
|
|
999
|
+
* landed directly against `.wave-docs-table-scroll`'s own border with nothing
|
|
1000
|
+
* between them: measured at the top-left corner, two adjacent rows of
|
|
1001
|
+
* `--wave-docs-border` instead of one, which reads as a thick or doubled
|
|
1002
|
+
* line rather than as a frame.
|
|
1003
|
+
*
|
|
1004
|
+
* The header's separator is the one below it, and that is drawn by the
|
|
1005
|
+
* `inset 0 -1px 0` box-shadow on `thead th` — a shadow rather than a border
|
|
1006
|
+
* so it survives the cell being `position: sticky`.
|
|
1007
|
+
*/
|
|
1008
|
+
.wave-docs-table tbody tr + tr {
|
|
736
1009
|
border-block-start: 1px solid var(--wave-docs-border);
|
|
737
1010
|
}
|
|
738
1011
|
}
|
|
@@ -797,10 +1070,19 @@
|
|
|
797
1070
|
|
|
798
1071
|
.wave-docs-code {
|
|
799
1072
|
position: relative;
|
|
800
|
-
/*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
|
|
1073
|
+
/*
|
|
1074
|
+
* ⚠️ `margin-inline`, NOT `margin`. The `<pre>` inside carries the border
|
|
1075
|
+
* and the background; this figure is only a positioning context, and a
|
|
1076
|
+
* default `<figure>` margin would indent the whole block by 40px — which is
|
|
1077
|
+
* what this rule was written for.
|
|
1078
|
+
*
|
|
1079
|
+
* Zeroing all four also took the *block* margin, and `.wave-docs-prose > *
|
|
1080
|
+
* + *` is the same specificity and declared earlier, so this won. Every
|
|
1081
|
+
* code block sat flush against whatever followed it: two fences touching,
|
|
1082
|
+
* a fence touching the paragraph under it. The flow spacing owns the block
|
|
1083
|
+
* axis; this rule owns the inline one.
|
|
1084
|
+
*/
|
|
1085
|
+
margin-inline: 0;
|
|
804
1086
|
}
|
|
805
1087
|
|
|
806
1088
|
.wave-docs-code__title {
|
|
@@ -895,6 +1177,21 @@
|
|
|
895
1177
|
/* A title bar already reserves the space, so there is nothing to reveal. */
|
|
896
1178
|
.wave-docs-code:has(.wave-docs-code__title) .wave-docs-code__copy {
|
|
897
1179
|
opacity: 1;
|
|
1180
|
+
/*
|
|
1181
|
+
* ⚠️ CENTRED IN THE TITLE BAR, NOT INSET FROM THE FIGURE.
|
|
1182
|
+
*
|
|
1183
|
+
* The button is positioned against the `<figure>`, which is the whole code
|
|
1184
|
+
* block, so a single `top` cannot be right for both shapes. Against a bare
|
|
1185
|
+
* fence, `0.5rem` insets it from the top of the `<pre>`, which is correct.
|
|
1186
|
+
* Against a title bar it measured 8px of space above and **0 below** — the
|
|
1187
|
+
* button sitting flush on the bar's own border, which reads as misaligned
|
|
1188
|
+
* rather than as inset.
|
|
1189
|
+
*
|
|
1190
|
+
* The bar is 40px and the button is 32px, so 4px is the centre. A `top:
|
|
1191
|
+
* 50%` would be wrong here: the percentage resolves against the figure,
|
|
1192
|
+
* which includes the code block below.
|
|
1193
|
+
*/
|
|
1194
|
+
top: 0.25rem;
|
|
898
1195
|
}
|
|
899
1196
|
|
|
900
1197
|
/*
|
|
@@ -1428,6 +1725,89 @@
|
|
|
1428
1725
|
color: var(--wave-docs-fg);
|
|
1429
1726
|
}
|
|
1430
1727
|
|
|
1728
|
+
/*
|
|
1729
|
+
* ⚠️ IT ONLY APPEARS ONCE THERE IS SOMETHING TO GO BACK TO, AND NOTHING IN
|
|
1730
|
+
* JAVASCRIPT DECIDES THAT.
|
|
1731
|
+
*
|
|
1732
|
+
* A scroll-driven animation, so scroll position alone drives it: no listener,
|
|
1733
|
+
* no state, no re-render on every frame of a scroll, and it is right before
|
|
1734
|
+
* hydration rather than after it. `toc` is the smallest client component this
|
|
1735
|
+
* package ships — 900 bytes, and its budget note exists to keep it that way —
|
|
1736
|
+
* so the alternative was to grow the one bundle that should never grow, in
|
|
1737
|
+
* order to recompute a number the compositor already has.
|
|
1738
|
+
*
|
|
1739
|
+
* ⚠️ `root`, NOT `nearest`. Above 80rem `.wave-docs-layout__toc` is itself a
|
|
1740
|
+
* scroll container (`overflow-y: auto`, so a long column of headings scrolls
|
|
1741
|
+
* in place), and `nearest` resolves to it. The animation would then track how
|
|
1742
|
+
* far the reader had scrolled the *table of contents*, which on almost every
|
|
1743
|
+
* page never scrolls at all — an inactive timeline, and a link that never
|
|
1744
|
+
* appears. Same shape as the `nearest`/`self` trap on the table shadow above,
|
|
1745
|
+
* arrived at from the opposite direction.
|
|
1746
|
+
*
|
|
1747
|
+
* ⚠️ AND THE RULE ABOVE IS THE FALLBACK, WHICH IS WHY NOTHING HERE OVERRIDES
|
|
1748
|
+
* IT. Three cases collapse into one: Firefox does not match this `@supports`
|
|
1749
|
+
* yet; a page too short to scroll leaves the timeline inactive; and so does a
|
|
1750
|
+
* host that scrolls an inner pane rather than the document. In all three the
|
|
1751
|
+
* link is simply always present, exactly as it was before this block existed.
|
|
1752
|
+
* Nothing hides a control on the strength of a feature the engine did not run.
|
|
1753
|
+
*
|
|
1754
|
+
* `dvh` rather than `px`: the link is redundant while the top of the document
|
|
1755
|
+
* is still on screen, and "still on screen" is a fraction of the viewport
|
|
1756
|
+
* rather than a constant. On a 900px window it fades in between 225px and
|
|
1757
|
+
* 315px of scroll. `dvh` because this sheet uses no `vh` anywhere, and the
|
|
1758
|
+
* reason it does not — the retracted mobile URL bar — costs nothing here: the
|
|
1759
|
+
* column this link lives in is `display: none` below 80rem.
|
|
1760
|
+
*
|
|
1761
|
+
* No `prefers-reduced-motion` gate, deliberately. That setting is about things
|
|
1762
|
+
* that move under a reader who did not ask them to; this is opacity, and the
|
|
1763
|
+
* reader's own scrolling is its clock.
|
|
1764
|
+
*/
|
|
1765
|
+
@supports (animation-timeline: scroll()) {
|
|
1766
|
+
.wave-docs-toc__top {
|
|
1767
|
+
animation-timeline: scroll(root block);
|
|
1768
|
+
animation-timing-function: linear;
|
|
1769
|
+
animation-fill-mode: both;
|
|
1770
|
+
animation-name: wave-docs-toc-top-reveal;
|
|
1771
|
+
animation-range: 25dvh 35dvh;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/*
|
|
1776
|
+
* ⚠️ `visibility` KEEPS A LINK NOBODY CAN SEE OUT OF THE TAB ORDER, AND THE
|
|
1777
|
+
* MIDDLE FRAME IS WHAT DECIDES WHEN IT REJOINS.
|
|
1778
|
+
*
|
|
1779
|
+
* `opacity` alone leaves a fully focusable, fully clickable link sitting
|
|
1780
|
+
* invisibly at the foot of the rail: Tab reaches it and the focus ring is
|
|
1781
|
+
* drawn around nothing.
|
|
1782
|
+
*
|
|
1783
|
+
* `visibility` interpolates as a step with one exception — across an interval
|
|
1784
|
+
* where *either* endpoint is `visible` it is `visible` the whole way. Two
|
|
1785
|
+
* frames would therefore flip it one pixel past the threshold and hand the
|
|
1786
|
+
* link back to the tab order at an opacity of almost zero, which is the same
|
|
1787
|
+
* defect a frame later. Two adjacent `hidden` frames have no `visible`
|
|
1788
|
+
* endpoint between them, the exception does not apply, and the step lands at
|
|
1789
|
+
* 60% — where the link is legible.
|
|
1790
|
+
*
|
|
1791
|
+
* That middle frame sits exactly on the linear ramp, so it changes the fade
|
|
1792
|
+
* by nothing at all. Being the second `hidden` is its whole job.
|
|
1793
|
+
*/
|
|
1794
|
+
@keyframes wave-docs-toc-top-reveal {
|
|
1795
|
+
0% {
|
|
1796
|
+
opacity: 0;
|
|
1797
|
+
visibility: hidden;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
60% {
|
|
1801
|
+
opacity: 0.6;
|
|
1802
|
+
visibility: hidden;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
100% {
|
|
1806
|
+
opacity: 1;
|
|
1807
|
+
visibility: visible;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1431
1811
|
.wave-docs-sidebar__external {
|
|
1432
1812
|
/* Baseline-ish against the label, and never a flex item that grows. */
|
|
1433
1813
|
margin-inline-start: 0.25rem;
|
|
@@ -1436,6 +1816,18 @@
|
|
|
1436
1816
|
color: var(--wave-docs-fg-subtle);
|
|
1437
1817
|
}
|
|
1438
1818
|
|
|
1819
|
+
.wave-docs-toc__title {
|
|
1820
|
+
margin: 0 0 0.5rem;
|
|
1821
|
+
/* Aligns with the link text below it. The links carry a 2px rail on their
|
|
1822
|
+
* inline start and 0.75rem of padding inside it; this has no rail, so it
|
|
1823
|
+
* takes the rail's width as padding instead. */
|
|
1824
|
+
padding-inline-start: calc(0.75rem + 2px);
|
|
1825
|
+
color: var(--wave-docs-fg);
|
|
1826
|
+
font-size: 0.8125rem;
|
|
1827
|
+
font-weight: 600;
|
|
1828
|
+
letter-spacing: 0.01em;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1439
1831
|
.wave-docs-toc__link:hover {
|
|
1440
1832
|
color: var(--wave-docs-fg);
|
|
1441
1833
|
border-inline-start-color: var(--wave-docs-border-strong);
|
|
@@ -1457,8 +1849,8 @@
|
|
|
1457
1849
|
/* ---------------------------------------------------------------------------
|
|
1458
1850
|
* Layout
|
|
1459
1851
|
*
|
|
1460
|
-
* The shell `docs.Layout` renders. Every class here is
|
|
1461
|
-
*
|
|
1852
|
+
* The shell `docs.Layout` renders. Every class here is public API from 0.3.0
|
|
1853
|
+
* and changes only in a release that carries the migration.
|
|
1462
1854
|
*
|
|
1463
1855
|
* These are the first width-based media queries in this file. Before them there
|
|
1464
1856
|
* were none at all — every `@media` was `prefers-color-scheme`,
|
|
@@ -1468,47 +1860,95 @@
|
|
|
1468
1860
|
|
|
1469
1861
|
@layer components {
|
|
1470
1862
|
/*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1863
|
+
* ONE SIDEBAR, AT EVERY WIDTH, AND IT IS A SHELL OF TWO.
|
|
1864
|
+
*
|
|
1865
|
+
* .wave-docs-shell the query container
|
|
1866
|
+
* └─ .wave-docs-layout the grid
|
|
1867
|
+
* ├─ .wave-docs-layout__sidebar paints nothing, and moves
|
|
1868
|
+
* │ ├─ …__sidebar-nav the surface, and the one border
|
|
1869
|
+
* │ └─ …__sidebar-trigger the strip — paints nothing at rest
|
|
1870
|
+
* ├─ .wave-docs-layout__sidebar-scrim
|
|
1871
|
+
* ├─ .wave-docs-layout__main
|
|
1872
|
+
* └─ .wave-docs-layout__toc
|
|
1873
|
+
*
|
|
1874
|
+
* There is no drawer, no dialog, no second control and no second copy of the
|
|
1875
|
+
* tree. Pressing the trigger translates the sidebar; the trigger rides on the
|
|
1876
|
+
* navigation's outer edge because it is the next flex item, not because a
|
|
1877
|
+
* number says so.
|
|
1878
|
+
*
|
|
1879
|
+
* ⚠️ `@container`, AND EVERY WIDTH-BASED `@media` IS GONE WITH IT.
|
|
1880
|
+
*
|
|
1881
|
+
* This package is mounted at `/docs` inside applications that own the rest of
|
|
1882
|
+
* the page. `@media` asks how wide the *screen* is, which is the wrong
|
|
1883
|
+
* question: a host who puts this in a 700px panel on a 1920px monitor gets
|
|
1884
|
+
* the wide layout and a reading column of about 60px. That is the same
|
|
1885
|
+
* failure as the fixed header — a collision arriving from a direction no
|
|
1886
|
+
* breakpoint can see. `@container` asks how wide the box we were *given* is,
|
|
1887
|
+
* which is the question with an answer.
|
|
1888
|
+
*
|
|
1889
|
+
* ⚠️ AND THAT IS WHY THERE IS AN OUTER ELEMENT. A container query never
|
|
1890
|
+
* matches its own container, so with `container-type` on the grid, no rule
|
|
1891
|
+
* inside a query could touch `grid-template-columns` — which is exactly the
|
|
1892
|
+
* declaration the layout has to change. One wrapper, and every rule below is
|
|
1893
|
+
* addressable.
|
|
1473
1894
|
*/
|
|
1895
|
+
/*
|
|
1896
|
+
* ⚠️ NO `max-width` AND NO `margin-inline: auto` HERE, AND THAT IS A FIX
|
|
1897
|
+
* RATHER THAN AN OMISSION.
|
|
1898
|
+
*
|
|
1899
|
+
* This box was capped at `--wave-docs-shell-width` and centred. Below the cap
|
|
1900
|
+
* the layout's inline start edge and the viewport's are the same place, so
|
|
1901
|
+
* `translate: calc(var(--wave-docs-trigger-width) - 100%)` put the closed
|
|
1902
|
+
* navigation off the screen. Above it they are not: on a 2000px viewport the
|
|
1903
|
+
* shell starts at 200, and the closed navigation parked at −56…200 — two
|
|
1904
|
+
* hundred pixels of it still on screen, beside a trigger sitting 200px in
|
|
1905
|
+
* from the edge. Measured.
|
|
1906
|
+
*
|
|
1907
|
+
* The sidebar owns the page's inline start edge at every width now, so
|
|
1908
|
+
* "closed" means off the screen by construction rather than by coincidence.
|
|
1909
|
+
* What was capped is capped where it belongs: `.wave-docs-prose` holds
|
|
1910
|
+
* `--wave-docs-measure` and centres itself in the track, so a 2560px display
|
|
1911
|
+
* gets a reading column between its navigation and its table of contents
|
|
1912
|
+
* instead of text pinned against one of them.
|
|
1913
|
+
*/
|
|
1914
|
+
.wave-docs-shell {
|
|
1915
|
+
container-type: inline-size;
|
|
1916
|
+
container-name: wave-docs;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1474
1919
|
.wave-docs-layout {
|
|
1920
|
+
/* Absolutely positioned children of ours resolve against this, so nothing
|
|
1921
|
+
* of ours resolves against the viewport and nothing of ours can land on a
|
|
1922
|
+
* host's chrome. */
|
|
1923
|
+
position: relative;
|
|
1475
1924
|
display: grid;
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1925
|
+
/*
|
|
1926
|
+
* ⚠️ `auto`, AND THE SIDEBAR DECIDES WHAT IT MEANS. The track takes the
|
|
1927
|
+
* sidebar's *outer* size, and the sidebar sets that with a negative margin:
|
|
1928
|
+
* 44px whenever the navigation is off the page or covering the article,
|
|
1929
|
+
* its full width only when it is meant to sit beside it. So one track
|
|
1930
|
+
* expresses both modes and both states, and the layout never has to know
|
|
1931
|
+
* which it is in.
|
|
1932
|
+
*
|
|
1933
|
+
* A fixed `var(--wave-docs-trigger-width)` here was the first attempt, and
|
|
1934
|
+
* it could not grow — push mode silently behaved exactly like cover mode.
|
|
1935
|
+
* Measured at 1440px: the article started at 68px in both states.
|
|
1936
|
+
*
|
|
1937
|
+
* In cover mode the article's measure is untouched by the toggle, because
|
|
1938
|
+
* the track never changes: 330px at 390px whether the navigation is open or
|
|
1939
|
+
* shut. In push mode it necessarily changes — that is what push means, and
|
|
1940
|
+
* it is why cover exists for the widths where 330px is all there is.
|
|
1941
|
+
*/
|
|
1942
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1943
|
+
gap: 0 0.5rem;
|
|
1944
|
+
/* Nothing on the inline start: the trigger's track owns that edge. */
|
|
1945
|
+
padding-inline: 0 0.5rem;
|
|
1946
|
+
/* Without this, `max-width` at the cap applies to the *content* box and the
|
|
1947
|
+
* shell renders 48px wider than the token says — measured 1648 against a
|
|
1948
|
+
* declared 1600. */
|
|
1485
1949
|
box-sizing: border-box;
|
|
1486
1950
|
}
|
|
1487
1951
|
|
|
1488
|
-
@media (min-width: 30rem) {
|
|
1489
|
-
.wave-docs-layout {
|
|
1490
|
-
padding-inline: 1.5rem;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
/*
|
|
1495
|
-
* ⚠️ `contents`, NOT `none`, AND THAT IS NOT A STYLE PREFERENCE.
|
|
1496
|
-
*
|
|
1497
|
-
* The drawer `<dialog>` lives inside this wrapper, so that one nav DOM can
|
|
1498
|
-
* serve both breakpoints. An element inside a `display: none` subtree
|
|
1499
|
-
* generates no boxes at all — including a dialog promoted to the top layer —
|
|
1500
|
-
* so `showModal()` would open a drawer that paints nothing, on every
|
|
1501
|
-
* viewport narrower than 64rem. Which is to say: on the phones this drawer
|
|
1502
|
-
* exists for.
|
|
1503
|
-
*
|
|
1504
|
-
* `contents` makes the wrapper generate no box of its own, so it reserves no
|
|
1505
|
-
* grid track, while the closed dialog inside stays hidden by the UA's own
|
|
1506
|
-
* `dialog:not([open]) { display: none }`.
|
|
1507
|
-
*/
|
|
1508
|
-
.wave-docs-layout__sidebar {
|
|
1509
|
-
display: contents;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
1952
|
.wave-docs-layout__toc {
|
|
1513
1953
|
display: none;
|
|
1514
1954
|
}
|
|
@@ -1526,259 +1966,713 @@
|
|
|
1526
1966
|
* and removing `min-width: 0` alone passes; only removing both reproduces the
|
|
1527
1967
|
* overflow. They are kept as a pair because they fail in opposite directions
|
|
1528
1968
|
* — the track constraint protects a child that does not carry `min-width`,
|
|
1529
|
-
* and `min-width: 0` protects a track someone respells
|
|
1530
|
-
* that is left looks arbitrary without the other beside it.
|
|
1531
|
-
*
|
|
1532
|
-
* No unit test can see any of this: the bug needs a wide child and a real
|
|
1533
|
-
* layout engine, and jsdom reports every width as 0.
|
|
1534
|
-
* `src/styles.browser.test.ts` is what stops a "simplification" here from
|
|
1535
|
-
* reintroducing it silently.
|
|
1969
|
+
* and `min-width: 0` protects a track someone respells.
|
|
1536
1970
|
*/
|
|
1537
1971
|
.wave-docs-layout__main {
|
|
1538
1972
|
min-width: 0;
|
|
1539
1973
|
padding-block: 2rem 4rem;
|
|
1974
|
+
/*
|
|
1975
|
+
* ⚠️ EVERY `z-index` IN THE ARTICLE IS TRAPPED HERE, AND ONE OF THEM WAS
|
|
1976
|
+
* PUNCHING THROUGH THE SCRIM.
|
|
1977
|
+
*
|
|
1978
|
+
* A sticky table header is `z-index: 1`, and so is the scrim. Same stacking
|
|
1979
|
+
* context, equal z-index, and the header comes later in the document — so
|
|
1980
|
+
* with the navigation open over a table on a phone, the header row was the
|
|
1981
|
+
* one thing on the page that stayed bright. Measured on the installation
|
|
1982
|
+
* page.
|
|
1983
|
+
*
|
|
1984
|
+
* Raising the scrim would have fixed that one element and left the next
|
|
1985
|
+
* one. `isolation: isolate` makes the article its own stacking context, so
|
|
1986
|
+
* nothing inside it — ours or a consumer's own content — can be compared
|
|
1987
|
+
* against the scrim at all. The article resolves as a whole, at one layer,
|
|
1988
|
+
* underneath.
|
|
1989
|
+
*/
|
|
1990
|
+
isolation: isolate;
|
|
1540
1991
|
}
|
|
1541
1992
|
|
|
1542
|
-
/*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1993
|
+
/* -------------------------------------------------------------------------
|
|
1994
|
+
* The hero
|
|
1995
|
+
*
|
|
1996
|
+
* A page header a landing page opts into by declaring `actions` in its
|
|
1997
|
+
* frontmatter. Every other page renders none of this.
|
|
1998
|
+
* ---------------------------------------------------------------------- */
|
|
1549
1999
|
|
|
2000
|
+
.wave-docs-hero {
|
|
2001
|
+
position: relative;
|
|
1550
2002
|
/*
|
|
1551
|
-
* ⚠️
|
|
2003
|
+
* ⚠️ BOTH, AND FOR TWO DIFFERENT REASONS. `isolate` keeps the grid's
|
|
2004
|
+
* `z-index: -1` inside the hero instead of dropping it behind the page;
|
|
2005
|
+
* `clip` holds the 150% rotated layer inside the header rather than letting
|
|
2006
|
+
* it run down the article. `clip` rather than `hidden` so no scroll
|
|
2007
|
+
* container is created and nothing sticky below is affected.
|
|
2008
|
+
*/
|
|
2009
|
+
isolation: isolate;
|
|
2010
|
+
overflow: clip;
|
|
2011
|
+
/*
|
|
2012
|
+
* ⚠️ THE NEGATIVE MARGIN CANCELS `.wave-docs-layout__main`'s
|
|
2013
|
+
* `padding-block-start`, AND THE PADDING PUTS IT BACK INSIDE.
|
|
1552
2014
|
*
|
|
1553
|
-
*
|
|
1554
|
-
* the
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
* someone "fixes" that by rendering a second sidebar outside the dialog,
|
|
1559
|
-
* every page ships its nav twice and a screen-reader user gets two
|
|
1560
|
-
* identical navigation landmarks.
|
|
2015
|
+
* The grid is meant to reach the top of the article column. Left in the
|
|
2016
|
+
* flow the hero starts 2rem down — measured at y=32 against a main at y=0 —
|
|
2017
|
+
* so the pattern began with a band of blank page above it. Full-bleed
|
|
2018
|
+
* upwards, and the space the header wants is its own padding rather than
|
|
2019
|
+
* its parent's.
|
|
1561
2020
|
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
* narrow one.
|
|
2021
|
+
* Horizontally there is nothing to cancel: `__main` carries no inline
|
|
2022
|
+
* padding, so the hero already spans it exactly.
|
|
1565
2023
|
*/
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2024
|
+
margin-block-start: -2rem;
|
|
2025
|
+
margin-block-end: 3rem;
|
|
2026
|
+
padding-block: 5rem 3.5rem;
|
|
2027
|
+
}
|
|
1569
2028
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
2029
|
+
/*
|
|
2030
|
+
* ⚠️ FOUR GRADIENTS, NOT AN SVG. The reference for this is a 100px SVG tile
|
|
2031
|
+
* of twenty stroked lines, inlined as a data URI. Drawn as gradients it is
|
|
2032
|
+
* the same picture with three properties that matter more than the fidelity:
|
|
2033
|
+
* not one byte of data URI in the stylesheet, the line colour is a token so
|
|
2034
|
+
* it follows the theme instead of being hardcoded black, and the tile is two
|
|
2035
|
+
* numbers rather than twenty coordinates.
|
|
2036
|
+
*
|
|
2037
|
+
* The grid is 20px with every fifth line emphasised, offset 15px so the
|
|
2038
|
+
* strong lines land at 15, 115, 215 — the tile boundary the SVG drew at 15.5
|
|
2039
|
+
* with a 1px stroke. Strong pairs are listed first because a background image
|
|
2040
|
+
* list paints first-on-top.
|
|
2041
|
+
*/
|
|
2042
|
+
.wave-docs-hero::before {
|
|
2043
|
+
content: '';
|
|
2044
|
+
position: absolute;
|
|
2045
|
+
/*
|
|
2046
|
+
* Bigger than the box and offset by a quarter, so a 3° rotation cannot
|
|
2047
|
+
* expose a corner. The overflow is clipped by the header.
|
|
2048
|
+
*/
|
|
2049
|
+
inset-block-start: -25%;
|
|
2050
|
+
inset-inline-start: -25%;
|
|
2051
|
+
width: 150%;
|
|
2052
|
+
height: 150%;
|
|
2053
|
+
/* Below the scrims, which are at -1. */
|
|
2054
|
+
z-index: -2;
|
|
2055
|
+
pointer-events: none;
|
|
2056
|
+
rotate: -3deg;
|
|
2057
|
+
background-image:
|
|
2058
|
+
repeating-linear-gradient(
|
|
2059
|
+
to right,
|
|
2060
|
+
transparent 0 15px,
|
|
2061
|
+
var(--wave-docs-hero-grid-strong) 15px 16px,
|
|
2062
|
+
transparent 16px 100px
|
|
2063
|
+
),
|
|
2064
|
+
repeating-linear-gradient(
|
|
2065
|
+
to bottom,
|
|
2066
|
+
transparent 0 15px,
|
|
2067
|
+
var(--wave-docs-hero-grid-strong) 15px 16px,
|
|
2068
|
+
transparent 16px 100px
|
|
2069
|
+
),
|
|
2070
|
+
repeating-linear-gradient(
|
|
2071
|
+
to right,
|
|
2072
|
+
transparent 0 15px,
|
|
2073
|
+
var(--wave-docs-hero-grid) 15px 16px,
|
|
2074
|
+
transparent 16px 20px
|
|
2075
|
+
),
|
|
2076
|
+
repeating-linear-gradient(
|
|
2077
|
+
to bottom,
|
|
2078
|
+
transparent 0 15px,
|
|
2079
|
+
var(--wave-docs-hero-grid) 15px 16px,
|
|
2080
|
+
transparent 16px 20px
|
|
2081
|
+
);
|
|
1588
2082
|
}
|
|
1589
2083
|
|
|
1590
|
-
|
|
2084
|
+
/*
|
|
2085
|
+
* ⚠️ THREE SCRIMS IN THE PAGE'S OWN COLOUR, PAINTED OVER THE GRID — NOT A
|
|
2086
|
+
* MASK.
|
|
2087
|
+
*
|
|
2088
|
+
* Every earlier version of this masked the grid layer: two or three gradients
|
|
2089
|
+
* multiplied with `mask-composite: intersect`. It worked, and it was never
|
|
2090
|
+
* smooth. Mask layers multiply *alphas*, so the falloffs compound — two
|
|
2091
|
+
* gentle ramps crossing make a hard shoulder, and one arithmetic slip turns
|
|
2092
|
+
* the whole thing into a ring. Painting the page colour on top instead is
|
|
2093
|
+
* ordinary alpha compositing of a solid: the ramps blend rather than
|
|
2094
|
+
* multiply, and each one can be reasoned about on its own.
|
|
2095
|
+
*
|
|
2096
|
+
* It also means the percentages are of the header, not of a layer 150% its
|
|
2097
|
+
* size — which is what three passes went wrong on.
|
|
2098
|
+
*
|
|
2099
|
+
* A separate element from the grid because this one must not rotate. The
|
|
2100
|
+
* grid is at −3°; a scrim tilted with it would put its own edge on a slant.
|
|
2101
|
+
*
|
|
2102
|
+
* Painted first-on-top, so the order below reads outermost-last:
|
|
2103
|
+
*
|
|
2104
|
+
* 1. the reading scrim — a soft ellipse over the words
|
|
2105
|
+
* 2. the bottom fade — nothing at the top, solid before the prose
|
|
2106
|
+
*
|
|
2107
|
+
* The corner vignette is not among them: it is the `box-shadow` below,
|
|
2108
|
+
* because a gradient cannot be given a corner radius and this shape needs
|
|
2109
|
+
* one.
|
|
2110
|
+
*/
|
|
2111
|
+
.wave-docs-hero::after {
|
|
2112
|
+
content: '';
|
|
2113
|
+
position: absolute;
|
|
2114
|
+
inset: 0;
|
|
2115
|
+
z-index: -1;
|
|
2116
|
+
pointer-events: none;
|
|
2117
|
+
background-image:
|
|
2118
|
+
radial-gradient(
|
|
2119
|
+
ellipse 52% 58% at 38% 48%,
|
|
2120
|
+
var(--wave-docs-bg) 0 25%,
|
|
2121
|
+
transparent 80%
|
|
2122
|
+
),
|
|
2123
|
+
linear-gradient(to bottom, transparent 40%, var(--wave-docs-bg) 90%);
|
|
2124
|
+
/*
|
|
2125
|
+
* ⚠️ THE VIGNETTE IS A SHADOW, NOT A RADIAL — BECAUSE A RADIAL HAS NO
|
|
2126
|
+
* CORNER RADIUS.
|
|
2127
|
+
*
|
|
2128
|
+
* `radial-gradient` is only ever a circle or an ellipse: its shape is two
|
|
2129
|
+
* radii and nothing else, so an elliptical vignette pinches the middle of
|
|
2130
|
+
* every edge and opens the corners. An inset `box-shadow` with blur and
|
|
2131
|
+
* spread fades inward from a *rounded rectangle*, which is the shape this
|
|
2132
|
+
* wants — and `border-radius` is the knob the gradient never had.
|
|
2133
|
+
*
|
|
2134
|
+
* ⚠️ `inset: -4rem`, SO THE BOX'S OWN CORNERS FALL OUTSIDE THE HEADER. One
|
|
2135
|
+
* `border-radius` sets both the shape the shadow fades from and the shape
|
|
2136
|
+
* the box is clipped to; oversizing the box puts the clip out of sight and
|
|
2137
|
+
* leaves only the inner shape doing any work.
|
|
2138
|
+
*
|
|
2139
|
+
* `corner-shape` turns that rounded rectangle into a true squircle. It is
|
|
2140
|
+
* Chrome 139+ and degrades to an ordinary radius everywhere else, which is
|
|
2141
|
+
* the same picture with slightly rounder corners — a progressive
|
|
2142
|
+
* enhancement in the honest sense rather than a fallback nobody checked.
|
|
2143
|
+
*/
|
|
2144
|
+
inset: -5rem;
|
|
2145
|
+
border-radius: 14rem;
|
|
2146
|
+
corner-shape: superellipse(3);
|
|
1591
2147
|
/*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
2148
|
+
* ⚠️ THE SPREAD HAS TO CLEAR THE OVERHANG BEFORE IT DOES ANY WORK. The box
|
|
2149
|
+
* sits 5rem outside the header on every side, so a spread of 5rem is solid
|
|
2150
|
+
* only as far as the header's rim and the whole fade happens out of sight.
|
|
2151
|
+
* 9rem puts the solid band 4rem inside, and the 10rem blur carries it in
|
|
2152
|
+
* from there.
|
|
1596
2153
|
*/
|
|
1597
|
-
|
|
1598
|
-
grid-template-columns:
|
|
1599
|
-
var(--wave-docs-sidebar-width)
|
|
1600
|
-
minmax(0, 1fr)
|
|
1601
|
-
var(--wave-docs-toc-width);
|
|
1602
|
-
}
|
|
2154
|
+
box-shadow: inset 0 0 10rem 9rem var(--wave-docs-bg);
|
|
1603
2155
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2156
|
+
/*
|
|
2157
|
+
* ⚠️ EVERY PERCENTAGE ABOVE IS OF THIS LAYER, NOT OF THE HEADER — AND THE
|
|
2158
|
+
* LAYER IS HALF AS BIG AGAIN.
|
|
2159
|
+
*
|
|
2160
|
+
* The header occupies the middle third of it, from 33% to 67% on both axes.
|
|
2161
|
+
* Written as `125% 100%` the radial's edge landed a long way outside the
|
|
2162
|
+
* header, so what showed inside it was all near-centre: 15px of blank along
|
|
2163
|
+
* every edge before the pattern faded up, measured. `83%` and `67%` are
|
|
2164
|
+
* those numbers divided by the 1.5 — the same ellipse, expressed against
|
|
2165
|
+
* the box it is actually seen through.
|
|
2166
|
+
*
|
|
2167
|
+
* The two linear layers are the edge fade, and they are why the pattern
|
|
2168
|
+
* dissolves into the page on all four sides instead of being cut off by
|
|
2169
|
+
* `overflow: clip`. Each one is transparent exactly at the header's edge —
|
|
2170
|
+
* 33% and 67% of the layer — and opaque a few per cent inside it, so the
|
|
2171
|
+
* ramp happens within the header rather than outside where nothing is seen.
|
|
2172
|
+
* The vertical one is asymmetric on purpose: it holds full only to the
|
|
2173
|
+
* midpoint before falling away, so the grid has faded out well before the
|
|
2174
|
+
* prose begins.
|
|
2175
|
+
*
|
|
2176
|
+
* ⚠️ THE RADIAL AND THESE PULL IN OPPOSITE DIRECTIONS, WHICH IS THE POINT.
|
|
2177
|
+
* The radial is transparent at the centre and opaque outward; these are
|
|
2178
|
+
* opaque inside and transparent at the rim. Multiplied, what survives is a
|
|
2179
|
+
* band between the two — a halo around the words that fades at the page
|
|
2180
|
+
* and fades again where the reading starts.
|
|
2181
|
+
*/
|
|
1616
2182
|
}
|
|
1617
2183
|
|
|
1618
|
-
/*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2184
|
+
/*
|
|
2185
|
+
* The heading, and it is the page's `<h1>` — `render` stands its own down
|
|
2186
|
+
* when a page declares `actions`. Sized here rather than inherited from
|
|
2187
|
+
* `.wave-docs-prose h1`, because it is outside the prose.
|
|
2188
|
+
*/
|
|
2189
|
+
/*
|
|
2190
|
+
* The words, aligned with the prose beneath them. The header itself spans the
|
|
2191
|
+
* whole article track so the grid behind it is full-bleed; this is what keeps
|
|
2192
|
+
* the title from sitting 70px left of the first paragraph.
|
|
2193
|
+
*/
|
|
2194
|
+
.wave-docs-hero__body {
|
|
2195
|
+
max-width: var(--wave-docs-measure);
|
|
2196
|
+
margin-inline: auto;
|
|
1626
2197
|
}
|
|
1627
2198
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2199
|
+
.wave-docs-hero__title {
|
|
2200
|
+
margin: 0;
|
|
2201
|
+
color: var(--wave-docs-fg);
|
|
2202
|
+
font-size: clamp(2.25rem, 1.5rem + 3vw, 3.5rem);
|
|
2203
|
+
font-weight: 700;
|
|
2204
|
+
line-height: 1.1;
|
|
2205
|
+
letter-spacing: -0.02em;
|
|
2206
|
+
text-wrap: balance;
|
|
2207
|
+
}
|
|
1631
2208
|
|
|
1632
|
-
.
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
height:
|
|
1639
|
-
|
|
1640
|
-
border-block-end: 1px solid var(--wave-docs-border);
|
|
2209
|
+
/* `frontmatter.description`, which is otherwise only a `<meta>` tag. */
|
|
2210
|
+
.wave-docs-hero__tagline {
|
|
2211
|
+
margin: 1rem 0 0;
|
|
2212
|
+
max-width: 34rem;
|
|
2213
|
+
color: var(--wave-docs-fg-muted);
|
|
2214
|
+
font-size: 1.125rem;
|
|
2215
|
+
line-height: 1.6;
|
|
2216
|
+
text-wrap: pretty;
|
|
1641
2217
|
}
|
|
1642
2218
|
|
|
1643
|
-
.wave-docs-
|
|
2219
|
+
.wave-docs-hero__actions {
|
|
1644
2220
|
display: flex;
|
|
1645
|
-
|
|
2221
|
+
flex-wrap: wrap;
|
|
1646
2222
|
gap: 0.75rem;
|
|
1647
|
-
|
|
1648
|
-
/* Matches the grid's own cap and gutters, so the brand lines up with the
|
|
1649
|
-
* sidebar and the actions with the TOC rather than floating past both. */
|
|
1650
|
-
max-width: var(--wave-docs-shell-width);
|
|
1651
|
-
margin-inline: auto;
|
|
1652
|
-
padding-inline: 1rem;
|
|
1653
|
-
box-sizing: border-box;
|
|
2223
|
+
margin-block-start: 2rem;
|
|
1654
2224
|
}
|
|
1655
2225
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2226
|
+
.wave-docs-hero__action {
|
|
2227
|
+
display: inline-flex;
|
|
2228
|
+
align-items: center;
|
|
2229
|
+
/* 44px tall: the same minimum the sidebar's trigger answers to. */
|
|
2230
|
+
min-height: 2.75rem;
|
|
2231
|
+
padding-inline: 1.25rem;
|
|
2232
|
+
border: 1px solid transparent;
|
|
2233
|
+
border-radius: var(--wave-docs-radius);
|
|
2234
|
+
font-size: 0.9375rem;
|
|
2235
|
+
font-weight: 600;
|
|
2236
|
+
text-decoration: none;
|
|
1660
2237
|
}
|
|
1661
2238
|
|
|
1662
|
-
.wave-docs-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
* product name would otherwise push search off the right edge. */
|
|
1666
|
-
min-width: 0;
|
|
1667
|
-
overflow: hidden;
|
|
1668
|
-
text-overflow: ellipsis;
|
|
1669
|
-
white-space: nowrap;
|
|
2239
|
+
.wave-docs-hero__action[data-variant='primary'] {
|
|
2240
|
+
background-color: var(--wave-docs-accent);
|
|
2241
|
+
color: var(--wave-docs-accent-fg);
|
|
1670
2242
|
}
|
|
1671
2243
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
* the title that would not exist. */
|
|
1675
|
-
.wave-docs-layout__search {
|
|
1676
|
-
margin-inline-start: auto;
|
|
2244
|
+
.wave-docs-hero__action[data-variant='primary']:hover {
|
|
2245
|
+
background-color: var(--wave-docs-accent-hover);
|
|
1677
2246
|
}
|
|
1678
2247
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2248
|
+
/*
|
|
2249
|
+
* The tinted one. `accent-subtle` is the same surface the active sidebar link
|
|
2250
|
+
* sits on, and its 4.5:1 pair is `accent` — so this is a contrast ratio the
|
|
2251
|
+
* token test already checks rather than a new pairing to argue about.
|
|
2252
|
+
*/
|
|
2253
|
+
.wave-docs-hero__action[data-variant='secondary'] {
|
|
2254
|
+
background-color: var(--wave-docs-accent-subtle);
|
|
2255
|
+
color: var(--wave-docs-accent);
|
|
1686
2256
|
}
|
|
1687
2257
|
|
|
1688
|
-
.wave-docs-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2258
|
+
.wave-docs-hero__action[data-variant='secondary']:hover {
|
|
2259
|
+
border-color: var(--wave-docs-accent);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
.wave-docs-hero__action:focus-visible {
|
|
2263
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
2264
|
+
outline-offset: 2px;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2268
|
+
.wave-docs-hero__action {
|
|
2269
|
+
transition:
|
|
2270
|
+
background-color 120ms ease-out,
|
|
2271
|
+
border-color 120ms ease-out;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/* -------------------------------------------------------------------------
|
|
2276
|
+
* The sidebar
|
|
2277
|
+
* ---------------------------------------------------------------------- */
|
|
2278
|
+
|
|
2279
|
+
/*
|
|
2280
|
+
* It paints nothing. The navigation carries the surface and the border, the
|
|
2281
|
+
* trigger carries neither — so there is no third surface to keep in step with
|
|
2282
|
+
* the other two, which is what made the strip read as filled every previous
|
|
2283
|
+
* time this was drawn.
|
|
2284
|
+
*/
|
|
2285
|
+
.wave-docs-layout__sidebar {
|
|
2286
|
+
display: flex;
|
|
2287
|
+
/* No gap: the trigger sits directly against the navigation's border. */
|
|
2288
|
+
gap: 0;
|
|
2289
|
+
width: max-content;
|
|
2290
|
+
justify-self: start;
|
|
2291
|
+
/*
|
|
2292
|
+
* ⚠️ THE COLUMN IN THE PAGE, NOT THE PANEL THAT FOLLOWS THE VIEWPORT, AND
|
|
2293
|
+
* KEEPING THOSE TWO APART IS THE POINT.
|
|
2294
|
+
*
|
|
2295
|
+
* This box was `position: sticky` with `height: 100dvh` — sized against the
|
|
2296
|
+
* viewport while positioned against the layout, which is two reference
|
|
2297
|
+
* frames in one element. The moment a host put anything above it, the two
|
|
2298
|
+
* disagreed by exactly that much: with the UA's 8px of `body` margin the
|
|
2299
|
+
* divider stopped 8px short of the top and ran 8px past the bottom, and it
|
|
2300
|
+
* snapped into place as soon as the page scrolled and sticky engaged.
|
|
2301
|
+
* Measured.
|
|
2302
|
+
*
|
|
2303
|
+
* Stretched to the grid row, it is exactly as tall as the docs region —
|
|
2304
|
+
* starting where the host put it and ending where it ends, symmetric at any
|
|
2305
|
+
* offset, with no token to set and nothing to keep in sync. The scrolling
|
|
2306
|
+
* half is `position: sticky` on the two children, which is where a viewport
|
|
2307
|
+
* measurement belongs.
|
|
2308
|
+
*/
|
|
2309
|
+
align-self: stretch;
|
|
2310
|
+
/* So a page shorter than a screen does not leave the sticky panel hanging
|
|
2311
|
+
* below the column that draws its edge. */
|
|
2312
|
+
min-height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
2313
|
+
position: relative;
|
|
2314
|
+
/* Above the scrim, which is above the article. */
|
|
2315
|
+
z-index: 2;
|
|
1701
2316
|
background: none;
|
|
1702
|
-
|
|
1703
|
-
|
|
2317
|
+
border: 0;
|
|
2318
|
+
/*
|
|
2319
|
+
* ⚠️ THE BOX IS ALWAYS `max-content`; ONLY ITS CONTRIBUTION MOVES.
|
|
2320
|
+
*
|
|
2321
|
+
* This one declaration is the whole of "the article does not get out of the
|
|
2322
|
+
* way" — closed at any width, and open in cover mode. The box still paints
|
|
2323
|
+
* its full width, so in cover mode it overlays; it just does not ask the
|
|
2324
|
+
* grid for the room. Push mode drops it, and the same box takes its own
|
|
2325
|
+
* space with nothing else changing.
|
|
2326
|
+
*/
|
|
2327
|
+
margin-inline-end: calc(-1 * var(--wave-docs-sidebar-width));
|
|
2328
|
+
/*
|
|
2329
|
+
* Exactly the navigation's width, so it goes entirely off the page and the
|
|
2330
|
+
* trigger's outer edge lands on the inline start edge — whatever the strip
|
|
2331
|
+
* happens to be.
|
|
2332
|
+
*
|
|
2333
|
+
* ⚠️ THE SAME TOKEN AS THE MARGIN ABOVE, AND DELIBERATELY. It was
|
|
2334
|
+
* `calc(var(--wave-docs-trigger-width) - 100%)` — "minus all of me, plus the
|
|
2335
|
+
* trigger back" — which avoided naming the navigation but only worked while
|
|
2336
|
+
* the strip had a width to name. The strip is `auto` now, so this is both
|
|
2337
|
+
* shorter and the only spelling that cannot drift: the two declarations
|
|
2338
|
+
* that move the sidebar read the same token.
|
|
2339
|
+
*/
|
|
2340
|
+
translate: calc(-1 * var(--wave-docs-sidebar-width));
|
|
1704
2341
|
}
|
|
1705
2342
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2343
|
+
/*
|
|
2344
|
+
* ⚠️ THREE STATES, NOT TWO, AND THE THIRD ONE IS WHY THERE IS NO FLASH.
|
|
2345
|
+
*
|
|
2346
|
+
* `data-state` absent means "nobody has chosen yet" — the server renders
|
|
2347
|
+
* that, and CSS decides per mode: closed where the navigation would cover the
|
|
2348
|
+
* article, open where it would sit beside it. So the first paint is already
|
|
2349
|
+
* right at both shapes, with no JavaScript and nothing to correct. Once the
|
|
2350
|
+
* reader presses the trigger their choice is explicit and wins at every
|
|
2351
|
+
* width.
|
|
2352
|
+
*/
|
|
2353
|
+
.wave-docs-layout__sidebar[data-state='open'] {
|
|
2354
|
+
translate: 0;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/*
|
|
2358
|
+
* ⚠️ NOT UNTIL THE SECOND FRAME. A transition declared at first paint
|
|
2359
|
+
* animates the resolved state *in* from wherever the initial one was — the
|
|
2360
|
+
* navigation visibly slides across on every page load. `data-ready` is set
|
|
2361
|
+
* after mount, so the first paint is a jump nobody sees and every later
|
|
2362
|
+
* change is animated.
|
|
2363
|
+
*/
|
|
2364
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2365
|
+
.wave-docs-layout__sidebar[data-ready] {
|
|
2366
|
+
transition:
|
|
2367
|
+
translate 200ms ease,
|
|
2368
|
+
margin-inline-end 200ms ease;
|
|
2369
|
+
}
|
|
1709
2370
|
}
|
|
1710
2371
|
|
|
1711
2372
|
/* -------------------------------------------------------------------------
|
|
1712
|
-
*
|
|
2373
|
+
* The navigation
|
|
1713
2374
|
* ---------------------------------------------------------------------- */
|
|
1714
2375
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2376
|
+
/*
|
|
2377
|
+
* The only painted surface in the sidebar, and it has to be opaque: in cover
|
|
2378
|
+
* mode it sits on top of the article, and a translucent one would put two
|
|
2379
|
+
* columns of text through each other.
|
|
2380
|
+
*/
|
|
2381
|
+
.wave-docs-layout__sidebar-nav {
|
|
2382
|
+
flex: none;
|
|
1720
2383
|
box-sizing: border-box;
|
|
1721
|
-
width:
|
|
1722
|
-
|
|
1723
|
-
height: 100dvh;
|
|
1724
|
-
max-height: none;
|
|
1725
|
-
padding: 1.5rem 1rem;
|
|
2384
|
+
width: var(--wave-docs-sidebar-width);
|
|
2385
|
+
padding: 2rem 1rem;
|
|
1726
2386
|
border: 0;
|
|
2387
|
+
/*
|
|
2388
|
+
* Cover mode only, and it is a different thing from the divider in push
|
|
2389
|
+
* mode: there the line divides the page, here it is the edge of a panel
|
|
2390
|
+
* lying on top of one. A panel's edge is the panel's height, so this one
|
|
2391
|
+
* belongs on the panel.
|
|
2392
|
+
*/
|
|
1727
2393
|
border-inline-end: 1px solid var(--wave-docs-border);
|
|
1728
2394
|
background-color: var(--wave-docs-bg);
|
|
1729
|
-
|
|
2395
|
+
/* The panel that follows the viewport: sticky, one screen tall, and its own
|
|
2396
|
+
* scroller. `dvh`, not `vh` — with a mobile URL bar showing, `vh` is taller
|
|
2397
|
+
* than the visible viewport and the last few nav items cannot be reached. */
|
|
2398
|
+
position: sticky;
|
|
2399
|
+
top: var(--wave-docs-chrome-offset);
|
|
2400
|
+
height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
1730
2401
|
overflow-y: auto;
|
|
1731
|
-
/*
|
|
2402
|
+
/* Without this, reaching the end of the nav keeps scrolling — the article
|
|
2403
|
+
* jumps under the reader's cursor while they are looking at the sidebar. */
|
|
1732
2404
|
overscroll-behavior: contain;
|
|
2405
|
+
/* Reserves the scrollbar's width whether or not it is showing, so the
|
|
2406
|
+
* column does not jump when the nav grows past a screen. */
|
|
2407
|
+
scrollbar-gutter: stable;
|
|
1733
2408
|
}
|
|
1734
2409
|
|
|
1735
|
-
|
|
1736
|
-
|
|
2410
|
+
/* First child of the navigation, above the tree. */
|
|
2411
|
+
.wave-docs-layout__search {
|
|
2412
|
+
margin-block-end: 1rem;
|
|
1737
2413
|
}
|
|
1738
2414
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2415
|
+
/* -------------------------------------------------------------------------
|
|
2416
|
+
* The trigger
|
|
2417
|
+
* ---------------------------------------------------------------------- */
|
|
2418
|
+
|
|
2419
|
+
/*
|
|
2420
|
+
* ⚠️ NO BACKGROUND AND NO BORDER, IN ANY STATE. 44px wide because that is
|
|
2421
|
+
* Apple's minimum tap target and the strip *is* the target — it carried a
|
|
2422
|
+
* 20px transparent border for a while to answer to 44 while painting 24, and
|
|
2423
|
+
* a box the eye cannot see is a box the next person deletes.
|
|
2424
|
+
*/
|
|
2425
|
+
.wave-docs-layout__sidebar-trigger {
|
|
2426
|
+
flex: none;
|
|
2427
|
+
/*
|
|
2428
|
+
* Sticky for the same reason the navigation is: the column around it is the
|
|
2429
|
+
* page's height now, and a control centred in *that* would sit halfway down
|
|
2430
|
+
* a long document instead of halfway down the screen.
|
|
2431
|
+
*
|
|
2432
|
+
* ⚠️ AND IT IS ALSO WHAT POSITIONS THE BUTTON. The button and the dots are
|
|
2433
|
+
* `position: absolute`, and an absolutely positioned box resolves against
|
|
2434
|
+
* the nearest *positioned* ancestor — `sticky` qualifies, `static` does not.
|
|
2435
|
+
* Without a positioned ancestor here they escape to the layout and paint a
|
|
2436
|
+
* slab across the page. Measured.
|
|
2437
|
+
*/
|
|
2438
|
+
position: sticky;
|
|
2439
|
+
top: var(--wave-docs-chrome-offset);
|
|
2440
|
+
height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
2441
|
+
/*
|
|
2442
|
+
* ⚠️ `auto`, SO THE TARGET GROWS WITH THE BUTTON. The strip was a fixed
|
|
2443
|
+
* 2.75rem and the button was positioned inside it, which made the gap
|
|
2444
|
+
* between them a third number nobody set: 24px of button left 10px a side,
|
|
2445
|
+
* 16px left 14px. Sized by its content it is always the button plus 4px,
|
|
2446
|
+
* and `--wave-docs-trigger-width` is the only thing to change.
|
|
2447
|
+
*/
|
|
2448
|
+
display: grid;
|
|
2449
|
+
place-items: center;
|
|
2450
|
+
width: auto;
|
|
2451
|
+
padding: 4px;
|
|
1747
2452
|
border: 0;
|
|
1748
|
-
border-radius: var(--wave-docs-radius-sm);
|
|
1749
2453
|
background: none;
|
|
1750
2454
|
color: var(--wave-docs-fg-muted);
|
|
1751
2455
|
cursor: pointer;
|
|
1752
2456
|
}
|
|
1753
2457
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2458
|
+
/*
|
|
2459
|
+
* The button: `--wave-docs-trigger-width` × 80, and grey until hovered.
|
|
2460
|
+
*
|
|
2461
|
+
* ⚠️ IN FLOW, WHICH IS WHAT MAKES THE STRIP SIZE ITSELF. Absolutely
|
|
2462
|
+
* positioned it contributed nothing, so the strip needed its own width and
|
|
2463
|
+
* the gap between the two was whatever the subtraction happened to be. This
|
|
2464
|
+
* is the only in-flow child, so the strip is exactly this plus its 4px of
|
|
2465
|
+
* padding — one number, and the target follows the paint.
|
|
2466
|
+
*
|
|
2467
|
+
* `place-items: center` on the strip does the centring the insets used to.
|
|
2468
|
+
*/
|
|
2469
|
+
.wave-docs-layout__sidebar-trigger::before {
|
|
2470
|
+
content: '';
|
|
2471
|
+
width: var(--wave-docs-trigger-width);
|
|
2472
|
+
height: 5rem;
|
|
2473
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
2474
|
+
background-color: var(--wave-docs-border-strong);
|
|
1757
2475
|
}
|
|
1758
2476
|
|
|
1759
2477
|
/*
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
2478
|
+
* Three dots. Measured off docsify.js.org rather than reasoned about: three
|
|
2479
|
+
* 4px circles 3px apart. One element and two `box-shadow` copies rather than
|
|
2480
|
+
* three spans — no markup, and it follows `currentcolor` through the hover
|
|
2481
|
+
* state for free. 4 + 3 + 4 + 3 + 4 is 18px tall, so the offset is 7.
|
|
2482
|
+
*
|
|
2483
|
+
* ⚠️ `::after`, AND THE ORDER IS LOAD-BEARING. Neither pseudo-element carries
|
|
2484
|
+
* a `z-index`, so tree order decides which paints on top.
|
|
1763
2485
|
*/
|
|
1764
|
-
|
|
1765
|
-
|
|
2486
|
+
.wave-docs-layout__sidebar-trigger::after {
|
|
2487
|
+
content: '';
|
|
2488
|
+
position: absolute;
|
|
2489
|
+
inset-block-start: 50%;
|
|
2490
|
+
inset-inline-start: 50%;
|
|
2491
|
+
width: 4px;
|
|
2492
|
+
height: 4px;
|
|
2493
|
+
translate: -50% -50%;
|
|
2494
|
+
border-radius: 50%;
|
|
2495
|
+
background: currentcolor;
|
|
2496
|
+
box-shadow:
|
|
2497
|
+
0 -7px 0 currentcolor,
|
|
2498
|
+
0 7px 0 currentcolor;
|
|
1766
2499
|
}
|
|
1767
2500
|
|
|
1768
2501
|
/*
|
|
1769
|
-
*
|
|
2502
|
+
* ⚠️ THE STRIP'S HOVER LIGHTS THE BUTTON TOO, AND THAT IS WHAT MAKES IT ONE
|
|
2503
|
+
* CONTROL. Hovering 44px of strip while the button inside it stays grey reads
|
|
2504
|
+
* as a dead zone around a small target; Docsify pairs them for the same
|
|
2505
|
+
* reason.
|
|
2506
|
+
*/
|
|
2507
|
+
.wave-docs-layout__sidebar-trigger:hover {
|
|
2508
|
+
background-color: color-mix(
|
|
2509
|
+
in oklab,
|
|
2510
|
+
var(--wave-docs-accent) 30%,
|
|
2511
|
+
transparent
|
|
2512
|
+
);
|
|
2513
|
+
color: var(--wave-docs-accent-fg);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.wave-docs-layout__sidebar-trigger:hover::before {
|
|
2517
|
+
background-color: var(--wave-docs-accent);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2521
|
+
.wave-docs-layout__sidebar-trigger,
|
|
2522
|
+
.wave-docs-layout__sidebar-trigger::before,
|
|
2523
|
+
.wave-docs-layout__sidebar-trigger::after {
|
|
2524
|
+
/*
|
|
2525
|
+
* ⚠️ ON THE PSEUDO-ELEMENTS TOO, NOT ONLY THE BUTTON. The dots are
|
|
2526
|
+
* `background: currentcolor`, and an inherited colour changing does not
|
|
2527
|
+
* start a transition on a descendant that has not declared one — the
|
|
2528
|
+
* button would fade and the dots would snap.
|
|
2529
|
+
*/
|
|
2530
|
+
transition:
|
|
2531
|
+
background-color 120ms ease-out,
|
|
2532
|
+
color 120ms ease-out;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/*
|
|
2537
|
+
* ⚠️ AN OUTLINE, NOT THE FILL. These shared the hover rule for one commit,
|
|
2538
|
+
* and `focus-indicators` caught it: a background change is the one thing
|
|
2539
|
+
* forced-colours mode discards, so a keyboard reader there would have had no
|
|
2540
|
+
* indicator at all.
|
|
2541
|
+
*/
|
|
2542
|
+
.wave-docs-layout__sidebar-trigger:focus-visible {
|
|
2543
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
2544
|
+
outline-offset: -2px;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/* -------------------------------------------------------------------------
|
|
2548
|
+
* The scrim
|
|
2549
|
+
*
|
|
2550
|
+
* Cover mode only. `inert` keeps the keyboard out of what is underneath; this
|
|
2551
|
+
* is what tells everyone else.
|
|
2552
|
+
* ---------------------------------------------------------------------- */
|
|
2553
|
+
|
|
2554
|
+
.wave-docs-layout__sidebar-scrim {
|
|
2555
|
+
/* Absolute inside the layout rather than fixed to the viewport: a fixed
|
|
2556
|
+
* scrim would darken a host's own chrome along with our article. */
|
|
2557
|
+
position: absolute;
|
|
2558
|
+
inset: 0;
|
|
2559
|
+
z-index: 1;
|
|
2560
|
+
border: 0;
|
|
2561
|
+
padding: 0;
|
|
2562
|
+
background-color: oklch(0.2 0.02 262 / 0.55);
|
|
2563
|
+
opacity: 0;
|
|
2564
|
+
pointer-events: none;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
.wave-docs-layout__sidebar[data-state='open']
|
|
2568
|
+
~ .wave-docs-layout__sidebar-scrim {
|
|
2569
|
+
opacity: 1;
|
|
2570
|
+
pointer-events: auto;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2574
|
+
.wave-docs-layout__sidebar-scrim {
|
|
2575
|
+
transition: opacity 200ms ease;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
/* -------------------------------------------------------------------------
|
|
2580
|
+
* Cover and push
|
|
1770
2581
|
*
|
|
1771
|
-
*
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
|
|
2582
|
+
* The whole of what differs between a narrow container and a wide one. Same
|
|
2583
|
+
* markup, same classes, same control, same translate — only whether the
|
|
2584
|
+
* article gets out of the way.
|
|
2585
|
+
* ---------------------------------------------------------------------- */
|
|
2586
|
+
|
|
2587
|
+
/*
|
|
2588
|
+
* ⚠️ A CUSTOM PROPERTY, BECAUSE JAVASCRIPT CANNOT ASK A CONTAINER QUERY.
|
|
2589
|
+
* `matchMedia` answers about the viewport, which is the question this file
|
|
2590
|
+
* deliberately stopped asking — so the mode is declared here and read back
|
|
2591
|
+
* with `getComputedStyle`. One source of truth, and the breakpoint appears
|
|
2592
|
+
* nowhere in the component.
|
|
1776
2593
|
*/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2594
|
+
.wave-docs-layout__sidebar {
|
|
2595
|
+
--wave-docs-sidebar-mode: cover;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
@container wave-docs (min-width: 64rem) {
|
|
2599
|
+
.wave-docs-layout__sidebar {
|
|
2600
|
+
--wave-docs-sidebar-mode: push;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/*
|
|
2604
|
+
* ⚠️ THE DIVISION IS THE PAGE'S, SO IT IS DRAWN BY THE PAGE'S COLUMN.
|
|
2605
|
+
*
|
|
2606
|
+
* In push mode the sidebar is part of the layout and this line is what
|
|
2607
|
+
* separates navigation from article — so it runs the full height of the
|
|
2608
|
+
* docs region, top to bottom, wherever the host placed it. Drawn on the
|
|
2609
|
+
* navigation instead it would be one screen tall, which is only the same
|
|
2610
|
+
* thing when the layout happens to start at the viewport's top edge.
|
|
2611
|
+
*
|
|
2612
|
+
* `inset-inline-start` at the navigation's width rather than a border on
|
|
2613
|
+
* the shell: the shell's own inline end is past the trigger, and the
|
|
2614
|
+
* division belongs between the navigation and the article.
|
|
2615
|
+
*/
|
|
2616
|
+
.wave-docs-layout__sidebar::after {
|
|
2617
|
+
content: '';
|
|
2618
|
+
position: absolute;
|
|
2619
|
+
inset-block: 0;
|
|
2620
|
+
inset-inline-start: var(--wave-docs-sidebar-width);
|
|
2621
|
+
width: 1px;
|
|
2622
|
+
background-color: var(--wave-docs-border);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/*
|
|
2626
|
+
* Unchosen means open up here, where there is room for the navigation
|
|
2627
|
+
* beside the article rather than on top of it — and open in push mode is
|
|
2628
|
+
* the one combination that asks the grid for its own space.
|
|
2629
|
+
*
|
|
2630
|
+
* ⚠️ KEYED ON THE STATE, NOT THE MODE. Dropping the margin for the whole of
|
|
2631
|
+
* push mode leaves a 300px hole in the grid whenever the sidebar is closed:
|
|
2632
|
+
* the box is translated off the page and the track is still holding room
|
|
2633
|
+
* for it.
|
|
2634
|
+
*/
|
|
2635
|
+
.wave-docs-layout__sidebar:not([data-state='closed']) {
|
|
2636
|
+
translate: 0;
|
|
2637
|
+
margin-inline-end: 0;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/* Nothing is covered, so nothing is scrimmed. */
|
|
2641
|
+
.wave-docs-layout__sidebar-scrim {
|
|
1780
2642
|
display: none;
|
|
1781
2643
|
}
|
|
2644
|
+
|
|
2645
|
+
/* And the panel's edge gives way to the page's division above. */
|
|
2646
|
+
.wave-docs-layout__sidebar-nav {
|
|
2647
|
+
border-inline-end: 0;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
.wave-docs-layout {
|
|
2651
|
+
gap: 0 1.5rem;
|
|
2652
|
+
padding-inline: 0 1.5rem;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
@container wave-docs (min-width: 80rem) {
|
|
2657
|
+
.wave-docs-layout:has(.wave-docs-layout__toc) {
|
|
2658
|
+
grid-template-columns: auto minmax(0, 1fr) var(--wave-docs-toc-width);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
.wave-docs-layout__toc {
|
|
2662
|
+
display: block;
|
|
2663
|
+
/* Same reason as the article: its contents cannot be weighed against the
|
|
2664
|
+
* scrim, only the column as a whole. */
|
|
2665
|
+
isolation: isolate;
|
|
2666
|
+
position: sticky;
|
|
2667
|
+
/* `align-self: start`, or the sticky box stretches to the row height and
|
|
2668
|
+
* has nothing left to travel within. */
|
|
2669
|
+
align-self: start;
|
|
2670
|
+
top: var(--wave-docs-chrome-offset);
|
|
2671
|
+
max-height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
2672
|
+
overflow-y: auto;
|
|
2673
|
+
overscroll-behavior: contain;
|
|
2674
|
+
padding-block: 2rem;
|
|
2675
|
+
}
|
|
1782
2676
|
}
|
|
1783
2677
|
}
|
|
1784
2678
|
|
|
@@ -1809,6 +2703,25 @@
|
|
|
1809
2703
|
cursor: pointer;
|
|
1810
2704
|
}
|
|
1811
2705
|
|
|
2706
|
+
/*
|
|
2707
|
+
* In the shell, the trigger fills whatever it is placed in — the width of the
|
|
2708
|
+
* strip below 64rem, the width of the column above it.
|
|
2709
|
+
*
|
|
2710
|
+
* ⚠️ THIS USED TO BE A THREE-STEP `max-width` LADDER (9rem, 11rem at 30rem,
|
|
2711
|
+
* 20rem at 48rem) AND EVERY NUMBER IN IT WAS HEADER ARITHMETIC. The trigger
|
|
2712
|
+
* is `width: 100%`, so in a flex row its base was the whole row; the ladder
|
|
2713
|
+
* existed to stop it starving the brand beside it, and at 390px it had
|
|
2714
|
+
* ellipsised "Wave Docs" to "Wa…". With no header there is no row to share
|
|
2715
|
+
* and no brand to leave room for, so the container decides.
|
|
2716
|
+
*
|
|
2717
|
+
* The bare `.wave-docs-search-trigger` keeps its own `max-width: 20rem` for
|
|
2718
|
+
* the standalone case — a host rendering `DocsSearch` in their own header,
|
|
2719
|
+
* where something else *is* competing for the row.
|
|
2720
|
+
*/
|
|
2721
|
+
.wave-docs-search-trigger.wave-docs-layout__search {
|
|
2722
|
+
max-width: none;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
1812
2725
|
.wave-docs-search-trigger:hover {
|
|
1813
2726
|
border-color: var(--wave-docs-border-strong);
|
|
1814
2727
|
color: var(--wave-docs-fg);
|
|
@@ -1828,6 +2741,18 @@
|
|
|
1828
2741
|
white-space: nowrap;
|
|
1829
2742
|
}
|
|
1830
2743
|
|
|
2744
|
+
/*
|
|
2745
|
+
* ⚠️ HIDDEN WHERE THE SHORTCUT CANNOT BE PRESSED, WHICH IS NOT A WIDTH
|
|
2746
|
+
* QUESTION. `⌘K` is an instruction, and on a phone or a tablet it is one the
|
|
2747
|
+
* reader cannot follow — a 10-inch tablet at 1024px would keep it under any
|
|
2748
|
+
* breakpoint rule.
|
|
2749
|
+
*/
|
|
2750
|
+
@media (hover: none) and (pointer: coarse) {
|
|
2751
|
+
.wave-docs-search-trigger-kbd {
|
|
2752
|
+
display: none;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
1831
2756
|
.wave-docs-search-trigger-kbd {
|
|
1832
2757
|
flex: none;
|
|
1833
2758
|
padding: 0.05rem 0.35rem;
|