Fix light-mode regression and finish design-system cleanup

A code-review pass on the previous commit found a real bug: the explore
genre tiles used var(--mf-ink) as a decorative gradient endpoint, but that
token is meant for text-on-accent and flips to white in light mode,
turning the tiles solid white. Replaces it with a new, deliberately
theme-invariant --mf-tile-shade token (same category as --mf-cover-grad).

Also: raises CollectionHeader's meta-line contrast in light mode
(text-muted, ~3:1, to text-fg-2), makes its actions prop optional so
pages without conditional actions don't render an empty wrapper, moves
AccountMenu.svelte into $lib/components/ui/ per the repo's own
componentization rule, adds the aria-expanded TopBar's trigger was
missing relative to MobileHeader's, and migrates the profile page
(u/[id]/+page.svelte) onto CollectionHeader — a sixth duplicate of the
same header pattern that the previous commit touched but missed.
This commit is contained in:
2026-09-15 18:06:37 +02:00
parent f1756468bc
commit 5696683859
8 changed files with 78 additions and 60 deletions
+25 -9
View File
@@ -20,15 +20,31 @@
> sobre el "chrome" de la app o sobre arte/gradiente saturado, se extrajeron > sobre el "chrome" de la app o sobre arte/gradiente saturado, se extrajeron
> `lib/components/ui/CollectionHeader.svelte` (portada + título + meta + acciones, usado > `lib/components/ui/CollectionHeader.svelte` (portada + título + meta + acciones, usado
> en `AlbumHeader`, `PlaylistHeader`, `liked/+page.svelte`, `mixes/[id]/+page.svelte`, > en `AlbumHeader`, `PlaylistHeader`, `liked/+page.svelte`, `mixes/[id]/+page.svelte`,
> `albums/external/.../+page.svelte`) y `lib/components/AccountMenu.svelte` (estado de > `albums/external/.../+page.svelte` y `u/[id]/+page.svelte`) y
> apertura + panel del menú de cuenta, usado en `TopBar`/`MobileHeader`), y se corrigió > `lib/components/ui/AccountMenu.svelte` (estado de apertura + panel del menú de cuenta,
> `--color-hover` en `layout.css`, que estaba hardcodeado en vez de apuntar a > usado en `TopBar`/`MobileHeader`), y se corrigió `--color-hover` en `layout.css`, que
> `--mf-surface-2` (rompía el modo claro para cualquier `hover:bg-hover`). Pendiente de > estaba hardcodeado en vez de apuntar a `--mf-surface-2` (rompía el modo claro para
> quien retome esto: verificación visual en el navegador con una sesión autenticada real > cualquier `hover:bg-hover`).
> (esta sesión no tenía credenciales del IdP de Zitadel ni acceso para activar >
> `allowAnonymousListening`), y `lib/components/ui/Rail.svelte`, que el grep de > **Repaso posterior a la verificación visual** (ya con sesión autenticada): se detectó
> huérfanos de §5 marca sin importadores — no se tocó porque no forma parte de este > que el fix de §2 anterior para la tarjeta de género de `explore/+page.svelte` estaba
> barrido, pero conviene confirmarlo a mano y borrarlo si procede. > 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 ## 0. Objetivo
@@ -4,7 +4,7 @@
import Folder from '@lucide/svelte/icons/folder'; import Folder from '@lucide/svelte/icons/folder';
import LogIn from '@lucide/svelte/icons/log-in'; import LogIn from '@lucide/svelte/icons/log-in';
import MenuItem from './ui/MenuItem.svelte'; import MenuItem from './ui/MenuItem.svelte';
import AccountMenu from './AccountMenu.svelte'; import AccountMenu from './ui/AccountMenu.svelte';
interface Props { interface Props {
user: SessionUser | null; user: SessionUser | null;
+3 -2
View File
@@ -7,7 +7,7 @@
import X from '@lucide/svelte/icons/x'; import X from '@lucide/svelte/icons/x';
import ChevronLeft from '@lucide/svelte/icons/chevron-left'; import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right'; import ChevronRight from '@lucide/svelte/icons/chevron-right';
import AccountMenu from './AccountMenu.svelte'; import AccountMenu from './ui/AccountMenu.svelte';
interface Props { interface Props {
user: SessionUser | null; user: SessionUser | null;
@@ -107,11 +107,12 @@
{accountUrl} {accountUrl}
panelClass="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden" panelClass="absolute top-full right-0 z-20 mt-2 w-52 overflow-hidden"
> >
{#snippet trigger({ toggle })} {#snippet trigger({ toggle, open })}
<button <button
type="button" type="button"
onclick={toggle} onclick={toggle}
aria-label="Tu cuenta" aria-label="Tu cuenta"
aria-expanded={open}
class="block h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-line-strong transition active:scale-95" class="block h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-line-strong transition active:scale-95"
> >
{#if user.picture} {#if user.picture}
@@ -6,8 +6,8 @@
import Settings from '@lucide/svelte/icons/settings'; import Settings from '@lucide/svelte/icons/settings';
import Sun from '@lucide/svelte/icons/sun'; import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon'; import Moon from '@lucide/svelte/icons/moon';
import MenuItem from './ui/MenuItem.svelte'; import MenuItem from './MenuItem.svelte';
import GlassMenu from './ui/GlassMenu.svelte'; import GlassMenu from './GlassMenu.svelte';
import { themeMode } from '$lib/theme/mode.svelte'; import { themeMode } from '$lib/theme/mode.svelte';
interface Props { interface Props {
@@ -8,7 +8,7 @@
meta?: string; meta?: string;
align?: 'center' | 'end'; align?: 'center' | 'end';
cover: Snippet; cover: Snippet;
actions: Snippet; actions?: Snippet;
} }
let { eyebrow, title, description, meta, align = 'end', cover, actions }: Props = $props(); let { eyebrow, title, description, meta, align = 'end', cover, actions }: Props = $props();
@@ -33,11 +33,13 @@
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p> <p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
{/if} {/if}
{#if meta} {#if meta}
<p class="mt-3 text-sm text-muted">{meta}</p> <p class="mt-3 text-sm text-fg-2">{meta}</p>
{/if} {/if}
</div> </div>
</div> </div>
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7"> {#if actions}
{@render actions()} <div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
</div> {@render actions()}
</div>
{/if}
+1
View File
@@ -36,6 +36,7 @@
--mf-logo-glow: 0 0 20px oklch(0.74 0.145 145 / 0.3); --mf-logo-glow: 0 0 20px oklch(0.74 0.145 145 / 0.3);
--mf-cover-grad: linear-gradient(150deg, oklch(0.5 0.13 145), oklch(0.16 0.035 145)); --mf-cover-grad: linear-gradient(150deg, oklch(0.5 0.13 145), oklch(0.16 0.035 145));
--mf-liked-grad: linear-gradient(150deg, oklch(0.4 0.09 330), oklch(0.18 0.04 10)); --mf-liked-grad: linear-gradient(150deg, oklch(0.4 0.09 330), oklch(0.18 0.04 10));
--mf-tile-shade: rgba(9, 9, 12, 0.95);
--mf-accent: oklch(0.74 0.145 145); --mf-accent: oklch(0.74 0.145 145);
--mf-accent-title: oklch(0.87 0.07 145); --mf-accent-title: oklch(0.87 0.07 145);
@@ -477,7 +477,7 @@
href={buildHref(genre.query)} href={buildHref(genre.query)}
class="animate-enter group relative block h-28 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-125" class="animate-enter group relative block h-28 overflow-hidden rounded-2xl p-4.25 transition-[filter] hover:brightness-125"
style="animation-delay:{Math.min(i, 10) * style="animation-delay:{Math.min(i, 10) *
40}ms; background:linear-gradient(140deg, oklch(0.24 0.07 {genre.hue}), var(--mf-ink))" 40}ms; background:linear-gradient(140deg, oklch(0.24 0.07 {genre.hue}), var(--mf-tile-shade))"
> >
<div class="font-display text-base font-semibold tracking-[-0.02em] text-fg"> <div class="font-display text-base font-semibold tracking-[-0.02em] text-fg">
{genre.label} {genre.label}
+38 -40
View File
@@ -8,11 +8,13 @@
import EmptyState from '$lib/components/ui/EmptyState.svelte'; import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
import ArtistAvatar from '$lib/components/ui/ArtistAvatar.svelte'; import ArtistAvatar from '$lib/components/ui/ArtistAvatar.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
let { data, form } = $props(); let { data, form } = $props();
const profile = $derived(data.profile); const profile = $derived(data.profile);
const playlists = $derived(data.playlists); const playlists = $derived(data.playlists);
const canFollow = $derived(!data.isOwnProfile && !data.isAnonymous);
let following = $state(untrack(() => data.isFollowing)); let following = $state(untrack(() => data.isFollowing));
let submitting = $state(false); let submitting = $state(false);
@@ -28,47 +30,43 @@
<meta name="description" content="Perfil de {profile.name} en Musify." /> <meta name="description" content="Perfil de {profile.name} en Musify." />
</svelte:head> </svelte:head>
<Page> {#snippet followAction()}
<div class="flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6"> <form
<ArtistAvatar name={profile.name} imageUrl={profile.profilePictureUrl} size={144} /> method="POST"
<div class="min-w-0 flex-1"> action={following ? '?/unfollow' : '?/follow'}
<p class="text-xs font-semibold tracking-[0.16em] text-fg-2 uppercase">Perfil</p> use:enhance={() => {
<h1 submitting = true;
class="mt-3 font-display text-3xl font-semibold tracking-[-0.035em] break-words text-fg sm:text-4xl" return async ({ update }) => {
> await update();
{profile.name} submitting = false;
</h1> };
<p class="mt-3.25 text-sm text-fg-2"> }}
{profile.followersCount} >
{profile.followersCount === 1 ? 'seguidor' : 'seguidores'} · {profile.followingCount} siguiendo <Button
</p> type="submit"
</div> size="sm"
</div> variant={following ? 'secondary' : 'primary'}
disabled={submitting}
>
{following ? 'Dejar de seguir' : 'Seguir'}
</Button>
</form>
{/snippet}
{#if !data.isOwnProfile && !data.isAnonymous} <Page>
<div class="mt-6 flex items-center gap-2.5 sm:mt-7"> <CollectionHeader
<form eyebrow="Perfil"
method="POST" title={profile.name}
action={following ? '?/unfollow' : '?/follow'} meta="{profile.followersCount} {profile.followersCount === 1
use:enhance={() => { ? 'seguidor'
submitting = true; : 'seguidores'} · {profile.followingCount} siguiendo"
return async ({ update }) => { align="center"
await update(); actions={canFollow ? followAction : undefined}
submitting = false; >
}; {#snippet cover()}
}} <ArtistAvatar name={profile.name} imageUrl={profile.profilePictureUrl} size={144} />
> {/snippet}
<Button </CollectionHeader>
type="submit"
size="sm"
variant={following ? 'secondary' : 'primary'}
disabled={submitting}
>
{following ? 'Dejar de seguir' : 'Seguir'}
</Button>
</form>
</div>
{/if}
<div class="mt-9"> <div class="mt-9">
<h2 class="font-display text-lg font-semibold tracking-[-0.02em] text-fg"> <h2 class="font-display text-lg font-semibold tracking-[-0.02em] text-fg">