You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/src/routes/musica/+page.svelte

256 lines
6.6 KiB

<script lang="ts">
import Meta from '$lib/components/Meta.svelte';
import RejillaCanciones from '$lib/components/RejillaCanciones.svelte';
import BloqueDeEstilo from '$lib/components/BloqueDeEstilo.svelte';
import TablaCanciones from '$lib/components/TablaCanciones.svelte';
import type { Cancion } from '$lib/types';
let { data } = $props();
/** Vista elegida. Las tarjetas son para descubrir; la tabla, para comparar. */
let vista = $state<'tarjetas' | 'tabla'>('tarjetas');
let consulta = $state('');
/**
* Todos los temas, en plano. El estilo y el disco de cada uno los resuelve la
* tabla contra el catálogo; aquí solo hacen falta para buscar, y para eso se
* arma aparte un índice de texto.
*/
const filas = $derived(
data.bloques.flatMap((bloque) =>
bloque.albumes.flatMap((album) =>
album.canciones.map((cancion) => ({
cancion,
/* Todo lo que identifica un tema, junto y sin tildes: quien
recuerda «el dueto de baladas» o «desamor» tiene que llegar. */
buscable: llano(
[
cancion.titulo,
bloque.estilo.nombre,
album.titulo,
String(cancion.ano ?? ''),
...cancion.etiquetas,
...cancion.generos.map((g) => g.nombre),
...cancion.interpretes.map((i) => i.nombre)
].join(' ')
)
}))
)
)
);
/** Sin tildes y en minúsculas: quien escribe «cancion» busca «canción». */
function llano(texto: string): string {
return texto
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.toLowerCase();
}
const termino = $derived(llano(consulta.trim()));
const filtradas = $derived<Cancion[]>(
(termino ? filas.filter((f) => f.buscable.includes(termino)) : filas).map((f) => f.cancion)
);
const buscando = $derived(termino.length > 0);
</script>
<Meta
titulo="Música"
descripcion="La obra completa por estilos: baladas románticas, clásica, rock y más."
/>
<header class="intro">
<h1>Música</h1>
<p class="entradilla">
Todo por estilos. De cada uno se ven sus discos y sus temas destacados; el reproductor de abajo
mantiene la escucha mientras navegas.
</p>
</header>
<!--
Una sola pieza y no dos cajas con un hueco entre ellas: buscar y elegir cómo
se ve son el mismo gesto —acotar lo que hay en pantalla— y presentarlos
separados los hacía parecer dos controles sin relación. Por eso el borde es de
la barra y dentro no hay más línea que la que separa el campo de los botones.
-->
<div class="barra">
<label class="visually-hidden" for="buscar-tema">Buscar un tema</label>
<input
id="buscar-tema"
type="search"
bind:value={consulta}
placeholder="Buscar por título, estilo, disco, año o etiqueta"
autocomplete="off"
/>
<div class="vistas" role="group" aria-label="Cómo ver la música">
<button
type="button"
class="vista"
class:oro={vista === 'tarjetas'}
aria-pressed={vista === 'tarjetas'}
onclick={() => (vista = 'tarjetas')}
>
Tarjetas
</button>
<button
type="button"
class="vista"
class:oro={vista === 'tabla'}
aria-pressed={vista === 'tabla'}
onclick={() => (vista = 'tabla')}
>
Tabla
</button>
</div>
</div>
<p class="resultado muted" role="status">
{#if buscando}
{filtradas.length}
{filtradas.length === 1 ? 'tema' : 'temas'} para «{consulta.trim()}»
{:else}
{filas.length} temas en total
{/if}
</p>
{#if vista === 'tabla'}
<TablaCanciones canciones={filtradas} ordenable />
{:else if buscando}
<!--
Con una búsqueda en marcha, agrupar por estilo no dice nada: lo que
importa son las coincidencias, vengan del estilo que vengan.
-->
<RejillaCanciones canciones={filtradas} vacio="Ningún tema coincide." />
{:else}
{#each data.bloques as bloque (bloque.estilo.slug)}
<BloqueDeEstilo {bloque} />
{:else}
<p class="muted">Todavía no hay música publicada.</p>
{/each}
{/if}
<style>
/*
* Dos columnas: los estilos siempre a la vista y el contenido al lado. En
* pantalla estrecha los estilos suben arriba en horizontal, porque son la
* navegación de la sección y esconderlos deja la página sin salidas.
*/
.intro {
padding-block: var(--sp-7) var(--sp-4);
}
.entradilla {
max-width: 56ch;
font-size: var(--fs-lg);
}
.barra {
display: flex;
align-items: stretch;
border: 1px solid var(--c-border);
border-radius: var(--radius-boton);
background: var(--c-bg-sunken);
/* Para que el oro del botón activo no se salga por la esquina. */
overflow: hidden;
}
/*
* El foco lo acusa la pieza entera y no el campo: dentro ya no hay cajas
* sueltas que puedan llevar su propio cerco.
*/
.barra:focus-within {
border-color: var(--c-accent);
box-shadow: 0 0 0 2px color-mix(in oklab, var(--c-accent) 30%, transparent);
}
input[type='search'] {
flex: 1 1 12rem;
min-width: 0;
padding: var(--boton-relleno);
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: var(--boton-texto);
}
input[type='search']:focus-visible {
outline: none;
}
.vistas {
display: flex;
/* La única línea de dentro: la que separa lo que se escribe de lo que se pulsa. */
border-left: 1px solid var(--c-border);
}
.vista {
padding: var(--boton-relleno);
border: 0;
background-color: transparent;
font: inherit;
font-size: var(--boton-texto);
white-space: nowrap;
cursor: pointer;
transition:
background-color 0.2s var(--suave),
color 0.2s var(--suave);
}
/*
* El color del inactivo va con `:not(.oro)` y no a secas: Svelte añade su
* clase de ámbito a todo lo que escribe aquí, así que un `.vista` de este
* archivo le gana por especificidad al `.oro` global y el botón activo se
* quedaba con la letra gris encima del oro.
*/
.vista:not(.oro) {
color: var(--c-text-muted);
}
.vista + .vista {
border-left: 1px solid var(--c-border);
}
.vista:hover:not(.oro) {
background-color: var(--c-bg-raised);
color: var(--c-text);
}
/*
* El activo lleva `.oro`, que es el mismo dorado de los botones —degradado,
* filo de luz y el barrido al pasar el puntero— y no un color plano. Estaba
* al lado de un botón dorado de verdad y se notaba que no era el mismo.
*/
/* Apilada, la línea que separa deja de ser vertical. */
@media (max-width: 30rem) {
.barra {
flex-direction: column;
}
/*
* En columna, `flex-basis` mide el alto: las doce rem que dan ancho al
* campo en horizontal lo convertían en un cajón de casi doscientos
* píxeles de alto.
*/
input[type='search'] {
flex: 0 0 auto;
}
.vistas {
border-left: 0;
border-top: 1px solid var(--c-border);
}
.vista {
flex: 1;
}
}
.resultado {
padding-block: var(--sp-3);
font-size: var(--fs-sm);
}
</style>

Powered by TurnKey Linux.