134 lines
6.6 KiB
Markdown
134 lines
6.6 KiB
Markdown
# 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` ~89–99). 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).
|