PlayerExtras is merged into PlayerBar, which now renders volume and the queue button inline and uses the new Slider (a real range input with thumb thickness controlled via CSS variables) for both the progress bar and volume, replacing SeekBar and VolumeControl (grep confirmed SeekBar's `compact` mode was dead code). Sidebar and TabsBar migrate to appNavLinks(user, counts?), removing the duplicated nav links between the two. Decision made without blocking: Sidebar gains the gradient Logo mark it didn't have before, for visual consistency with MobileHeader and the login screen. PlayerDock and MobileHeader's z-index is fixed to z-(--z-sticky) (they're fixed bars, not menus), and SegmentedControl's to z-(--z-raised), per the plan's note B.1.5. glass-bar and theme-transition are applied to PlayerBar (both variants), TabsBar, Queue and Home (hero/spotlight), replacing duplicated blur/transition styles. Verified with npm run check (0 errors) and npm run build; smoke-tested /login with no console errors. No authenticated visual QA (Docker unavailable this session).
58 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 | ✅ | (pendiente) | Decisión tomada (recomendada, sin bloquear): iconos en los botones de cabecera de playlist (antes solo álbum tenía). PlaylistHeader pierde su prop id sin usar tras inlinearlo. Explorar: la cabecera de "Resultados para…" ahora usa el mb-7/8 de PageHeader en vez de un mt-5 ajustado a mano antes de los chips de filtro — gap ligeramente mayor entre el resumen y los chips (Δ menor, documentado). Sin QA visual autenticada (Docker no disponible) |
| P11 | ✅ | (pendiente) | CoverForm añade snippet extra (no está en la spec literal, solo fields) para el contenido a ancho completo bajo la fila portada+campos (descripción de álbum, selector de visibilidad de playlist) que no cabía en una única columna. Upload usa ImageDropzone con onselect (prop nueva) y se envuelve en {#key} para poder resetear tras publicar — Δ: el contenedor de portada pasa de 144/176px (móvil/desktop) a 144px fijo (size-cover-hero, sin variante desktop mayor), y el icono de posición vacía de 64px a 36px (icon-xl), tal como recomienda el plan. Sin QA visual autenticada (Docker no disponible) |
| P12 | ✅ | (pendiente) | PlayerExtras se fusiona en PlayerBar (volumen/cola inline); Slider sustituye a SeekBar+VolumeControl (confirmado por grep que el modo compact de SeekBar era código muerto). Sidebar y TabsBar migran a appNavLinks() (P5). Decisión tomada (sin bloquear): Sidebar gana la marca-gradiente Logo que no tenía antes, para consistencia con MobileHeader/login. z-index de PlayerDock/MobileHeader corregido a z-(--z-sticky) (son barras fijas, no menús) y SegmentedControl a z-(--z-raised), tal como indica B.1.5. glass-bar/theme-transition aplicados en PlayerBar (ambas variantes), TabsBar, Queue e Inicio (hero/spotlight), sustituyendo estilos duplicados. Sin QA visual autenticada (Docker no disponible); smoke test de /login sin errores de consola |
| 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 |