@waveso/docs 0.1.0 → 0.3.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 +194 -0
- package/README.md +592 -88
- package/dist/code-frame.d.ts +29 -0
- package/dist/code-frame.js +41 -0
- package/dist/code-meta.d.ts +48 -0
- package/dist/code-meta.js +72 -0
- package/dist/docs-content-id.d.ts +19 -0
- package/dist/docs-content-id.js +19 -0
- package/dist/docs-error.d.ts +19 -0
- package/dist/docs-error.js +28 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.js +45 -0
- package/dist/frontmatter.d.ts +39 -7
- package/dist/frontmatter.js +51 -24
- package/dist/highlighter.d.ts +2 -2
- package/dist/highlighter.js +3 -2
- package/dist/map-pooled.d.ts +26 -0
- package/dist/map-pooled.js +45 -0
- package/dist/meta.d.ts +7 -3
- package/dist/meta.js +61 -15
- package/dist/next.d.ts +182 -35
- package/dist/next.js +177 -49
- package/dist/plugins/rehype-capture-toc.js +52 -20
- package/dist/plugins/rehype-code-frame.d.ts +10 -0
- package/dist/plugins/rehype-code-frame.js +88 -0
- package/dist/plugins/rehype-code-language.d.ts +24 -0
- package/dist/plugins/rehype-code-language.js +54 -0
- package/dist/plugins/rehype-fallback-heading-ids.d.ts +6 -0
- package/dist/plugins/rehype-fallback-heading-ids.js +51 -0
- package/dist/plugins/rehype-flatten-roots.d.ts +7 -0
- package/dist/plugins/rehype-flatten-roots.js +39 -0
- package/dist/plugins/remark-doc-links.d.ts +12 -1
- package/dist/plugins/remark-doc-links.js +147 -20
- package/dist/react/code-runtime.d.ts +14 -0
- package/dist/react/code-runtime.js +161 -0
- package/dist/react/doc-content.d.ts +39 -2
- package/dist/react/doc-content.js +42 -10
- package/dist/react/layout.d.ts +44 -0
- package/dist/react/layout.js +65 -0
- package/dist/react/markdown-components.js +71 -6
- package/dist/react/nav.d.ts +28 -0
- package/dist/react/nav.js +70 -0
- package/dist/react/nearest-scroll-top.d.ts +45 -0
- package/dist/react/nearest-scroll-top.js +44 -0
- package/dist/react/next-link.d.ts +34 -0
- package/dist/react/next-link.js +30 -0
- package/dist/react/next-nav.d.ts +11 -0
- package/dist/react/next-nav.js +32 -0
- package/dist/react/next-search.d.ts +22 -0
- package/dist/react/next-search.js +52 -0
- package/dist/react/search-dialog.d.ts +35 -7
- package/dist/react/search-dialog.js +55 -33
- package/dist/react/shell-labels.d.ts +43 -0
- package/dist/react/shell-labels.js +27 -0
- package/dist/react/sidebar.d.ts +38 -3
- package/dist/react/sidebar.js +104 -12
- package/dist/react/skip-link.d.ts +1 -9
- package/dist/react/skip-link.js +6 -5
- package/dist/react/toc.d.ts +12 -4
- package/dist/react/toc.js +46 -12
- package/dist/react/youtube.d.ts +31 -5
- package/dist/react/youtube.js +76 -52
- package/dist/render.d.ts +78 -10
- package/dist/render.js +137 -54
- package/dist/route-path.d.ts +46 -0
- package/dist/route-path.js +51 -0
- package/dist/search-index.d.ts +22 -21
- package/dist/search-index.js +27 -78
- package/dist/search-options.d.ts +32 -1
- package/dist/search-options.js +66 -3
- package/dist/section-boundary.d.ts +17 -0
- package/dist/section-boundary.js +43 -0
- package/dist/sitemap-limit.d.ts +34 -0
- package/dist/sitemap-limit.js +37 -0
- package/dist/source.d.ts +12 -22
- package/dist/source.js +165 -72
- package/dist/styles.css +1117 -125
- package/dist/types.d.ts +52 -29
- package/package.json +70 -34
package/dist/styles.css
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* the classes. Both win, and NOT because of source order — everything this file
|
|
15
15
|
* declares lives in a `@layer`, and unlayered CSS beats every layer regardless
|
|
16
16
|
* of specificity. That distinction is the whole fix: the dark token block is
|
|
17
|
-
* `:root
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* `:root[data-theme='dark']`, which is (0,2,0), so an unlayered `:root` at
|
|
18
|
+
* (0,1,0) would lose no matter where it was loaded — the cascade never gets as
|
|
19
|
+
* far as order. A consumer would have to write `:root:root:root` to outrank us.
|
|
20
20
|
*
|
|
21
21
|
* `@tailwindcss/typography` is deliberately not used. `.prose` pins
|
|
22
22
|
* `max-width: 65ch` — wrong inside a three-column docs shell — and styles
|
|
@@ -24,12 +24,6 @@
|
|
|
24
24
|
* below does the same job without the fight.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
/* Register this package's compiled components as Tailwind sources, so a
|
|
28
|
-
* consumer using Tailwind gets any utility class we ship. It belongs here, not
|
|
29
|
-
* in the app: a consumer-side `@source "../node_modules/@waveso/docs"` hardcodes
|
|
30
|
-
* the relative depth of node_modules and breaks under pnpm's nested store. */
|
|
31
|
-
@source "./";
|
|
32
|
-
|
|
33
27
|
/* Explicit, so the order does not silently depend on which block appears
|
|
34
28
|
* first. `theme` holds the tokens, `base` the element resets, `components`
|
|
35
29
|
* the classes. A consumer's own unlayered CSS outranks all three. */
|
|
@@ -38,10 +32,17 @@
|
|
|
38
32
|
/* ---------------------------------------------------------------------------
|
|
39
33
|
* Tokens
|
|
40
34
|
*
|
|
35
|
+
* ⚠️ DARK IS OPT-IN, NEVER AUTOMATIC. This sheet paints a region of somebody
|
|
36
|
+
* else's page, and it cannot see that page's background. Installing the dark
|
|
37
|
+
* ramp from `prefers-color-scheme` alone put near-white text (1.23:1) on the
|
|
38
|
+
* white page of every light-only site with a /docs section — and on every site
|
|
39
|
+
* using next-themes, whose default `attribute="class"` sets `.dark` and never
|
|
40
|
+
* `data-theme`. So: `[data-theme='dark']` or `.dark` switches, and a host that
|
|
41
|
+
* genuinely wants to follow the OS says so with `[data-theme='system']`.
|
|
42
|
+
*
|
|
41
43
|
* The dark block is spelled twice on purpose: `@media` and an attribute
|
|
42
|
-
* selector cannot be combined into one selector list.
|
|
43
|
-
*
|
|
44
|
-
* `[data-theme='dark']` opts into dark on a light one.
|
|
44
|
+
* selector cannot be combined into one selector list. The two copies are
|
|
45
|
+
* asserted identical by `src/styles.test.ts`.
|
|
45
46
|
* ------------------------------------------------------------------------ */
|
|
46
47
|
|
|
47
48
|
/*
|
|
@@ -56,16 +57,22 @@
|
|
|
56
57
|
* ~0.555 and still clear 4.5:1 on `bg-subtle`; `fg-muted` moved down to keep
|
|
57
58
|
* three visibly distinct steps rather than two-and-a-half.
|
|
58
59
|
*
|
|
59
|
-
* The dark ramp is fg (0.93) / fg-muted (0.72) / fg-subtle (0.
|
|
60
|
+
* The dark ramp is fg (0.93) / fg-muted (0.72) / fg-subtle (0.665), and its
|
|
60
61
|
* `accent-subtle` sits at 0.26 rather than 0.30 — `fg-subtle` paints the search
|
|
61
62
|
* breadcrumb, which lands on `accent-subtle` in the highlighted result, and a
|
|
62
63
|
* lighter tint there would force `fg-subtle` up into `fg-muted`.
|
|
63
64
|
*/
|
|
64
65
|
@layer theme {
|
|
65
66
|
/* Layered so a consumer's unlayered `:root` outranks these without having
|
|
66
|
-
* to out-specify `:root
|
|
67
|
-
*
|
|
67
|
+
* to out-specify `:root[data-theme='dark']`. Inside this layer the usual
|
|
68
|
+
* rules still apply, so the dark blocks keep beating the light one. */
|
|
68
69
|
:root {
|
|
70
|
+
/* Native scrollbars, form controls and the canvas behind an overscroll are
|
|
71
|
+
* painted by the UA, not by us; without this they stay light on a dark
|
|
72
|
+
* page. It is declared alongside the ramp it belongs to, so the two can
|
|
73
|
+
* never disagree. */
|
|
74
|
+
color-scheme: light;
|
|
75
|
+
|
|
69
76
|
--wave-docs-bg: oklch(1 0 0);
|
|
70
77
|
--wave-docs-bg-subtle: oklch(0.985 0.002 260);
|
|
71
78
|
--wave-docs-fg: oklch(0.25 0.02 262);
|
|
@@ -84,7 +91,11 @@
|
|
|
84
91
|
|
|
85
92
|
--wave-docs-code-bg: oklch(0.97 0.003 262);
|
|
86
93
|
--wave-docs-code-fg: oklch(0.32 0.03 300);
|
|
87
|
-
|
|
94
|
+
/* Darker than `bg-subtle`: this one has to read as a surface against the
|
|
95
|
+
* page (1.07:1) rather than merge into it, because it is the only thing
|
|
96
|
+
* separating a code block from the prose around it apart from a 1px
|
|
97
|
+
* border. */
|
|
98
|
+
--wave-docs-code-block-bg: oklch(0.975 0.003 262);
|
|
88
99
|
|
|
89
100
|
/* Each accent is the label colour on its own tinted background, so each one
|
|
90
101
|
* is a 4.5:1 pair in its own right — not merely legible against the page. */
|
|
@@ -94,29 +105,84 @@
|
|
|
94
105
|
--wave-docs-callout-tip-bg: oklch(0.97 0.03 156);
|
|
95
106
|
--wave-docs-callout-important: oklch(0.55 0.19 300);
|
|
96
107
|
--wave-docs-callout-important-bg: oklch(0.97 0.025 300);
|
|
97
|
-
--wave-docs-callout-warning: oklch(0.53 0.14
|
|
98
|
-
--wave-docs-callout-warning-bg: oklch(0.97 0.04
|
|
108
|
+
--wave-docs-callout-warning: oklch(0.53 0.14 80);
|
|
109
|
+
--wave-docs-callout-warning-bg: oklch(0.97 0.04 80);
|
|
99
110
|
--wave-docs-callout-caution: oklch(0.55 0.2 24);
|
|
100
111
|
--wave-docs-callout-caution-bg: oklch(0.97 0.025 24);
|
|
101
112
|
|
|
113
|
+
/* The table's "there is more this way" shadow.
|
|
114
|
+
*
|
|
115
|
+
* A token, not a literal: it was a hardcoded `oklch(0 0 0 / 0.12)`, and
|
|
116
|
+
* black at 12% over a `0.19` background is invisible — so in dark mode the
|
|
117
|
+
* one affordance telling a reader a table scrolls sideways was not there at
|
|
118
|
+
* all. A shadow darkens a light page and lightens a dark one; it cannot be
|
|
119
|
+
* a single colour. */
|
|
120
|
+
--wave-docs-scroll-shadow: oklch(0 0 0 / 0.12);
|
|
121
|
+
/* Elevation, for the one element that floats over the page. Same
|
|
122
|
+
* reasoning as `scroll-shadow`: a black shadow over a dark surface is
|
|
123
|
+
* no shadow, so the dark ramp deepens it rather than reusing it. */
|
|
124
|
+
--wave-docs-shadow: oklch(0 0 0 / 0.3);
|
|
125
|
+
|
|
102
126
|
--wave-docs-radius: 0.5rem;
|
|
103
127
|
--wave-docs-radius-sm: 0.375rem;
|
|
104
|
-
|
|
128
|
+
/* The shell, per `docs/adr/001-shell-contract.md`. These four plus
|
|
129
|
+
* `--wave-docs-measure` are the whole settable layout surface; the gutter
|
|
130
|
+
* and the drawer width are literals, because each appears once and an
|
|
131
|
+
* ordinary override is already the cleanest tool for them. */
|
|
132
|
+
--wave-docs-header-height: 3.5rem;
|
|
133
|
+
--wave-docs-sidebar-width: 16rem;
|
|
134
|
+
--wave-docs-toc-width: 15rem;
|
|
135
|
+
--wave-docs-shell-width: 100rem;
|
|
136
|
+
|
|
137
|
+
/* How far below the top of the scrollport an anchored heading parks.
|
|
138
|
+
*
|
|
139
|
+
* Derived, not set: it is the sticky header's height plus a rem of air, and
|
|
140
|
+
* the shell's sticky columns offset by the same token — so a host that
|
|
141
|
+
* changes the header height moves all three together. An earlier version of
|
|
142
|
+
* this comment said nothing else in the sheet read it, which stopped being
|
|
143
|
+
* true the moment the layout landed. */
|
|
144
|
+
--wave-docs-scroll-padding: calc(var(--wave-docs-header-height) + 1rem);
|
|
105
145
|
--wave-docs-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
|
|
106
146
|
Consolas, "Liberation Mono", monospace;
|
|
107
147
|
|
|
108
|
-
/*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
/* The body typeface.
|
|
149
|
+
*
|
|
150
|
+
* A default rather than an inheritance, because inheriting means a host
|
|
151
|
+
* that never set one renders documentation in the UA serif — which is what
|
|
152
|
+
* this package did until now, and which reads as broken rather than as
|
|
153
|
+
* unstyled.
|
|
154
|
+
*
|
|
155
|
+
* A host with its own typeface sets `--wave-docs-font-sans: inherit`, and
|
|
156
|
+
* the whole package follows `body` again. That is one line, and it is the
|
|
157
|
+
* documented opt-out; overriding the family on every element this sheet
|
|
158
|
+
* touches is not. */
|
|
159
|
+
--wave-docs-font-sans: ui-sans-serif, system-ui, -apple-system,
|
|
160
|
+
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
161
|
+
|
|
162
|
+
/* Prose column width.
|
|
163
|
+
*
|
|
164
|
+
* 46rem is 736px at a 16px root, about 83 `ch` in the stack above — wider
|
|
165
|
+
* than a pure-prose measure wants, and deliberately: this column also
|
|
166
|
+
* carries code blocks, API tables and callouts, and narrowing it to suit
|
|
167
|
+
* paragraphs pushes those into horizontal scroll. Without any measure at
|
|
168
|
+
* all, a 1440px viewport produced a ~140-character line.
|
|
169
|
+
*
|
|
170
|
+
* `--wave-docs-measure: none` opts out entirely, for a host whose own
|
|
171
|
+
* shell already constrains the column. See `docs/adr/001-shell-contract.md`
|
|
172
|
+
* — this is one of the five settable layout tokens. */
|
|
173
|
+
--wave-docs-measure: 46rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Only for a host that has explicitly delegated the choice to the OS. */
|
|
113
177
|
@media (prefers-color-scheme: dark) {
|
|
114
|
-
:root
|
|
178
|
+
:root[data-theme='system'] {
|
|
179
|
+
color-scheme: dark;
|
|
180
|
+
|
|
115
181
|
--wave-docs-bg: oklch(0.19 0.015 265);
|
|
116
182
|
--wave-docs-bg-subtle: oklch(0.23 0.015 265);
|
|
117
183
|
--wave-docs-fg: oklch(0.93 0.008 265);
|
|
118
184
|
--wave-docs-fg-muted: oklch(0.72 0.014 265);
|
|
119
|
-
--wave-docs-fg-subtle: oklch(0.
|
|
185
|
+
--wave-docs-fg-subtle: oklch(0.665 0.015 265);
|
|
120
186
|
--wave-docs-border: oklch(0.31 0.014 265);
|
|
121
187
|
--wave-docs-border-strong: oklch(0.4 0.016 265);
|
|
122
188
|
|
|
@@ -125,6 +191,9 @@
|
|
|
125
191
|
--wave-docs-accent-fg: oklch(0.18 0.02 265);
|
|
126
192
|
--wave-docs-accent-subtle: oklch(0.26 0.06 248);
|
|
127
193
|
|
|
194
|
+
--wave-docs-scroll-shadow: oklch(1 0 0 / 0.16);
|
|
195
|
+
--wave-docs-shadow: oklch(0 0 0 / 0.6);
|
|
196
|
+
|
|
128
197
|
--wave-docs-code-bg: oklch(0.26 0.016 265);
|
|
129
198
|
--wave-docs-code-fg: oklch(0.86 0.04 300);
|
|
130
199
|
--wave-docs-code-block-bg: oklch(0.17 0.015 265);
|
|
@@ -135,19 +204,28 @@
|
|
|
135
204
|
--wave-docs-callout-tip-bg: oklch(0.27 0.05 156);
|
|
136
205
|
--wave-docs-callout-important: oklch(0.76 0.14 300);
|
|
137
206
|
--wave-docs-callout-important-bg: oklch(0.28 0.06 300);
|
|
138
|
-
--wave-docs-callout-warning: oklch(0.82 0.14
|
|
139
|
-
--wave-docs-callout-warning-bg: oklch(0.29 0.05
|
|
207
|
+
--wave-docs-callout-warning: oklch(0.82 0.14 80);
|
|
208
|
+
--wave-docs-callout-warning-bg: oklch(0.29 0.05 80);
|
|
140
209
|
--wave-docs-callout-caution: oklch(0.72 0.16 24);
|
|
141
210
|
--wave-docs-callout-caution-bg: oklch(0.28 0.06 24);
|
|
142
211
|
}
|
|
143
212
|
}
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
/* `.dark` as well as `[data-theme='dark']`: next-themes defaults to
|
|
215
|
+
* `attribute="class"`, and that default is what most consumers ship. */
|
|
216
|
+
:root[data-theme='dark'],
|
|
217
|
+
:root.dark {
|
|
218
|
+
color-scheme: dark;
|
|
219
|
+
|
|
146
220
|
--wave-docs-bg: oklch(0.19 0.015 265);
|
|
147
221
|
--wave-docs-bg-subtle: oklch(0.23 0.015 265);
|
|
148
222
|
--wave-docs-fg: oklch(0.93 0.008 265);
|
|
149
223
|
--wave-docs-fg-muted: oklch(0.72 0.014 265);
|
|
150
|
-
|
|
224
|
+
/* 0.665, not 0.64: `fg-subtle` paints the list markers and heading
|
|
225
|
+
* anchors, and inside a callout it composes against `callout-*-bg`, where
|
|
226
|
+
* 0.64 measured 4.21-4.47:1. It still clears 4.5:1 on `accent-subtle`,
|
|
227
|
+
* which is the constraint pulling it the other way. */
|
|
228
|
+
--wave-docs-fg-subtle: oklch(0.665 0.015 265);
|
|
151
229
|
--wave-docs-border: oklch(0.31 0.014 265);
|
|
152
230
|
--wave-docs-border-strong: oklch(0.4 0.016 265);
|
|
153
231
|
|
|
@@ -156,6 +234,9 @@
|
|
|
156
234
|
--wave-docs-accent-fg: oklch(0.18 0.02 265);
|
|
157
235
|
--wave-docs-accent-subtle: oklch(0.26 0.06 248);
|
|
158
236
|
|
|
237
|
+
--wave-docs-scroll-shadow: oklch(1 0 0 / 0.16);
|
|
238
|
+
--wave-docs-shadow: oklch(0 0 0 / 0.6);
|
|
239
|
+
|
|
159
240
|
--wave-docs-code-bg: oklch(0.26 0.016 265);
|
|
160
241
|
--wave-docs-code-fg: oklch(0.86 0.04 300);
|
|
161
242
|
--wave-docs-code-block-bg: oklch(0.17 0.015 265);
|
|
@@ -166,33 +247,88 @@
|
|
|
166
247
|
--wave-docs-callout-tip-bg: oklch(0.27 0.05 156);
|
|
167
248
|
--wave-docs-callout-important: oklch(0.76 0.14 300);
|
|
168
249
|
--wave-docs-callout-important-bg: oklch(0.28 0.06 300);
|
|
169
|
-
--wave-docs-callout-warning: oklch(0.82 0.14
|
|
170
|
-
--wave-docs-callout-warning-bg: oklch(0.29 0.05
|
|
250
|
+
--wave-docs-callout-warning: oklch(0.82 0.14 80);
|
|
251
|
+
--wave-docs-callout-warning-bg: oklch(0.29 0.05 80);
|
|
171
252
|
--wave-docs-callout-caution: oklch(0.72 0.16 24);
|
|
172
253
|
--wave-docs-callout-caution-bg: oklch(0.28 0.06 24);
|
|
173
254
|
}
|
|
174
|
-
|
|
175
|
-
/* ---------------------------------------------------------------------------
|
|
176
|
-
* Base
|
|
177
|
-
* ------------------------------------------------------------------------ */
|
|
178
255
|
}
|
|
179
256
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
* sticky header. Match it to your header's height. */
|
|
184
|
-
scroll-padding-top: var(--wave-docs-header-height);
|
|
185
|
-
}
|
|
257
|
+
/* ---------------------------------------------------------------------------
|
|
258
|
+
* Base
|
|
259
|
+
* ------------------------------------------------------------------------ */
|
|
186
260
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
261
|
+
@layer base {
|
|
262
|
+
/*
|
|
263
|
+
* The ground the ramp is composed against.
|
|
264
|
+
*
|
|
265
|
+
* A foreground token is only as legible as the background under it, and this
|
|
266
|
+
* sheet cannot see the host's. Painting our own containers — never `body`,
|
|
267
|
+
* which belongs to the host — is what makes every ratio in
|
|
268
|
+
* `src/styles.test.ts` a fact about the shipped page rather than about a
|
|
269
|
+
* background nobody declared.
|
|
270
|
+
*/
|
|
271
|
+
/*
|
|
272
|
+
* ⚠️ `.wave-docs-layout` IS IN THIS LIST, AND IT WAS NOT. Painting only the
|
|
273
|
+
* three content containers left the grid's own gutters — 1.5rem between each
|
|
274
|
+
* column, plus the inline padding — showing whatever the browser paints the
|
|
275
|
+
* canvas, because `body` is deliberately never touched.
|
|
276
|
+
*
|
|
277
|
+
* In light mode that is white against a white token and invisible. In dark
|
|
278
|
+
* mode the UA canvas and `--wave-docs-bg` are two different darks, so the
|
|
279
|
+
* sidebar and the table of contents rendered as slightly lighter panels
|
|
280
|
+
* floating on a darker page. Caught by looking at a screenshot of the real
|
|
281
|
+
* site; every contrast ratio in `styles.test.ts` was correct throughout,
|
|
282
|
+
* because each one was a fact about a container that *was* painted.
|
|
283
|
+
*
|
|
284
|
+
* The grid is a container this package owns, so painting it breaks no rule
|
|
285
|
+
* — `body` still belongs to the host, and a host whose page is a different
|
|
286
|
+
* colour overrides the token rather than fighting a `body` rule they did not
|
|
287
|
+
* write.
|
|
288
|
+
*/
|
|
289
|
+
.wave-docs-layout,
|
|
290
|
+
.wave-docs-prose,
|
|
291
|
+
.wave-docs-sidebar,
|
|
292
|
+
.wave-docs-toc {
|
|
293
|
+
background: var(--wave-docs-bg);
|
|
294
|
+
color: var(--wave-docs-fg);
|
|
295
|
+
/* Inert unless the container is itself the scrollport, which a sidebar or
|
|
296
|
+
* a TOC pane usually is. Headings carry the matching `scroll-margin-top`
|
|
297
|
+
* for the far commoner case where the document scrolls. */
|
|
298
|
+
scroll-padding-top: var(--wave-docs-scroll-padding);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* The typeface, on every root this package owns.
|
|
303
|
+
*
|
|
304
|
+
* Separate from the grounding rule above because these six are not the same
|
|
305
|
+
* set: the dialog is portalled out of the prose and the trigger usually sits
|
|
306
|
+
* in the host's own header, so neither inherits from anything this sheet
|
|
307
|
+
* paints — while the skip link and the trigger have their own backgrounds and
|
|
308
|
+
* must not be handed the page one.
|
|
309
|
+
*
|
|
310
|
+
* Declared at all because inheriting means a host that never set a family
|
|
311
|
+
* renders its documentation in the UA serif. `--wave-docs-font-sans: inherit`
|
|
312
|
+
* gives the host's typeface back in one line.
|
|
313
|
+
*/
|
|
314
|
+
.wave-docs-prose,
|
|
315
|
+
.wave-docs-sidebar,
|
|
316
|
+
.wave-docs-toc,
|
|
317
|
+
.wave-docs-skip-link,
|
|
318
|
+
.wave-docs-search-trigger,
|
|
319
|
+
.wave-docs-search-dialog {
|
|
320
|
+
font-family: var(--wave-docs-font-sans);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/*
|
|
324
|
+
* No `scroll-behavior: smooth` anywhere, and not for the usual reason.
|
|
325
|
+
* Next 16 only suppresses smooth scrolling across a route change when
|
|
326
|
+
* `<html>` carries `data-scroll-behavior="smooth"` — an attribute only the
|
|
327
|
+
* host can set — so a package-level `html { scroll-behavior: smooth }`
|
|
328
|
+
* animates every navigation and makes dev builds warn about a stylesheet
|
|
329
|
+
* inside `node_modules`. Hosts that want it opt in on their own `<html>`,
|
|
330
|
+
* with that attribute, behind `prefers-reduced-motion: no-preference`.
|
|
331
|
+
*/
|
|
196
332
|
}
|
|
197
333
|
|
|
198
334
|
/* ---------------------------------------------------------------------------
|
|
@@ -239,8 +375,18 @@
|
|
|
239
375
|
* plain `:focus` for the same reason. */
|
|
240
376
|
.wave-docs-skip-link:focus {
|
|
241
377
|
transform: translateY(0);
|
|
242
|
-
outline
|
|
243
|
-
|
|
378
|
+
/* `outline`, here and at every other focus ring in this file, never
|
|
379
|
+
* `box-shadow`. Forced-colors mode drops `box-shadow` and forces
|
|
380
|
+
* `outline-color` to a system colour, so an outline is repainted for free
|
|
381
|
+
* where a shadow ring had to be restored by hand — which is what a 24-line
|
|
382
|
+
* `forced-colors` block used to do, for all eleven of them, with a test
|
|
383
|
+
* policing the list. An outline also follows the element's own
|
|
384
|
+
* `border-radius`, and shows the real surface through `outline-offset`
|
|
385
|
+
* instead of painting `--wave-docs-bg` as a spacer — which was a lie on the
|
|
386
|
+
* active search result, where the row is `accent-subtle` and the ring drew
|
|
387
|
+
* a white gap across it. */
|
|
388
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
389
|
+
outline-offset: 2px;
|
|
244
390
|
}
|
|
245
391
|
|
|
246
392
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -259,53 +405,128 @@
|
|
|
259
405
|
color: var(--wave-docs-fg);
|
|
260
406
|
font-size: 1rem;
|
|
261
407
|
line-height: 1.75;
|
|
262
|
-
/*
|
|
408
|
+
/* Inherited by every descendant, which is the point: a sha256 digest in a
|
|
409
|
+
* paragraph, a long word in a heading or a list item each pushed the page
|
|
410
|
+
* to a 553-784px scrollWidth at the 320px viewport WCAG 1.4.10 Reflow
|
|
411
|
+
* tests at. `break-word` and not `word-break: break-all`, which would also
|
|
412
|
+
* chop ordinary prose mid-syllable. */
|
|
413
|
+
overflow-wrap: break-word;
|
|
414
|
+
/* The comment here used to say the docs shell owns column width. No shell
|
|
415
|
+
* shipped, so in practice every consumer's first override was the same
|
|
416
|
+
* container CSS — a default the package should have carried itself. */
|
|
417
|
+
max-width: var(--wave-docs-measure);
|
|
263
418
|
}
|
|
264
419
|
|
|
265
420
|
.wave-docs-prose > * + * {
|
|
266
421
|
margin-block-start: 1.25em;
|
|
267
422
|
}
|
|
268
423
|
|
|
424
|
+
/* `pretty`, not `balance`: balance is for headings, where every line matters
|
|
425
|
+
* and there are two or three of them. On a paragraph it is capped at four
|
|
426
|
+
* lines in most engines anyway; `pretty` is the one that stops a paragraph
|
|
427
|
+
* ending on a single orphaned word. */
|
|
428
|
+
.wave-docs-prose p {
|
|
429
|
+
text-wrap: pretty;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/*
|
|
433
|
+
* A 1.2 minor third, four sizes, six unmistakable levels.
|
|
434
|
+
*
|
|
435
|
+
* The old scale stepped 1.50 / 1.20 / 1.18 / 1.06 / 1.00 — one enormous jump
|
|
436
|
+
* and then four nearly flat ones, so h3, h4 and h5 were interchangeable at a
|
|
437
|
+
* glance. h5 and h6 were the same size *and* weight as body text, differing
|
|
438
|
+
* only in colour: a coloured paragraph, not a level. They are an eyebrow now,
|
|
439
|
+
* which is a different axis (tracking and case) rather than a fifth size
|
|
440
|
+
* nobody can distinguish from the fourth.
|
|
441
|
+
*
|
|
442
|
+
* Leading falls as size rises — 1.15 through 1.5 — because one shared 1.25
|
|
443
|
+
* let the 36px h1 float apart and crowded the 16px h5. Tracking tightens the
|
|
444
|
+
* same way, for the same reason.
|
|
445
|
+
*/
|
|
269
446
|
.wave-docs-prose :is(h1, h2, h3, h4, h5, h6) {
|
|
270
|
-
position: relative;
|
|
271
447
|
color: var(--wave-docs-fg);
|
|
272
448
|
font-weight: 650;
|
|
273
|
-
line-height: 1.25;
|
|
274
449
|
text-wrap: balance;
|
|
275
|
-
scroll-margin-top: var(--wave-docs-
|
|
450
|
+
scroll-margin-top: var(--wave-docs-scroll-padding);
|
|
276
451
|
}
|
|
277
452
|
|
|
278
|
-
|
|
453
|
+
/* Top margin scales with the size of the break the heading signals, so a new
|
|
454
|
+
* section reads as a bigger pause than a subsection. */
|
|
455
|
+
.wave-docs-prose > h2 {
|
|
456
|
+
margin-block-start: 2.5em;
|
|
457
|
+
}
|
|
458
|
+
.wave-docs-prose > h3 {
|
|
279
459
|
margin-block-start: 2em;
|
|
280
460
|
}
|
|
461
|
+
.wave-docs-prose > :is(h4, h5, h6) {
|
|
462
|
+
margin-block-start: 1.75em;
|
|
463
|
+
}
|
|
464
|
+
/* ...and a heading sits close to what it introduces. */
|
|
465
|
+
.wave-docs-prose :is(h1, h2, h3, h4, h5, h6) + * {
|
|
466
|
+
margin-block-start: 0.75em;
|
|
467
|
+
}
|
|
281
468
|
|
|
282
469
|
.wave-docs-prose h1 {
|
|
283
|
-
|
|
284
|
-
|
|
470
|
+
/* The one fluid size. A fixed 2.0736rem h1 wraps an ordinary page title to
|
|
471
|
+
* three lines at 390px; everything below it stays fixed, because fluid type
|
|
472
|
+
* against a fixed measure means characters-per-line silently drifts and the
|
|
473
|
+
* measure token stops meaning what it says. */
|
|
474
|
+
font-size: clamp(1.75rem, 1.45rem + 1.3vw, 2.0736rem);
|
|
475
|
+
line-height: 1.15;
|
|
476
|
+
letter-spacing: -0.021em;
|
|
477
|
+
font-weight: 680;
|
|
285
478
|
}
|
|
286
479
|
.wave-docs-prose h2 {
|
|
287
|
-
font-size: 1.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
480
|
+
font-size: 1.44rem;
|
|
481
|
+
line-height: 1.25;
|
|
482
|
+
letter-spacing: -0.014em;
|
|
483
|
+
font-weight: 660;
|
|
291
484
|
}
|
|
292
485
|
.wave-docs-prose h3 {
|
|
293
|
-
font-size: 1.
|
|
486
|
+
font-size: 1.2rem;
|
|
487
|
+
line-height: 1.35;
|
|
488
|
+
letter-spacing: -0.008em;
|
|
294
489
|
}
|
|
295
490
|
.wave-docs-prose h4 {
|
|
296
|
-
font-size:
|
|
491
|
+
font-size: 1rem;
|
|
492
|
+
line-height: 1.45;
|
|
297
493
|
}
|
|
494
|
+
/* An eyebrow, not a fifth size: at this depth the distinction has to come
|
|
495
|
+
* from case and tracking, because two more steps of a 1.2 scale would land
|
|
496
|
+
* inside a rounding error of the body text. */
|
|
298
497
|
.wave-docs-prose :is(h5, h6) {
|
|
299
|
-
font-size:
|
|
498
|
+
font-size: 0.875rem;
|
|
499
|
+
line-height: 1.5;
|
|
500
|
+
letter-spacing: 0.02em;
|
|
501
|
+
text-transform: uppercase;
|
|
300
502
|
color: var(--wave-docs-fg-muted);
|
|
301
503
|
}
|
|
302
504
|
|
|
505
|
+
/*
|
|
506
|
+
* No rule under `h2`. A full-width hairline under every section heading is
|
|
507
|
+
* the single loudest "rendered GitHub README" signal a page can carry, and
|
|
508
|
+
* with no measure it ran the entire viewport. A long reference page that
|
|
509
|
+
* genuinely wants them is one attribute away, and shipping the opt-out in the
|
|
510
|
+
* same breath as the deletion is what stops someone forking the stylesheet
|
|
511
|
+
* to get it back:
|
|
512
|
+
*/
|
|
513
|
+
.wave-docs-prose[data-rules] > h2 {
|
|
514
|
+
padding-block-end: 0.3em;
|
|
515
|
+
border-block-end: 1px solid var(--wave-docs-border);
|
|
516
|
+
}
|
|
517
|
+
|
|
303
518
|
.wave-docs-prose a {
|
|
304
519
|
color: var(--wave-docs-accent);
|
|
305
520
|
text-decoration: underline;
|
|
306
521
|
text-underline-offset: 0.2em;
|
|
307
522
|
text-decoration-thickness: 1px;
|
|
308
523
|
border-radius: 2px;
|
|
524
|
+
/* `anywhere`, not `break-word`: a link label is often one unbreakable
|
|
525
|
+
* token (`/api/v1/some-long-path`), and `break-word` only breaks a word
|
|
526
|
+
* that would overflow a line of its own. A bare autolinked URL already
|
|
527
|
+
* breaks after `/` under UAX#14 without help — which is why no
|
|
528
|
+
* `word-break` appears anywhere in this file. */
|
|
529
|
+
overflow-wrap: anywhere;
|
|
309
530
|
}
|
|
310
531
|
|
|
311
532
|
.wave-docs-prose a:hover {
|
|
@@ -317,8 +538,8 @@
|
|
|
317
538
|
.wave-docs-sidebar :is(a, button):focus-visible,
|
|
318
539
|
.wave-docs-toc a:focus-visible,
|
|
319
540
|
.wave-docs-youtube__facade:focus-visible {
|
|
320
|
-
outline:
|
|
321
|
-
|
|
541
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
542
|
+
outline-offset: 2px;
|
|
322
543
|
}
|
|
323
544
|
|
|
324
545
|
.wave-docs-prose :is(ul, ol) {
|
|
@@ -354,7 +575,10 @@
|
|
|
354
575
|
padding-inline-start: 1rem;
|
|
355
576
|
border-inline-start: 3px solid var(--wave-docs-border-strong);
|
|
356
577
|
color: var(--wave-docs-fg-muted);
|
|
357
|
-
|
|
578
|
+
/* Not italic. The rule and the muted colour already say "quotation"; a
|
|
579
|
+
* long italic passage is measurably slower to read, and markdown authors
|
|
580
|
+
* use blockquotes for asides and notes, not only for speech. */
|
|
581
|
+
font-style: normal;
|
|
358
582
|
}
|
|
359
583
|
|
|
360
584
|
.wave-docs-prose hr {
|
|
@@ -406,20 +630,96 @@
|
|
|
406
630
|
overflow-x: auto;
|
|
407
631
|
border: 1px solid var(--wave-docs-border);
|
|
408
632
|
border-radius: var(--wave-docs-radius);
|
|
633
|
+
scrollbar-width: thin;
|
|
634
|
+
scrollbar-color: var(--wave-docs-border-strong) transparent;
|
|
635
|
+
/*
|
|
636
|
+
* ⚠️ NOT DECORATIVE, AND NOT DELETABLE AS SUCH.
|
|
637
|
+
*
|
|
638
|
+
* This is the whole scroll affordance, and it is four gradients because
|
|
639
|
+
* that is what makes it stateless. The two `local` covers are painted in
|
|
640
|
+
* the page colour and scroll *with* the content, so each one sits over its
|
|
641
|
+
* shadow only while that edge is at rest; the two `scroll` shadows are
|
|
642
|
+
* pinned to the box. The result is a shadow that appears on precisely the
|
|
643
|
+
* side that has content off-screen, with no listener, no state and no
|
|
644
|
+
* hydration — on a table the reader may well meet before JS arrives.
|
|
645
|
+
*/
|
|
646
|
+
background:
|
|
647
|
+
linear-gradient(to right, var(--wave-docs-bg) 30%, transparent) left /
|
|
648
|
+
2rem 100% no-repeat local,
|
|
649
|
+
linear-gradient(to left, var(--wave-docs-bg) 30%, transparent) right /
|
|
650
|
+
2rem 100% no-repeat local,
|
|
651
|
+
radial-gradient(
|
|
652
|
+
farthest-side at 0 50%,
|
|
653
|
+
var(--wave-docs-scroll-shadow),
|
|
654
|
+
transparent
|
|
655
|
+
)
|
|
656
|
+
left / 0.75rem 100% no-repeat scroll,
|
|
657
|
+
radial-gradient(
|
|
658
|
+
farthest-side at 100% 50%,
|
|
659
|
+
var(--wave-docs-scroll-shadow),
|
|
660
|
+
transparent
|
|
661
|
+
)
|
|
662
|
+
right / 0.75rem 100% no-repeat scroll;
|
|
409
663
|
}
|
|
410
664
|
|
|
411
665
|
.wave-docs-table-scroll:focus-visible {
|
|
412
|
-
outline:
|
|
413
|
-
|
|
666
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
667
|
+
outline-offset: 2px;
|
|
414
668
|
}
|
|
415
669
|
|
|
416
670
|
.wave-docs-table {
|
|
417
671
|
width: 100%;
|
|
672
|
+
/*
|
|
673
|
+
* A FLOOR, NOT A SIZING MODE — and `width: 100%` is not one.
|
|
674
|
+
*
|
|
675
|
+
* Auto table layout floors at min-content, so `width: 100%` inside an
|
|
676
|
+
* `overflow-x: auto` container does not stop a table from being squeezed;
|
|
677
|
+
* it only stops it from being wider than the container. What actually
|
|
678
|
+
* collapses the columns is `.wave-docs-prose a { overflow-wrap: anywhere }`
|
|
679
|
+
* above: per CSS Text 3, `anywhere` affects *intrinsic* sizing and
|
|
680
|
+
* `break-word` does not, so a cell holding a link — i.e. every column of a
|
|
681
|
+
* real API table — reports a min-content width of about one character. The
|
|
682
|
+
* table then fits 320px, does not overflow, and renders rows five lines
|
|
683
|
+
* tall with one word per line.
|
|
684
|
+
*
|
|
685
|
+
* `width: max-content` is the obvious fix and is worse: it forbids a
|
|
686
|
+
* description cell from wrapping at all, so a table with one sentence in it
|
|
687
|
+
* is over 1600px wide at every viewport and you scroll sideways to read
|
|
688
|
+
* one sentence.
|
|
689
|
+
*
|
|
690
|
+
* A floor solves both. Below 40rem of available width the table stops
|
|
691
|
+
* shrinking and the scroll container takes over; above it, `width: 100%`
|
|
692
|
+
* takes over and the description column wraps as it should.
|
|
693
|
+
*
|
|
694
|
+
* `min-width: 40rem`, NOT `min(100%, 40rem)` — that spelling is a no-op at
|
|
695
|
+
* exactly the widths it is meant to govern, because the `100%` resolves
|
|
696
|
+
* against the box being floored: in a 318px scroll container it computes to
|
|
697
|
+
* `min(318px, 640px)` = 318px, i.e. the container's own width, so nothing
|
|
698
|
+
* ever overflows and the table squeezes exactly as before. The browser tier
|
|
699
|
+
* caught it at 318 vs 318.
|
|
700
|
+
*
|
|
701
|
+
* A consumer with an eight-column monster overrides `min-width` from their
|
|
702
|
+
* own unlayered rule and wins by the cascade contract this file already
|
|
703
|
+
* guarantees — so no token for it.
|
|
704
|
+
*/
|
|
705
|
+
min-width: 40rem;
|
|
418
706
|
border-collapse: collapse;
|
|
707
|
+
font-variant-numeric: tabular-nums;
|
|
419
708
|
font-size: 0.9375rem;
|
|
420
709
|
line-height: 1.6;
|
|
421
710
|
}
|
|
422
711
|
|
|
712
|
+
/* `box-shadow`, not `border-block-end`: with `border-collapse: collapse` the
|
|
713
|
+
* border belongs to the table, not the cell, so it scrolls away from a
|
|
714
|
+
* sticky header instead of staying under it. */
|
|
715
|
+
.wave-docs-table thead th {
|
|
716
|
+
position: sticky;
|
|
717
|
+
top: 0;
|
|
718
|
+
z-index: 1;
|
|
719
|
+
background: var(--wave-docs-bg-subtle);
|
|
720
|
+
box-shadow: inset 0 -1px 0 var(--wave-docs-border);
|
|
721
|
+
}
|
|
722
|
+
|
|
423
723
|
.wave-docs-table :is(th, td) {
|
|
424
724
|
padding: 0.625rem 0.875rem;
|
|
425
725
|
text-align: start;
|
|
@@ -475,50 +775,272 @@
|
|
|
475
775
|
/* ---------------------------------------------------------------------------
|
|
476
776
|
* Shiki
|
|
477
777
|
*
|
|
478
|
-
* Dual-theme promotion
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
778
|
+
* Dual-theme promotion, driven entirely by custom properties. `render.ts`
|
|
779
|
+
* passes `defaultColor: false`, so Shiki writes NO inline `color` at all — each
|
|
780
|
+
* token span carries `--shiki-light` and `--shiki-dark` and nothing else, and
|
|
781
|
+
* picking a theme is an ordinary cascade problem solved by an ordinary
|
|
782
|
+
* selector. No `!important` is involved, and none may be reintroduced: it would
|
|
783
|
+
* outrank the consumer's own overrides for no benefit.
|
|
784
|
+
*
|
|
785
|
+
* Scoped to `.wave-docs-prose`, and inside `@layer components` like everything
|
|
786
|
+
* else here. A bare, unlayered `.shiki` beat every layer on any page that also
|
|
787
|
+
* renders Shiki output from another package, silently repainting it.
|
|
483
788
|
* ------------------------------------------------------------------------ */
|
|
484
789
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
790
|
+
@layer components {
|
|
791
|
+
/* -------------------------------------------------------------------------
|
|
792
|
+
* Code frame
|
|
793
|
+
*
|
|
794
|
+
* The `<figure>` `rehypeCodeFrame` wraps every highlighted fence in: an
|
|
795
|
+
* optional title bar, a copy button, and the `<pre>` itself.
|
|
796
|
+
* ---------------------------------------------------------------------- */
|
|
797
|
+
|
|
798
|
+
.wave-docs-code {
|
|
799
|
+
position: relative;
|
|
800
|
+
/* The `<pre>` inside carries the border and background; the figure is only
|
|
801
|
+
* a positioning context, and a default `<figure>` margin would indent the
|
|
802
|
+
* whole block by 40px. */
|
|
803
|
+
margin: 0;
|
|
804
|
+
}
|
|
496
805
|
|
|
497
|
-
.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
806
|
+
.wave-docs-code__title {
|
|
807
|
+
display: flex;
|
|
808
|
+
align-items: center;
|
|
809
|
+
/* Room for the button, which is absolutely positioned over this row. */
|
|
810
|
+
padding: 0.5rem 3rem 0.5rem 1.125rem;
|
|
811
|
+
border: 1px solid var(--wave-docs-border);
|
|
812
|
+
border-block-end: 0;
|
|
813
|
+
border-start-start-radius: var(--wave-docs-radius);
|
|
814
|
+
border-start-end-radius: var(--wave-docs-radius);
|
|
815
|
+
background-color: var(--wave-docs-bg-subtle);
|
|
816
|
+
color: var(--wave-docs-fg-muted);
|
|
817
|
+
font-family: var(--wave-docs-font-mono);
|
|
818
|
+
font-size: 0.8125rem;
|
|
819
|
+
}
|
|
501
820
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
821
|
+
/* Square off the top of a `<pre>` that sits under a title bar.
|
|
822
|
+
*
|
|
823
|
+
* `.wave-docs-prose` first, even though `.wave-docs-code` is our class and
|
|
824
|
+
* only ever appears inside it: `styles.test.ts` requires every `.shiki` rule
|
|
825
|
+
* to name the prose scope, so the package can never style a code block it
|
|
826
|
+
* did not render. One form for that invariant is worth the extra selector. */
|
|
827
|
+
/*
|
|
828
|
+
* ⚠️ THE `<pre>`'s USER-AGENT MARGIN, WHICH UNDID THE FRAME.
|
|
829
|
+
*
|
|
830
|
+
* A `<pre>` defaults to `margin-block: 1em`, and at this block's 0.875rem
|
|
831
|
+
* that is 14px — measured in Chromium on the real site, not reasoned about.
|
|
832
|
+
* So the title bar dropped its bottom border and squared its bottom corners
|
|
833
|
+
* to join the code below it, and then the browser pushed the two 14px apart:
|
|
834
|
+
* a caption floating over a gap, which is the one arrangement the frame was
|
|
835
|
+
* designed not to be.
|
|
836
|
+
*
|
|
837
|
+
* It mis-seated the copy button too. The button is positioned against the
|
|
838
|
+
* `<figure>`, so on a fence with no title the margin slid the code down out
|
|
839
|
+
* from under it.
|
|
840
|
+
*
|
|
841
|
+
* Invisible to every test here before the site existed: jsdom has no layout,
|
|
842
|
+
* and `styles.test.ts` reads rules as text. It took a screenshot of a real
|
|
843
|
+
* page to see it.
|
|
844
|
+
*/
|
|
845
|
+
.wave-docs-prose .wave-docs-code .shiki {
|
|
846
|
+
margin: 0;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.wave-docs-prose .wave-docs-code:has(.wave-docs-code__title) .shiki {
|
|
850
|
+
border-start-start-radius: 0;
|
|
851
|
+
border-start-end-radius: 0;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.wave-docs-code__copy {
|
|
855
|
+
position: absolute;
|
|
856
|
+
top: 0.5rem;
|
|
857
|
+
inset-inline-end: 0.5rem;
|
|
858
|
+
display: inline-flex;
|
|
859
|
+
align-items: center;
|
|
860
|
+
justify-content: center;
|
|
861
|
+
width: 2rem;
|
|
862
|
+
height: 2rem;
|
|
863
|
+
border: 1px solid var(--wave-docs-border);
|
|
864
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
865
|
+
background-color: var(--wave-docs-bg);
|
|
866
|
+
color: var(--wave-docs-fg-muted);
|
|
867
|
+
font-size: 0.875rem;
|
|
868
|
+
cursor: pointer;
|
|
869
|
+
/*
|
|
870
|
+
* ⚠️ HIDDEN UNTIL THE RUNTIME SAYS OTHERWISE, AND THIS IS STRUCTURAL.
|
|
871
|
+
*
|
|
872
|
+
* The button is in the HTML whether or not any JavaScript runs — a reader
|
|
873
|
+
* with scripts off, a `renderToStaticMarkup` consumer, anyone rendering
|
|
874
|
+
* the hast by hand. `visibility: hidden` also takes it out of the tab
|
|
875
|
+
* order, which `opacity: 0` would not: without it a keyboard reader meets
|
|
876
|
+
* a control that is invisible, focusable, and does nothing.
|
|
877
|
+
*/
|
|
878
|
+
visibility: hidden;
|
|
879
|
+
opacity: 0;
|
|
880
|
+
transition: opacity 120ms ease;
|
|
881
|
+
}
|
|
505
882
|
|
|
506
|
-
@media (prefers-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
883
|
+
@media (prefers-reduced-motion: reduce) {
|
|
884
|
+
.wave-docs-code__copy {
|
|
885
|
+
transition: none;
|
|
886
|
+
}
|
|
510
887
|
}
|
|
511
|
-
|
|
512
|
-
|
|
888
|
+
|
|
889
|
+
html[data-wave-docs-code-ready] .wave-docs-code__copy {
|
|
890
|
+
visibility: visible;
|
|
513
891
|
}
|
|
514
|
-
}
|
|
515
892
|
|
|
516
|
-
:
|
|
517
|
-
:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
:
|
|
521
|
-
|
|
893
|
+
.wave-docs-code:hover .wave-docs-code__copy,
|
|
894
|
+
.wave-docs-code:focus-within .wave-docs-code__copy,
|
|
895
|
+
/* A title bar already reserves the space, so there is nothing to reveal. */
|
|
896
|
+
.wave-docs-code:has(.wave-docs-code__title) .wave-docs-code__copy {
|
|
897
|
+
opacity: 1;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/*
|
|
901
|
+
* A control that only appears on hover does not exist on a phone. This is
|
|
902
|
+
* the whole of the mobile story for copy, and it is one query.
|
|
903
|
+
*/
|
|
904
|
+
@media (hover: none) {
|
|
905
|
+
.wave-docs-code__copy {
|
|
906
|
+
opacity: 1;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.wave-docs-code__copy:hover {
|
|
911
|
+
background-color: var(--wave-docs-bg-subtle);
|
|
912
|
+
color: var(--wave-docs-fg);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* The success state, swapped by CSS rather than by React: no component owns
|
|
916
|
+
* a button, so there is no state to re-render and nothing to hydrate. */
|
|
917
|
+
.wave-docs-code__copy[data-copied='true'] {
|
|
918
|
+
border-color: var(--wave-docs-callout-tip);
|
|
919
|
+
color: var(--wave-docs-callout-tip);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.wave-docs-code__copy[data-copied='true'] > * {
|
|
923
|
+
visibility: hidden;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.wave-docs-code__copy[data-copied='true']::after {
|
|
927
|
+
content: '\2713';
|
|
928
|
+
position: absolute;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/*
|
|
932
|
+
* ⚠️ AND THE FAILURE STATE, WHICH HAD NO RULE AT ALL. The runtime has always
|
|
933
|
+
* written `data-copied="false"` and announced "Copy failed. Select the code
|
|
934
|
+
* and press Control or Command + C." — to a live region, so a screen-reader
|
|
935
|
+
* user was told and a sighted user saw a button that simply did nothing.
|
|
936
|
+
*
|
|
937
|
+
* The most common way to land here is not exotic: `next dev` reached from a
|
|
938
|
+
* phone over `http://192.168.x.x:3000` is not a secure context, so
|
|
939
|
+
* `navigator.clipboard` is undefined and no amount of pressing helps. A
|
|
940
|
+
* silent no-op is the worst possible answer to that.
|
|
941
|
+
*/
|
|
942
|
+
.wave-docs-code__copy[data-copied='false'] {
|
|
943
|
+
border-color: var(--wave-docs-callout-caution);
|
|
944
|
+
color: var(--wave-docs-callout-caution);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.wave-docs-code__copy[data-copied='false'] > * {
|
|
948
|
+
visibility: hidden;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.wave-docs-code__copy[data-copied='false']::after {
|
|
952
|
+
/* A cross, not a warning triangle: it pairs with the tick above at the
|
|
953
|
+
* same weight, and the triangle reads as a page-level alert. */
|
|
954
|
+
content: '\00d7';
|
|
955
|
+
position: absolute;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/*
|
|
959
|
+
* The live region. Visually hidden rather than `display: none`, which most
|
|
960
|
+
* screen readers skip entirely — an announcement nobody hears is not one.
|
|
961
|
+
*/
|
|
962
|
+
.wave-docs-code__status {
|
|
963
|
+
position: absolute;
|
|
964
|
+
width: 1px;
|
|
965
|
+
height: 1px;
|
|
966
|
+
margin: -1px;
|
|
967
|
+
padding: 0;
|
|
968
|
+
overflow: hidden;
|
|
969
|
+
clip-path: inset(50%);
|
|
970
|
+
white-space: nowrap;
|
|
971
|
+
border: 0;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/*
|
|
975
|
+
* A fence whose language is in `excludeLangs` never reaches Shiki, so it has
|
|
976
|
+
* none of the rules below: no background, no border, no padding, no radius,
|
|
977
|
+
* no monospace sizing and no horizontal scroll. It rendered as a wall of UA
|
|
978
|
+
* default text bleeding out of the reading column — `excludeLangs` shipped
|
|
979
|
+
* as half a feature, and this is the other half. Everything a consumer then
|
|
980
|
+
* renders in its place (a Mermaid diagram, say) overrides it from their own
|
|
981
|
+
* unlayered CSS, which beats every layer here.
|
|
982
|
+
*/
|
|
983
|
+
.wave-docs-prose pre:not(.shiki) {
|
|
984
|
+
padding: 1rem 1.125rem;
|
|
985
|
+
border: 1px solid var(--wave-docs-border);
|
|
986
|
+
border-radius: var(--wave-docs-radius);
|
|
987
|
+
background-color: var(--wave-docs-code-block-bg);
|
|
988
|
+
overflow-x: auto;
|
|
989
|
+
font-family: var(--wave-docs-font-mono);
|
|
990
|
+
font-size: 0.875rem;
|
|
991
|
+
line-height: 1.7;
|
|
992
|
+
tab-size: 2;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.wave-docs-prose .shiki {
|
|
996
|
+
padding: 1rem 1.125rem;
|
|
997
|
+
border: 1px solid var(--wave-docs-border);
|
|
998
|
+
border-radius: var(--wave-docs-radius);
|
|
999
|
+
/* Our token, not `--shiki-light-bg`: the code block is part of the page's
|
|
1000
|
+
* surface ramp, and a theme's own background is whatever GitHub chose. */
|
|
1001
|
+
background-color: var(--wave-docs-code-block-bg);
|
|
1002
|
+
overflow-x: auto;
|
|
1003
|
+
font-family: var(--wave-docs-font-mono);
|
|
1004
|
+
font-size: 0.875rem;
|
|
1005
|
+
line-height: 1.7;
|
|
1006
|
+
tab-size: 2;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.wave-docs-prose .shiki,
|
|
1010
|
+
.wave-docs-prose .shiki span {
|
|
1011
|
+
color: var(--shiki-light);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/* Shiki puts `tabindex="0"` on the `<pre>` so a keyboard reader can scroll a
|
|
1015
|
+
* wide block — which makes it the one focusable element in this package that
|
|
1016
|
+
* had no focus style. */
|
|
1017
|
+
.wave-docs-prose .shiki:focus-visible {
|
|
1018
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
1019
|
+
outline-offset: 2px;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.wave-docs-prose .shiki code {
|
|
1023
|
+
display: grid;
|
|
1024
|
+
font-family: inherit;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.wave-docs-prose .shiki .line {
|
|
1028
|
+
min-height: 1lh;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
@media (prefers-color-scheme: dark) {
|
|
1032
|
+
:root[data-theme='system'] .wave-docs-prose .shiki,
|
|
1033
|
+
:root[data-theme='system'] .wave-docs-prose .shiki span {
|
|
1034
|
+
color: var(--shiki-dark);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
:root[data-theme='dark'] .wave-docs-prose .shiki,
|
|
1039
|
+
:root[data-theme='dark'] .wave-docs-prose .shiki span,
|
|
1040
|
+
:root.dark .wave-docs-prose .shiki,
|
|
1041
|
+
:root.dark .wave-docs-prose .shiki span {
|
|
1042
|
+
color: var(--shiki-dark);
|
|
1043
|
+
}
|
|
522
1044
|
}
|
|
523
1045
|
|
|
524
1046
|
/* ---------------------------------------------------------------------------
|
|
@@ -598,6 +1120,87 @@
|
|
|
598
1120
|
border: 0;
|
|
599
1121
|
background: none;
|
|
600
1122
|
cursor: pointer;
|
|
1123
|
+
/* The UA disclosure triangle, which would sit on top of the thumbnail. */
|
|
1124
|
+
list-style: none;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/* Safari still needs the pseudo-element form; it ignores `list-style` here. */
|
|
1128
|
+
.wave-docs-youtube__facade::-webkit-details-marker {
|
|
1129
|
+
display: none;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/*
|
|
1133
|
+
* ⚠️ HIDDEN, NOT REMOVED, AND FOCUSABLE EITHER WAY. `display: none` here
|
|
1134
|
+
* would destroy the element the reader just activated, dropping focus to the
|
|
1135
|
+
* top of the document — which is the bug the previous, client-side version
|
|
1136
|
+
* needed a `useEffect` to paper over. Visually hidden keeps focus where the
|
|
1137
|
+
* reader put it and leaves a way to collapse the player again.
|
|
1138
|
+
*/
|
|
1139
|
+
.wave-docs-youtube[open] .wave-docs-youtube__facade {
|
|
1140
|
+
position: absolute;
|
|
1141
|
+
width: 1px;
|
|
1142
|
+
height: 1px;
|
|
1143
|
+
margin: -1px;
|
|
1144
|
+
overflow: hidden;
|
|
1145
|
+
clip-path: inset(50%);
|
|
1146
|
+
white-space: nowrap;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/*
|
|
1150
|
+
* ⚠️ AND VISIBLE AGAIN THE MOMENT IT HAS FOCUS. The rule above ran
|
|
1151
|
+
* unconditionally, so a keyboard reader who pressed Enter to start the video
|
|
1152
|
+
* was left holding focus on a 1×1 clipped element: the focus indicator
|
|
1153
|
+
* disappeared from the page entirely (WCAG 2.4.7), with no way to see that
|
|
1154
|
+
* the next Enter would collapse the player again. `youtube.browser.test.tsx`
|
|
1155
|
+
* asserts focus deliberately *stays* here, which made the hiding rule a
|
|
1156
|
+
* guaranteed failure rather than a possible one.
|
|
1157
|
+
*
|
|
1158
|
+
* Same shape as the skip link: off-screen until focused, a real control when
|
|
1159
|
+
* focused. It overlays the player rather than displacing it, because the
|
|
1160
|
+
* `<details>` has a fixed aspect ratio and reflowing the video under the
|
|
1161
|
+
* reader's cursor to reveal a button is its own defect.
|
|
1162
|
+
*
|
|
1163
|
+
* `:focus-visible` here, where the skip link 700 lines up deliberately uses
|
|
1164
|
+
* `:focus` — the divergence is the point. Clicking the thumbnail focuses this
|
|
1165
|
+
* summary as a side effect of opening the player, and a mouse user does not
|
|
1166
|
+
* want a "Hide video" control appearing over the video they just started.
|
|
1167
|
+
* The reader who needs it is the one who pressed Enter, which is exactly what
|
|
1168
|
+
* `:focus-visible` matches. Unlike the skip link, this element is also inert
|
|
1169
|
+
* to a pointer while hidden — it is 1px, under nothing — so there is no
|
|
1170
|
+
* unreachable-focused-control case to guard against.
|
|
1171
|
+
*/
|
|
1172
|
+
.wave-docs-youtube[open] .wave-docs-youtube__facade:focus-visible {
|
|
1173
|
+
position: absolute;
|
|
1174
|
+
z-index: 1;
|
|
1175
|
+
inset-block-start: 0.5rem;
|
|
1176
|
+
inset-inline-start: 0.5rem;
|
|
1177
|
+
width: auto;
|
|
1178
|
+
height: auto;
|
|
1179
|
+
margin: 0;
|
|
1180
|
+
padding: 0.5rem 0.75rem;
|
|
1181
|
+
overflow: visible;
|
|
1182
|
+
clip-path: none;
|
|
1183
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
1184
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
1185
|
+
/* Inside, not outside: this control is inset 0.5rem over a black player,
|
|
1186
|
+
* and an offset ring would be drawn half onto the video and half off the
|
|
1187
|
+
* rounded corner of the frame. */
|
|
1188
|
+
outline-offset: -2px;
|
|
1189
|
+
background: var(--wave-docs-bg);
|
|
1190
|
+
color: var(--wave-docs-fg);
|
|
1191
|
+
font-size: 0.875rem;
|
|
1192
|
+
font-weight: 600;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/*
|
|
1196
|
+
* The label follows the action. A `<summary>` that reads "Play video" while
|
|
1197
|
+
* activating it *stops* the video is a control that lies about what it does,
|
|
1198
|
+
* and the state is in the DOM already — no script needed to keep the two in
|
|
1199
|
+
* step.
|
|
1200
|
+
*/
|
|
1201
|
+
.wave-docs-youtube[open] .wave-docs-youtube__label-play,
|
|
1202
|
+
.wave-docs-youtube:not([open]) .wave-docs-youtube__label-hide {
|
|
1203
|
+
display: none;
|
|
601
1204
|
}
|
|
602
1205
|
|
|
603
1206
|
.wave-docs-youtube__thumbnail {
|
|
@@ -659,6 +1262,22 @@
|
|
|
659
1262
|
|
|
660
1263
|
/* Nested levels get a guide rail instead of raw indentation, so a deep tree
|
|
661
1264
|
* stays readable at 240px wide. */
|
|
1265
|
+
/*
|
|
1266
|
+
* `__item` and `__group-title` are emitted by `sidebar.tsx` and had no rules
|
|
1267
|
+
* at all — markup with a public class name and no definition, which is the
|
|
1268
|
+
* worst of the three options: a consumer targeting it gets no baseline to
|
|
1269
|
+
* override, and a maintainer cannot tell whether it is a hook or a leftover.
|
|
1270
|
+
* They are hooks: the drawer styles items at a different density, and a group
|
|
1271
|
+
* title is the one place a sidebar needs a label that is not a link.
|
|
1272
|
+
*/
|
|
1273
|
+
.wave-docs-sidebar__item {
|
|
1274
|
+
margin-block: 0.125rem;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.wave-docs-sidebar__group-title {
|
|
1278
|
+
font-weight: 600;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
662
1281
|
.wave-docs-sidebar__list:not([data-depth='0']) {
|
|
663
1282
|
margin-inline-start: 0.5rem;
|
|
664
1283
|
padding-inline-start: 0.5rem;
|
|
@@ -785,17 +1404,36 @@
|
|
|
785
1404
|
text-decoration: none;
|
|
786
1405
|
}
|
|
787
1406
|
|
|
788
|
-
/* Indent by heading depth rather than by nesting, so
|
|
789
|
-
*
|
|
1407
|
+
/* Indent by heading depth rather than by nesting, so the rule survives a
|
|
1408
|
+
* document that skips a level. Only depth 3 exists — `rehypeCaptureToc`
|
|
1409
|
+
* captures h2 and h3 and nothing deeper, so the rules for 4, 5 and 6 that
|
|
1410
|
+
* used to sit here styled entries that can no longer be produced. */
|
|
790
1411
|
.wave-docs-toc__link[data-depth='3'] {
|
|
791
1412
|
padding-inline-start: 1.5rem;
|
|
792
1413
|
}
|
|
793
|
-
|
|
794
|
-
|
|
1414
|
+
|
|
1415
|
+
/* The back-to-top link, styled as a peer of the entries rather than as a
|
|
1416
|
+
* floating control — it is the last stop in the rail's reading order. */
|
|
1417
|
+
.wave-docs-toc__top {
|
|
1418
|
+
display: inline-block;
|
|
1419
|
+
margin-block-start: 0.75rem;
|
|
1420
|
+
padding-inline-start: 0.75rem;
|
|
1421
|
+
border-inline-start: 2px solid transparent;
|
|
1422
|
+
color: var(--wave-docs-fg-subtle);
|
|
1423
|
+
font-size: 0.8125rem;
|
|
1424
|
+
text-decoration: none;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
.wave-docs-toc__top:hover {
|
|
1428
|
+
color: var(--wave-docs-fg);
|
|
795
1429
|
}
|
|
796
|
-
|
|
797
|
-
.wave-docs-
|
|
798
|
-
|
|
1430
|
+
|
|
1431
|
+
.wave-docs-sidebar__external {
|
|
1432
|
+
/* Baseline-ish against the label, and never a flex item that grows. */
|
|
1433
|
+
margin-inline-start: 0.25rem;
|
|
1434
|
+
flex: none;
|
|
1435
|
+
vertical-align: -0.05em;
|
|
1436
|
+
color: var(--wave-docs-fg-subtle);
|
|
799
1437
|
}
|
|
800
1438
|
|
|
801
1439
|
.wave-docs-toc__link:hover {
|
|
@@ -816,6 +1454,334 @@
|
|
|
816
1454
|
}
|
|
817
1455
|
}
|
|
818
1456
|
|
|
1457
|
+
/* ---------------------------------------------------------------------------
|
|
1458
|
+
* Layout
|
|
1459
|
+
*
|
|
1460
|
+
* The shell `docs.Layout` renders. Every class here is fixed by
|
|
1461
|
+
* `docs/adr/001-shell-contract.md` and is public API from 0.3.0.
|
|
1462
|
+
*
|
|
1463
|
+
* These are the first width-based media queries in this file. Before them there
|
|
1464
|
+
* were none at all — every `@media` was `prefers-color-scheme`,
|
|
1465
|
+
* `prefers-reduced-motion` or `forced-colors` — so there was no mobile layout,
|
|
1466
|
+
* and no CSS for a layout component to use.
|
|
1467
|
+
* ------------------------------------------------------------------------ */
|
|
1468
|
+
|
|
1469
|
+
@layer components {
|
|
1470
|
+
/*
|
|
1471
|
+
* Mobile first, and single-column by construction: the sidebar and the TOC
|
|
1472
|
+
* are not laid out at all below 64rem, so nothing reserves a track for them.
|
|
1473
|
+
*/
|
|
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
|
+
/*
|
|
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
|
+
.wave-docs-layout__toc {
|
|
1513
|
+
display: none;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/*
|
|
1517
|
+
* ⚠️ `min-width: 0`, AND `minmax(0, 1fr)` ON EVERY CONTENT TRACK BELOW.
|
|
1518
|
+
*
|
|
1519
|
+
* `1fr` is shorthand for `minmax(auto, 1fr)`, and `auto` floors at the
|
|
1520
|
+
* content's min-content width — so a wide table pushes the track past the
|
|
1521
|
+
* viewport and takes the whole document into horizontal scroll. Measured with
|
|
1522
|
+
* both removed: 1048px of document inside a 1024px viewport.
|
|
1523
|
+
*
|
|
1524
|
+
* MEASURED, AND WORTH KNOWING BEFORE YOU DELETE ONE: they are redundant with
|
|
1525
|
+
* each other, not jointly required. Removing `minmax(0, 1fr)` alone passes,
|
|
1526
|
+
* and removing `min-width: 0` alone passes; only removing both reproduces the
|
|
1527
|
+
* overflow. They are kept as a pair because they fail in opposite directions
|
|
1528
|
+
* — the track constraint protects a child that does not carry `min-width`,
|
|
1529
|
+
* and `min-width: 0` protects a track someone respells — and because the one
|
|
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.
|
|
1536
|
+
*/
|
|
1537
|
+
.wave-docs-layout__main {
|
|
1538
|
+
min-width: 0;
|
|
1539
|
+
padding-block: 2rem 4rem;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/* 16rem sidebar + 46rem measure + two 1.5rem gutters = 65rem, so the sidebar
|
|
1543
|
+
* arrives when there is room for it and not one pixel sooner. A 60rem
|
|
1544
|
+
* breakpoint introduces it exactly where it starts eating the measure. */
|
|
1545
|
+
@media (min-width: 64rem) {
|
|
1546
|
+
.wave-docs-layout {
|
|
1547
|
+
grid-template-columns: var(--wave-docs-sidebar-width) minmax(0, 1fr);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/*
|
|
1551
|
+
* ⚠️ THE MOST DELETABLE-LOOKING LINE IN THIS FILE. IT IS NOT DELETABLE.
|
|
1552
|
+
*
|
|
1553
|
+
* `display: contents` makes the dialog box vanish from layout **and from
|
|
1554
|
+
* the accessibility tree**, so the `<DocsSidebar>` inside it becomes the
|
|
1555
|
+
* sticky column directly. That is what lets one nav serve both breakpoints:
|
|
1556
|
+
* one landmark, one copy of the links in the payload, nothing to keep in
|
|
1557
|
+
* step. Remove it and the desktop sidebar disappears entirely — or, if
|
|
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.
|
|
1561
|
+
*
|
|
1562
|
+
* `:not(:modal)` so the drawer still opens as a real modal at any width;
|
|
1563
|
+
* it is the same element, and a 64rem viewport with a 200% zoom is a
|
|
1564
|
+
* narrow one.
|
|
1565
|
+
*/
|
|
1566
|
+
dialog.wave-docs-layout__drawer:not(:modal) {
|
|
1567
|
+
display: contents;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
.wave-docs-layout__sidebar {
|
|
1571
|
+
display: block;
|
|
1572
|
+
position: sticky;
|
|
1573
|
+
top: var(--wave-docs-header-height);
|
|
1574
|
+
/* `dvh`, not `vh`: with a mobile URL bar showing, `vh` is taller than the
|
|
1575
|
+
* visible viewport, so the last few nav items sit under the chrome and
|
|
1576
|
+
* cannot be reached. */
|
|
1577
|
+
max-height: calc(100dvh - var(--wave-docs-header-height));
|
|
1578
|
+
overflow-y: auto;
|
|
1579
|
+
/* Without this, reaching the end of the nav keeps scrolling — the article
|
|
1580
|
+
* jumps under the reader's cursor while they are looking at the sidebar. */
|
|
1581
|
+
overscroll-behavior: contain;
|
|
1582
|
+
/* Reserves the scrollbar's width whether or not it is showing, so the
|
|
1583
|
+
* column does not jump when the nav grows past a screen. Costs ~15px on
|
|
1584
|
+
* platforms with classic scrollbars; a jumping column costs more. */
|
|
1585
|
+
scrollbar-gutter: stable;
|
|
1586
|
+
padding-block: 2rem;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
@media (min-width: 80rem) {
|
|
1591
|
+
/*
|
|
1592
|
+
* Inside the query on purpose. As a top-level rule this `:has()` reserves
|
|
1593
|
+
* `--wave-docs-toc-width` of nothing at every width — measured at 390px it
|
|
1594
|
+
* squeezed the article to 94px — because the TOC element is in the DOM at
|
|
1595
|
+
* all sizes and only its `display` changes.
|
|
1596
|
+
*/
|
|
1597
|
+
.wave-docs-layout:has(.wave-docs-layout__toc) {
|
|
1598
|
+
grid-template-columns:
|
|
1599
|
+
var(--wave-docs-sidebar-width)
|
|
1600
|
+
minmax(0, 1fr)
|
|
1601
|
+
var(--wave-docs-toc-width);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
.wave-docs-layout__toc {
|
|
1605
|
+
display: block;
|
|
1606
|
+
position: sticky;
|
|
1607
|
+
/* `align-self: start`, or the sticky box stretches to the row height and
|
|
1608
|
+
* has nothing left to travel within. */
|
|
1609
|
+
align-self: start;
|
|
1610
|
+
top: var(--wave-docs-header-height);
|
|
1611
|
+
max-height: calc(100dvh - var(--wave-docs-header-height));
|
|
1612
|
+
overflow-y: auto;
|
|
1613
|
+
overscroll-behavior: contain;
|
|
1614
|
+
padding-block: 2rem;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/* One declaration, for the only screenshot anyone takes on a 2560px display:
|
|
1619
|
+
* uncapped, the sidebar pins to the far-left bezel and the TOC to the
|
|
1620
|
+
* far-right, about 1500px apart, with the text floating between them. */
|
|
1621
|
+
@media (min-width: 100rem) {
|
|
1622
|
+
.wave-docs-layout {
|
|
1623
|
+
max-width: var(--wave-docs-shell-width);
|
|
1624
|
+
margin-inline: auto;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/* -------------------------------------------------------------------------
|
|
1629
|
+
* Header
|
|
1630
|
+
* ---------------------------------------------------------------------- */
|
|
1631
|
+
|
|
1632
|
+
.wave-docs-layout__header {
|
|
1633
|
+
position: sticky;
|
|
1634
|
+
top: 0;
|
|
1635
|
+
/* Above the sticky sidebar and TOC, which establish their own stacking
|
|
1636
|
+
* contexts; below the search dialog, which portals to `<body>`. */
|
|
1637
|
+
z-index: 20;
|
|
1638
|
+
height: var(--wave-docs-header-height);
|
|
1639
|
+
background-color: var(--wave-docs-bg);
|
|
1640
|
+
border-block-end: 1px solid var(--wave-docs-border);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
.wave-docs-layout__header-inner {
|
|
1644
|
+
display: flex;
|
|
1645
|
+
align-items: center;
|
|
1646
|
+
gap: 0.75rem;
|
|
1647
|
+
height: 100%;
|
|
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;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
@media (min-width: 30rem) {
|
|
1657
|
+
.wave-docs-layout__header-inner {
|
|
1658
|
+
padding-inline: 1.5rem;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
.wave-docs-layout__title {
|
|
1663
|
+
font-weight: 600;
|
|
1664
|
+
/* The brand is not a link target here and must not swallow the row: a long
|
|
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;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/* Pushes search and actions to the end, and stays correct when `title` is
|
|
1673
|
+
* absent — which is why it is on the search wrapper rather than a margin on
|
|
1674
|
+
* the title that would not exist. */
|
|
1675
|
+
.wave-docs-layout__search {
|
|
1676
|
+
margin-inline-start: auto;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.wave-docs-layout__actions {
|
|
1680
|
+
display: flex;
|
|
1681
|
+
align-items: center;
|
|
1682
|
+
gap: 0.5rem;
|
|
1683
|
+
/* When `search={false}` there is no auto margin ahead of this, so it
|
|
1684
|
+
* carries its own. Both set, only one ever has room to apply. */
|
|
1685
|
+
margin-inline-start: auto;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.wave-docs-layout__nav-trigger {
|
|
1689
|
+
display: inline-flex;
|
|
1690
|
+
align-items: center;
|
|
1691
|
+
justify-content: center;
|
|
1692
|
+
/* 44px: the smallest target iOS and WCAG 2.5.8 both accept, and this is
|
|
1693
|
+
* the only control on a phone that reaches the rest of the site. */
|
|
1694
|
+
width: 2.75rem;
|
|
1695
|
+
height: 2.75rem;
|
|
1696
|
+
/* Optical alignment: the icon's own padding already insets it from the
|
|
1697
|
+
* screen edge, so the button hangs left of the text below it without this. */
|
|
1698
|
+
margin-inline-start: -0.75rem;
|
|
1699
|
+
border: 0;
|
|
1700
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
1701
|
+
background: none;
|
|
1702
|
+
color: var(--wave-docs-fg-muted);
|
|
1703
|
+
cursor: pointer;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.wave-docs-layout__nav-trigger:hover {
|
|
1707
|
+
background-color: var(--wave-docs-bg-subtle);
|
|
1708
|
+
color: var(--wave-docs-fg);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/* -------------------------------------------------------------------------
|
|
1712
|
+
* Drawer
|
|
1713
|
+
* ---------------------------------------------------------------------- */
|
|
1714
|
+
|
|
1715
|
+
dialog.wave-docs-layout__drawer {
|
|
1716
|
+
/* A full-height panel against the inline start edge, not a centred box.
|
|
1717
|
+
* `margin` rather than `inset`, because a dialog is centred by `margin:
|
|
1718
|
+
* auto` in the UA sheet and only a margin overrides that. */
|
|
1719
|
+
margin: 0 auto 0 0;
|
|
1720
|
+
box-sizing: border-box;
|
|
1721
|
+
width: min(20rem, 85vw);
|
|
1722
|
+
max-width: none;
|
|
1723
|
+
height: 100dvh;
|
|
1724
|
+
max-height: none;
|
|
1725
|
+
padding: 1.5rem 1rem;
|
|
1726
|
+
border: 0;
|
|
1727
|
+
border-inline-end: 1px solid var(--wave-docs-border);
|
|
1728
|
+
background-color: var(--wave-docs-bg);
|
|
1729
|
+
color: var(--wave-docs-fg);
|
|
1730
|
+
overflow-y: auto;
|
|
1731
|
+
/* Reaching the end of the nav must not start scrolling the page behind. */
|
|
1732
|
+
overscroll-behavior: contain;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
dialog.wave-docs-layout__drawer::backdrop {
|
|
1736
|
+
background-color: oklch(0.2 0.02 262 / 0.55);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.wave-docs-layout__drawer-close {
|
|
1740
|
+
display: inline-flex;
|
|
1741
|
+
align-items: center;
|
|
1742
|
+
justify-content: center;
|
|
1743
|
+
width: 2.75rem;
|
|
1744
|
+
height: 2.75rem;
|
|
1745
|
+
margin-block-end: 0.5rem;
|
|
1746
|
+
margin-inline-start: auto;
|
|
1747
|
+
border: 0;
|
|
1748
|
+
border-radius: var(--wave-docs-radius-sm);
|
|
1749
|
+
background: none;
|
|
1750
|
+
color: var(--wave-docs-fg-muted);
|
|
1751
|
+
cursor: pointer;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
.wave-docs-layout__drawer-close:hover {
|
|
1755
|
+
background-color: var(--wave-docs-bg-subtle);
|
|
1756
|
+
color: var(--wave-docs-fg);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/*
|
|
1760
|
+
* ⚠️ SCOPED TO OUR DRAWER, DELIBERATELY. `html:has(dialog:modal)` would lock
|
|
1761
|
+
* the page for a consumer's own modal too — and unlock it again the moment
|
|
1762
|
+
* ours closed underneath theirs.
|
|
1763
|
+
*/
|
|
1764
|
+
html:has(dialog.wave-docs-layout__drawer:modal) {
|
|
1765
|
+
overflow: hidden;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/*
|
|
1769
|
+
* Drawer furniture, hidden once the drawer is a column.
|
|
1770
|
+
*
|
|
1771
|
+
* ⚠️ AFTER the rules it overrides, not up with the other 64rem block. Same
|
|
1772
|
+
* specificity means source order decides, and declared earlier this lost to
|
|
1773
|
+
* the `display: inline-flex` above at every width — leaving a menu button
|
|
1774
|
+
* and a close button sitting in a desktop layout that has nothing to open or
|
|
1775
|
+
* close. Measured at 1280px: `flex`, not `none`.
|
|
1776
|
+
*/
|
|
1777
|
+
@media (min-width: 64rem) {
|
|
1778
|
+
.wave-docs-layout__nav-trigger,
|
|
1779
|
+
.wave-docs-layout__drawer-close {
|
|
1780
|
+
display: none;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
819
1785
|
/* ---------------------------------------------------------------------------
|
|
820
1786
|
* Search
|
|
821
1787
|
*
|
|
@@ -849,10 +1815,9 @@
|
|
|
849
1815
|
}
|
|
850
1816
|
|
|
851
1817
|
.wave-docs-search-trigger:focus-visible,
|
|
852
|
-
.wave-docs-search-close:focus-visible
|
|
853
|
-
|
|
854
|
-
outline:
|
|
855
|
-
box-shadow: var(--wave-docs-ring);
|
|
1818
|
+
.wave-docs-search-close:focus-visible {
|
|
1819
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
1820
|
+
outline-offset: 2px;
|
|
856
1821
|
}
|
|
857
1822
|
|
|
858
1823
|
/* The label is the flexible half of the trigger; the shortcut hint beside it
|
|
@@ -881,7 +1846,10 @@
|
|
|
881
1846
|
justify-content: center;
|
|
882
1847
|
/* Not centred: a dialog that grows downward from a fixed top does not
|
|
883
1848
|
* shift under the reader as results stream in. */
|
|
884
|
-
|
|
1849
|
+
/* `dvh`, not `vh`: on a phone `vh` is the viewport with the URL bar
|
|
1850
|
+
* *retracted*, so a dialog sized against it is taller than what the reader
|
|
1851
|
+
* can actually see and its bottom edge sits under the browser chrome. */
|
|
1852
|
+
padding: 10dvh 1rem 1rem;
|
|
885
1853
|
background: oklch(0.2 0.02 265 / 0.5);
|
|
886
1854
|
backdrop-filter: blur(2px);
|
|
887
1855
|
}
|
|
@@ -891,12 +1859,12 @@
|
|
|
891
1859
|
flex-direction: column;
|
|
892
1860
|
width: 100%;
|
|
893
1861
|
max-width: 34rem;
|
|
894
|
-
max-height: min(32rem,
|
|
1862
|
+
max-height: min(32rem, 80dvh);
|
|
895
1863
|
overflow: hidden;
|
|
896
1864
|
border: 1px solid var(--wave-docs-border);
|
|
897
1865
|
border-radius: var(--wave-docs-radius);
|
|
898
1866
|
background: var(--wave-docs-bg);
|
|
899
|
-
box-shadow: 0 24px 48px -12px
|
|
1867
|
+
box-shadow: 0 24px 48px -12px var(--wave-docs-shadow);
|
|
900
1868
|
}
|
|
901
1869
|
|
|
902
1870
|
.wave-docs-search-input-row {
|
|
@@ -905,6 +1873,22 @@
|
|
|
905
1873
|
gap: 0.5rem;
|
|
906
1874
|
padding: 0.75rem;
|
|
907
1875
|
border-block-end: 1px solid var(--wave-docs-border);
|
|
1876
|
+
/* Matches the dialog's corners, so the focus ring below traces them
|
|
1877
|
+
* instead of poking out of the rounded frame. */
|
|
1878
|
+
border-start-start-radius: var(--wave-docs-radius);
|
|
1879
|
+
border-start-end-radius: var(--wave-docs-radius);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/* The ring belongs to the row, not the input. A 2px ring drawn around a
|
|
1883
|
+
* borderless full-width input reads as an error state, and the previous
|
|
1884
|
+
* answer — "the dialog frame is the indicator" — was a static 1.31:1 border
|
|
1885
|
+
* that looks identical focused and unfocused (WCAG 2.4.7 wants a change of
|
|
1886
|
+
* appearance, 1.4.11 wants 3:1; `accent` on `bg` is 5.16:1 / 7.50:1).
|
|
1887
|
+
* `outline-offset: -2px` draws it inward, because the dialog clips with
|
|
1888
|
+
* `overflow: hidden` and an outward ring would be sliced off. */
|
|
1889
|
+
.wave-docs-search-input-row:has(.wave-docs-search-input:focus-visible) {
|
|
1890
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
1891
|
+
outline-offset: -2px;
|
|
908
1892
|
}
|
|
909
1893
|
|
|
910
1894
|
.wave-docs-search-input {
|
|
@@ -918,9 +1902,10 @@
|
|
|
918
1902
|
font-size: 1rem;
|
|
919
1903
|
}
|
|
920
1904
|
|
|
1905
|
+
/* The one `outline: none` in the file, and it is not a missing indicator:
|
|
1906
|
+
* the row around the input draws a real outline on `:has(:focus-visible)`,
|
|
1907
|
+
* and a second one on the input itself would double up. */
|
|
921
1908
|
.wave-docs-search-input:focus {
|
|
922
|
-
/* The dialog frame is the focus indicator here; a ring on a full-width
|
|
923
|
-
* borderless input reads as an error state. */
|
|
924
1909
|
outline: none;
|
|
925
1910
|
}
|
|
926
1911
|
|
|
@@ -956,9 +1941,16 @@
|
|
|
956
1941
|
}
|
|
957
1942
|
|
|
958
1943
|
/* Driven by `aria-selected` through the active class the dialog sets — the
|
|
959
|
-
* combobox keeps focus in the input
|
|
1944
|
+
* combobox keeps focus in the input and every result is `tabindex="-1"`, so
|
|
1945
|
+
* `:focus-visible` can never fire here and the tint IS the whole indicator.
|
|
1946
|
+
* A tint alone is 1.12:1 light / 1.19:1 dark; WCAG 1.4.11 asks 3:1 of a
|
|
1947
|
+
* state indicator, so the outline carries it (`accent` on `accent-subtle` is
|
|
1948
|
+
* 4.60:1 / 6.30:1) and the tint is left to do the soft work. Inset by 2px so
|
|
1949
|
+
* it stays inside the scrolling results list. */
|
|
960
1950
|
.wave-docs-search-result-active {
|
|
961
1951
|
background: var(--wave-docs-accent-subtle);
|
|
1952
|
+
outline: 2px solid var(--wave-docs-accent);
|
|
1953
|
+
outline-offset: -2px;
|
|
962
1954
|
}
|
|
963
1955
|
|
|
964
1956
|
.wave-docs-search-result-link {
|