RomM v2 — Tokens, Theming & Visual Language
Tokens are the only source of truth for theming. Zero hex/rgba() literals in v2 components. If a value is missing, add a token. Every component must work in both v2-dark and v2-light.
Token pipeline
src/v2/tokens/index.ts is the source. It feeds two consumers:
src/v2/styles/tokens.css — generated by scripts/build-tokens.ts (npm run build:tokens, hooked into predev/prebuild). Do not hand-edit. This is how the vast majority of tokens are consumed: var(--r-color-...) in CSS.
- Direct JS/TS imports of named exports (
colorCanvas, colorCoverArt, layout, …) for the few cases needing a token value in JavaScript — baking colors into an SVG string (utils/covers), canvas/QR backgrounds (Player/Ruffle.vue, ShowQRCodeDialog), and the virtualiser's pixel math (Gallery/listColumns reading layout).
v2 has no Vuetify theme of its own. tokens.css emits a palette block per theme under .r-v2.r-v2-dark / .r-v2.r-v2-light; RomM.vue toggles those classes on <html>. v2 surfaces never read Vuetify's runtime theme.
Caveat: a wrapped Vuetify component still resolves color="primary" against Vuetify's own registered themes (src/plugins/vuetify.ts, sourced from v1's @/styles/themes). They mirror the brand tokens by hand (both #8B74E8), so they line up, but it's a parallel source. Prefer var(--r-...) over the color prop.
Adding a new token
- Add it to
src/v2/tokens/index.ts with a semantic, role-based name (--r-color-danger, not --r-color-red).
- Provide both dark and light values.
- Consume via
var(--r-...) (CSS) or the named export (JS).
- Run
npm run build:tokens to regenerate tokens.css.
- If the JS→CSS variable name needs an exception (e.g.
--r-nav-h), add an entry to NAME_OVERRIDES in the generator.
Where the scope classes live — and why <html>
.r-v2, .r-v2-dark, .r-v2-light go on <html> (RomM.vue toggles them whenever uiVersion or the active theme changes). Vuetify teleports overlays (VDialog, VMenu, VTooltip) to <body> > .v-overlay-container, outside <v-app>. Only <html> covers both the regular tree and the teleports — without it, overlays lose their tokens.
Diagnostics — when var(--r-color-...) resolves to nothing on an overlay
- Check
RomM.vue's watch on documentElement.classList (load-bearing).
- Check that the teleported component carries a
content-class tying it back to scope (e.g. RDialog uses content-class="r-dialog").
- Never "fix" it by swapping the token for a hex literal — that hides the bug and breaks the dual theme.
Visual language
- Single visual vocabulary. Every surface (dialog, menu, popover, card, toolbar) reads as a sibling — same blur, curvature, depth. No standalone "dialog look" vs "menu look".
- Canonical references when designing: ask the user before consulting
https://mockup.thebirdcage.tv/. They decide whether the mockup or existing primitives take priority.
- State semantics are shared across primitives (don't reinvent per component):
- hover (neutral, or brand-tinted on selected rows)
- selected/checked (
--r-color-brand-primary)
- active/favorite (
--r-color-fav)
- focus (modality-gated; visible only on
key/pad — see frontend-v2-input)
- busy/pending · disabled
- Implementation gotchas:
RDialog ships unscoped <style> at the bottom that strips Vuetify defaults from .v-overlay__content. Load-bearing — without it the --r-radius-card corners disappear.
- Every dialog goes through
RDialog; every menu through RMenu.
Color-literal policy: zero exceptions
Outside src/v2/tokens/index.ts (the source-of-truth TS module) and the generated src/v2/styles/tokens.css, no hex or rgba() literals exist anywhere in v2. Everything previously "excepted" is now a token or a color-mix:
- Cover-overlay glass →
--r-color-overlay-* (fixed dark glass; never theme-flips).
- Cover artwork placeholder & shimmer →
--r-color-cover-placeholder, --r-color-cover-placeholder-bright.
- Panel / tooltip / shimmer-sweep →
--r-color-panel, --r-color-panel-border, --r-color-tooltip-bg, --r-color-shimmer-sweep.
- Backdrop scrims (
global.css) → color-mix(in srgb, var(--r-color-bg) X%, transparent).
- Status tints →
color-mix(in srgb, var(--r-color-status-base-{success,warning,danger,info}) X%, transparent).
- Brand-tinted backgrounds (selected rows, focus rings) →
color-mix(in srgb, var(--r-color-brand-primary) X%, transparent).
- Black/white shadows →
color-mix(in srgb, black X%, transparent) (CSS named color, not a hex literal).
- Metadata-provider chips →
--r-color-provider-*. Player canvas → --r-color-canvas-bg, --r-color-canvas-bg-deep.
- Emphasis pill (always-white-on-dark "Play" CTA over cover art) →
--r-color-overlay-emphasis-bg/-fg/-bg-hover.
If a literal would otherwise be needed, the answer is: add a token (steps above), then consume via var(--r-color-...) or the named export.
Style conventions
- Scoped
<style> by default; unscoped only for teleport overrides.
- BEM-ish class names:
.feature__element--modifier. Prefixes: .r-v2-... for app-shell surfaces outside components; .r-... for globally shared utilities/tokens.
- No plain CSS where a Vuetify utility class covers the case (
d-flex, pa-4, align-center).
1---2name: frontend-v2-theming3description: Theming, design tokens, colors, and visual language in the RomM v2 frontend. Use when styling v2 components, picking colors, adding/using CSS variables, working with light/dark themes, or whenever you'd reach for a hex/rgba literal. Covers the token pipeline (src/v2/tokens/index.ts → build:tokens → tokens.css), the .r-v2 scope classes, the zero-hex-literal policy, and shared state semantics. Trigger on any color/theme/token work under frontend/src/v2/.4---56# RomM v2 — Tokens, Theming & Visual Language78**Tokens are the only source of truth for theming. Zero hex/`rgba()` literals in v2 components.** If a value is missing, add a token. Every component must work in both `v2-dark` and `v2-light`.910---1112## Token pipeline1314`src/v2/tokens/index.ts` is the source. It feeds two consumers:1516- **`src/v2/styles/tokens.css`** — _generated_ by `scripts/build-tokens.ts` (`npm run build:tokens`, hooked into `predev`/`prebuild`). **Do not hand-edit.** This is how the vast majority of tokens are consumed: `var(--r-color-...)` in CSS.17- **Direct JS/TS imports** of named exports (`colorCanvas`, `colorCoverArt`, `layout`, …) for the few cases needing a token value in JavaScript — baking colors into an SVG string (`utils/covers`), canvas/QR backgrounds (`Player/Ruffle.vue`, `ShowQRCodeDialog`), and the virtualiser's pixel math (`Gallery/listColumns` reading `layout`).1819v2 has **no Vuetify theme of its own.** `tokens.css` emits a palette block per theme under `.r-v2.r-v2-dark` / `.r-v2.r-v2-light`; `RomM.vue` toggles those classes on `<html>`. v2 surfaces never read Vuetify's runtime theme.2021> Caveat: a wrapped Vuetify component still resolves `color="primary"` against Vuetify's _own_ registered themes (`src/plugins/vuetify.ts`, sourced from v1's `@/styles/themes`). They mirror the brand tokens by hand (both `#8B74E8`), so they line up, but it's a parallel source. **Prefer `var(--r-...)` over the `color` prop.**2223### Adding a new token24251. Add it to `src/v2/tokens/index.ts` with a **semantic, role-based** name (`--r-color-danger`, not `--r-color-red`).262. Provide **both** dark and light values.273. Consume via `var(--r-...)` (CSS) or the named export (JS).284. Run `npm run build:tokens` to regenerate `tokens.css`.295. If the JS→CSS variable name needs an exception (e.g. `--r-nav-h`), add an entry to `NAME_OVERRIDES` in the generator.3031### Where the scope classes live — and why `<html>`3233`.r-v2`, `.r-v2-dark`, `.r-v2-light` go on `<html>` (`RomM.vue` toggles them whenever `uiVersion` or the active theme changes). Vuetify teleports overlays (`VDialog`, `VMenu`, `VTooltip`) to `<body> > .v-overlay-container`, **outside** `<v-app>`. Only `<html>` covers both the regular tree and the teleports — without it, overlays lose their tokens.3435### Diagnostics — when `var(--r-color-...)` resolves to nothing on an overlay36371. Check `RomM.vue`'s watch on `documentElement.classList` (load-bearing).382. Check that the teleported component carries a `content-class` tying it back to scope (e.g. `RDialog` uses `content-class="r-dialog"`).393. **Never** "fix" it by swapping the token for a hex literal — that hides the bug and breaks the dual theme.4041---4243## Visual language44451. **Single visual vocabulary.** Every surface (dialog, menu, popover, card, toolbar) reads as a sibling — same blur, curvature, depth. No standalone "dialog look" vs "menu look".462. **Canonical references when designing**: ask the user before consulting `https://mockup.thebirdcage.tv/`. They decide whether the mockup or existing primitives take priority.473. **State semantics are shared across primitives** (don't reinvent per component):48 - hover (neutral, or brand-tinted on selected rows)49 - selected/checked (`--r-color-brand-primary`)50 - active/favorite (`--r-color-fav`)51 - focus (modality-gated; visible only on `key`/`pad` — see `frontend-v2-input`)52 - busy/pending · disabled534. **Implementation gotchas:**54 - `RDialog` ships unscoped `<style>` at the bottom that strips Vuetify defaults from `.v-overlay__content`. Load-bearing — without it the `--r-radius-card` corners disappear.55 - Every dialog goes through `RDialog`; every menu through `RMenu`.5657---5859## Color-literal policy: zero exceptions6061Outside `src/v2/tokens/index.ts` (the source-of-truth TS module) and the generated `src/v2/styles/tokens.css`, **no hex or `rgba()` literals exist anywhere in v2.** Everything previously "excepted" is now a token or a `color-mix`:6263- Cover-overlay glass → `--r-color-overlay-*` (fixed dark glass; never theme-flips).64- Cover artwork placeholder & shimmer → `--r-color-cover-placeholder`, `--r-color-cover-placeholder-bright`.65- Panel / tooltip / shimmer-sweep → `--r-color-panel`, `--r-color-panel-border`, `--r-color-tooltip-bg`, `--r-color-shimmer-sweep`.66- Backdrop scrims (`global.css`) → `color-mix(in srgb, var(--r-color-bg) X%, transparent)`.67- Status tints → `color-mix(in srgb, var(--r-color-status-base-{success,warning,danger,info}) X%, transparent)`.68- Brand-tinted backgrounds (selected rows, focus rings) → `color-mix(in srgb, var(--r-color-brand-primary) X%, transparent)`.69- Black/white shadows → `color-mix(in srgb, black X%, transparent)` (CSS named color, not a hex literal).70- Metadata-provider chips → `--r-color-provider-*`. Player canvas → `--r-color-canvas-bg`, `--r-color-canvas-bg-deep`.71- Emphasis pill (always-white-on-dark "Play" CTA over cover art) → `--r-color-overlay-emphasis-bg/-fg/-bg-hover`.7273If a literal would otherwise be needed, the answer is: **add a token** (steps above), then consume via `var(--r-color-...)` or the named export.7475## Style conventions7677- Scoped `<style>` by default; unscoped only for teleport overrides.78- BEM-ish class names: `.feature__element--modifier`. Prefixes: `.r-v2-...` for app-shell surfaces outside components; `.r-...` for globally shared utilities/tokens.79- No plain CSS where a Vuetify utility class covers the case (`d-flex`, `pa-4`, `align-center`).