feat(web): adopt TRM design-system tokens (tokens only)

Linked Claude Design project "TRM — Tracking & Race Management" is a
race-timing kit, not a parking design. Adopted its TOKENS only — no TRM
components. Aligned the existing term-* accents onto TRM's exact night/
semantic values (surfaces → night scale; amber→#f2a516, green→#2e8c4a,
red→#e8412b flag, cyan→#2563c8 blue) so the whole booth UI shifts palette
with zero component edits. Exposed TRM's full vocabulary (night/ink/paper
scales, flag/amber/green/blue, viz-1..8, 4px spacing, type scale, square
radii, sharp offset shadows) as Tailwind v4 utilities for new work.

Offline appliance: dropped TRM's Google-Fonts @import (no runtime network);
Goldplay display face not self-hosted yet — falls back to a sans stack.

Web build green; login renders on the new palette.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-18 19:46:56 +02:00
parent dfa76346d6
commit 91cc79b14e
3 changed files with 170 additions and 17 deletions
+154 -16
View File
@@ -3,28 +3,159 @@
/* Bloomberg-terminal aesthetic: dense, dark, monospace, keyboard-first. /* Bloomberg-terminal aesthetic: dense, dark, monospace, keyboard-first.
Tailwind v4 — design tokens live here in @theme (no tailwind.config.js). Tailwind v4 — design tokens live here in @theme (no tailwind.config.js).
The booth runs on a fixed appliance display; we optimise for a dark room, The booth runs on a fixed appliance display; we optimise for a dark room,
glanceable status colour, and high information density over whitespace. */ glanceable status colour, and high information density over whitespace.
Token vocabulary adopted from the "TRM" design system (Tracking & Race
Management) — TOKENS ONLY: colours, type scale, spacing, radii, shadows.
None of TRM's race-timing components are used. The existing `term-*` accents
are aligned onto TRM's exact values so the whole booth UI inherits the TRM
palette without renaming a single class. TRM's full vocabulary is also
exposed as utilities (night-*, ink-*, paper-*, flag/amber/green/blue, the
spacing/type/shadow scales) for new work.
Offline appliance: NO webfont @import (no network at runtime). Goldplay (the
TRM display face) is not self-hosted yet — display/heading text falls back to
a clean sans stack; wire local Goldplay @font-face here if it's wanted. */
@theme { @theme {
/* Surfaces — near-black, layered greys for panels/borders. */ /* ============================================================
--color-term-bg: #0a0e12; TERMINAL ACCENTS — aligned onto TRM's exact values.
--color-term-panel: #11161c; These keep their `term-*` names (used across every screen),
--color-term-panel-2: #161d25; but now resolve to TRM colours so the palette is unified.
--color-term-border: #232c37; ============================================================ */
--color-term-muted: #6b7785; /* Surfaces — TRM "night" (trackside dark) scale. */
--color-term-text: #c9d3de; --color-term-bg: #0b0d10; /* TRM --night */
--color-term-panel: #14171c; /* TRM --night-2 */
--color-term-panel-2: #1e222a; /* TRM --night-3 */
--color-term-border: #2a2f38; /* TRM --night-line */
--color-term-muted: #8a8a82; /* TRM --night-fg-3 / --ink-4 */
--color-term-text: #f2f2ee; /* TRM --night-fg */
/* Status accents — the terminal's signal colours. */ /* Status accents — TRM semantic colours. */
--color-term-amber: #f5a623; /* primary accent / headings / focus */ --color-term-amber: #f2a516; /* TRM --amber (caution / accent / focus) */
--color-term-green: #2ecc71; /* entry / ok / free */ --color-term-green: #2e8c4a; /* TRM --green (entry / ok / free) */
--color-term-red: #ff4d4f; /* exit / fault / full */ --color-term-red: #e8412b; /* TRM --flag (exit / fault / full) */
--color-term-cyan: #38bdf8; /* payment / info */ --color-term-cyan: #2563c8; /* TRM --blue (payment / info / live) */
/* Monospace stack — IBM Plex Mono / JetBrains first, system mono fallback. */ /* ============================================================
TRM FULL VOCABULARY — exposed as Tailwind utilities for new work.
============================================================ */
/* Ink & paper (light surfaces — for any light-on-dark inversions). */
--color-paper: #fafaf7;
--color-paper-2: #f2f2ee;
--color-paper-3: #e8e8e2;
--color-ink: #0e0e0c;
--color-ink-2: #2a2a26;
--color-ink-3: #5a5a53;
--color-ink-4: #8a8a82;
--color-ink-5: #b8b8b0;
--color-ink-6: #dcdcd4;
/* Night scale (the booth's working surfaces). */
--color-night: #0b0d10;
--color-night-2: #14171c;
--color-night-3: #1e222a;
--color-night-line: #2a2f38;
--color-night-fg: #f2f2ee;
--color-night-fg-2: #b8b8b0;
--color-night-fg-3: #8a8a82;
/* Race accents + semantic. */
--color-flag: #e8412b;
--color-flag-2: #c8331f;
--color-flag-tint: #fbe3de;
--color-amber: #f2a516;
--color-amber-2: #c88500;
--color-amber-tint: #fbefd0;
--color-green: #2e8c4a;
--color-green-2: #1f6a36;
--color-green-tint: #ddefe2;
--color-blue: #2563c8;
--color-blue-2: #1a4fa8;
--color-blue-tint: #dce6f8;
--color-violet: #6b46c1;
--color-magenta: #c9296f;
--color-teal: #188c8a;
--color-ok: #2e8c4a;
--color-warn: #f2a516;
--color-danger: #e8412b;
--color-info: #2563c8;
/* Data-viz categorical (8). */
--color-viz-1: #e8412b;
--color-viz-2: #2563c8;
--color-viz-3: #2e8c4a;
--color-viz-4: #f2a516;
--color-viz-5: #6b46c1;
--color-viz-6: #188c8a;
--color-viz-7: #c9296f;
--color-viz-8: #5a5a53;
/* ---------- TYPE — families ---------- */
/* Mono is the booth's primary face (data-dense, tabular). Display/UI fall
back to a clean sans (Goldplay not self-hosted — see header note). */
--font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SFMono-Regular", --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SFMono-Regular",
"Menlo", "Consolas", monospace; "Menlo", "Consolas", monospace;
--font-display: "Goldplay", "Helvetica Neue", Arial, sans-serif;
--font-ui: "Goldplay", "Helvetica Neue", Arial, sans-serif;
--font-body: "Inter", "Helvetica Neue", Arial, sans-serif;
/* Tight radius — terminals are square. */ /* ---------- TYPE — scale (TRM, optimised for data density) ---------- */
--radius-term: 2px; --text-overline: 11px;
--text-micro: 12px;
--text-small: 13px;
--text-body: 15px;
--text-lead: 17px;
--text-h6: 14px;
--text-h5: 16px;
--text-h4: 20px;
--text-h3: 26px;
--text-h2: 34px;
--text-h1: 48px;
--text-display: 72px;
--text-jumbo: 120px;
/* ---------- SPACING (TRM 4px base) ---------- */
--spacing-s0: 0;
--spacing-s1: 2px;
--spacing-s2: 4px;
--spacing-s3: 8px;
--spacing-s4: 12px;
--spacing-s5: 16px;
--spacing-s6: 20px;
--spacing-s7: 24px;
--spacing-s8: 32px;
--spacing-s9: 40px;
--spacing-s10: 48px;
--spacing-s11: 64px;
--spacing-s12: 80px;
--spacing-s13: 96px;
/* ---------- RADIUS — TRM is square-edged ---------- */
--radius-term: 2px; /* existing alias, kept */
--radius-r0: 0;
--radius-r1: 2px;
--radius-r2: 4px;
--radius-r3: 6px;
--radius-r4: 10px;
/* ---------- ELEVATION — TRM sharp "printed" offset shadows ---------- */
--shadow-term-1: 0 1px 0 0 #0e0e0c;
--shadow-term-2: 2px 2px 0 0 #0e0e0c;
--shadow-term-3: 4px 4px 0 0 #0e0e0c;
--shadow-soft: 0 1px 2px rgba(14, 14, 12, 0.06), 0 4px 12px rgba(14, 14, 12, 0.04);
--shadow-pop: 0 8px 24px rgba(14, 14, 12, 0.12);
/* ---------- MOTION (TRM) ---------- */
--ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
/* ---------- COMPONENT TOKENS (TRM control heights, table rows) ---------- */
--control-h-sm: 28px;
--control-h-md: 36px;
--control-h-lg: 44px;
--table-row-h: 36px;
--table-row-h-dense: 28px;
} }
html, html,
@@ -45,6 +176,13 @@ body {
overscroll-behavior: none; overscroll-behavior: none;
} }
/* Tabular numerics everywhere — counts, money, clocks must not jitter. */
.num,
.tabular {
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
/* Terminal scrollbars — thin, dark, unobtrusive. */ /* Terminal scrollbars — thin, dark, unobtrusive. */
* { * {
scrollbar-width: thin; scrollbar-width: thin;
+6 -1
View File
@@ -24,7 +24,12 @@ The operator UI outgrew "plain React + useState" once it needed live updates and
the latest pushed occupancy. Anything durable is re-fetched via Query. the latest pushed occupancy. Anything durable is re-fetched via Query.
- **Tailwind v4** with a **"Bloomberg-terminal" theme** (`apps/web/src/index.css`, `@theme`): - **Tailwind v4** with a **"Bloomberg-terminal" theme** (`apps/web/src/index.css`, `@theme`):
near-black surfaces, amber/green/red/cyan status accents, monospace, dense/keyboard-first. **Radix** near-black surfaces, amber/green/red/cyan status accents, monospace, dense/keyboard-first. **Radix**
primitives (Dialog, etc.) for accessible unstyled components. primitives (Dialog, etc.) for accessible unstyled components. The token VALUES are adopted from the
**"TRM" design system** (Claude Design project; race-timing kit) — **tokens only**, no TRM
components: the `term-*` accents are aligned onto TRM's `night`/semantic colours and TRM's full
vocabulary (night/ink/paper scales, flag/amber/green/blue, the spacing/type/shadow scales) is
exposed as utilities for new work. Offline appliance ⇒ no webfont `@import`; Goldplay (TRM's display
face) not self-hosted yet — display text falls back to a sans stack.
- **react-i18next** for [[i18n]] (Albanian default). - **react-i18next** for [[i18n]] (Albanian default).
> This SUPERSEDES the original "plain React, no framework" note on [[react-vite-spa]] — that held > This SUPERSEDES the original "plain React, no framework" note on [[react-vite-spa]] — that held
+10
View File
@@ -854,3 +854,13 @@ The version selector picks "latest tariff_version with effectiveFrom ≤ session
The owner asked for "first N hours × X, next N hours × Y, …, 24h cap" — which the stepped-block engine ALREADY does (ordered blocks, per-block rate, rolling-24h cap; computeFee tested). So no new axis: the work was making the model complete + footgun-free. Two changes. (1) **Forbid a bounded last block** — `validateTariffStructure` (shared) now rejects a final block with a non-null `uptoMin`, so the "thereafter" rate is always explicit; previously a bounded tail silently inherited its own rate past its bound (a hidden, never-stated price — e.g. the live ALL tariff's 180-min last block billed hour 4+ at the 3rd-hour rate). `rateAt()` still prices legacy bounded-tail versions gracefully and validation is publish-only, so immutable published versions are unaffected (no migration). (2) **Composer edits bands as a DURATION in hours**, not cumulative minutes — `BlockForm` carries `hours`; `toStructure` accumulates into cumulative `uptoMin` minutes; the last row is a pinned, non-removable, hours-less "thereafter (open-ended)" band; `blocksToForm` round-trips stored minutes back to band hours (legacy bounded tails still load). i18n: replaced `upToMin`/`egExample` with `bandDuration`/`hoursUnit`/`egHours` in sq+en (catalog parity green). Verified: validator rejects bounded-last / accepts open-ended; computeFee correct at 1/2/3/5/6/24h for a 0-2h@200,2-5h@100,5h+@50 + 1000 cap card. Full build green (shared/server/web). Updated [[tariff]]. No migration. The owner asked for "first N hours × X, next N hours × Y, …, 24h cap" — which the stepped-block engine ALREADY does (ordered blocks, per-block rate, rolling-24h cap; computeFee tested). So no new axis: the work was making the model complete + footgun-free. Two changes. (1) **Forbid a bounded last block** — `validateTariffStructure` (shared) now rejects a final block with a non-null `uptoMin`, so the "thereafter" rate is always explicit; previously a bounded tail silently inherited its own rate past its bound (a hidden, never-stated price — e.g. the live ALL tariff's 180-min last block billed hour 4+ at the 3rd-hour rate). `rateAt()` still prices legacy bounded-tail versions gracefully and validation is publish-only, so immutable published versions are unaffected (no migration). (2) **Composer edits bands as a DURATION in hours**, not cumulative minutes — `BlockForm` carries `hours`; `toStructure` accumulates into cumulative `uptoMin` minutes; the last row is a pinned, non-removable, hours-less "thereafter (open-ended)" band; `blocksToForm` round-trips stored minutes back to band hours (legacy bounded tails still load). i18n: replaced `upToMin`/`egExample` with `bandDuration`/`hoursUnit`/`egHours` in sq+en (catalog parity green). Verified: validator rejects bounded-last / accepts open-ended; computeFee correct at 1/2/3/5/6/24h for a 0-2h@200,2-5h@100,5h+@50 + 1000 cap card. Full build green (shared/server/web). Updated [[tariff]]. No migration.
NB considered-and-rejected: time-of-day / weekday wall-clock tiers ("timeframe") were offered but the owner explicitly chose the elapsed-duration ladder only — see [[tariff-time-tiers]] for the deferred wall-clock axis. NB considered-and-rejected: time-of-day / weekday wall-clock tiers ("timeframe") were offered but the owner explicitly chose the elapsed-duration ladder only — see [[tariff-time-tiers]] for the deferred wall-clock axis.
## [2026-06-18] feat | Tariff V2 — legacy-parity pricing (time-of-day, category, seasonal, flat)
Brought the legacy ParkSQL2017 pricing BREADTH onto our engine (keeping integer-minor-unit money + immutable signed versions; rejecting legacy float money / mutable rows). `TariffStructure` is now a discriminated union: V1 = the original bare ladder (UNCHANGED, verbatim algorithm — golden-regression-tested against the live production version); V2 = `{version:2, tz, <shared knobs>, defaultCard, windowedCards[]}` where each card is flat OR a block ladder and may be scoped by wall-clock hour window / day-of-week / date range / vehicle category. computeFeeV2 prices by stepping one increment at a time, advancing the ladder by ELAPSED minutes (continuous) while selecting the active card by WALL-CLOCK time in the version's FROZEN tz. Decisions (with user): tz is a per-site setting (site_config.timezone, default Europe/Tirane) stamped server-side into each version on publish — never the host clock (reproducibility); default-card cap governs a mixed day; precedence = specificity (date>dow>hour) → priority → name (total, order-independent), validation rejects ambiguous ties; category = a card FIELD (not tariff scope), frozen in the signed vehicle_entry payload (site_config.default_vehicle_category default), read at both pricing call-sites. Composer: default card front-and-centre (flat/ladder toggle), tiers under an "Advanced" disclosure (window builder), emits BARE V1 when no tiers (back-compat). DB: migrations 0005 (timezone) + 0006 (default_vehicle_category) — applied to the live DB (backed up). Stood up vitest in @parking/shared (was zero tests on the ledger-feeding fee fn); 36 tests incl. golden V1 regression, happy-hour/overnight/dow/flat/category/cap edges, precedence shuffle-invariance, Europe/Tirane DST determinism, validation matrix — all green. Full monorepo build green; V2 publish verified end-to-end via inject (tz stamped from config not client; malformed V2 rejected). Updated [[tariff-time-tiers]] (status open→settled, as-built), [[tariff]], index. No event-chain change.
NB incident: the running tsx-watch dev server (server + vite) crashed mid-edit on a half-saved file + a schema column the live DB lacked; recovered by finishing the edits, applying the migration to the live DB, and restarting both watchers. Live DB backup left at apps/server/parking.sqlite.bak-*.
## [2026-06-18] feat | Booth UI — adopted "TRM" design-system tokens (tokens only)
The owner linked a Claude Design project (`019ddfee-…`, "TRM — Tracking & Race Management") and asked to implement its designs in `apps/web/`. TRM is a RACE-TIMING design system (dashboard/leaderboard/marketing/mobile kits: RaceControl, HeroClock, LiveTable, BibCard, Ticker) — NOT a parking design; literally porting it would have reskinned the booth UI with race components. Flagged the mismatch; owner chose **tokens only**. So: brought TRM's token VOCABULARY into the Tailwind v4 `@theme` (`apps/web/src/index.css`) and ALIGNED the existing `term-*` accents onto TRM's exact values — surfaces → TRM `night` scale (#0b0d10/#14171c/#1e222a/#2a2f38), amber #f5a623→#f2a516, green #2ecc71→#2e8c4a, red #ff4d4f→#e8412b (flag), cyan #38bdf8→#2563c8 (blue). No component touched (170+ `term-*` references resolve unchanged). Also exposed TRM's full vocabulary as utilities for new work: night/ink/paper scales, flag/amber/green/blue + tints, viz-1..8, the 4px spacing scale (s0..s13), type scale (overline..jumbo), square radii, sharp "printed" offset shadows, control/table-row heights. Offline-appliance constraint ⇒ deliberately did NOT keep TRM's Google-Fonts `@import` (no runtime network); Goldplay (TRM display face) left un-self-hosted — display/heading falls back to a sans stack (mono is the booth's primary face anyway), noted for later wiring. Verified: web build green; login renders on the new palette (amber focus ring = #f2a516). Updated [[booth-console]]. No logic/schema/event-chain change.