web-player: final sweep and guardrails

Add scripts/check-tokens.mjs (npm run lint:tokens, wired into lint) as
a guardrail: fails if src/**/*.svelte reintroduces arbitrary values
outside the B.4 allowlist, factory rounded-*, h-N w-N pairs (should
be size-N), quarter-step fractional spacing, or literal hex/rgba()
colors.

Cleans up the repo until the script reports 0 violations: ~40 h-N w-N
pairs -> size-N (mostly icons, via a one-off script), ~26 quarter-step
fractional spacings rounded to the nearest integer (delta <=1px, e.g.
py-2.75->py-3, gap-1.75->gap-2), stray factory radii (rounded-sm/lg,
bare rounded) migrated to the matching tokens, and two
tracking-[-0.02em] that matched the existing tracking-display token
exactly but had never been migrated. Removes the
stroke-thin/regular/bold recipes (added earlier, zero consumers) —
decision made, not blocking: strokeWidth={n} (13 values, across
almost every component) isn't migrated to CSS because there's no way
to verify without Docker that lucide-svelte honors stroke-width by
class instead of the prop, and a regression there would be silent
across the whole app; the script documents this gap explicitly
instead of pretending full coverage.

Orphan detector: 0 components with no importers. Removes
docs/design-system-refactor-plan.md (earlier plan, superseded by
frontend-refactor-plan.md). web-player/CLAUDE.md gains a "Styles"
section with the B.4 allowlist and the base components to reuse.
Recalculates the baseline table (A.4) and logs it: 61->47 components,
44->11 arbitrary values (the remaining 11 on the allowlist), 13->8
distinct radii, 0 lint:tokens violations.

