Files
Musify/web-player/src/routes/(app)/mixes/[id]/+page.svelte
T
AdminandClaude Sonnet 5 ec6313dda1 web-player: P14 — barrido final y guardarraíles
Añade scripts/check-tokens.mjs (npm run lint:tokens, incorporado a
lint) como guardrail: falla si src/**/*.svelte reintroduce valores
arbitrarios fuera de la lista blanca de B.4, rounded-* de fábrica,
pares h-N w-N (deberían ser size-N), espaciado fraccionario en
cuartos, o colores hex/rgba() literales.

Se limpia el repo hasta dejar el script en 0 violaciones: ~40 pares
h-N w-N → size-N (sobre todo iconos, vía script puntual), ~26
espaciados fraccionarios en cuartos redondeados al entero más cercano
(Δ≤1px, p. ej. py-2.75→py-3, gap-1.75→gap-2), radios de fábrica
sueltos (rounded-sm/lg, rounded a secas) migrados a los tokens
correspondientes, y dos tracking-[-0.02em] que coincidían
exactamente con el token tracking-display ya existente pero nunca se
habían migrado. Se retiran las recetas stroke-thin/regular/bold
(creadas en P1, cero consumidores) — decisión tomada, sin bloquear:
no se migra strokeWidth={n} (13 valores, en casi todos los
componentes) a 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 en vez de fingir cobertura completa.

Detector de huérfanos: 0 componentes sin importadores. Se borra
docs/design-system-refactor-plan.md (plan anterior, superseded por
frontend-refactor-plan.md). web-player/CLAUDE.md gana una sección
"Estilos" con la lista blanca B.4 y los componentes base a reutilizar.
Se recalcula la tabla de línea base (A.4) y se anota en el Registro
(D.1): 61→47 componentes, 44→11 arbitrarios (los 11 restantes en
lista blanca), 13→8 radios distintos, 0 violaciones de lint:tokens.

Verificado con npm run check (0 errores), npm run lint:tokens (0
violaciones) y npm run build. Smoke test de /login sin errores de
consola achacables al cambio (Docker no disponible en esta sesión).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 15:05:07 +02:00

126 lines
3.3 KiB
Svelte

<script lang="ts">
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import Shuffle from '@lucide/svelte/icons/shuffle';
import {
player,
isQueueCurrent,
playAllOrToggle,
playShuffled,
toQueueItem
} from '$lib/player/player.svelte';
import { fmtTime, plural } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import PageHeader from '$lib/components/ui/PageHeader.svelte';
import TrackList from '$lib/components/ui/TrackList.svelte';
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import ListPlus from '@lucide/svelte/icons/list-plus';
import { createTrackMenu } from '$lib/menus.svelte';
import { mixItemTrack } from '$lib/mixes';
let { data, form } = $props();
const mix = $derived(data.mix);
const items = $derived(mix.items);
const tracks = $derived(items.map(mixItemTrack));
const queue = $derived(tracks.map(toQueueItem));
const listTracks = $derived(
items.map((item) => ({
id: item.trackId,
title: item.title,
artist: item.artist,
duration: item.durationSeconds,
listensCount: item.listensCount
}))
);
const totalSeconds = $derived(
items.reduce((total, item) => total + Number(item.durationSeconds), 0)
);
const isCurrentQueue = $derived(isQueueCurrent(queue));
const trackMenu = createTrackMenu();
function playFrom(index: number) {
player.playOrToggle(queue, index);
}
</script>
<svelte:head>
<title>{mix.title}</title>
<meta name="description" content="Mezcla {mix.title}, hecha para ti." />
</svelte:head>
<Page>
<BackLink href="/" label="Volver al inicio" />
<PageHeader
eyebrow="Mezcla"
title={mix.title}
description={mix.subtitle ?? undefined}
meta="{plural(items.length, 'canción', 'canciones')} · {fmtTime(totalSeconds)}"
>
{#snippet cover()}
<Artwork
trackIds={items.map((item) => item.trackId)}
size="hero"
alt={mix.title}
class="shrink-0"
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={() => playAllOrToggle(queue)} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="size-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="size-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
<Button
variant="secondary"
size="sm"
onclick={() => playShuffled(queue)}
disabled={queue.length === 0}
>
<Shuffle class="size-4" />
Aleatorio
</Button>
{/snippet}
</PageHeader>
{#if form?.message}
<Alert tone="danger" class="mb-6">{form.message}</Alert>
{/if}
<TrackList
tracks={listTracks}
columns={['plays']}
onPlay={playFrom}
oncontextmenu={(e, _track, i) => trackMenu.open(e, tracks[i])}
/>
</Page>
{#if trackMenu.state}
<ContextMenu
x={trackMenu.state.x}
y={trackMenu.state.y}
openLeft={trackMenu.state.openLeft}
onClose={() => trackMenu.close()}
items={[
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
]}
playlistAction={{
action: '?/addTrack',
fields: { trackId: String(trackMenu.state.track.id) },
label: 'Añadir a una playlist'
}}
playlists={data.playlists}
/>
{/if}