@waveso/docs 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -1
- package/README.md +114 -37
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.js +7 -1
- package/dist/next.d.ts +34 -27
- package/dist/next.js +20 -11
- package/dist/react/hero.d.ts +19 -0
- package/dist/react/hero.js +44 -0
- package/dist/react/layout.d.ts +1 -3
- package/dist/react/layout.js +17 -54
- package/dist/react/nav.d.ts +11 -11
- package/dist/react/nav.js +106 -58
- package/dist/react/next-nav.d.ts +5 -1
- package/dist/react/next-nav.js +4 -2
- package/dist/react/shell-labels.d.ts +8 -2
- package/dist/react/toc.d.ts +13 -3
- package/dist/react/toc.js +17 -10
- package/dist/render.js +2 -1
- package/dist/source.js +1 -1
- package/dist/styles.css +1079 -237
- package/dist/types.d.ts +31 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,159 @@
|
|
|
1
1
|
# @waveso/docs
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- da25315: **A page becomes a landing page by declaring its actions.** `actions` in the
|
|
8
|
+
frontmatter turns `title` and `description` into a page header with those links
|
|
9
|
+
beneath them:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
---
|
|
13
|
+
title: Wave Docs
|
|
14
|
+
description: Markdown documentation for Next.js.
|
|
15
|
+
actions:
|
|
16
|
+
- label: Quick start
|
|
17
|
+
href: /getting-started
|
|
18
|
+
- label: GitHub
|
|
19
|
+
href: https://github.com/waveso/docs
|
|
20
|
+
variant: secondary
|
|
21
|
+
---
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Leave it off and the page is exactly what it was: `description` stays a `<meta>`
|
|
25
|
+
tag and the title is the first thing in the prose. That is the whole of the
|
|
26
|
+
adaptation for the two shapes this package serves — documentation that is the
|
|
27
|
+
entire site puts a hero on its index, and documentation mounted at `/docs`
|
|
28
|
+
inside an application that already has a marketing page leaves `actions` off.
|
|
29
|
+
There is no mode, no `standalone` flag and nothing to configure.
|
|
30
|
+
|
|
31
|
+
Actions are primary-then-secondary by position, so the common case needs no
|
|
32
|
+
`variant`. Anything that leaves the site gets a plain `<a>` rather than the
|
|
33
|
+
router link, plus `target`, `rel` and a screen-reader suffix; `mailto:` and
|
|
34
|
+
`tel:` get none of that, because they open no tab. Unsafe hrefs fail the build
|
|
35
|
+
rather than reaching an `<a>` — frontmatter was the one door into an `href` that
|
|
36
|
+
bypassed `isSafeHref`.
|
|
37
|
+
|
|
38
|
+
`DocsHero` is exported at `@waveso/docs/react/hero`, and `DocAction` from
|
|
39
|
+
`@waveso/docs/types`.
|
|
40
|
+
|
|
41
|
+
⚠️ A HERO PAGE MUST NOT ALSO WRITE ITS OWN `# Title`. `render` normally prepends
|
|
42
|
+
an `<h1>` from `frontmatter.title`; on a hero page the hero renders that heading
|
|
43
|
+
instead, because the tagline and the actions have to sit beneath it. Writing one
|
|
44
|
+
in the body as well ships two `h1`s — the same duplication `titleHeading` has
|
|
45
|
+
always warned about.
|
|
46
|
+
|
|
47
|
+
The background is a rotated line grid drawn with `repeating-linear-gradient`
|
|
48
|
+
rather than an inlined SVG: no data URI in the stylesheet, and the lines are
|
|
49
|
+
`--wave-docs-hero-grid` and `--wave-docs-hero-grid-strong` — Wave 200 and Wave
|
|
50
|
+
300 from `@waveso/ui` in the light theme, Wave 900 and Wave 800 in the dark one
|
|
51
|
+
— so they follow the theme. They are tokens of their own rather than the border
|
|
52
|
+
colours, because a decorative tint must not be tied to a functional contrast
|
|
53
|
+
ratio.
|
|
54
|
+
|
|
55
|
+
- 915729f: **`docs.Layout` renders no header, and the navigation is one sidebar at every
|
|
56
|
+
width.** A full-width sticky bar is the one element that competes with a host
|
|
57
|
+
application's own for the viewport's top edge, and two of the sites using this
|
|
58
|
+
have one — measured against a fixed 64px host bar, ours landed on top of theirs
|
|
59
|
+
at every width. The chrome it held lives in the sidebar now:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
.wave-docs-shell the query container
|
|
63
|
+
└─ .wave-docs-layout the grid
|
|
64
|
+
├─ .wave-docs-layout__sidebar paints nothing, and moves
|
|
65
|
+
│ ├─ …__sidebar-nav the surface, and the one border
|
|
66
|
+
│ └─ …__sidebar-trigger a 44px strip — paints nothing at rest
|
|
67
|
+
├─ .wave-docs-layout__sidebar-scrim
|
|
68
|
+
├─ .wave-docs-layout__main
|
|
69
|
+
└─ .wave-docs-layout__toc
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Pressing the trigger translates the sidebar by exactly the navigation's width,
|
|
73
|
+
so it leaves the page entirely and the trigger's outer edge lands on the inline
|
|
74
|
+
start edge. There is no drawer, no `<dialog>`, no second control and no second
|
|
75
|
+
copy of the tree.
|
|
76
|
+
|
|
77
|
+
**Nothing this package renders is anchored to the viewport.** The sidebar is a
|
|
78
|
+
grid item, the trigger is a flex child of it, and the scrim is `absolute` inside
|
|
79
|
+
`.wave-docs-layout` — so every one of them resolves against a box this package
|
|
80
|
+
owns and _your_ layout placed. `position: fixed` is the thing to avoid, and the
|
|
81
|
+
reason is specific: a fixed element is anchored to the viewport you share with
|
|
82
|
+
it, your navbar is in the same viewport, and neither can detect the other. The
|
|
83
|
+
search dialog is the one exception and always was.
|
|
84
|
+
|
|
85
|
+
**There is not one width-based `@media` query left.** Every breakpoint is
|
|
86
|
+
`@container`. `@media` asks how wide the _screen_ is, which is the wrong
|
|
87
|
+
question for a package mounted at `/docs` inside an application that owns the
|
|
88
|
+
rest of the page: put this in a 700px panel on a 1920px monitor and `@media`
|
|
89
|
+
says "wide", the sidebar takes its 16rem column, and the reading column comes
|
|
90
|
+
out around 60px. Two shapes fall out of the container width — beside the
|
|
91
|
+
article, or over it behind a scrim with `inert`, Escape, click-to-dismiss and
|
|
92
|
+
focus moved in and restored.
|
|
93
|
+
|
|
94
|
+
Migration, in full:
|
|
95
|
+
|
|
96
|
+
| Was | Becomes |
|
|
97
|
+
| ------------------------------------------------------------- | ------------------------------------------------------ |
|
|
98
|
+
| `<docs.Layout title={<Brand/>}>` | Delete it. The index page's title brands the docs |
|
|
99
|
+
| `<docs.Layout actions={<ThemeToggle/>}>` | Render it in your own layout, around `docs.Layout` |
|
|
100
|
+
| `--wave-docs-header-height` set so sticky columns clear a bar | `--wave-docs-chrome-offset`, same value |
|
|
101
|
+
| `--wave-docs-header-height` set to size the bar | Nothing sizes a bar. There is no bar |
|
|
102
|
+
| `--wave-docs-shell-width` set to cap the shell | Nothing. `--wave-docs-measure` caps the reading column |
|
|
103
|
+
| `actions` as the client-search escape hatch | `search={false}` plus your own `DocsSearch` |
|
|
104
|
+
| A `rootMargin` you relied on defaulting to `-80px` | Pass it explicitly |
|
|
105
|
+
| `.wave-docs-layout__header` / `__title` / `__actions` | Not rendered |
|
|
106
|
+
| `.wave-docs-layout__sidebar > .wave-docs-sidebar` | The tree is a grandchild now |
|
|
107
|
+
|
|
108
|
+
⚠️ `--wave-docs-header-height` DID TWO JOBS AND ONLY ONE SURVIVES. It sized the
|
|
109
|
+
header, and it was the offset both sticky columns parked below. Nothing of this
|
|
110
|
+
package's sits above the content any more, so the sizing job is gone; the offset
|
|
111
|
+
job is `--wave-docs-chrome-offset`, and a host with their own fixed bar still
|
|
112
|
+
needs to set it or the sidebar and the table of contents park underneath it.
|
|
113
|
+
|
|
114
|
+
⚠️ `--wave-docs-chrome-offset` DEFAULTS TO `0rem`, WITH A UNIT, AND THE UNIT IS
|
|
115
|
+
LOAD-BEARING. It is read inside `calc(100dvh - …)`, where a unitless `0` is
|
|
116
|
+
invalid at computed-value time — the height on the sidebar and the `max-height`
|
|
117
|
+
on the table of contents would die rather than resolve to no change.
|
|
118
|
+
|
|
119
|
+
⚠️ `--wave-docs-shell-width` IS GONE RATHER THAN RENAMED. It capped the whole
|
|
120
|
+
shell and centred it, which pushed the sidebar's inline start 480px in from the
|
|
121
|
+
screen on a 2560px display — and left a _closed_ navigation parked in the
|
|
122
|
+
centring margin instead of off the page. The sidebar owns the page's inline
|
|
123
|
+
start edge at every width now, which is what makes "closed" mean off the screen
|
|
124
|
+
by construction, and the reading column is what is capped.
|
|
125
|
+
|
|
126
|
+
`DocsToc`'s `rootMargin` default changes from `'-80px 0px -60% 0px'` to
|
|
127
|
+
`'0px 0px -60% 0px'`. The 80px reserved room for a sticky header that is no
|
|
128
|
+
longer rendered; a host whose own chrome overlays the content passes the prop.
|
|
129
|
+
|
|
130
|
+
`DocsLayoutProps` is three props — `children`, `search` and `labels`.
|
|
131
|
+
|
|
132
|
+
Two behaviours are lost with the drawer and are worth knowing: the navigation no
|
|
133
|
+
longer opens before hydration (it was a server-rendered
|
|
134
|
+
`<button command="show-modal">`), and the client bundle grows about 500 bytes
|
|
135
|
+
for the containment work `<dialog>` used to give for free.
|
|
136
|
+
|
|
137
|
+
### Patch Changes
|
|
138
|
+
|
|
139
|
+
- 1313e77: The shell no longer depends on the host shipping a CSS reset.
|
|
140
|
+
|
|
141
|
+
⚠️ `box-sizing` WAS THE HOST'S TO SET, AND ALMOST EVERY HOST SETS IT.
|
|
142
|
+
`.wave-docs-sidebar__link` is `width: 100%` with `0.5rem` of inline padding, so
|
|
143
|
+
under `content-box` it is a 272px box in a 256px track — and the external-link
|
|
144
|
+
icon that `justify-content: space-between` pins to the far end renders 8px
|
|
145
|
+
outside the sidebar, clipped in half. Tailwind's preflight and every
|
|
146
|
+
normalize-style reset declare `border-box` globally, so this was invisible in
|
|
147
|
+
every project that has one, and visible on the only site here that ships no CSS
|
|
148
|
+
at all. `box-sizing: border-box` now applies to elements carrying a
|
|
149
|
+
`wave-docs-` class, scoped to this package's own namespace rather than to `*`,
|
|
150
|
+
because the prose renders a consumer's components too.
|
|
151
|
+
|
|
152
|
+
Three README claims were wrong and are corrected: `react/*` is ten subpaths and
|
|
153
|
+
not nine, three modules import from `next/*` and not two, and the twenty-two
|
|
154
|
+
chrome labels break down into six groups rather than the five that summed to
|
|
155
|
+
nineteen.
|
|
156
|
+
|
|
3
157
|
## 0.6.0
|
|
4
158
|
|
|
5
159
|
### Minor Changes
|
|
@@ -404,7 +558,7 @@
|
|
|
404
558
|
|
|
405
559
|
Every subpath in `exports` is listed in the README, and so is every runtime name each one exports — `manifest.test.ts` enumerates both against the built output and fails the build on a name this README does not mention. That test is new in this release, and writing it immediately found five: `DOCS_ERROR_PREFIX`, `DEFAULT_DOCS_THEMES`, `CALLOUT_TYPES`, `defaultMarkdownComponents` and `DOCS_CONTENT_ID` were all public and documented nowhere.
|
|
406
560
|
|
|
407
|
-
**The default page is worth looking at.** A 46rem measure and a system font stack, a 1.2 minor-third type scale, tables that scroll instead of shredding the layout, one focus `outline` in place of five `box-shadow` rings (which also deletes the forced-colors block that existed to patch them), and a responsive shell with breakpoints at 64/80/100rem. The element tree, the five layout tokens and the breakpoints are frozen
|
|
561
|
+
**The default page is worth looking at.** A 46rem measure and a system font stack, a 1.2 minor-third type scale, tables that scroll instead of shredding the layout, one focus `outline` in place of five `box-shadow` rings (which also deletes the forced-colors block that existed to patch them), and a responsive shell with breakpoints at 64/80/100rem. The element tree, the five layout tokens and the breakpoints are frozen.
|
|
408
562
|
|
|
409
563
|
**Three new harnesses, because the old ones could not see these failures.** A browser tier running real Chromium — jsdom reports every width as `0`, so the measure, the type scale, reflow and the table floor were unassertable. A smoke build of a real Next application against the published `exports` map, in both output modes. And `pnpm check:readme`, which type-checks every example in this README as one project: it immediately found `app/docs/layout.tsx` defined twice with different bodies, and an `imageResolver` example calling `imageSize(path)` when `image-size` v2 takes a `Uint8Array`.
|
|
410
564
|
|
package/README.md
CHANGED
|
@@ -69,16 +69,16 @@ Every figure below is a **ceiling**, and `pnpm size` fails the build if the meas
|
|
|
69
69
|
|
|
70
70
|
| | At most |
|
|
71
71
|
| --- | --- |
|
|
72
|
-
| Everything the quick start ships, gzipped | 13.
|
|
72
|
+
| Everything the quick start ships, gzipped | 13.9 KB |
|
|
73
73
|
| Search dialog and router wiring | 9.3 KB |
|
|
74
|
-
| Navigation: sidebar and
|
|
74
|
+
| Navigation: one sidebar, open and closed | 2.9 KB |
|
|
75
75
|
| Table of contents | 0.9 KB |
|
|
76
76
|
| Copy-button runtime | 1.1 KB |
|
|
77
77
|
| hast over the wire vs HTML, prose page | 1.20× |
|
|
78
78
|
| hast over the wire vs HTML, code and tables | 1.12× |
|
|
79
79
|
| Highlighting vs no highlighting | 2.00× |
|
|
80
80
|
|
|
81
|
-
The first row is the honest total: a reader who lands on a page of your documentation downloads under 13.
|
|
81
|
+
The first row is the honest total: a reader who lands on a page of your documentation downloads under 13.9 KB gzipped of JavaScript from this package, and that is the whole of it. No markdown parser and no syntax highlighter reach the browser at all — those run in Node at build time. Drop the search dialog and it is under 4 KB.
|
|
82
82
|
|
|
83
83
|
The one real cost is the middle pair: shipping a tree instead of a string is about 20% more brotli on a prose page, and about 12% on a page with code and tables, where Shiki's token spans dominate both representations equally. That is the price of never handing markup to `dangerouslySetInnerHTML`, and it is the first number a skeptical reviewer should ask for.
|
|
84
84
|
|
|
@@ -140,7 +140,7 @@ content/docs/
|
|
|
140
140
|
authentication.md
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
That is a working documentation site: routing, a navigation sidebar, a table of contents, syntax highlighting, search
|
|
143
|
+
That is a working documentation site: routing, a navigation sidebar that opens and closes, a table of contents, syntax highlighting, search and a skip link.
|
|
144
144
|
|
|
145
145
|
The search route is in the quick start rather than in a section further down because `docs.Layout` renders the search trigger by default — leave the route out and a reader gets a control that opens onto "Search is unavailable". If you genuinely do not want search, `export default function Layout(props) { return docs.Layout({ ...props, search: false }) }` drops both the trigger and this file. See [Search](#search) for tuning.
|
|
146
146
|
|
|
@@ -158,7 +158,7 @@ There is no root export. Every entry point is a subpath, so an import always nam
|
|
|
158
158
|
| `@waveso/docs/render` | Node | `createDocsRenderer`, `resolveMarkdownLink` |
|
|
159
159
|
| `@waveso/docs/highlighter` | Node | `createDocsHighlighter`, `DEFAULT_DOCS_LANGS`, `DEFAULT_DOCS_THEMES` |
|
|
160
160
|
| `@waveso/docs/search-index` | Node | `extractSearchRecords`, `buildSearchIndex` |
|
|
161
|
-
| `@waveso/docs/react/<name>` | Browser + RSC |
|
|
161
|
+
| `@waveso/docs/react/<name>` | Browser + RSC | Ten subpaths, one component each — see [Components](#components) |
|
|
162
162
|
| `@waveso/docs/frontmatter` | Any | `docFrontmatterSchema`, `parseFrontmatter`, `z` |
|
|
163
163
|
| `@waveso/docs/types` | Any | Every shared type. Type-only |
|
|
164
164
|
| `@waveso/docs/errors` | Any | `DocsErrorCode`, `DocsError`, `isDocsError`, `DOCS_ERROR_PREFIX` |
|
|
@@ -172,15 +172,21 @@ That is about **weight, not about `node:fs`** — and the distinction matters, b
|
|
|
172
172
|
|
|
173
173
|
### Layout tokens
|
|
174
174
|
|
|
175
|
-
Five custom properties size the shell, all layered so an unlayered `:root` of your own still wins.
|
|
175
|
+
Five custom properties size the shell, all layered so an unlayered `:root` of your own still wins. All six are public API: a name changes only in a release that carries the migration.
|
|
176
176
|
|
|
177
177
|
| Token | Default | Controls |
|
|
178
178
|
| --- | --- | --- |
|
|
179
179
|
| `--wave-docs-measure` | `46rem` | Prose column width. `none` opts out |
|
|
180
|
-
| `--wave-docs-
|
|
181
|
-
| `--wave-docs-
|
|
180
|
+
| `--wave-docs-sidebar-width` | `16rem` | The navigation's width |
|
|
181
|
+
| `--wave-docs-trigger-width` | `1.25rem` | The trigger's button. The strip around it is this plus 4px a side |
|
|
182
182
|
| `--wave-docs-toc-width` | `15rem` | Table-of-contents track |
|
|
183
|
-
| `--wave-docs-
|
|
183
|
+
| `--wave-docs-chrome-offset` | `0rem` | Where our sticky chrome starts, below a bar of yours |
|
|
184
|
+
|
|
185
|
+
**`--wave-docs-shell-width` was removed in 0.7.0, and it is the sidebar's edge that replaced it.** It capped the whole shell and centred it, which put the sidebar's inline start 480px in from the screen on a 2560px display — and, worse, left a *closed* navigation parked in the centring margin instead of off the page. The reading column is the thing that should not stretch, so `--wave-docs-measure` caps it and it centres itself in its track; the sidebar keeps the page's inline start edge at every width, which is what makes "closed" mean off the screen by construction.
|
|
186
|
+
|
|
187
|
+
**`--wave-docs-header-height` is gone in 0.7.0, and renaming it to one thing would have lost the other.** It sized the header *and* it was the offset both sticky columns parked below. The header is gone, and so is every shape that sat above the content, so the sizing half has nothing left to size. The offset half is the one that matters and it is `--wave-docs-chrome-offset`: `--wave-docs-chrome-offset: 4rem` starts our sidebar and our table of contents 4rem down, and feeds the scroll padding that keeps an anchored heading clear of your bar.
|
|
188
|
+
|
|
189
|
+
The default is `0rem` rather than `0`, and the unit is load-bearing: it is read inside `calc(100dvh - …)` on both sticky columns, and `calc(100dvh - 0)` is invalid at computed-value time — a unitless zero kills the `max-height` instead of resolving to no change.
|
|
184
190
|
|
|
185
191
|
The shell has three breakpoints, in `rem` so they scale with the reader's base font size: the sidebar appears at **64rem**, the table of contents at **80rem**, and the whole grid stops growing at **100rem**. 64rem is arithmetic rather than taste — a 16rem sidebar plus a 46rem measure plus two 1.5rem gutters is 65rem, so anything narrower introduces the sidebar exactly where it starts eating the measure it frames.
|
|
186
192
|
|
|
@@ -190,11 +196,12 @@ Every subpath is enumerated in `exports` — there is no wildcard. A name that i
|
|
|
190
196
|
|
|
191
197
|
## Components
|
|
192
198
|
|
|
193
|
-
Every component takes data as props, and
|
|
199
|
+
Every component takes data as props, and every module that imports from `next/*` is named for it — `next-nav` for `usePathname`, `next-search` for `useRouter`, `next-link` for `next/link` itself — so the exception is visible in the file list rather than three imports deep. Everything else has `next/link` and `next/image` injected. That keeps the renderer host-agnostic and testable without a router. `DocsSearch` is the one exception, and it exists precisely so that the exception is ours rather than yours: it is the fifteen-line wrapper you would otherwise write around `SearchDialog`.
|
|
194
200
|
|
|
195
201
|
| Component | Subpath | Notes |
|
|
196
202
|
| --- | --- | --- |
|
|
197
203
|
| `DocContent` | `react/doc-content` | Renders a hast tree, inside `.wave-docs-prose`. Server Component |
|
|
204
|
+
| `DocsHero` | `react/hero` | A landing page's header. `title`, `description`, `actions`, `Link`, `externalLabel`. Server Component |
|
|
198
205
|
| `DocsSidebar` | `react/sidebar` | Takes `pathname` as a prop, not from `next/navigation` |
|
|
199
206
|
| `DocsToc` | `react/toc` | Scrollspy via `IntersectionObserver`. `label`, `topLabel`, `rootMargin`, `className` |
|
|
200
207
|
| `DocsSearch` | `react/next-search` | `SearchDialog`, wired to Next's router. What you want |
|
|
@@ -211,47 +218,101 @@ The two components the adapter injects take a little more than an `<a>` and an `
|
|
|
211
218
|
|
|
212
219
|
### Layout
|
|
213
220
|
|
|
214
|
-
`export default docs.Layout` — the one line from the [quick start](#quick-start) — is a Server Component that renders the whole shell: skip link,
|
|
221
|
+
`export default docs.Layout` — the one line from the [quick start](#quick-start) — is a Server Component that renders the whole shell: skip link, sidebar, search trigger, and the grid that arranges them. It reads the navigation tree and the search index URL itself, so there is nothing to fetch and nothing to pass.
|
|
222
|
+
|
|
223
|
+
**It renders no header, and that is deliberate.** Two kinds of site use this package: documentation mounted inside an application that already has a header, a navbar and its own search, and documentation that is the whole site. A full-width sticky bar of ours serves the second and fights the first — two stacked bars competing for the viewport's top edge, and two search boxes on one page, one of which knows nothing about the documentation.
|
|
224
|
+
|
|
225
|
+
So the sidebar is the chrome. It is a real grid item at every width, and it is the same shell on a phone and on a desktop.
|
|
226
|
+
|
|
227
|
+
**Nothing this package renders is anchored to the viewport.** The sidebar is a grid item, the trigger is a flex child of it, and the scrim is `position: absolute` inside `.wave-docs-layout` — so every one of them resolves against a box this package owns and *your* layout placed. `position: fixed` is the thing to avoid, and the reason is specific: a fixed element is anchored to the viewport you share with it, your navbar is in the same viewport, and neither can detect the other. The search dialog is the one exception and always was — top-layer, present only while open, and nothing collides with something that is not there.
|
|
215
228
|
|
|
216
229
|
Your layout stays a Server Component. The two pieces that need a client — the navigation's `usePathname`, the search dialog — carry their own `'use client'` boundaries inside the package.
|
|
217
230
|
|
|
218
|
-
|
|
231
|
+
Your own chrome goes *around* `docs.Layout`, in the layout file you already write — the same place `<html>` and `<body>` live. Call it instead of re-exporting it when you want to wrap it, configure it, or both:
|
|
219
232
|
|
|
220
233
|
```tsx
|
|
221
234
|
import type { ReactNode } from 'react';
|
|
222
235
|
import '@waveso/docs/styles.css';
|
|
223
236
|
import { docs } from '@/lib/docs';
|
|
224
237
|
|
|
238
|
+
/** Yours: the header, theme toggle and repository link the rest of the site has. */
|
|
239
|
+
declare function SiteHeader(): ReactNode;
|
|
240
|
+
|
|
225
241
|
export default function DocsLayout({ children }: { children: ReactNode }) {
|
|
226
242
|
return (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
<>
|
|
244
|
+
<SiteHeader />
|
|
245
|
+
<docs.Layout search={{ placeholder: 'Search the docs' }}>
|
|
246
|
+
{children}
|
|
247
|
+
</docs.Layout>
|
|
248
|
+
</>
|
|
233
249
|
);
|
|
234
250
|
}
|
|
235
251
|
```
|
|
236
252
|
|
|
253
|
+
If that header of yours is sticky, say how tall it is once and the shell's sticky columns start below it:
|
|
254
|
+
|
|
255
|
+
```css
|
|
256
|
+
:root {
|
|
257
|
+
--wave-docs-chrome-offset: 4rem;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
237
261
|
| Prop | Type | Default | |
|
|
238
262
|
| --- | --- | --- | --- |
|
|
239
|
-
| `
|
|
240
|
-
| `actions` | `ReactNode` | — | Header end, after search |
|
|
263
|
+
| `children` | `ReactNode` | — | What `docs.Page` returns — the `<main>` and the TOC, as two siblings |
|
|
241
264
|
| `search` | `boolean \| DocsSearchProps` | `true` | The search trigger. An object configures the dialog |
|
|
242
265
|
| `labels` | `DocsLabels` | the route's | Overrides `createDocsRoute`'s labels, key by key |
|
|
243
266
|
|
|
244
|
-
|
|
267
|
+
Three props, and one of them is `children`. That is deliberate, and it is the difference between this and an eleven-slot layout: everything else a docs shell gets asked for is already reachable. An announcement banner goes *above* `<docs.Layout>` in your own layout, because this does not own `<body>`. A content footer goes inside `children`. Sidebar links, social icons and separators are `DocNavNode`s you author in `meta.json`. A theme toggle and a repository link go in the layout you write around this one.
|
|
268
|
+
|
|
269
|
+
**`title` and `actions` were removed in 0.7.0, with the header they lived in.** `title` was a brand slot, and a brand belongs to the index page's own title — content, authored and translatable, part of what the reader came for. The argument for `actions` was that the header bar was the one region nothing else could reach; there is no header bar, and the host wraps `docs.Layout` exactly as it already wraps `<html>` and `<body>`, so there is no region only this package can reach. The one place a host cannot reach through this prop list is *inside* the sidebar, and the answer to that is [composing the primitives yourself](#composing-it-yourself).
|
|
245
270
|
|
|
246
271
|
`search` takes anything `DocsSearch` takes except `indexUrl`, which stays derived from your `basePath`. You do not need to repeat `miniSearchOptions` here to match `createDocsRoute` — the route's own value is forwarded, so the object that built the index is the object that queries it.
|
|
247
272
|
|
|
248
273
|
`labels` belongs on `createDocsRoute` — see [Translating the chrome](#translating-the-chrome) — and this prop overrides it key by key, for a site with two shells or a section in another language.
|
|
249
274
|
|
|
250
|
-
####
|
|
275
|
+
#### One sidebar, open and closed
|
|
276
|
+
|
|
277
|
+
There is no mobile version. The sidebar is a shell holding two things in a row:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
.wave-docs-shell the query container
|
|
281
|
+
└─ .wave-docs-layout the grid
|
|
282
|
+
├─ .wave-docs-layout__sidebar paints nothing, and moves
|
|
283
|
+
│ ├─ …__sidebar-nav the surface, and the one border
|
|
284
|
+
│ └─ …__sidebar-trigger the strip — paints nothing at rest
|
|
285
|
+
├─ .wave-docs-layout__sidebar-scrim
|
|
286
|
+
├─ .wave-docs-layout__main
|
|
287
|
+
└─ .wave-docs-layout__toc
|
|
288
|
+
```
|
|
251
289
|
|
|
252
|
-
|
|
290
|
+
Pressing the trigger translates the shell by `calc(var(--wave-docs-trigger-width) - 100%)` — "minus all of me, plus the trigger back" — so the navigation goes entirely off the page and the trigger's outer edge lands exactly on the inline start edge. **The navigation's width appears nowhere in that expression**, so the two cannot drift apart. The trigger rides on the navigation's outer edge because it is the next flex item, not because a number says so.
|
|
253
291
|
|
|
254
|
-
|
|
292
|
+
One navigation in the DOM at every width: one landmark, one copy of the links in the payload, nothing to keep in step.
|
|
293
|
+
|
|
294
|
+
#### It adapts to its container, not to your screen
|
|
295
|
+
|
|
296
|
+
**There is not one width-based `@media` query in this package.** Every breakpoint is `@container`, and that is not a stylistic preference — it is the difference between a docs theme and a component you can mount inside something else.
|
|
297
|
+
|
|
298
|
+
`@media` asks how wide the *screen* is. If you put this in a 700px panel on a 1920px monitor, `@media` says "wide", the sidebar takes its 16rem column, and the reading column comes out around 60px. `@container` asks how wide the box you *gave* it is, which is the question with an answer.
|
|
299
|
+
|
|
300
|
+
Two shapes fall out of that:
|
|
301
|
+
|
|
302
|
+
- **Push**, in a container 64rem or wider: the navigation sits beside the article, and opening or closing it changes the article's width.
|
|
303
|
+
- **Cover**, below that: the navigation sits on top of the article behind a scrim, and the article's measure never changes when you toggle.
|
|
304
|
+
|
|
305
|
+
Same markup, same classes, same control, same translate. The only thing that differs is whether the article gets out of the way.
|
|
306
|
+
|
|
307
|
+
Cover mode is a real overlay, so it ships what an overlay owes a reader: `inert` on everything the navigation covers, Escape to close, click-the-scrim to dismiss, and focus moving into the navigation and back out again. Those five were the browser's while this was a `<dialog>`; they are hand-written now, and skipping them is how an overlay becomes a keyboard trap in the wrong direction.
|
|
308
|
+
|
|
309
|
+
`--wave-docs-sidebar-mode` is how the component knows which shape it is in: the stylesheet declares it, the component reads it back with `getComputedStyle`. `matchMedia` cannot answer a container query, and duplicating the breakpoint in JavaScript is how the two drift.
|
|
310
|
+
|
|
311
|
+
#### Three states, and no flash
|
|
312
|
+
|
|
313
|
+
The server renders no `data-state` at all. That absence means "nobody has chosen yet", and CSS resolves it per mode — closed where the navigation would cover the article, open where it would sit beside it. So the first paint is already right at both shapes, with no JavaScript and nothing to correct. Once a reader presses the trigger their choice is explicit and wins at every width.
|
|
314
|
+
|
|
315
|
+
**This replaced a `<dialog>` drawer in 0.7.0.** Below 64rem the navigation used to be a modal opened by a second control, with `display: contents` above it so the same DOM could serve as the desktop column. It bought focus trapping, Escape and a scroll lock from the browser, and it worked before hydration. It cost two controls for one piece of navigation, a drawer that painted over the tree it contained, and a scroll-into-view that could never run on a phone — a closed `<dialog>` has no layout, so the current page was always below the fold. A closed sidebar is *moved*, not hidden, so that last one is structurally impossible now.
|
|
255
316
|
|
|
256
317
|
#### Composing it yourself
|
|
257
318
|
|
|
@@ -286,7 +347,7 @@ export default async function Page({ params }: { params: Promise<{ slug?: string
|
|
|
286
347
|
|
|
287
348
|
`docs.Page` returns exactly this shape: the `<main>` and the table of contents as **two siblings**, not one wrapped element. They land as direct children of the grid, which is what puts them in separate columns — so if you compose your own page inside `docs.Layout`, return a fragment rather than a wrapper.
|
|
288
349
|
|
|
289
|
-
**The two class names are load-bearing**, and they are the part of this that is easy to leave off. `wave-docs-layout__main` carries `min-width: 0`, without which a wide table pushes the whole document into horizontal scroll (measured: 1048px of document inside a 1024px viewport). `wave-docs-layout__toc` is what the grid reserves its third track with, via `:has()` — unclassed, the table of contents auto-places into the next row underneath the sidebar above 80rem, and renders inline on a phone instead of being hidden. Both are
|
|
350
|
+
**The two class names are load-bearing**, and they are the part of this that is easy to leave off. `wave-docs-layout__main` carries `min-width: 0`, without which a wide table pushes the whole document into horizontal scroll (measured: 1048px of document inside a 1024px viewport). `wave-docs-layout__toc` is what the grid reserves its third track with, via `:has()` — unclassed, the table of contents auto-places into the next row underneath the sidebar above 80rem, and renders inline on a phone instead of being hidden. Both names are public API and change only in a release that carries the migration, so they are safe to write by hand.
|
|
290
351
|
|
|
291
352
|
The `null` is load-bearing too: `:has()` matches an empty `<aside>` exactly as well as a full one, so a page with no headings would give up 15rem to nothing.
|
|
292
353
|
|
|
@@ -300,6 +361,12 @@ label: Auth # sidebar label, when the title is too long
|
|
|
300
361
|
draft: true # excluded from nav, search and static params
|
|
301
362
|
order: 10 # sort weight where there is no meta.json
|
|
302
363
|
aliases: [old-auth, legacy/auth] # former URLs → permanent redirects
|
|
364
|
+
actions: # calls to action — and the opt-in for a hero
|
|
365
|
+
- label: Quick start
|
|
366
|
+
href: /getting-started
|
|
367
|
+
- label: GitHub
|
|
368
|
+
href: https://github.com/waveso/docs
|
|
369
|
+
variant: secondary
|
|
303
370
|
---
|
|
304
371
|
```
|
|
305
372
|
|
|
@@ -307,6 +374,18 @@ aliases: [old-auth, legacy/auth] # former URLs → permanent redirects
|
|
|
307
374
|
|
|
308
375
|
`draft` is deliberately **not** tied to `NODE_ENV`. Preview deployments are production builds, so branching on it would hide drafts in exactly the place reviewers look — drive `includeDrafts` from your own environment check instead.
|
|
309
376
|
|
|
377
|
+
### The hero
|
|
378
|
+
|
|
379
|
+
`actions` is the only thing that turns a page into a landing page. Declare it and `title` and `description` become a page header — a large heading, the description as a tagline under it, and these links beneath that. Leave it off and the page is exactly what it was: `description` stays a `<meta>` tag and the title is the first thing in the prose.
|
|
380
|
+
|
|
381
|
+
Each action takes a `label`, an `href` and an optional `variant` of `primary` or `secondary`. Omit the variant and the first action is the primary and the rest are secondary, which is the shape every landing page has. An `href` that leaves the site gets `target="_blank"`, `rel="noreferrer"` and a screen-reader suffix; `mailto:` and `tel:` do not, because they open no tab. Unsafe hrefs fail the build rather than reaching an `<a>`.
|
|
382
|
+
|
|
383
|
+
**That is the whole of the adaptation for the two shapes this package serves.** Documentation that is the entire site puts a hero on its index. Documentation mounted at `/docs` inside an application that already has a marketing page leaves `actions` off and gets an ordinary page. There is no mode, no `standalone` flag and nothing to configure — the opt-in lives in the file that wants it.
|
|
384
|
+
|
|
385
|
+
⚠️ **A hero page must not also write its own `# Title`.** `render` normally prepends an `<h1>` from `frontmatter.title`; on a hero page the hero renders that heading instead, because the tagline and the actions have to sit beneath it. Writing one in the body as well ships two `h1`s — the same duplication `titleHeading` has always warned about.
|
|
386
|
+
|
|
387
|
+
The background is a rotated line grid under scrims painted in the page's own colour rather than behind a mask: a soft ellipse over the words, a bottom fade, and a vignette that closes at the corners. The vignette is an inset `box-shadow` rather than a gradient — a `radial-gradient` is only ever a circle or an ellipse, and this one needs a corner radius. `border-radius` is that knob, and `corner-shape: superellipse(3)` makes it a squircle where the browser supports it. Alpha compositing of a solid blends where mask layers multiply, so the falloff is smooth instead of compounding into a shoulder. It is drawn with `repeating-linear-gradient` rather than an inlined SVG — no data URI in the stylesheet. The lines are `--wave-docs-hero-grid` and `--wave-docs-hero-grid-strong`, which are Wave 200 and Wave 300 from `@waveso/ui` in the light theme and Wave 900 and Wave 800 in the dark one. They are tokens of their own rather than the border colours: the grid is decoration behind a mask that leaves it near-invisible where the words are, and a border is a boundary a reader has to be able to see.
|
|
388
|
+
|
|
310
389
|
### Your own fields
|
|
311
390
|
|
|
312
391
|
Pass a `frontmatterSchema` and every `DocFile` and `RenderedDoc` carries your fields, inferred, with no type argument anywhere:
|
|
@@ -715,7 +794,7 @@ export function DocsSearchTrigger({ indexUrl }: { indexUrl: string }) {
|
|
|
715
794
|
}
|
|
716
795
|
```
|
|
717
796
|
|
|
718
|
-
Add the same function to your `createDocsRoute` call — `miniSearchOptions: { processTerm: stripDashes }` — so the index is built with it. Then turn the built-in trigger off and render yours in `
|
|
797
|
+
Add the same function to your `createDocsRoute` call — `miniSearchOptions: { processTerm: stripDashes }` — so the index is built with it. Then turn the built-in trigger off and render yours in the layout you write around `docs.Layout`, in `app/docs/layout.tsx`:
|
|
719
798
|
|
|
720
799
|
```tsx
|
|
721
800
|
import '@waveso/docs/styles.css';
|
|
@@ -725,17 +804,15 @@ import { docs } from '@/lib/docs';
|
|
|
725
804
|
|
|
726
805
|
export default function DocsLayout({ children }: { children: ReactNode }) {
|
|
727
806
|
return (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
{children}
|
|
733
|
-
</docs.Layout>
|
|
807
|
+
<>
|
|
808
|
+
<DocsSearchTrigger indexUrl={docs.searchIndexUrl} />
|
|
809
|
+
<docs.Layout search={false}>{children}</docs.Layout>
|
|
810
|
+
</>
|
|
734
811
|
);
|
|
735
812
|
}
|
|
736
813
|
```
|
|
737
814
|
|
|
738
|
-
`search={false}` omits the built-in trigger so yours is the only one, and it is also why the refusal is scoped to the forward: the route keeps the function for the index it builds on the server, and nothing crosses to the client but a string.
|
|
815
|
+
`search={false}` omits the built-in trigger so yours is the only one, and it is also why the refusal is scoped to the forward: the route keeps the function for the index it builds on the server, and nothing crosses to the client but a string. To put your trigger *inside* the sidebar rather than above it, [compose the shell yourself](#composing-it-yourself) — `docs.Layout` has no slot for it, deliberately.
|
|
739
816
|
|
|
740
817
|
### Building the index yourself
|
|
741
818
|
|
|
@@ -849,7 +926,7 @@ A link is skipped when it equals one of these or begins with one followed by `/`
|
|
|
849
926
|
|
|
850
927
|
### Translating the chrome
|
|
851
928
|
|
|
852
|
-
Twenty-two strings, and every one of them is yours to set. They go on `createDocsRoute` rather than on `docs.Layout`, because they are not all rendered in the same place: four are the shell's, two the table of contents', nine come from the markdown component map, two are baked into the HTML by a rehype plugin at build time, and two are announced by a client-side runtime after a copy. A layout prop is upstream of the first four and nothing else.
|
|
929
|
+
Twenty-two strings, and every one of them is yours to set. They go on `createDocsRoute` rather than on `docs.Layout`, because they are not all rendered in the same place: four are the shell's, three the navigation tree's, two the table of contents', nine come from the markdown component map, two are baked into the HTML by a rehype plugin at build time, and two are announced by a client-side runtime after a copy. A layout prop is upstream of the first four and nothing else.
|
|
853
930
|
|
|
854
931
|
```ts
|
|
855
932
|
// lib/docs-pt.ts
|
|
@@ -1051,9 +1128,9 @@ breaking — which is the part most packages leave unsaid until someone is angry
|
|
|
1051
1128
|
| --- | --- |
|
|
1052
1129
|
| Every subpath in `exports`, and every runtime name it exports | ✅ enforced by `manifest.test.ts` |
|
|
1053
1130
|
| Exported types, including `DocsErrorCode`'s members | ✅ enforced by `error-taxonomy.test.ts` |
|
|
1054
|
-
| **CSS class names** — `wave-docs-*`, and the shell's element tree | ✅
|
|
1131
|
+
| **CSS class names** — `wave-docs-*`, and the shell's element tree | ✅ a name changes only in a release that carries the migration |
|
|
1055
1132
|
| **The hast this emits** — element names, and the attributes on them | ✅ the same policy as the types |
|
|
1056
|
-
| Layout tokens — the
|
|
1133
|
+
| Layout tokens — the six custom properties above | ✅ |
|
|
1057
1134
|
| Anything reachable only through `dist/` internals, or a private module | ❌ |
|
|
1058
1135
|
|
|
1059
1136
|
The two in bold are the ones usually omitted, and omitting them is how a
|
|
@@ -1147,7 +1224,7 @@ src/
|
|
|
1147
1224
|
next.ts # The App Router adapter
|
|
1148
1225
|
meta.ts # meta.json ordering
|
|
1149
1226
|
plugins/ # remark/rehype plugins
|
|
1150
|
-
react/ # Components. Only next
|
|
1227
|
+
react/ # Components. Only the next-* modules touch next/*
|
|
1151
1228
|
styles.css # Theme tokens + prose styles
|
|
1152
1229
|
```
|
|
1153
1230
|
|
package/dist/frontmatter.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
32
32
|
draft: z.ZodExactOptional<z.ZodBoolean>;
|
|
33
33
|
aliases: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
|
|
34
34
|
order: z.ZodExactOptional<z.ZodNumber>;
|
|
35
|
+
actions: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
|
|
36
|
+
label: z.ZodString;
|
|
37
|
+
href: z.ZodString;
|
|
38
|
+
variant: z.ZodExactOptional<z.ZodEnum<{
|
|
39
|
+
primary: "primary";
|
|
40
|
+
secondary: "secondary";
|
|
41
|
+
}>>;
|
|
42
|
+
}, z.core.$strip>>>;
|
|
35
43
|
}, z.core.$strip>;
|
|
36
44
|
/**
|
|
37
45
|
* Validate one file's frontmatter, or throw an error that names the file.
|
package/dist/frontmatter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { docsError } from "./docs-error.js";
|
|
2
|
+
import { isSafeHref } from "./safe-href.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/frontmatter.ts
|
|
4
5
|
/**
|
|
@@ -30,7 +31,12 @@ const docFrontmatterSchema = z.object({
|
|
|
30
31
|
label: z.string().exactOptional(),
|
|
31
32
|
draft: z.boolean().exactOptional(),
|
|
32
33
|
aliases: z.array(z.string()).exactOptional(),
|
|
33
|
-
order: z.number().exactOptional()
|
|
34
|
+
order: z.number().exactOptional(),
|
|
35
|
+
actions: z.array(z.object({
|
|
36
|
+
label: z.string().min(1),
|
|
37
|
+
href: z.string().min(1).refine(isSafeHref, "must be a safe URL (no javascript: or data:)"),
|
|
38
|
+
variant: z.enum(["primary", "secondary"]).exactOptional()
|
|
39
|
+
})).exactOptional()
|
|
34
40
|
});
|
|
35
41
|
/**
|
|
36
42
|
* The package's own fields, every one of them optional, for the overlay pass
|
package/dist/next.d.ts
CHANGED
|
@@ -96,27 +96,25 @@ interface DocsRouteOptions<TFrontmatter extends DocFrontmatter = DocFrontmatter>
|
|
|
96
96
|
/**
|
|
97
97
|
* Props for {@link DocsRoute.Layout}.
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
* turned out to be reachable already: an announcement banner renders
|
|
101
|
-
* `<docs.Layout>` in your own `layout.tsx`, because this does not own
|
|
102
|
-
* a content footer goes inside `children`; and sidebar links, social
|
|
103
|
-
* separators are `DocNavNode`s authored in `meta.json`.
|
|
104
|
-
*
|
|
99
|
+
* Three, and one of them is `children`. Everything else a docs shell is asked
|
|
100
|
+
* for turned out to be reachable already: an announcement banner renders
|
|
101
|
+
* *above* `<docs.Layout>` in your own `layout.tsx`, because this does not own
|
|
102
|
+
* `<body>`; a content footer goes inside `children`; and sidebar links, social
|
|
103
|
+
* icons and separators are `DocNavNode`s authored in `meta.json`. A theme
|
|
104
|
+
* toggle and a repository link go in the layout you write around this one — the
|
|
105
|
+
* host wraps `docs.Layout` exactly as it already wraps `<html>` and `<body>`,
|
|
106
|
+
* so there is no region only this package can reach.
|
|
105
107
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
+
* The one region a host cannot reach through `docs.Layout` is *inside* the
|
|
109
|
+
* sidebar, and the exported primitives are the answer for that: `DocsSidebar`,
|
|
110
|
+
* `DocsToc`, `DocContent` and `SkipLink` compose into a layout of your own.
|
|
111
|
+
*
|
|
112
|
+
* A `slots` map was the alternative, and shipping none is the reversible half —
|
|
113
|
+
* a map can be added the day something needs one, a map that shipped cannot be
|
|
114
|
+
* taken back.
|
|
108
115
|
*/
|
|
109
116
|
interface DocsLayoutProps {
|
|
110
117
|
children: ReactNode;
|
|
111
|
-
/**
|
|
112
|
-
* Brand at the header start. A string, or your own logo component.
|
|
113
|
-
*
|
|
114
|
-
* `ReactNode`, so it cannot also serve as the `<title>` or as the header's
|
|
115
|
-
* accessible name; the landmark carries a fixed label instead.
|
|
116
|
-
*/
|
|
117
|
-
title?: ReactNode;
|
|
118
|
-
/** Header end, after search: a theme toggle, a version switcher, a link. */
|
|
119
|
-
actions?: ReactNode;
|
|
120
118
|
/**
|
|
121
119
|
* The search trigger. Defaults to on, and the URL is always derived.
|
|
122
120
|
*
|
|
@@ -318,24 +316,33 @@ interface DocsRoute<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
|
|
|
318
316
|
* export default docs.Layout;
|
|
319
317
|
* ```
|
|
320
318
|
*
|
|
321
|
-
* Or, with your own chrome
|
|
319
|
+
* Or, with your own chrome *around* it — the same layout file `<html>` and
|
|
320
|
+
* `<body>` live in, and `SiteHeader` is yours:
|
|
322
321
|
*
|
|
323
322
|
* ```tsx
|
|
324
323
|
* export default function DocsLayout({ children }: { children: ReactNode }) {
|
|
325
324
|
* return (
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
325
|
+
* <>
|
|
326
|
+
* <SiteHeader />
|
|
327
|
+
* <docs.Layout search={{ placeholder: 'Search the docs' }}>
|
|
328
|
+
* {children}
|
|
329
|
+
* </docs.Layout>
|
|
330
|
+
* </>
|
|
329
331
|
* );
|
|
330
332
|
* }
|
|
331
333
|
* ```
|
|
332
334
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
335
|
+
* If that header of yours is sticky, say how tall it is once —
|
|
336
|
+
* `--wave-docs-chrome-offset: 4rem` — and our sticky columns start below it.
|
|
337
|
+
*
|
|
338
|
+
* It owns the skip link, the sidebar — one shell at every width, holding the
|
|
339
|
+
* navigation and the 44px strip that moves it — the search trigger and the
|
|
340
|
+
* grid.
|
|
341
|
+
* It reads `source.nav()` and `searchIndexUrl` itself, so there is no nav to
|
|
342
|
+
* fetch and no URL to pass. It does **not** own the table of contents: a Next
|
|
343
|
+
* layout receives `{children, params}` and cannot know which page is
|
|
344
|
+
* rendering, so `docs.Page` emits the TOC as its second child and the grid
|
|
345
|
+
* places it.
|
|
339
346
|
*
|
|
340
347
|
* Your `layout.tsx` stays a Server Component. The two pieces that need a
|
|
341
348
|
* client — the nav's `usePathname`, the search dialog — carry their own
|