Verified with npm run check (0 errors), npm run lint:tokens (0
violations) and npm run build. Smoke-tested /login with no console
errors attributable to this change.
This commit is contained in:
2026-09-21 15:05:07 +02:00
parent b60a87adb5
commit 1b6d7ecb42
32 changed files with 200 additions and 453 deletions
+18 -4
View File
@@ -11,11 +11,12 @@ No hay `svelte.config.js` (config inline en `vite.config.ts`) ni `tailwind.confi
## Comandos
```sh
npm run dev # servidor de desarrollo (puerto 5173)
npm run dev # servidor de desarrollo (puerto 5173)
npm run build
npm run check # svelte-check — debe salir 0 errores antes de cerrar un cambio
npm run format # prettier --write .
npm run lint # prettier --check . && eslint .
npm run check # svelte-check — debe salir 0 errores antes de cerrar un cambio
npm run format # prettier --write .
npm run lint # prettier --check . && eslint . && npm run lint:tokens
npm run lint:tokens # scripts/check-tokens.mjs — guardrail del sistema de diseño (ver abajo)
```
## Reglas
@@ -28,6 +29,19 @@ npm run lint # prettier --check . && eslint .
- Enlaces a endpoints `+server.ts` (login/logout) llevan `data-sveltekit-reload`.
- Reutiliza los tokens/recetas del sistema de diseño; extrae a `$lib/components/ui/` lo que se repita.
## Estilos
El sistema de diseño vive en `src/routes/layout.css` (tokens `@theme` + recetas `@utility`) y `src/lib/theme/` (`theme.css` para neutros estáticos, `tokens.ts` para los tokens dinámicos por hue de usuario). `scripts/check-tokens.mjs` (`npm run lint:tokens`, parte de `lint`) falla si `src/**/*.svelte` reintroduce lo que el refactor de `docs/frontend-refactor-plan.md` (P0–P14) eliminó — léelo antes de añadir un valor arbitrario o un radio/tamaño "de fábrica".
- **Radios:** siempre `rounded-tag/thumb/control/art/art-lg/panel/panel-lg` (o `rounded-full` para píldoras/círculos). Nunca `rounded-sm/md/lg/xl/2xl/3xl` ni `rounded` a secas.
- **Tamaños cuadrados:** `size-*` en vez de `h-N w-N` repetido (icon: `size-icon-xs/sm/md/lg/xl`; portadas: `size-cover-xs…hero`).
- **Tipografía:** recetas `text-display-1/2/3/4`, `text-eyebrow`, `text-body`, `text-count`; tracking con `tracking-display`/`tracking-eyebrow`, no `tracking-[…]` suelto.
- **Espaciado:** escala de Tailwind en pasos de `.5` (evita cuartos como `.25`/`.75`; solo aceptables si el token semántico lo pide, p. ej. `mb-4.5` en `SectionHeading`).
- **Colores:** siempre `--mf-*`/`text-*`/`bg-*`/`border-*` de `theme.css`/`tokens.ts`. Nunca hex ni `rgba()` literales en un `.svelte` (esos literales solo existen como fuente de verdad dentro de `theme.css`/`tokens.ts`).
- **`style="…"` y arbitrarios `-[…]`:** solo la lista blanca de B.4 en `docs/frontend-refactor-plan.md` (stagger `--i`, hue de tile, posición de menú, anchos/alturas dinámicos vía `var(--mf-*)`, `text-[clamp(…)]`, `grid-cols-[auto_1fr]`, `max-h-[85dvh]`, paddings en `vh` para centrado vertical). Todo lo demás, a un token.
- Componentes base a reutilizar antes de crear uno nuevo: `Artwork`, `Avatar`, `ListRow`, `MediaIdentity`, `TrackList`, `MediaCard`, `PageHeader`, `ContextMenu`, `ConfirmDialog`, `PlayButton`, `Slider`, `Logo`, `Chip`, `SegmentedControl`, `CoverForm`.
- Gaps conocidos y aceptados (documentados en `scripts/check-tokens.mjs`): `strokeWidth={n}` en iconos no está unificado (13 valores distintos repartidos por casi todos los componentes; migrarlo a las recetas `stroke-thin/regular/bold` de forma segura requeriría poder verificar visualmente que `lucide-svelte` respeta `stroke-width` por CSS, así que se dejó fuera de esta pasada).
## Auth
`hooks.server.ts` valida la cookie de sesión cifrada (`mf_session`), refresca el `access_token` y puebla `locals.user` / `locals.accessToken`. Para proteger una ruta, comprueba `locals.user` en su `load` y redirige a `/login?returnTo=…`. Para el backend, envía `locals.accessToken` como Bearer hacia `API_BASE_URL`.
@@ -1,375 +0,0 @@
# Plan — Unificación del sistema de diseño y limpieza del front (web-player)
> Este documento es un **plan de trabajo para otro agente/IA**, no un informe de lo ya
> hecho. Recoge conocimiento concreto (bugs reales, patrones, cifras) obtenido durante
> una sesión larga de ajustes de UI, para que quien continúe no tenga que re-descubrirlo.
> No es necesario ejecutar todo de una vez: está pensado para trabajarse página por
> página o componente por componente, en PRs pequeños y verificables.
> **Estado (sesión de seguimiento posterior):** §1–§3 y §5 de este documento (hex
> literales, tamaños de fuente arbitrarios, menú "de cristal" duplicado, componentes
> huérfanos listados) ya están resueltos. El problema había reaparecido con otra forma:
> valores de opacidad sueltos sobre `white`/`black` (`bg-white/[6%]`, `border-white/10`,
> etc.) fuera de los tokens `--mf-surface*`/`--mf-border*`, sombras/gradientes de portada
> duplicados sin token, un bloque de "cabecera de colección" reimplementado 5 veces, un
> menú de cuenta duplicado en `TopBar`/`MobileHeader`, y textos de estado vacío con
> redacción inconsistente ("Aún no..." vs "Todavía no..."). Todo eso se corrigió: se
> añadieron tokens `--shadow-cover-lg/md/sm/xs` y `--mf-liked-grad` a
> `layout.css`/`theme.css`, se migraron los literales `white`/`black` a
> `bg-surface*`/`border-line*`/`hover:bg-hover`/`text-on-art` según si el color está
> sobre el "chrome" de la app o sobre arte/gradiente saturado, se extrajeron
> `lib/components/ui/CollectionHeader.svelte` (portada + título + meta + acciones, usado
> en `AlbumHeader`, `PlaylistHeader`, `liked/+page.svelte`, `mixes/[id]/+page.svelte`,
> `albums/external/.../+page.svelte` y `u/[id]/+page.svelte`) y
> `lib/components/ui/AccountMenu.svelte` (estado de apertura + panel del menú de cuenta,
> usado en `TopBar`/`MobileHeader`), y se corrigió `--color-hover` en `layout.css`, que
> estaba hardcodeado en vez de apuntar a `--mf-surface-2` (rompía el modo claro para
> cualquier `hover:bg-hover`).
>
> **Repaso posterior a la verificación visual** (ya con sesión autenticada): se detectó
> que el fix de §2 anterior para la tarjeta de género de `explore/+page.svelte` estaba
> mal — usaba `var(--mf-ink)` como extremo oscuro del degradado, pero `--mf-ink` es el
> token de "texto sobre botón de acento" y se invierte a blanco en modo claro, rompiendo
> la tarjeta (fondo blanco en vez de degradado oscuro). Se creó un token nuevo,
> `--mf-tile-shade` (deliberadamente **sin** variante de modo claro, igual que
> `--mf-cover-grad`/`--mf-liked-grad`, porque es "arte" decorativo, no "chrome"), y se usa
> ahí en su lugar. También se cambió la línea de meta de `CollectionHeader` de
> `text-muted` a `text-fg-2` (mejor contraste en modo claro; `text-muted` rondaba ~3:1,
> por debajo de WCAG AA para texto normal) y se hizo `actions` opcional para poder migrar
> páginas sin acciones condicionales (como `u/[id]/+page.svelte`) sin renderizar un
> `<div>` vacío.
>
> Pendiente de quien retome esto: `lib/components/ui/Rail.svelte` no tiene importadores
> (confirmado con el mismo método de §5, no porque §5 lo listara — ese hallazgo es nuevo)
> — no se tocó porque no forma parte de este barrido, pero conviene confirmarlo a mano y
> borrarlo si procede. También queda pendiente repetir la verificación visual (§6) sobre
> los cambios de este repaso: tarjetas de género de Explorar, cabeceras de colección
> (contraste de la línea de meta) y `u/[id]/+page.svelte`.
## 0. Objetivo
Tres problemas, en orden de aparición histórica pero **no** de prioridad recomendada:
1. **Colores fijos en vez de variables**: muchas páginas y componentes escriben colores
literales (`text-[#8a8a8e]`, `bg-[#c6c4cb]`, tamaños `text-[13px]` sueltos, etc.) en
vez de usar los tokens del sistema (`text-fg-2`, `bg-surface`, la escala tipográfica
establecida). Esto es lo que impide que cosas como el modo claro/oscuro (ver §2)
funcionen de forma consistente, y hace que cada página tenga ligeras variaciones de
gris que deberían ser el mismo tono.
2. **Falta de componentización**: hay patrones (tarjeta de track, fila de "canción
reciente", menú desplegable tipo cristal, etc.) reimplementados a mano en varias
páginas en vez de reusar (o crear) un componente compartido. Algunos componentes ya
existentes ni siquiera se usan donde deberían.
3. **Código muerto**: imports sin usar, componentes sin importadores, clases residuales.
**Referencia canónica**: la página de Inicio (`src/routes/+page.svelte`) es el patrón a
seguir para el modo oscuro — sus tonos, jerarquía tipográfica y look general están bien
ajustados (fue la página más iterada). Cuando una página no tenga claro qué tono/tamaño
usar para algo, mirar cómo lo resuelve Inicio para lo mismo, **no inventar uno nuevo**.
Ojo: Inicio también usa hex sueltos en vez de tokens en varios sitios (ver §3) — es la
referencia de **valores/jerarquía visual**, no de "cómo escribir la clase". Cuando se
migre Inicio a tokens, los valores resultantes deben ser los mismos que ya tiene hoy.
---
## 1. Limitación conocida: modo claro ("Modo blanco") incompleto
Se añadió un toggle de tema (`$lib/theme/mode.svelte.ts`, menú de cuenta en
`TopBar.svelte` / `MobileHeader.svelte`) que pone `data-theme="light"` en `<html>` y
persiste la preferencia en `localStorage` (`musify.theme`).
**Lo que funciona:** los tokens semánticos definidos como CSS puro en
`$lib/theme/theme.css` (`--mf-text*`, `--mf-surface*`, `--mf-border*`, `--mf-track`,
`--mf-scrim`, `--mf-ink`) tienen su contrapartida en el bloque
`:root[data-theme='light'] { ... }` al final de ese archivo, y los tokens dinámicos por
hue (`--mf-bg`, `--mf-elevated`, `--mf-sidebar-bg`, `--mf-panel-bg`, `--mf-bar-bg`,
`--mf-hairline`) se regeneran correctamente en modo claro dentro de
`buildThemeTokens(hue, mode)` (`$lib/theme/tokens.ts`).
**Gotcha importante para quien toque esto:** `+layout.svelte` reaplica esos tokens
dinámicos como **estilos inline** sobre `document.documentElement` en un `$effect` que
corre en cada cambio de hue (cada canción). Un estilo inline siempre gana sobre
cualquier regla de una hoja de estilos, así que **una variable que se anima por hue no
se puede sobrescribir con `:root[data-theme='light'] { --mf-x: ... }`** — hay que
añadirla dentro de `buildThemeTokens()` con una rama `light ? ... : ...`, no en
`theme.css`. Si en el futuro se anima algo más por hue, aplica la misma regla.
**Lo que NO funciona:** todo componente que usa un color hardcodeado (ver §3) no se
entera del cambio de tema, porque no está leyendo ningún token. En la práctica esto
significa que hoy el modo claro cambia correctamente el lienzo de fondo, la sidebar, los
paneles tipo cristal (menús, modal) y cualquier texto/superficie que ya use
`text-fg`/`bg-surface`/etc., pero deja con contraste pobre o directamente ilegible
cualquier texto en hex literal (la mayoría de títulos, subtítulos y metadatos de la
app hoy).
**No hay forma barata de arreglar esto sin la migración de §3.** El modo claro real
depende, técnicamente, en un 100%, de terminar esa migración. No merece la pena
"parchear" el modo claro sin más — es la misma tarea.
---
## 2. Inventario de bugs/patrones ya encontrados (usar como guía de búsqueda)
Estos son bugs concretos que se repitieron varias veces en distintos componentes esta
sesión. Antes de tocar cada página, buscar estos mismos patrones ahí:
### 2.1 Tamaño de fuente que falta → hereda 16px del navegador
Varios componentes tenían un texto sin ninguna clase `text-*`, lo que hace que use el
tamaño por defecto del navegador (16px) en vez de la escala real de la app (que va de
`10.5px` a `14px` en casi todo). Ya se corrigió en `MediaCard.svelte`,
`TrackTitleCell.svelte` y `MenuItem.svelte`. **Sigue sin corregir** en
`TrackContextMenu.svelte` (usa `text-base`/`text-sm` sueltos igual que tenía
`MenuItem` antes de arreglarlo — mismo fix: bajar a la escala `13–13.5px` y los iconos
de `h-6 w-6`/`h-5 w-5` a `h-[18px] w-[18px]` o similar).
Cómo detectarlo: cualquier `<div>`/`<span>` que pinte texto de usuario (título, nombre,
label) sin una clase `text-[…]`/`text-sm`/`text-xs` explícita es sospechoso.
### 2.2 Conflicto de clases Tailwind que se resuelve por orden interno, no por el HTML
Tailwind no respeta el orden en que escribes las clases en el `class="…"`; resuelve
conflictos de la misma propiedad (`background-color`, `font-weight`, etc.) por el orden
en que esas utilidades existen en su propia hoja de estilos generada. Esto causó bugs
reales y confusos:
- Un botón con `bg-white/[6%] ... {isCurrent ? 'bg-accent-tint' : ''}` — con `isCurrent`
true, el fondo **seguía viéndose** `bg-white/[6%]` porque esa clase "ganaba" en el
cascade aunque apareciera antes en el HTML.
- `Button.svelte` fuerza `font-semibold` en su clase base; para poner un botón en peso
normal (`variant="secondary"` con texto normal) hay que usar `class="!font-normal"`
(con el prefijo `!` de Tailwind, que sí fuerza especificidad), un `class="font-normal"`
a secas no habría ganado.
**Regla a aplicar en la auditoría**: cualquier sitio donde dos clases de la misma
propiedad CSS convivan condicional + incondicionalmente en el mismo elemento (patrón
`class="base-x ... {cond ? 'other-x' : ''}"`) hay que reescribirlo para que sea
mutuamente excluyente (`class="{cond ? 'a' : 'b'}"`), nunca acumulativo. Y cualquier
override de una clase base de un componente compartido (`Button`, etc.) debe usar `!`
si toca la misma propiedad que ya fija la clase base.
### 2.3 Iconos con brillo verde/acento en `:hover` de forma inconsistente
Varias tarjetas tenían `group-hover:text-accent-soft` en el título para dar un hover
"bonito", pero era inconsistente con el resto de la app (la mayoría de textos no
cambian de color al hover, solo cuando están realmente "activos"/reproduciéndose). Se
quitó de `MediaCard.svelte` y de la fila de "Tus playlists" en Inicio. **Quedan
pendientes** los mismos `group-hover/card:text-accent-soft` /
`group-hover/row:text-accent-soft` en `src/routes/(app)/explore/+page.svelte` (líneas
con tarjetas de álbum y filas de resultado — grep por `group-hover.*text-accent-soft`
para localizarlos exactos, las líneas se mueven con cada edición).
---
## 3. Migración de colores/tamaños a tokens
### 3.1 Datos de partida (medidos en esta sesión, `src/` de web-player)
```
27 archivos .svelte con al menos un text-[#…]/bg-[#…]/border-[#…]
106 apariciones totales de color hex literal en clases Tailwind
```
Los 10 valores hex más repetidos como `text-[#…]` (comando usado:
`grep -roE "(text|bg|border)-\[#[0-9a-fA-F]{3,8}\]" --include="*.svelte" . | sort | uniq -c | sort -rn`):
| Ocurrencias | Valor | Coincide EXACTO con token |
| ----------- | --------- | ------------------------------------- |
| 13 | `#8a8a8e` | `--mf-text-2` → usar `text-fg-2` |
| 9 | `#747478` | ninguno exacto (cerca de text-3/4) |
| 7 | `#d5d4d8` | ninguno exacto (cerca de `--mf-text`) |
| 7 | `#7f7f83` | ninguno exacto (cerca de text-3) |
| 6 | `#d9d8dc` | `--mf-text` → usar `text-fg` |
| 6 | `#d4d3d7` | ninguno exacto |
| 4 | `#d0cfd4` | ninguno exacto |
| 4 | `#818185` | ninguno exacto |
| 3 | `#d8d7db` | ninguno exacto |
| 2 | `#7d7d81` | `--mf-text-3` → usar `text-fg-3` |
Interpretación: hay un grupo de valores que son **copias exactas** de un token ya
existente (probablemente porque alguien copió el valor en vez de la clase) — esos son
reemplazo mecánico, bajo riesgo. Y hay un grupo mucho más grande de valores "parecidos
pero no iguales" a los 4 tonos de texto (`--mf-text`, `-2`, `-3`, `-4`) — esto sugiere
que cada componente inventó su propio gris en vez de reusar la escala, y hay que decidir
para cada uno **a qué tier semántico pertenece** (¿es un título? ¿un metadato? ¿un
label?) y mapearlo al token de esa categoría, no crear un tier nuevo por cada valor.
Solo se encontraron 3 `bg-[#…]` (en `MixTile.svelte` y `TransportControls.svelte`,
este último repite el mismo `#c6c4cb`/`#d6d5da` que tenía el botón "Reanudar" de Inicio
antes de moverlo a `Button variant="accent"` con override — es el mismo "botón claro"
reinventado dos veces; candidato a token propio, ver §4.4) y 0 `border-[#…]`.
### 3.2 Metodología sugerida (por archivo)
1. `grep -n "text-\[#\|bg-\[#\|border-\[#" archivo.svelte` para listar todos los casos.
2. Para cada uno, decidir el tier semántico (título/cuerpo/metadato/label/borde) y
sustituir por la utilidad de Tailwind ya mapeada en
`src/routes/layout.css` (`@theme` → `--color-fg`, `--color-fg-2`, `--color-fg-3`,
`--color-muted`, `--color-surface`, `--color-surface-2`, `--color-line`,
`--color-accent*`, etc.) o, si el valor es realmente un tamaño de fuente en vez de
color, a la escala ya usada en el resto de la app (10.5 / 11 / 11.5 / 12 / 12.5 / 13 /
13.5 / 14px — no inventar un tamaño intermedio nuevo salvo que Inicio ya lo use para
ese mismo tipo de texto).
3. Ejecutar `npm run check` tras cada archivo — Tailwind con clases arbitrarias no da
error de compilación aunque el token no exista, así que la única validación real es
visual: abrir la página en el navegador (Browser pane) y comparar contra Inicio.
4. Si dos o más componentes usan **el mismo valor hex no-token** para el mismo propósito
(p. ej. `#c6c4cb` como "botón claro"), considerar promoverlo a un token nuevo en
`theme.css` en vez de simplemente sustituirlo por el más parecido — ver §4.4.
5. `npm run lint` (prettier + eslint) antes de dar por cerrado cada archivo, según las
reglas del `CLAUDE.md` del proyecto.
### 3.3 Prioridad sugerida de páginas/componentes a migrar
Por impacto (páginas que ve todo usuario, no solo estados de error/edge):
1. `Sidebar.svelte`, `TopBar.svelte`, `MobileHeader.svelte` (chrome global, visible
siempre) — MobileHeader/TopBar ya tienen el menú de cuenta arreglado, falta el resto.
2. `src/routes/(app)/playlists/[id]/components/PlaylistHeader.svelte` y
`src/routes/(app)/albums/[id]/components/AlbumHeader.svelte` (comparten patrón,
arreglar juntos para no divergir otra vez).
3. `src/routes/(app)/liked/+page.svelte`, `src/routes/(app)/explore/+page.svelte`,
`src/routes/(app)/upload/+page.svelte`, `src/routes/(app)/mixes/[id]/+page.svelte`.
4. Componentes del reproductor (`lib/components/player/*.svelte`) — muchos hex propios,
pero cambian menos visualmente entre páginas, así que menor prioridad de contraste
pero igual de importantes para el modo claro (la barra del reproductor es visible en
toda la app).
5. `src/routes/+page.svelte` (Inicio) al final — es la referencia, así que migrarla
última minimiza el riesgo de que un "acabado" a medio migrar se convierta en la
referencia visual equivocada mientras se trabaja el resto.
---
## 4. Componentización pendiente
### 4.1 Patrón de fila de track reimplementado en vez de reusar `TrackTitleCell`
`src/routes/+page.svelte` reimplementa a mano, con su propio marcado, **tres** variantes
de "fila de canción" distintas entre sí (Continuar escuchando, Populares esta semana,
filas de la Playlist destacada) en vez de usar los componentes ya existentes
`TrackTitleCell.svelte` / `TrackRow.svelte` / `TrackMeta.svelte` /
`TrackIndexCell.svelte` que sí se usan correctamente en
`PlaylistTrackTable.svelte`/`library/+page.svelte`. Evaluar si esas tres filas de Inicio
se pueden expresar con los componentes de `TrackTable` (aunque sea sin la tabla
completa, solo reusando `TrackTitleCell` para title+cover+artist) para no mantener 4
implementaciones de "fila de canción" en paralelo.
### 4.2 `MediaCard`/`PlaylistCard`/`AlbumCard` vs. tarjetas hechas a mano
`src/routes/+page.svelte` (sección "Tus playlists") y `src/routes/(app)/explore/+page.svelte`
tienen su propia tarjeta de álbum/playlist en vez de usar `MediaCard`/`PlaylistCard`/
`AlbumCard` (`lib/components/ui/`). Antes de tocar el estilo de esas tarjetas a mano,
comprobar si pueden sustituirse directamente por el componente compartido — sale gratis
el arreglo de tipografía/color si el componente compartido ya está migrado a tokens.
### 4.3 Menú desplegable "de cristal" duplicado
`TopBar.svelte` y `MobileHeader.svelte` duplican: estado `menuOpen`, el
`onDocumentClick` para cerrar al hacer click fuera, y el marcado del panel
(`animate-pop ... backdrop-blur-[22px]` + `background-color:var(--mf-panel-bg);
background-image:var(--mf-modal-glow)`). `TrackContextMenu.svelte` tiene un menú
flotante distinto (`bg-elevated` + `border-line`, sin el efecto cristal, y con el bug de
tamaño de fuente de §2.1). Candidatos:
- Extraer un primitivo `Dropdown.svelte` o `FloatingMenu.svelte` que resuelva
posicionamiento + cierre al clicar fuera + Escape, y que ambos menús de cuenta lo usen.
- Definir la receta "panel de cristal" (el `style="background-color:var(--mf-panel-bg);
background-image:var(--mf-modal-glow)"` + `backdrop-blur-[22px]`) en una utilidad o
clase reusable en vez de repetir el `style=` inline en cada sitio, y aplicarla también
a `TrackContextMenu` para que los tres menús floating de la app se vean iguales.
### 4.4 Candidato a token nuevo: "botón claro" (`#c6c4cb`/`#d6d5da`/`#d8d7dc`)
Aparece en al menos: el antiguo botón "Reanudar" de Inicio (ya migrado a
`Button variant="accent"` con `class="!bg-white ..."`, ver commit reciente) y
`TransportControls.svelte` (botón de play principal del reproductor, sigue con hex
propio). Si el diseño quiere mantener un botón "casi blanco" como acento fuerte en
ciertos sitios (distinto del verde de acento normal), vale la pena darle un token
propio (p. ej. `--mf-cta-strong-bg`/`--mf-cta-strong-fg`) en vez de repetir el hex cada
vez, así el modo claro también puede darle un tratamiento coherente en vez de heredar el
mismo blanco fijo en ambos temas (que en modo claro quedaría invisible sobre el fondo).
---
## 5. Código muerto — puntos de partida
Ya se han borrado en esta rama `MobileNav.svelte` y `MixBentoTile.svelte` por no
usarse. Grep rápido (basado en "ningún otro `.svelte`/`.ts` bajo `src/` menciona el
nombre del componente fuera de su propio archivo") encontró estos candidatos
adicionales — **verificar a mano antes de borrar** (el grep no será 100% preciso: no
detecta uso vía imports dinámicos, alias, o si el componente se monta desde fuera de
`src/`):
- `src/lib/components/ui/CoverBadge.svelte`
- `src/lib/components/ui/PlayPauseOverlay.svelte`
Método para repetir esta búsqueda tras cada ronda de refactor (los nombres de
componente que queden sin importadores son limpieza segura):
```bash
for f in $(find src/lib/components -name "*.svelte"); do
name=$(basename "$f" .svelte)
count=$(grep -rl "$name" --include="*.svelte" --include="*.ts" src | grep -v "/$name.svelte$" | wc -l)
[ "$count" -eq 0 ] && echo "$f"
done
```
Además de componentes huérfanos, revisar por archivo (esto no se ha auditado
sistemáticamente esta sesión, son solo los tipos de código muerto más comunes en este
código base):
- Imports de iconos (`@lucide/svelte/icons/*`) que dejaron de usarse tras quitar un
icono de un botón (pasó varias veces esta sesión con `Play`/`Pause`/`NowPlaying`;
`eslint` los marca como `no-unused-vars` — correr `npx eslint <archivo>` tras cada
cambio de marcado, no solo `svelte-check`, porque `svelte-check` no siempre lo pilla).
- Props de componentes que ya no lee nadie (ej. si se elimina el único sitio que pasaba
`description` a `PlaylistCard`, y el componente ya no la usa, quitar el prop además
del sitio que la usaba).
- Clases Tailwind puestas "por si acaso" que no tienen efecto porque una clase
posterior en el cascade las anula (ver §2.2) — no es código muerto en el sentido
clásico, pero es ruido que confunde a quien lea el archivo después.
---
## 6. Cómo verificar cada cambio
Del `CLAUDE.md` del proyecto (`web-player/CLAUDE.md`), aplica igual aquí:
```sh
npm run check # svelte-check — debe salir 0 errores
npm run lint # prettier --check . && eslint . — debe salir limpio
```
Y para lo visual, que `svelte-check`/`eslint` no cubren:
1. Levantar `musify-web` (y `musify-api` si la página necesita datos reales) con el
Browser pane / `.claude/launch.json`.
2. Abrir la página tocada y la página de Inicio **una al lado de otra** (o alternando)
para comparar tono de gris, tamaño de fuente y separaciones — no solo "que no se vea
roto", sino "que se vea igual que Inicio para el mismo tipo de elemento".
3. Si la página tiene estado activo/actual (ej. una fila de canción reproduciéndose),
probarlo — varios de los bugs de esta sesión (§2.2) solo eran visibles con el
`isCurrent`/hover activo, no en el estado por defecto.
4. Alternar el toggle de tema (menú de cuenta → "Modo blanco"/"Modo oscuro") en la
página tocada para confirmar que el texto sigue siendo legible en ambos modos.
---
## 7. Orden de trabajo sugerido
1. Terminar §2 (los 3 bugs conocidos que quedan sueltos: `TrackContextMenu.svelte`
tamaño de fuente, hover verde en `explore/+page.svelte`).
2. Migrar a tokens los componentes de chrome global (§3.3, punto 1) — máximo impacto
visual con menor superficie de código.
3. Extraer el "panel de cristal" y/o `Dropdown.svelte` (§4.3) **antes** de tocar
`TrackContextMenu` a mano, para no maquetarlo dos veces.
4. Migrar el resto de páginas por la prioridad de §3.3.
5. Revisar componentización (§4.1, §4.2) — mejor después de que los componentes base
(`TrackTitleCell`, `MediaCard`, etc.) ya estén en tokens, así "adoptarlos" en Inicio
no introduce una regresión visual.
6. Pasada final de código muerto (§5) con el listado de componentes/props ya
consolidado tras los pasos anteriores (habrá más candidatos a huérfanos según se
vayan unificando duplicados).
7. Migrar Inicio a tokens en último lugar (§3.3, punto 5), verificando que los valores
resultantes son pixel-iguales a los que tiene hoy (es la referencia, no debe cambiar
de aspecto, solo de cómo está escrito el código).
+19 -1
View File
@@ -786,7 +786,7 @@ Tareas:
| 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 | ✅ | (pendiente) | Se aborda junto con esta parte la tarea 5 diferida en P5: se elimina el envoltorio `TrackTarget` de `tracks.ts` y sus 8 accesores (`targetId`/`targetTitle`/`targetArtist`/`targetOwnerUserId`/`targetExplicit`/`targetListensCount`/`isTargetCurrent`/`queueItemForTarget`/`targetForQueueItem`); Explorar, Inicio, Mezcla, `menus.svelte.ts` y `mixes.ts` pasan a usar los objetos de pista (`ApiTrackLike`/`LocalTrack`) directamente, con `toQueueItem`/`trackFromQueueItem` (nuevo, en `player.svelte.ts`) como únicos conversores. TypeScript reveló que el campo `isExplicit` no existe en el schema real de `/tracks` — el badge "explicit" de Explorar ya era un no-op antes del refactor (siempre `undefined ?? false`); se retira el binding en vez de fingir soporte. Explorar: `GENRE_INFO`/`genreTiles` → `src/lib/genres.ts`; lógica de búsqueda (`showGroup`/`capped`/`matchPlaylists`/`searchCounts`/`searchChips`/`findTopResult`) → `src/lib/search.ts` (funciones puras, parametrizadas por `sfilter`/`query` en vez de cerrar sobre el estado del componente); "Mejor resultado" unifica el botón/enlace duplicado con `<svelte:element this={...}>`. Decisión tomada sin bloquear: **no** se colapsan los 4 bloques de grupo (Canciones/Álbumes/Usuarios/Playlists) en un único `{#each groups}` — cada grupo tiene snippets, contextmenu y props de `ListRow` genuinamente distintos (menú de pista vs. de álbum, overlay solo en canciones, trailing con formato distinto), y forzar una configuración genérica habría añadido más indirección de la que elimina. Inicio: usa `Page` (antes `page-x pt-3 sm:pt-4` a mano); stats ×3 → `{#each}`; fondos de hero/spotlight (`style=` con `var(--mf-hero-bg)`/`var(--mf-spotlight-bg)`) → utilities `hero-surface`/`spotlight-surface` (nuevas, en `layout.css`) y token `--spacing-section: 2.875rem` → `gap-section` sustituye `gap-11.5`. Sin QA visual autenticada (Docker no disponible); `/explore` sin sesión redirige a `/login` porque `+layout.server.ts` exige `allowAnonymousListening` desde el backend (inalcanzable sin Docker) — comportamiento preexistente, no una regresión; smoke test de `/login` sin errores de consola |
| P14 | ⬜ | | |
| P14 | ✅ | (pendiente) | `scripts/check-tokens.mjs` (`npm run lint:tokens`, añadido a `lint`) escanea `src/**/*.svelte` y falla en: arbitrarios `-[…]` fuera de la lista blanca de B.4, `rounded-(sm/md/lg/xl/2xl/3xl)`/`rounded` a secas, pares `h-N w-N` (deberían ser `size-N`), espaciado fraccionario en cuartos (`.25`/`.75`), hex/`rgba()` literales. Se limpió el repo hasta dejarlo en 0 violaciones: ~40 pares `h-N w-N` → `size-N` (icons sobre todo, vía script puntual), ~26 espaciados en cuartos redondeados al entero más cercano (Δ≤1px, p. ej. `py-2.75`→`py-3`, `gap-1.75`→`gap-2`), 3 `rounded-sm/lg` + 1 `rounded` a secas → tokens (`rounded-tag`/`rounded-thumb`/`rounded-control`), 2 `tracking-[-0.02em]` → `tracking-display` (coincidían exactamente con el token ya existente y no se habían migrado), 1 `tracking-[0.14em]` en Explorar → receta `text-eyebrow`, `rounded-3xl` del hero de Inicio → `rounded-panel-lg` (consistente con la tarjeta de playlist destacada). Se retiraron las recetas `stroke-thin/regular/bold` (creadas en P1, 0 consumidores) — **decisión tomada, sin bloquear**: no se migra `strokeWidth={n}` (13 valores, en casi todos los componentes) a esas recetas por CSS porque no se puede verificar sin Docker que `lucide-svelte` respete `stroke-width` por clase en vez del prop, y una regresión ahí sería silenciosa en toda la app; el script documenta este gap explícitamente y no lo exige. Se abordó también junto con la tarea 5 diferida de P5 (ver P13). Detector de huérfanos: 0 componentes sin importadores (los 47 de `src/lib/components/` están todos en uso). `docs/design-system-refactor-plan.md` (plan anterior, superseded) borrado. `web-player/CLAUDE.md` gana sección "Estilos" con la lista blanca B.4, componentes base a reutilizar y el gap de `strokeWidth` documentado. Línea base recalculada (ver tabla debajo) |
Decisiones de producto pendientes (preguntar al usuario antes de la parte indicada):
- P0/P4: ¿`Button` primary debe verse distinto de secondary?
@@ -797,6 +797,24 @@ Decisiones de producto pendientes (preguntar al usuario antes de la parte indica
- P10: ¿iconos en los botones de cabecera de álbum **y** playlist?
- P12: ¿logo con cuadrado de gradiente también en la Sidebar?
### D.1 Línea base recalculada (tras P14, cierre del refactor)
Recalculada con los mismos criterios que A.4 (grep sobre `src/`), en la misma sesión de
cierre de P14.
| Métrica | A.4 (antes) | Tras P14 |
| -------------------------------------------------- | ----------- | ------------------------------------------ |
| Componentes `.svelte` en `src/lib/components/` | 61 | 47 |
| Líneas `.svelte`/`.css`/`.ts` cliente | ~8.700 | ~8.310 (−~390; el objetivo de −1.500/−2.000 ya se había alcanzado en P3–P11, antes de que P13 añadiera `genres.ts`/`search.ts` como código nuevo bien separado) |
| Clases arbitrarias `x-[…]` | 44 | 11 (las 11 restantes, en la lista blanca de B.4) |
| Espaciados fraccionarios en cuartos (`.25`/`.75`) | ~190 apariciones | 0 (redondeados al entero más cercano, Δ≤1px) |
| Radios distintos usados | 13 | 8 (7 tokens B.1.1 + `rounded-full`) |
| `strokeWidth` distintos | 13 | 12 (fuera de alcance, ver Registro P14 y CLAUDE.md) |
| Atributos `style="…"` | 24 | 16 (en 12 archivos; todos dinámicos por instancia — stagger, posición de menú, `var(--mf-*)`, animación — ninguno con color/medida literal) |
| Pluralizaciones a mano `=== 1 ?` | 15 | 2 (1 es la propia `plural()` en `format.ts`; 1 es concordancia de participio en `library` que `plural()` no puede cubrir) |
| Errores de `eslint` | 2 | 0 |
| `npm run lint:tokens` (nuevo, P14) | — | 0 violaciones |
---
## E. Inventario completo (veredicto por archivo)
+2 -1
View File
@@ -12,7 +12,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"gen:api": "npx -y openapi-typescript@7 http://localhost:5111/openapi/v1.json -o src/lib/api/schema.d.ts",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
"lint:tokens": "node scripts/check-tokens.mjs",
"lint": "prettier --check . && eslint . && npm run lint:tokens"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env node
// Guardrail contra la reintroducción de lo que P0-P14 limpió
// (ver docs/frontend-refactor-plan.md, secciones B.4 y P14). Escanea
// src/**/*.svelte en busca de valores arbitrarios y utilidades de Tailwind
// que deberían ser tokens del sistema de diseño (src/routes/layout.css).
//
// Gaps conocidos, no cubiertos aquí a propósito:
// - `strokeWidth={n}` en iconos: hay ~13 valores distintos repartidos por
// casi todos los componentes. Migrarlo a las recetas `stroke-*` requeriría
// verificar que lucide-svelte respeta `stroke-width` por CSS en vez del
// atributo del componente, y no hay forma de comprobarlo visualmente en
// esta sesión (sin Docker). Se deja fuera para no arriesgar una regresión
// silenciosa en todos los iconos de la app.
// - `text-base` sin receta: solo queda un caso real (Queue.svelte, título
// "En cola", 16px) sin equivalente en la escala text-display-*. Crear una
// receta nueva para un único consumidor sería una abstracción prematura;
// se acepta como excepción documentada en vez de forzarlo a un tamaño que
// no le corresponde.
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
const files = execSync('git ls-files "src/**/*.svelte"', { encoding: 'utf8' })
.split('\n')
.filter(Boolean);
// B.4 — arbitrarios permitidos tras el refactor.
const ARBITRARY_WHITELIST = [
/^text-\[clamp\([^)]+\)\]$/, // tipografía fluida (login, Logo)
/^leading-\[[\d.]+\]$/, // leading ajustado del display gigante de login
/^grid-cols-\[auto_1fr\]$/, // upload: portada + campos
/^max-h-\[85dvh\]$/, // Modal
/^my-\[\d+d?vh\]$/, // +error.svelte: centrado vertical de página de error
/^py-\[\d+d?vh\]$/, // Explorar: centrado vertical de "sin resultados"
/^transition-\[filter\]$/, // Explorar: genre-tile hover
/^bg-\[image:var\(--mf-[\w-]+\)\]$/ // gradientes dinámicos por tema (Artwork, Logo, liked)
];
// Radios semánticos de B.1.1 — cualquier otro rounded-* de Tailwind (o el
// `rounded` a secas de fábrica) es una regresión. `rounded-full` y los
// tokens (`rounded-tag/thumb/control/art/art-lg/panel/panel-lg`) están bien:
// el lookahead exige que no venga más texto de clase pegado con `-`.
const RAW_ROUNDED = /\brounded(-(none|xs|sm|md|lg|xl|2xl|3xl))?(?![\w-])/g;
const PAIRED_HW = /\b(?:([a-z0-9-]+:))?h-([\d.]+)\s+\1w-\2\b/g;
const QUARTER_STEP_SPACING = /\b[a-z-]+-\d+\.(25|75)\b/g;
const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/g;
const RGBA_LITERAL = /\brgba?\(/g;
let violations = [];
for (const file of files) {
const content = readFileSync(file, 'utf8');
const lines = content.split('\n');
lines.forEach((line, i) => {
const lineNo = i + 1;
for (const match of line.matchAll(/[a-zA-Z][a-zA-Z0-9-]*-\[[^\]]+\]/g)) {
if (!ARBITRARY_WHITELIST.some((re) => re.test(match[0]))) {
violations.push(`${file}:${lineNo} — arbitrario fuera de la lista blanca: \`${match[0]}\``);
}
}
for (const match of line.matchAll(RAW_ROUNDED)) {
violations.push(`${file}:${lineNo} — radio de Tailwind sin tokenizar: \`${match[0]}\``);
}
for (const match of line.matchAll(PAIRED_HW)) {
violations.push(`${file}:${lineNo} — usa \`size-${match[2]}\` en vez de \`${match[0]}\``);
}
for (const match of line.matchAll(QUARTER_STEP_SPACING)) {
violations.push(
`${file}:${lineNo} — espaciado fraccionario en cuartos: \`${match[0]}\` (redondear al entero más cercano)`
);
}
for (const match of line.matchAll(HEX_COLOR)) {
violations.push(
`${file}:${lineNo} — color hex literal: \`${match[0]}\` (usa un token --mf-*)`
);
}
for (const match of line.matchAll(RGBA_LITERAL)) {
violations.push(
`${file}:${lineNo} — color rgba() literal: \`${match[0]}\` (usa un token --mf-*)`
);
}
});
}
if (violations.length > 0) {
console.error(`\n${violations.length} violación(es) del sistema de diseño:\n`);
for (const v of violations) console.error(` ${v}`);
console.error('\nVer docs/frontend-refactor-plan.md, sección B.4.\n');
process.exit(1);
}
console.log('check-tokens: sin violaciones.');
+4 -4
View File
@@ -31,7 +31,7 @@
</script>
<aside
class="theme-transition hidden shrink-0 flex-col border-r border-hairline bg-bg p-5 lg:flex"
class="hidden shrink-0 flex-col border-r border-hairline bg-bg p-5 theme-transition lg:flex"
style="width:var(--mf-sidebar-w)"
>
<a href="/" class="px-3 pb-3">
@@ -48,7 +48,7 @@
? 'bg-surface-2 text-fg'
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
>
<link.icon class="h-4.5 w-4.5 shrink-0" strokeWidth={1.5} />
<link.icon class="size-4.5 shrink-0" strokeWidth={1.5} />
<span class="flex-1 truncate">{link.label}</span>
{#if link.count}
<span class="text-xs text-muted tabular-nums">{link.count}</span>
@@ -85,7 +85,7 @@
playlist.updatedAt
)}"
trackIds={playlist.coverTrackIds}
class="px-3 py-1.75"
class="px-3 py-2"
/>
{:else}
<button
@@ -93,7 +93,7 @@
onclick={() => createPlaylistModal.show()}
class="flex items-center gap-2 rounded-control px-3 py-2 text-xs text-muted transition-colors hover:bg-hover hover:text-fg-2"
>
<Plus class="h-3.5 w-3.5" strokeWidth={1.8} />
<Plus class="size-3.5" strokeWidth={1.8} />
Crear tu primera lista
</button>
{/each}
+3 -3
View File
@@ -18,7 +18,7 @@
</script>
<nav
class="pointer-events-auto flex items-center justify-around glass-bar px-4 py-1.75 lg:hidden {klass}"
class="pointer-events-auto flex items-center justify-around glass-bar px-4 py-2 lg:hidden {klass}"
style="padding-bottom:calc(0.4375rem + var(--mf-safe-b))"
>
{#each links as link (link.href)}
@@ -26,11 +26,11 @@
<a
href={link.href}
aria-current={active ? 'page' : undefined}
class="flex flex-col items-center gap-1.25 rounded-xl px-2.5 py-1.5 {active
class="flex flex-col items-center gap-1 rounded-control px-2.5 py-1.5 {active
? 'text-fg'
: 'text-fg-2'}"
>
<link.icon class="h-4.25 w-4.25" strokeWidth={1.6} />
<link.icon class="size-4" strokeWidth={1.6} />
<span class="text-xs font-medium">{link.label}</span>
</a>
{/each}
+3 -3
View File
@@ -69,7 +69,7 @@
<label
class="flex h-9.5 w-full items-center gap-2.5 rounded-control bg-surface-2 px-3.5 transition focus-within:bg-surface-hover"
>
<Search class="h-3.75 w-3.75 shrink-0 text-fg-3" strokeWidth={1.8} />
<Search class="size-4 shrink-0 text-fg-3" strokeWidth={1.8} />
<input
type="text"
bind:this={searchInput}
@@ -83,9 +83,9 @@
type="button"
onclick={clearQuery}
aria-label="Borrar búsqueda"
class="grid h-5 w-5 shrink-0 place-items-center rounded-full text-muted transition hover:text-fg"
class="grid size-5 shrink-0 place-items-center rounded-full text-muted transition hover:text-fg"
>
<X class="h-3.5 w-3.5" />
<X class="size-3.5" />
</button>
{/if}
</label>
@@ -15,7 +15,7 @@
</script>
<div
class="animate-enter pointer-events-auto flex items-center gap-4.5 glass-bar px-4 py-2.75 sm:hidden"
class="animate-enter pointer-events-auto flex items-center gap-4.5 glass-bar px-4 py-3 sm:hidden"
>
<div class="min-w-0 flex-1">
<TrackInfo compact />
@@ -24,14 +24,14 @@
</div>
<div
class="animate-enter pointer-events-auto hidden items-center gap-5 glass-bar px-4 py-2.75 sm:flex"
class="animate-enter pointer-events-auto hidden items-center gap-5 glass-bar px-4 py-3 sm:flex"
>
<div class="w-(--mf-player-side-w) min-w-0 shrink-0">
<TrackInfo />
</div>
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
<TransportControls />
<div class="flex w-full max-w-hero items-center gap-2.75">
<div class="flex w-full max-w-hero items-center gap-3">
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
{fmtTime(player.progress)}
</span>
@@ -29,7 +29,7 @@
</div>
{#if player.current.id}
<p class="mb-2.75 text-eyebrow text-fg-3">Reproduciendo</p>
<p class="mb-3 text-eyebrow text-fg-3">Reproduciendo</p>
<div class="mb-6 rounded-control bg-accent-tint p-2.5">
<MediaIdentity
trackId={player.current.id}
@@ -41,7 +41,7 @@
</div>
{/if}
<div class="mb-2.75 flex items-center justify-between gap-3">
<div class="mb-3 flex items-center justify-between gap-3">
<span class="min-w-0 flex-1 truncate text-eyebrow text-fg-3"> A continuación </span>
{#if upcoming.length > 0}
<button
@@ -64,7 +64,7 @@
subtitle={track.artist || '—'}
subtitleHref={track.ownerUserId}
trackId={track.id}
class="p-2.25"
class="p-2"
>
{#snippet trailing()}
{#if track.duration}
@@ -28,7 +28,7 @@
}
</script>
<div class="flex min-w-0 items-center gap-2.5 md:gap-3.25">
<div class="flex min-w-0 items-center gap-2.5 md:gap-3">
{#if player.current.id}
<Artwork
trackIds={[player.current.id]}
@@ -12,7 +12,7 @@
const sideClass = $derived(
compact
? 'grid h-10 w-10 place-items-center text-fg opacity-70 transition active:scale-90'
? 'grid size-10 place-items-center text-fg opacity-70 transition active:scale-90'
: 'flex text-fg-2 transition-colors hover:text-fg active:scale-90'
);
</script>
@@ -26,7 +26,7 @@
aria-label="Aleatorio"
class="transition-colors hover:text-fg {player.shuffle ? 'text-accent' : 'text-fg-3'}"
>
<Shuffle class="h-3.75 w-3.75" strokeWidth={1.8} />
<Shuffle class="size-4" strokeWidth={1.8} />
</button>
{/if}
@@ -58,7 +58,7 @@
aria-label="Repetir"
class="transition-colors hover:text-fg {player.repeat ? 'text-accent' : 'text-fg-3'}"
>
<Repeat class="h-3.75 w-3.75" strokeWidth={1.8} />
<Repeat class="size-4" strokeWidth={1.8} />
</button>
{/if}
</div>
@@ -58,7 +58,7 @@
{:else}
<button {type} disabled={disabled || loading} {onclick} class={classes}>
{#if loading}
<LoaderCircle class="h-4 w-4 animate-spin" />
<LoaderCircle class="size-4 animate-spin" />
{/if}
{@render children()}
</button>
@@ -13,15 +13,15 @@
</script>
<label class="flex items-center gap-3 text-sm text-fg-2 {klass}">
<span class="relative inline-grid h-5 w-5 shrink-0 place-items-center">
<span class="relative inline-grid size-5 shrink-0 place-items-center">
<input
type="checkbox"
{name}
bind:checked
class="peer h-5 w-5 cursor-pointer appearance-none rounded border border-line bg-surface transition checked:border-accent-soft checked:bg-accent-soft focus-visible:border-accent/60 focus-visible:outline-none"
class="peer size-5 cursor-pointer appearance-none rounded-tag border border-line bg-surface transition checked:border-accent-soft checked:bg-accent-soft focus-visible:border-accent/60 focus-visible:outline-none"
/>
<Check
class="pointer-events-none absolute h-3.5 w-3.5 text-ink opacity-0 transition peer-checked:opacity-100"
class="pointer-events-none absolute size-3.5 text-ink opacity-0 transition peer-checked:opacity-100"
strokeWidth={3}
/>
</span>
+1 -1
View File
@@ -14,7 +14,7 @@
const classes = $derived(
href
? 'rounded-full border border-line-strong px-3.5 py-2 text-xs text-fg-2 transition-colors hover:bg-hover hover:text-fg'
: `flex items-center gap-1.75 rounded-full px-3.5 py-1.75 text-xs font-medium transition ${
: `flex items-center gap-2 rounded-full px-3.5 py-2 text-xs font-medium transition ${
selected
? 'bg-cta-strong text-ink'
: 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg'
@@ -19,10 +19,10 @@
</script>
{#if overlay}
<div class="absolute inset-0 flex items-end justify-center gap-0.75 bg-scrim/45 pb-2.5 {klass}">
<div class="absolute inset-0 flex items-end justify-center gap-1 bg-scrim/45 pb-2.5 {klass}">
{#each delays as delay (delay)}
<span
class="h-3.5 w-0.75 origin-bottom rounded-full bg-on-art"
class="h-3.5 w-1 origin-bottom rounded-full bg-on-art"
style="animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
? 'paused'
: 'running'}"
@@ -7,7 +7,7 @@
</script>
<span
class="grid h-4 w-4 shrink-0 place-items-center rounded-sm bg-fg-3 text-xs leading-4 font-bold text-bg {klass}"
class="grid size-4 shrink-0 place-items-center rounded-tag bg-fg-3 text-xs leading-4 font-bold text-bg {klass}"
title="Contenido explícito"
aria-label="Contenido explícito">E</span
>
@@ -25,7 +25,7 @@
}: Props = $props();
const SIZE: Record<'default' | 'hero', string> = {
default: 'h-40 w-40 sm:h-52 sm:w-52 rounded-control',
default: 'size-40 sm:size-52 rounded-control',
hero: 'size-cover-hero rounded-art-lg'
};
@@ -34,6 +34,6 @@
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
{#if loading}
<LoaderCircle class="h-5 w-5 animate-spin text-muted" />
<LoaderCircle class="size-5 animate-spin text-muted" />
{/if}
</div>
@@ -49,7 +49,7 @@
const classes = $derived(
`flex items-center gap-3 text-left transition-colors ${
variant === 'card' ? 'rounded-art p-2.25 pr-3.5' : 'rounded-control p-2'
variant === 'card' ? 'rounded-art p-2 pr-3.5' : 'rounded-control p-2'
} ${active ? 'bg-accent-tint' : 'hover:bg-hover'} ${klass}`
);
</script>
+1 -1
View File
@@ -6,7 +6,7 @@
let { size = 'sm' }: Props = $props();
const MARK: Record<'sm' | 'lg', string> = {
sm: 'size-5.5 rounded-lg',
sm: 'size-5.5 rounded-thumb',
lg: 'size-8 rounded-control'
};
@@ -95,7 +95,7 @@
</h2>
</div>
<IconButton label="Cerrar" onclick={onClose} class="shrink-0">
<X class="h-4 w-4" />
<X class="size-4" />
</IconButton>
</div>
{/if}
@@ -30,7 +30,7 @@
{title}
</h2>
{#if subtitle}
<p class="mt-1.25 text-xs text-fg-3">{subtitle}</p>
<p class="mt-1 text-xs text-fg-3">{subtitle}</p>
{/if}
{/snippet}
@@ -27,7 +27,7 @@
{#snippet actions()}
{#if items.length > 0}
<Button onclick={() => (creating = true)}>
<Plus class="h-4 w-4" strokeWidth={2.5} />
<Plus class="size-4" strokeWidth={2.5} />
Nuevo álbum
</Button>
{/if}
@@ -76,20 +76,20 @@
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
<Pause class="size-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
<Play class="size-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
{#if isOwner}
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
<SquarePencil class="size-4" strokeWidth={1.5} />
Editar
</Button>
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
<Trash class="h-4 w-4" strokeWidth={1.5} />
<Trash class="size-4" strokeWidth={1.5} />
Eliminar
</Button>
{/if}
@@ -212,7 +212,7 @@
)} en canciones, álbumes, playlists y usuarios"
/>
<div class="mb-7 flex flex-wrap gap-1.75 sm:mb-8">
<div class="mb-7 flex flex-wrap gap-2 sm:mb-8">
{#each chips as chip (chip.label)}
<Chip
selected={sfilter === chip.label}
@@ -249,11 +249,11 @@
{/if}
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
<div class="text-xs leading-none font-semibold tracking-[0.14em] text-fg-3 uppercase">
<div class="text-eyebrow leading-none font-semibold text-fg-3">
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
</div>
<div
class="truncate font-display text-lg leading-none font-medium tracking-[-0.02em] text-fg"
class="truncate font-display text-lg leading-none font-medium tracking-display text-fg"
>
{topResult.kind === 'track'
? topResult.track.title
@@ -313,7 +313,7 @@
{#each genreTiles as genre, i (genre.query)}
<a
href={searchHref(genre.query)}
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-panel p-4.25 transition-[filter] genre-tile hover:brightness-110"
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-panel p-4 transition-[filter] genre-tile hover:brightness-110"
style="--i:{i}; --tile-hue:{genre.hue}"
>
<div class="text-on-art">
@@ -34,7 +34,7 @@
{#snippet actions()}
{#if items.length > 0}
<Button href="/upload">
<Upload class="h-4 w-4" />
<Upload class="size-4" />
Subir canción
</Button>
{/if}
@@ -56,7 +56,7 @@
>
{#snippet actions()}
<Button href="/upload">
<Upload class="h-4 w-4" />
<Upload class="size-4" />
Sube la primera
</Button>
{/snippet}
@@ -75,10 +75,10 @@
{#snippet actions()}
<Button size="sm" onclick={() => playAllOrToggle(queue)} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
<Pause class="size-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
<Play class="size-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
@@ -88,7 +88,7 @@
onclick={() => playShuffled(queue)}
disabled={queue.length === 0}
>
<Shuffle class="h-4 w-4" />
<Shuffle class="size-4" />
Aleatorio
</Button>
{/snippet}
@@ -66,19 +66,19 @@
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
<Pause class="size-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
<Play class="size-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
<SquarePencil class="size-4" strokeWidth={1.5} />
Editar
</Button>
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
<Trash class="h-4 w-4" strokeWidth={1.5} />
<Trash class="size-4" strokeWidth={1.5} />
Eliminar
</Button>
{/snippet}
@@ -110,14 +110,13 @@
<Surface padding="sm" class={stepClass(stepStates[i])}>
<div class="flex items-center gap-3">
<span
class="grid h-7 w-7 shrink-0 place-items-center rounded-full text-sm {stepStates[
i
] === 'idle'
class="grid size-7 shrink-0 place-items-center rounded-full text-sm {stepStates[i] ===
'idle'
? 'bg-surface-2 text-fg-2'
: 'bg-accent-btn text-accent-soft'}"
>
{#if stepStates[i] === 'done'}
<Check class="h-3.5 w-3.5" strokeWidth={2.5} />
<Check class="size-3.5" strokeWidth={2.5} />
{:else}
{i + 1}
{/if}
@@ -132,10 +131,10 @@
{#if status === 'done'}
<Surface padding="lg" class="mt-10 bg-accent-tint text-center">
<span class="mx-auto grid h-14 w-14 place-items-center rounded-full bg-accent-soft text-ink">
<Check class="h-7 w-7" strokeWidth={2.5} />
<span class="mx-auto grid size-14 place-items-center rounded-full bg-accent-soft text-ink">
<Check class="size-7" strokeWidth={2.5} />
</span>
<h2 class="mt-6 font-display text-2xl font-semibold tracking-[-0.02em] text-fg">¡Subida!</h2>
<h2 class="mt-6 font-display text-2xl font-semibold tracking-display text-fg">¡Subida!</h2>
<p class="mt-3 text-sm text-fg-2">
«{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en
unos momentos.
@@ -192,9 +191,9 @@
aria-label="Seleccionar archivo de audio"
/>
{#if audioName === ''}
<Headphones class="h-9 w-9 text-muted sm:h-10 sm:w-10" strokeWidth={1.4} />
<Headphones class="size-9 text-muted sm:size-10" strokeWidth={1.4} />
{:else}
<Music class="h-9 w-9 text-accent-soft sm:h-10 sm:w-10" strokeWidth={1.4} />
<Music class="size-9 text-accent-soft sm:size-10" strokeWidth={1.4} />
{/if}
{#if audioName === ''}
<p class="mt-4 text-sm font-medium text-fg">
+1 -1
View File
@@ -135,7 +135,7 @@
<Page>
<div
class="relative overflow-hidden rounded-3xl hero-surface px-6 py-8 theme-transition sm:px-9 sm:py-8.5"
class="relative overflow-hidden rounded-panel-lg hero-surface px-6 py-8 theme-transition sm:px-9 sm:py-8.5"
>
<div
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
-10
View File
@@ -97,16 +97,6 @@
scrollbar-gutter: stable;
}
@utility stroke-thin {
stroke-width: 1.3;
}
@utility stroke-regular {
stroke-width: 1.8;
}
@utility stroke-bold {
stroke-width: 2.5;
}
@utility text-display-1 {
@apply font-display text-3xl font-medium tracking-tight sm:text-4xl;
}