feat: directory search, favorites, motion system, home button, transport fix

- radio-browser.info directory search (debounced, ~58k stations) with
  codec/bitrate/country metadata and save-to-stations action
- station favorites (star) persisted in StationRepository; Starred
  section pinned in Stations; active-station row highlight
- motion system (Motion.kt): shared-axis nav transitions, hub hop,
  metadata rise, play/pause pop; LocalReduceMotion honors system
  'remove animations'
- AppHomeButton (launcher icon) top-left on every surface + SatsAmp
  wordmark on the hub; home pops the stack back to the player
- transport row: add missing Next button, conventional order
  stop · prev · play · next
- WaveGraphic center glow: wider, fainter, no perceptible disc edge
- StreamMetadata: strip unsubstituted {{template}} residue
- PRODUCT.md + DESIGN.md (impeccable design context); .gitignore for
  re-downloadable skill engine binaries
This commit is contained in:
avi 2026-09-08 00:37:33 -05:00
commit bc6396a570
20 changed files with 1068 additions and 73 deletions

120
DESIGN.md Normal file
View file

@ -0,0 +1,120 @@
---
name: SatsAmp
description: Android radio player with a glass-on-charcoal surface system and four user-selectable AA-verified accents.
colors:
base-top: "#23232B"
base-bottom: "#1B1B22"
panel-dark: "#202029"
paper-top: "#F2F1F7"
paper-bottom: "#E7E5F0"
panel-light: "#FBFAFE"
text-primary-dark: "#FFFFFF"
text-secondary-dark: "#D9D9DE"
text-muted-dark: "#8C8C96"
text-primary-light: "#1B1B22"
violet-fill: "#7C4DFF"
violet-text: "#A78BFF"
blue-fill: "#4C7DFF"
blue-text: "#93B4FF"
green-fill: "#1FA855"
green-text: "#5BD98A"
orange-fill: "#F97316"
orange-text: "#FFB37E"
violet-fill-light: "#6A3DE0"
blue-fill-light: "#2563EB"
green-fill-light: "#0C7A38"
orange-fill-light: "#C2570A"
ink-on-fill: "#141419"
success: "#34C759"
error: "#EF4444"
warning: "#F59E0B"
typography:
display:
fontFamily: "System default (Roboto)"
fontWeight: 700
title:
fontFamily: "System default (Roboto)"
fontWeight: 600
body:
fontFamily: "System default (Roboto)"
fontWeight: 400
caption:
fontFamily: "System default (Roboto)"
fontSize: "12sp"
fontWeight: 400
rounded:
card: "20dp"
input: "14dp"
surface: "12dp"
full: "circle"
components:
button-accent:
backgroundColor: "{colors.violet-fill}"
textColor: "#FFFFFF"
rounded: "{rounded.input}"
save-button:
backgroundColor: "{colors.success}"
textColor: "#FFFFFF"
rounded: "{rounded.full}"
glass-card:
backgroundColor: "rgba(255,255,255,0.06)"
textColor: "{colors.text-primary-dark}"
rounded: "{rounded.card}"
input-glass:
backgroundColor: "rgba(255,255,255,0.09)"
textColor: "{colors.text-primary-dark}"
rounded: "{rounded.input}"
---
## Overview
SatsAmp is a personal Android radio player (Kotlin + Jetpack Compose, Material 3) whose surface language is translucent glass panels over a charcoal gradient (dark) or paper gradient (light), lit by a soft radial accent glow. The user picks one of four accents — violet, blue, green, orange — and one of System/Dark/Light modes at runtime; every screen must resolve through the token set, never through hardcoded colors. Accessibility is a hard floor: every accent/mode combination is AA-verified.
The normative source of truth is `app/src/main/java/com/rada/ui/theme/Tokens.kt` (`UiTokens`, `tokensFor()`), mirrored by `design/glass-radio-theme.css` for web demos. `Theme.kt` bridges the tokens into the M3 `ColorScheme` and provides `LocalTokens`; the M3 scheme is derived, not independent.
## Colors
**Eight real themes** = 4 accents × dark/light, each a `UiTokens` instance of 21 roles read via `LocalTokens.current`:
- **Base:** dark charcoal gradient `#23232B → #1B1B22` with solid panel `#202029`; light paper gradient `#F2F1F7 → #E7E5F0` with panel `#FBFAFE`.
- **Glass layers (dark):** fill `rgba(255,255,255,0.06)`, strong fill `0.09`, decorative border `0.10`, interactive border `0.36` (≥3:1 per WCAG 1.4.11). Light mode inverts to translucent white over paper (`0.62`/`0.78`) with ink borders.
- **textSafe:** a ~90% opaque base-colored overlay always placed under text sitting on glass. Text never touches bare glass.
- **Text:** dark mode white / `#D9D9DE` / `#8C8C96` (muted is the caption floor, ≥4.5:1); light mode `#1B1B22` at 100% / 80% / 62%.
- **Accents:** dark-mode fills violet `#7C4DFF`, blue `#4C7DFF`, green `#1FA855`, orange `#F97316`; light-mode fills are deepened (`#6A3DE0`, `#2563EB`, `#0C7A38`, `#C2570A`) to hold contrast on paper. Each accent defines fill, hover fill, `accentOnFill` (white for violet, ink `#141419` for blue/green/orange in dark — this flip is the AA mechanism), `accentText` for text/icons on surfaces, and `accentSoft` (~16% alpha) for active-state backfills.
- **Status:** success `#34C759` (save/confirm), error `#EF4444`, warning `#F59E0B` — mode-independent constants.
## Typography
System default font (Roboto), no bundled typefaces. Hierarchy comes from `MaterialTheme.typography` M3 roles; weight emphasis clusters at SemiBold/Bold for titles and controls. Only four local `fontSize` overrides exist (12–13sp captions) — prefer M3 roles over new overrides.
## Layout
Single-activity Compose app with bottom navigation across library / stations / streams / player surfaces. Full-bleed `GlassBackground` (base gradient + one radial accent glow anchored upper-right, alpha 0.10–0.14) sits under all content. Panels, not page sections, carry grouping; content scrolls over the fixed background.
## Elevation & Depth
No shadow vocabulary — depth is tonal layering: base gradient → radial glow → translucent glass fill → `textSafe` overlay → content. Compose has no backdrop blur without a library, so "glass" means the translucent layer plus a 1px inner highlight border. Do not introduce `shadowElevation` to fake depth; add a tonal layer instead.
## Shapes
Two named radii: `SatsAmpGlass.rCard` = 20dp (cards, sheets) and `rInput` = 14dp (inputs, small buttons). `CircleShape` is reserved for playback controls, avatars, and record/save FABs. New surfaces should use a named radius, not a new one-off value.
## Components
- **Accent button** — `accentFill` background, `accentOnFill` label, 14dp radius, `accentFillHover` on press.
- **Glass card** — translucent fill + decorative border + 20dp radius; text goes on the `textSafe` overlay.
- **Input field** — `glassFillStrong`, 14dp radius, `borderInteractive` when focused (`focusRing` = `accentText`).
- **Save/record control** — `success` green, circular; the one fixed-color control by design (it reads the same in every accent).
- **Active nav/tab state** — `accentSoft` backfill + `accentText` icon/label, never a filled accent.
**Motion system** (`ui/theme/Motion.kt`): one shared vocabulary — `MotionArrival` (0.16,1,0.3,1) for entrances, `MotionExit` (M3 emphasized-accelerate) for faster exits, `MotionStandard` for state; duration budget 130 feedback / 220 state / 340 view / 140 exit. Navigation is shared-axis (details slide in from the right ±¼ travel, hub hops scale-settle 0.94→1); metadata changes rise ⅓-line in and fast-fade out; `LocalReduceMotion` (system animator scale = 0) reduces every path to a short opacity cut.
## Do's and Don'ts
- **Do** read colors via `LocalTokens.current` or the derived M3 scheme; every new screen must be correct in all 8 theme combinations.
- **Do** place `textSafe` under any text on glass, and keep `textMuted`-equivalent contrast ≥4.5:1.
- **Do** follow the active accent for anything tinted — buttons, glows, active states, brush gradients (use `SatsAmpGradients.accentBrush()`, not the static gradients).
- **Don't** use `SatsAmpGradients.primary`/`primaryHorizontal` or the fixed `PremiumBlue/Cyan/Pink` brand colors — they are violet-locked legacy and break non-violet themes; prefer `accentBrush()`.
- **Don't** pull from the legacy terminal palette (`TerminalGreen`, `TerminalGold`, …) in `Color.kt`; it belongs to no shipped surface.
- **Don't** use `PremiumError` `#EF4444` as text on light backgrounds without darkening — it fails AA on paper (known gap; fix or pair with an icon when used on light).
- **Don't** introduce one-off radii (ad-hoc 12/16dp already exist in places); extend the named set deliberately if the system genuinely needs a new step.