@waveso/docs 0.6.0 → 0.7.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 +155 -1
- package/README.md +114 -37
- 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 +1079 -237
- 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
|
/*
|
|
@@ -1436,6 +1733,18 @@
|
|
|
1436
1733
|
color: var(--wave-docs-fg-subtle);
|
|
1437
1734
|
}
|
|
1438
1735
|
|
|
1736
|
+
.wave-docs-toc__title {
|
|
1737
|
+
margin: 0 0 0.5rem;
|
|
1738
|
+
/* Aligns with the link text below it. The links carry a 2px rail on their
|
|
1739
|
+
* inline start and 0.75rem of padding inside it; this has no rail, so it
|
|
1740
|
+
* takes the rail's width as padding instead. */
|
|
1741
|
+
padding-inline-start: calc(0.75rem + 2px);
|
|
1742
|
+
color: var(--wave-docs-fg);
|
|
1743
|
+
font-size: 0.8125rem;
|
|
1744
|
+
font-weight: 600;
|
|
1745
|
+
letter-spacing: 0.01em;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1439
1748
|
.wave-docs-toc__link:hover {
|
|
1440
1749
|
color: var(--wave-docs-fg);
|
|
1441
1750
|
border-inline-start-color: var(--wave-docs-border-strong);
|
|
@@ -1457,8 +1766,8 @@
|
|
|
1457
1766
|
/* ---------------------------------------------------------------------------
|
|
1458
1767
|
* Layout
|
|
1459
1768
|
*
|
|
1460
|
-
* The shell `docs.Layout` renders. Every class here is
|
|
1461
|
-
*
|
|
1769
|
+
* The shell `docs.Layout` renders. Every class here is public API from 0.3.0
|
|
1770
|
+
* and changes only in a release that carries the migration.
|
|
1462
1771
|
*
|
|
1463
1772
|
* These are the first width-based media queries in this file. Before them there
|
|
1464
1773
|
* were none at all — every `@media` was `prefers-color-scheme`,
|
|
@@ -1468,45 +1777,93 @@
|
|
|
1468
1777
|
|
|
1469
1778
|
@layer components {
|
|
1470
1779
|
/*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1780
|
+
* ONE SIDEBAR, AT EVERY WIDTH, AND IT IS A SHELL OF TWO.
|
|
1781
|
+
*
|
|
1782
|
+
* .wave-docs-shell the query container
|
|
1783
|
+
* └─ .wave-docs-layout the grid
|
|
1784
|
+
* ├─ .wave-docs-layout__sidebar paints nothing, and moves
|
|
1785
|
+
* │ ├─ …__sidebar-nav the surface, and the one border
|
|
1786
|
+
* │ └─ …__sidebar-trigger the strip — paints nothing at rest
|
|
1787
|
+
* ├─ .wave-docs-layout__sidebar-scrim
|
|
1788
|
+
* ├─ .wave-docs-layout__main
|
|
1789
|
+
* └─ .wave-docs-layout__toc
|
|
1790
|
+
*
|
|
1791
|
+
* There is no drawer, no dialog, no second control and no second copy of the
|
|
1792
|
+
* tree. Pressing the trigger translates the sidebar; the trigger rides on the
|
|
1793
|
+
* navigation's outer edge because it is the next flex item, not because a
|
|
1794
|
+
* number says so.
|
|
1795
|
+
*
|
|
1796
|
+
* ⚠️ `@container`, AND EVERY WIDTH-BASED `@media` IS GONE WITH IT.
|
|
1797
|
+
*
|
|
1798
|
+
* This package is mounted at `/docs` inside applications that own the rest of
|
|
1799
|
+
* the page. `@media` asks how wide the *screen* is, which is the wrong
|
|
1800
|
+
* question: a host who puts this in a 700px panel on a 1920px monitor gets
|
|
1801
|
+
* the wide layout and a reading column of about 60px. That is the same
|
|
1802
|
+
* failure as the fixed header — a collision arriving from a direction no
|
|
1803
|
+
* breakpoint can see. `@container` asks how wide the box we were *given* is,
|
|
1804
|
+
* which is the question with an answer.
|
|
1805
|
+
*
|
|
1806
|
+
* ⚠️ AND THAT IS WHY THERE IS AN OUTER ELEMENT. A container query never
|
|
1807
|
+
* matches its own container, so with `container-type` on the grid, no rule
|
|
1808
|
+
* inside a query could touch `grid-template-columns` — which is exactly the
|
|
1809
|
+
* declaration the layout has to change. One wrapper, and every rule below is
|
|
1810
|
+
* addressable.
|
|
1473
1811
|
*/
|
|
1474
|
-
.wave-docs-layout {
|
|
1475
|
-
display: grid;
|
|
1476
|
-
grid-template-columns: minmax(0, 1fr);
|
|
1477
|
-
gap: 0 1.5rem;
|
|
1478
|
-
/* 1rem on a phone, not 1.5rem: 48px of side padding is 15% of a 320px
|
|
1479
|
-
* screen, and it took the reading column to 272px — under the 280px this
|
|
1480
|
-
* package treats as the floor for a usable measure. Measured. */
|
|
1481
|
-
padding-inline: 1rem;
|
|
1482
|
-
/* Without this, `max-width` at the 100rem cap applies to the *content* box
|
|
1483
|
-
* and the shell renders 48px wider than the token says — measured 1648
|
|
1484
|
-
* against a declared 1600. */
|
|
1485
|
-
box-sizing: border-box;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
@media (min-width: 30rem) {
|
|
1489
|
-
.wave-docs-layout {
|
|
1490
|
-
padding-inline: 1.5rem;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
1812
|
/*
|
|
1495
|
-
* ⚠️ `
|
|
1813
|
+
* ⚠️ NO `max-width` AND NO `margin-inline: auto` HERE, AND THAT IS A FIX
|
|
1814
|
+
* RATHER THAN AN OMISSION.
|
|
1496
1815
|
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1816
|
+
* This box was capped at `--wave-docs-shell-width` and centred. Below the cap
|
|
1817
|
+
* the layout's inline start edge and the viewport's are the same place, so
|
|
1818
|
+
* `translate: calc(var(--wave-docs-trigger-width) - 100%)` put the closed
|
|
1819
|
+
* navigation off the screen. Above it they are not: on a 2000px viewport the
|
|
1820
|
+
* shell starts at 200, and the closed navigation parked at −56…200 — two
|
|
1821
|
+
* hundred pixels of it still on screen, beside a trigger sitting 200px in
|
|
1822
|
+
* from the edge. Measured.
|
|
1503
1823
|
*
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1824
|
+
* The sidebar owns the page's inline start edge at every width now, so
|
|
1825
|
+
* "closed" means off the screen by construction rather than by coincidence.
|
|
1826
|
+
* What was capped is capped where it belongs: `.wave-docs-prose` holds
|
|
1827
|
+
* `--wave-docs-measure` and centres itself in the track, so a 2560px display
|
|
1828
|
+
* gets a reading column between its navigation and its table of contents
|
|
1829
|
+
* instead of text pinned against one of them.
|
|
1507
1830
|
*/
|
|
1508
|
-
.wave-docs-
|
|
1509
|
-
|
|
1831
|
+
.wave-docs-shell {
|
|
1832
|
+
container-type: inline-size;
|
|
1833
|
+
container-name: wave-docs;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.wave-docs-layout {
|
|
1837
|
+
/* Absolutely positioned children of ours resolve against this, so nothing
|
|
1838
|
+
* of ours resolves against the viewport and nothing of ours can land on a
|
|
1839
|
+
* host's chrome. */
|
|
1840
|
+
position: relative;
|
|
1841
|
+
display: grid;
|
|
1842
|
+
/*
|
|
1843
|
+
* ⚠️ `auto`, AND THE SIDEBAR DECIDES WHAT IT MEANS. The track takes the
|
|
1844
|
+
* sidebar's *outer* size, and the sidebar sets that with a negative margin:
|
|
1845
|
+
* 44px whenever the navigation is off the page or covering the article,
|
|
1846
|
+
* its full width only when it is meant to sit beside it. So one track
|
|
1847
|
+
* expresses both modes and both states, and the layout never has to know
|
|
1848
|
+
* which it is in.
|
|
1849
|
+
*
|
|
1850
|
+
* A fixed `var(--wave-docs-trigger-width)` here was the first attempt, and
|
|
1851
|
+
* it could not grow — push mode silently behaved exactly like cover mode.
|
|
1852
|
+
* Measured at 1440px: the article started at 68px in both states.
|
|
1853
|
+
*
|
|
1854
|
+
* In cover mode the article's measure is untouched by the toggle, because
|
|
1855
|
+
* the track never changes: 330px at 390px whether the navigation is open or
|
|
1856
|
+
* shut. In push mode it necessarily changes — that is what push means, and
|
|
1857
|
+
* it is why cover exists for the widths where 330px is all there is.
|
|
1858
|
+
*/
|
|
1859
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1860
|
+
gap: 0 0.5rem;
|
|
1861
|
+
/* Nothing on the inline start: the trigger's track owns that edge. */
|
|
1862
|
+
padding-inline: 0 0.5rem;
|
|
1863
|
+
/* Without this, `max-width` at the cap applies to the *content* box and the
|
|
1864
|
+
* shell renders 48px wider than the token says — measured 1648 against a
|
|
1865
|
+
* declared 1600. */
|
|
1866
|
+
box-sizing: border-box;
|
|
1510
1867
|
}
|
|
1511
1868
|
|
|
1512
1869
|
.wave-docs-layout__toc {
|
|
@@ -1526,259 +1883,713 @@
|
|
|
1526
1883
|
* and removing `min-width: 0` alone passes; only removing both reproduces the
|
|
1527
1884
|
* overflow. They are kept as a pair because they fail in opposite directions
|
|
1528
1885
|
* — 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.
|
|
1886
|
+
* and `min-width: 0` protects a track someone respells.
|
|
1536
1887
|
*/
|
|
1537
1888
|
.wave-docs-layout__main {
|
|
1538
1889
|
min-width: 0;
|
|
1539
1890
|
padding-block: 2rem 4rem;
|
|
1891
|
+
/*
|
|
1892
|
+
* ⚠️ EVERY `z-index` IN THE ARTICLE IS TRAPPED HERE, AND ONE OF THEM WAS
|
|
1893
|
+
* PUNCHING THROUGH THE SCRIM.
|
|
1894
|
+
*
|
|
1895
|
+
* A sticky table header is `z-index: 1`, and so is the scrim. Same stacking
|
|
1896
|
+
* context, equal z-index, and the header comes later in the document — so
|
|
1897
|
+
* with the navigation open over a table on a phone, the header row was the
|
|
1898
|
+
* one thing on the page that stayed bright. Measured on the installation
|
|
1899
|
+
* page.
|
|
1900
|
+
*
|
|
1901
|
+
* Raising the scrim would have fixed that one element and left the next
|
|
1902
|
+
* one. `isolation: isolate` makes the article its own stacking context, so
|
|
1903
|
+
* nothing inside it — ours or a consumer's own content — can be compared
|
|
1904
|
+
* against the scrim at all. The article resolves as a whole, at one layer,
|
|
1905
|
+
* underneath.
|
|
1906
|
+
*/
|
|
1907
|
+
isolation: isolate;
|
|
1540
1908
|
}
|
|
1541
1909
|
|
|
1542
|
-
/*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1910
|
+
/* -------------------------------------------------------------------------
|
|
1911
|
+
* The hero
|
|
1912
|
+
*
|
|
1913
|
+
* A page header a landing page opts into by declaring `actions` in its
|
|
1914
|
+
* frontmatter. Every other page renders none of this.
|
|
1915
|
+
* ---------------------------------------------------------------------- */
|
|
1549
1916
|
|
|
1917
|
+
.wave-docs-hero {
|
|
1918
|
+
position: relative;
|
|
1550
1919
|
/*
|
|
1551
|
-
* ⚠️
|
|
1920
|
+
* ⚠️ BOTH, AND FOR TWO DIFFERENT REASONS. `isolate` keeps the grid's
|
|
1921
|
+
* `z-index: -1` inside the hero instead of dropping it behind the page;
|
|
1922
|
+
* `clip` holds the 150% rotated layer inside the header rather than letting
|
|
1923
|
+
* it run down the article. `clip` rather than `hidden` so no scroll
|
|
1924
|
+
* container is created and nothing sticky below is affected.
|
|
1925
|
+
*/
|
|
1926
|
+
isolation: isolate;
|
|
1927
|
+
overflow: clip;
|
|
1928
|
+
/*
|
|
1929
|
+
* ⚠️ THE NEGATIVE MARGIN CANCELS `.wave-docs-layout__main`'s
|
|
1930
|
+
* `padding-block-start`, AND THE PADDING PUTS IT BACK INSIDE.
|
|
1552
1931
|
*
|
|
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.
|
|
1932
|
+
* The grid is meant to reach the top of the article column. Left in the
|
|
1933
|
+
* flow the hero starts 2rem down — measured at y=32 against a main at y=0 —
|
|
1934
|
+
* so the pattern began with a band of blank page above it. Full-bleed
|
|
1935
|
+
* upwards, and the space the header wants is its own padding rather than
|
|
1936
|
+
* its parent's.
|
|
1561
1937
|
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
* narrow one.
|
|
1938
|
+
* Horizontally there is nothing to cancel: `__main` carries no inline
|
|
1939
|
+
* padding, so the hero already spans it exactly.
|
|
1565
1940
|
*/
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1941
|
+
margin-block-start: -2rem;
|
|
1942
|
+
margin-block-end: 3rem;
|
|
1943
|
+
padding-block: 5rem 3.5rem;
|
|
1944
|
+
}
|
|
1569
1945
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1946
|
+
/*
|
|
1947
|
+
* ⚠️ FOUR GRADIENTS, NOT AN SVG. The reference for this is a 100px SVG tile
|
|
1948
|
+
* of twenty stroked lines, inlined as a data URI. Drawn as gradients it is
|
|
1949
|
+
* the same picture with three properties that matter more than the fidelity:
|
|
1950
|
+
* not one byte of data URI in the stylesheet, the line colour is a token so
|
|
1951
|
+
* it follows the theme instead of being hardcoded black, and the tile is two
|
|
1952
|
+
* numbers rather than twenty coordinates.
|
|
1953
|
+
*
|
|
1954
|
+
* The grid is 20px with every fifth line emphasised, offset 15px so the
|
|
1955
|
+
* strong lines land at 15, 115, 215 — the tile boundary the SVG drew at 15.5
|
|
1956
|
+
* with a 1px stroke. Strong pairs are listed first because a background image
|
|
1957
|
+
* list paints first-on-top.
|
|
1958
|
+
*/
|
|
1959
|
+
.wave-docs-hero::before {
|
|
1960
|
+
content: '';
|
|
1961
|
+
position: absolute;
|
|
1962
|
+
/*
|
|
1963
|
+
* Bigger than the box and offset by a quarter, so a 3° rotation cannot
|
|
1964
|
+
* expose a corner. The overflow is clipped by the header.
|
|
1965
|
+
*/
|
|
1966
|
+
inset-block-start: -25%;
|
|
1967
|
+
inset-inline-start: -25%;
|
|
1968
|
+
width: 150%;
|
|
1969
|
+
height: 150%;
|
|
1970
|
+
/* Below the scrims, which are at -1. */
|
|
1971
|
+
z-index: -2;
|
|
1972
|
+
pointer-events: none;
|
|
1973
|
+
rotate: -3deg;
|
|
1974
|
+
background-image:
|
|
1975
|
+
repeating-linear-gradient(
|
|
1976
|
+
to right,
|
|
1977
|
+
transparent 0 15px,
|
|
1978
|
+
var(--wave-docs-hero-grid-strong) 15px 16px,
|
|
1979
|
+
transparent 16px 100px
|
|
1980
|
+
),
|
|
1981
|
+
repeating-linear-gradient(
|
|
1982
|
+
to bottom,
|
|
1983
|
+
transparent 0 15px,
|
|
1984
|
+
var(--wave-docs-hero-grid-strong) 15px 16px,
|
|
1985
|
+
transparent 16px 100px
|
|
1986
|
+
),
|
|
1987
|
+
repeating-linear-gradient(
|
|
1988
|
+
to right,
|
|
1989
|
+
transparent 0 15px,
|
|
1990
|
+
var(--wave-docs-hero-grid) 15px 16px,
|
|
1991
|
+
transparent 16px 20px
|
|
1992
|
+
),
|
|
1993
|
+
repeating-linear-gradient(
|
|
1994
|
+
to bottom,
|
|
1995
|
+
transparent 0 15px,
|
|
1996
|
+
var(--wave-docs-hero-grid) 15px 16px,
|
|
1997
|
+
transparent 16px 20px
|
|
1998
|
+
);
|
|
1588
1999
|
}
|
|
1589
2000
|
|
|
1590
|
-
|
|
2001
|
+
/*
|
|
2002
|
+
* ⚠️ THREE SCRIMS IN THE PAGE'S OWN COLOUR, PAINTED OVER THE GRID — NOT A
|
|
2003
|
+
* MASK.
|
|
2004
|
+
*
|
|
2005
|
+
* Every earlier version of this masked the grid layer: two or three gradients
|
|
2006
|
+
* multiplied with `mask-composite: intersect`. It worked, and it was never
|
|
2007
|
+
* smooth. Mask layers multiply *alphas*, so the falloffs compound — two
|
|
2008
|
+
* gentle ramps crossing make a hard shoulder, and one arithmetic slip turns
|
|
2009
|
+
* the whole thing into a ring. Painting the page colour on top instead is
|
|
2010
|
+
* ordinary alpha compositing of a solid: the ramps blend rather than
|
|
2011
|
+
* multiply, and each one can be reasoned about on its own.
|
|
2012
|
+
*
|
|
2013
|
+
* It also means the percentages are of the header, not of a layer 150% its
|
|
2014
|
+
* size — which is what three passes went wrong on.
|
|
2015
|
+
*
|
|
2016
|
+
* A separate element from the grid because this one must not rotate. The
|
|
2017
|
+
* grid is at −3°; a scrim tilted with it would put its own edge on a slant.
|
|
2018
|
+
*
|
|
2019
|
+
* Painted first-on-top, so the order below reads outermost-last:
|
|
2020
|
+
*
|
|
2021
|
+
* 1. the reading scrim — a soft ellipse over the words
|
|
2022
|
+
* 2. the bottom fade — nothing at the top, solid before the prose
|
|
2023
|
+
*
|
|
2024
|
+
* The corner vignette is not among them: it is the `box-shadow` below,
|
|
2025
|
+
* because a gradient cannot be given a corner radius and this shape needs
|
|
2026
|
+
* one.
|
|
2027
|
+
*/
|
|
2028
|
+
.wave-docs-hero::after {
|
|
2029
|
+
content: '';
|
|
2030
|
+
position: absolute;
|
|
2031
|
+
inset: 0;
|
|
2032
|
+
z-index: -1;
|
|
2033
|
+
pointer-events: none;
|
|
2034
|
+
background-image:
|
|
2035
|
+
radial-gradient(
|
|
2036
|
+
ellipse 52% 58% at 38% 48%,
|
|
2037
|
+
var(--wave-docs-bg) 0 25%,
|
|
2038
|
+
transparent 80%
|
|
2039
|
+
),
|
|
2040
|
+
linear-gradient(to bottom, transparent 40%, var(--wave-docs-bg) 90%);
|
|
1591
2041
|
/*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
2042
|
+
* ⚠️ THE VIGNETTE IS A SHADOW, NOT A RADIAL — BECAUSE A RADIAL HAS NO
|
|
2043
|
+
* CORNER RADIUS.
|
|
2044
|
+
*
|
|
2045
|
+
* `radial-gradient` is only ever a circle or an ellipse: its shape is two
|
|
2046
|
+
* radii and nothing else, so an elliptical vignette pinches the middle of
|
|
2047
|
+
* every edge and opens the corners. An inset `box-shadow` with blur and
|
|
2048
|
+
* spread fades inward from a *rounded rectangle*, which is the shape this
|
|
2049
|
+
* wants — and `border-radius` is the knob the gradient never had.
|
|
2050
|
+
*
|
|
2051
|
+
* ⚠️ `inset: -4rem`, SO THE BOX'S OWN CORNERS FALL OUTSIDE THE HEADER. One
|
|
2052
|
+
* `border-radius` sets both the shape the shadow fades from and the shape
|
|
2053
|
+
* the box is clipped to; oversizing the box puts the clip out of sight and
|
|
2054
|
+
* leaves only the inner shape doing any work.
|
|
2055
|
+
*
|
|
2056
|
+
* `corner-shape` turns that rounded rectangle into a true squircle. It is
|
|
2057
|
+
* Chrome 139+ and degrades to an ordinary radius everywhere else, which is
|
|
2058
|
+
* the same picture with slightly rounder corners — a progressive
|
|
2059
|
+
* enhancement in the honest sense rather than a fallback nobody checked.
|
|
1596
2060
|
*/
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2061
|
+
inset: -5rem;
|
|
2062
|
+
border-radius: 14rem;
|
|
2063
|
+
corner-shape: superellipse(3);
|
|
2064
|
+
/*
|
|
2065
|
+
* ⚠️ THE SPREAD HAS TO CLEAR THE OVERHANG BEFORE IT DOES ANY WORK. The box
|
|
2066
|
+
* sits 5rem outside the header on every side, so a spread of 5rem is solid
|
|
2067
|
+
* only as far as the header's rim and the whole fade happens out of sight.
|
|
2068
|
+
* 9rem puts the solid band 4rem inside, and the 10rem blur carries it in
|
|
2069
|
+
* from there.
|
|
2070
|
+
*/
|
|
2071
|
+
box-shadow: inset 0 0 10rem 9rem var(--wave-docs-bg);
|
|
1603
2072
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2073
|
+
/*
|
|
2074
|
+
* ⚠️ EVERY PERCENTAGE ABOVE IS OF THIS LAYER, NOT OF THE HEADER — AND THE
|
|
2075
|
+
* LAYER IS HALF AS BIG AGAIN.
|
|
2076
|
+
*
|
|
2077
|
+
* The header occupies the middle third of it, from 33% to 67% on both axes.
|
|
2078
|
+
* Written as `125% 100%` the radial's edge landed a long way outside the
|
|
2079
|
+
* header, so what showed inside it was all near-centre: 15px of blank along
|
|
2080
|
+
* every edge before the pattern faded up, measured. `83%` and `67%` are
|
|
2081
|
+
* those numbers divided by the 1.5 — the same ellipse, expressed against
|
|
2082
|
+
* the box it is actually seen through.
|
|
2083
|
+
*
|
|
2084
|
+
* The two linear layers are the edge fade, and they are why the pattern
|
|
2085
|
+
* dissolves into the page on all four sides instead of being cut off by
|
|
2086
|
+
* `overflow: clip`. Each one is transparent exactly at the header's edge —
|
|
2087
|
+
* 33% and 67% of the layer — and opaque a few per cent inside it, so the
|
|
2088
|
+
* ramp happens within the header rather than outside where nothing is seen.
|
|
2089
|
+
* The vertical one is asymmetric on purpose: it holds full only to the
|
|
2090
|
+
* midpoint before falling away, so the grid has faded out well before the
|
|
2091
|
+
* prose begins.
|
|
2092
|
+
*
|
|
2093
|
+
* ⚠️ THE RADIAL AND THESE PULL IN OPPOSITE DIRECTIONS, WHICH IS THE POINT.
|
|
2094
|
+
* The radial is transparent at the centre and opaque outward; these are
|
|
2095
|
+
* opaque inside and transparent at the rim. Multiplied, what survives is a
|
|
2096
|
+
* band between the two — a halo around the words that fades at the page
|
|
2097
|
+
* and fades again where the reading starts.
|
|
2098
|
+
*/
|
|
1616
2099
|
}
|
|
1617
2100
|
|
|
1618
|
-
/*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2101
|
+
/*
|
|
2102
|
+
* The heading, and it is the page's `<h1>` — `render` stands its own down
|
|
2103
|
+
* when a page declares `actions`. Sized here rather than inherited from
|
|
2104
|
+
* `.wave-docs-prose h1`, because it is outside the prose.
|
|
2105
|
+
*/
|
|
2106
|
+
/*
|
|
2107
|
+
* The words, aligned with the prose beneath them. The header itself spans the
|
|
2108
|
+
* whole article track so the grid behind it is full-bleed; this is what keeps
|
|
2109
|
+
* the title from sitting 70px left of the first paragraph.
|
|
2110
|
+
*/
|
|
2111
|
+
.wave-docs-hero__body {
|
|
2112
|
+
max-width: var(--wave-docs-measure);
|
|
2113
|
+
margin-inline: auto;
|
|
1626
2114
|
}
|
|
1627
2115
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2116
|
+
.wave-docs-hero__title {
|
|
2117
|
+
margin: 0;
|
|
2118
|
+
color: var(--wave-docs-fg);
|
|
2119
|
+
font-size: clamp(2.25rem, 1.5rem + 3vw, 3.5rem);
|
|
2120
|
+
font-weight: 700;
|
|
2121
|
+
line-height: 1.1;
|
|
2122
|
+
letter-spacing: -0.02em;
|
|
2123
|
+
text-wrap: balance;
|
|
2124
|
+
}
|
|
1631
2125
|
|
|
1632
|
-
.
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
height:
|
|
1639
|
-
|
|
1640
|
-
border-block-end: 1px solid var(--wave-docs-border);
|
|
2126
|
+
/* `frontmatter.description`, which is otherwise only a `<meta>` tag. */
|
|
2127
|
+
.wave-docs-hero__tagline {
|
|
2128
|
+
margin: 1rem 0 0;
|
|
2129
|
+
max-width: 34rem;
|
|
2130
|
+
color: var(--wave-docs-fg-muted);
|
|
2131
|
+
font-size: 1.125rem;
|
|
2132
|
+
line-height: 1.6;
|
|
2133
|
+
text-wrap: pretty;
|
|
1641
2134
|
}
|
|
1642
2135
|
|
|
1643
|
-
.wave-docs-
|
|
2136
|
+
.wave-docs-hero__actions {
|
|
1644
2137
|
display: flex;
|
|
1645
|
-
|
|
2138
|
+
flex-wrap: wrap;
|
|
1646
2139
|
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;
|
|
2140
|
+
margin-block-start: 2rem;
|
|
1654
2141
|
}
|
|
1655
2142
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2143
|
+
.wave-docs-hero__action {
|
|
2144
|
+
display: inline-flex;
|
|
2145
|
+
align-items: center;
|
|
2146
|
+
/* 44px tall: the same minimum the sidebar's trigger answers to. */
|
|
2147
|
+
min-height: 2.75rem;
|
|
2148
|
+
padding-inline: 1.25rem;
|
|
2149
|
+
border: 1px solid transparent;
|
|
2150
|
+
border-radius: var(--wave-docs-radius);
|
|
2151
|
+
font-size: 0.9375rem;
|
|
2152
|
+
font-weight: 600;
|
|
2153
|
+
text-decoration: none;
|
|
1660
2154
|
}
|
|
1661
2155
|
|
|
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;
|
|
2156
|
+
.wave-docs-hero__action[data-variant='primary'] {
|
|
2157
|
+
background-color: var(--wave-docs-accent);
|
|
2158
|
+
color: var(--wave-docs-accent-fg);
|
|
1670
2159
|
}
|
|
1671
2160
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
* the title that would not exist. */
|
|
1675
|
-
.wave-docs-layout__search {
|
|
1676
|
-
margin-inline-start: auto;
|
|
2161
|
+
.wave-docs-hero__action[data-variant='primary']:hover {
|
|
2162
|
+
background-color: var(--wave-docs-accent-hover);
|
|
1677
2163
|
}
|
|
1678
2164
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2165
|
+
/*
|
|
2166
|
+
* The tinted one. `accent-subtle` is the same surface the active sidebar link
|
|
2167
|
+
* sits on, and its 4.5:1 pair is `accent` — so this is a contrast ratio the
|
|
2168
|
+
* token test already checks rather than a new pairing to argue about.
|
|
2169
|
+
*/
|
|
2170
|
+
.wave-docs-hero__action[data-variant='secondary'] {
|
|
2171
|
+
background-color: var(--wave-docs-accent-subtle);
|
|
2172
|
+
color: var(--wave-docs-accent);
|
|
1686
2173
|
}
|
|
1687
2174
|
|
|
1688
|
-
.wave-docs-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2175
|
+
.wave-docs-hero__action[data-variant='secondary']:hover {
|
|
2176
|
+
border-color: var(--wave-docs-accent);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
.wave-docs-hero__action:focus-visible {
|
|
2180
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
2181
|
+
outline-offset: 2px;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2185
|
+
.wave-docs-hero__action {
|
|
2186
|
+
transition:
|
|
2187
|
+
background-color 120ms ease-out,
|
|
2188
|
+
border-color 120ms ease-out;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/* -------------------------------------------------------------------------
|
|
2193
|
+
* The sidebar
|
|
2194
|
+
* ---------------------------------------------------------------------- */
|
|
2195
|
+
|
|
2196
|
+
/*
|
|
2197
|
+
* It paints nothing. The navigation carries the surface and the border, the
|
|
2198
|
+
* trigger carries neither — so there is no third surface to keep in step with
|
|
2199
|
+
* the other two, which is what made the strip read as filled every previous
|
|
2200
|
+
* time this was drawn.
|
|
2201
|
+
*/
|
|
2202
|
+
.wave-docs-layout__sidebar {
|
|
2203
|
+
display: flex;
|
|
2204
|
+
/* No gap: the trigger sits directly against the navigation's border. */
|
|
2205
|
+
gap: 0;
|
|
2206
|
+
width: max-content;
|
|
2207
|
+
justify-self: start;
|
|
2208
|
+
/*
|
|
2209
|
+
* ⚠️ THE COLUMN IN THE PAGE, NOT THE PANEL THAT FOLLOWS THE VIEWPORT, AND
|
|
2210
|
+
* KEEPING THOSE TWO APART IS THE POINT.
|
|
2211
|
+
*
|
|
2212
|
+
* This box was `position: sticky` with `height: 100dvh` — sized against the
|
|
2213
|
+
* viewport while positioned against the layout, which is two reference
|
|
2214
|
+
* frames in one element. The moment a host put anything above it, the two
|
|
2215
|
+
* disagreed by exactly that much: with the UA's 8px of `body` margin the
|
|
2216
|
+
* divider stopped 8px short of the top and ran 8px past the bottom, and it
|
|
2217
|
+
* snapped into place as soon as the page scrolled and sticky engaged.
|
|
2218
|
+
* Measured.
|
|
2219
|
+
*
|
|
2220
|
+
* Stretched to the grid row, it is exactly as tall as the docs region —
|
|
2221
|
+
* starting where the host put it and ending where it ends, symmetric at any
|
|
2222
|
+
* offset, with no token to set and nothing to keep in sync. The scrolling
|
|
2223
|
+
* half is `position: sticky` on the two children, which is where a viewport
|
|
2224
|
+
* measurement belongs.
|
|
2225
|
+
*/
|
|
2226
|
+
align-self: stretch;
|
|
2227
|
+
/* So a page shorter than a screen does not leave the sticky panel hanging
|
|
2228
|
+
* below the column that draws its edge. */
|
|
2229
|
+
min-height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
2230
|
+
position: relative;
|
|
2231
|
+
/* Above the scrim, which is above the article. */
|
|
2232
|
+
z-index: 2;
|
|
1701
2233
|
background: none;
|
|
1702
|
-
|
|
1703
|
-
|
|
2234
|
+
border: 0;
|
|
2235
|
+
/*
|
|
2236
|
+
* ⚠️ THE BOX IS ALWAYS `max-content`; ONLY ITS CONTRIBUTION MOVES.
|
|
2237
|
+
*
|
|
2238
|
+
* This one declaration is the whole of "the article does not get out of the
|
|
2239
|
+
* way" — closed at any width, and open in cover mode. The box still paints
|
|
2240
|
+
* its full width, so in cover mode it overlays; it just does not ask the
|
|
2241
|
+
* grid for the room. Push mode drops it, and the same box takes its own
|
|
2242
|
+
* space with nothing else changing.
|
|
2243
|
+
*/
|
|
2244
|
+
margin-inline-end: calc(-1 * var(--wave-docs-sidebar-width));
|
|
2245
|
+
/*
|
|
2246
|
+
* Exactly the navigation's width, so it goes entirely off the page and the
|
|
2247
|
+
* trigger's outer edge lands on the inline start edge — whatever the strip
|
|
2248
|
+
* happens to be.
|
|
2249
|
+
*
|
|
2250
|
+
* ⚠️ THE SAME TOKEN AS THE MARGIN ABOVE, AND DELIBERATELY. It was
|
|
2251
|
+
* `calc(var(--wave-docs-trigger-width) - 100%)` — "minus all of me, plus the
|
|
2252
|
+
* trigger back" — which avoided naming the navigation but only worked while
|
|
2253
|
+
* the strip had a width to name. The strip is `auto` now, so this is both
|
|
2254
|
+
* shorter and the only spelling that cannot drift: the two declarations
|
|
2255
|
+
* that move the sidebar read the same token.
|
|
2256
|
+
*/
|
|
2257
|
+
translate: calc(-1 * var(--wave-docs-sidebar-width));
|
|
1704
2258
|
}
|
|
1705
2259
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2260
|
+
/*
|
|
2261
|
+
* ⚠️ THREE STATES, NOT TWO, AND THE THIRD ONE IS WHY THERE IS NO FLASH.
|
|
2262
|
+
*
|
|
2263
|
+
* `data-state` absent means "nobody has chosen yet" — the server renders
|
|
2264
|
+
* that, and CSS decides per mode: closed where the navigation would cover the
|
|
2265
|
+
* article, open where it would sit beside it. So the first paint is already
|
|
2266
|
+
* right at both shapes, with no JavaScript and nothing to correct. Once the
|
|
2267
|
+
* reader presses the trigger their choice is explicit and wins at every
|
|
2268
|
+
* width.
|
|
2269
|
+
*/
|
|
2270
|
+
.wave-docs-layout__sidebar[data-state='open'] {
|
|
2271
|
+
translate: 0;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/*
|
|
2275
|
+
* ⚠️ NOT UNTIL THE SECOND FRAME. A transition declared at first paint
|
|
2276
|
+
* animates the resolved state *in* from wherever the initial one was — the
|
|
2277
|
+
* navigation visibly slides across on every page load. `data-ready` is set
|
|
2278
|
+
* after mount, so the first paint is a jump nobody sees and every later
|
|
2279
|
+
* change is animated.
|
|
2280
|
+
*/
|
|
2281
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2282
|
+
.wave-docs-layout__sidebar[data-ready] {
|
|
2283
|
+
transition:
|
|
2284
|
+
translate 200ms ease,
|
|
2285
|
+
margin-inline-end 200ms ease;
|
|
2286
|
+
}
|
|
1709
2287
|
}
|
|
1710
2288
|
|
|
1711
2289
|
/* -------------------------------------------------------------------------
|
|
1712
|
-
*
|
|
2290
|
+
* The navigation
|
|
1713
2291
|
* ---------------------------------------------------------------------- */
|
|
1714
2292
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2293
|
+
/*
|
|
2294
|
+
* The only painted surface in the sidebar, and it has to be opaque: in cover
|
|
2295
|
+
* mode it sits on top of the article, and a translucent one would put two
|
|
2296
|
+
* columns of text through each other.
|
|
2297
|
+
*/
|
|
2298
|
+
.wave-docs-layout__sidebar-nav {
|
|
2299
|
+
flex: none;
|
|
1720
2300
|
box-sizing: border-box;
|
|
1721
|
-
width:
|
|
1722
|
-
|
|
1723
|
-
height: 100dvh;
|
|
1724
|
-
max-height: none;
|
|
1725
|
-
padding: 1.5rem 1rem;
|
|
2301
|
+
width: var(--wave-docs-sidebar-width);
|
|
2302
|
+
padding: 2rem 1rem;
|
|
1726
2303
|
border: 0;
|
|
2304
|
+
/*
|
|
2305
|
+
* Cover mode only, and it is a different thing from the divider in push
|
|
2306
|
+
* mode: there the line divides the page, here it is the edge of a panel
|
|
2307
|
+
* lying on top of one. A panel's edge is the panel's height, so this one
|
|
2308
|
+
* belongs on the panel.
|
|
2309
|
+
*/
|
|
1727
2310
|
border-inline-end: 1px solid var(--wave-docs-border);
|
|
1728
2311
|
background-color: var(--wave-docs-bg);
|
|
1729
|
-
|
|
2312
|
+
/* The panel that follows the viewport: sticky, one screen tall, and its own
|
|
2313
|
+
* scroller. `dvh`, not `vh` — with a mobile URL bar showing, `vh` is taller
|
|
2314
|
+
* than the visible viewport and the last few nav items cannot be reached. */
|
|
2315
|
+
position: sticky;
|
|
2316
|
+
top: var(--wave-docs-chrome-offset);
|
|
2317
|
+
height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
1730
2318
|
overflow-y: auto;
|
|
1731
|
-
/*
|
|
2319
|
+
/* Without this, reaching the end of the nav keeps scrolling — the article
|
|
2320
|
+
* jumps under the reader's cursor while they are looking at the sidebar. */
|
|
1732
2321
|
overscroll-behavior: contain;
|
|
2322
|
+
/* Reserves the scrollbar's width whether or not it is showing, so the
|
|
2323
|
+
* column does not jump when the nav grows past a screen. */
|
|
2324
|
+
scrollbar-gutter: stable;
|
|
1733
2325
|
}
|
|
1734
2326
|
|
|
1735
|
-
|
|
1736
|
-
|
|
2327
|
+
/* First child of the navigation, above the tree. */
|
|
2328
|
+
.wave-docs-layout__search {
|
|
2329
|
+
margin-block-end: 1rem;
|
|
1737
2330
|
}
|
|
1738
2331
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2332
|
+
/* -------------------------------------------------------------------------
|
|
2333
|
+
* The trigger
|
|
2334
|
+
* ---------------------------------------------------------------------- */
|
|
2335
|
+
|
|
2336
|
+
/*
|
|
2337
|
+
* ⚠️ NO BACKGROUND AND NO BORDER, IN ANY STATE. 44px wide because that is
|
|
2338
|
+
* Apple's minimum tap target and the strip *is* the target — it carried a
|
|
2339
|
+
* 20px transparent border for a while to answer to 44 while painting 24, and
|
|
2340
|
+
* a box the eye cannot see is a box the next person deletes.
|
|
2341
|
+
*/
|
|
2342
|
+
.wave-docs-layout__sidebar-trigger {
|
|
2343
|
+
flex: none;
|
|
2344
|
+
/*
|
|
2345
|
+
* Sticky for the same reason the navigation is: the column around it is the
|
|
2346
|
+
* page's height now, and a control centred in *that* would sit halfway down
|
|
2347
|
+
* a long document instead of halfway down the screen.
|
|
2348
|
+
*
|
|
2349
|
+
* ⚠️ AND IT IS ALSO WHAT POSITIONS THE BUTTON. The button and the dots are
|
|
2350
|
+
* `position: absolute`, and an absolutely positioned box resolves against
|
|
2351
|
+
* the nearest *positioned* ancestor — `sticky` qualifies, `static` does not.
|
|
2352
|
+
* Without a positioned ancestor here they escape to the layout and paint a
|
|
2353
|
+
* slab across the page. Measured.
|
|
2354
|
+
*/
|
|
2355
|
+
position: sticky;
|
|
2356
|
+
top: var(--wave-docs-chrome-offset);
|
|
2357
|
+
height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
2358
|
+
/*
|
|
2359
|
+
* ⚠️ `auto`, SO THE TARGET GROWS WITH THE BUTTON. The strip was a fixed
|
|
2360
|
+
* 2.75rem and the button was positioned inside it, which made the gap
|
|
2361
|
+
* between them a third number nobody set: 24px of button left 10px a side,
|
|
2362
|
+
* 16px left 14px. Sized by its content it is always the button plus 4px,
|
|
2363
|
+
* and `--wave-docs-trigger-width` is the only thing to change.
|
|
2364
|
+
*/
|
|
2365
|
+
display: grid;
|
|
2366
|
+
place-items: center;
|
|
2367
|
+
width: auto;
|
|
2368
|
+
padding: 4px;
|
|
1747
2369
|
border: 0;
|
|
1748
|
-
border-radius: var(--wave-docs-radius-sm);
|
|
1749
2370
|
background: none;
|
|
1750
2371
|
color: var(--wave-docs-fg-muted);
|
|
1751
2372
|
cursor: pointer;
|
|
1752
2373
|
}
|
|
1753
2374
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2375
|
+
/*
|
|
2376
|
+
* The button: `--wave-docs-trigger-width` × 80, and grey until hovered.
|
|
2377
|
+
*
|
|
2378
|
+
* ⚠️ IN FLOW, WHICH IS WHAT MAKES THE STRIP SIZE ITSELF. Absolutely
|
|
2379
|
+
* positioned it contributed nothing, so the strip needed its own width and
|
|
2380
|
+
* the gap between the two was whatever the subtraction happened to be. This
|
|
2381
|
+
* is the only in-flow child, so the strip is exactly this plus its 4px of
|
|
2382
|
+
* padding — one number, and the target follows the paint.
|
|
2383
|
+
*
|
|
2384
|
+
* `place-items: center` on the strip does the centring the insets used to.
|
|
2385
|
+
*/
|
|
2386
|
+
.wave-docs-layout__sidebar-trigger::before {
|
|
2387
|
+
content: '';
|
|
2388
|
+
width: var(--wave-docs-trigger-width);
|
|
2389
|
+
height: 5rem;
|
|
2390
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
2391
|
+
background-color: var(--wave-docs-border-strong);
|
|
1757
2392
|
}
|
|
1758
2393
|
|
|
1759
2394
|
/*
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
2395
|
+
* Three dots. Measured off docsify.js.org rather than reasoned about: three
|
|
2396
|
+
* 4px circles 3px apart. One element and two `box-shadow` copies rather than
|
|
2397
|
+
* three spans — no markup, and it follows `currentcolor` through the hover
|
|
2398
|
+
* state for free. 4 + 3 + 4 + 3 + 4 is 18px tall, so the offset is 7.
|
|
2399
|
+
*
|
|
2400
|
+
* ⚠️ `::after`, AND THE ORDER IS LOAD-BEARING. Neither pseudo-element carries
|
|
2401
|
+
* a `z-index`, so tree order decides which paints on top.
|
|
1763
2402
|
*/
|
|
1764
|
-
|
|
1765
|
-
|
|
2403
|
+
.wave-docs-layout__sidebar-trigger::after {
|
|
2404
|
+
content: '';
|
|
2405
|
+
position: absolute;
|
|
2406
|
+
inset-block-start: 50%;
|
|
2407
|
+
inset-inline-start: 50%;
|
|
2408
|
+
width: 4px;
|
|
2409
|
+
height: 4px;
|
|
2410
|
+
translate: -50% -50%;
|
|
2411
|
+
border-radius: 50%;
|
|
2412
|
+
background: currentcolor;
|
|
2413
|
+
box-shadow:
|
|
2414
|
+
0 -7px 0 currentcolor,
|
|
2415
|
+
0 7px 0 currentcolor;
|
|
1766
2416
|
}
|
|
1767
2417
|
|
|
1768
2418
|
/*
|
|
1769
|
-
*
|
|
2419
|
+
* ⚠️ THE STRIP'S HOVER LIGHTS THE BUTTON TOO, AND THAT IS WHAT MAKES IT ONE
|
|
2420
|
+
* CONTROL. Hovering 44px of strip while the button inside it stays grey reads
|
|
2421
|
+
* as a dead zone around a small target; Docsify pairs them for the same
|
|
2422
|
+
* reason.
|
|
2423
|
+
*/
|
|
2424
|
+
.wave-docs-layout__sidebar-trigger:hover {
|
|
2425
|
+
background-color: color-mix(
|
|
2426
|
+
in oklab,
|
|
2427
|
+
var(--wave-docs-accent) 30%,
|
|
2428
|
+
transparent
|
|
2429
|
+
);
|
|
2430
|
+
color: var(--wave-docs-accent-fg);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
.wave-docs-layout__sidebar-trigger:hover::before {
|
|
2434
|
+
background-color: var(--wave-docs-accent);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2438
|
+
.wave-docs-layout__sidebar-trigger,
|
|
2439
|
+
.wave-docs-layout__sidebar-trigger::before,
|
|
2440
|
+
.wave-docs-layout__sidebar-trigger::after {
|
|
2441
|
+
/*
|
|
2442
|
+
* ⚠️ ON THE PSEUDO-ELEMENTS TOO, NOT ONLY THE BUTTON. The dots are
|
|
2443
|
+
* `background: currentcolor`, and an inherited colour changing does not
|
|
2444
|
+
* start a transition on a descendant that has not declared one — the
|
|
2445
|
+
* button would fade and the dots would snap.
|
|
2446
|
+
*/
|
|
2447
|
+
transition:
|
|
2448
|
+
background-color 120ms ease-out,
|
|
2449
|
+
color 120ms ease-out;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/*
|
|
2454
|
+
* ⚠️ AN OUTLINE, NOT THE FILL. These shared the hover rule for one commit,
|
|
2455
|
+
* and `focus-indicators` caught it: a background change is the one thing
|
|
2456
|
+
* forced-colours mode discards, so a keyboard reader there would have had no
|
|
2457
|
+
* indicator at all.
|
|
2458
|
+
*/
|
|
2459
|
+
.wave-docs-layout__sidebar-trigger:focus-visible {
|
|
2460
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
2461
|
+
outline-offset: -2px;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/* -------------------------------------------------------------------------
|
|
2465
|
+
* The scrim
|
|
1770
2466
|
*
|
|
1771
|
-
*
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1774
|
-
|
|
1775
|
-
|
|
2467
|
+
* Cover mode only. `inert` keeps the keyboard out of what is underneath; this
|
|
2468
|
+
* is what tells everyone else.
|
|
2469
|
+
* ---------------------------------------------------------------------- */
|
|
2470
|
+
|
|
2471
|
+
.wave-docs-layout__sidebar-scrim {
|
|
2472
|
+
/* Absolute inside the layout rather than fixed to the viewport: a fixed
|
|
2473
|
+
* scrim would darken a host's own chrome along with our article. */
|
|
2474
|
+
position: absolute;
|
|
2475
|
+
inset: 0;
|
|
2476
|
+
z-index: 1;
|
|
2477
|
+
border: 0;
|
|
2478
|
+
padding: 0;
|
|
2479
|
+
background-color: oklch(0.2 0.02 262 / 0.55);
|
|
2480
|
+
opacity: 0;
|
|
2481
|
+
pointer-events: none;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
.wave-docs-layout__sidebar[data-state='open']
|
|
2485
|
+
~ .wave-docs-layout__sidebar-scrim {
|
|
2486
|
+
opacity: 1;
|
|
2487
|
+
pointer-events: auto;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2491
|
+
.wave-docs-layout__sidebar-scrim {
|
|
2492
|
+
transition: opacity 200ms ease;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
/* -------------------------------------------------------------------------
|
|
2497
|
+
* Cover and push
|
|
2498
|
+
*
|
|
2499
|
+
* The whole of what differs between a narrow container and a wide one. Same
|
|
2500
|
+
* markup, same classes, same control, same translate — only whether the
|
|
2501
|
+
* article gets out of the way.
|
|
2502
|
+
* ---------------------------------------------------------------------- */
|
|
2503
|
+
|
|
2504
|
+
/*
|
|
2505
|
+
* ⚠️ A CUSTOM PROPERTY, BECAUSE JAVASCRIPT CANNOT ASK A CONTAINER QUERY.
|
|
2506
|
+
* `matchMedia` answers about the viewport, which is the question this file
|
|
2507
|
+
* deliberately stopped asking — so the mode is declared here and read back
|
|
2508
|
+
* with `getComputedStyle`. One source of truth, and the breakpoint appears
|
|
2509
|
+
* nowhere in the component.
|
|
1776
2510
|
*/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2511
|
+
.wave-docs-layout__sidebar {
|
|
2512
|
+
--wave-docs-sidebar-mode: cover;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
@container wave-docs (min-width: 64rem) {
|
|
2516
|
+
.wave-docs-layout__sidebar {
|
|
2517
|
+
--wave-docs-sidebar-mode: push;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/*
|
|
2521
|
+
* ⚠️ THE DIVISION IS THE PAGE'S, SO IT IS DRAWN BY THE PAGE'S COLUMN.
|
|
2522
|
+
*
|
|
2523
|
+
* In push mode the sidebar is part of the layout and this line is what
|
|
2524
|
+
* separates navigation from article — so it runs the full height of the
|
|
2525
|
+
* docs region, top to bottom, wherever the host placed it. Drawn on the
|
|
2526
|
+
* navigation instead it would be one screen tall, which is only the same
|
|
2527
|
+
* thing when the layout happens to start at the viewport's top edge.
|
|
2528
|
+
*
|
|
2529
|
+
* `inset-inline-start` at the navigation's width rather than a border on
|
|
2530
|
+
* the shell: the shell's own inline end is past the trigger, and the
|
|
2531
|
+
* division belongs between the navigation and the article.
|
|
2532
|
+
*/
|
|
2533
|
+
.wave-docs-layout__sidebar::after {
|
|
2534
|
+
content: '';
|
|
2535
|
+
position: absolute;
|
|
2536
|
+
inset-block: 0;
|
|
2537
|
+
inset-inline-start: var(--wave-docs-sidebar-width);
|
|
2538
|
+
width: 1px;
|
|
2539
|
+
background-color: var(--wave-docs-border);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/*
|
|
2543
|
+
* Unchosen means open up here, where there is room for the navigation
|
|
2544
|
+
* beside the article rather than on top of it — and open in push mode is
|
|
2545
|
+
* the one combination that asks the grid for its own space.
|
|
2546
|
+
*
|
|
2547
|
+
* ⚠️ KEYED ON THE STATE, NOT THE MODE. Dropping the margin for the whole of
|
|
2548
|
+
* push mode leaves a 300px hole in the grid whenever the sidebar is closed:
|
|
2549
|
+
* the box is translated off the page and the track is still holding room
|
|
2550
|
+
* for it.
|
|
2551
|
+
*/
|
|
2552
|
+
.wave-docs-layout__sidebar:not([data-state='closed']) {
|
|
2553
|
+
translate: 0;
|
|
2554
|
+
margin-inline-end: 0;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
/* Nothing is covered, so nothing is scrimmed. */
|
|
2558
|
+
.wave-docs-layout__sidebar-scrim {
|
|
1780
2559
|
display: none;
|
|
1781
2560
|
}
|
|
2561
|
+
|
|
2562
|
+
/* And the panel's edge gives way to the page's division above. */
|
|
2563
|
+
.wave-docs-layout__sidebar-nav {
|
|
2564
|
+
border-inline-end: 0;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
.wave-docs-layout {
|
|
2568
|
+
gap: 0 1.5rem;
|
|
2569
|
+
padding-inline: 0 1.5rem;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
@container wave-docs (min-width: 80rem) {
|
|
2574
|
+
.wave-docs-layout:has(.wave-docs-layout__toc) {
|
|
2575
|
+
grid-template-columns: auto minmax(0, 1fr) var(--wave-docs-toc-width);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
.wave-docs-layout__toc {
|
|
2579
|
+
display: block;
|
|
2580
|
+
/* Same reason as the article: its contents cannot be weighed against the
|
|
2581
|
+
* scrim, only the column as a whole. */
|
|
2582
|
+
isolation: isolate;
|
|
2583
|
+
position: sticky;
|
|
2584
|
+
/* `align-self: start`, or the sticky box stretches to the row height and
|
|
2585
|
+
* has nothing left to travel within. */
|
|
2586
|
+
align-self: start;
|
|
2587
|
+
top: var(--wave-docs-chrome-offset);
|
|
2588
|
+
max-height: calc(100dvh - var(--wave-docs-chrome-offset));
|
|
2589
|
+
overflow-y: auto;
|
|
2590
|
+
overscroll-behavior: contain;
|
|
2591
|
+
padding-block: 2rem;
|
|
2592
|
+
}
|
|
1782
2593
|
}
|
|
1783
2594
|
}
|
|
1784
2595
|
|
|
@@ -1809,6 +2620,25 @@
|
|
|
1809
2620
|
cursor: pointer;
|
|
1810
2621
|
}
|
|
1811
2622
|
|
|
2623
|
+
/*
|
|
2624
|
+
* In the shell, the trigger fills whatever it is placed in — the width of the
|
|
2625
|
+
* strip below 64rem, the width of the column above it.
|
|
2626
|
+
*
|
|
2627
|
+
* ⚠️ THIS USED TO BE A THREE-STEP `max-width` LADDER (9rem, 11rem at 30rem,
|
|
2628
|
+
* 20rem at 48rem) AND EVERY NUMBER IN IT WAS HEADER ARITHMETIC. The trigger
|
|
2629
|
+
* is `width: 100%`, so in a flex row its base was the whole row; the ladder
|
|
2630
|
+
* existed to stop it starving the brand beside it, and at 390px it had
|
|
2631
|
+
* ellipsised "Wave Docs" to "Wa…". With no header there is no row to share
|
|
2632
|
+
* and no brand to leave room for, so the container decides.
|
|
2633
|
+
*
|
|
2634
|
+
* The bare `.wave-docs-search-trigger` keeps its own `max-width: 20rem` for
|
|
2635
|
+
* the standalone case — a host rendering `DocsSearch` in their own header,
|
|
2636
|
+
* where something else *is* competing for the row.
|
|
2637
|
+
*/
|
|
2638
|
+
.wave-docs-search-trigger.wave-docs-layout__search {
|
|
2639
|
+
max-width: none;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
1812
2642
|
.wave-docs-search-trigger:hover {
|
|
1813
2643
|
border-color: var(--wave-docs-border-strong);
|
|
1814
2644
|
color: var(--wave-docs-fg);
|
|
@@ -1828,6 +2658,18 @@
|
|
|
1828
2658
|
white-space: nowrap;
|
|
1829
2659
|
}
|
|
1830
2660
|
|
|
2661
|
+
/*
|
|
2662
|
+
* ⚠️ HIDDEN WHERE THE SHORTCUT CANNOT BE PRESSED, WHICH IS NOT A WIDTH
|
|
2663
|
+
* QUESTION. `⌘K` is an instruction, and on a phone or a tablet it is one the
|
|
2664
|
+
* reader cannot follow — a 10-inch tablet at 1024px would keep it under any
|
|
2665
|
+
* breakpoint rule.
|
|
2666
|
+
*/
|
|
2667
|
+
@media (hover: none) and (pointer: coarse) {
|
|
2668
|
+
.wave-docs-search-trigger-kbd {
|
|
2669
|
+
display: none;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
|
|
1831
2673
|
.wave-docs-search-trigger-kbd {
|
|
1832
2674
|
flex: none;
|
|
1833
2675
|
padding: 0.05rem 0.35rem;
|