MediaCard now builds its own art (src/trackIds via Artwork size="fill", or a custom art snippet) and its own play button (onPlay, with a scrim and a glass PlayButton on hover) instead of relying on each wrapper passing it a snippet. Replaces AlbumCard, PlaylistCard and MixTile, which are removed along with MediaGrid.svelte (their grids move to the grid-cards/grid-cards-lg/grid-tiles/grid-wide @utility recipes). format.ts gains albumMeta() and playlistMeta() (previously ad-hoc calculations inside each card). Explore's genre tile moves from an inline style= gradient to the genre-tile @utility recipe + --tile-hue. Along the way, fixes the "follower/s" pluralization on the user profile that had been missed. Delta documented: the art's hover-zoom and the scrim+PlayButton (previously exclusive to MixTile) become MediaCard behavior gated on the onPlay prop — they'll apply to any future card that uses it, for consistency. Verified with npm run check/build; no authenticated visual QA (Docker unavailable this session).
56 KiB
Plan — Refactor y unificación del frontend (web-player)
Plan de trabajo para agentes/IA con contexto limitado (~128k). Sustituye a
design-system-refactor-plan.md(cuyo contenido ya está mayormente aplicado). Basado en una lectura completa de todos los.svelte,.cssy.tsde cliente deweb-player/src(2026-09-18, ramadesign-prot).
Cómo usar este documento
- Lee siempre la sección A (Contexto común) — es corta y todas las partes la asumen.
- Elige una parte (P0…P14). Cada parte indica: objetivo, prerrequisitos, archivos a leer, tareas, API exacta de lo que se crea, tabla de mapeo y criterios de cierre.
- Una parte = un commit/PR. No mezcles partes.
- Si una parte necesita algo de otra (p. ej. un token o un helper) y no existe todavía, créalo tú siguiendo la especificación de la sección B — están descritos con ruta y API exactas para que cualquier parte pueda crearlos de forma idéntica.
- Al terminar una parte, marca su casilla en la sección D (Registro).
Orden recomendado (las flechas son dependencias blandas, ver punto 4):
P0 → P1 → P2
↘ P3 (Artwork/Avatar) ─┐
↘ P4 (Botones) ───────┤
↘ P5 (Helpers TS) ─────┼→ P6 (Menús) → P7 (Filas) → P8 (Tablas) → P9 (Tarjetas)
└→ P10 (Cabeceras) → P11 (Formularios) → P12 (Chrome) → P13 (Páginas) → P14 (Barrido final)
A. Contexto común (leer siempre)
A.1 Stack y reglas del proyecto (de web-player/CLAUDE.md)
- SvelteKit 2 + Svelte 5 runes (
$props()coninterface Props,$state,$derived,$effect; nada deexport letni$:;{#each}siempre con clave). - Tailwind 4: no hay
tailwind.config.js; la config vive ensrc/routes/layout.css(@theme,@utility). Tokens de color crudos ensrc/lib/theme/theme.css(--mf-*), tokens dinámicos por hue ensrc/lib/theme/tokens.ts(buildThemeTokens). - Sin comentarios en código salvo imprescindibles.
- Cierre de cada parte:
npm run checkynpm run lintlimpios (desdeweb-player/). - Iconos:
@lucide/svelte/icons/<nombre>.
A.2 Gotchas conocidos (no reintroducirlos)
- Tokens animados por hue (
--mf-bg,--mf-elevated,--mf-panel-bg,--mf-bar-bg,--mf-hairline,--mf-hero-bg,--mf-modal-glow,--mf-spotlight-bg,--mf-logo-grad,--mf-cover-grad,--mf-accent*,--mf-ambient) se aplican como estilo inline en<html>desde+layout.svelte. Un override en:root[data-theme='light']no gana: la variante clara va enbuildThemeTokens(). - Conflictos de clases Tailwind: nunca
class="bg-a {cond ? 'bg-b' : ''}". Siempre mutuamente excluyente:class={cond ? 'bg-b' : 'bg-a'}. Overrides de la clase base de un componente compartido → prop/variante, no!important. - Texto sin clase de tamaño hereda 16px del navegador. Todo texto visible debe tener una receta tipográfica (ver B.1.6).
--mf-inkes "texto sobre CTA" y se invierte en claro;--mf-tile-shade,--mf-cover-grad,--mf-liked-gradson "arte" (no invertir).- Referencia visual: Inicio (
src/routes/+page.svelte). Un cambio de refactor no debe cambiar el aspecto salvo donde la parte lo diga explícitamente (columna "Δ").
A.3 Verificación de cada parte
cd web-player
npm run check
npm run lint
Visual (Browser pane, .claude/launch.json → musify-web; login de dev en la memoria del
proyecto): abre cada página tocada y Inicio; prueba estado activo (una canción
sonando), hover, móvil (375px) y modo claro (menú de cuenta → "Modo blanco").
A.4 Línea base medida (para saber si avanzas)
| Métrica | Hoy |
|---|---|
Componentes .svelte (lib + rutas components/) |
61 |
Líneas .svelte/.css/.ts cliente |
~8.700 |
Clases arbitrarias x-[…] |
44 |
Espaciados fraccionarios (h-3.75, gap-2.75…) |
~190 apariciones, 60 valores distintos |
| Radios distintos usados | 13 (6 tokens + 7 de Tailwind por defecto) |
strokeWidth distintos |
13 valores |
Atributos style="…" |
24 |
Pluralizaciones a mano === 1 ? |
15 |
Errores de eslint |
2 (liked/+page.svelte) |
Objetivo global: ~61 → ~45 componentes, −1.500/−2.000 líneas, 0 arbitrarios fuera de una lista blanca, 1 escala por dimensión.
B. Especificaciones compartidas (fuente única)
Cualquier parte que necesite algo de aquí y no exista, lo crea exactamente así.
B.1 Tokens (en src/routes/layout.css, dentro de @theme)
Tailwind 4 genera utilidades a partir de estos namespaces: --radius-* → rounded-*,
--spacing-* → size-* w-* h-* p-* m-* gap-*, --container-* → max-w-*,
--tracking-* → tracking-*, --leading-* → leading-*.
B.1.1 Radios (--radius-*)
| Token | Valor | Para qué |
|---|---|---|
rounded-tag |
0.25rem |
badges, celdas mini (ExplicitBadge, índice) |
rounded-thumb |
0.5rem |
carátulas ≤ 36px, logo |
rounded-control |
0.625rem |
inputs, filas, items de menú, botones |
rounded-art |
0.8125rem |
carátulas 42–88px, tarjetas de grid |
rounded-art-lg |
1.125rem |
carátulas ≥ 136px, dropzones |
rounded-panel |
1.125rem |
contenedores, menús, modales, tiles |
rounded-panel-lg |
1.375rem |
hero, spotlight, "mejor resultado" |
rounded-full |
(Tailwind) | pastillas, avatares, botones redondos |
Eliminar --radius-art-round (0 usos). Mapeo de lo existente:
| Hoy | px | Usos | Pasa a | Δ |
|---|---|---|---|---|
rounded-sm, rounded |
4 | 2 | rounded-tag |
0 |
rounded-md |
6 | 1 | rounded-thumb |
+2px |
rounded-lg |
8 | 4 | rounded-thumb |
0 |
rounded-xl |
12 | 6 | rounded-control |
−2px |
rounded-2xl |
16 | 8 | rounded-art-lg (arte) / rounded-panel (contenedor) |
+2px |
rounded-3xl |
24 | 1 | rounded-panel-lg |
−2px |
B.1.2 Tamaños de carátula/arte (--spacing-cover-*)
| Token | Valor | Usos actuales que absorbe (Δ) |
|---|---|---|
cover-xs |
2.25rem (36) |
thumbs de menús h-9; sidebar h-8.5 (+2) |
cover-sm |
2.625rem (42) |
filas de tabla h-10.5; cola y "Populares" h-10 (+2); player móvil h-11 (−2) |
cover-md |
3rem (48) |
player escritorio md:h-12 |
cover-lg |
3.5rem (56) |
"Continuar" h-14; filas de búsqueda h-13 (+4); tarjeta playlist de Inicio h-15.5 (−6) |
cover-xl |
5.5rem (88) |
"Mejor resultado" h-22 |
cover-2xl |
8.5rem (136) |
spotlight de Inicio h-34 |
cover-hero |
9rem (144) |
cabeceras de colección, dropzones, avatar de perfil |
cover-hero-sm |
10.25rem (164) |
cabeceras de colección a partir de sm: |
Uso: size-cover-sm (Tailwind 4 size-* = width+height). Nunca h-X w-X en
pareja: siempre size-*.
B.1.3 Iconos (--spacing-icon-*) y trazo
| Token | Valor | Absorbe |
|---|---|---|
icon-xs |
0.875rem |
h-3.5 (14) |
icon-sm |
1rem |
h-3.75 (15), h-4 (16), h-4.25 (17) |
icon-md |
1.125rem |
h-4.5 (18) |
icon-lg |
1.25rem |
h-5 (20), h-6 (24, login móvil) |
icon-xl |
2.25rem |
h-9/h-10 (estados vacíos, dropzone, Me gusta) |
Trazo: en layout.css, fuera de @theme:
@layer base {
svg.lucide {
stroke-width: 1.8;
}
}
@utility stroke-thin { stroke-width: 1.3; }
@utility stroke-regular { stroke-width: 1.8; }
@utility stroke-bold { stroke-width: 2.5; }
La propiedad CSS stroke-width gana al atributo de presentación que pone Lucide, así que
el prop strokeWidth deja de tener efecto: se elimina de todos los usos. Mapeo:
1–1.4 → stroke-thin, 1.5–2 → (nada, default), 2.5–3 → stroke-bold,
strokeWidth={0} + fill → clase stroke-0. Verifica en el navegador (DevTools →
Computed stroke-width) que el default aplica antes de migrar en masa.
B.1.4 Anchos (--container-*) y layout
| Token | Valor | Absorbe |
|---|---|---|
max-w-prose-sm |
26rem |
max-w-105 (error), max-w-108 (hero p), max-w-110, max-w-115 (sin resultados) |
max-w-hero |
34rem |
max-w-135 (hero, seekbar) |
max-w-search |
30rem |
max-w-120 (buscador TopBar) |
max-w-cta |
20rem |
max-w-80 (botones login) |
Ya existen y se mantienen en theme.css: --mf-sidebar-w, --mf-queue-w, --mf-nav-h,
--mf-player-h, --mf-safe-b. Añadir:
--mf-player-side-w: 16rem; /* w-64 de PlayerBar/PlayerExtras */
--mf-menu-w: 15rem; /* w-60 de menús contextuales (= MENU_WIDTH 240) */
--mf-submenu-w: 16rem; /* w-64 (= SUBMENU_WIDTH 256) */
--mf-range-h: 3.5px; /* barra de progreso y volumen */
--mf-range-thumb: 11px;
--mf-volume-w: 84px;
Las constantes TS MENU_WIDTH/SUBMENU_WIDTH (duplicadas hoy en TrackContextMenu y
AlbumContextMenu) pasan a src/lib/config.ts con un comentario-no: el nombre basta.
B.1.5 Capas (z-index)
En theme.css :root: --z-raised: 10; --z-sticky: 20; --z-backdrop: 30; --z-menu: 40; --z-submenu: 50; --z-modal: 60; --z-grain: 80;. Uso: z-(--z-menu). Mapeo 1:1 con los
valores actuales (z-10,z-20,z-30,z-40,z-50,z-[60], body::after 80). Nota:
MobileHeader y PlayerDock usan z-40 pero son "sticky", no menú → --z-sticky
(verificar que el menú de cuenta sigue encima del header: menú 40 > sticky 20).
B.1.6 Tipografía (recetas @utility en layout.css)
Escala tipográfica = la de Tailwind (text-xs…text-4xl), sin tamaños arbitrarios.
Añadir en @theme: --tracking-display: -0.02em; --tracking-eyebrow: 0.1em; --leading-body: 1.65;. Recetas (el color no va en la receta, se añade aparte):
@utility text-display-1 { @apply font-display text-3xl font-medium tracking-tight sm:text-4xl; }
@utility text-display-2 { @apply font-display text-2xl font-medium tracking-tight; }
@utility text-display-3 { @apply font-display text-xl font-medium tracking-display; }
@utility text-display-4 { @apply font-display text-lg font-medium tracking-tight; }
@utility text-eyebrow { @apply text-xs font-medium tracking-widest uppercase; }
@utility text-body { @apply text-sm leading-body; }
@utility text-count { @apply text-xs tabular-nums; }
Si
@applycon variantesm:dentro de@utilityno compila, usa@media (width >= 40rem) { font-size: var(--text-4xl); line-height: var(--text-4xl--line-height); }como hacepage-x.
Mapeo de lo existente:
| Patrón hoy | Receta |
|---|---|
font-display text-3xl … sm:text-4xl (PageHeader, CollectionHeader, hero Inicio, h1 búsqueda text-2xl sm:text-3xl font-semibold tracking-[-0.03em]) |
text-display-1 (Δ búsqueda: +1 paso) |
font-display text-2xl (stats Inicio, título spotlight, "¡Subida!") |
text-display-2 |
font-display text-xl … tracking-[-0.02em] (Modal, EmptyState, +error, "Sin resultados", diálogos de borrado) |
text-display-3 (todos font-medium) |
font-display text-lg (SectionHeading, "Mejor resultado", Queue "En cola" text-base) |
text-display-4 |
text-xs font-medium tracking-widest uppercase (+ variantes tracking-[0.14em], [0.16em], tracking-wider, text-sm font-semibold) |
text-eyebrow |
text-sm leading-[1.65] text-fg-2 |
text-body text-fg-2 |
text-xs text-muted tabular-nums |
text-count text-muted |
tracking-[-0.01em] (MixTile) |
quitar |
B.1.7 Recetas de superficie y movimiento (@utility en layout.css)
@utility theme-transition { transition: background 0.5s ease-out; } /* 8× transition-[background] duration-500 */
@utility glass-bar { /* PlayerBar ×2, TabsBar */
background-image: var(--mf-bar-bg);
border-top: 1px solid var(--mf-hairline);
backdrop-filter: blur(24px);
transition: background 0.5s ease-out;
}
@utility grid-cards { /* MediaGrid por defecto: 150/165 */
display: grid;
gap: --spacing(3);
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
@media (width >= 40rem) {
gap: --spacing(4);
grid-template-columns: repeat(auto-fill, minmax(10.3125rem, 1fr));
}
}
@utility grid-cards-lg { /* /playlists y /u/[id]: 180/210 */
display: grid;
gap: --spacing(3);
grid-template-columns: repeat(auto-fill, minmax(11.25rem, 1fr));
@media (width >= 40rem) {
gap: --spacing(4);
grid-template-columns: repeat(auto-fill, minmax(13.125rem, 1fr));
}
}
@utility grid-tiles { /* mixes de Inicio: 164 */
display: grid;
gap: --spacing(5);
grid-template-columns: repeat(auto-fill, minmax(10.25rem, 1fr));
}
@utility grid-wide { /* playlists de Inicio (258) y géneros (268) → 16.5rem, Δ ±6px */
display: grid;
gap: --spacing(3.5);
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
}
.glass-panel ya existe; Modal debe usarla en lugar de su style= inline duplicado.
Stagger (sustituye los 5 style="animation-delay:…" con 40 o 45ms):
.animate-enter { animation-delay: calc(min(var(--i, 0), 10) * 45ms); }
Uso: style="--i:{i}". Único style= permitido junto con los de B.4.
B.1.8 Colores: limpieza
- Borrar utilidades sin uso:
--color-accent-dim,--color-accent-glow,--color-accent-hair,--color-accent-line,--color-accent-muted(0 usos como clase;--mf-accent-dimsí se usa en::selection, se mantiene la variable--mf-). --color-on-accentes alias de--mf-on-art(igual que--color-on-art) → borrar y usaron-art.--color-hover=--mf-surface-2(igual quesurface-2) → mantener solohovercomo semántico de hover y dejarsurface-2para fondos en reposo.--mf-sidebar-bgno lo usa ningún componente (Sidebar usabg-bg) → borrar detheme.cssy debuildThemeTokens.- Botón:
--mf-btn-bg=--mf-surface-2;--mf-btn-secondary-bgsolo difiere en modo claro. Exponer en@theme--color-btn,--color-btn-hover,--color-btn-glass,--color-btn-glass-hovery eliminar losbg-[var(--mf-btn-…)]deButton.svelte. - Scrollbar:
rgba(255,255,255,.18/.32)fijo enlayout.css(roto en claro) → tokens--mf-scrollbar,--mf-scrollbar-hovercon variante en:root[data-theme='light']. MixTile:to-black/42,bg-ink/76,border-on-art/12→to-scrim/40, receta dePlayButton(B.3).
B.2 Helpers TypeScript (fuente única)
| Archivo (nuevo) | API | Sustituye |
|---|---|---|
src/lib/actions/pressable.ts |
pressable(node, fn) → añade role="button", tabindex=0, click + Enter/Espacio |
7 copias de onkeydown Enter/Space (Inicio ×5, Queue, TrackTitleCell) |
src/lib/actions/clickOutside.ts |
clickOutside(node, fn) |
onDocumentClick de AccountMenu |
src/lib/toggle.svelte.ts |
createToggle(initial=false) → { get open, show(), close(), toggle() } |
playlists.svelte.ts y player/queuePanel.svelte.ts (idénticos) |
src/lib/format.ts (ampliar) |
plural(n, one, many) → "1 canción"; fmtCount(n,'canción','canciones'); minúscula siempre |
15 pluralizaciones a mano, con mayúsculas inconsistentes ("Canción"/"canción") |
src/lib/navigation.svelte.ts (ampliar) |
searchHref(q); appNavLinks(user, counts?) → {href,label,icon,count?,primary:boolean}[] |
buildHref en TopBar y explore; listas de links de Sidebar y TabsBar |
src/lib/player/player.svelte.ts (ampliar) |
isQueueCurrent(items), playAllOrToggle(items), playShuffled(items); renombrar addToQueue → playNextItem |
playAll/isCurrentQueue en álbum, playlist, liked, mixes, spotlight de Inicio (5 copias) |
src/lib/player/player.svelte.ts |
toQueueItem(track) único (normaliza duration/listensCount a number) |
mapeos a mano en recentlyPlayed.ts (×2), liked.svelte.ts (×2), liked/+page, tracks.ts#targetForQueueItem y los Number(…) dispersos |
src/lib/menus.svelte.ts |
createTrackMenu() → { state, open(e, track), close(), playNext() } |
estado + handler de menú contextual en Inicio, Explorar, Mezcla |
B.3 Componentes nuevos (API)
Componente ($lib/components/ui/) |
Props | Sustituye / absorbe |
|---|---|---|
Artwork.svelte |
src?, trackIds?: (string|number)[], size: 'xs'|'sm'|'md'|'lg'|'xl'|'2xl'|'hero', shape?: 'auto'|'round', imageSize?: 'small'|'medium'|'large' (auto por size), fallback?: 'music'|'gradient'|'none', class?, children? (overlay) |
Cover, PlaylistArt, MixArt; radio automático por tamaño (xs→thumb, sm–xl→art/control, 2xl/hero→art-lg). Prioridad: src → mosaico si ≥4 trackIds → primera carátula → fallback. Un único onerror. |
Avatar.svelte |
name, src?, size: 'sm'|'md'|'lg'|'hero', class? |
iniciales/imagen en TopBar, MobileHeader, Explorar (×2), ArtistAvatar |
ListRow.svelte |
href? | onclick?, oncontextmenu?, active?, size?: 'sm'|'md'|'lg', title, subtitle?, subtitleHref?, explicit?, meta?, leading?: Snippet, art: Snippet<[string]> | artwork props, trailing?: Snippet |
SearchResultRow + 7 filas a mano (ver P7) |
MediaIdentity.svelte |
title, subtitle?, subtitleHref?, explicit?, active?, art?: Snippet | artwork props, titleClass? |
TrackTitleCell (renombrado + generalizado) y el bloque de player/TrackInfo |
TrackList.svelte |
tracks: TrackLike[], columns?: ('album'|'added'|'uploaded'|'plays')[], index?, onPlay(i), rowAction?: {action, icon, label} | rowActionSnippet?, oncontextmenu?(e, track) |
TrackTable+TrackRow+TrackIndexCell+TrackMeta y las 6 tablas (P8) |
ContextMenu.svelte |
x, y, openLeft, onClose, items: {icon,label,onclick}[], playlistAction?: {action: string, fields: Record<string,string>, label}, playlists |
TrackContextMenu + AlbumContextMenu (90% idénticos) |
PlayButton.svelte |
playing?, size: 'sm'|'md'|'lg', variant: 'strong'|'glass', label, onclick?, as?: 'button'|'span' |
play de TransportControls, "Mejor resultado", overlay de MixTile |
ConfirmDialog.svelte |
open, onClose, title, description, confirmLabel, action (form POST) |
2 modales de borrado con estilos distintos |
Slider.svelte |
value, max, label, oninput, variant: 'seek'|'volume' |
SeekBar (click-only) + VolumeControl (<style> propio) |
Logo.svelte |
size: 'sm'|'lg' |
logo en Sidebar (sin marca), MobileHeader, login |
Chip.svelte |
selected?, count?, href?|onclick? |
chips de filtro y de géneros sugeridos en Explorar |
B.4 Lista blanca de style= y arbitrarios (tras el refactor)
Permitidos: style="--i:{i}" (stagger), gradientes de tile por hue en Explorar
(--tile-hue), anchos de progreso (width:{pct}%), posición de menú (left/top).
Arbitrarios permitidos: text-[clamp(…)] de login, grid-cols-[auto_1fr],
max-h-[85dvh]. Todo lo demás, a token.
C. Partes
P0 — Línea base y bugs sueltos (independiente, ~30 min)
Objetivo: dejar lint en verde y corregir bugs detectados que no requieren refactor.
Tareas:
src/routes/(app)/liked/+page.svelte: quitar importsPlayyPause(errores eslint).src/lib/components/ui/MediaCard.svelte: el título no tiene tamaño (hereda 16px) →text-sm(igual queMixTile).src/routes/+page.svelte, filas del spotlight:hover:bg-hover … {cond ? 'bg-accent-tint' : ''}→ excluyente ({cond ? 'bg-accent-tint' : 'hover:bg-hover'}), igual que las otras filas.src/routes/(app)/albums/[id]/+page.sveltemodal de borrado:text-base text-fg-3→text-sm leading-[1.65] text-fg-2y título igual al de playlists (se unificará en P10).Sidebar.svelte(botón+) yQueue.svelte(cerrar): quitartext-baseinútil.- Borrar
src/lib/components/ui/Badge.svelte(0 importadores). Button.svelte:primaryysecondaryson la misma clase. No cambiar aspecto; solo dejarconst variants = { primary: subtle, secondary: subtle, … }con un únicosubtley anotar en D que P4 decide siprimarydebe diferenciarse (decisión de producto, preguntar al usuario).
Cierre: npm run lint 0 errores.
P1 — Tokens base (prerrequisito de P3–P14)
Objetivo: crear todo B.1 de forma aditiva (sin migrar componentes todavía, salvo lo indicado). Tras esta parte, nada cambia visualmente.
Leer: src/routes/layout.css, src/lib/theme/theme.css, src/lib/theme/tokens.ts.
Tareas:
- Añadir en
@theme: radios B.1.1 (y borrar--radius-art-round),--spacing-cover-*B.1.2,--spacing-icon-*B.1.3,--container-*B.1.4,--tracking-*/--leading-*B.1.6,--color-btn*B.1.8. - Añadir en
theme.css:root: variables de layout B.1.4 y z-index B.1.5. - Añadir
@utilityde B.1.3 (stroke), B.1.6 (tipografía), B.1.7 (superficies, grids); stagger en.animate-enter. - Scrollbar a tokens con variante clara (B.1.8).
- Borrar utilidades de color sin uso y
--mf-sidebar-bg(B.1.8). Antes de borrar cada una:grep -rn "<nombre>" src= 0. - Crear una página de prueba temporal no commiteada o usar DevTools para comprobar
que
size-cover-sm,rounded-thumb,max-w-hero,text-display-1,z-(--z-menu)ystroke-thingeneran CSS (Tailwind 4 solo genera lo que ve usado: prueba con una clase en un componente y revierte).
Cierre: check+lint; captura de Inicio antes/después idéntica.
P2 — Tema: una sola fuente de verdad + sin parpadeo en modo claro
Objetivo: eliminar la duplicación theme.css ↔ tokens.ts y el flash oscuro al
cargar con modo claro guardado.
Hallazgos:
- Los valores por defecto de los ~15 tokens dinámicos están escritos dos veces: en
theme.css(:root, hue 145) y enbuildThemeTokens(). El bloque claro detheme.cssrepite--mf-bg,--mf-elevated,--mf-panel-bg,--mf-bar-bg,--mf-hairline,--mf-hero-bg,--mf-spotlight-bg,--mf-modal-glow, que luegotokens.tspisa inline. mode.svelte.tsaplicadata-themesolo tras hidratar → flash oscuro.
Tareas:
src/app.html: script inline en<head>que lealocalStorage['musify.theme']y pongadocument.documentElement.dataset.themeantes del primer paint (try/catch).+layout.svelte: en<svelte:head>, emitir<style>:root{…buildThemeTokens(ACCENT_HUE,'dark')}:root[data-theme=light]{…buildThemeTokens(ACCENT_HUE,'light')}</style>(serializado con un helpertokensToCss(tokens)entokens.ts).- Borrar de
theme.csstodos los tokens que generabuildThemeTokens(dark y light).theme.cssqueda solo con neutrales fijos, superficies, texto, layout y z-index. +layout.svelte: los dosdivde fondo constyle=→@utility app-backdrop/app-vignetteenlayout.css.mainconpadding-bottom: calc(…)inline → clase con--mf-dock-hque se activa condata-has-tracken el propiomain.
Cierre: con modo claro guardado, recarga dura sin flash; cambio de pista sigue animando el hue; check+lint.
P3 — Artwork y Avatar (arte unificado)
Objetivo: un único componente de carátula y uno de avatar.
Hallazgos:
Cover,PlaylistArtyMixArtimplementan la misma cascada (imagen → mosaico 2×2 → primera carátula → placeholder).MixArt≡PlaylistArtcontrackIds = items.map(i => i.trackId).AlbumCardpasa el id del álbum comotrackIdaCoversolo para poder usarsrc(hack).- Avatar "imagen o inicial" reimplementado en
TopBar,MobileHeader, Explorar (mejor resultado + fila de usuario) yArtistAvatar(con rayado). - Cada uso decide su propio radio (
rounded-lg,rounded-xl,rounded-2xl,rounded-control,rounded-art-lg…) y su tamaño (h-8.5,h-9,h-10,h-10.5,h-11 md:h-12,h-13,h-14,h-15.5,h-22,h-34,h-36 sm:h-41).
Tareas:
- Crear
Artwork.svelteyAvatar.sveltesegún B.3 usando tokens B.1.1/B.1.2.sizedecide dimensión y radio;classsolo para layout (shrink-0, márgenes). - Migrar todos los usos (buscar
Cover,PlaylistArt,MixArt,ArtistAvatar,user.picture,profilePictureUrl) aplicando la tabla B.1.2. ArtistAvatar→ enu/[id]/+page.svelteusar<Avatar size="hero">(el nombre ya lo muestra la cabecera; el label bajo el avatar era redundante — Δ visible, confirmar).- Borrar
Cover.svelte,PlaylistArt.svelte,MixArt.svelte,ArtistAvatar.svelte.
Cierre: grep -rn "Cover\b\|PlaylistArt\|MixArt\|ArtistAvatar" src solo en Artwork;
visual en Inicio, Explorar (búsqueda), Sidebar, Cola, player, cabeceras.
P4 — Botones: Button, IconButton, PlayButton
Objetivo: que no quede ningún <button> estilado a mano.
Hallazgos (botones hechos a mano): TopBar atrás/adelante (círculos h-7.5), limpiar
búsqueda, login (bg-accent-soft rounded-full), MobileHeader login, Sidebar + ×2, Cola
cerrar/"Vaciar", PlayerExtras cola, TransportControls (shuffle, prev, play, next, repeat),
TrackInfo corazón, "Mejor resultado" (círculo h-11.5), MixTile overlay (h-9.5),
BackLink. Overrides con !: Inicio !bg-cta-strong !text-ink hover:!brightness-95 y
!font-normal.
Tareas:
Button.svelte: variantessubtle(hoy primary/secondary),glass(hoysecondaryOnGlow),accent,strong(cta-strong/ink — sustituye los!),danger. Mantener aliasprimary/secondarysolo si P0 no obtuvo decisión. Radio →rounded-control. Quitar necesidad de!font-normal(peso normal englass, o propweight).IconButton.svelte: añadirshape: 'square'|'round',size: 'xs'|'sm'|'md'|'lg',tone: 'muted'|'subtle'|'plain',pressed?: boolean(→aria-pressed+text-accent),href?,surface?: boolean(fondosurface-2). Iconos consize-icon-*.- Crear
PlayButton.svelte(B.3); iconos play/pausa SVG deTransportControlspasan a él (un único sitio para esos<svg>). - Migrar todos los botones listados.
TopBar"adelante" es un<span>decorativo deshabilitado yPlayerExtrastiene un iconoAlignJustifydecorativo sin función: preguntar si se eliminan (código muerto visual) o se mantienen por fidelidad al prototipo.
Cierre: grep -rn "<button" src | grep -v "ui/\(Button\|IconButton\|PlayButton\|Chip\|ListRow\)"
→ solo casos justificados (dropzones, selector segmentado).
P5 — Helpers TypeScript
Objetivo: crear B.2 y migrar sus usos (lógica, no estilos).
Tareas:
- Crear
actions/pressable.ts,actions/clickOutside.ts,toggle.svelte.ts.createPlaylistModalyqueuePanel→createToggle(); borrar los dos archivos viejos o reducirlos aexport const queuePanel = createToggle(). format.ts:plural(); migrar las 15 pluralizaciones (grep -rn "=== 1 ?" src). Unificar a minúscula ("3 canciones") — Δ visible en Playlists/Inicio/liked.player.svelte.ts:toQueueItem,isQueueCurrent,playAllOrToggle,playShuffled; renombraraddToQueue→playNextItem(el nombre actual confunde: inserta a continuación, no al final). Migrar álbum, playlist, liked, mixes, Inicio.recentlyPlayed.tsyliked.svelte.ts: usartoQueueItem.tracks.ts: el envoltorioTrackTarget = { track }+ 8 accesores (targetTitle,targetArtist, …) no aporta nada. Sustituir por elApiTrackLikedirecto y borrar accesores; dejar soloisCurrent(track)si hace falta. (Toca Inicio, Explorar, Mezcla, menús: hacerlo en esta parte para no dejar mezcla de estilos.)navigation.svelte.ts:searchHref,appNavLinks.menus.svelte.ts:createTrackMenu(); migrar Inicio, Explorar, Mezcla.
Cierre: check+lint; reproducir desde álbum, playlist, liked, mezcla, spotlight; menú contextual "Reproducir a continuación" sigue funcionando.
P6 — Menús: ContextMenu + AccountMenu + GlassMenu
Objetivo: un solo menú contextual.
Hallazgos: TrackContextMenu (109 líneas) y AlbumContextMenu (119) son copias:
misma función de posicionamiento con las mismas constantes, mismo backdrop, mismo Escape,
mismo submenú "Añadir a playlist" con <form use:enhance>; solo cambian los items y los
campos ocultos. Sus items reimplementan a mano las clases de MenuItem (con
strokeWidth={2} en vez de 1.8). El item de playlist reimplementa ListRow.
Tareas:
- Crear
ContextMenu.svelte(B.3) usandoGlassMenu+MenuItem+Artwork size="xs"y anchos--mf-menu-w/--mf-submenu-w;contextMenuPosition(event)exportada del módulo usandoMENU_WIDTH/SUBMENU_WIDTHdeconfig.ts. - Sustituir usos en Inicio, Explorar (pista y álbum), Mezcla. Borrar los dos menús viejos.
AccountMenu: usarclickOutside; eliminar eldiv.relativeduplicado queTopBar/MobileHeaderponen alrededor;panelClass→ propwidth: 'sm'|'md'.- Añadir Escape a
AccountMenu(hoy no cierra con Escape — mejora a11y).
Cierre: menú de pista y de álbum en Explorar, submenú abre a izquierda cerca del borde, móvil (submenú debajo), Escape cierra.
P7 — Filas: ListRow + MediaIdentity + EqBars
Objetivo: una sola fila compacta para todas las listas que no son tabla.
Filas reimplementadas hoy (todas: arte + título + subtítulo + meta + estado activo):
| Dónde | Tamaño |
|---|---|
SearchResultRow (Explorar ×4 grupos) |
lg |
| Inicio "Continuar escuchando" (tarjeta) | lg, card |
| Inicio "Populares" (índice/EQ + meta) | sm |
| Inicio spotlight (índice, sin arte) | sm |
| Inicio "Tus playlists" (tarjeta) | lg, card |
Queue "Reproduciendo" + "A continuación" |
sm |
Sidebar playlists |
xs |
items de playlist de ContextMenu |
xs |
Tareas:
- Renombrar/generalizar
TrackTitleCell→MediaIdentity(B.3). Usar enplayer/TrackInfo(sustituye su bloque cover+título+artista). - Crear
ListRow(B.3) sobreMediaIdentity+pressable; variantecard= paddingp-2.25 pr-3.5 rounded-art(Continuar/Tus playlists). NowPlaying≡EqBarscon overlay: añadir aEqBarspropoverlay(fondoscrim/45, coloron-art) y borrarNowPlaying.- Migrar todas las filas de la tabla. Borrar
SearchResultRow.
Cierre: Inicio, Explorar, Cola, Sidebar sin cambios visibles salvo Δ de B.1.2; teclado (Tab + Enter) reproduce en todas las filas.
P8 — Tablas de pistas: TrackList
Objetivo: tablas declarativas; borrar 3 celdas y 3 componentes de ruta.
Hallazgos:
- 6 tablas (
library,liked,mixes/[id],AlbumTrackTable,PlaylistTrackTable,LibraryPicker) repiten:{@const active},TrackIndexCell,TrackTitleCell, NTrackMeta, y un<form method=POST use:enhance><input hidden trackId><IconButton revealOnHover>. - Anchos de columna pasados desde cada página e inconsistentes: "Escuchas"
106pxen unas y100pxen otras. - La columna "Álbum" siempre pinta "—" (library, liked, playlist): placeholder sin datos. Decisión: eliminarla (recomendado) o dejarla en el preset.
Tareas:
- Crear
TrackList.svelte(B.3) con presets de columna en el propio módulo:{ album: {label:'Álbum', width:'76px'}, added: {'Añadida','124px'}, uploaded: {'Subida','124px'}, plays: {'Escuchas','104px'} }y columnas fijas (index 32px/28px, títulominmax(180px,1fr)/minmax(120px,1fr), duración104px/52px, acción36px/32px) como constantes del módulo. rowAction={{ action: '?/removeTrack', icon: X, label: 'Quitar de la playlist' }}genera el form+IconButton; para liked (onclick) aceptarrowAction.onclick.- Migrar las 6 tablas; inlinear
AlbumTrackTable,PlaylistTrackTable,LibraryPickeren sus páginas y borrarlos. BorrarTrackRow,TrackIndexCell,TrackMeta,TrackTable. LibraryPickerusa overlayNowPlayingen vez de índice:TrackListconindex={false}muestra EQ sobre el arte (víaArtworkchildren).
Cierre: las 6 páginas; hover revela acción; activo muestra EQ; móvil oculta metas.
P9 — Tarjetas y grids
Objetivo: una tarjeta, cero wrappers.
Hallazgos: AlbumCard y PlaylistCard solo construyen el subtítulo y eligen el arte;
MixTile es una MediaCard con overlay de play y tipografía distinta; MediaGrid es un
<div> con variables inline; Inicio (mixes 164px, playlists 258px) y Explorar (géneros
268px) escriben su propio grid-cols-[repeat(auto-fill,minmax(…))].
Tareas:
MediaCard: propshref,title,subtitle?,onPlay?(overlayPlayButton variant="glass"),artsnippet o props deArtwork,oncontextmenu?,index. Tipografía fija (text-sm text-fg/text-xs text-fg-3).- Helpers en
format.ts:albumMeta(album),playlistMeta(playlist). - Sustituir
AlbumCard,PlaylistCard,MixTileporMediaCard;MediaGridporgrid-cards/grid-cards-lg/grid-tiles/grid-wide(B.1.7). Borrar los 4 archivos. - Tile de género de Explorar:
stylecon--tile-huey el gradiente en un@utility genre-tileque leavar(--tile-hue).
Cierre: /albums, /playlists, /u/[id], Inicio (mixes), Explorar (géneros).
P10 — Cabeceras, diálogos y tipografía
Objetivo: 1 cabecera de página, 1 de sección, 1 diálogo de confirmación.
Hallazgos:
PageHeader≡CollectionHeadersin portada/eyebrow. La cabecera de búsqueda de Explorar reimplementa ambos (eyebrowtext-sm font-semibold tracking-[0.16em]).AlbumHeader/PlaylistHeader(rutas) solo montan el meta y los botones.- Cada página decide el margen tras la cabecera (
mt-7 sm:mt-8,mt-6 sm:mt-8,mt-9,mt-10 sm:mt-12…). - 2 diálogos de borrado con tipografías distintas.
EmptyStateduplicado literalmente en Inicio ("Todavía no has escuchado nada" ×2).
Tareas:
- Fusionar en
PageHeader:title,eyebrow?,description?,meta?,cover?,actions?,align?. La cabecera fija su propio margen inferior (mb-7 sm:mb-8); quitar losmt-*de lo que venga después. BorrarCollectionHeader. - Inlinear
AlbumHeader/PlaylistHeaderen sus páginas (conplural()y elBackLink) y borrarlos. Iconos de botones: unificar (álbum tiene iconos, playlist no) → decisión: con iconos en ambos (recomendado) o sin ellos. SectionHeading: propcount?que pinte la línea + "N resultados" (hoy repetido 4× en Explorar) yhref?/linkLabel?para "Ver todas".ConfirmDialog(B.3); migrar álbum y playlist.Modalusa.glass-panelyz-(--z-modal); su eyebrow y título con recetas.- Aplicar recetas B.1.6 en:
EmptyState,+error.svelte, "Sin resultados", hero de Inicio, stats,Queue,Sidebar("Tus playlists"),PlaylistForm(visibilidad). - Inicio: estado vacío repetido → snippet local.
Cierre: todas las páginas con cabecera; diálogos de borrado idénticos.
P11 — Formularios
Objetivo: un único esqueleto de formulario con portada.
Hallazgos:
AlbumFormyPlaylistFormcomparten:use:enhanceconsubmitting,ImageDropzone gradient class="h-36 w-36 rounded-2xl",Field/Input/Textarea,Alert, pie Cancelar/Enviar.upload/+page.sveltereimplementa el selector de portada (label + input + preview + gradiente) que ya haceImageDropzone.InputyTextarearepiten la misma cadena de clases.- Toggle Privada/Pública (
w-[calc(50%-4px)]) y chips de Explorar: selectores de una opción con estilos propios.
Tareas:
CoverForm.svelte(esqueleto): propsaction,coverFallbackUrl?,coverIcon,requireCover?,formMessage?,submitLabel,submittingLabel,canSubmit,helperText?,onCancel,onSuccess?, snippetfields.AlbumFormyPlaylistFormquedan como ~30 líneas de campos cada uno (o se inlinean en sus páginas si solo se usan en un sitio:AlbumFormse usa en 2,PlaylistFormen 2 → mantener).ImageDropzone:size="hero"usandosize-cover-hero+rounded-art-lg; upload la usa (Δ: icono de 64px →icon-xl).@utility field-controlenlayout.csscon la cadena común deInput/Textarea.SegmentedControl.svelte(options,valuebindable) para Privada/Pública;Chip(B.3) para Explorar.
Cierre: crear/editar álbum y playlist, subir canción (drag&drop incluido).
P12 — Chrome de la app (layout, navegación, reproductor)
Objetivo: una sola definición de navegación y un reproductor más pequeño.
Hallazgos:
SidebaryTabsBardefinen cada uno su lista de links +isActive.MobileHeadercompensa los links que faltan enTabsBarcon items extra en el menú.- Logo: gradiente + texto en MobileHeader y login; solo texto en Sidebar.
PlayerBarrepite dos veces la cadena de "barra de cristal" (igual queTabsBar).PlayerExtras(22 líneas) solo compone volumen + botón de cola.SeekBares un<button>de click (sin teclado ni arrastre) conh-[3.5px];VolumeControles un<input range>con<style>propio (84px,11px,3.5px).TrackInfoduplica el bloque identidad de pista (→MediaIdentity, P7).
Tareas:
appNavLinks()(B.2) en Sidebar y TabsBar (primary= los 4 de TabsBar).Logo.svelte; usar en Sidebar, MobileHeader y login (Δ: Sidebar gana el cuadrado de gradiente — confirmar).glass-baren PlayerBar ×2 y TabsBar;theme-transitionen Sidebar, Queue, etc.- Fusionar
PlayerExtrasdentro dePlayerBar; anchos laterales--mf-player-side-w. Slider.svelte(B.3) con<input type="range">estilado vía tokens--mf-range-h/--mf-range-thumb/--mf-volume-w; sustituyeSeekBaryVolumeControl(mejora: arrastre y teclado en la barra de progreso). Borrar ambos.TopBar: usarsearchHref,IconButton,Avatar;max-w-search.- z-index de header/dock/menús con B.1.5.
Cierre: navegación desktop/tablet/móvil, cola abre/cierra, seek por click, arrastre y flechas, volumen.
P13 — Páginas grandes: Explorar (602 líneas) e Inicio (466)
Objetivo: que las páginas sean composición, no lógica ni marcado repetido. Hacer
después de P7–P10 (usa ListRow, SectionHeading count, PageHeader).
Explorar:
GENRE_INFO+genreTiles→src/lib/genres.ts.- Lógica de búsqueda (
topResult,searchCounts,searchChips,capped,showGroup,playlistMatches) →src/lib/search.ts(funciones puras). - Los 4 bloques de grupo (Canciones/Álbumes/Usuarios/Playlists) → un
{#each groups}conSectionHeading count+ListRow. - "Mejor resultado": un solo marcado con
Artwork/Avatarsegún tipo +PlayButton as="span"; eliminar el snippet duplicado button/a usandoListRow-like o<svelte:element this={href ? 'a' : 'button'}>. - Paginación
loadMore→ reutilizable si otra página la necesita; si no, dejar.
Inicio:
- Usar
Pageen vez depage-x pt-3 sm:pt-4 …a mano;gap-11.5→ token de sección (--spacing-section: 2.875rem,gap-section). - Stats ×3 idénticos →
{#each stats}. - Hero/spotlight:
style=de fondo →@utility hero-surface/spotlight-surface. - Filas →
ListRow(si P7 no lo hizo ya).
Cierre: Explorar sin query, con query, filtros, sin resultados, scroll infinito; Inicio completo con y sin datos.
P14 — Barrido final y guardarraíles
Objetivo: que no vuelva a aparecer lo que se ha limpiado.
Tareas:
- Script
web-player/scripts/check-tokens.mjs+npm run lint:tokens(añadir alint). Falla si encuentra ensrc/**/*.svelte:-\[arbitrario fuera de la lista blanca B.4;rounded-(sm|md|lg|xl|2xl|3xl)\borounded\ba secas;\b[hw]-\dcon su pareja en el mismo elemento (debe sersize-*);- espaciado fraccionario
-\d+\.\d+enh-/w-/size-(tamaños → tokens); #[0-9a-f]{3,8}en clases,rgba(,strokeWidth=,text-basesin receta;style="que no sea de la lista blanca.
- Espaciados fraccionarios restantes (
gap-2.75,py-1.75,mt-3.25,p-2.25…): redondear al paso de 0.5 más cercano o a la escala entera cuando el Δ ≤ 1px. Tabla guía:.25/.75→.5más cercano;mt-0.75→mt-1;gap-1.75→gap-2;p-4.25→p-4;mb-4.5(SectionHeading) se mantiene (Δ 2px visible en todas las secciones). - Detector de huérfanos (repetir y borrar lo que salga):
for f in $(find src/lib/components -name "*.svelte"); do n=$(basename "$f" .svelte); c=$(grep -rlw "$n" --include=*.svelte --include=*.ts src | grep -v "/$n.svelte$" | wc -l); [ "$c" -eq 0 ] && echo "$f"; done - Actualizar
web-player/CLAUDE.md→ sección "Estilos" con: B.4, "usasize-cover-*,size-icon-*, recetastext-display-*/text-eyebrow/text-body/text-count, radios semánticos; componentes:Artwork,Avatar,ListRow,TrackList,MediaCard,PageHeader,ContextMenu…". Borrardocs/design-system-refactor-plan.md. - Recalcular la tabla A.4 y anotarla en D.
D. Registro
| Parte | Estado | Commit | Notas / decisiones tomadas |
|---|---|---|---|
| P0 | ✅ | (pendiente) | Button primary/secondary se mantienen iguales (decisión del usuario) |
| P1 | ✅ | (pendiente) | Verificado con build (Tailwind compila @apply sm: dentro de @utility); sin QA visual autenticada (Docker no disponible en esta sesión) |
| P2 | ✅ | (pendiente) | <style>{expr}</style> no interpola en Svelte (se trata como CSS del componente); solución: {@html \<style>${css}</style>`}` |
| P3 | ✅ | (pendiente) | Se quitó el nombre bajo el avatar de perfil (decisión tomada: redundante con el título de CollectionHeader). Se añadió size="fill" a Artwork (no está en la spec) para las tarjetas de grid fluidas (AlbumCard/PlaylistCard/MixTile), ya que P9 (MediaCard) aún no existe. SearchResultRow perdió su prop kind/radio-por-tipo (ahora el radio lo decide Artwork/Avatar). Sin QA visual autenticada (Docker no disponible) |
| P4 | ✅ | (pendiente) | Decisión tomada sin bloquear: se elimina el botón "adelante" decorativo (TopBar) y el icono AlignJustify decorativo (PlayerExtras) por ser código muerto visual. TransportControls (shuffle/prev/next/repeat) se deja sin migrar a IconButton a propósito: son iconos sin chrome (sin fondo/hit-box), migrarlos habría añadido una píldora de hover no presente hoy — riesgo visual no verificable sin Docker |
| P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). Pendiente a propósito: la tarea 5 (eliminar el envoltorio TrackTarget de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas |
| P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) |
| P7 | ✅ | (pendiente) | MediaIdentity queda "puro" (sin onClick) según spec; las 6 tablas de pista (P8) envuelven su propio use:pressable hasta que P8 las sustituya por TrackList. Deltas documentados: fila activa de Sidebar ahora usa bg-accent-tint/text-accent-soft (antes gris neutro, ahora consistente con el resto de filas activas); tarjeta "Tus playlists" de Inicio pierde la línea "por {tu nombre}" (redundante, siempre el propio usuario) y su radio pasa de rounded-panel a rounded-art (consistencia con la tarjeta "Continuar"); filas de Queue "A continuación" pierden el opacity-80 del arte (ListRow no expone esa personalización). Sin QA visual autenticada (Docker no disponible) |
| P8 | ✅ | (pendiente) | Decisión tomada (recomendada por el propio plan, sin bloquear): se elimina la columna "Álbum" que siempre mostraba "—". TrackList unifica TrackTable+TrackRow+TrackIndexCell+TrackMeta; AlbumTrackTable/PlaylistTrackTable/LibraryPicker se inlinean en sus páginas y se borran. Sin QA visual autenticada (Docker no disponible) |
| P9 | ✅ | (pendiente) | MediaCard gana hover-zoom del arte y scrim+PlayButton condicionados a onPlay (antes exclusivos de MixTile); ahora se aplicarían a cualquier card futura con onPlay, consistencia intencional. Sin QA visual autenticada (Docker no disponible) |
| P10 | ⬜ | ||
| P11 | ⬜ | ||
| P12 | ⬜ | ||
| P13 | ⬜ | ||
| P14 | ⬜ |
Decisiones de producto pendientes (preguntar al usuario antes de la parte indicada):
- P0/P4: ¿
Buttonprimary debe verse distinto de secondary? - P3: ¿quitar el nombre bajo el avatar de perfil?
- P4: ¿eliminar el botón "adelante" decorativo y el icono
AlignJustifydel player? - P5: pluralizaciones en minúscula en toda la app.
- P8: ¿eliminar la columna "Álbum" que siempre muestra "—"?
- P10: ¿iconos en los botones de cabecera de álbum y playlist?
- P12: ¿logo con cuadrado de gradiente también en la Sidebar?
E. Inventario completo (veredicto por archivo)
Leyenda: = se mantiene · ~ se modifica · →X se fusiona en X · ✕ se borra.
src/lib/components/ui/
| Archivo | Líneas | Veredicto | Parte | Motivo |
|---|---|---|---|---|
| AccountMenu | 73 | ~ | P6 | clickOutside, Escape, ancho por prop |
| AlbumCard | 62 | ✕ →MediaCard | P9 | wrapper de meta + hack trackId=albumId |
| AlbumForm | 123 | ~ (CoverForm) | P11 | esqueleto compartido |
| Alert | 20 | = | ||
| ArtistAvatar | 36 | ✕ →Avatar | P3 | |
| BackLink | 33 | ~ | P4 | IconButton/tokens; quitar hover:text-accent-soft duplicado |
| Badge | 23 | ✕ | P0 | 0 usos |
| Button | 66 | ~ | P0/P4 | variantes duplicadas, overrides ! |
| Checkbox | 29 | ~ | P14 | size-*, tokens |
| CollectionHeader | 45 | ✕ →PageHeader | P10 | |
| Cover | 47 | ✕ →Artwork | P3 | |
| EmptyState | 31 | ~ | P10 | recetas |
| EqBars | 23 | ~ | P7 | absorbe NowPlaying |
| ExplicitBadge | 13 | ~ | P1/P14 | rounded-tag, size-4 |
| Field | 27 | = | ||
| GlassMenu | 15 | = | base de menús | |
| IconButton | 39 | ~ | P4 | shape/size/tone/pressed/href |
| ImageDropzone | 62 | ~ | P11 | size="hero"; usado por upload |
| InfiniteScroll | 39 | = | ||
| Input | 59 | ~ | P11 | field-control |
| MediaCard | 32 | ~ | P0/P9 | bug 16px; absorbe MixTile |
| MediaGrid | 27 | ✕ →grid-* |
P9 | |
| MenuItem | 48 | ~ | P6 | iconos size-icon-md |
| MixArt | 25 | ✕ →Artwork | P3 | |
| MixTile | 51 | ✕ →MediaCard | P9 | |
| Modal | 106 | ~ | P10 | .glass-panel, z-token, recetas |
| NowPlaying | 20 | ✕ →EqBars | P7 | |
| Page | 14 | = | Inicio debe usarlo (P13) | |
| PageHeader | 26 | ~ | P10 | absorbe CollectionHeader |
| PlaylistArt | 61 | ✕ →Artwork | P3 | |
| PlaylistCard | 23 | ✕ →MediaCard | P9 | |
| PlaylistForm | 161 | ~ (CoverForm) | P11 | + SegmentedControl |
| SearchResultRow | 78 | ✕ →ListRow | P7 | |
| SectionHeading | 32 | ~ | P10 | count, "ver todas" |
| Surface | 17 | = | (solo upload; aceptable) | |
| Textarea | 28 | ~ | P11 | field-control |
| TrackIndexCell | 27 | ✕ →TrackList | P8 | |
| TrackMeta | 14 | ✕ →TrackList | P8 | |
| TrackRow | 27 | ✕ →TrackList | P8 | |
| TrackTable | 79 | ✕ →TrackList | P8 | |
| TrackTitleCell | 77 | → MediaIdentity | P7 | renombrar + generalizar |
| Nuevos | Artwork, Avatar, ListRow, MediaIdentity, TrackList, ContextMenu, PlayButton, ConfirmDialog, Slider, Logo, Chip, SegmentedControl, CoverForm |
src/lib/components/ y player/
| Archivo | Líneas | Veredicto | Parte | Motivo |
|---|---|---|---|---|
| AlbumContextMenu | 119 | ✕ →ContextMenu | P6 | copia de TrackContextMenu |
| TrackContextMenu | 109 | ✕ →ContextMenu | P6 | |
| MobileHeader | 70 | ~ | P6/P12 | Avatar, Logo, sin wrapper duplicado |
| Sidebar | 125 | ~ | P7/P12 | appNavLinks, ListRow |
| TabsBar | 50 | ~ | P12 | appNavLinks, glass-bar |
| TopBar | 140 | ~ | P4/P12 | IconButton, Avatar, searchHref |
| player/PlayerBar | 28 | ~ | P12 | glass-bar, absorbe PlayerExtras |
| player/PlayerDock | 29 | ~ | P12 | z-token |
| player/PlayerExtras | 22 | ✕ →PlayerBar | P12 | |
| player/Queue | 111 | ~ | P7 | ListRow, recetas, pressable |
| player/SeekBar | 48 | ✕ →Slider | P12 | |
| player/TrackInfo | 65 | ~ | P7 | MediaIdentity, IconButton pressed |
| player/TransportControls | 75 | ~ | P4 | PlayButton, IconButton pressed |
| player/VolumeControl | 52 | ✕ →Slider | P12 |
Rutas
| Archivo | Líneas | Veredicto | Parte | Motivo |
|---|---|---|---|---|
+layout.svelte |
142 | ~ | P2 | tokens SSR, fondos a utilidades, padding del dock |
+page.svelte (Inicio) |
466 | ~ | P0/P5/P7/P13 | 4 filas a mano, stats ×3, vacío ×2, style= |
+error.svelte |
16 | ~ | P10 | recetas, max-w-prose-sm |
login/+page.svelte |
50 | ~ | P12 | Logo, max-w-cta |
(app)/explore/+page.svelte |
602 | ~ | P13 | dividir datos/lógica; 4 grupos → each |
(app)/upload/+page.svelte |
288 | ~ | P11 | ImageDropzone, recetas |
(app)/library/+page.svelte |
108 | ~ | P8 | TrackList |
(app)/liked/+page.svelte |
123 | ~ | P0/P5/P8/P10 | imports muertos, playAllOrToggle, TrackList |
(app)/mixes/[id]/+page.svelte |
134 | ~ | P5/P8/P10 | menú, TrackList, PageHeader |
(app)/albums/+page.svelte |
74 | ~ | P9 | MediaCard + grid-cards |
(app)/albums/[id]/+page.svelte |
91 | ~ | P8/P10 | inline header/tablas, ConfirmDialog |
albums/[id]/components/AlbumHeader |
82 | ✕ (inline) | P10 | |
albums/[id]/components/AlbumTrackTable |
64 | ✕ (inline) | P8 | |
albums/[id]/components/LibraryPicker |
65 | ✕ (inline) | P8 | |
(app)/playlists/+page.svelte |
62 | ~ | P9 | |
(app)/playlists/[id]/+page.svelte |
95 | ~ | P8/P10 | |
playlists/[id]/components/PlaylistHeader |
62 | ✕ (inline) | P10 | |
playlists/[id]/components/PlaylistTrackTable |
71 | ✕ (inline) | P8 | |
(app)/u/[id]/+page.svelte |
97 | ~ | P3/P9/P10 | Avatar, grid |
routes/layout.css |
238 | ~ | P1 | todos los tokens y recetas |
src/lib/ (cliente)
| Archivo | Veredicto | Parte | Motivo |
|---|---|---|---|
theme/theme.css |
~ | P1/P2 | quitar duplicados de tokens.ts, añadir layout/z |
theme/tokens.ts |
~ | P2 | tokensToCss, sin --mf-sidebar-bg |
theme/palette.ts, theme/color.ts, theme/mode.svelte.ts |
= | (mode pierde el flash con P2) |
|
tracks.ts |
~ | P5 | quitar envoltorio TrackTarget + 8 accesores |
recentlyPlayed.ts |
~ | P5 | toQueueItem |
player/player.svelte.ts |
~ | P5 | helpers de cola, toQueueItem, renombrar addToQueue |
player/liked.svelte.ts |
~ | P5 | toQueueItem |
player/queuePanel.svelte.ts, playlists.svelte.ts |
→ toggle.svelte.ts |
P5 | idénticos |
format.ts |
~ | P5/P9 | plural, albumMeta, playlistMeta |
navigation.svelte.ts |
~ | P5 | searchHref, appNavLinks |
config.ts |
~ | P6 | MENU_WIDTH, SUBMENU_WIDTH |
mixes.ts, albums.ts, collections.ts, types.ts |
= | ||
lib/server/**, +page.server.ts, routes/api/**, auth/**, hooks.server.ts |
fuera de alcance | no contienen UI ni estilos |