web-player: P10 — cabeceras, diálogos y tipografía
PageHeader fusiona CollectionHeader (title/eyebrow/description/meta/
cover?/actions?/align?): sin cover se comporta como el antiguo
PageHeader simple (acciones en línea), con cover como el antiguo
CollectionHeader (acciones debajo). Fija su propio margen inferior
(mb-7/8); se quitan los mt-* que ponía cada página después. Migra los
9 sitios que usaban uno u otro (álbumes, biblioteca, listas, subir,
Explorar ×2, me gusta, mezcla, perfil). CollectionHeader se borra.
AlbumHeader y PlaylistHeader se inlinean en sus páginas y se borran.
Decisión (recomendada, sin bloquear): PlaylistHeader gana iconos en
Editar/Eliminar/Reproducir, igual que ya tenía AlbumHeader.
ConfirmDialog nuevo sustituye los dos diálogos de borrado (álbum y
playlist), que tenían tipografías distintas. Modal pasa a usar
.glass-panel (en vez de duplicar el style= inline) y z-(--z-modal); su
eyebrow y título usan las recetas text-eyebrow/text-display-3.
SectionHeading gana `count?` (línea "N resultados", sustituye 4 copias
en Explorar) y `href?`/`linkLabel?` ("Ver todas", sustituye la de
Inicio). Aplica recetas de tipografía (text-display-2/3,
text-eyebrow, text-body) y tokens de ancho (max-w-prose-sm, max-w-hero)
en EmptyState, +error, "Sin resultados", hero y stats de Inicio, Queue
y Sidebar. El estado vacío "Todavía no has escuchado nada" (repetido
2 veces en Inicio) pasa a un snippet local.
Verificado con npm run check/build; sin QA visual autenticada (Docker
no disponible en esta sesión).
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<Page>
|
||||
<PageHeader title="Álbumes" subtitle="Agrupa la música que subes en discos.">
|
||||
<PageHeader title="Álbumes" description="Agrupa la música que subes en discos.">
|
||||
{#snippet actions()}
|
||||
{#if items.length > 0}
|
||||
<Button onclick={() => (creating = true)}>
|
||||
@@ -35,7 +35,7 @@
|
||||
</PageHeader>
|
||||
|
||||
{#if items.length > 0}
|
||||
<div class="mt-7 grid-cards sm:mt-8">
|
||||
<div class="grid-cards">
|
||||
{#each items as album, i (album.id)}
|
||||
<MediaCard
|
||||
href="/albums/{album.id}"
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Play from '@lucide/svelte/icons/play';
|
||||
import Pause from '@lucide/svelte/icons/pause';
|
||||
import SquarePencil from '@lucide/svelte/icons/square-pen';
|
||||
import Trash from '@lucide/svelte/icons/trash';
|
||||
import Page from '$lib/components/ui/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||
import Modal from '$lib/components/ui/Modal.svelte';
|
||||
import ConfirmDialog from '$lib/components/ui/ConfirmDialog.svelte';
|
||||
import Alert from '$lib/components/ui/Alert.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import AlbumForm from '$lib/components/ui/AlbumForm.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import BackLink from '$lib/components/ui/BackLink.svelte';
|
||||
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
||||
import TrackList from '$lib/components/ui/TrackList.svelte';
|
||||
import AlbumHeader from './components/AlbumHeader.svelte';
|
||||
import { albumMeta } from '$lib/format';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
@@ -42,21 +50,54 @@
|
||||
</svelte:head>
|
||||
|
||||
<Page>
|
||||
<AlbumHeader
|
||||
title={album.title}
|
||||
description={album.description ?? undefined}
|
||||
releaseYear={album.releaseYear === null ? undefined : Number(album.releaseYear)}
|
||||
trackIds={tracks.map((track) => track.id)}
|
||||
coverUrl="/api/albums/{album.id}/cover?size=large"
|
||||
playing={isCurrentQueue && player.playing}
|
||||
{isOwner}
|
||||
onPlayAll={playAll}
|
||||
onEdit={() => (editing = true)}
|
||||
onDelete={() => (confirmingDelete = true)}
|
||||
<BackLink
|
||||
href={isOwner ? '/albums' : '/'}
|
||||
label={isOwner ? 'Volver a tus álbumes' : 'Volver al inicio'}
|
||||
/>
|
||||
|
||||
<PageHeader
|
||||
eyebrow="Álbum"
|
||||
title={album.title}
|
||||
description={album.description ?? undefined}
|
||||
meta={albumMeta(
|
||||
album.releaseYear === null ? undefined : Number(album.releaseYear),
|
||||
tracks.length
|
||||
)}
|
||||
>
|
||||
{#snippet cover()}
|
||||
<Artwork
|
||||
src="/api/albums/{album.id}/cover?size=large"
|
||||
trackIds={tracks.map((track) => track.id)}
|
||||
size="hero"
|
||||
alt={album.title}
|
||||
class="shrink-0"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
||||
{#if isCurrentQueue && player.playing}
|
||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
||||
Pausar
|
||||
{:else}
|
||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
||||
Reproducir
|
||||
{/if}
|
||||
</Button>
|
||||
{#if isOwner}
|
||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>
|
||||
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
|
||||
Editar
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
||||
<Trash class="h-4 w-4" strokeWidth={1.5} />
|
||||
Eliminar
|
||||
</Button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
|
||||
{#if form?.message}
|
||||
<Alert tone="danger" class="mt-6">{form.message}</Alert>
|
||||
<Alert tone="danger" class="mb-6">{form.message}</Alert>
|
||||
{/if}
|
||||
|
||||
{#if tracks.length > 0}
|
||||
@@ -66,7 +107,6 @@
|
||||
rowAction={isOwner
|
||||
? { action: '?/removeTrack', icon: X, label: 'Quitar del álbum' }
|
||||
: undefined}
|
||||
class="mt-6 sm:mt-8"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -98,17 +138,11 @@
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal open={confirmingDelete} onClose={() => (confirmingDelete = false)} maxWidth="max-w-sm">
|
||||
<h2 class="text-lg font-semibold tracking-tight text-fg">
|
||||
¿Eliminar «{album.title}»?
|
||||
</h2>
|
||||
<p class="mt-2.5 text-sm leading-[1.65] text-fg-2">
|
||||
Esta acción no se puede deshacer. Las canciones que contiene seguirán en tu biblioteca.
|
||||
</p>
|
||||
<div class="mt-6 flex items-center justify-end gap-3">
|
||||
<Button variant="secondary" onclick={() => (confirmingDelete = false)}>Cancelar</Button>
|
||||
<form method="POST" action="?/delete">
|
||||
<Button type="submit" variant="danger">Eliminar</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmDialog
|
||||
open={confirmingDelete}
|
||||
onClose={() => (confirmingDelete = false)}
|
||||
title="¿Eliminar «{album.title}»?"
|
||||
description="Esta acción no se puede deshacer. Las canciones que contiene seguirán en tu biblioteca."
|
||||
confirmLabel="Eliminar"
|
||||
action="?/delete"
|
||||
/>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Play from '@lucide/svelte/icons/play';
|
||||
import Pause from '@lucide/svelte/icons/pause';
|
||||
import SquarePencil from '@lucide/svelte/icons/square-pen';
|
||||
import Trash from '@lucide/svelte/icons/trash';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import BackLink from '$lib/components/ui/BackLink.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||
import { plural } from '$lib/format';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
releaseYear?: number;
|
||||
trackIds: (string | number)[];
|
||||
coverUrl?: string;
|
||||
playing: boolean;
|
||||
isOwner: boolean;
|
||||
onPlayAll: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
releaseYear,
|
||||
trackIds,
|
||||
coverUrl,
|
||||
playing,
|
||||
isOwner,
|
||||
onPlayAll,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: Props = $props();
|
||||
|
||||
const meta = $derived(
|
||||
[releaseYear ? String(releaseYear) : undefined, plural(trackIds.length, 'canción', 'canciones')]
|
||||
.filter((part) => part !== undefined)
|
||||
.join(' · ')
|
||||
);
|
||||
</script>
|
||||
|
||||
<BackLink
|
||||
href={isOwner ? '/albums' : '/'}
|
||||
label={isOwner ? 'Volver a tus álbumes' : 'Volver al inicio'}
|
||||
/>
|
||||
|
||||
<CollectionHeader eyebrow="Álbum" {title} {description} {meta}>
|
||||
{#snippet cover()}
|
||||
<Artwork src={coverUrl} {trackIds} size="hero" alt={title} class="shrink-0" />
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
|
||||
{#if playing}
|
||||
<Pause class="h-4 w-4" strokeWidth={1.5} />
|
||||
Pausar
|
||||
{:else}
|
||||
<Play class="h-4 w-4" strokeWidth={1.5} />
|
||||
Reproducir
|
||||
{/if}
|
||||
</Button>
|
||||
{#if isOwner}
|
||||
<Button size="sm" variant="secondary" onclick={onEdit}>
|
||||
<SquarePencil class="h-4 w-4" strokeWidth={1.5} />
|
||||
Editar
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onclick={onDelete}>
|
||||
<Trash class="h-4 w-4" strokeWidth={1.5} />
|
||||
Eliminar
|
||||
</Button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</CollectionHeader>
|
||||
Reference in New Issue
Block a user