Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
15 changed files with 155 additions and 185 deletions
Showing only changes of commit 5fa901c2e6 - Show all commits
+1 -1
View File
@@ -784,7 +784,7 @@ Tareas:
| P9 | ✅ | (pendiente) | MediaCard gana hover-zoom del arte y scrim+PlayButton condicionados a `onPlay` (antes exclusivos de MixTile); ahora se aplicarían a cualquier card futura con onPlay, consistencia intencional. Sin QA visual autenticada (Docker no disponible) | | P9 | ✅ | (pendiente) | MediaCard gana hover-zoom del arte y scrim+PlayButton condicionados a `onPlay` (antes exclusivos de MixTile); ahora se aplicarían a cualquier card futura con onPlay, consistencia intencional. Sin QA visual autenticada (Docker no disponible) |
| P10 | ✅ | (pendiente) | Decisión tomada (recomendada, sin bloquear): iconos en los botones de cabecera de playlist (antes solo álbum tenía). PlaylistHeader pierde su prop `id` sin usar tras inlinearlo. Explorar: la cabecera de "Resultados para…" ahora usa el `mb-7/8` de PageHeader en vez de un `mt-5` ajustado a mano antes de los chips de filtro — gap ligeramente mayor entre el resumen y los chips (Δ menor, documentado). Sin QA visual autenticada (Docker no disponible) | | P10 | ✅ | (pendiente) | Decisión tomada (recomendada, sin bloquear): iconos en los botones de cabecera de playlist (antes solo álbum tenía). PlaylistHeader pierde su prop `id` sin usar tras inlinearlo. Explorar: la cabecera de "Resultados para…" ahora usa el `mb-7/8` de PageHeader en vez de un `mt-5` ajustado a mano antes de los chips de filtro — gap ligeramente mayor entre el resumen y los chips (Δ menor, documentado). 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) | | 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 | ⬜ | | | | 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 | ⬜ | | | | P13 | ⬜ | | |
| P14 | ⬜ | | | | P14 | ⬜ | | |
@@ -7,6 +7,7 @@
import AccountMenu from './ui/AccountMenu.svelte'; import AccountMenu from './ui/AccountMenu.svelte';
import Avatar from './ui/Avatar.svelte'; import Avatar from './ui/Avatar.svelte';
import IconButton from './ui/IconButton.svelte'; import IconButton from './ui/IconButton.svelte';
import Logo from './ui/Logo.svelte';
interface Props { interface Props {
user: SessionUser | null; user: SessionUser | null;
@@ -17,14 +18,10 @@
</script> </script>
<header <header
class="sticky top-0 z-40 flex h-14 items-center justify-between border-b border-hairline bg-bg/85 page-x backdrop-blur-md sm:hidden" class="sticky top-0 z-(--z-sticky) flex h-14 items-center justify-between border-b border-hairline bg-bg/85 page-x backdrop-blur-md sm:hidden"
> >
<a <a href="/">
href="/" <Logo size="sm" />
class="flex items-center gap-2 font-display text-lg font-semibold tracking-[-0.02em] text-fg"
>
<span class="h-5.5 w-5.5 rounded-lg bg-[image:var(--mf-logo-grad)]"></span>
Musify
</a> </a>
{#if user} {#if user}
+7 -23
View File
@@ -1,18 +1,12 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { isSectionActive } from '$lib/navigation.svelte'; import { isSectionActive, appNavLinks } from '$lib/navigation.svelte';
import { liked } from '$lib/player/liked.svelte'; import { liked } from '$lib/player/liked.svelte';
import { createPlaylistModal } from '$lib/playlists.svelte'; import { createPlaylistModal } from '$lib/playlists.svelte';
import type { SessionUser } from '$lib/types'; import type { SessionUser } from '$lib/types';
import ListRow from './ui/ListRow.svelte'; import ListRow from './ui/ListRow.svelte';
import IconButton from './ui/IconButton.svelte'; import IconButton from './ui/IconButton.svelte';
import Home from '@lucide/svelte/icons/house'; import Logo from './ui/Logo.svelte';
import Compass from '@lucide/svelte/icons/compass';
import ListMusic from '@lucide/svelte/icons/list-music';
import Heart from '@lucide/svelte/icons/heart';
import Disc from '@lucide/svelte/icons/disc-2';
import Folder from '@lucide/svelte/icons/folder';
import Upload from '@lucide/svelte/icons/upload';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
interface SidebarPlaylist { interface SidebarPlaylist {
@@ -29,19 +23,7 @@
let { user, playlists }: Props = $props(); let { user, playlists }: Props = $props();
const navLinks = $derived( const navLinks = $derived(appNavLinks(user, { playlists: playlists.length, liked: liked.count }));
user
? [
{ href: '/', label: 'Inicio', icon: Home },
{ href: '/explore', label: 'Descubrir', icon: Compass },
{ href: '/playlists', label: 'Playlists', icon: ListMusic, count: playlists.length },
{ href: '/liked', label: 'Me gusta', icon: Heart, count: liked.count },
{ href: '/albums', label: 'Mis álbumes', icon: Disc },
{ href: '/library', label: 'Canciones subidas', icon: Folder },
{ href: '/upload', label: 'Subir música', icon: Upload }
]
: [{ href: '/explore', label: 'Descubrir', icon: Compass }]
);
function isActive(href: string) { function isActive(href: string) {
return isSectionActive(href, page.url.pathname, page.data.section); return isSectionActive(href, page.url.pathname, page.data.section);
@@ -49,10 +31,12 @@
</script> </script>
<aside <aside
class="hidden shrink-0 flex-col border-r border-hairline bg-bg p-5 transition-[background] duration-500 ease-out lg:flex" class="theme-transition hidden shrink-0 flex-col border-r border-hairline bg-bg p-5 lg:flex"
style="width:var(--mf-sidebar-w)" style="width:var(--mf-sidebar-w)"
> >
<a href="/" class="px-3 pb-3 font-display text-lg font-semibold tracking-tight text-fg">Musify</a> <a href="/" class="px-3 pb-3">
<Logo size="sm" />
</a>
<nav class="flex flex-col gap-1"> <nav class="flex flex-col gap-1">
{#each navLinks as link (link.href)} {#each navLinks as link (link.href)}
+3 -16
View File
@@ -1,11 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { isSectionActive } from '$lib/navigation.svelte'; import { isSectionActive, appNavLinks } from '$lib/navigation.svelte';
import type { SessionUser } from '$lib/types'; import type { SessionUser } from '$lib/types';
import Home from '@lucide/svelte/icons/house';
import Compass from '@lucide/svelte/icons/compass';
import ListMusic from '@lucide/svelte/icons/list-music';
import Heart from '@lucide/svelte/icons/heart';
interface Props { interface Props {
user: SessionUser | null; user: SessionUser | null;
@@ -14,16 +10,7 @@
let { user, class: klass = '' }: Props = $props(); let { user, class: klass = '' }: Props = $props();
const links = $derived( const links = $derived(appNavLinks(user).filter((link) => link.primary));
user
? [
{ href: '/', label: 'Inicio', icon: Home },
{ href: '/explore', label: 'Descubrir', icon: Compass },
{ href: '/playlists', label: 'Playlists', icon: ListMusic },
{ href: '/liked', label: 'Me gusta', icon: Heart }
]
: [{ href: '/explore', label: 'Descubrir', icon: Compass }]
);
function isActive(href: string) { function isActive(href: string) {
return isSectionActive(href, page.url.pathname, page.data.section); return isSectionActive(href, page.url.pathname, page.data.section);
@@ -31,7 +18,7 @@
</script> </script>
<nav <nav
class="pointer-events-auto flex items-center justify-around border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-1.75 backdrop-blur-xl transition-[background] duration-500 lg:hidden {klass}" class="pointer-events-auto flex items-center justify-around glass-bar px-4 py-1.75 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)}
@@ -1,12 +1,21 @@
<script lang="ts"> <script lang="ts">
import TrackInfo from './TrackInfo.svelte'; import TrackInfo from './TrackInfo.svelte';
import TransportControls from './TransportControls.svelte'; import TransportControls from './TransportControls.svelte';
import SeekBar from './SeekBar.svelte'; import Slider from '$lib/components/ui/Slider.svelte';
import PlayerExtras from './PlayerExtras.svelte'; import IconButton from '$lib/components/ui/IconButton.svelte';
import { player } from '$lib/player/player.svelte';
import { queuePanel } from '$lib/player/queuePanel.svelte';
import { fmtTime } from '$lib/format';
import Volume2 from '@lucide/svelte/icons/volume-2';
import ListMusic from '@lucide/svelte/icons/list-music';
function seek(value: number) {
if (player.current.duration > 0) player.seekFraction(value / player.current.duration);
}
</script> </script>
<div <div
class="animate-enter pointer-events-auto flex items-center gap-4.5 border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-2.75 backdrop-blur-xl transition-[background] duration-500 sm:hidden" class="animate-enter pointer-events-auto flex items-center gap-4.5 glass-bar px-4 py-2.75 sm:hidden"
> >
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<TrackInfo compact /> <TrackInfo compact />
@@ -15,14 +24,46 @@
</div> </div>
<div <div
class="animate-enter pointer-events-auto hidden items-center gap-5 border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-2.75 backdrop-blur-xl transition-[background] duration-500 sm:flex" class="animate-enter pointer-events-auto hidden items-center gap-5 glass-bar px-4 py-2.75 sm:flex"
> >
<div class="w-64 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 />
<SeekBar /> <div class="flex w-full max-w-hero items-center gap-2.75">
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
{fmtTime(player.progress)}
</span>
<Slider
value={player.progress}
max={player.current.duration}
label="Barra de progreso"
oninput={seek}
variant="seek"
/>
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
{fmtTime(player.current.duration)}
</span>
</div>
</div>
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
<Volume2 class="size-icon-sm shrink-0 text-fg-3" strokeWidth={1.8} />
<Slider
value={player.volume}
max={100}
label="Volumen"
oninput={(value) => player.setVolume(value)}
variant="volume"
/>
<IconButton
label="Cola de reproducción"
tone="plain"
size="xs"
pressed={queuePanel.open}
onclick={() => queuePanel.toggle()}
>
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
</IconButton>
</div> </div>
<PlayerExtras />
</div> </div>
@@ -15,7 +15,7 @@
</script> </script>
<div <div
class="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex flex-col lg:left-[var(--mf-sidebar-w)]" class="pointer-events-none fixed inset-x-0 bottom-0 z-(--z-sticky) flex flex-col lg:left-(--mf-sidebar-w)"
> >
{#if player.error} {#if player.error}
<div class="pointer-events-auto px-4 pb-2.5"> <div class="pointer-events-auto px-4 pb-2.5">
@@ -1,19 +0,0 @@
<script lang="ts">
import VolumeControl from './VolumeControl.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
import { queuePanel } from '$lib/player/queuePanel.svelte';
import ListMusic from '@lucide/svelte/icons/list-music';
</script>
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
<VolumeControl />
<IconButton
label="Cola de reproducción"
tone="plain"
size="xs"
pressed={queuePanel.open}
onclick={() => queuePanel.toggle()}
>
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
</IconButton>
</div>
@@ -18,7 +18,7 @@
{#if queuePanel.open} {#if queuePanel.open}
<aside <aside
transition:fly={{ x: 24, duration: 220, easing: cubicOut }} transition:fly={{ x: 24, duration: 220, easing: cubicOut }}
class="hidden shrink-0 flex-col border-l border-hairline bg-bg px-4 pt-5 transition-[background] duration-500 sm:flex" class="hidden shrink-0 flex-col border-l border-hairline bg-bg px-4 pt-5 theme-transition sm:flex"
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)" style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
> >
<div class="mb-5 flex items-center justify-between"> <div class="mb-5 flex items-center justify-between">
@@ -1,48 +0,0 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
interface Props {
compact?: boolean;
}
let { compact = false }: Props = $props();
function seek(event: MouseEvent) {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
player.seekFraction((event.clientX - rect.left) / rect.width);
}
</script>
{#if compact}
<button
type="button"
onclick={seek}
aria-label="Barra de progreso"
class="relative block h-1 w-full cursor-pointer bg-track"
>
<div
class="absolute inset-y-0 left-0 bg-accent/70"
style="width:{player.progressPercent}%"
></div>
</button>
{:else}
<div class="flex w-full max-w-hero items-center gap-2.75">
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
{fmtTime(player.progress)}
</span>
<button
type="button"
onclick={seek}
aria-label="Barra de progreso"
class="group relative h-3.5 flex-1 cursor-pointer"
>
<div class="h-[3.5px] w-full overflow-hidden rounded-full bg-track">
<div class="h-full rounded-full bg-accent" style="width:{player.progressPercent}%"></div>
</div>
</button>
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
{fmtTime(player.current.duration)}
</span>
</div>
{/if}
@@ -1,52 +0,0 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import Volume2 from '@lucide/svelte/icons/volume-2';
function handleVolumeChange(event: Event) {
const target = event.currentTarget as HTMLInputElement;
player.setVolume(Number(target.value));
}
</script>
<div class="flex items-center gap-2.25">
<Volume2 class="h-3.75 w-3.75 shrink-0 text-fg-3" strokeWidth={1.8} />
<input
type="range"
min="0"
max="100"
value={player.volume}
oninput={handleVolumeChange}
aria-label="Volumen"
class="mf-volume"
style="background:linear-gradient(to right, var(--mf-text-2) {player.volume}%, var(--mf-track) {player.volume}%)"
/>
</div>
<style>
.mf-volume {
-webkit-appearance: none;
appearance: none;
width: 84px;
height: 3.5px;
border-radius: 100px;
outline: none;
cursor: pointer;
}
.mf-volume::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--mf-text);
cursor: pointer;
}
.mf-volume::-moz-range-thumb {
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--mf-text);
border: none;
cursor: pointer;
}
</style>
@@ -0,0 +1,22 @@
<script lang="ts">
interface Props {
size?: 'sm' | 'lg';
}
let { size = 'sm' }: Props = $props();
const MARK: Record<'sm' | 'lg', string> = {
sm: 'size-5.5 rounded-lg',
lg: 'size-8 rounded-control'
};
const TEXT: Record<'sm' | 'lg', string> = {
sm: 'font-display text-lg font-semibold tracking-tight text-fg',
lg: 'font-display text-[clamp(1.5rem,3vw,2.5rem)] font-semibold tracking-tight'
};
</script>
<span class="flex items-center gap-3">
<span class="{MARK[size]} bg-[image:var(--mf-logo-grad)]"></span>
<span class={TEXT[size]}>Musify</span>
</span>
@@ -23,7 +23,7 @@
<button <button
type="button" type="button"
onclick={() => (value = option.value)} onclick={() => (value = option.value)}
class="relative z-10 flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value === class="relative z-(--z-raised) flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value ===
option.value option.value
? 'text-ink' ? 'text-ink'
: 'text-muted hover:text-fg-2'}" : 'text-muted hover:text-fg-2'}"
@@ -0,0 +1,62 @@
<script lang="ts">
interface Props {
value: number;
max: number;
label: string;
oninput: (value: number) => void;
variant: 'seek' | 'volume';
}
let { value, max, label, oninput, variant }: Props = $props();
const percent = $derived(max > 0 ? (value / max) * 100 : 0);
function handleInput(event: Event) {
oninput(Number((event.currentTarget as HTMLInputElement).value));
}
</script>
<input
type="range"
min="0"
{max}
{value}
oninput={handleInput}
aria-label={label}
class="mf-slider mf-slider-{variant}"
style="background:linear-gradient(to right, var(--mf-text-2) {percent}%, var(--mf-track) {percent}%)"
/>
<style>
.mf-slider {
-webkit-appearance: none;
appearance: none;
height: var(--mf-range-h);
border-radius: 100px;
outline: none;
cursor: pointer;
}
.mf-slider-seek {
width: 100%;
}
.mf-slider-volume {
width: var(--mf-volume-w);
}
.mf-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: var(--mf-range-thumb);
height: var(--mf-range-thumb);
border-radius: 50%;
background: var(--mf-text);
cursor: pointer;
}
.mf-slider::-moz-range-thumb {
width: var(--mf-range-thumb);
height: var(--mf-range-thumb);
border-radius: 50%;
background: var(--mf-text);
border: none;
cursor: pointer;
}
</style>
+2 -2
View File
@@ -137,7 +137,7 @@
<section class="page-x pt-3 sm:pt-4"> <section class="page-x pt-3 sm:pt-4">
<div <div
class="relative overflow-hidden rounded-3xl bg-[image:var(--mf-hero-bg)] px-6 py-8 transition-[background] duration-500 sm:px-9 sm:py-8.5" class="relative overflow-hidden rounded-3xl bg-[image:var(--mf-hero-bg)] 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"
@@ -220,7 +220,7 @@
{#if spotlight} {#if spotlight}
<section> <section>
<div <div
class="grid grid-cols-1 overflow-hidden rounded-panel-lg transition-[background] duration-500 lg:grid-cols-2" class="grid grid-cols-1 overflow-hidden rounded-panel-lg theme-transition lg:grid-cols-2"
style="background-image:var(--mf-spotlight-bg), linear-gradient(to right, transparent 50%, color-mix(in oklch, var(--mf-scrim) 22%, transparent) 61%, transparent 92.5%)" style="background-image:var(--mf-spotlight-bg), linear-gradient(to right, transparent 50%, color-mix(in oklch, var(--mf-scrim) 22%, transparent) 61%, transparent 92.5%)"
> >
<div class="flex flex-col justify-center p-8 sm:p-9"> <div class="flex flex-col justify-center p-8 sm:p-9">
+3 -7
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Button from '$lib/components/ui/Button.svelte'; import Button from '$lib/components/ui/Button.svelte';
import Logo from '$lib/components/ui/Logo.svelte';
let { data } = $props(); let { data } = $props();
@@ -15,12 +16,7 @@
<div class="pointer-events-none absolute inset-0" style="background:var(--mf-ambient)"></div> <div class="pointer-events-none absolute inset-0" style="background:var(--mf-ambient)"></div>
<div class="relative flex w-full max-w-4xl flex-col items-center gap-12 text-center"> <div class="relative flex w-full max-w-4xl flex-col items-center gap-12 text-center">
<div class="flex items-center gap-3"> <Logo size="lg" />
<span class="h-8 w-8 rounded-xl bg-[image:var(--mf-logo-grad)]"></span>
<span class="font-display text-[clamp(1.5rem,3vw,2.5rem)] font-semibold tracking-tight">
Musify
</span>
</div>
<div class="animate-enter flex flex-col items-center gap-5"> <div class="animate-enter flex flex-col items-center gap-5">
<h1 <h1
@@ -32,7 +28,7 @@
<p class="text-lg text-fg-2">Inicia sesión para continuar.</p> <p class="text-lg text-fg-2">Inicia sesión para continuar.</p>
</div> </div>
<div class="flex w-full max-w-80 flex-col gap-3"> <div class="flex w-full max-w-cta flex-col gap-3">
<Button href={`/auth/login?${returnParam}`} reload size="lg" class="w-full"> <Button href={`/auth/login?${returnParam}`} reload size="lg" class="w-full">
Iniciar sesión Iniciar sesión
</Button> </Button>