@waveso/docs 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/dist/frontmatter.d.ts +55 -0
  4. package/dist/frontmatter.js +80 -0
  5. package/dist/highlighter.d.ts +99 -0
  6. package/dist/highlighter.js +183 -0
  7. package/dist/meta.d.ts +75 -0
  8. package/dist/meta.js +183 -0
  9. package/dist/next.d.ts +256 -0
  10. package/dist/next.js +365 -0
  11. package/dist/plugins/rehype-capture-toc.d.ts +18 -0
  12. package/dist/plugins/rehype-capture-toc.js +69 -0
  13. package/dist/plugins/remark-doc-links.d.ts +63 -0
  14. package/dist/plugins/remark-doc-links.js +122 -0
  15. package/dist/plugins/remark-unwrap-images.d.ts +11 -0
  16. package/dist/plugins/remark-unwrap-images.js +25 -0
  17. package/dist/plugins/remark-youtube.d.ts +22 -0
  18. package/dist/plugins/remark-youtube.js +84 -0
  19. package/dist/react/callout.d.ts +37 -0
  20. package/dist/react/callout.js +113 -0
  21. package/dist/react/doc-content.d.ts +29 -0
  22. package/dist/react/doc-content.js +30 -0
  23. package/dist/react/markdown-components.d.ts +84 -0
  24. package/dist/react/markdown-components.js +122 -0
  25. package/dist/react/search-dialog.d.ts +41 -0
  26. package/dist/react/search-dialog.js +404 -0
  27. package/dist/react/sidebar.d.ts +29 -0
  28. package/dist/react/sidebar.js +196 -0
  29. package/dist/react/skip-link.d.ts +37 -0
  30. package/dist/react/skip-link.js +37 -0
  31. package/dist/react/toc.d.ts +35 -0
  32. package/dist/react/toc.js +87 -0
  33. package/dist/react/youtube.d.ts +27 -0
  34. package/dist/react/youtube.js +75 -0
  35. package/dist/render.d.ts +72 -0
  36. package/dist/render.js +279 -0
  37. package/dist/search-index.d.ts +51 -0
  38. package/dist/search-index.js +274 -0
  39. package/dist/search-options.d.ts +18 -0
  40. package/dist/search-options.js +40 -0
  41. package/dist/source.d.ts +67 -0
  42. package/dist/source.js +332 -0
  43. package/dist/styles.css +1033 -0
  44. package/dist/types.d.ts +334 -0
  45. package/dist/types.js +0 -0
  46. package/package.json +166 -0
