refactor(web-player): centralise styles and drop dead code

Styles: adds the scrim and on-art tokens so the black overlays and the
white marks drawn over artwork stop being raw tailwind colours, and
routes every component through the semantic layer instead of reaching
into the --mf-* palette. Renames --mf-accent-60, which was actually an
80% mix, to --mf-accent-soft, and removes the unused --mf-radius and
the --color-neutral-950 override.

Dead code: the root +layout.server.ts already redirects everything
except /login, so the anonymous branches in the home and explore loads
and the needsAuth empty state could never run. Deletes the unused
accentSoft helper, and stops exporting helpers nobody imports.

fmtTime and fmtDate move out of theme/color.ts into $lib/format.ts;
they are not colour concerns.

The accent animation now cancels its pending frame on teardown.
This commit is contained in:
2026-07-20 14:35:45 +02:00
parent e10c24cad0
commit d4a66e4320
22 changed files with 68 additions and 79 deletions
@@ -1,5 +1,5 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { createApiClient, requireUser, unwrapOrError } from '$lib/server/api';
import { fetchYoutubeFiller } from '$lib/server/youtube';
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
import { EXPLORE_PAGE_SIZE as PAGE_SIZE, YOUTUBE_FILLER_LIMIT } from '$lib/config';
@@ -8,23 +8,21 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
const query = url.searchParams.get('q')?.trim() ?? '';
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const tracksPromise = api.GET('/tracks', {
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
});
const searchPromise =
query && locals.accessToken
? api.GET('/youtube/search', { params: { query: { query } } })
: Promise.resolve(null);
const playlistsPromise = locals.user
? api.GET('/playlists/users/{userId}', {
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } }
})
const searchPromise = query
? api.GET('/youtube/search', { params: { query: { query } } })
: Promise.resolve(null);
const playlistsPromise = api.GET('/playlists/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
});
const [tracksRes, searchRes, playlistsRes] = await Promise.all([
tracksPromise,
searchPromise,
@@ -41,8 +39,7 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
youtubeFiller: query
? Promise.resolve(null)
: fetchYoutubeFiller(api, locals.accessToken, YOUTUBE_FILLER_LIMIT),
playlists: playlistsRes?.data?.items ?? [],
needsAuth: !locals.user
playlists: playlistsRes?.data?.items ?? []
};
};
@@ -84,9 +84,7 @@
const hasMore = $derived(localHasNext || ytContinuation !== '');
const nothingFound = $derived(
!!data.query && items.length === 0 && !data.needsAuth && !data.ytError
);
const nothingFound = $derived(!!data.query && items.length === 0 && !data.ytError);
function buildHref(query: string) {
return query ? `/explore?q=${encodeURIComponent(query)}` : '/explore';
@@ -212,12 +210,7 @@
<InfiniteScroll onLoadMore={loadMore} {hasMore} loading={loadingMore} />
{/if}
{#if data.needsAuth}
<EmptyState
icon={Music}
description="Inicia sesión para ver también resultados de YouTube Music."
/>
{:else if data.ytError}
{#if data.ytError}
<EmptyState
icon={Music}
description="YouTube Music no está disponible ahora mismo. Inténtalo de nuevo."
@@ -4,7 +4,8 @@
import X from '@lucide/svelte/icons/x';
import Music from '@lucide/svelte/icons/music';
import { player, toQueueItems } from '$lib/player/player.svelte';
import { hueFor, fmtTime, fmtDate } from '$lib/theme/color';
import { hueFor } from '$lib/theme/color';
import { fmtTime, fmtDate } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import PageHeader from '$lib/components/ui/PageHeader.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
@@ -9,7 +9,8 @@
youTubeThumbnailUrl,
type ApiTrackLike
} from '$lib/player/player.svelte';
import { hueFor, fmtTime, fmtDate } from '$lib/theme/color';
import { hueFor } from '$lib/theme/color';
import { fmtTime, fmtDate } from '$lib/format';
import Badge from '$lib/components/ui/Badge.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
@@ -7,7 +7,8 @@
type ApiTrackLike,
type QueueItem
} from '$lib/player/player.svelte';
import { hueFor, fmtTime } from '$lib/theme/color';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import type { YouTubeSong } from '$lib/types';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
+3 -2
View File
@@ -52,18 +52,19 @@
if (t < 1) rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafId);
});
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{#if isAuthPage}
{#if isAuthPage || !data.user}
<div class="min-h-screen bg-bg text-fg antialiased">
{@render children()}
</div>
{:else}
<div class="flex min-h-screen bg-bg text-fg antialiased">
<Sidebar user={data.user!} />
<Sidebar user={data.user} />
<div class="flex min-w-0 flex-1 flex-col">
<main class="flex-1 transition-[padding] {hasTrack ? 'pb-[92px]' : 'pb-0'}">
{#key page.url.pathname}
+9 -10
View File
@@ -1,24 +1,23 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, playlistCoverTrackIds } from '$lib/server/api';
import { createApiClient, playlistCoverTrackIds, requireUser } from '$lib/server/api';
import { fetchYoutubeFiller, shuffle } from '$lib/server/youtube';
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
import { HOME_LATEST_PAGE_SIZE, YOUTUBE_FILLER_LIMIT } from '$lib/config';
import { pickGreeting } from '$lib/server/greeting';
export const load: PageServerLoad = async ({ locals, fetch }) => {
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const latestPromise = api.GET('/tracks', {
params: { query: { pageNumber: 1, pageSize: HOME_LATEST_PAGE_SIZE } }
});
const playlistsPromise = locals.user
? api.GET('/playlists/users/{userId}', {
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 12 } }
})
: Promise.resolve({ data: undefined });
const recentlyPlayedPromise = locals.user
? api.GET('/users/{id}/listening-history', { params: { path: { id: locals.user.sub } } })
: Promise.resolve({ data: undefined });
const playlistsPromise = api.GET('/playlists/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 12 } }
});
const recentlyPlayedPromise = api.GET('/users/{id}/listening-history', {
params: { path: { id: user.sub } }
});
const [latest, playlists, recentlyPlayed] = await Promise.all([
latestPromise,
+1 -1
View File
@@ -171,7 +171,7 @@
<section class="relative overflow-hidden page-x pt-11 pb-16">
<div
class="pointer-events-none absolute inset-0 blur-[70px] saturate-150"
style="background:linear-gradient(135deg, var(--mf-accent), color-mix(in oklch, var(--mf-accent), black 55%));animation:breathe 9s ease-in-out infinite"
style="background:linear-gradient(135deg, var(--color-accent), color-mix(in oklch, var(--color-accent), black 55%));animation:breathe 9s ease-in-out infinite"
></div>
<div class="animate-enter relative flex flex-col gap-2.5">
<p class="tracking-[0.14em] text-fg-2 uppercase">{data.greeting}</p>
+4 -3
View File
@@ -2,7 +2,6 @@
@theme {
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
--color-neutral-950: #08080a;
--color-bg: var(--mf-bg);
--color-elevated: var(--mf-elevated);
@@ -18,8 +17,10 @@
--color-fg-3: var(--mf-text-3);
--color-muted: var(--mf-text-4);
--color-accent: var(--mf-accent);
--color-accent-soft: var(--mf-accent-60);
--color-on-accent: #ffffff;
--color-accent-soft: var(--mf-accent-soft);
--color-on-accent: var(--mf-on-art);
--color-scrim: var(--mf-scrim);
--color-on-art: var(--mf-on-art);
--color-danger: oklch(63.7% 0.237 25.331);
--color-danger-fg: oklch(88.5% 0.062 18.334);
+1 -1
View File
@@ -12,7 +12,7 @@
</svelte:head>
<div class="grid min-h-screen place-items-center bg-bg px-6 text-fg">
<div class="flex w-full max-w-[440px] flex-col items-center gap-9 text-center">
<div class="flex w-full max-w-110 flex-col items-center gap-9 text-center">
<div class="text-3xl font-extrabold tracking-tight">Musify</div>
<div>