forked from c-fu/arah2
301 lines
16 KiB
Markdown
301 lines
16 KiB
Markdown
<!-- 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. --> |