@@ -0,0 +1,1033 @@
1
+ /*
2
+ * @waveso/docs — the stylesheet for the React layer.
3
+ *
4
+ * In your app's CSS entry:
5
+ *
6
+ * @import 'tailwindcss'; (optional — nothing below requires it)
7
+ * @import '@waveso/docs/styles.css';
8
+ *
9
+ * Everything is plain CSS driven by `--wave-docs-*` custom properties, with no
10
+ * `@apply` anywhere: Tailwind is an *optional* peer dependency, and a
11
+ * stylesheet that explodes without it would be a broken promise.
12
+ *
13
+ * Retheme by redefining the tokens in your own `:root`; restyle by overriding
14
+ * the classes. Both win, and NOT because of source order — everything this file
15
+ * declares lives in a `@layer`, and unlayered CSS beats every layer regardless
16
+ * of specificity. That distinction is the whole fix: the dark token block is
17
+ * `:root:not([data-theme='light'])`, which is (0,2,0), so an unlayered `:root`
18
+ * at (0,1,0) used to lose no matter where it was loaded — the cascade never got
19
+ * as far as order. A consumer had to write `:root:root:root` to outrank us.
20
+ *
21
+ * `@tailwindcss/typography` is deliberately not used. `.prose` pins
22
+ * `max-width: 65ch` — wrong inside a three-column docs shell — and styles
23
+ * `pre`/`code` in direct conflict with Shiki's own output. `.wave-docs-prose`
24
+ * below does the same job without the fight.
25
+ */
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
+ /* Explicit, so the order does not silently depend on which block appears
34
+ * first. `theme` holds the tokens, `base` the element resets, `components`
35
+ * the classes. A consumer's own unlayered CSS outranks all three. */
36
+ @layer theme, base, components;
37
+
38
+ /* ---------------------------------------------------------------------------
39
+ * Tokens
40
+ *
41
+ * The dark block is spelled twice on purpose: `@media` and an attribute
42
+ * selector cannot be combined into one selector list. `data-theme` wins over
43
+ * the OS in both directions — `[data-theme='light']` opts out of a dark system,
44
+ * `[data-theme='dark']` opts into dark on a light one.
45
+ * ------------------------------------------------------------------------ */
46
+
47
+ /*
48
+ * Every foreground/background pair these tokens compose is asserted to clear
49
+ * WCAG 1.4.3 (4.5:1) by `src/styles.test.ts`. None of the text this package
50
+ * renders is "large text" in the WCAG sense — the callout labels are 16px, the
51
+ * search breadcrumbs 12px, the sidebar separators 12px — so 3:1 is never
52
+ * enough. Retheme by all means; run that test afterwards.
53
+ *
54
+ * The light ramp reads fg (0.25) / fg-muted (0.45) / fg-subtle (0.54). The gap
55
+ * between the last two is small because `fg-subtle` cannot go lighter than
56
+ * ~0.555 and still clear 4.5:1 on `bg-subtle`; `fg-muted` moved down to keep
57
+ * three visibly distinct steps rather than two-and-a-half.
58
+ *
59
+ * The dark ramp is fg (0.93) / fg-muted (0.72) / fg-subtle (0.64), and its
60
+ * `accent-subtle` sits at 0.26 rather than 0.30 — `fg-subtle` paints the search
61
+ * breadcrumb, which lands on `accent-subtle` in the highlighted result, and a
62
+ * lighter tint there would force `fg-subtle` up into `fg-muted`.
63
+ */
64
+ @layer theme {
65
+ /* Layered so a consumer's unlayered `:root` outranks these without having
66
+ * to out-specify `:root:not([data-theme='light'])`. Inside this layer the
67
+ * usual rules still apply, so the dark blocks keep beating the light one. */
68
+ :root {
69
+ --wave-docs-bg: oklch(1 0 0);
70
+ --wave-docs-bg-subtle: oklch(0.985 0.002 260);
71
+ --wave-docs-fg: oklch(0.25 0.02 262);
72
+ --wave-docs-fg-muted: oklch(0.45 0.02 262);
73
+ --wave-docs-fg-subtle: oklch(0.54 0.015 262);
74
+ --wave-docs-border: oklch(0.91 0.006 262);
75
+ --wave-docs-border-strong: oklch(0.84 0.008 262);
76
+
77
+ /* Wave Blue #0074DE, darkened to #006EC8: the brand value clears 4.5:1 on
78
+ * white but only 4.26:1 on `accent-subtle`, which is what the active sidebar
79
+ * link is painted on. */
80
+ --wave-docs-accent: oklch(0.53 0.17 248);
81
+ --wave-docs-accent-hover: oklch(0.46 0.17 248);
82
+ --wave-docs-accent-fg: oklch(0.99 0 0);
83
+ --wave-docs-accent-subtle: oklch(0.96 0.025 248);
84
+
85
+ --wave-docs-code-bg: oklch(0.97 0.003 262);
86
+ --wave-docs-code-fg: oklch(0.32 0.03 300);
87
+ --wave-docs-code-block-bg: oklch(0.985 0.002 260);
88
+
89
+ /* Each accent is the label colour on its own tinted background, so each one
90
+ * is a 4.5:1 pair in its own right — not merely legible against the page. */
91
+ --wave-docs-callout-note: oklch(0.53 0.17 248);
92
+ --wave-docs-callout-note-bg: oklch(0.97 0.02 248);
93
+ --wave-docs-callout-tip: oklch(0.52 0.13 156);
94
+ --wave-docs-callout-tip-bg: oklch(0.97 0.03 156);
95
+ --wave-docs-callout-important: oklch(0.55 0.19 300);
96
+ --wave-docs-callout-important-bg: oklch(0.97 0.025 300);
97
+ --wave-docs-callout-warning: oklch(0.53 0.14 72);
98
+ --wave-docs-callout-warning-bg: oklch(0.97 0.04 82);
99
+ --wave-docs-callout-caution: oklch(0.55 0.2 24);
100
+ --wave-docs-callout-caution-bg: oklch(0.97 0.025 24);
101
+
102
+ --wave-docs-radius: 0.5rem;
103
+ --wave-docs-radius-sm: 0.375rem;
104
+ --wave-docs-header-height: 4rem;
105
+ --wave-docs-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
106
+ Consolas, "Liberation Mono", monospace;
107
+
108
+ /* Focus ring, shared by every interactive thing in the package. */
109
+ --wave-docs-ring: 0 0 0 2px var(--wave-docs-bg),
110
+ 0 0 0 4px var(--wave-docs-accent);
111
+ }
112
+
113
+ @media (prefers-color-scheme: dark) {
114
+ :root:not([data-theme='light']) {
115
+ --wave-docs-bg: oklch(0.19 0.015 265);
116
+ --wave-docs-bg-subtle: oklch(0.23 0.015 265);
117
+ --wave-docs-fg: oklch(0.93 0.008 265);
118
+ --wave-docs-fg-muted: oklch(0.72 0.014 265);
119
+ --wave-docs-fg-subtle: oklch(0.64 0.015 265);
120
+ --wave-docs-border: oklch(0.31 0.014 265);
121
+ --wave-docs-border-strong: oklch(0.4 0.016 265);
122
+
123
+ --wave-docs-accent: oklch(0.72 0.14 248);
124
+ --wave-docs-accent-hover: oklch(0.8 0.12 248);
125
+ --wave-docs-accent-fg: oklch(0.18 0.02 265);
126
+ --wave-docs-accent-subtle: oklch(0.26 0.06 248);
127
+
128
+ --wave-docs-code-bg: oklch(0.26 0.016 265);
129
+ --wave-docs-code-fg: oklch(0.86 0.04 300);
130
+ --wave-docs-code-block-bg: oklch(0.17 0.015 265);
131
+
132
+ --wave-docs-callout-note: oklch(0.75 0.13 248);
133
+ --wave-docs-callout-note-bg: oklch(0.27 0.05 248);
134
+ --wave-docs-callout-tip: oklch(0.78 0.14 156);
135
+ --wave-docs-callout-tip-bg: oklch(0.27 0.05 156);
136
+ --wave-docs-callout-important: oklch(0.76 0.14 300);
137
+ --wave-docs-callout-important-bg: oklch(0.28 0.06 300);
138
+ --wave-docs-callout-warning: oklch(0.82 0.14 82);
139
+ --wave-docs-callout-warning-bg: oklch(0.29 0.05 72);
140
+ --wave-docs-callout-caution: oklch(0.72 0.16 24);
141
+ --wave-docs-callout-caution-bg: oklch(0.28 0.06 24);
142
+ }
143
+ }
144
+
145
+ :root[data-theme='dark'] {
146
+ --wave-docs-bg: oklch(0.19 0.015 265);
147
+ --wave-docs-bg-subtle: oklch(0.23 0.015 265);
148
+ --wave-docs-fg: oklch(0.93 0.008 265);
149
+ --wave-docs-fg-muted: oklch(0.72 0.014 265);
150
+ --wave-docs-fg-subtle: oklch(0.64 0.015 265);
151
+ --wave-docs-border: oklch(0.31 0.014 265);
152
+ --wave-docs-border-strong: oklch(0.4 0.016 265);
153
+
154
+ --wave-docs-accent: oklch(0.72 0.14 248);
155
+ --wave-docs-accent-hover: oklch(0.8 0.12 248);
156
+ --wave-docs-accent-fg: oklch(0.18 0.02 265);
157
+ --wave-docs-accent-subtle: oklch(0.26 0.06 248);
158
+
159
+ --wave-docs-code-bg: oklch(0.26 0.016 265);
160
+ --wave-docs-code-fg: oklch(0.86 0.04 300);
161
+ --wave-docs-code-block-bg: oklch(0.17 0.015 265);
162
+
163
+ --wave-docs-callout-note: oklch(0.75 0.13 248);
164
+ --wave-docs-callout-note-bg: oklch(0.27 0.05 248);
165
+ --wave-docs-callout-tip: oklch(0.78 0.14 156);
166
+ --wave-docs-callout-tip-bg: oklch(0.27 0.05 156);
167
+ --wave-docs-callout-important: oklch(0.76 0.14 300);
168
+ --wave-docs-callout-important-bg: oklch(0.28 0.06 300);
169
+ --wave-docs-callout-warning: oklch(0.82 0.14 82);
170
+ --wave-docs-callout-warning-bg: oklch(0.29 0.05 72);
171
+ --wave-docs-callout-caution: oklch(0.72 0.16 24);
172
+ --wave-docs-callout-caution-bg: oklch(0.28 0.06 24);
173
+ }
174
+
175
+ /* ---------------------------------------------------------------------------
176
+ * Base
177
+ * ------------------------------------------------------------------------ */
178
+ }
179
+
180
+ @layer base {
181
+ html {
182
+ /* Without this, every `#anchor` link parks its heading underneath the
183
+ * sticky header. Match it to your header's height. */
184
+ scroll-padding-top: var(--wave-docs-header-height);
185
+ }
186
+
187
+ /* Smooth scrolling is opt-in by the reader's OS setting, never by us:
188
+ * animated scrolling triggers nausea and migraine in people with vestibular
189
+ * disorders. Declared here rather than in JavaScript so there is exactly one
190
+ * place that decides it. */
191
+ @media (prefers-reduced-motion: no-preference) {
192
+ html {
193
+ scroll-behavior: smooth;
194
+ }
195
+ }
196
+ }
197
+
198
+ /* ---------------------------------------------------------------------------
199
+ * Utilities used by the components
200
+ * ------------------------------------------------------------------------ */
201
+
202
+ @layer components {
203
+ /* Ours, not Tailwind's `sr-only` — Tailwind is optional here. */
204
+ .wave-docs-sr-only {
205
+ position: absolute;
206
+ width: 1px;
207
+ height: 1px;
208
+ padding: 0;
209
+ margin: -1px;
210
+ overflow: hidden;
211
+ clip-path: inset(50%);
212
+ white-space: nowrap;
213
+ border-width: 0;
214
+ }
215
+
216
+ .wave-docs-skip-link {
217
+ position: absolute;
218
+ top: 0;
219
+ left: 0;
220
+ z-index: 100;
221
+ /* Off-screen rather than `display: none`: a hidden element is not
222
+ * focusable, and an unfocusable skip link is no skip link. */
223
+ transform: translateY(-150%);
224
+ padding: 0.625rem 1rem;
225
+ border-radius: 0 0 var(--wave-docs-radius) 0;
226
+ background: var(--wave-docs-accent);
227
+ color: var(--wave-docs-accent-fg);
228
+ font-weight: 600;
229
+ text-decoration: none;
230
+ }
231
+
232
+ /* `:focus`, not `:focus-visible`. A host that moves focus here on a
233
+ * client-side route change is focusing it programmatically, and
234
+ * `:focus-visible` does not match a programmatic focus that followed a
235
+ * pointer interaction — leaving a focused control parked off-screen with no
236
+ * indicator anywhere (WCAG 2.4.7). This element is unreachable by pointer
237
+ * while it is off-screen, so there is no stray-focus-ring case to avoid.
238
+ * Bootstrap's `.visually-hidden-focusable` and GOV.UK's skip link both use
239
+ * plain `:focus` for the same reason. */
240
+ .wave-docs-skip-link:focus {
241
+ transform: translateY(0);
242
+ outline: none;
243
+ box-shadow: var(--wave-docs-ring);
244
+ }
245
+
246
+ @media (prefers-reduced-motion: no-preference) {
247
+ .wave-docs-skip-link {
248
+ transition: transform 150ms ease-out;
249
+ }
250
+ }
251
+ }
252
+
253
+ /* ---------------------------------------------------------------------------
254
+ * Prose
255
+ * ------------------------------------------------------------------------ */
256
+
257
+ @layer components {
258
+ .wave-docs-prose {
259
+ color: var(--wave-docs-fg);
260
+ font-size: 1rem;
261
+ line-height: 1.75;
262
+ /* No `max-width` on purpose — the docs shell owns column width. */
263
+ }
264
+
265
+ .wave-docs-prose > * + * {
266
+ margin-block-start: 1.25em;
267
+ }
268
+
269
+ .wave-docs-prose :is(h1, h2, h3, h4, h5, h6) {
270
+ position: relative;
271
+ color: var(--wave-docs-fg);
272
+ font-weight: 650;
273
+ line-height: 1.25;
274
+ text-wrap: balance;
275
+ scroll-margin-top: var(--wave-docs-header-height);
276
+ }
277
+
278
+ .wave-docs-prose > :is(h2, h3, h4, h5, h6) {
279
+ margin-block-start: 2em;
280
+ }
281
+
282
+ .wave-docs-prose h1 {
283
+ font-size: 2.25rem;
284
+ letter-spacing: -0.02em;
285
+ }
286
+ .wave-docs-prose h2 {
287
+ font-size: 1.5rem;
288
+ letter-spacing: -0.015em;
289
+ padding-block-end: 0.3em;
290
+ border-block-end: 1px solid var(--wave-docs-border);
291
+ }
292
+ .wave-docs-prose h3 {
293
+ font-size: 1.25rem;
294
+ }
295
+ .wave-docs-prose h4 {
296
+ font-size: 1.0625rem;
297
+ }
298
+ .wave-docs-prose :is(h5, h6) {
299
+ font-size: 1rem;
300
+ color: var(--wave-docs-fg-muted);
301
+ }
302
+
303
+ .wave-docs-prose a {
304
+ color: var(--wave-docs-accent);
305
+ text-decoration: underline;
306
+ text-underline-offset: 0.2em;
307
+ text-decoration-thickness: 1px;
308
+ border-radius: 2px;
309
+ }
310
+
311
+ .wave-docs-prose a:hover {
312
+ color: var(--wave-docs-accent-hover);
313
+ text-decoration-thickness: 2px;
314
+ }
315
+
316
+ .wave-docs-prose a:focus-visible,
317
+ .wave-docs-sidebar :is(a, button):focus-visible,
318
+ .wave-docs-toc a:focus-visible,
319
+ .wave-docs-youtube__facade:focus-visible {
320
+ outline: none;
321
+ box-shadow: var(--wave-docs-ring);
322
+ }
323
+
324
+ .wave-docs-prose :is(ul, ol) {
325
+ padding-inline-start: 1.5rem;
326
+ }
327
+ .wave-docs-prose ul {
328
+ list-style: disc;
329
+ }
330
+ .wave-docs-prose ol {
331
+ list-style: decimal;
332
+ }
333
+ .wave-docs-prose li + li {
334
+ margin-block-start: 0.4em;
335
+ }
336
+ .wave-docs-prose li > :is(ul, ol) {
337
+ margin-block-start: 0.4em;
338
+ }
339
+ .wave-docs-prose li::marker {
340
+ color: var(--wave-docs-fg-subtle);
341
+ }
342
+
343
+ /* GFM task lists: the checkbox replaces the bullet. */
344
+ .wave-docs-prose li:has(> input[type='checkbox']) {
345
+ list-style: none;
346
+ margin-inline-start: -1.25rem;
347
+ }
348
+ .wave-docs-prose input[type='checkbox'] {
349
+ margin-inline-end: 0.5rem;
350
+ accent-color: var(--wave-docs-accent);
351
+ }
352
+
353
+ .wave-docs-prose blockquote {
354
+ padding-inline-start: 1rem;
355
+ border-inline-start: 3px solid var(--wave-docs-border-strong);
356
+ color: var(--wave-docs-fg-muted);
357
+ font-style: italic;
358
+ }
359
+
360
+ .wave-docs-prose hr {
361
+ border: 0;
362
+ border-block-start: 1px solid var(--wave-docs-border);
363
+ margin-block: 2.5em;
364
+ }
365
+
366
+ .wave-docs-prose :is(strong, b) {
367
+ font-weight: 650;
368
+ color: var(--wave-docs-fg);
369
+ }
370
+
371
+ .wave-docs-prose kbd {
372
+ padding: 0.15em 0.4em;
373
+ border: 1px solid var(--wave-docs-border-strong);
374
+ border-block-end-width: 2px;
375
+ border-radius: var(--wave-docs-radius-sm);
376
+ background: var(--wave-docs-bg-subtle);
377
+ font-family: var(--wave-docs-font-mono);
378
+ font-size: 0.85em;
379
+ }
380
+
381
+ .wave-docs-image {
382
+ max-width: 100%;
383
+ height: auto;
384
+ border-radius: var(--wave-docs-radius);
385
+ }
386
+
387
+ /* Inline code only. `.shiki` owns everything inside a `<pre>`. */
388
+ .wave-docs-prose :not(pre) > code {
389
+ padding: 0.15em 0.35em;
390
+ border-radius: var(--wave-docs-radius-sm);
391
+ background: var(--wave-docs-code-bg);
392
+ color: var(--wave-docs-code-fg);
393
+ font-family: var(--wave-docs-font-mono);
394
+ font-size: 0.875em;
395
+ /* Long identifiers must not push the whole column sideways. */
396
+ overflow-wrap: break-word;
397
+ }
398
+ }
399
+
400
+ /* ---------------------------------------------------------------------------
401
+ * Tables
402
+ * ------------------------------------------------------------------------ */
403
+
404
+ @layer components {
405
+ .wave-docs-table-scroll {
406
+ overflow-x: auto;
407
+ border: 1px solid var(--wave-docs-border);
408
+ border-radius: var(--wave-docs-radius);
409
+ }
410
+
411
+ .wave-docs-table-scroll:focus-visible {
412
+ outline: none;
413
+ box-shadow: var(--wave-docs-ring);
414
+ }
415
+
416
+ .wave-docs-table {
417
+ width: 100%;
418
+ border-collapse: collapse;
419
+ font-size: 0.9375rem;
420
+ line-height: 1.6;
421
+ }
422
+
423
+ .wave-docs-table :is(th, td) {
424
+ padding: 0.625rem 0.875rem;
425
+ text-align: start;
426
+ vertical-align: top;
427
+ }
428
+
429
+ .wave-docs-table th {
430
+ background: var(--wave-docs-bg-subtle);
431
+ font-weight: 600;
432
+ white-space: nowrap;
433
+ }
434
+
435
+ .wave-docs-table :is(thead, tbody tr + tr) {
436
+ border-block-start: 1px solid var(--wave-docs-border);
437
+ }
438
+ }
439
+
440
+ /* ---------------------------------------------------------------------------
441
+ * Heading anchors
442
+ *
443
+ * Emitted by `rehype-autolink-headings` as
444
+ * `<a class="heading-anchor" aria-hidden="true" tabindex="-1">#</a>`, and
445
+ * therefore purely decorative: it is out of the tab order and out of the
446
+ * accessibility tree, so it can never be focused and has no `:focus-visible`
447
+ * state to style. Revealed on hover of its heading, and nowhere else. The
448
+ * keyboard route to a section is the table of contents, which links the same
449
+ * anchors with real text.
450
+ * ------------------------------------------------------------------------ */
451
+
452
+ @layer components {
453
+ .heading-anchor {
454
+ margin-inline-start: 0.35em;
455
+ color: var(--wave-docs-fg-subtle);
456
+ text-decoration: none;
457
+ opacity: 0;
458
+ }
459
+
460
+ :is(h1, h2, h3, h4, h5, h6):hover > .heading-anchor {
461
+ opacity: 1;
462
+ }
463
+
464
+ .heading-anchor:hover {
465
+ color: var(--wave-docs-accent);
466
+ }
467
+
468
+ @media (prefers-reduced-motion: no-preference) {
469
+ .heading-anchor {
470
+ transition: opacity 120ms ease-out;
471
+ }
472
+ }
473
+ }
474
+
475
+ /* ---------------------------------------------------------------------------
476
+ * Shiki
477
+ *
478
+ * Dual-theme promotion. Shiki writes the light colour as an INLINE style on
479
+ * every token span and the dark one beside it as `--shiki-dark`, so switching
480
+ * themes means overriding an inline style. `!important` is the only thing in
481
+ * CSS that outranks one — this is a specificity requirement, not a preference,
482
+ * and removing it silently leaves dark mode rendering light-theme code.
483
+ * ------------------------------------------------------------------------ */
484
+
485
+ .shiki {
486
+ padding: 1rem 1.125rem;
487
+ border: 1px solid var(--wave-docs-border);
488
+ border-radius: var(--wave-docs-radius);
489
+ background-color: var(--wave-docs-code-block-bg);
490
+ overflow-x: auto;
491
+ font-family: var(--wave-docs-font-mono);
492
+ font-size: 0.875rem;
493
+ line-height: 1.7;
494
+ tab-size: 2;
495
+ }
496
+
497
+ .shiki code {
498
+ display: grid;
499
+ font-family: inherit;
500
+ }
501
+
502
+ .shiki .line {
503
+ min-height: 1lh;
504
+ }
505
+
506
+ @media (prefers-color-scheme: dark) {
507
+ :root:not([data-theme='light']) .shiki,
508
+ :root:not([data-theme='light']) .shiki span {
509
+ color: var(--shiki-dark) !important;
510
+ }
511
+ :root:not([data-theme='light']) .shiki {
512
+ background-color: var(--shiki-dark-bg) !important;
513
+ }
514
+ }
515
+
516
+ :root[data-theme='dark'] .shiki,
517
+ :root[data-theme='dark'] .shiki span {
518
+ color: var(--shiki-dark) !important;
519
+ }
520
+ :root[data-theme='dark'] .shiki {
521
+ background-color: var(--shiki-dark-bg) !important;
522
+ }
523
+
524
+ /* ---------------------------------------------------------------------------
525
+ * Callouts
526
+ * ------------------------------------------------------------------------ */
527
+
528
+ @layer components {
529
+ .wave-docs-callout {
530
+ --callout-accent: var(--wave-docs-callout-note);
531
+ --callout-bg: var(--wave-docs-callout-note-bg);
532
+
533
+ padding: 0.875rem 1rem;
534
+ border: 1px solid color-mix(in oklab, var(--callout-accent) 35%, transparent);
535
+ border-inline-start: 3px solid var(--callout-accent);
536
+ border-radius: var(--wave-docs-radius);
537
+ background: var(--callout-bg);
538
+ }
539
+
540
+ .wave-docs-callout--tip {
541
+ --callout-accent: var(--wave-docs-callout-tip);
542
+ --callout-bg: var(--wave-docs-callout-tip-bg);
543
+ }
544
+ .wave-docs-callout--important {
545
+ --callout-accent: var(--wave-docs-callout-important);
546
+ --callout-bg: var(--wave-docs-callout-important-bg);
547
+ }
548
+ .wave-docs-callout--warning {
549
+ --callout-accent: var(--wave-docs-callout-warning);
550
+ --callout-bg: var(--wave-docs-callout-warning-bg);
551
+ }
552
+ .wave-docs-callout--caution {
553
+ --callout-accent: var(--wave-docs-callout-caution);
554
+ --callout-bg: var(--wave-docs-callout-caution-bg);
555
+ }
556
+
557
+ .wave-docs-callout__label {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 0.5rem;
561
+ margin: 0 0 0.35rem;
562
+ color: var(--callout-accent);
563
+ font-weight: 650;
564
+ line-height: 1.4;
565
+ }
566
+
567
+ .wave-docs-callout__icon {
568
+ flex: none;
569
+ }
570
+
571
+ .wave-docs-callout__body > * {
572
+ margin: 0;
573
+ }
574
+
575
+ .wave-docs-callout__body > * + * {
576
+ margin-block-start: 0.75em;
577
+ }
578
+ }
579
+
580
+ /* ---------------------------------------------------------------------------
581
+ * YouTube facade
582
+ * ------------------------------------------------------------------------ */
583
+
584
+ @layer components {
585
+ .wave-docs-youtube {
586
+ position: relative;
587
+ aspect-ratio: 16 / 9;
588
+ overflow: hidden;
589
+ border-radius: var(--wave-docs-radius);
590
+ background: oklch(0 0 0);
591
+ }
592
+
593
+ .wave-docs-youtube__facade {
594
+ display: block;
595
+ width: 100%;
596
+ height: 100%;
597
+ padding: 0;
598
+ border: 0;
599
+ background: none;
600
+ cursor: pointer;
601
+ }
602
+
603
+ .wave-docs-youtube__thumbnail {
604
+ width: 100%;
605
+ height: 100%;
606
+ /* `hqdefault` is 4:3 with letterbox bars baked in; cropping to 16:9 hides
607
+ * them. */
608
+ object-fit: cover;
609
+ }
610
+
611
+ .wave-docs-youtube__play {
612
+ position: absolute;
613
+ inset: 0;
614
+ display: grid;
615
+ place-items: center;
616
+ }
617
+
618
+ .wave-docs-youtube__play-bg {
619
+ fill: oklch(0.3 0 0 / 0.75);
620
+ }
621
+
622
+ .wave-docs-youtube__play-arrow {
623
+ fill: oklch(1 0 0);
624
+ }
625
+
626
+ .wave-docs-youtube__facade:is(:hover, :focus-visible)
627
+ .wave-docs-youtube__play-bg {
628
+ fill: oklch(0.55 0.22 27);
629
+ }
630
+
631
+ .wave-docs-youtube__frame {
632
+ width: 100%;
633
+ height: 100%;
634
+ border: 0;
635
+ }
636
+
637
+ @media (prefers-reduced-motion: no-preference) {
638
+ .wave-docs-youtube__play-bg {
639
+ transition: fill 150ms ease-out;
640
+ }
641
+ }
642
+ }
643
+
644
+ /* ---------------------------------------------------------------------------
645
+ * Sidebar
646
+ * ------------------------------------------------------------------------ */
647
+
648
+ @layer components {
649
+ .wave-docs-sidebar {
650
+ font-size: 0.875rem;
651
+ line-height: 1.5;
652
+ }
653
+
654
+ .wave-docs-sidebar__list {
655
+ margin: 0;
656
+ padding: 0;
657
+ list-style: none;
658
+ }
659
+
660
+ /* Nested levels get a guide rail instead of raw indentation, so a deep tree
661
+ * stays readable at 240px wide. */
662
+ .wave-docs-sidebar__list:not([data-depth='0']) {
663
+ margin-inline-start: 0.5rem;
664
+ padding-inline-start: 0.5rem;
665
+ border-inline-start: 1px solid var(--wave-docs-border);
666
+ }
667
+
668
+ .wave-docs-sidebar__link,
669
+ .wave-docs-sidebar__group-button {
670
+ display: flex;
671
+ align-items: center;
672
+ justify-content: space-between;
673
+ gap: 0.5rem;
674
+ width: 100%;
675
+ padding: 0.3125rem 0.5rem;
676
+ border: 0;
677
+ border-radius: var(--wave-docs-radius-sm);
678
+ background: none;
679
+ color: var(--wave-docs-fg-muted);
680
+ font: inherit;
681
+ text-align: start;
682
+ text-decoration: none;
683
+ }
684
+
685
+ .wave-docs-sidebar__group-button {
686
+ cursor: pointer;
687
+ font-weight: 600;
688
+ color: var(--wave-docs-fg);
689
+ }
690
+
691
+ .wave-docs-sidebar__link:hover,
692
+ .wave-docs-sidebar__group-button:hover {
693
+ background: var(--wave-docs-bg-subtle);
694
+ color: var(--wave-docs-fg);
695
+ }
696
+
697
+ /* The active page. Driven by `aria-current`, so the visual state and the
698
+ * announced state cannot drift apart. */
699
+ .wave-docs-sidebar__link[aria-current='page'] {
700
+ background: var(--wave-docs-accent-subtle);
701
+ color: var(--wave-docs-accent);
702
+ font-weight: 600;
703
+ }
704
+
705
+ .wave-docs-sidebar__group-header {
706
+ display: flex;
707
+ align-items: center;
708
+ }
709
+
710
+ .wave-docs-sidebar__group-header .wave-docs-sidebar__link {
711
+ flex: 1 1 auto;
712
+ font-weight: 600;
713
+ color: var(--wave-docs-fg);
714
+ }
715
+
716
+ .wave-docs-sidebar__group-toggle {
717
+ flex: none;
718
+ display: grid;
719
+ place-items: center;
720
+ padding: 0.375rem;
721
+ border: 0;
722
+ border-radius: var(--wave-docs-radius-sm);
723
+ background: none;
724
+ color: var(--wave-docs-fg-subtle);
725
+ cursor: pointer;
726
+ }
727
+
728
+ .wave-docs-sidebar__group-toggle:hover {
729
+ background: var(--wave-docs-bg-subtle);
730
+ color: var(--wave-docs-fg);
731
+ }
732
+
733
+ .wave-docs-sidebar__chevron {
734
+ flex: none;
735
+ color: var(--wave-docs-fg-subtle);
736
+ }
737
+
738
+ .wave-docs-sidebar__chevron[data-open] {
739
+ rotate: 90deg;
740
+ }
741
+
742
+ .wave-docs-sidebar__separator-item {
743
+ margin-block: 1rem 0.25rem;
744
+ }
745
+
746
+ .wave-docs-sidebar__separator {
747
+ display: block;
748
+ padding-inline: 0.5rem;
749
+ color: var(--wave-docs-fg-subtle);
750
+ font-size: 0.75rem;
751
+ font-weight: 650;
752
+ letter-spacing: 0.06em;
753
+ text-transform: uppercase;
754
+ }
755
+
756
+ @media (prefers-reduced-motion: no-preference) {
757
+ .wave-docs-sidebar__chevron {
758
+ transition: rotate 150ms ease-out;
759
+ }
760
+ }
761
+ }
762
+
763
+ /* ---------------------------------------------------------------------------
764
+ * Table of contents
765
+ * ------------------------------------------------------------------------ */
766
+
767
+ @layer components {
768
+ .wave-docs-toc {
769
+ font-size: 0.8125rem;
770
+ line-height: 1.5;
771
+ }
772
+
773
+ .wave-docs-toc__list {
774
+ margin: 0;
775
+ padding: 0;
776
+ list-style: none;
777
+ }
778
+
779
+ .wave-docs-toc__link {
780
+ display: block;
781
+ padding: 0.25rem 0.75rem;
782
+ /* The rail the active marker slides along. */
783
+ border-inline-start: 2px solid var(--wave-docs-border);
784
+ color: var(--wave-docs-fg-muted);
785
+ text-decoration: none;
786
+ }
787
+
788
+ /* Indent by heading depth rather than by nesting, so an h4 under an h2 with
789
+ * no h3 between them still lines up. */
790
+ .wave-docs-toc__link[data-depth='3'] {
791
+ padding-inline-start: 1.5rem;
792
+ }
793
+ .wave-docs-toc__link[data-depth='4'] {
794
+ padding-inline-start: 2.25rem;
795
+ }
796
+ .wave-docs-toc__link[data-depth='5'],
797
+ .wave-docs-toc__link[data-depth='6'] {
798
+ padding-inline-start: 3rem;
799
+ }
800
+
801
+ .wave-docs-toc__link:hover {
802
+ color: var(--wave-docs-fg);
803
+ border-inline-start-color: var(--wave-docs-border-strong);
804
+ }
805
+
806
+ .wave-docs-toc__link[aria-current='location'] {
807
+ color: var(--wave-docs-accent);
808
+ border-inline-start-color: var(--wave-docs-accent);
809
+ font-weight: 600;
810
+ }
811
+
812
+ @media (prefers-reduced-motion: no-preference) {
813
+ .wave-docs-toc__link {
814
+ transition: color 120ms ease-out, border-color 120ms ease-out;
815
+ }
816
+ }
817
+ }
818
+
819
+ /* ---------------------------------------------------------------------------
820
+ * Search
821
+ *
822
+ * Classes emitted by `@waveso/docs/react/search-dialog`. The dialog is a
823
+ * `role="dialog"` div in a portal, not a `<dialog>` element, so the backdrop
824
+ * and stacking are ours to draw.
825
+ * ------------------------------------------------------------------------ */
826
+
827
+ @layer components {
828
+ .wave-docs-search-trigger {
829
+ display: flex;
830
+ align-items: center;
831
+ justify-content: space-between;
832
+ gap: 1rem;
833
+ width: 100%;
834
+ max-width: 20rem;
835
+ padding: 0.4375rem 0.625rem;
836
+ border: 1px solid var(--wave-docs-border);
837
+ border-radius: var(--wave-docs-radius);
838
+ background: var(--wave-docs-bg-subtle);
839
+ color: var(--wave-docs-fg-muted);
840
+ font: inherit;
841
+ font-size: 0.875rem;
842
+ text-align: start;
843
+ cursor: pointer;
844
+ }
845
+
846
+ .wave-docs-search-trigger:hover {
847
+ border-color: var(--wave-docs-border-strong);
848
+ color: var(--wave-docs-fg);
849
+ }
850
+
851
+ .wave-docs-search-trigger:focus-visible,
852
+ .wave-docs-search-close:focus-visible,
853
+ .wave-docs-search-result-link:focus-visible {
854
+ outline: none;
855
+ box-shadow: var(--wave-docs-ring);
856
+ }
857
+
858
+ /* The label is the flexible half of the trigger; the shortcut hint beside it
859
+ * must never be the thing that gets squeezed out at narrow widths. */
860
+ .wave-docs-search-trigger-label {
861
+ overflow: hidden;
862
+ text-overflow: ellipsis;
863
+ white-space: nowrap;
864
+ }
865
+
866
+ .wave-docs-search-trigger-kbd {
867
+ flex: none;
868
+ padding: 0.05rem 0.35rem;
869
+ border: 1px solid var(--wave-docs-border-strong);
870
+ border-radius: var(--wave-docs-radius-sm);
871
+ color: var(--wave-docs-fg-subtle);
872
+ font-family: var(--wave-docs-font-mono);
873
+ font-size: 0.75rem;
874
+ }
875
+
876
+ .wave-docs-search-backdrop {
877
+ position: fixed;
878
+ inset: 0;
879
+ z-index: 50;
880
+ display: flex;
881
+ justify-content: center;
882
+ /* Not centred: a dialog that grows downward from a fixed top does not
883
+ * shift under the reader as results stream in. */
884
+ padding: 10vh 1rem 1rem;
885
+ background: oklch(0.2 0.02 265 / 0.5);
886
+ backdrop-filter: blur(2px);
887
+ }
888
+
889
+ .wave-docs-search-dialog {
890
+ display: flex;
891
+ flex-direction: column;
892
+ width: 100%;
893
+ max-width: 34rem;
894
+ max-height: min(32rem, 80vh);
895
+ overflow: hidden;
896
+ border: 1px solid var(--wave-docs-border);
897
+ border-radius: var(--wave-docs-radius);
898
+ background: var(--wave-docs-bg);
899
+ box-shadow: 0 24px 48px -12px oklch(0 0 0 / 0.3);
900
+ }
901
+
902
+ .wave-docs-search-input-row {
903
+ display: flex;
904
+ align-items: center;
905
+ gap: 0.5rem;
906
+ padding: 0.75rem;
907
+ border-block-end: 1px solid var(--wave-docs-border);
908
+ }
909
+
910
+ .wave-docs-search-input {
911
+ flex: 1 1 auto;
912
+ min-width: 0;
913
+ padding: 0.375rem 0.25rem;
914
+ border: 0;
915
+ background: none;
916
+ color: var(--wave-docs-fg);
917
+ font: inherit;
918
+ font-size: 1rem;
919
+ }
920
+
921
+ .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
+ outline: none;
925
+ }
926
+
927
+ .wave-docs-search-input::placeholder {
928
+ color: var(--wave-docs-fg-subtle);
929
+ }
930
+
931
+ .wave-docs-search-close {
932
+ flex: none;
933
+ padding: 0.3125rem 0.625rem;
934
+ border: 1px solid var(--wave-docs-border);
935
+ border-radius: var(--wave-docs-radius-sm);
936
+ background: none;
937
+ color: var(--wave-docs-fg-muted);
938
+ font: inherit;
939
+ font-size: 0.8125rem;
940
+ cursor: pointer;
941
+ }
942
+
943
+ .wave-docs-search-close:hover {
944
+ color: var(--wave-docs-fg);
945
+ border-color: var(--wave-docs-border-strong);
946
+ }
947
+
948
+ .wave-docs-search-results {
949
+ flex: 1 1 auto;
950
+ overflow-y: auto;
951
+ padding: 0.375rem;
952
+ }
953
+
954
+ .wave-docs-search-result {
955
+ border-radius: var(--wave-docs-radius-sm);
956
+ }
957
+
958
+ /* Driven by `aria-selected` through the active class the dialog sets — the
959
+ * combobox keeps focus in the input, so `:focus` never lands here. */
960
+ .wave-docs-search-result-active {
961
+ background: var(--wave-docs-accent-subtle);
962
+ }
963
+
964
+ .wave-docs-search-result-link {
965
+ display: flex;
966
+ flex-direction: column;
967
+ gap: 0.125rem;
968
+ padding: 0.5rem 0.625rem;
969
+ border-radius: var(--wave-docs-radius-sm);
970
+ color: var(--wave-docs-fg);
971
+ text-decoration: none;
972
+ }
973
+
974
+ .wave-docs-search-result-heading {
975
+ font-size: 0.9375rem;
976
+ font-weight: 600;
977
+ line-height: 1.4;
978
+ }
979
+
980
+ .wave-docs-search-result-breadcrumb {
981
+ color: var(--wave-docs-fg-subtle);
982
+ font-size: 0.75rem;
983
+ line-height: 1.4;
984
+ }
985
+
986
+ /* A crumb never wraps mid-title: the trail reads as a path or not at all.
987
+ * The breadcrumb line itself is free to wrap between crumbs. */
988
+ .wave-docs-search-result-crumb {
989
+ white-space: nowrap;
990
+ }
991
+
992
+ .wave-docs-search-result-crumb-separator {
993
+ margin-inline: 0.25rem;
994
+ }
995
+
996
+ .wave-docs-search-status {
997
+ margin: 0;
998
+ padding: 1.5rem 1rem;
999
+ color: var(--wave-docs-fg-muted);
1000
+ font-size: 0.875rem;
1001
+ text-align: center;
1002
+ }
1003
+
1004
+ /* The four status modifiers share the base rule; only two need to look
1005
+ * different from it. `-hint` and `-loading` are deliberately quiet — they
1006
+ * appear on every open, and an empty dialog should not shout. */
1007
+ .wave-docs-search-status-hint,
1008
+ .wave-docs-search-status-loading {
1009
+ color: var(--wave-docs-fg-subtle);
1010
+ }
1011
+
1012
+ .wave-docs-search-status-empty {
1013
+ color: var(--wave-docs-fg);
1014
+ }
1015
+
1016
+ .wave-docs-search-status-error {
1017
+ color: var(--wave-docs-callout-caution);
1018
+ }
1019
+
1020
+ /* The live region must stay in the accessibility tree to be announced, so it
1021
+ * is hidden the same way as any other visually-hidden text — never
1022
+ * `display: none`. */
1023
+ .wave-docs-search-announcer {
1024
+ position: absolute;
1025
+ width: 1px;
1026
+ height: 1px;
1027
+ padding: 0;
1028
+ margin: -1px;
1029
+ overflow: hidden;
1030
+ clip-path: inset(50%);
1031
+ white-space: nowrap;
1032
+ }
1033
+ }