Use type recipes and Button danger variant, add danger tint, dropzone and cover-fill tokens, give SearchInput a size prop so the top bar keeps its larger search, show queue details inline on mobile, derive upload limits from constants and document the card hover style.
Track lists and the upload queue now render through a declarative DataGrid, the album editor reuses AlbumForm with a split layout and a track picker, and genre labels and types are shared helpers.
Discover used a hardcoded list of genre tiles that searched track titles,
and the upload form kept its own copy of the genre list and compatibility
rules. Both now come from the backend.
Backend:
- GET /genres returns genres with at least one track and their counts.
- GET /genres/available returns every assignable genre with the genres
it cannot be combined with, so the rules live only in
GenreCompatibility.
- GET /tracks accepts ?genre= (case-insensitive, 400 on unknown values).
web-player:
- Discover tiles are built from /genres; each opens /explore?genre=X, a
song list filtered by that tag. Unknown genres fall back to a label
derived from their name.
- The upload form loads /genres/available, disables incompatible genres
as you pick, and the server validates before reserving upload URLs.
- lib/data/trackGenres.ts is removed; its helpers merge into
lib/data/genres.ts.
- Regenerate the typed API client.
ApiTrackLike/QueueItem had no duration field, so toQueueItem(s) always
dropped it and toTrack() hardcoded a new track's duration to 0, relying
entirely on the <audio> element's durationchange/loadedmetadata events
to fill it in once that track actually became current. Upcoming queue
rows never reach that point, so "A continuación" always showed a blank
duration even though the backend already returns it.
Add duration to both types, thread it through toQueueItems/toQueueItem/
trackFromQueueItem, and seed toTrack() from it. The real-time audio
sync still overwrites it once a track actually plays, so this only
fills the gap for tracks that haven't played yet.
Also map MixItemApplicationResponse's durationSeconds through
mixItemTrack, since mixes use a differently-named field.