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).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
meta?: string;
|
||||
align?: 'center' | 'end';
|
||||
cover: Snippet;
|
||||
actions?: Snippet;
|
||||
}
|
||||
|
||||
let { eyebrow, title, description, meta, align = 'end', cover, actions }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-5 sm:flex-row sm:gap-6 {align === 'end'
|
||||
? 'mt-3 sm:mt-4 sm:items-end'
|
||||
: 'sm:items-center'}"
|
||||
>
|
||||
{@render cover()}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs font-medium tracking-widest text-fg-2 uppercase">{eyebrow}</p>
|
||||
<h1
|
||||
class="{align === 'end'
|
||||
? 'mt-1.5'
|
||||
: 'mt-3'} font-display text-3xl font-medium tracking-tight wrap-break-word text-fg sm:text-4xl"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{#if description}
|
||||
<p class="mt-3 max-w-2xl text-sm text-fg-2 sm:text-base">{description}</p>
|
||||
{/if}
|
||||
{#if meta}
|
||||
<p class="mt-3 text-sm text-fg-2">{meta}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if actions}
|
||||
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
|
||||
{@render actions()}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import Modal from './Modal.svelte';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
let { open, onClose, title, description, confirmLabel, action }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Modal {open} {onClose} maxWidth="max-w-sm">
|
||||
<h2 class="text-display-3 text-fg">{title}</h2>
|
||||
<p class="mt-2.5 text-body text-fg-2">{description}</p>
|
||||
<div class="mt-6 flex items-center justify-end gap-3">
|
||||
<Button variant="secondary" onclick={onClose}>Cancelar</Button>
|
||||
<form method="POST" {action}>
|
||||
<Button type="submit" variant="danger">{confirmLabel}</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -15,10 +15,10 @@
|
||||
|
||||
<div class="flex flex-col items-center rounded-panel px-6 py-16 text-center {klass}">
|
||||
{#if Icon}
|
||||
<Icon class="h-9 w-9 text-fg-2" strokeWidth={1.25} />
|
||||
<Icon class="size-icon-xl text-fg-2" strokeWidth={1.25} />
|
||||
{/if}
|
||||
{#if title}
|
||||
<h3 class="mt-6 font-display text-xl font-medium tracking-tight text-fg">
|
||||
<h3 class="mt-6 text-display-3 text-fg">
|
||||
{title}
|
||||
</h3>
|
||||
{/if}
|
||||
|
||||
@@ -70,14 +70,13 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="animate-fade fixed inset-0 z-[60] grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
|
||||
class="animate-fade fixed inset-0 z-(--z-modal) grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
|
||||
role="presentation"
|
||||
onclick={onBackdropClick}
|
||||
>
|
||||
<div
|
||||
bind:this={panel}
|
||||
class="animate-pop max-h-[85dvh] w-full overflow-y-auto {maxWidth} rounded-panel p-5 backdrop-blur-xl transition-[background] duration-500 outline-none sm:p-6 {panelClass}"
|
||||
style="background-color:var(--mf-panel-bg); background-image:var(--mf-modal-glow)"
|
||||
class="glass-panel animate-pop max-h-[85dvh] w-full overflow-y-auto {maxWidth} rounded-panel p-5 outline-none sm:p-6 {panelClass}"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
@@ -87,11 +86,11 @@
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
{#if eyebrow}
|
||||
<p class="mb-2.5 text-xs font-medium tracking-widest text-muted uppercase">
|
||||
<p class="mb-2.5 text-eyebrow text-muted">
|
||||
{eyebrow}
|
||||
</p>
|
||||
{/if}
|
||||
<h2 class="font-display text-xl font-medium tracking-[-0.02em] text-fg">
|
||||
<h2 class="text-display-3 text-fg">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -3,24 +3,59 @@
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
class?: string;
|
||||
eyebrow?: string;
|
||||
description?: string;
|
||||
meta?: string;
|
||||
cover?: Snippet;
|
||||
actions?: Snippet;
|
||||
align?: 'center' | 'end';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { title, subtitle, class: klass = '', actions }: Props = $props();
|
||||
let {
|
||||
title,
|
||||
eyebrow,
|
||||
description,
|
||||
meta,
|
||||
cover,
|
||||
actions,
|
||||
align = 'end',
|
||||
class: klass = ''
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}">
|
||||
<div class="min-w-0">
|
||||
<h1 class="font-display text-3xl font-medium tracking-tight text-fg sm:text-4xl">
|
||||
{title}
|
||||
</h1>
|
||||
{#if subtitle}
|
||||
<p class="mt-2 text-sm text-fg-2">{subtitle}</p>
|
||||
<div class="mb-7 sm:mb-8 {klass}">
|
||||
<div
|
||||
class="flex flex-col gap-5 sm:flex-row sm:gap-6 {cover
|
||||
? align === 'end'
|
||||
? 'sm:items-end'
|
||||
: 'sm:items-center'
|
||||
: 'sm:items-end sm:justify-between'}"
|
||||
>
|
||||
{#if cover}
|
||||
{@render cover()}
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
{#if eyebrow}
|
||||
<p class="text-eyebrow text-fg-2">{eyebrow}</p>
|
||||
{/if}
|
||||
<h1 class="text-display-1 wrap-break-word text-fg {eyebrow ? 'mt-1.5' : ''}">
|
||||
{title}
|
||||
</h1>
|
||||
{#if description}
|
||||
<p class="mt-3 max-w-2xl text-body text-fg-2">{description}</p>
|
||||
{/if}
|
||||
{#if meta}
|
||||
<p class="mt-3 text-sm text-fg-2">{meta}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if actions && !cover}
|
||||
<div class="flex shrink-0 items-center gap-3">{@render actions()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if actions}
|
||||
<div class="flex shrink-0 items-center gap-3">{@render actions()}</div>
|
||||
{#if actions && cover}
|
||||
<div class="mt-6 flex flex-wrap items-center gap-2.5 sm:mt-7">
|
||||
{@render actions()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { plural } from '$lib/format';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
count?: number;
|
||||
href?: string;
|
||||
linkLabel?: string;
|
||||
class?: string;
|
||||
actions?: Snippet;
|
||||
}
|
||||
|
||||
let { title, subtitle, class: klass = '', actions }: Props = $props();
|
||||
let {
|
||||
title,
|
||||
subtitle,
|
||||
count,
|
||||
href,
|
||||
linkLabel = 'Ver todas',
|
||||
class: klass = '',
|
||||
actions
|
||||
}: Props = $props();
|
||||
|
||||
const hasActions = $derived(!!actions || count !== undefined || !!href);
|
||||
</script>
|
||||
|
||||
{#snippet heading()}
|
||||
<h2 class="font-display text-lg font-medium tracking-tight text-fg">
|
||||
<h2 class="text-display-4 text-fg">
|
||||
{title}
|
||||
</h2>
|
||||
{#if subtitle}
|
||||
@@ -20,10 +34,31 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if actions}
|
||||
{#snippet autoActions()}
|
||||
{#if count !== undefined}
|
||||
<div class="h-px flex-1 self-center bg-line"></div>
|
||||
<span class="shrink-0 text-xs text-muted tabular-nums">
|
||||
{plural(count, 'resultado', 'resultados')}
|
||||
</span>
|
||||
{/if}
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
class="shrink-0 text-xs font-medium tracking-wider text-fg-3 uppercase transition-colors hover:text-fg"
|
||||
>
|
||||
{linkLabel}
|
||||
</a>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if hasActions}
|
||||
<div class="mb-4 flex items-end justify-between gap-4 sm:mb-4.5 {klass}">
|
||||
<div>{@render heading()}</div>
|
||||
{@render actions()}
|
||||
{#if actions}
|
||||
{@render actions()}
|
||||
{:else}
|
||||
{@render autoActions()}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4 sm:mb-4.5 {klass}">
|
||||
|
||||
Reference in New Issue
Block a user