Ashiedu's picture
Sync unified workbench
0490201 verified

A newer version of the Gradio SDK is available: 6.14.0

Upgrade

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:

/* 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:

/* 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

/* ─── 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

/* ─── 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

<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

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

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)