ADR-0018 — Docs pages get orbit stations, a trajectory and the full horizon
Docs pages gain a planet row and orbit stations in the sidebar, a trajectory for On this page, and the full Horizon footer.
- Status: Approved
- Date: 2026-11-01
- Deciders: @puncoz
- Approved by: @puncoz
- Approved on: 2026-11-01
- Supersedes: ADR-0017 decision 8 (in part: docs pages dropping the horizon's scene) and decision 10 (in part: the sidebar's link list and the table of contents hanging on a thin orbit line, and the sidebar top's layout)
- Superseded by: ADR-0019 (in part: decision 2, how an ADR folder's pages show); ADR-0023 (in part: decision 2's Open and closed point, stations now open one at a time), ADR-0027 for decision 2, in part (every folder a card; how top-level pages and separators look); ADR-0030 (in part: decision 4, the footer sitting below the sidebar, and its scene)
- Design documents:
../app-flow/docs-sidebars.mdx,../prd/docs-sidebars.mdx,../app-flow/solar-system.mdx,../app-flow/site-pages.mdx,../architecture/system.mdx,DESIGN.md
Context
ADR-0017 made the whole site one solar system, but the two columns a reader uses most on a docs page still look like Fumadocs' defaults with a dot added:
- The left sidebar is Fumadocs' list: folders as plain rows with a straight guide line, the current page marked by a dot. The metaphor stops at the planet chip.
- The right-hand "On this page" is Fumadocs'
normaltable of contents. Its rail curves only where heading depth changes (components/toc/default.jsdraws a cubic curve between items of different depth and a straight line otherwise), so on a page whose headings are all##it is one straight line. The owner remembers it as "tech-like" and curvy, and finds it plain now. - The horizon below docs pages drops its scene and closing line (ADR-0017 decision 8). The owner wants the full horizon on every page.
The owner chose, from options with previews (2026-11-01): orbit stations for the left sidebar, a trajectory with a probe for "On this page", and the full horizon on docs pages.
Facts this decision rests on, read from the installed Fumadocs 16.17.9:
- The notebook layout's
sidebar.componentsacceptsItem,FolderandSeparatorcomponents (SidebarPageTreeComponents,components/sidebar/page-tree.d.ts). The default renderer opens a folder when the current page is inside it (active: path.includes(node)), and otherwise followsdefaultOpenLevel, 0 by default, so other folders start closed. DocsPagetakesslots.tocwithprovider,mainandpopover(layouts/notebook/page/index.d.ts);mainis the desktop column andpopoverthe bar on narrow screens.fumadocs-core/tocexportsuseItems,useActiveAnchorsandAnchorProvider; each item hastitle,urlanddepth.- Each page's
structuredData(fumadocs-mdx) holds its headings and text, so its word count is known at build time without reading the file again.
Decision
-
The sidebar top becomes one planet row. The planet chip (mark and name, linked to the overview) and the day/night switch share one row: the switch shrinks to two icon buttons, a drawn sun for Use and a drawn moon for Maintain, each with its full name as its accessible name and tooltip. The moon picker sits under it on Use, as today.
-
Every section is an orbit station. A sidebar folder renders as a station: a card (12px radius, 1px border, the panel colour) with a header and its pages.
- Header: a small orbit drawing (a ring, with a filled planet on it when the current page is inside), the section's name, the number of pages in it, and a chevron. When the folder has an index page, the name links to it, as Fumadocs does today.
- Pages are moons. Inside an open station each page is a row with a moon dot, and the
dots sit on one arc of the station's orbit, drawn in the left gutter: the arc bows outward,
so a long station is a gentle curve, never a ruled line. The current page is the planet: an
accent dot with a halo, its row tinted with the entry's ecosystem colour, marked
aria-current="page". Hovering a row fills its dot. - Open and closed. The station holding the current page opens and its border takes the ecosystem colour; the others start closed and show only their header, so a long sidebar (this site's Maintain track has twelve sections) scans as a list of station names. The chevron opens and closes a station.
- Nested folders are smaller stations inside their parent, without a card border: a smaller orbit drawing and a shorter arc. Deeper levels indent without a further drawing.
- Top-level pages outside any folder (an overview) sit above the stations on their own short arc, with the same moon dots. A Fumadocs separator is a dotted line with its label.
-
"On this page" is a trajectory with a probe. Our own component replaces Fumadocs'
normaltable of contents in bothslots.toc.mainandslots.toc.popover, built onuseItemsanduseActiveAnchors.- The path. One continuous curved path through a dot per heading, drawn in a 28px gutter.
Depth sets each dot's offset (
##6px,###18px, deeper 26px). Between two headings of the same depth the path bows outward in a shallow arc; across a depth change it swings in an S-curve. So the path curves on every page, including one with only##headings. - Read and unread. The stretch already read is drawn solid in the accent colour, and its dots filled. The stretch ahead is drawn dashed and muted, with hollow dots, the same dashed language as an unreleased moon.
- The probe. A small planet (accent, with a halo and a shaded night side) rides the path at the reader's position, gliding between dots as they scroll through a section, not jumping from heading to heading. The active heading's text takes the foreground colour.
- Time left. The header reads "On this page" and, at the right, "4 min left": the page's reading time, from its word count at build time at 220 words a minute, scaled by how much is still unread and rounded up. At the end of the page it reads "End of page". A page under one minute shows no time.
- Back to top. Under the list, "Back to top ↑", shown once the reader has scrolled past the first heading.
- On phones, Fumadocs' bar under the header becomes our bar: a small ring with the probe at the reading position, the current heading, and the time left. Tapping it opens the same trajectory as a sheet; choosing a heading scrolls there and closes it.
- Choosing a heading scrolls to it smoothly, and instantly under reduced motion, where the probe also jumps from dot to dot instead of gliding.
- The path. One continuous curved path through a dot per heading, drawn in a 28px gutter.
Depth sets each dot's offset (
-
The full horizon closes every page (supersedes ADR-0017 decision 8, in part). Docs pages get the same footer as the home page: the closing line, the star rising over the planet's edge, the planet row and the link row, across the full width below the sidebar and the page. Its last link reads "Back to top" on docs pages and "Back to orbit" on the home page and the 404. The
compactvariant is removed. -
What stays. The moon picker, version notices, planet header, page headings, body text and reading column are unchanged. Search, the header and the theme switch are unchanged. Every drawing is inline SVG in the existing tokens (ADR-0017 decision 13); no dependency is added.
Options considered
| Concern | Option | Pros | Cons | Verdict |
|---|---|---|---|---|
| Left sidebar | A — orbit stations | Each section reads as a place in the system; closed stations make long sidebars short | A custom Folder and Item to own; a card per section takes more height when open | Chosen (owner) |
| Left sidebar | B — planet console: the moons as the version picker, a sliding terminator for Use and Maintain, one continuous path through every page | The strongest use of the metaphor | The most custom code; replaces Fumadocs' version menu | Rejected |
| Left sidebar | C — refined classic: today's structure, polished | Lowest risk | Changes little; the metaphor still stops at the chip | Rejected |
| On this page | A — trajectory with a probe and time left | Curves on every page; shows progress and what is left | Our own component in place of Fumadocs' | Chosen (owner) |
| On this page | B — orbit dial: a reading-progress orbit above a curved list | Compact and playful | Progress and the list sit apart | Rejected |
| On this page | C — Fumadocs' clerk style, recoloured | No custom code | Least distinctive; still Fumadocs' look | Rejected |
| Horizon on docs pages | A — the full horizon | One footer everywhere | A long page ends with a tall footer | Chosen (owner) |
| Horizon on docs pages | B — the scene without the closing line | Shorter | Two footers to keep | Rejected |
| Reading time | A — word count at build time from structuredData | No work in the browser; the same number in every visit | Code blocks count as words | Chosen |
| Reading time | B — count the rendered text in the browser | Counts exactly what is shown | Work on every visit; differs before hydration | Rejected |
Consequences
Positive
- Both columns carry the metaphor: sections are stations, pages are moons, reading is a trajectory.
- "On this page" curves on every page and tells the reader how far they are and how long is left.
- Long sidebars, such as this site's Maintain track, collapse to station names.
- One footer on every page.
Negative / accepted trade-offs
- Two Fumadocs parts are replaced by our own (the sidebar's folder and item, the table of contents and its phone bar). A Fumadocs upgrade that changes those slots needs a check.
- An open station takes more height than a plain folder.
- The full horizon adds height to the end of every docs page.
- Reading time is an estimate; long code blocks overstate it.
Content invariants
- 3 (Version on every page) and 5 (Readable as Markdown): unchanged. The trajectory and
stations are page chrome; the per-page Markdown,
llms.txtandllms-full.txtdon't include them. - No other invariant is touched; no content changes.
Security considerations
None. This change touches no data, no endpoint and no secret. The reading time is computed at build time from the page's own public text; nothing new leaves the system, and nothing is stored in the browser.
Implementation notes
-
src/lib/trajectory.ts: the path builder (dot offsets by depth, arcs and S-curves between dots, the length at each dot), the probe's position, the time-left function, and the sidebar's station arcs (arcRow,arcSlot). Pure functions, no React. -
src/lib/reading-time.ts: words in a page'sstructuredData, and its minutes at 220 a minute. Found on implementation: fumadocs-mdx structures headings, paragraphs, quotes, tables and JSX blocks, not code blocks, so code does not count towards the reading time. -
src/components/docs/trajectory-toc.tsx(client): the desktop column and the phone bar, passed asslots.toc.mainandslots.toc.popoverfromsrc/app/(site)/[...slug]/page.tsx, which also passes the page's minutes throughReadingTimeProvider. Fumadocs'TOCProviderstays the provider. The reading position is measured from the headings' places on the page (the probe needs how far between two headings the reader is), so it usesuseTOCItemsrather thanuseActiveAnchors. -
src/components/docs/orbit-station.tsx(client):OrbitStation,OrbitItemandOrbitSeparator, passed assidebar.componentsinsrc/app/(site)/layout.tsx, built on Fumadocs' sidebar primitives (fumadocs-ui/components/sidebar/base) so opening, closing and the drawer keep working. -
src/components/docs/track-switch.tsx: the planet row (decision 1); the banner carriesdata-ring, from which the sidebar takes its ecosystem colour. -
src/app/(site)/layout.tsx: the fullHorizonFooterwithbackLabel="Back to top";compactremoved fromsrc/components/horizon-footer.tsx. -
src/app/site.css: station, trajectory, probe and phone bar styles, with reduced-motion andhover: nonerules; the orbit-line and dot overrides on Fumadocs' list and table of contents removed. - Tests (ADR-0007, runner ADR-0021):
src/lib/trajectory.test.tsandsrc/lib/reading-time.test.tscover the offsets by depth, the arc between equal depths and the S-curve across a change, one heading and none, the probe before, within and past a section, the time left (counting down, the end, before hydration, under a minute, never zero), the station arc and its slots, and reading time. The client components that draw them need a browser and are not unit-tested (ADR-0021 decision 5). - Checked in the built HTML: twelve stations on this site's Maintain pages with the current
one open and marked, the current page with
aria-current="page", the trajectory column and phone bar, and the full horizon with its closing line and scene. Not yet looked at in a browser. -
bun run lintis blocked by typescript-eslint and TypeScript 7 (reported);bun run types:checkandbunx next buildpass.
Docs to update on implementation
-
../app-flow/docs-sidebars.mdx: status Current -
../prd/docs-sidebars.mdx: status Current -
../app-flow/solar-system.mdx: screen 3 (sidebar top) and screen 5 (the footer on docs pages) -
../app-flow/site-pages.mdx: the docs page screen -
../architecture/system.mdx: the new components and lib files -
DESIGN.mdand.impeccable/design.json: the station, trajectory and probe components -
AGENTS.md§1, §5 and §10;README.mdif its layout section names the changed files -
./index.mdx: index row and status; ADR-0017's "Superseded by" header
ADR-0017 — The whole site is one solar system
The site's single metaphor: the star is the maker, a package is a solid planet, a project a ringed one, and each version line a moon.
ADR-0019 — ADR folders show as a constellation in the sidebar
A folder named adr shows in the sidebar as a constellation, with one moon per decision phased by its status, in any package or project.