Spaces:
Runtime error
Runtime error
| # T-009: CSS Design Token System β Semantic Tokens, Component Tokens, Utility Classes | |
| **Type:** Task | |
| **Phase:** 0 β Foundation | |
| **Autonomy:** `agent:autonomous` β Pure CSS, fully specified, no decisions required. | |
| **Stack:** `stack:typescript` | |
| **Version:** v0.1 | |
| **Iteration:** iter-1 | |
| **Effort:** XS (1 hour) | |
| --- | |
| > β οΈ **Agent Scope:** T-001 defined primitive tokens in `tokens.css`. T-009 adds the second layer: semantic component tokens, reusable utility classes, and keyframe animations. Do not modify the primitive tokens from T-001 β extend only. Do not wire any interactivity or JavaScript β pure CSS. | |
| --- | |
| ## Context | |
| T-001 created `src/styles/tokens.css` with raw primitives: colors, spacing, typography scale. That layer answers "what colors exist." This task adds the layer that answers "what does a panel header look like" β semantic tokens and utility classes that UI components reference directly instead of reaching back to raw primitives. | |
| Having both layers means a component like `TrackMixer.ts` writes `class="panel-header"` and picks up the correct font, color, spacing, and border in one class β not a sequence of raw token references that must be manually kept in sync. | |
| --- | |
| ## Prerequisites | |
| - [ ] T-001 merged β `src/styles/tokens.css` exists with all primitive tokens | |
| - [ ] Fonts bundled (`JetBrains Mono`, `Space Grotesk`) per T-001 spec | |
| --- | |
| ## Acceptance Criteria | |
| - [ ] `src/styles/tokens.css` β T-001's primitive tokens intact and verified complete (add any missing from the list below) | |
| - [ ] `src/styles/components.css` β all semantic token blocks defined and imported in `index.html` after `tokens.css` | |
| - [ ] `src/styles/animations.css` β all keyframes defined and imported after `components.css` | |
| - [ ] `pnpm typecheck` still passes (CSS-only change, no TS impact) | |
| - [ ] No raw hex values in `components.css` or `animations.css` β all colors reference `var(--color-*)` tokens | |
| - [ ] No `!important` anywhere in any style file | |
| - [ ] CSS loads in correct order: `reset.css` β `tokens.css` β `components.css` β `animations.css` β `app.css` | |
| --- | |
| ## Verify T-001 Primitive Tokens Are Complete | |
| Before writing anything new, check `tokens.css` has all of the following. Add any that are missing: | |
| ```css | |
| /* Required primitives from T-001 β verify each exists */ | |
| --color-bg, --color-surface, --color-surface-2, --color-border | |
| --color-text, --color-text-dim, --color-text-bright | |
| --color-accent, --color-accent-dim, --color-accent-2 | |
| --color-red, --color-green, --color-yellow, --color-orange | |
| --color-rust, --color-cpp, --color-ts, --color-python, --color-ml | |
| --font-mono, --font-ui | |
| --font-size-xs, --font-size-sm, --font-size-base, --font-size-md, --font-size-lg | |
| --font-weight-light, --font-weight-normal, --font-weight-medium, --font-weight-semi, --font-weight-bold | |
| --space-1 through --space-8 | |
| --radius-sm, --radius-md, --radius-lg | |
| --transition-fast, --transition-base | |
| ``` | |
| Add one missing primitive that T-001 did not include: | |
| ```css | |
| /* Add to :root in tokens.css */ | |
| /* Layout heights β used by app.css grid and component positioning */ | |
| --height-header: 56px; | |
| --height-bottom: 100px; | |
| /* Panel widths */ | |
| --width-left-panel: 240px; | |
| --width-right-panel: 280px; | |
| /* Z-index scale */ | |
| --z-base: 0; | |
| --z-overlay: 10; | |
| --z-modal: 100; | |
| --z-toast: 200; | |
| ``` | |
| --- | |
| ## `src/styles/components.css` | |
| ```css | |
| /* βββ Semantic tokens βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* These are aliases of primitives scoped to UI roles. */ | |
| /* Change a primitive β all its semantic aliases update automatically. */ | |
| :root { | |
| /* Panel structure */ | |
| --panel-bg: var(--color-surface); | |
| --panel-bg-alt: var(--color-surface-2); | |
| --panel-border: var(--color-border); | |
| --panel-padding: var(--space-5); | |
| /* Panel header row (e.g. "TRACK MIXER [MODEL: MUSICVAE]") */ | |
| --panel-header-font: var(--font-mono); | |
| --panel-header-size: var(--font-size-xs); | |
| --panel-header-color: var(--color-text-dim); | |
| --panel-header-weight: var(--font-weight-normal); | |
| --panel-header-gap: var(--space-2); | |
| --panel-header-track: 0.1em; | |
| /* Bracket-style label e.g. [MODEL: MUSICVAE], [ENGINE: WEBGPU] */ | |
| --label-bracket-color: var(--color-text-dim); | |
| --label-bracket-font: var(--font-mono); | |
| --label-bracket-size: var(--font-size-xs); | |
| /* Large display values β Mood, Energy, Texture panels */ | |
| --display-value-size: var(--font-size-lg); | |
| --display-value-weight: var(--font-weight-bold); | |
| --display-value-color: var(--color-text-bright); | |
| --display-label-size: var(--font-size-xs); | |
| --display-label-color: var(--color-text-dim); | |
| --display-label-track: 0.12em; | |
| /* Channel strips (LEAD, PAD, BASS) */ | |
| --channel-name-size: var(--font-size-base); | |
| --channel-name-weight: var(--font-weight-bold); | |
| --channel-name-color: var(--color-text-bright); | |
| --channel-label-color: var(--color-text-dim); | |
| --channel-label-size: var(--font-size-xs); | |
| /* Fader / slider track */ | |
| --fader-track-bg: var(--color-surface-2); | |
| --fader-track-border: var(--color-border); | |
| --fader-fill-color: var(--color-accent); | |
| --fader-thumb-color: var(--color-text-bright); | |
| --fader-height: 4px; | |
| /* Transport display */ | |
| --transport-bpm-size: 28px; | |
| --transport-bpm-weight: var(--font-weight-bold); | |
| --transport-bpm-font: var(--font-mono); | |
| --transport-meta-size: var(--font-size-xs); | |
| --transport-meta-color: var(--color-text-dim); | |
| /* Status pill (WebGPU Active, LIVE) */ | |
| --pill-bg: var(--color-accent-dim); | |
| --pill-border: var(--color-accent); | |
| --pill-color: var(--color-accent); | |
| --pill-font: var(--font-mono); | |
| --pill-size: var(--font-size-xs); | |
| --pill-padding: var(--space-1) var(--space-3); | |
| --pill-radius: var(--radius-sm); | |
| /* REC indicator */ | |
| --rec-color: var(--color-red); | |
| --rec-dot-size: 8px; | |
| /* Accent button (FREEZE) */ | |
| --btn-accent-bg: transparent; | |
| --btn-accent-border: var(--color-border); | |
| --btn-accent-color: var(--color-text); | |
| --btn-accent-bg-hover: var(--color-surface-2); | |
| --btn-accent-border-hover: var(--color-accent); | |
| /* Icon button (settings gear, +/-) */ | |
| --btn-icon-color: var(--color-text-dim); | |
| --btn-icon-color-hover: var(--color-text-bright); | |
| /* Macro knob/fader label */ | |
| --macro-label-size: var(--font-size-xs); | |
| --macro-label-color: var(--color-text-dim); | |
| --macro-label-font: var(--font-mono); | |
| --macro-value-color: var(--color-text); | |
| /* Radar chart */ | |
| --radar-line-color: rgba(0, 200, 212, 0.2); | |
| --radar-fill-color: rgba(0, 200, 212, 0.08); | |
| --radar-point-color: var(--color-accent); | |
| --radar-label-size: var(--font-size-xs); | |
| --radar-label-color: var(--color-text-dim); | |
| } | |
| /* βββ Utility classes βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* Bracket label: [MODEL: MUSICVAE] */ | |
| .label-bracket { | |
| font-family: var(--label-bracket-font); | |
| font-size: var(--label-bracket-size); | |
| color: var(--label-bracket-color); | |
| letter-spacing: 0.08em; | |
| } | |
| /* Panel header row */ | |
| .panel-header { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--panel-header-gap); | |
| font-family: var(--panel-header-font); | |
| font-size: var(--panel-header-size); | |
| font-weight: var(--panel-header-weight); | |
| color: var(--panel-header-color); | |
| letter-spacing: var(--panel-header-track); | |
| text-transform: uppercase; | |
| padding: var(--space-3) var(--space-4); | |
| border-bottom: 1px solid var(--panel-border); | |
| } | |
| /* Status pill */ | |
| .pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: var(--space-2); | |
| background: var(--pill-bg); | |
| border: 1px solid var(--pill-border); | |
| border-radius: var(--pill-radius); | |
| color: var(--pill-color); | |
| font-family: var(--pill-font); | |
| font-size: var(--pill-size); | |
| padding: var(--pill-padding); | |
| } | |
| .pill-dot { | |
| width: var(--rec-dot-size); | |
| height: var(--rec-dot-size); | |
| border-radius: 50%; | |
| background: currentColor; | |
| flex-shrink: 0; | |
| } | |
| /* REC indicator */ | |
| .rec-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: var(--space-2); | |
| color: var(--rec-color); | |
| font-family: var(--font-mono); | |
| font-size: var(--font-size-xs); | |
| font-weight: var(--font-weight-medium); | |
| } | |
| /* Large display value (e.g. "MYSTERIOUS", "0.8 TeV", "DENSE") */ | |
| .display-value { | |
| font-size: var(--display-value-size); | |
| font-weight: var(--display-value-weight); | |
| color: var(--display-value-color); | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| } | |
| .display-label { | |
| font-family: var(--font-mono); | |
| font-size: var(--display-label-size); | |
| color: var(--display-label-color); | |
| letter-spacing: var(--display-label-track); | |
| text-transform: uppercase; | |
| } | |
| /* Channel name in Track Mixer */ | |
| .channel-name { | |
| font-size: var(--channel-name-size); | |
| font-weight: var(--channel-name-weight); | |
| color: var(--channel-name-color); | |
| } | |
| /* Accent button (FREEZE) */ | |
| .btn-accent { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: var(--space-2); | |
| background: var(--btn-accent-bg); | |
| border: 1px solid var(--btn-accent-border); | |
| border-radius: var(--radius-sm); | |
| color: var(--btn-accent-color); | |
| font-family: var(--font-mono); | |
| font-size: var(--font-size-xs); | |
| padding: var(--space-1) var(--space-3); | |
| cursor: pointer; | |
| transition: border-color var(--transition-fast), background var(--transition-fast); | |
| } | |
| .btn-accent:hover { | |
| background: var(--btn-accent-bg-hover); | |
| border-color: var(--btn-accent-border-hover); | |
| } | |
| /* Icon button (+/-, gear) */ | |
| .btn-icon { | |
| background: transparent; | |
| border: none; | |
| color: var(--btn-icon-color); | |
| cursor: pointer; | |
| padding: var(--space-1); | |
| transition: color var(--transition-fast); | |
| line-height: 1; | |
| } | |
| .btn-icon:hover { color: var(--btn-icon-color-hover); } | |
| /* Monospace data label (BPM, bar/beat counter, parameter values) */ | |
| .mono-data { | |
| font-family: var(--font-mono); | |
| font-size: var(--font-size-xs); | |
| color: var(--color-text-dim); | |
| letter-spacing: 0.05em; | |
| } | |
| /* Divider line */ | |
| .divider { | |
| border: none; | |
| border-top: 1px solid var(--panel-border); | |
| margin: 0; | |
| } | |
| ``` | |
| --- | |
| ## `src/styles/animations.css` | |
| ```css | |
| /* βββ Keyframe definitions βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* All animation durations use CSS custom properties so they can be overridden */ | |
| /* by JS when synced to BPM (e.g. --beat-ms set from transport_tick events). */ | |
| :root { | |
| --anim-pulse-duration: 2s; /* Default heartbeat pulse β overridden by BPM */ | |
| --anim-blink-duration: 1s; /* REC indicator blink */ | |
| --anim-fade-in: 200ms; | |
| } | |
| /* Status dot pulse (WebGPU Active indicator) */ | |
| @keyframes dot-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.35; } | |
| } | |
| /* REC indicator blink */ | |
| @keyframes rec-blink { | |
| 0%, 49% { opacity: 1; } | |
| 50%, 100% { opacity: 0; } | |
| } | |
| /* Beat flash β applied to the transport area on each downbeat */ | |
| @keyframes beat-flash { | |
| 0% { opacity: 1; } | |
| 15% { opacity: 0.4; } | |
| 100% { opacity: 1; } | |
| } | |
| /* Fade in from transparent β used for panels mounting */ | |
| @keyframes fade-in { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| /* Subtle slide-up on mount */ | |
| @keyframes slide-up { | |
| from { opacity: 0; transform: translateY(4px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* βββ Animation utility classes ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .animate-pulse { | |
| animation: dot-pulse var(--anim-pulse-duration) ease-in-out infinite; | |
| } | |
| .animate-rec { | |
| animation: rec-blink var(--anim-blink-duration) step-start infinite; | |
| } | |
| .animate-beat-flash { | |
| animation: beat-flash 200ms ease-out forwards; | |
| } | |
| .animate-fade-in { | |
| animation: fade-in var(--anim-fade-in) ease both; | |
| } | |
| .animate-slide-up { | |
| animation: slide-up var(--anim-fade-in) ease both; | |
| } | |
| ``` | |
| --- | |
| ## Update `index.html` Import Order | |
| ```html | |
| <link rel="stylesheet" href="/src/styles/reset.css" /> | |
| <link rel="stylesheet" href="/src/styles/tokens.css" /> | |
| <link rel="stylesheet" href="/src/styles/components.css" /> | |
| <link rel="stylesheet" href="/src/styles/animations.css" /> | |
| <link rel="stylesheet" href="/src/styles/app.css" /> | |
| ``` | |
| --- | |
| ## Testing | |
| ```bash | |
| pnpm typecheck # No TS impact β must still pass | |
| pnpm format:check | |
| ``` | |
| Manual: open `cargo tauri dev`, open DevTools β Elements tab. Verify: | |
| - `--color-accent` resolves to `#00c8d4` on `:root` | |
| - `.panel-header` class applied to Track Mixer header shows correct uppercase monospace style | |
| - `.animate-pulse` on the WebGPU status dot produces a visible fade | |
| - `.animate-rec` on the REC indicator produces a hard blink | |
| --- | |
| ## GitHub CLI | |
| ```bash | |
| gh issue create \ | |
| --title "T-009: CSS design token system β semantic tokens, component tokens, utility classes" \ | |
| --label "type:task,stack:typescript,agent:autonomous,priority:high,status:ready,day:1" \ | |
| --body-file T-009.md | |
| ``` | |
| --- | |
| **Parent:** GENESIS | |
| **Blocks:** T-036 (skeleton renderer uses `.animate-pulse`), T-052 (Gemma panels use `.display-value`, `.display-label`), T-058 (waveform renderer uses radar tokens), T-113 (Unity data bridge overlay labels) | |
| **Blocked By:** T-001 | |
| **Version:** v0.1 Β· **Iteration:** iter-1 Β· **Effort:** XS (1 hour) | |