Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
<script lang="ts">
|
|
|
|
|
import { resolve } from '$app/paths';
|
|
|
|
|
import BotonCarrito from '$lib/components/BotonCarrito.svelte';
|
|
|
|
|
import BotonFavorito from '$lib/components/BotonFavorito.svelte';
|
|
|
|
|
import BotonReproducir from '$lib/components/BotonReproducir.svelte';
|
|
|
|
|
import EnlacesPlataformas from '$lib/components/EnlacesPlataformas.svelte';
|
|
|
|
|
import FichaDeCancion from '$lib/components/FichaDeCancion.svelte';
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
import Meta from '$lib/components/Meta.svelte';
|
|
|
|
|
import Pestanas, { type Pestana } from '$lib/components/Pestanas.svelte';
|
|
|
|
|
import VideoYoutube from '$lib/components/VideoYoutube.svelte';
|
|
|
|
|
import { aPista, aPistaDeVersion, aPistas } from '$lib/player/pista';
|
|
|
|
|
import { CANCIONES_POR_PACK, formatearPrecio, PRECIO_PACK_CENTIMOS } from '$lib/tienda';
|
Deja de repetir el fondo de la portada, las migas y las etiquetas
Tres piezas estaban copiadas en varios sitios y ahora se escriben una vez: el
fondo a sangre de la portada, las migas de pan y la pastilla de una etiqueta.
El fondo ya había divergido entre la cabecera y las láminas de llamada: el
degradado era distinto por descuido y no por decisión, que es lo que pasa con el
marcado duplicado.
Las migas pasan a ser una lista ordenada, que es su marcado correcto: un lector
de pantalla anuncia cuántos escalones hay y en cuál se va, y el último deja de
ser un enlace a donde ya estás.
Tres cosas salieron de hacerlo, ninguna cosmética:
- Con el texto como clave del bucle, un disco y un tema que se llaman igual
—«Como si nada» es las dos cosas— daban claves duplicadas y tumbaban la ficha
al hidratar. El SSR salía bien, así que solo se veía en el navegador.
- Al compartir el fondo, las tres láminas montaban el clip de vídeo. En tema
claro eso eran tres copias del mismo vídeo en la portada. Ahora lo pide solo
la cabecera.
- La lista de temas de un disco no tenía nombre, así que no se distinguía de
ninguna otra lista de la página. Ahora se llama «Temas».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
import Migas from '$lib/components/Migas.svelte';
|
|
|
|
|
import { listaDeNombres, segundosADuracion } from '$lib/utils/format';
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
let { data } = $props();
|
|
|
|
|
|
|
|
|
|
const cancion = $derived(data.cancion);
|
|
|
|
|
const Cuerpo = $derived(data.Cuerpo);
|
|
|
|
|
|
|
|
|
|
/** El contexto es el álbum entero; el índice, la posición del tema en él. */
|
|
|
|
|
const pistas = $derived(aPistas(data.hermanas));
|
|
|
|
|
const indice = $derived(data.hermanas.findIndex((c) => c.slug === cancion.slug));
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Las alternativas musicales forman su propio contexto, con la versión
|
|
|
|
|
* principal a la cabeza: así se pueden comparar arreglos seguidos sin que
|
|
|
|
|
* el reproductor salte al tema siguiente del disco.
|
|
|
|
|
*/
|
|
|
|
|
const pistasDeVersiones = $derived([
|
|
|
|
|
aPista(cancion),
|
|
|
|
|
...cancion.versiones.map((v) => aPistaDeVersion(cancion, v))
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Las pestañas del cuerpo. La letra va primero y abierta: es el contenido
|
|
|
|
|
* principal de la ficha. Las otras dos solo aparecen si el tema las tiene.
|
|
|
|
|
*/
|
|
|
|
|
const pestanas: Pestana[] = $derived([
|
|
|
|
|
{ id: 'letra', etiqueta: cancion.instrumental ? 'Sobre la pieza' : 'Letra' },
|
|
|
|
|
...(cancion.inspiracion.length > 0 ? [{ id: 'inspiracion', etiqueta: 'De dónde sale' }] : []),
|
|
|
|
|
...(cancion.versiones.length > 0
|
|
|
|
|
? [{ id: 'versiones', etiqueta: 'Alternativas musicales' }]
|
|
|
|
|
: []),
|
|
|
|
|
...(cancion.videos.length > 0
|
|
|
|
|
? [{ id: 'videos', etiqueta: cancion.videos.length === 1 ? 'Vídeo' : 'Vídeos' }]
|
|
|
|
|
: [])
|
|
|
|
|
]);
|
|
|
|
|
|
Carrusel en la portada y letras a dos columnas
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
/**
|
|
|
|
|
* La letra va a dos columnas cuando no está en español y hay traducción:
|
|
|
|
|
* original a la izquierda, castellano a la derecha. Con una sola de las dos
|
|
|
|
|
* cosas no hay nada que enfrentar, y una columna vacía sería peor que
|
|
|
|
|
* ninguna.
|
|
|
|
|
*/
|
|
|
|
|
const enEspanol = $derived(cancion.idioma.split('-')[0] === 'es');
|
|
|
|
|
const aDosColumnas = $derived(!enEspanol && Boolean(cancion.traduccion));
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
* algo.
|
|
|
|
|
*/
|
|
|
|
|
const nombreIdioma = $derived.by(() => {
|
|
|
|
|
try {
|
|
|
|
|
return (
|
|
|
|
|
new Intl.DisplayNames(['es'], { type: 'language' }).of(cancion.idioma) ?? cancion.idioma
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return cancion.idioma;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
const descripcion = $derived(
|
|
|
|
|
`${cancion.titulo}. Letra de ${listaDeNombres(cancion.letra)}, música de ${listaDeNombres(cancion.musica)}.`
|
|
|
|
|
);
|
Deja de repetir el fondo de la portada, las migas y las etiquetas
Tres piezas estaban copiadas en varios sitios y ahora se escriben una vez: el
fondo a sangre de la portada, las migas de pan y la pastilla de una etiqueta.
El fondo ya había divergido entre la cabecera y las láminas de llamada: el
degradado era distinto por descuido y no por decisión, que es lo que pasa con el
marcado duplicado.
Las migas pasan a ser una lista ordenada, que es su marcado correcto: un lector
de pantalla anuncia cuántos escalones hay y en cuál se va, y el último deja de
ser un enlace a donde ya estás.
Tres cosas salieron de hacerlo, ninguna cosmética:
- Con el texto como clave del bucle, un disco y un tema que se llaman igual
—«Como si nada» es las dos cosas— daban claves duplicadas y tumbaban la ficha
al hidratar. El SSR salía bien, así que solo se veía en el navegador.
- Al compartir el fondo, las tres láminas montaban el clip de vídeo. En tema
claro eso eran tres copias del mismo vídeo en la portada. Ahora lo pide solo
la cabecera.
- La lista de temas de un disco no tenía nombre, así que no se distinguía de
ninguna otra lista de la página. Ahora se llama «Temas».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
const migas = $derived([
|
|
|
|
|
{ texto: 'Música', href: resolve('/musica') },
|
|
|
|
|
...(data.estilo
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
texto: data.estilo.nombre,
|
|
|
|
|
href: resolve('/musica/[estilo]', { estilo: data.estilo.slug })
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
: []),
|
|
|
|
|
...(data.album
|
|
|
|
|
? [{ texto: data.album.titulo, href: resolve('/albumes/[slug]', { slug: data.album.slug }) }]
|
|
|
|
|
: []),
|
|
|
|
|
{ texto: cancion.titulo }
|
|
|
|
|
]);
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Meta titulo={cancion.titulo} {descripcion} imagen={data.portada} />
|
|
|
|
|
|
|
|
|
|
<div class="container">
|
Deja de repetir el fondo de la portada, las migas y las etiquetas
Tres piezas estaban copiadas en varios sitios y ahora se escriben una vez: el
fondo a sangre de la portada, las migas de pan y la pastilla de una etiqueta.
El fondo ya había divergido entre la cabecera y las láminas de llamada: el
degradado era distinto por descuido y no por decisión, que es lo que pasa con el
marcado duplicado.
Las migas pasan a ser una lista ordenada, que es su marcado correcto: un lector
de pantalla anuncia cuántos escalones hay y en cuál se va, y el último deja de
ser un enlace a donde ya estás.
Tres cosas salieron de hacerlo, ninguna cosmética:
- Con el texto como clave del bucle, un disco y un tema que se llaman igual
—«Como si nada» es las dos cosas— daban claves duplicadas y tumbaban la ficha
al hidratar. El SSR salía bien, así que solo se veía en el navegador.
- Al compartir el fondo, las tres láminas montaban el clip de vídeo. En tema
claro eso eran tres copias del mismo vídeo en la portada. Ahora lo pide solo
la cabecera.
- La lista de temas de un disco no tenía nombre, así que no se distinguía de
ninguna otra lista de la página. Ahora se llama «Temas».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
<Migas {migas} />
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
|
|
|
|
|
<header class="ficha">
|
|
|
|
|
<img
|
|
|
|
|
class="portada"
|
|
|
|
|
src={data.portada}
|
|
|
|
|
alt="Portada de {cancion.titulo}"
|
|
|
|
|
width="280"
|
|
|
|
|
height="280"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div class="titulo">
|
|
|
|
|
<h1>{cancion.titulo}</h1>
|
|
|
|
|
<p class="meta muted">
|
|
|
|
|
<!--
|
|
|
|
|
El album de singles es sintetico y se titula «Singles», asi que
|
|
|
|
|
nombrarlo aqui diria «Single del estilo Singles». Lo que situa un
|
|
|
|
|
tema suelto es su estilo; a la coleccion se llega por las migas.
|
|
|
|
|
-->
|
|
|
|
|
{#if data.album?.sintetico && data.estilo}
|
|
|
|
|
Single del estilo
|
|
|
|
|
<a href={resolve('/musica/[estilo]', { estilo: data.estilo.slug })}>
|
|
|
|
|
{data.estilo.nombre}
|
|
|
|
|
</a>
|
|
|
|
|
{:else if data.album}
|
|
|
|
|
Del disco
|
|
|
|
|
<a href={resolve('/albumes/[slug]', { slug: data.album.slug })}>{data.album.titulo}</a>
|
|
|
|
|
{/if}
|
|
|
|
|
{#if cancion.ano}
|
|
|
|
|
<span aria-hidden="true">·</span> {cancion.ano}
|
|
|
|
|
{/if}
|
|
|
|
|
{#if cancion.duracion}
|
|
|
|
|
<span aria-hidden="true">·</span>
|
|
|
|
|
{segundosADuracion(cancion.duracion)}
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div class="acciones">
|
|
|
|
|
<BotonReproducir
|
|
|
|
|
{pistas}
|
|
|
|
|
indice={Math.max(0, indice)}
|
|
|
|
|
texto="Escuchar la muestra"
|
|
|
|
|
variante="solido"
|
|
|
|
|
tamano={18}
|
|
|
|
|
/>
|
|
|
|
|
<BotonCarrito {cancion} />
|
|
|
|
|
<BotonFavorito cancionSlug={cancion.slug} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if cancion.alaVenta}
|
|
|
|
|
<p class="venta muted">
|
|
|
|
|
En la web suena un fragmento. La descarga completa en MP3, con todas sus versiones, va en
|
|
|
|
|
packs de {CANCIONES_POR_PACK} canciones por {formatearPrecio(PRECIO_PACK_CENTIMOS)}.
|
|
|
|
|
</p>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<EnlacesPlataformas enlaces={cancion.enlaces} etiqueta="Escuchar {cancion.titulo} en" />
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="cuerpo">
|
|
|
|
|
<div class="principal">
|
|
|
|
|
<Pestanas {pestanas} etiqueta="Contenido del tema">
|
|
|
|
|
{#snippet panel(id)}
|
|
|
|
|
{#if id === 'letra'}
|
|
|
|
|
{#if Cuerpo}
|
Carrusel en la portada y letras a dos columnas
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
<div class="letra" class:pareada={aDosColumnas}>
|
|
|
|
|
<div class="texto" class:versos={!cancion.instrumental} lang={cancion.idioma}>
|
|
|
|
|
{#if aDosColumnas}
|
|
|
|
|
<p class="rotulo-columna muted">Letra original · {nombreIdioma}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
<Cuerpo />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if aDosColumnas}
|
|
|
|
|
<!--
|
|
|
|
|
`pre-line` conserva el corte de los versos sin necesidad de
|
|
|
|
|
compilar Markdown: una traducción que pierde los cortes deja
|
|
|
|
|
de servir para seguir la original línea a línea.
|
|
|
|
|
-->
|
|
|
|
|
<div class="texto versos traduccion" lang="es">
|
|
|
|
|
<p class="rotulo-columna muted">Traducción al español</p>
|
|
|
|
|
<p class="verso-a-verso">{cancion.traduccion}</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<p class="muted">
|
|
|
|
|
{cancion.instrumental
|
|
|
|
|
? 'Todavía no hay notas publicadas de esta pieza.'
|
|
|
|
|
: 'La letra de este tema aún no está publicada.'}
|
|
|
|
|
</p>
|
|
|
|
|
{/if}
|
|
|
|
|
{:else if id === 'inspiracion'}
|
|
|
|
|
<div class="texto">
|
|
|
|
|
{#each cancion.inspiracion as parrafo, i (i)}
|
|
|
|
|
<p>{parrafo}</p>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
{:else if id === 'videos'}
|
|
|
|
|
<div class="videos">
|
|
|
|
|
{#each cancion.videos as video (video.youtube)}
|
|
|
|
|
<VideoYoutube youtube={video.youtube} titulo={video.titulo} poster={data.portada} />
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
<p class="muted aviso">
|
|
|
|
|
El vídeo se carga solo al pulsarlo: hasta entonces esta página no pide nada a YouTube.
|
|
|
|
|
</p>
|
|
|
|
|
{:else if id === 'versiones'}
|
|
|
|
|
<p class="muted">
|
|
|
|
|
El mismo tema en otros arreglos. Quien compra la canción se lleva todas sus versiones.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<ul class="versiones">
|
|
|
|
|
{#each cancion.versiones as version, i (version.id)}
|
|
|
|
|
<li class="version">
|
|
|
|
|
<BotonReproducir pistas={pistasDeVersiones} indice={i + 1} />
|
|
|
|
|
<div class="datos">
|
|
|
|
|
<p class="nombre">{version.nombre}</p>
|
|
|
|
|
{#if version.nota}
|
|
|
|
|
<p class="nota muted">{version.nota}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<span class="duracion muted">
|
|
|
|
|
{version.duracion ? segundosADuracion(version.duracion) : '--:--'}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Pestanas>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FichaDeCancion
|
|
|
|
|
{cancion}
|
|
|
|
|
album={data.album}
|
|
|
|
|
estilo={data.estilo}
|
|
|
|
|
valoracion={data.valoracion}
|
|
|
|
|
miValoracion={data.miValoracion}
|
|
|
|
|
/>
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if data.anterior || data.siguiente}
|
|
|
|
|
<nav class="vecinos" aria-label="Otros temas del disco">
|
|
|
|
|
{#if data.anterior}
|
|
|
|
|
<a class="vecino" href={resolve('/canciones/[slug]', { slug: data.anterior.slug })}>
|
|
|
|
|
<span class="muted">Anterior</span>
|
|
|
|
|
<span>{data.anterior.titulo}</span>
|
|
|
|
|
</a>
|
|
|
|
|
{:else}
|
|
|
|
|
<span></span>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if data.siguiente}
|
|
|
|
|
<a
|
|
|
|
|
class="vecino derecha"
|
|
|
|
|
href={resolve('/canciones/[slug]', { slug: data.siguiente.slug })}
|
|
|
|
|
>
|
|
|
|
|
<span class="muted">Siguiente</span>
|
|
|
|
|
<span>{data.siguiente.titulo}</span>
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
</nav>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
Carrusel en la portada y letras a dos columnas
La portada abre con tres láminas: el disco en el tocadiscos, una invitación al
taller y otra a la tienda. Un carrusel mal hecho es de las piezas más hostiles
que hay —cambia mientras lees, no se para, y para un lector de pantalla es un
cajón que aparece y desaparece—, así que este se para al pasar el puntero o al
entrar el foco, tiene botón de pausa, no gira solo con `prefers-reduced-motion`
y deja las láminas ocultas con `inert`, fuera del orden de tabulación.
Las letras que no están en español se enfrentan con su traducción en dos
columnas, verso a verso. Cada columna declara su `lang`, que es lo que impide
que un lector de pantalla lea el inglés con voz española, y la traducción se
pinta con `pre-line`: es verso, y perder los cortes la deja inservible para
seguir la original. El nombre del idioma sale de `Intl.DisplayNames`.
Sobre el reproductor: ya sobrevivía a la navegación, porque vive en el layout
raíz fuera de `main`. Lo que faltaba era comprobarlo de verdad; el test decía
que la barra seguía visible, no que siguiera sonando. Ahora mira que la posición
avance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
/*
|
|
|
|
|
* A dos columnas se leen en paralelo, que es para lo que sirve tener la
|
|
|
|
|
* traducción al lado. Al estrecharse caen una debajo de otra: partir un
|
|
|
|
|
* verso para que quepan dos columnas sería peor que apilarlas.
|
|
|
|
|
*/
|
|
|
|
|
.letra.pareada {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
gap: var(--sp-5) var(--sp-6);
|
|
|
|
|
align-items: start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rotulo-columna {
|
|
|
|
|
margin: 0 0 var(--sp-3);
|
|
|
|
|
padding-bottom: var(--sp-2);
|
|
|
|
|
border-bottom: 1px solid var(--c-border);
|
|
|
|
|
font-family: var(--font-body);
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.verso-a-verso {
|
|
|
|
|
margin: 0;
|
|
|
|
|
white-space: pre-line;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 52rem) {
|
|
|
|
|
.letra.pareada {
|
|
|
|
|
grid-template-columns: minmax(0, 1fr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
.ficha {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 17rem) minmax(0, 1fr);
|
|
|
|
|
gap: var(--sp-6);
|
|
|
|
|
align-items: start;
|
|
|
|
|
padding-bottom: var(--sp-5);
|
|
|
|
|
border-bottom: 1px solid var(--c-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada {
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border: 1px solid var(--c-border);
|
Cantos menos redondos, tarjetas coherentes y portada a ras
Los botones dejan de ser cápsulas y las tarjetas pierden la mitad del canto. De
paso se ordena el sistema: había tres radios sueltos y una píldora de 999px
repetida en quince archivos, y quedan dos —uno para lo que se pulsa, otro para
las superficies—. Los botones de icono suelto siguen redondos, que ahí sí tiene
sentido.
Las fichas del taller no usaban la tarjeta compartida: tenían su propio borde,
su propio canto y su propio hover, así que se veían de otra familia que las de
música. Ahora usan `.tarjeta`, con el mismo reflejo bajo el puntero y el mismo
subrayado.
Y la portada arranca en el borde de la ventana, con la cabecera flotando encima.
Para eso la cabecera pasa a medir su propio alto y publicarlo: estaba escrito a
mano en 4,75rem cuando mide 68px, y todo lo que dependía de ese número —el salto
a un ancla, las barras laterales pegajosas— quedaba desplazado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
border-radius: var(--radius-tarjeta);
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
background: var(--c-bg-sunken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.meta {
|
|
|
|
|
font-size: var(--fs-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.acciones {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: var(--sp-3);
|
|
|
|
|
margin-bottom: var(--sp-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.venta {
|
|
|
|
|
max-width: 46ch;
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cuerpo {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 16rem);
|
|
|
|
|
gap: var(--sp-6);
|
|
|
|
|
padding-block: var(--sp-6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* La letra se compone en bloque estrecho y con los saltos de línea del
|
|
|
|
|
* original: en una canción, dónde termina cada verso es parte del texto.
|
|
|
|
|
*/
|
|
|
|
|
.texto {
|
|
|
|
|
max-width: 46ch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.versos {
|
|
|
|
|
font-family: var(--font-versos);
|
|
|
|
|
font-size: var(--fs-lg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.versos :global(p) {
|
|
|
|
|
margin-bottom: var(--sp-5);
|
|
|
|
|
line-height: 1.75;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.videos {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--sp-4);
|
|
|
|
|
max-width: 44rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.aviso {
|
|
|
|
|
margin-top: var(--sp-3);
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
max-width: 44rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.versiones {
|
|
|
|
|
list-style: none;
|
|
|
|
|
margin: var(--sp-4) 0 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
border-top: 1px solid var(--c-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.version {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--sp-3);
|
|
|
|
|
padding: var(--sp-2) var(--sp-1);
|
|
|
|
|
border-bottom: 1px solid var(--c-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.version .datos p {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nombre {
|
|
|
|
|
font-size: var(--fs-sm);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nota {
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.duracion {
|
|
|
|
|
font-size: var(--fs-sm);
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
}
|
|
|
|
|
.vecinos {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: var(--sp-4);
|
|
|
|
|
padding-block: var(--sp-5);
|
|
|
|
|
border-top: 1px solid var(--c-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vecino {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
font-size: var(--fs-sm);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vecino:hover span:last-child {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vecino.derecha {
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 48rem) {
|
|
|
|
|
.ficha,
|
|
|
|
|
.cuerpo {
|
|
|
|
|
grid-template-columns: minmax(0, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada {
|
|
|
|
|
max-width: 14rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|