@waveso/docs 0.2.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +490 -75
  3. package/dist/code-frame.d.ts +29 -0
  4. package/dist/code-frame.js +41 -0
  5. package/dist/code-meta.d.ts +48 -0
  6. package/dist/code-meta.js +72 -0
  7. package/dist/docs-content-id.d.ts +19 -0
  8. package/dist/docs-content-id.js +19 -0
  9. package/dist/docs-error.d.ts +2 -57
  10. package/dist/docs-error.js +3 -15
  11. package/dist/errors.d.ts +94 -0
  12. package/dist/errors.js +45 -0
  13. package/dist/next.d.ts +153 -28
  14. package/dist/next.js +65 -33
  15. package/dist/plugins/rehype-capture-toc.js +26 -5
  16. package/dist/plugins/rehype-code-frame.d.ts +10 -0
  17. package/dist/plugins/rehype-code-frame.js +88 -0
  18. package/dist/plugins/rehype-code-language.js +7 -1
  19. package/dist/react/code-runtime.d.ts +14 -0
  20. package/dist/react/code-runtime.js +161 -0
  21. package/dist/react/doc-content.d.ts +39 -2
  22. package/dist/react/doc-content.js +42 -10
  23. package/dist/react/layout.d.ts +44 -0
  24. package/dist/react/layout.js +65 -0
  25. package/dist/react/nav.d.ts +28 -0
  26. package/dist/react/nav.js +70 -0
  27. package/dist/react/nearest-scroll-top.d.ts +45 -0
  28. package/dist/react/nearest-scroll-top.js +44 -0
  29. package/dist/react/next-link.d.ts +34 -0
  30. package/dist/react/next-link.js +30 -0
  31. package/dist/react/next-nav.d.ts +11 -0
  32. package/dist/react/next-nav.js +32 -0
  33. package/dist/react/next-search.d.ts +22 -0
  34. package/dist/react/next-search.js +52 -0
  35. package/dist/react/search-dialog.d.ts +20 -8
  36. package/dist/react/search-dialog.js +15 -10
  37. package/dist/react/shell-labels.d.ts +43 -0
  38. package/dist/react/shell-labels.js +27 -0
  39. package/dist/react/sidebar.d.ts +38 -3
  40. package/dist/react/sidebar.js +104 -12
  41. package/dist/react/skip-link.d.ts +1 -9
  42. package/dist/react/skip-link.js +6 -5
  43. package/dist/react/toc.d.ts +12 -4
  44. package/dist/react/toc.js +18 -7
  45. package/dist/react/youtube.d.ts +31 -5
  46. package/dist/react/youtube.js +76 -54
  47. package/dist/render.d.ts +35 -1
  48. package/dist/render.js +35 -14
  49. package/dist/route-path.d.ts +46 -0
  50. package/dist/route-path.js +51 -0
  51. package/dist/search-index.d.ts +6 -23
  52. package/dist/search-index.js +6 -51
  53. package/dist/sitemap-limit.d.ts +34 -0
  54. package/dist/sitemap-limit.js +37 -0
  55. package/dist/source.d.ts +1 -23
  56. package/dist/source.js +40 -43
  57. package/dist/styles.css +939 -93
  58. package/dist/types.d.ts +11 -2
  59. package/package.json +58 -23
