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:
+18
-4
@@ -11,11 +11,12 @@ No hay `svelte.config.js` (config inline en `vite.config.ts`) ni `tailwind.confi
|
|||||||
## Comandos
|
## Comandos
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run dev # servidor de desarrollo (puerto 5173)
|
npm run dev # servidor de desarrollo (puerto 5173)
|
||||||
npm run build
|
npm run build
|
||||||
npm run check # svelte-check — debe salir 0 errores antes de cerrar un cambio
|
npm run check # svelte-check — debe salir 0 errores antes de cerrar un cambio
|
||||||
npm run format # prettier --write .
|
npm run format # prettier --write .
|
||||||
npm run lint # prettier --check . && eslint .
|
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
|
## Reglas
|
||||||
@@ -28,6 +29,19 @@ npm run lint # prettier --check . && eslint .
|
|||||||
- Enlaces a endpoints `+server.ts` (login/logout) llevan `data-sveltekit-reload`.
|
- 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.
|
- 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
|
## 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`.
|
`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).
|
|
||||||
@@ -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) |
|
| 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 |
|
| 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 |
|
| 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):
|
Decisiones de producto pendientes (preguntar al usuario antes de la parte indicada):
|
||||||
- P0/P4: ¿`Button` primary debe verse distinto de secondary?
|
- 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?
|
- P10: ¿iconos en los botones de cabecera de álbum **y** playlist?
|
||||||
- P12: ¿logo con cuadrado de gradiente también en la Sidebar?
|
- 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)
|
## E. Inventario completo (veredicto por archivo)
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"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",
|
"gen:api": "npx -y openapi-typescript@7 http://localhost:5111/openapi/v1.json -o src/lib/api/schema.d.ts",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint:tokens": "node scripts/check-tokens.mjs",
|
||||||
|
"lint": "prettier --check . && eslint . && npm run lint:tokens"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
@@ -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.');
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<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)"
|
style="width:var(--mf-sidebar-w)"
|
||||||
>
|
>
|
||||||
<a href="/" class="px-3 pb-3">
|
<a href="/" class="px-3 pb-3">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
? 'bg-surface-2 text-fg'
|
? 'bg-surface-2 text-fg'
|
||||||
: 'text-fg-2 hover:bg-hover hover: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>
|
<span class="flex-1 truncate">{link.label}</span>
|
||||||
{#if link.count}
|
{#if link.count}
|
||||||
<span class="text-xs text-muted tabular-nums">{link.count}</span>
|
<span class="text-xs text-muted tabular-nums">{link.count}</span>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
playlist.updatedAt
|
playlist.updatedAt
|
||||||
)}"
|
)}"
|
||||||
trackIds={playlist.coverTrackIds}
|
trackIds={playlist.coverTrackIds}
|
||||||
class="px-3 py-1.75"
|
class="px-3 py-2"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
onclick={() => createPlaylistModal.show()}
|
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"
|
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
|
Crear tu primera lista
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<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))"
|
style="padding-bottom:calc(0.4375rem + var(--mf-safe-b))"
|
||||||
>
|
>
|
||||||
{#each links as link (link.href)}
|
{#each links as link (link.href)}
|
||||||
@@ -26,11 +26,11 @@
|
|||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
aria-current={active ? 'page' : undefined}
|
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'
|
||||||
: 'text-fg-2'}"
|
: '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>
|
<span class="text-xs font-medium">{link.label}</span>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<label
|
<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"
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:this={searchInput}
|
bind:this={searchInput}
|
||||||
@@ -83,9 +83,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={clearQuery}
|
onclick={clearQuery}
|
||||||
aria-label="Borrar búsqueda"
|
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>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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">
|
<div class="min-w-0 flex-1">
|
||||||
<TrackInfo compact />
|
<TrackInfo compact />
|
||||||
@@ -24,14 +24,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<div class="w-(--mf-player-side-w) min-w-0 shrink-0">
|
||||||
<TrackInfo />
|
<TrackInfo />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
|
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
|
||||||
<TransportControls />
|
<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">
|
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
|
||||||
{fmtTime(player.progress)}
|
{fmtTime(player.progress)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if player.current.id}
|
{#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">
|
<div class="mb-6 rounded-control bg-accent-tint p-2.5">
|
||||||
<MediaIdentity
|
<MediaIdentity
|
||||||
trackId={player.current.id}
|
trackId={player.current.id}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
<span class="min-w-0 flex-1 truncate text-eyebrow text-fg-3"> A continuación </span>
|
||||||
{#if upcoming.length > 0}
|
{#if upcoming.length > 0}
|
||||||
<button
|
<button
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
subtitle={track.artist || '—'}
|
subtitle={track.artist || '—'}
|
||||||
subtitleHref={track.ownerUserId}
|
subtitleHref={track.ownerUserId}
|
||||||
trackId={track.id}
|
trackId={track.id}
|
||||||
class="p-2.25"
|
class="p-2"
|
||||||
>
|
>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
{#if track.duration}
|
{#if track.duration}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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}
|
{#if player.current.id}
|
||||||
<Artwork
|
<Artwork
|
||||||
trackIds={[player.current.id]}
|
trackIds={[player.current.id]}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
const sideClass = $derived(
|
const sideClass = $derived(
|
||||||
compact
|
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'
|
: 'flex text-fg-2 transition-colors hover:text-fg active:scale-90'
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
aria-label="Aleatorio"
|
aria-label="Aleatorio"
|
||||||
class="transition-colors hover:text-fg {player.shuffle ? 'text-accent' : 'text-fg-3'}"
|
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>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
aria-label="Repetir"
|
aria-label="Repetir"
|
||||||
class="transition-colors hover:text-fg {player.repeat ? 'text-accent' : 'text-fg-3'}"
|
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>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button {type} disabled={disabled || loading} {onclick} class={classes}>
|
<button {type} disabled={disabled || loading} {onclick} class={classes}>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<LoaderCircle class="h-4 w-4 animate-spin" />
|
<LoaderCircle class="size-4 animate-spin" />
|
||||||
{/if}
|
{/if}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,15 +13,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label class="flex items-center gap-3 text-sm text-fg-2 {klass}">
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{name}
|
{name}
|
||||||
bind:checked
|
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
|
<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}
|
strokeWidth={3}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
const classes = $derived(
|
const classes = $derived(
|
||||||
href
|
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'
|
? '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
|
selected
|
||||||
? 'bg-cta-strong text-ink'
|
? 'bg-cta-strong text-ink'
|
||||||
: 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg'
|
: 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg'
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if overlay}
|
{#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)}
|
{#each delays as delay (delay)}
|
||||||
<span
|
<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
|
style="animation:equalize .9s ease-in-out {delay}s infinite; animation-play-state:{paused
|
||||||
? 'paused'
|
? 'paused'
|
||||||
: 'running'}"
|
: 'running'}"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<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"
|
title="Contenido explícito"
|
||||||
aria-label="Contenido explícito">E</span
|
aria-label="Contenido explícito">E</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const SIZE: Record<'default' | 'hero', string> = {
|
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'
|
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">
|
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<LoaderCircle class="h-5 w-5 animate-spin text-muted" />
|
<LoaderCircle class="size-5 animate-spin text-muted" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
const classes = $derived(
|
const classes = $derived(
|
||||||
`flex items-center gap-3 text-left transition-colors ${
|
`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}`
|
} ${active ? 'bg-accent-tint' : 'hover:bg-hover'} ${klass}`
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
let { size = 'sm' }: Props = $props();
|
let { size = 'sm' }: Props = $props();
|
||||||
|
|
||||||
const MARK: Record<'sm' | 'lg', string> = {
|
const MARK: Record<'sm' | 'lg', string> = {
|
||||||
sm: 'size-5.5 rounded-lg',
|
sm: 'size-5.5 rounded-thumb',
|
||||||
lg: 'size-8 rounded-control'
|
lg: 'size-8 rounded-control'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<IconButton label="Cerrar" onclick={onClose} class="shrink-0">
|
<IconButton label="Cerrar" onclick={onClose} class="shrink-0">
|
||||||
<X class="h-4 w-4" />
|
<X class="size-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{#if subtitle}
|
{#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}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
{#if items.length > 0}
|
{#if items.length > 0}
|
||||||
<Button onclick={() => (creating = true)}>
|
<Button onclick={() => (creating = true)}>
|
||||||
<Plus class="h-4 w-4" strokeWidth={2.5} />
|
<Plus class="size-4" strokeWidth={2.5} />
|
||||||
Nuevo álbum
|
Nuevo álbum
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -76,20 +76,20 @@
|
|||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
||||||
{#if isCurrentQueue && player.playing}
|
{#if isCurrentQueue && player.playing}
|
||||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
<Pause class="size-4" strokeWidth={1.5} />
|
||||||
Pausar
|
Pausar
|
||||||
{:else}
|
{:else}
|
||||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
<Play class="size-4" strokeWidth={1.5} />
|
||||||
Reproducir
|
Reproducir
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
|
<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
|
Editar
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
<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
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@
|
|||||||
)} en canciones, álbumes, playlists y usuarios"
|
)} 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)}
|
{#each chips as chip (chip.label)}
|
||||||
<Chip
|
<Chip
|
||||||
selected={sfilter === chip.label}
|
selected={sfilter === chip.label}
|
||||||
@@ -249,11 +249,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
<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]}
|
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
|
||||||
</div>
|
</div>
|
||||||
<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.kind === 'track'
|
||||||
? topResult.track.title
|
? topResult.track.title
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
{#each genreTiles as genre, i (genre.query)}
|
{#each genreTiles as genre, i (genre.query)}
|
||||||
<a
|
<a
|
||||||
href={searchHref(genre.query)}
|
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}"
|
style="--i:{i}; --tile-hue:{genre.hue}"
|
||||||
>
|
>
|
||||||
<div class="text-on-art">
|
<div class="text-on-art">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
{#if items.length > 0}
|
{#if items.length > 0}
|
||||||
<Button href="/upload">
|
<Button href="/upload">
|
||||||
<Upload class="h-4 w-4" />
|
<Upload class="size-4" />
|
||||||
Subir canción
|
Subir canción
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button href="/upload">
|
<Button href="/upload">
|
||||||
<Upload class="h-4 w-4" />
|
<Upload class="size-4" />
|
||||||
Sube la primera
|
Sube la primera
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -75,10 +75,10 @@
|
|||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={() => playAllOrToggle(queue)} disabled={queue.length === 0}>
|
<Button size="sm" onclick={() => playAllOrToggle(queue)} disabled={queue.length === 0}>
|
||||||
{#if isCurrentQueue && player.playing}
|
{#if isCurrentQueue && player.playing}
|
||||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
<Pause class="size-4" strokeWidth={1.5} />
|
||||||
Pausar
|
Pausar
|
||||||
{:else}
|
{:else}
|
||||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
<Play class="size-4" strokeWidth={1.5} />
|
||||||
Reproducir
|
Reproducir
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
onclick={() => playShuffled(queue)}
|
onclick={() => playShuffled(queue)}
|
||||||
disabled={queue.length === 0}
|
disabled={queue.length === 0}
|
||||||
>
|
>
|
||||||
<Shuffle class="h-4 w-4" />
|
<Shuffle class="size-4" />
|
||||||
Aleatorio
|
Aleatorio
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -66,19 +66,19 @@
|
|||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
||||||
{#if isCurrentQueue && player.playing}
|
{#if isCurrentQueue && player.playing}
|
||||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
<Pause class="size-4" strokeWidth={1.5} />
|
||||||
Pausar
|
Pausar
|
||||||
{:else}
|
{:else}
|
||||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
<Play class="size-4" strokeWidth={1.5} />
|
||||||
Reproducir
|
Reproducir
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
|
<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
|
Editar
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
<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
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -110,14 +110,13 @@
|
|||||||
<Surface padding="sm" class={stepClass(stepStates[i])}>
|
<Surface padding="sm" class={stepClass(stepStates[i])}>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
class="grid h-7 w-7 shrink-0 place-items-center rounded-full text-sm {stepStates[
|
class="grid size-7 shrink-0 place-items-center rounded-full text-sm {stepStates[i] ===
|
||||||
i
|
'idle'
|
||||||
] === 'idle'
|
|
||||||
? 'bg-surface-2 text-fg-2'
|
? 'bg-surface-2 text-fg-2'
|
||||||
: 'bg-accent-btn text-accent-soft'}"
|
: 'bg-accent-btn text-accent-soft'}"
|
||||||
>
|
>
|
||||||
{#if stepStates[i] === 'done'}
|
{#if stepStates[i] === 'done'}
|
||||||
<Check class="h-3.5 w-3.5" strokeWidth={2.5} />
|
<Check class="size-3.5" strokeWidth={2.5} />
|
||||||
{:else}
|
{:else}
|
||||||
{i + 1}
|
{i + 1}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -132,10 +131,10 @@
|
|||||||
|
|
||||||
{#if status === 'done'}
|
{#if status === 'done'}
|
||||||
<Surface padding="lg" class="mt-10 bg-accent-tint text-center">
|
<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">
|
<span class="mx-auto grid size-14 place-items-center rounded-full bg-accent-soft text-ink">
|
||||||
<Check class="h-7 w-7" strokeWidth={2.5} />
|
<Check class="size-7" strokeWidth={2.5} />
|
||||||
</span>
|
</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">
|
<p class="mt-3 text-sm text-fg-2">
|
||||||
«{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en
|
«{publishedTitle}» se está procesando. Aparecerá lista para reproducir en tu biblioteca en
|
||||||
unos momentos.
|
unos momentos.
|
||||||
@@ -192,9 +191,9 @@
|
|||||||
aria-label="Seleccionar archivo de audio"
|
aria-label="Seleccionar archivo de audio"
|
||||||
/>
|
/>
|
||||||
{#if audioName === ''}
|
{#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}
|
{: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}
|
||||||
{#if audioName === ''}
|
{#if audioName === ''}
|
||||||
<p class="mt-4 text-sm font-medium text-fg">
|
<p class="mt-4 text-sm font-medium text-fg">
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<div
|
<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
|
<div
|
||||||
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
|
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
|
||||||
|
|||||||
@@ -97,16 +97,6 @@
|
|||||||
scrollbar-gutter: stable;
|
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 {
|
@utility text-display-1 {
|
||||||
@apply font-display text-3xl font-medium tracking-tight sm:text-4xl;
|
@apply font-display text-3xl font-medium tracking-tight sm:text-4xl;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user