thermograph/frontend/DESIGN.md

135 lines
6.6 KiB
Markdown
Raw Permalink Normal View History

# Thermograph — design system
The project-specific visual spec. For general aesthetic guidance the global
`frontend-design` skill still applies; **this file is the source of truth for
Thermograph's own tokens and conventions.** When they disagree, this file wins.
The authoritative values live in **`static/style.css`** (a single hand-written
stylesheet — no framework, no build step). This doc describes *how* to use them;
it deliberately avoids copying hex values that could drift. When in doubt, read
the `:root` block at the top of `style.css`.
## Tokens (never hardcode)
Every color is a CSS custom property in `static/style.css` `:root` (top of the
file), with a `@media (prefers-color-scheme: light)` override right below it.
**Always use `var(--token)`; never paste a raw hex into a rule or an inline
style.** New surfaces get their color from the existing tokens so light mode and
future retints keep working for free.
Structural tokens: `--bg`, `--surface`, `--surface-2`, `--border`, `--text`,
`--muted`, and the warm orange brand `--accent` (`#f0803c`). The light-mode block
remaps the first six; the grade scales below are shared across both schemes.
### Grade palettes
These encode meaning, not decoration — keep their order and midpoints intact:
- **Temperature** — a 9-step diverging scale, cold → green → hot, chosen to be
colorblind-safe: `--rec-cold` `--very-cold` `--cold` `--cool` **`--normal`
(green midpoint)** `--warm` `--hot` `--very-hot` `--rec-hot`. The two `--rec-*`
ends are the "Near Record" danger tiers — deliberately dark and saturated.
- **Precipitation** — `--dry` plus `--wet-1`…`--wet-9` (light green → teal →
deep navy), with `--wet-5` the scale midpoint.
- **Seasons** — `--season-winter/-spring/-summer/-fall` for month/season chrome.
The inline-SVG charts (`static/chart.js`, and the SVG strings built in
`app.js`) pull from these same tokens, so a chart and its legend never diverge.
## Typography
`font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`.
**Inter is not self-hosted or imported** — it renders only where the OS has it,
otherwise the stack falls back to `system-ui`. Do not add a webfont link without
a deliberate decision (it's a network dependency on every page).
- Headings are tight: `h1` is 22px with `letter-spacing: -0.02em`.
- Section/eyebrow labels are small, uppercase, muted, and letter-spaced
(~12px, `letter-spacing: .04.05em`, `color: var(--muted)`).
- Body `line-height: 1.45`.
## Components
Match the existing recipes rather than inventing new ones:
- **Panels** — `.panel`: `--surface` background, `1px solid var(--border)`,
`border-radius: 14px`, generous padding. The primary content container.
- **Grade cards** — `.normal-card`: `--surface-2`, ~11px radius, the big value
tinted by its grade color via `color-mix(in oklab, …)`; hover lifts the border
to `--accent`.
- **Buttons** — accent background, dark text, `border-radius: 10px`,
`font-weight: 600`. Toggle chips (e.g. `.today-chip`) go outlined → filled when
active.
- **Segmented toggles** — unit (°C/°F) and metric switches; the active segment is
tinted by that metric's grade color (tmax → warm/hot reds, tmin → cold blues,
precip → wet blues).
- **Inputs** — `--surface` background, `1px solid var(--border)`, and
**`font-size: 16px` minimum** — smaller text makes iOS zoom on focus. This is a
hard rule, not a preference.
- **Charts** — bespoke inline SVG (percentile fan + median + value trace +
pointer-driven `.chart-tip`). No canvas, no charting library.
- **Map** — Leaflet in the shared modal picker (`mappicker.js`).
## Layout & responsive
Mobile-first: the base stylesheet is the phone layout; wider screens layer on via
`min-width` queries. Design and test in this order.
- **Phone** — single column. Primary phone breakpoint is **640/641px**; a few
tweaks at 560px. Keep ~44px touch targets and use pointer (not mouse-only)
events. The mobile header folds into a hamburger menu.
- **Large monitors** — the content column widens in real steps, it does **not**
stay a centered 1200px strip: `main` grows to 1440px at **1680px**, 1640px at
**2400px**, 1880px at **3400px** (`style.css` ~8999). Per-page grids (normals,
calendar months, day cards) flow into the extra room; charts scale with it.
- **Both color schemes** — dark is the default; light comes from
`prefers-color-scheme`. Every change must look right in both.
- Honor `prefers-reduced-motion: reduce` — gate non-essential animation behind it.
**Validate every visual change at 390 / 800 / 1920 / 2560 / 3840px in both light
and dark.** These viewport widths straddle the breakpoints above (phone, tablet,
1440p, 2K, 4K). Use `make shots` (below) to capture the full matrix.
## Conventions
- **Metric order is always `Precip · High · Low`** — chart legend, normal cards,
day rows, exported tables, everywhere.
- **Grades are relative, never absolute.** Use the percentile tier names
("Above Normal", "High", "Near Record"), never absolute-temperature words
("hot", "warm", "cold") — the same reading is Above Normal in a cool climate and
Below Normal in a hot one. This is a percentile against each place's own
climate history, not a thermometer reading (see `thermograph-backend`'s
`data/grading.py`'s `pct_ordinal()`, which `static/shared.js`'s `pctOrd()`
mirrors — see `CLAUDE.md`).
- The two chart temperature lines are labeled **"Daily high / Daily low"** so they
aren't confused with the **"High / Low"** percentile tiers.
## Viewing & iterating — `make shots`
There is no design without seeing it rendered. To view the running app across the
full breakpoint matrix, serve this repo's app (see `README.md`/`CLAUDE.md` for the
`uvicorn` invocation) and run the screenshot sweep:
```sh
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt # if not already set up
.venv/bin/pip install -q -r tools/requirements.txt
.venv/bin/python -m playwright install chromium
.venv/bin/python tools/shoot.py
```
`tools/shoot.py` drives headless Chromium over the served app and writes PNGs to
`.screenshots/` (gitignored), named `{page}@{width}-{scheme}.png` (e.g.
`index@390-dark.png`, `city@3840-light.png`). Read those PNGs back to see the
result, adjust `static/style.css`, and re-shoot.
Narrow the matrix while iterating on one thing:
```sh
.venv/bin/python tools/shoot.py index --width 390 --scheme dark
```
Point it at a different server with `SHOTS_BASE` (default
`http://127.0.0.1:8137/thermograph` — override to match wherever this repo's
`app.py` is actually serving, e.g. `http://127.0.0.1:8080` for a local run
without `THERMOGRAPH_BASE` set).