package/CHANGELOG.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # @waveso/docs
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3ba341f: A docs site is now four files and a folder of markdown. Search works out of the box, there is a real shell with a mobile drawer, and the public surface is frozen. Several changes are breaking; those come first.
8
+
9
+ **The page's content is a `main` landmark.** It was an `<article>`, so the shell rendered a banner, a navigation and a complementary and no `main` at all — a screen-reader user navigating by landmark, which is how you skip a hundred-link sidebar without tabbing, had nothing to jump to. **Breaking in rendered output:** a selector or an assertion targeting `article.wave-docs-layout__main` needs to say `main`.
10
+
11
+ **`docs.Layout` renders the whole shell**, as one line in your layout file:
12
+
13
+ ```tsx
14
+ // app/docs/layout.tsx
15
+ import "@waveso/docs/styles.css";
16
+ import { docs } from "@/lib/docs";
17
+
18
+ export default docs.Layout;
19
+ ```
20
+
21
+ Skip link, sticky header, sidebar column, mobile drawer and the grid that arranges them. It is a Server Component and your layout stays one — the two pieces that need a client carry their own boundaries inside the package — and it reads the navigation tree and the search index URL itself, so there is nothing to fetch and nothing to pass. It replaces four files every consumer used to write by hand, including a `'use client'` wrapper around `usePathname` that the README shipped as a recipe. Four props: `title`, `actions`, `search`, `children`. `search` takes the dialog's own props as well as a boolean, which is what makes `miniSearchOptions` reachable at all — MiniSearch reads `tokenize` and `processTerm` when indexing _and_ when querying, so an index built with one and queried with another matches nothing, and while `search` was a bare boolean there was no channel for it. The route's own `miniSearchOptions` are forwarded by default, so the object that built the index is the object that queries it without anyone having to know that.
22
+
23
+ **There is a mobile navigation drawer**, which there was not before: on a 390px viewport a reader could previously reach exactly one other page. It is one `<dialog closedby="any">` opened by a server-rendered `<button command="show-modal">`, so it works on the first tap — before hydration, and with JavaScript disabled. Focus moves inside and Tab stays there, Escape closes and restores focus, the backdrop dismisses it, and the page behind does not scroll; all of that is the browser's. At 64rem the same element becomes the sticky sidebar column via `display: contents`, so one navigation serves both breakpoints — one landmark, one copy of the links in the payload.
24
+
25
+ **`docs.Page` returns two children now**, the `<article>` and the table of contents, rather than one. They land as direct children of the grid, which is what puts them in separate columns. If you wrapped `docs.Page` in an element expecting a single child, that wrapper needs to go. A page with no headings emits no `<aside>` at all rather than an empty one, because the grid reserves that column with `:has()` and would otherwise give 15rem to nothing.
26
+
27
+ **The pipeline has plugin slots**, which it did not before — it was frozen end to end, and both apparent escape hatches are useless (`frozen.use()` throws; `frozen().use(p)` appends, so a plugin runs after Shiki and sees token spans where the author's code was):
28
+
29
+ ```ts
30
+ createDocsRoute({
31
+ contentDir: "content/docs",
32
+ remarkPlugins: [remarkMath],
33
+ rehypePlugins: [rehypeKatex],
34
+ });
35
+ ```
36
+
37
+ `remarkPlugins` run before link resolution, so what they emit is folded, contained and asserted exactly like authored markdown. `rehypePlugins` run after heading ids exist and before Shiki, so a fence is still the author's text. **The table of contents is now captured last**, after your plugins and everything else, so it describes the same document the search index does — a plugin that adds or removes a heading changes both together, and there is no validation pass because there is nothing left to validate.
38
+
39
+ **Every size and speed claim is now measured and budgeted — including the README's own table.** The figures there are ceilings rather than snapshots, and `pnpm size` fails when a measurement passes one, when one promises better than `size-budget.json` enforces, or when a row goes missing. That axis exists because the table had already rotted: it published the sidebar at 1.24 KB against a real 1.67 KB, and a budget raised two commits earlier, under a sentence promising every number was one a build fails over.
40
+
41
+ `pnpm size` runs in CI and in `prepublishOnly` across three axes — client bytes per `'use client'` entry, hast-over-the-wire against HTML as a brotli ratio, and highlighting cost against the same corpus unhighlighted. A "What it costs" table sits above the quick start with a budget entry behind every figure. Three numbers in the tree were wrong and are fixed by measuring rather than by picking a side: positions are 33% of the JSON, not the 38% one comment claimed or the 44% the README claimed, and the documented Node floor was 20.19.0 against a real floor of 22.12.0.
42
+
43
+ **The shell is translatable.** `docs.Layout` takes a `labels` object with the four strings it renders itself — the navigation landmark's name, the drawer's open and close buttons, the skip link — each falling back on its own, so a partial map is not a half-English shell. `DocsNav` had declared `label` and `closeLabel` props, documented them and defaulted them, while the layout that is the only thing rendering it passed neither and `DocsLayoutProps` had no way to say them: configuration that could not be configured.
44
+
45
+ **Two rendering defects that only a screenshot could find.** A code frame's title bar drops its bottom border to join the code below it, and a `<pre>`'s user-agent margin then pushed the two 14px apart — a caption hovering over a gap, and a copy button mis-seated on untitled fences. And the shell painted its own containers but not the grid between them, so in dark mode the sidebar and the table of contents rendered as lighter panels floating on a darker page; light mode hid it because both were white. Both are fixed, both are now pinned by browser tests, and the README carries screenshots taken from a real build so the next one is visible in review.
46
+
47
+ **Smaller repairs a reader would notice.** The video facade asks the player to start, so watching a video is one click rather than two — it is still not an autoplaying embed, because the iframe does not exist until the `<details>` opens. A failed copy now _looks_ failed: `data-copied="false"` was written from the beginning and had no rule in the stylesheet, so a screen-reader user was told and a sighted user watched a button do nothing, on the ordinary path of `next dev` opened from a phone over http. The table's horizontal-scroll shadow was hardcoded black and therefore invisible in dark mode. An excluded fence had no `tabindex`, making it the one code block on a page a keyboard could not scroll sideways.
48
+
49
+ **The sidebar scrolls the current page into view** — properly. The first version measured the item with `offsetTop - port.offsetTop`, and the navigation column is `position: sticky`, which makes it the `offsetParent`: the offset was already relative to it, so the subtraction removed the header height twice and parked the current page below the fold. Measured in Chromium and fixed with rectangles, and a browser test now asserts the item is inside the visible box rather than that something scrolled. A back-to-top link ends the table of contents. Both move focus rather than only the viewport, and neither uses `scrollIntoView` — which scrolls every scrollable ancestor including the document, so it would jump the article a reader just navigated to. External links in the navigation carry a small icon.
50
+
51
+ **Errors are branchable, at `@waveso/docs/errors`.** Every failure already carried a `code` from a 19-member union, and the module documenting that taxonomy also declared itself private — so there was no supported way to use it, and nothing in the README mentioned it. `DocsErrorCode`, `DocsError` and `isDocsError` are exported now, with a troubleshooting table carrying one row per code, and a test that keeps the union, the table and the call sites in step.
52
+
53
+ **The YouTube embed ships no JavaScript, and the default component map is now provably server-only.** It was a `'use client'` component mapped unconditionally, so every page of every consumer carried a reference to it whether or not it embedded a video. It is a `<details>` with a lazy iframe now — measured: a closed one issues no request, an open one does — which keeps the click-to-load facade, gains native keyboard support, and removes the hydration root. `src/server-boundary.test.ts` fails the build if anything reachable from the markdown map ever carries the directive again. **Breaking in rendered output:** the facade is a `<details>`/`<summary>` rather than a `<button>`, so styles or assertions targeting the button change with it. The open state hides that summary, which cost the keyboard reader their focus indicator entirely — it comes back on `:focus-visible` as a real control, and its label reads "Hide video" once activating it would hide the video.
54
+
55
+ **Sidebar links near the reader prefetch now.** Every link carried `prefetch={false}`, on Pages-Router reasoning that does not hold in the App Router — there it disables the hover and touch paths as well as the viewport one, so the most-clicked control in a docs site made every navigation a cold round-trip. The list holding the current page and the heading of the group around it are warm; the rest of the tree stays off.
56
+
57
+ **The table of contents stops at `h3`.** Measured on a synthetic API reference — 8 methods, 3 overloads, 3 subsections each — capturing h2–h6 gave 104 entries against 32. Deeper headings keep their ids and permalinks, so they are still deep-linkable and still open their own sections in search; only the rail entry is dropped. If you want them back, `rehypePlugins` is the escape hatch. **`RenderedDoc.toc` therefore contains less data than before** — if you render it yourself and relied on h4+, that is the change.
58
+
59
+ **`gray-matter` is gone**, replaced by `vfile-matter`. It did a bare `require('fs')` for a method this package never calls — the only gratuitous Node requirement in the whole tree — dragged a second copy of js-yaml, and memoised every file body it ever saw in a cache that is never evicted. Frontmatter parsing is unchanged in behaviour, including the UTF-8 BOM, which this package now strips itself.
60
+
61
+ **Runtime requirements are documented and asserted.** `render`, `frontmatter`, `highlighter`, `search-index` and the React layer require **no Node builtins at all** — the markdown pipeline runs wherever JavaScript does, and Shiki is loaded through its JavaScript regex engine rather than WASM deliberately. `source` needs `node:fs/promises` and `node:path`; `next` adds `node:crypto`. Every one of those is asserted as an exact set, so a new builtin three modules deep fails CI instead of silently ruling out a runtime.
62
+
63
+ **Code blocks have a frame, a title bar and a copy button**, which they did not before — a live render was a bare `<pre>` with no wrapper and no control of any kind:
64
+
65
+ ````md
66
+ ```ts title="app/page.tsx"
67
+ export default function Page() {}
68
+ ```
69
+ ````
70
+
71
+ The title becomes the caption, the button's accessible name (`Copy code from app/page.tsx`, not eight controls called "Copy code"), and a search hit. Anything else in the meta string passes through to Shiki untouched, so `{1,3-5}` keeps working when transformers land; a `title=` without double quotes now fails the build naming the document, rather than silently truncating at the first space. Copy is **one delegated listener for the page**, mounted by `DocContent` and only when the page has a fence — not a client component per code block — and the button is `visibility: hidden` until that listener attaches, so a reader with JavaScript off sees no button and finds no dead tab stop.
72
+
73
+ **`excludeLangs` stops shipping as half a feature.** An excluded fence had no background, border, padding or horizontal scroll anywhere in the stylesheet, so it rendered as UA-default text bleeding out of the reading column. It now gets the same surface as a highlighted block, is deliberately left unframed, and the README carries the Mermaid recipe — including the trap that its `<code>` className is an array where Shiki's is a string.
74
+
75
+ **`DocContent` now carries `wave-docs-prose` itself.** Nearly every rule in the stylesheet is scoped under that class, and the hand-rolled route in the README made you type it; forgetting it left a page whose code blocks kept their syntax colours and lost everything else. If you were putting the class on your own `<article>`, drop it — it is emitted once, by the component.
76
+
77
+ **Search is on by default, and the quick start includes its route.** `docs.Layout` renders the trigger, so leaving the route file out gave a reader a control that opened onto "Search is unavailable" — a broken box on the flagship path, under a release note promising search out of the box. The route is one of the three files in the quick start now, and a 404 on the index no longer reads as a transient failure: it names the file to create.
78
+
79
+ **Search is two files now, and neither is a build script.** The index is a prerendered route handler on the object you already hold, so it is rebuilt by the same `next build` that builds your pages — and in `next dev` it re-reads the disk per request, so a page you add is searchable on the next keystroke instead of at the next time you remember to run a script:
80
+
81
+ ```ts
82
+ // app/docs/search-index.json/route.ts — the whole file
83
+ import { docs } from "@/lib/docs";
84
+
85
+ export const GET = docs.searchIndex;
86
+ export const dynamic = "force-static";
87
+ ```
88
+
89
+ `docs.searchIndexUrl` is derived from your `basePath`, so it is right at `/`, at `/docs`, and under a nested prefix. **`writeSearchIndex` is removed** — it was the only documented way to build an index, and this replaces it; `buildSearchIndex` and `renderAll()` remain for an artifact the route cannot express, and `docs.searchIndex` is asserted byte-identical to them. Verified against a real `next build`: the body prerenders in both output modes and is byte-identical between them, response headers survive into the prerender manifest, and CI now runs that build on every pull request.
90
+
91
+ **`export const dynamic = 'force-static'` is not optional, and the handler enforces it.** Without it Next re-renders your entire corpus per request, from markdown that output tracing did not put in the deployment bundle — on a serverless host that throws, at the reader, inside the search dialog, and the build prints no warning. It now fails loudly with `code: 'search-index-dynamic'`, naming the file to edit.
92
+
93
+ **`DocsSearch`, at `@waveso/docs/react/next-search`**, is the `'use client'` wrapper around `useRouter()` and `next/link` that every consumer was writing by hand — and skipping `Link` silently cost hover prefetching on every result. `SearchDialog` is unchanged and still host-agnostic.
94
+
95
+ **`SearchDialog`'s `searchOptions` prop is now `miniSearchOptions`**, and `createDocsRoute` takes the same name for the same object. MiniSearch's own name for the query defaults is `searchOptions`, so the old prop produced `searchOptions={{ searchOptions: { fuzzy: 0.1 } }}` — a stutter nobody writes, which is why both README examples were written flat, did not compile, and would not have errored at runtime either.
96
+
97
+ **Your project is no longer traced into your server bundle.** `contentDir` is a string this package cannot resolve statically, so Turbopack fell back to tracing the whole project — every source file, your entire `public/` folder, your last build's output — into the server output for every docs route. Measured at 332 traced files for a three-page site, of which 39 were the project's own. Nothing here reads markdown at request time, so nothing needs tracing, and now nothing is.
98
+
99
+ **Six names are gone**, all pre-1.0. Three are subpaths: the `./react/*` wildcard is enumerated as explicit subpaths, and `./markdown-links` and `./search-options` are no longer exported. Three are options and exports that changed shape, and each needs an edit rather than a rename:
100
+
101
+ | Gone | What to do |
102
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `createDocsRoute({ contentId })` | Nothing. The main element always carries `id="docs-content"`, which is what `SkipLink` targets by default. The option could point the skip link at nothing, silently, because `SkipLink` had no matching option to follow it with |
104
+ | `createDocsRoute({ rescanPerRequest })` · `createDocsSitemap({ rescanPerRequest })` | Nothing, if you left it at its default: the content directory is re-scanned per request outside `NODE_ENV=production`, exactly as before. **If you set it to `false`, delete it and measure** — you will now pay a scan per non-production request, and `docs.source.invalidate()` is the explicit tool |
105
+ | `toAliasRoute` from `@waveso/docs/source` | Private. It is one line of string handling and was never documented |
106
+
107
+ 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.
108
+
109
+ **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 in `docs/adr/001-shell-contract.md`.
110
+
111
+ **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`.
112
+
3
113
  ## 0.2.0
4
114
 
5
115
  ### Minor Changes