Dashboard
The dashboard is a React single-page app in web/. It has no state of its own beyond the URL and a query cache: every screen is a view of what the JSON API returns.
Stack
| Piece | Choice |
|---|---|
| Language | TypeScript in strict mode, React 19. |
| Tooling | Vite+ (vp): dev server, build, format, lint, typecheck, and tests. |
| Routing | TanStack Router, file-based, in src/routes/. |
| Server state | TanStack Query. Hooks live in src/api/. |
| Primitives | Base UI, wrapped in src/components/. |
| Icons | lucide-react. |
| Styling | CSS Modules and design tokens in src/styles/global.css. |
There is no Tailwind and no CSS-in-JS. The build output, web/dist, is embedded in the Go binary. See Running shed locally for the dev loop.
Structure
routes/ file-based routes; thin, they pick a feature and load data features/<name>/ screens and their pieces: projects, services, deployments, ... components/ shared building blocks: Button, Card, Form, Overlay, Badge, ... api/ fetch client, query and mutation hooks, types.ts lib/ pure helpers: formatting, time, cron, logs styles/ global.css: tokens, fonts, resets showcase/ the /showcase route main.tsx router and query client routeTree.gen.ts generated by the router plugin, committed
Data flows one way, from the server to the screen:
- Routes in
src/routes/are named by path._appis the signed-in layout, and a$paramsegment is a route parameter. A route'sloadercallsqueryClient.ensureQueryDataso the page renders with data. - Query hooks in
src/api/pair a query key fromkeys.tswith a fetch, and mutations invalidate the keys they affect. Polling intervals live there too. - The client in
api/client.tsprefixes/api, sends JSON with cookies, and throwsApiErroron a non-2xx response. Logs and status streams useuseEventSourceinapi/events.ts. - Types in
api/types.tsmirror the API types indocs/design.md. Change both together. - Features own their screens. Shared pieces move to
components/only once a second feature needs them.
After you add or rename a file in src/routes/, run the dev server or a build so the plugin regenerates routeTree.gen.ts, and commit it.
Styling
Every color is a token in src/styles/global.css, defined as a light-dark() pair, so a component never branches on theme. Use tokens, not raw values: --ink to --ink-4 for text, --line for borders, --space-*, --r-*, and --text-* for sizes.
- Layering. Surfaces stack as
--bg,--panel,--layer,--raised.LayerCardis a tinted outer sheet with the title, holding a raised inner sheet with the content. - Crayons and tones. The hues (tangerine, sunflower, grass, teal, sky, grape, bubblegum, tomato) each have solid,
-soft,-ink, and-linevariants. Components take atone, setdata-tone, and read the variants from CSS.accentfollows the user's accent hue. - Browse them. The
/showcaseroute renders the foundations and every shared component in light and dark.
Rules:
- No gradients. Surfaces are flat and edges are crisp borders.
- Color means accent or status. Do not use it as decoration.
- A spinner means something is in progress. Do not use one for anything else.
.badge { background: var(--tone-soft); color: var(--tone-ink); border: 1px solid var(--tone-line); } [data-tone="grass"] { --tone-soft: var(--grass-soft); --tone-ink: var(--grass-ink); --tone-line: var(--grass-line); }
The snippet shows the idea. Look at components/Badge.module.css for the real rules.
Writing these docs
These pages are MDX files in src/features/docs/content/. Write them like any Markdown document, and reach for JSX only when a page needs a component. A Vite plugin (docs-plugin.ts) reads every file at build time, so there is no registry to edit and no page map to keep in sync.
Add a page
Create one file, content/<slug>.mdx. The file name is the slug and the URL, /docs/<slug>. Its frontmatter feeds the sidebar and the page header:
titleis the page title and its sidebar label.groupis the sidebar group. Groups appear in the order of their first page.ordersorts pages across all groups. It must be unique, and a test checks that.descriptionis the lede under the title.
Every ## heading becomes a section: it is listed under the page in the sidebar and gets an anchor id. The id is a github-slugger slug of the heading text, so ## Zero-downtime switchover is #zero-downtime-switchover. Renaming a heading changes its id and breaks links and tooltips that point at it, so search for the old id when you reword one. Use ### for subsections. They are not listed and still get ids.
--- title: Example group: Developing order: 200 description: One sentence on what the reader will learn. --- import { ClientServerDiagram } from "../components/example/diagrams"; ## First section Plain Markdown: **bold**, `inline code`, lists, and [links](https://example.com). <Figure caption="What the diagram shows."> <ClientServerDiagram /> </Figure> ```toml title="shed.toml" [server] url = "https://shed.example.com" ``` <Callout>Say something the reader must not miss.</Callout>
Write in the second person, keep paragraphs short, and verify every claim against the code. In prose, put anything with {, }, or < in backticks, because MDX reads them as JSX.
Components
These are available in every page without an import:
| Component | Use |
|---|---|
Callout | A highlighted note. |
CodeBlock | Code with a copy button. Fenced code blocks already render through it. |
Demo | Frames an interactive explainer that runs on sample data or reader input. |
Figure | Frames a diagram or demo with a caption. |
HelpTip | The ? tooltip, for a topic from topics.ts. |
Term | An inline identifier. In Markdown, backticks do the same. |
Markdown maps onto the docs styles too: ## is an anchored heading, a link to /docs/... is a client-side router link, any other link opens in a new tab, and a GFM table becomes a docs table. Give a fenced block a title with title="..." after the language, as in the example above.
A component that belongs to one page lives in components/<slug>/, with its CSS module next to it, and the .mdx imports it at the top, as the example does. Pure logic goes in features/docs/lib/ with a test beside it, and the component stays thin.
Diagrams
Diagram is an SVG canvas in viewBox units that scales to its container. Compose it from Zone (a labeled dashed region), Node (a box with a title and a monospace sub line), Edge (an arrow through points, with flow, dashed, and both), and Note (free text). Nothing measures text for you. A node title takes about 7.5 units per character and a sub line about 6.4, so check that every label fits its box and that no boxes overlap. Wrap the diagram in a Figure in the .mdx.
Demos
Use a Demo when moving a slider or typing a value teaches more than a paragraph would. A demo runs entirely in the browser on sample data or what the reader types, and never calls the API.
Add a ? tooltip
A HelpTip is a small ? that summarizes a concept on hover and links to its docs section. Add a topic to topics.ts, with the page slug, the id of one of its ## headings, and a one-line summary. Then place the tip next to the title or label it explains.
waitForCi: { page: "github", section: "waiting-for-ci", summary: "Hold the deploy until every check run and status on the commit passes.", },
<label> Wait for CI <HelpTip topic="waitForCi" /> </label>
docs.test.ts fails if a topic points at a page or heading that does not exist, and if any /docs/<slug>#<id> link in a page does too. A renamed heading fails the test instead of leaving a dead link. Keep the summary to one or two sentences.
Run bunx vp check --fix and bunx vp test run from web/ before you finish. bun run build compiles every page, so it also catches MDX syntax errors.