Developing

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

PieceChoice
LanguageTypeScript in strict mode, React 19.
ToolingVite+ (vp): dev server, build, format, lint, typecheck, and tests.
RoutingTanStack Router, file-based, in src/routes/.
Server stateTanStack Query. Hooks live in src/api/.
PrimitivesBase UI, wrapped in src/components/.
Iconslucide-react.
StylingCSS 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

web/src
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:

dashboard · web/srcRouteloaderQuery hookapi/*.tsAPI clientclient.tsQuery cachekeys.tsComponentfeatures/shed API/api/*SQLite, Dockersource of truthfetchThe route's loader and the component use the same query options, so the page opens withcached data. A mutation invalidates the keys it changed, and the hooks refetch.
A route's loader warms the cache, and components read it through the same query options.
  • Routes in src/routes/ are named by path. _app is the signed-in layout, and a $param segment is a route parameter. A route's loader calls queryClient.ensureQueryData so the page renders with data.
  • Query hooks in src/api/ pair a query key from keys.ts with a fetch, and mutations invalidate the keys they affect. Polling intervals live there too.
  • The client in api/client.ts prefixes /api, sends JSON with cookies, and throws ApiError on a non-2xx response. Logs and status streams use useEventSource in api/events.ts.
  • Types in api/types.ts mirror the API types in docs/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. LayerCard is 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 -line variants. Components take a tone, set data-tone, and read the variants from CSS. accent follows the user's accent hue.
  • Browse them. The /showcase route 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.module.css (pattern)
.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:

  • title is the page title and its sidebar label.
  • group is the sidebar group. Groups appear in the order of their first page.
  • order sorts pages across all groups. It must be unique, and a test checks that.
  • description is 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.

content/example.mdx
---
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:

ComponentUse
CalloutA highlighted note.
CodeBlockCode with a copy button. Fenced code blocks already render through it.
DemoFrames an interactive explainer that runs on sample data or reader input.
FigureFrames a diagram or demo with a caption.
HelpTipThe ? tooltip, for a topic from topics.ts.
TermAn 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.

topics.ts
waitForCi: {
  page: "github",
  section: "waiting-for-ci",
  summary: "Hold the deploy until every check run and status on the commit passes.",
},
in a component
<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.