Files
arah2/theme/README.md
2026-10-23 19:18:43 +08:00

301 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- generated-by: gsd-doc-writer -->
# @araharah/theme
The **Telemetry Rams** design system as a Tailwind v4 CSS-first token package: every color, spacing, type, and radius value in arah² is defined once in `src/tokens.css`, compiled to `dist/rams.css`, and consumed by `@araharah/web`. The package also hosts the MapLibre basemap style fork, the self-hosted typefaces, and the audit / screenshot evidence tooling.
Part of the [arah² monorepo](../README.md) — see the root [README](../README.md) and [`docs/`](../docs/) for bot and Mini App context.
---
## Contents
| Path | What it is |
| --- | --- |
| `src/tokens.css` | Single source of truth for every numeric design value: `@theme static` color / spacing / font / radius tokens plus the `--type-*` typography scale |
| `src/theme.css` | Entry stylesheet — imports Tailwind, `tokens.css`, `components.css`; declares the self-hosted `@font-face` rules and the Telegram light/dark shell slots |
| `src/components.css` | Component layer (`@layer components`): buttons, inputs, telemetry cards, event chips, maneuver banner, checkbox, toggle, and the `.text-*` typography helpers |
| `src/sheet.html` | Component sheet — the rendered proof that every component derives from tokens |
| `gallery.html` | DSGN-02 approval gallery: mockup reference render vs. implementation capture, side by side |
| `basemap/build.mjs` | Builds `basemap/liberty-rams.json` by forking the OpenFreeMap `liberty` style and deriving every cartographic color from `tokens.css` |
| `basemap/liberty-rams.json` | The generated MapLibre style — imported directly by `web/src/map/map.ts` |
| `basemap/index.html` | MapLibre GL proof page over KL, with a placeholder route vector |
| `scripts/audit.mjs` | Off-token audit: scans rendered CSS and flags any value that does not derive from `tokens.css` |
| `scripts/audit.test.mjs` | TDD harness for the audit (12 contract tests, zero dependencies) |
| `scripts/screenshot.mjs` | Playwright capture toolchain — writes `screenshots/` |
| `fonts/geist/`, `fonts/space-grotesk/` | Self-hosted woff2 subsets (SIL Open Font License 1.1) |
```mermaid
graph TD
TOK["src/tokens.css — colors, spacing, type, radius"]
COMP["src/components.css — component classes"]
ENTRY["src/theme.css — entry stylesheet"]
CLI["@tailwindcss/cli"]
DIST["dist/rams.css — built stylesheet"]
BUILD["basemap/build.mjs"]
STYLE["basemap/liberty-rams.json"]
WEB["@araharah/web — Mini App shell"]
AUDIT["scripts/audit.mjs"]
SHOTS["scripts/screenshot.mjs"]
GAL["gallery.html + src/sheet.html + basemap/index.html"]
TOK --> COMP
TOK --> ENTRY
COMP --> ENTRY
ENTRY --> CLI
CLI --> DIST
TOK --> BUILD
BUILD --> STYLE
DIST --> WEB
STYLE --> WEB
TOK --> AUDIT
GAL --> AUDIT
GAL --> SHOTS
SHOTS --> GAL
classDef tokens fill:#EB8008,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
classDef build fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
classDef out fill:#F5EDE0,stroke:#736B1E,stroke-width:2px,color:#1E1B14
class TOK,COMP,ENTRY tokens
class CLI,BUILD,AUDIT,SHOTS build
class DIST,STYLE,WEB,GAL out
```
---
## Installation
`@araharah/theme` is a workspace package — it is never published. Consumers depend on it by name and install from the monorepo root:
```bash
git clone http://gitmirah:3000/c-fu/arah2.git
cd arah2
npm install
```
Requires Node.js and npm. `basemap/build.mjs` needs Node >= 18 (global `fetch`), and `basemap:preview` shells out to `python3`.
<!-- VERIFY: Node.js minimum version — no `engines` field or `.nvmrc` exists in the repository; `basemap/build.mjs` documents `Node >= 18` in its header comment. -->
---
## Quick start
```bash
npm run build -w theme # src/theme.css -> dist/rams.css (minified)
npm run sheet # build, then open theme/src/sheet.html in a browser
npm run audit # audit the rendered CSS for off-token values
```
---
## Scripts
| Command | Description |
| --- | --- |
| `npm run build -w theme` | `tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify` |
| `npm run send -w theme` | Copy `fonts/geist` and `fonts/space-grotesk` into `dist/fonts/` so the built stylesheet ships with its typefaces |
| `npm run sheet -w theme` | `build`, then print the path to `src/sheet.html` (open it in a browser) |
| `npm run gallery -w theme` | `build` + `capture`, then serve the workspace on `:8123` and block on `basemap:preview` |
| `npm run basemap -w theme` | `node basemap/build.mjs` — fetch OpenFreeMap `liberty`, derive the Rams cartography, write `basemap/liberty-rams.json` |
| `npm run basemap:preview -w theme` | `python3 -m http.server 8123` — serve `theme/` as docroot so `dist/rams.css`, the woff2 fonts, and `basemap/` resolve |
| `npm run audit -w theme` | `node ./scripts/audit.mjs ./dist/rams.css` — off-token audit |
| `npm run capture -w theme` | `node ./scripts/screenshot.mjs` — Playwright captures into `theme/screenshots/` |
| `npm test -w theme` | `node ./scripts/audit.test.mjs` — the audit's contract tests |
The root `package.json` delegates `sheet`, `gallery`, `audit`, and `capture` to this workspace, so `npm run audit` from the root is the same command.
### Testing
`npm test` runs a plain Node harness — no test framework, no dev-server dependency. `audit.test.mjs` spawns `audit.mjs` against fixtures under `/tmp/opencode/audit-test/` and asserts exit codes:
| Contract | Expectation |
| --- | --- |
| T1 | Stylesheet using only token values → exit `0` |
| T2 | Off-token color / non-zero radius / off-token font / shadow / blur / backdrop-filter → exit `1`, each listed as `file:line: declaration` |
| T3 | `tokens.css` drifting below 40 colors → exit `2` |
| T4 | Any `--radius-*` token other than zero → exit `2` |
| T5 | The real `dist/rams.css` build → exit `0` |
| T6 | Off-token values inside `@layer components` → flagged |
| T7 | `@layer utilities` only audits classes the implementation pages actually wear |
| T8 | Inline `style="…"` attributes are audited too |
| T9 | The deprecated bare `--radius` key drives bare `rounded` at zero |
| T10–T12 | Literal `font:` shorthands, named colors / `oklch()` and friends, and spacing + typography numerics are flagged |
`audit.mjs` exit codes: `0` clean, `1` violations (each printed as `file:line: declaration`), `2` token-sheet drift or an error.
### Coverage
No coverage threshold configured. The audit itself is the enforcement mechanism: it is a token-conformance gate, not a line-coverage gate.
---
## Usage
### Importing the built stylesheet
`@araharah/web` pulls the package in from its shell stylesheet:
```css
/* web/src/shell.css */
@import "@araharah/theme/dist/rams.css";
```
That single import brings the full token layer (`@theme static`, so every token survives compilation as a plain CSS custom property), the `@layer components` class API, the self-hosted `@font-face` rules, and the Telegram light/dark shell slots `--shell-bg` / `--shell-on-surface`.
### Consuming tokens in your own CSS
Because `@theme static` forces every token into the output, tokens are addressable as custom properties from anywhere — no Tailwind build step needed:
```css
.surface {
background: var(--color-surface);
border: 1px solid var(--color-matte-black);
padding: var(--spacing-space-md);
border-radius: var(--radius-md); /* resolves to 0 — Rams forbids roundedness */
}
.headline {
font: var(--type-headline-md);
letter-spacing: var(--type-headline-md-tracking);
}
```
Note the convention: `font:` always takes the compound `--type-*` token and letter-spacing takes the `-tracking` companion, because the CSS `font` shorthand cannot carry tracking.
### Component classes
`src/components.css` is the public component API. Every declaration in it is a token reference; the only literals are the machined border geometry (1px / 2px rules, 3px focus brackets) and the exact 18×18 checkbox square.
```html
<button class="btn-primary">Recenter</button>
<button class="btn-secondary">Layers</button>
<label class="checkbox"><input type="checkbox" checked></label>
<div class="toggle">
<button class="toggle-option" aria-pressed="true">Off</button>
<button class="toggle-option" aria-pressed="false">On</button>
</div>
<div class="card">
<div class="card-header"><span>Traffic</span><span class="card-ref">KL-01</span></div>
<div class="card-content"><span class="text-telemetry-num">2.4</span><span class="text-label-sm">KM AHEAD</span></div>
</div>
<span class="chip-traffic">Congestion</span>
<span class="chip-hazard">Crash</span>
<span class="chip-safe">Clear</span>
<div class="maneuver-banner">
<span class="maneuver-arrow">→</span>
<span class="text-label-sm">STEP</span>
<span class="text-telemetry-num">320 m</span>
</div>
```
### Importing the basemap style
```ts
// web/src/map/map.ts
import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";
```
The style object is never re-tinted at runtime — MapLibre receives it as-is.
---
## How the tokens are defined
`src/tokens.css` holds every value and nothing else does. Its header records the vocabulary as copied verbatim from the frontmatter of `stitch_minimalist_route_navigator/telemetry_rams/DESIGN.md`, so mockup markup maps 1:1 onto utilities (`bg-signal-orange`, `px-margin`, `gap-space-md`).
- **`@theme static`** — Tailwind v4 tree-shakes unused theme variables by default; `static` forces every token into the compiled output so the package stays consumable as plain CSS custom properties, including by the basemap derivation script.
- **Radius at absolute zero** — every `--radius*` key is `0px`. The deprecated bare `--radius` key is set too, because that is the key v4 drives bare `rounded` from.
- **Rams-forbidden primitives disabled at the key** — `--shadow`, `--shadow-inner`, `--drop-shadow`, and `--blur` are all set to `initial`, so `shadow-*` / `blur-*` utilities cannot compile from any source.
- **Typography** — `--type-*` compound tokens follow the grammar `weight size/line-height family…`, each paired with a `-tracking` companion.
- **`--shell-bg` / `--shell-on-surface`** — declared in `src/theme.css`, mapping the OS color scheme onto shell-level surfaces only; components and cartography stay Rams-constant under both Telegram themes.
The four mockup screens under `stitch_minimalist_route_navigator/` are the read-only class vocabulary reference. `src/theme.css` lists them as explicit `@source` paths and excludes `scripts/` and `basemap/` from the auto-scan, so prose can never mint a utility class. The mockup files themselves are never edited.
---
## Audit
`scripts/audit.mjs` treats "zero off-token values" as evidence rather than an eyeball claim. It parses `src/tokens.css` into an allowlist, then scans **rendered** CSS — `dist/rams.css` plus the inline `<style>` blocks and `style="…"` attributes of every registered implementation page — and flags:
- hex / `rgb()` / `hsl()` / named-keyword / `oklch()`-family color literals that are not token values
- font families outside the sanctioned stacks, and any literal `font:` shorthand
- spacing numerics on `padding` / `margin` / `gap` that do not equal a `--spacing-*` token
- typography numerics (`font-size`, `font-weight`, `line-height`, `letter-spacing`) outside the `--type-*` scale
- any non-zero `border-radius`, any non-`none` shadow, any blur or `backdrop-filter`
Seven scope decisions are documented in the script header; the notable ones: Tailwind's own `@layer properties / theme / base` are plumbing and skipped, `@layer utilities` rules are only audited when the selector names a class a registered implementation page actually wears, and `rounded-full`'s full-circle primitive is treated as spec geometry like the 18×18 checkbox square.
The page registry lives at `scripts/audit.mjs:118` — `IMPLEMENTATION_PAGES`. Any new page that renders Rams components must be appended there, or the audit will exit `2` on a missing path.
```bash
npm run audit -w theme
node scripts/audit.mjs --tokens ./src/tokens.css --pages src/sheet.html ./dist/rams.css
```
---
## Basemap derivation
`basemap/build.mjs` fetches the OpenFreeMap `liberty` vector style and forks it into Telemetry Rams cartography. Every cartographic color is derived from `tokens.css` inside the script — there are no hand-tuned hexes in the output JSON, only the derivation parameters (saturation multipliers, per-role lightness targets).
- **Derivation core** — `desaturate(tokenHex, satMultiplier, lightness)`: hue genes come from the source token, saturation is clamped into a calibrated `[0.06, 0.12]` band, lightness snaps to the role target. Ground is `mix(soft-beige, chassis-subtle)`.
- **Accent guard** — `signal-orange`, `functional-red`, and `olive-green` are excluded by token name *and* by hex value; the build fails if any of the three (hex or `rgb()` form) appears in the output layers. Those accents are reserved for the route line and hazard semantics that later phases render.
- **Hierarchy by weight only** — zoom-interpolated color ramps flatten to role constants while width/opacity ramps are preserved.
- **Labels** — matte-black at reduced opacity with a ground-tone halo and `text-halo-blur: 0`.
- **`building-3d` removed** — fill-extrusion is faux 3D, rejected by the design system. The removal is recorded in `style.metadata.rams-removed-layers`.
- **Font probing** — the intended Rams label stacks are probed against the OpenFreeMap glyphs endpoint at build time. When they are not hosted (the Phase 1 case), the closest hosted stack is substituted and the substitution is recorded in `style.metadata.rams-font-substitutions` as honest fallback provenance.
The generated style also carries a full derivation report in `style.metadata.rams-cartography`: every role with its color and the token recipe behind it, the excluded accents, and the list of untouched layers.
```bash
npm run basemap -w theme # rebuild liberty-rams.json (needs network)
npm run basemap:preview -w theme # serve theme/ on http://localhost:8123/basemap/
```
---
## Gallery and captures
`scripts/screenshot.mjs` launches chromium, serves `theme/` on an ephemeral loopback port as docroot, and writes six PNGs to `theme/screenshots/` (gitignored):
| Capture | Source | Viewport |
| --- | --- | --- |
| `sheet-full.png` | `src/sheet.html` | 390 px, full page |
| `mockup-<screen>.png` ×4 | the four mockup screens via `file://` | 390 px |
| `basemap-kl.png` | `basemap/index.html` | 1280 × 800 |
The mockup renders need network (CDN Tailwind and Google fonts in the read-only mockups). On capture failure or an undersized / blank shot, the committed `screen.png` for that screen is copied in as a fallback — the mockups are reference material and are never edited. The basemap capture waits for the proof page's own `RAMS-BASEMAP-READY` console signal before settling on tiles. Missing, undersized (< 10 KB), or failed captures exit `1`.
```bash
npm run capture -w theme
npm run gallery -w theme
# → http://localhost:8123/gallery.html and http://localhost:8123/basemap/
```
`gallery.html` is the approval-gate mechanism template: every later UI phase adds one comparison row (mockup reference render on the left, Playwright implementation capture on the right). It is CDN-free by construction — only the built stylesheet plus local captures.
`npm run gallery` ends with a **blocking** `basemap:preview` server. Stop it with `Ctrl-C`.
---
## Notes
- `dist/` and `screenshots/` are gitignored build output. After a fresh checkout, run `npm run build -w theme` before opening `src/sheet.html` or `gallery.html`.
- Typefaces are self-hosted woff2 subsets under `fonts/` — no Google Fonts CDN. `font-display: block` is deliberate: no fallback-font flash.
- MapLibre GL v4 is loaded from a CDN on `basemap/index.html`; the product build wires its own copy.
- Windows `*:Zone.Identifier` metadata files may be present in the tree — exclude them from any tooling.
## Links
- Root [README](../README.md) — monorepo overview
- [`docs/GETTING-STARTED.md`](../docs/GETTING-STARTED.md) · [`docs/DEVELOPMENT.md`](../docs/DEVELOPMENT.md) · [`docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) · [`docs/TESTING.md`](../docs/TESTING.md)
- `stitch_minimalist_route_navigator/telemetry_rams/DESIGN.md` — the design-system document the token vocabulary is copied from
- `DESIGN.md` — Telemetry Rams philosophy
<!-- VERIFY: the private Gitea clone URL (`http://gitmirah:3000/c-fu/arah2.git`) is taken from the user-provided facts and is not verifiable from repository contents alone. -->