Add scripts/check-tokens.mjs (npm run lint:tokens, wired into lint) as
a guardrail: fails if src/**/*.svelte reintroduces arbitrary values
outside the B.4 allowlist, factory rounded-*, h-N w-N pairs (should
be size-N), quarter-step fractional spacing, or literal hex/rgba()
colors.
Cleans up the repo until the script reports 0 violations: ~40 h-N w-N
pairs -> size-N (mostly icons, via a one-off script), ~26 quarter-step
fractional spacings rounded to the nearest integer (delta <=1px, e.g.
py-2.75->py-3, gap-1.75->gap-2), stray factory radii (rounded-sm/lg,
bare rounded) migrated to the matching tokens, and two
tracking-[-0.02em] that matched the existing tracking-display token
exactly but had never been migrated. Removes the
stroke-thin/regular/bold recipes (added earlier, zero consumers) —
decision made, not blocking: strokeWidth={n} (13 values, across
almost every component) isn't migrated to CSS because there's no way
to verify without Docker that lucide-svelte honors stroke-width by
class instead of the prop, and a regression there would be silent
across the whole app; the script documents this gap explicitly
instead of pretending full coverage.
Orphan detector: 0 components with no importers. Removes
docs/design-system-refactor-plan.md (earlier plan, superseded by
frontend-refactor-plan.md). web-player/CLAUDE.md gains a "Styles"
section with the B.4 allowlist and the base components to reuse.
Recalculates the baseline table (A.4) and logs it: 61->47 components,
44->11 arbitrary values (the remaining 11 on the allowlist), 13->8
distinct radii, 0 lint:tokens violations.
Verified with npm run check (0 errors), npm run lint:tokens (0
violations) and npm run build. Smoke-tested /login with no console
errors attributable to this change.
This pass also tackles the task deferred earlier: removing
tracks.ts's TrackTarget wrapper and its 8 accessors
(targetId/targetTitle/targetArtist/targetOwnerUserId/targetExplicit/
targetListensCount/isTargetCurrent/queueItemForTarget/
targetForQueueItem). Explore, Home, Mix, menus.svelte.ts and mixes.ts
now use track objects directly, with
toQueueItem/trackFromQueueItem (new, in player.svelte.ts) as the only
converters. Along the way, TypeScript revealed that isExplicit
doesn't exist in the real /tracks schema: Explore's "explicit" badge
was already a no-op before this refactor (always undefined ??
false); the binding is removed instead of pretending support that
never existed.
Explore: GENRE_INFO/genreTiles are extracted to src/lib/genres.ts;
the search logic
(showGroup/capped/matchPlaylists/searchCounts/searchChips/findTopResult)
is extracted to src/lib/search.ts as pure functions parameterized by
filter/query instead of closing over component state. "Top result"
unifies the duplicated button/link with <svelte:element>. Decision
made without blocking: the 4 group blocks (Tracks/Albums/Users/
Playlists) are not collapsed into a single {#each groups} — each
group has genuinely different snippets, context menu and ListRow
props, and a generic config would have added more indirection than it
removed.
Home: uses Page instead of a hand-rolled page-x; the 3 stats become
an {#each}; the hero/spotlight backgrounds (inline style= with CSS
variables) become the hero-surface/spotlight-surface utilities, and
gap-11.5 is replaced by the --spacing-section token (2.875rem) via
gap-section.
Verified with npm run check (0 errors) and npm run build. /explore
without a session redirects to /login because +layout.server.ts
requires allowAnonymousListening from the backend (unreachable
without Docker this session) — pre-existing behavior, not a
regression. No authenticated visual QA; smoke-tested /login with no
console errors.
PageHeader absorbs CollectionHeader
(title/eyebrow/description/meta/cover?/actions?/align?): without a
cover it behaves like the old simple PageHeader (inline actions),
with a cover like the old CollectionHeader (actions below). It sets
its own bottom margin (mb-7/8); the mt-* each page used to add
afterward are removed. Migrates the 9 places that used either one
(albums, library, playlists, upload, Explore x2, liked, mix,
profile). CollectionHeader is removed.
AlbumHeader and PlaylistHeader are inlined into their pages and
removed. Decision (recommended, not blocking): PlaylistHeader gains
icons on Edit/Delete/Play, matching what AlbumHeader already had.
New ConfirmDialog replaces the two delete dialogs (album and
playlist), which had different typography. Modal now uses
.glass-panel (instead of duplicating the inline style=) and
z-(--z-modal); its eyebrow and title use the
text-eyebrow/text-display-3 recipes.
SectionHeading gains `count?` (a "N results" line, replacing 4 copies
in Explore) and `href?`/`linkLabel?` ("See all", replacing Home's).
Applies typography recipes (text-display-2/3, text-eyebrow,
text-body) and width tokens (max-w-prose-sm, max-w-hero) across
EmptyState, +error, "No results", and the Home/Queue/Sidebar hero and
stats. The "You haven't listened to anything yet" empty state
(duplicated twice on Home) becomes a local snippet.
Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
Add TrackList.svelte, replacing TrackTable+TrackRow+TrackIndexCell+
TrackMeta and the 6 track tables (library, liked, mix, album,
playlist, album's library picker): declarative columns
(added/uploaded/plays) with fixed widths as module constants, an
index with an EQ indicator for the active track (or an overlay on the
art when index={false}, for the library picker), and a rowAction that
builds the form+IconButton (removeTrack/addTrack/deleteTrack) or a
direct onclick (Like).
AlbumTrackTable, PlaylistTrackTable and LibraryPicker are inlined
into their pages and removed, along with the 4 components TrackList
replaces.
Decision (recommended by the plan itself, not blocking): remove the
"Album" column, which in all 3 tables that had it always showed "—"
(a placeholder with no real data).
Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
Add MediaIdentity.svelte (renames and generalizes TrackTitleCell,
purely presentational: art + title + subtitle, no click handling)
and ListRow.svelte on top of it (href|onclick, active, size xs/sm/lg,
variant row|card, leading/trailing/overlay). Also used by
player/TrackInfo... (TrackInfo itself is left for a later pass, which
already has an explicit task to touch that file).
Migrated to ListRow: SearchResultRow (Explore, 4 groups — component
removed), Home (Continue listening, Popular, spotlight, Your
playlists), Queue (Up next), Sidebar (playlists). The 6 track tables
(AlbumTrackTable, PlaylistTrackTable, library, liked, mixes,
LibraryPicker) migrate to MediaIdentity with their own local
use:pressable, since TrackList doesn't exist yet.
EqBars gains an `overlay` prop (scrim/45 background, on-art color)
replacing NowPlaying.svelte (removed). Along the way, fixes 6
plural() calls that a previous pass had left unmigrated (liked,
library, mixes, playlists, PlaylistHeader, Home).
Deltas documented in the plan's log: Sidebar's active row now uses
the accent tint (previously neutral gray); Home's "Your playlists"
card loses its redundant author line and its radius becomes
rounded-art; Queue rows lose the art's opacity-80.
Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
Add src/lib/components/ui/ContextMenu.svelte, replacing
TrackContextMenu and AlbumContextMenu (109 and 119 lines, 90%
identical): same positioning function (contextMenuPosition, exported
from the module, using MENU_WIDTH/SUBMENU_WIDTH moved to config.ts),
same backdrop, same Escape handling, same generic "Add to playlist"
submenu via {items, playlistAction, playlists}. Uses MenuItem for
actions (previously each reimplemented its own classes by hand) and
Artwork size="xs" for playlist thumbnails.
menus.svelte.ts (createTrackMenu) adapts to the new
contextMenuPosition; Home, Explore (track and album menus) and Mix
are migrated.
AccountMenu: panelClass (free-form Tailwind classes) becomes prop
width: 'sm'|'md'; adds Escape-to-close (accessibility improvement, it
didn't close before); TopBar and MobileHeader no longer wrap it in a
duplicate div.relative (its own root already is one). The panel's
z-index becomes z-(--z-menu), matching ContextMenu (previously z-20
in TopBar / no explicit z in MobileHeader).
Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
Add src/lib/actions/pressable.ts and clickOutside.ts (Svelte actions;
role/tabindex stay static in markup for SSR accessibility, the action
only adds click + Enter/Space) and src/lib/toggle.svelte.ts
(createToggle), used by playlists.svelte.ts and queuePanel.svelte.ts.
format.ts: adds plural(n, one, many); migrates the repo's 15 manual
pluralizations, unifying them to lowercase (visible diff in
Playlists, Home, liked, Explore).
player.svelte.ts: adds toQueueItem, isQueueCurrent, playAllOrToggle,
playShuffled; renames addToQueue -> playNextItem (the old name was
misleading: it inserts next, not at the end). Migrates the 5 places
that repeated the isCurrentQueue/playAll pattern (album, playlist,
liked, mix, Home). Simplifies liked.svelte.ts and recentlyPlayed.ts
with object spread instead of rebuilding every field by hand.
navigation.svelte.ts: adds searchHref (migrates the duplicated
buildHref from TopBar and Explore) and appNavLinks (added; its
adoption in Sidebar/TabsBar is left for a later pass, as noted
there).
menus.svelte.ts: createTrackMenu() replaces the manual context-menu
state (contextMenu + contextMenuStateFor + addToQueue) in Home,
Explore and Mix.
Deliberately left pending: removing tracks.ts's TrackTarget wrapper
and its 8 accessors is deferred — it's the riskiest change in the
plan, touches types across Home/Explore/Mix/menus, and couldn't be
visually verified without Docker this session.
Verified with npm run check/build.
Add src/lib/components/ui/Artwork.svelte and Avatar.svelte (B.3) and
migrate every use of Cover, PlaylistArt, MixArt and ArtistAvatar
(Sidebar, Queue, TrackInfo, TrackContextMenu, AlbumContextMenu,
TrackTitleCell, AlbumCard, PlaylistCard, MixTile, AlbumHeader,
PlaylistHeader, Home, Explore, mix, user profile, TopBar,
MobileHeader). Remove the 4 old components.
Artwork: size decides both dimension and radius automatically
(cover-*/radius tokens from the earlier token pass); fallback chain
is src -> mosaic (>=4 trackIds) -> first cover -> placeholder,
cascading if each step fails (previously split across 3 components
with slightly different logic each). Adds a 'fill' size (aspect-square
w-full), not in the spec, needed for fluid grid cards until MediaCard
lands later.
TrackTitleCell's `coverSize` prop (free-form Tailwind classes)
becomes `size: 'sm'|'lg'`, delegating to Artwork. SearchResultRow
loses its `kind` prop and the per-type radius (now decided by
Artwork/Avatar).
Product decision (not blocking): remove the name under the profile
avatar on /u/[id] since it duplicated the CollectionHeader title.
Small radius/size deltas are documented in the plan's README
(auto-radius by size replaces ad-hoc classes; see table B.1.1/B.1.2).
Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
Drops every YouTube-backed route, proxy and type (external
albums/tracks, thumbnail proxy, youtube.ts) and simplifies
player/track/mix types to the local-only catalog now that the
backend contract dropped Source/ExternalId. Genre tiles on Explore
and "Populares esta semana" on Home stay as deliberate client-only
placeholders per product decision, just without the YouTube backing.
Also finishes the design-system unification pass: Home's three
hand-rolled track rows (Continuar escuchando, Playlist destacada,
Populares esta semana) now reuse the shared TrackTitleCell component
instead of duplicating cover+title+artist markup, and the orphaned
Rail.svelte (no importers) is removed.
Migrates remaining one-off colors (bg-white/[N%], border-white/N, hardcoded
shadows/gradients) to the existing --mf-* tokens so light mode reacts
consistently everywhere, including a fix to --color-hover which was
hardcoded instead of pointing at --mf-surface-2. Extracts CollectionHeader
(album/playlist/mix/liked headers) and AccountMenu (TopBar/MobileHeader)
to remove five and two duplicated implementations respectively, and
normalizes inconsistent empty-state copy.
Backend:
- New Likes feature (toggle/get liked tracks) and endpoints
- User follow/unfollow, listening stats, public user profile
- Playlist visibility (public/private) support
- Various auth and DTO/response adjustments to support the above
Frontend (web-player):
- New design system pass: theme tokens/mode (light/dark), glass menu,
tabs bar, player dock/extras/queue components, mix tiles, artist link
- New routes: Liked, public user profile (/u/[id]), likes API proxy
- Search refactor: shared SearchBox component used by TopBar and
MobileHeader (search now reachable on mobile), live results as you
type instead of requiring Enter, dropped the "Buscando…" quick-result
preview
- Removed now-unused components (MobileNav, CoverBadge, MixBentoTile,
PlayPauseOverlay, SuggestionsTable, userDirectory)
- Design handoff reference docs added under web-player/design_handoff_musify_player
and web-player/docs/design-system-refactor-plan.md
Applies the display typeface (already used for small section labels)
consistently to every major heading — page titles, hero titles on
album/playlist/mix pages, the home hero, the sidebar/mobile wordmark, and
empty-state titles — instead of only the smaller ones, fixing an
inconsistent visual hierarchy. Also fixes a couple of real bugs found along
the way: Input had a focus border color with no border width to show it,
Button/IconButton had no focus-visible ring at all, and SectionHeading's
letter-spacing only applied when it had an actions slot.
Button's secondary/subtle variants drop their harsh border and flat grey
hover for a softer accent-tinted wash. The accent (and the per-track
extracted accent) is desaturated for a calmer, more pastel feel without
touching lightness, so on-accent text contrast is unaffected.
The login screen gets the same treatment plus a subtle ambient glow (reusing
the home page's existing accent-gradient technique) and a fluid,
viewport-scaled headline instead of a few fixed breakpoints, so it fills the
available space on wide screens instead of staying pinned to one size.
The playlist cards were showing "No description was pr…" because the API
stored that English sentence whenever a playlist was created without a
description. Descriptions are optional now and the old rows are cleared.
Section headings drop to 18/20px so they stop competing with the content,
and the space moved from inside the sections to between them.
The three media cards repeated the same anchor, art class and label
block, so they share a MediaCard that also carries the hover lift the
track tiles already had.
Every one of the seven tables repeated the same header markup and hand
written grid templates, always in the same shape: an optional index, the
title, optional desktop-only meta columns, the duration and an optional
action. TrackTable now takes that shape as props and builds both grid
templates and the headers itself.
A mix should read as one list, so the detail page drops the per-source
counters and the YouTube tag on each row, and the generated subtitles no
longer name Musify or YouTube Music.
Mixes are their own concept, not playlists: they live in their own
Mixes/MixItems tables, cannot be edited and never show up among the
user's playlists. Each item is either a Musify track or a YouTube Music
reference, so YouTube songs stream directly instead of being downloaded.
A daily Hangfire job seeds the mixes from the listening history: one
discovery mix, one daily mix and one per top artist.
The per-user YouTube filler query was already in the working tree and
rides along here because it shares files with this change.