diff --git a/e2e/musica.e2e.ts b/e2e/musica.e2e.ts index e0eeb96..a295479 100644 --- a/e2e/musica.e2e.ts +++ b/e2e/musica.e2e.ts @@ -89,7 +89,9 @@ test.describe('Página de música', () => { await columna.getByRole('button').click(); await expect(columna).toHaveAttribute('aria-sort', 'descending'); - const duraciones = await page.locator('tbody tr td:nth-child(6)').allInnerTexts(); + // Por su clase y no por su posición: la tabla lleva portada, y el número + // de columna cambia con lo que se enseñe en cada página. + const duraciones = await page.locator('tbody td.duracion').allInnerTexts(); const enSegundos = duraciones.map((d) => { const [m, s] = d.trim().split(':').map(Number); return m * 60 + s; diff --git a/e2e/navegacion.e2e.ts b/e2e/navegacion.e2e.ts index 898e8c9..abe456d 100644 --- a/e2e/navegacion.e2e.ts +++ b/e2e/navegacion.e2e.ts @@ -5,7 +5,7 @@ test.describe('Navegación', () => { await page.goto('/'); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Senza Paura'); - await expect(page.getByText('Letrista y compositor').first()).toBeVisible(); + await expect(page.getByText('Letrista y creador musical').first()).toBeVisible(); // La cabecera destaca un tema y ofrece el catálogo entero. await page.getByRole('link', { name: 'Toda la música' }).click(); diff --git a/e2e/responsive.e2e.ts b/e2e/responsive.e2e.ts index d5de119..cb2c0fa 100644 --- a/e2e/responsive.e2e.ts +++ b/e2e/responsive.e2e.ts @@ -90,9 +90,12 @@ test.describe('Responsive', () => { // Por nombre y no por cuenta: en móvil no se recorta el menú, y así la // prueba dice cuál falta en vez de que el número no cuadra. - for (const seccion of ['Música', 'Taller de letras', 'Géneros', 'Blog', 'Bio', 'Contacto']) { + for (const seccion of ['Música', 'Taller de letras', 'Géneros', 'Blog', 'Bio']) { await expect(nav.getByRole('link', { name: seccion })).toBeVisible(); } + + // Contacto no: va solo en el pie, que es donde se busca escribir. + await expect(nav.getByRole('link', { name: 'Contacto' })).toHaveCount(0); }); test('las barras laterales bajan en vez de desaparecer', async ({ page }) => { diff --git a/e2e/tienda.e2e.ts b/e2e/tienda.e2e.ts index 8db213d..71de243 100644 --- a/e2e/tienda.e2e.ts +++ b/e2e/tienda.e2e.ts @@ -5,15 +5,11 @@ const CANCIONES_POR_PACK = 10; /** Añade un tema desde el listado de un álbum, por su título. */ async function anadir(page: Page, titulo: string) { - // Dentro de la lista de temas, no en cualquier lista: las migas de pan - // también son elementos de lista, y el título del disco se repite en ellas. - const fila = page - .getByRole('list', { name: 'Temas' }) - .getByRole('listitem') - .filter({ hasText: titulo }) - .first(); - await fila.getByRole('button', { name: 'Añadir' }).click(); - await expect(fila.getByRole('button', { name: 'Quitar' })).toBeVisible(); + // Dentro de la tabla de temas, no en cualquier sitio: el título del disco se + // repite en las migas de pan y en la cabecera. + const fila = page.getByRole('row').filter({ hasText: titulo }).first(); + await fila.getByRole('button', { name: 'Añadir', exact: true }).click(); + await expect(fila.getByRole('button', { name: 'Quitar', exact: true })).toBeVisible(); } /** @@ -31,9 +27,7 @@ async function llenarUnPack(page: Page, cuantas = CANCIONES_POR_PACK) { if (puestas >= cuantas) break; await page.goto(`/albumes/${album}`); - const botones = page - .getByRole('list', { name: 'Temas' }) - .getByRole('button', { name: 'Añadir' }); + const botones = page.getByRole('table').getByRole('button', { name: 'Añadir', exact: true }); while (puestas < cuantas && (await botones.count()) > 0) { await botones.first().click(); // El botón cambia a «Quitar», así que la cuenta de «Añadir» baja sola. diff --git a/src/content/paginas/bio.md b/src/content/paginas/bio.md index 0cc8bd8..6869e0e 100644 --- a/src/content/paginas/bio.md +++ b/src/content/paginas/bio.md @@ -1,6 +1,6 @@ --- titulo: 'Biografía' -descripcion: 'Letrista y compositor. Escribe baladas románticas desde hace veinte años, y todo lo demás por curiosidad.' +descripcion: 'Letrista y creador musical. Escribe baladas románticas desde hace veinte años, y todo lo demás por curiosidad.' --- Escribo canciones desde antes de saber que eso era un oficio. diff --git a/src/lib/components/BotonCola.svelte b/src/lib/components/BotonCola.svelte new file mode 100644 index 0000000..c19f1a1 --- /dev/null +++ b/src/lib/components/BotonCola.svelte @@ -0,0 +1,133 @@ + + + + + + + + + {acusado ? `«${cancion.titulo}» añadida a la cola.` : ''} + + + + diff --git a/src/lib/components/Etiquetas.svelte b/src/lib/components/Etiquetas.svelte index 041f127..9440493 100644 --- a/src/lib/components/Etiquetas.svelte +++ b/src/lib/components/Etiquetas.svelte @@ -101,28 +101,14 @@ --velo: oklch(0.45 0.13 var(--tono) / 0.1); } - /* En una fila: sin caja, solo el nombre en su color y un punto delante. */ + /* La misma cápsula, más pequeña: en una fila no debe pesar más que el título. */ .menudas { - gap: var(--sp-3); + gap: var(--sp-2); } .menudas .etiqueta { - padding: 0; - border: 0; + padding: 0.1em 0.55em; font-size: 0.6875rem; - letter-spacing: 0.02em; - } - - .menudas li + li .etiqueta::before { - content: '·'; - margin-right: var(--sp-3); - color: var(--c-text-muted); - } - - .menudas a.etiqueta:hover, - .menudas a.etiqueta:focus-visible { - background: none; - text-decoration: underline; } a.etiqueta:hover, diff --git a/src/lib/components/Icono.svelte b/src/lib/components/Icono.svelte index 557467b..277b2fb 100644 --- a/src/lib/components/Icono.svelte +++ b/src/lib/components/Icono.svelte @@ -31,6 +31,21 @@ }, silencio: { tipo: 'linea', d: ['M11 5 6 9H2v6h4l5 4z', 'M22 9l-6 6', 'M16 9l6 6'] }, cola: { tipo: 'linea', d: ['M4 6h11', 'M4 12h11', 'M4 18h7', 'M17 11l4 3-4 3z'] }, + /* Las mismas tres rayas de la cola, con un «+» en vez de la flecha. */ + 'anadir-cola': { + tipo: 'linea', + d: ['M4 6h11', 'M4 12h11', 'M4 18h7', 'M18 12v8', 'M14 16h8'] + }, + /* Una lista con una estrella: la lista propia, no la de ahora. */ + lista: { + tipo: 'linea', + d: [ + 'M4 6h10', + 'M4 12h10', + 'M4 18h6', + 'M17.5 12.5l1.4 2.9 3.1.4-2.3 2.2.6 3.1-2.8-1.5-2.8 1.5.6-3.1-2.3-2.2 3.1-.4z' + ] + }, cerrar: { tipo: 'linea', d: ['M6 6l12 12', 'M18 6 6 18'] }, menu: { tipo: 'linea', d: ['M4 7h16', 'M4 12h16', 'M4 17h16'] }, externo: { diff --git a/src/lib/components/ListaCanciones.svelte b/src/lib/components/ListaCanciones.svelte deleted file mode 100644 index 99f5a45..0000000 --- a/src/lib/components/ListaCanciones.svelte +++ /dev/null @@ -1,325 +0,0 @@ - - -{#if canciones.length === 0} -

Todavía no hay temas publicados en esta sección.

-{:else} -
    - {#each canciones as cancion, i (cancion.slug)} -
  1. -
    - {#if numerar && cancion.numero !== null} - - {/if} - -
    - - - asomar(e, cancion)} - onmouseleave={() => (vista = null)} - > - {#if cancion.portada} - - {:else} - - {/if} - - -
    - - {cancion.titulo} - - -
    - {#if cancion.instrumental} - Instrumental - {/if} - {#each cancion.generos as g (g.slug)} - {g.nombre} - {/each} - - {#if cancion.generos.length === 0 && cancion.etiquetas.length === 0} - {listaDeNombres(cancion.interpretes)} - {/if} -
    -
    - -
    - - - {cancion.duracion ? segundosADuracion(cancion.duracion) : '--:--'} - -
    -
  2. - {/each} -
- - {#if vista} - - - {/if} -{/if} - - diff --git a/src/lib/components/ListaCanciones.svelte.spec.ts b/src/lib/components/ListaCanciones.svelte.spec.ts deleted file mode 100644 index 51e8d46..0000000 --- a/src/lib/components/ListaCanciones.svelte.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { page } from '@vitest/browser/context'; -import { describe, expect, it } from 'vitest'; -import { render } from 'vitest-browser-svelte'; -import ListaCanciones from './ListaCanciones.svelte'; -import type { Cancion } from '$lib/types'; - -function cancion(parcial: Partial = {}): Cancion { - return { - slug: 'como-si-nada', - titulo: 'Como si nada', - estilo: 'baladas-romanticas', - album: 'como-si-nada', - numero: 1, - audio: 'como-si-nada/01-como-si-nada.wav', - duracion: 252, - ano: 2024, - letra: ['Senza Paura'], - musica: ['Senza Paura'], - interpretes: ['Marta Cifuentes'], - destacada: false, - etiquetas: [], - generos: [], - enlaces: {}, - instrumental: false, - idioma: 'es', - inspiracion: [], - extracto: [], - versiones: [], - alaVenta: true, - registro: { reparto: [] }, - videos: [], - ...parcial - }; -} - -describe('ListaCanciones', () => { - it('pinta el titulo y la duracion de cada tema', async () => { - render(ListaCanciones, { canciones: [cancion()] }); - - await expect.element(page.getByRole('link', { name: 'Como si nada' })).toBeInTheDocument(); - await expect.element(page.getByText('4:12')).toBeInTheDocument(); - }); - - it('enlaza cada tema con su ficha', async () => { - render(ListaCanciones, { canciones: [cancion()] }); - - await expect - .element(page.getByRole('link', { name: 'Como si nada' })) - .toHaveAttribute('href', '/canciones/como-si-nada'); - }); - - it('enseña de qué está hecho el tema, no su letra', async () => { - // El primer verso ya no va aquí: la letra está entera en la ficha, y en - // una lista lo que sirve para elegir es el género y las etiquetas. - render(ListaCanciones, { - canciones: [ - cancion({ - extracto: ['Entras en la casa y no preguntas nada,'], - generos: [{ slug: 'bolero', nombre: 'Bolero' }], - etiquetas: ['Desamor'] - }) - ] - }); - - await expect.element(page.getByRole('link', { name: 'Bolero' })).toBeInTheDocument(); - await expect.element(page.getByRole('link', { name: 'Desamor' })).toBeInTheDocument(); - expect(document.body.textContent).not.toContain('Entras en la casa'); - }); - - it('cae en los intérpretes cuando no hay género ni etiqueta', async () => { - render(ListaCanciones, { - canciones: [cancion({ generos: [], etiquetas: [], interpretes: ['Marta Cifuentes'] })] - }); - - await expect.element(page.getByText('Marta Cifuentes')).toBeInTheDocument(); - }); - - it('marca las piezas instrumentales en vez de acreditar la letra', async () => { - render(ListaCanciones, { - canciones: [cancion({ slug: 'preludio-i', titulo: 'Preludio I', instrumental: true })] - }); - - await expect.element(page.getByText(/Instrumental/)).toBeInTheDocument(); - }); - - it('desactiva el play de los temas sin audio', async () => { - // Se publica la ficha y la letra aunque el audio aun no exista; lo que no - // puede pasar es que el boton prometa algo que no va a sonar. - render(ListaCanciones, { canciones: [cancion({ audio: undefined })] }); - - await expect - .element(page.getByRole('button', { name: 'Reproducir Como si nada' })) - .toBeDisabled(); - }); - - it('muestra la duracion desconocida como guiones', async () => { - render(ListaCanciones, { canciones: [cancion({ duracion: undefined })] }); - - await expect.element(page.getByText('--:--')).toBeInTheDocument(); - }); - - it('avisa cuando no hay temas', async () => { - render(ListaCanciones, { canciones: [] }); - - await expect.element(page.getByText(/no hay temas publicados/i)).toBeInTheDocument(); - }); -}); diff --git a/src/lib/components/TablaCanciones.svelte b/src/lib/components/TablaCanciones.svelte index 0d783fb..da4f3bb 100644 --- a/src/lib/components/TablaCanciones.svelte +++ b/src/lib/components/TablaCanciones.svelte @@ -1,14 +1,6 @@ @@ -114,69 +205,212 @@
- {#each COLUMNAS as columna (columna.campo)} + + {#if numerar} + + {/if} + + + + {#each visibles as columna (columna.campo)} + {/each} + + - {/each} + {:else} + Duración + {/if} + + + + {#each ordenadas as fila, i (fila.cancion.slug)} - + {@const c = fila.cancion} + - - - + {/if} + + + + {#each visibles as columna (columna.campo)} + + {/each} + + - - {/each}
- Todos los temas. Cada encabezado ordena por su columna. + {ordenable + ? 'Los temas. Cada encabezado ordena por su columna.' + : 'Los temas, en el orden del disco.'}
ReproducirPortada + Pista + + {#if ordenable} + + {:else} + Título + {/if} + + {#if ordenable} + + {:else} + {columna.texto} + {/if} + - - FavoritoCola Comprar
- - {fila.cancion.titulo} - - - - {fila.estiloNombre} + + + asomar(e, c)} + onmouseleave={() => (vista = null)} + > + {#if c.portada} + + {:else} + + {/if} - - {fila.albumTitulo} - + + {#if numerar} + {c.numero ?? '—'} + {c.titulo} + +
+ {#if c.instrumental} + Instrumental + {/if} + {#each c.generos as g (g.slug)} + + {g.nombre} + + {/each} + + {#if c.etiquetas.length > CUANTAS_ETIQUETAS} + +{c.etiquetas.length - CUANTAS_ETIQUETAS} + {/if} + {#if c.generos.length === 0 && c.etiquetas.length === 0} + {listaDeNombres(c.interpretes)} + {/if} +
+
+ {#if columna.campo === 'estilo'} + {#if fila.estilo} + + {fila.estilo.nombre} + + {:else} + — + {/if} + {:else if columna.campo === 'album'} + {#if fila.album} + + {fila.album.titulo} + + {:else} + — + {/if} + {:else} + {c.ano ?? '—'} + {/if} + + {c.duracion ? segundosADuracion(c.duracion) : '—'} {fila.cancion.ano ?? '—'} - {fila.cancion.duracion ? segundosADuracion(fila.cancion.duracion) : '—'} + + + - + - +
+ + {#if vista} + + + {/if} {/if} diff --git a/src/lib/navegacion.ts b/src/lib/navegacion.ts index 605d294..3e16ec6 100644 --- a/src/lib/navegacion.ts +++ b/src/lib/navegacion.ts @@ -17,6 +17,7 @@ export const navegacion: { href: ResolvedPathname; texto: string }[] = [ { href: resolve('/taller'), texto: 'Taller de letras' }, { href: resolve('/generos'), texto: 'Géneros' }, { href: resolve('/blog'), texto: 'Blog' }, - { href: resolve('/bio'), texto: 'Bio' }, - { href: resolve('/contacto'), texto: 'Contacto' } + { href: resolve('/bio'), texto: 'Bio' } + // Contacto no está aquí: va solo en el pie. La barra es para lo que se viene + // a ver, y escribir no es de eso; en el pie es donde se busca. ]; diff --git a/src/lib/player/player.svelte.ts b/src/lib/player/player.svelte.ts index 0881833..6a82bc2 100644 --- a/src/lib/player/player.svelte.ts +++ b/src/lib/player/player.svelte.ts @@ -283,6 +283,42 @@ class Reproductor { this.cargar(true); } + /** + * Anade pistas al final de la cola, sin tocar lo que suena. + * + * Hasta ahora la cola solo se podia sustituir: cada play la vaciaba y la + * llenaba con su contexto, asi que no habia forma de armar una escucha + * cogiendo temas de discos distintos. Esto es la otra mitad. + * + * Si no habia nada cargado, encolar arranca: pulsar "anadir a la cola" con el + * reproductor vacio y que no pase nada seria un boton que no hace nada. + * + * Lo que ya este en la cola no se repite. Un mismo tema dos veces seguidas no + * es lo que se pide al pulsar dos veces; lo que se pide es que este. + */ + encolar(pistas: PistaReproductor[]): number { + const reproducibles = pistas.filter((p) => p.audio); + const yaEstan = new Set(this.cola.map((p) => p.id)); + const nuevas = reproducibles.filter((p) => !yaEstan.has(p.id)); + + if (nuevas.length === 0) return 0; + + if (this.cola.length === 0) { + this.reproducirLista(nuevas, 0); + return nuevas.length; + } + + const antes = this.cola.length; + this.cola = [...this.cola, ...nuevas]; + /* + * El orden se alarga por el final con las posiciones nuevas. En aleatorio + * tambien: barajarlo entero movería de sitio lo que aún no ha sonado, y + * quien encola espera que lo suyo vaya después, no en medio. + */ + this.orden = [...this.orden, ...nuevas.map((_, i) => antes + i)]; + return nuevas.length; + } + /** * Alterna una pista concreta: si ya es la actual, pausa o reanuda; si no, * la pone a sonar dentro de su contexto (el album del que forma parte). diff --git a/src/lib/site.ts b/src/lib/site.ts index 4efb037..a142bc7 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -13,9 +13,9 @@ export const site = { nombre: 'Senza Paura', /** Como se describe el artista en una linea. */ - titular: 'Letrista y compositor', + titular: 'Letrista y creador musical', descripcion: - 'Letrista y compositor. Baladas románticas, y también clásica, rock y otros estilos, agrupados por bloques.', + 'Letrista y creador musical. Baladas románticas, y también clásica, rock y otros estilos, agrupados por bloques.', url: 'https://senzapaura.es', idioma: 'es', email: 'hola@senzapaura.es', diff --git a/src/routes/albumes/[slug]/+page.svelte b/src/routes/albumes/[slug]/+page.svelte index 9235a6f..e0a8843 100644 --- a/src/routes/albumes/[slug]/+page.svelte +++ b/src/routes/albumes/[slug]/+page.svelte @@ -2,7 +2,7 @@ import { resolve } from '$app/paths'; import BotonReproducir from '$lib/components/BotonReproducir.svelte'; import EnlacesPlataformas from '$lib/components/EnlacesPlataformas.svelte'; - import ListaCanciones from '$lib/components/ListaCanciones.svelte'; + import TablaCanciones from '$lib/components/TablaCanciones.svelte'; import RejillaCanciones from '$lib/components/RejillaCanciones.svelte'; import Meta from '$lib/components/Meta.svelte'; import SinPortada from '$lib/components/SinPortada.svelte'; @@ -107,7 +107,11 @@ --> {:else} - + + {/if} diff --git a/src/routes/canciones/[slug]/+page.svelte b/src/routes/canciones/[slug]/+page.svelte index 4bbfe50..b67860e 100644 --- a/src/routes/canciones/[slug]/+page.svelte +++ b/src/routes/canciones/[slug]/+page.svelte @@ -58,6 +58,22 @@ const enEspanol = $derived(cancion.idioma.split('-')[0] === 'es'); const aDosColumnas = $derived(!enEspanol && Boolean(cancion.traduccion)); + /** + * Una letra larga a una columna hace la página el doble de alta de lo que + * hace falta: casi dos mil píxeles solo de versos. A partir de seis estrofas + * se parte en dos columnas, si hay ancho para ello. + * + * Seis y no cuatro porque el corte tiene que dejar algo en cada lado: con + * tres estrofas por columna ya se lee como dos bloques, y con dos parece que + * la letra se ha quedado a medias. + * + * No se hace nunca cuando hay traducción: ahí las dos columnas ya están + * ocupadas por el original y el castellano, que es un enfrentamiento verso a + * verso y no una letra partida. + */ + const estrofas = $derived((letra?.match(/]/g) ?? []).length); + const enColumnas = $derived(!aDosColumnas && !cancion.instrumental && estrofas >= 6); + /** * El nombre del idioma en español, sin tabla que mantener. Si el navegador o * el servidor no saben traducirlo, se queda el código, que sigue diciendo @@ -182,7 +198,12 @@ {#if id === 'letra'} {#if letra}
-
+
{#if aDosColumnas}

Letra original · {nombreIdioma}

{/if} @@ -404,6 +425,36 @@ line-height: 1.75; } + /* + * Dos columnas de verso, solo con ancho de sobra: `column-width` y no + * `columns: 2` para que el navegador se quede en una sola en cuanto no + * quepan dos de 34 caracteres, sin tener que acertar el punto de corte en un + * `@media`. + * + * `break-inside: avoid` en cada estrofa es lo que hace que esto se pueda + * hacer: sin ello el corte cae en mitad de una y la letra deja de leerse. + */ + .texto.columnas { + max-width: none; + /* + * 37ch es el mínimo que deja caber dos columnas en la columna principal + * —864 px— y, a la vez, el verso más largo del catálogo, que mide 36. + * Importa afinarlo: en una letra, dónde acaba cada verso es parte del + * texto, así que una columna que doble renglones ahorra alto rompiendo + * justo lo que no se puede romper. + * + * Con `column-width` y no `columns: 2`: donde no quepan dos de 37ch, el + * navegador se queda en una sola sin que haya que acertar el punto de + * corte en un `@media`. + */ + column-width: 37ch; + column-gap: var(--sp-7); + } + + .texto.columnas :global(p) { + break-inside: avoid; + } + .videos { display: grid; gap: var(--sp-4); diff --git a/src/routes/musica/+page.svelte b/src/routes/musica/+page.svelte index e9dc7ad..ac30afe 100644 --- a/src/routes/musica/+page.svelte +++ b/src/routes/musica/+page.svelte @@ -2,7 +2,8 @@ import Meta from '$lib/components/Meta.svelte'; import RejillaCanciones from '$lib/components/RejillaCanciones.svelte'; import BloqueDeEstilo from '$lib/components/BloqueDeEstilo.svelte'; - import TablaCanciones, { type FilaCancion } from '$lib/components/TablaCanciones.svelte'; + import TablaCanciones from '$lib/components/TablaCanciones.svelte'; + import type { Cancion } from '$lib/types'; let { data } = $props(); @@ -11,18 +12,28 @@ let consulta = $state(''); /** - * Una fila por tema, con su estilo y su disco al lado. Sale de los bloques - * que ya vienen cargados, así que no viaja ni un byte de más. + * 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( + const filas = $derived( data.bloques.flatMap((bloque) => bloque.albumes.flatMap((album) => album.canciones.map((cancion) => ({ cancion, - estiloSlug: bloque.estilo.slug, - estiloNombre: bloque.estilo.nombre, - albumSlug: album.slug, - albumTitulo: album.titulo + /* 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 + ].join(' ') + ) })) ) ) @@ -36,24 +47,10 @@ .toLowerCase(); } - /** - * Se busca en todo lo que identifica un tema, no solo en el título: quien - * recuerda «el dueto de baladas» o «desamor» tiene que llegar igual. - */ - function coincide(fila: FilaCancion, termino: string): boolean { - const campos = [ - fila.cancion.titulo, - fila.estiloNombre, - fila.albumTitulo, - String(fila.cancion.ano ?? ''), - ...fila.cancion.etiquetas, - ...fila.cancion.interpretes - ]; - return campos.some((campo) => llano(campo).includes(termino)); - } - const termino = $derived(llano(consulta.trim())); - const filtradas = $derived(termino ? filas.filter((f) => coincide(f, termino)) : filas); + const filtradas = $derived( + (termino ? filas.filter((f) => f.buscable.includes(termino)) : filas).map((f) => f.cancion) + ); const buscando = $derived(termino.length > 0); @@ -118,13 +115,13 @@

{#if vista === 'tabla'} - + {:else if buscando} - f.cancion)} vacio="Ningún tema coincide." /> + {:else} {#each data.bloques as bloque (bloque.estilo.slug)}