From a918d55864d9854103270ce01cb036b086903d9c Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 7 Sep 2026 16:36:25 +0200 Subject: [PATCH] Cantos menos redondos, tarjetas coherentes y portada a ras MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 23 +++++ despliegue/senzapaura.nginx.conf | 74 +++++++++------- src/app.css | 19 +++-- src/lib/components/BotonCarrito.svelte | 2 +- src/lib/components/BotonFavorito.svelte | 2 +- src/lib/components/BotonReproducir.svelte | 2 + src/lib/components/Cabecera.svelte | 34 +++++++- src/lib/components/EnlacesPlataformas.svelte | 2 +- src/lib/components/Heroe.svelte | 3 +- src/lib/components/LaminaCta.svelte | 3 +- src/lib/components/ListaCanciones.svelte | 2 +- src/lib/components/Valoracion.svelte | 2 +- src/lib/components/VideoYoutube.svelte | 2 +- .../components/player/BarraReproductor.svelte | 2 +- .../components/player/ControlVolumen.svelte | 2 +- src/lib/components/player/PanelCola.svelte | 6 +- src/routes/+layout.svelte | 2 +- src/routes/+page.svelte | 85 +++++++++++-------- src/routes/albumes/[slug]/+page.svelte | 2 +- src/routes/blog/[slug]/+page.svelte | 2 +- src/routes/canciones/[slug]/+page.svelte | 2 +- src/routes/carrito/+page.svelte | 4 +- src/routes/contacto/+page.svelte | 4 +- src/routes/entrar/+page.svelte | 4 +- src/routes/etiquetas/+page.svelte | 2 +- src/routes/musica/+page.svelte | 4 +- src/routes/taller/+page.svelte | 49 ++++------- 27 files changed, 207 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 56f40a7..5eb7e2a 100644 --- a/README.md +++ b/README.md @@ -663,6 +663,17 @@ videos: ## La cabecera y las anclas +La portada **empieza en el borde de la ventana** y la cabecera flota encima: +por eso es translúcida con desenfoque, para apoyarse sobre la imagen en vez de +tapar una franja. Se consigue tirando de la portada hacia arriba con un margen +negativo del alto de la cabecera, que cada lámina devuelve en relleno para que +su contenido no se mueva. + +Ese alto **lo mide la cabecera** y lo publica en `--altura-cabecera`, igual que +hace la barra del reproductor con el suyo. Estaba escrito a mano en 4,75rem +cuando la cabecera mide 68px, así que todo lo que dependía de él —el salto a un +ancla, las barras laterales pegajosas— quedaba desplazado. + La cabecera **acompaña al desplazamiento** (`position: sticky`) y deja ver lo que pasa por debajo. La transparencia va **con desenfoque, no sola**: sobre la portada, un fondo solo translúcido dejaría el contraste del menú a merced de la @@ -706,6 +717,18 @@ Tres cosas que estaban copiadas en varios sitios y ahora se escriben una vez: un enlace dentro de otro deja una zona donde el clic hace una cosa distinta según el píxel—, así que es una clase y no un componente. +## Cantos y radios + +Dos valores y no más, para que nada se vea de otra familia: +`--radius-boton` (6px) para lo que se pulsa o se escribe —botones, campos, +pastillas—, y `--radius-tarjeta` (8px) para las superficies —tarjetas, paneles, +portadas—. Antes había tres radios sueltos y una píldora de 999px repetida en +quince archivos. + +Los botones de **icono suelto** siguen siendo redondos: el de reproducir sobre +una portada, el del tema, los puntos del carrusel. Un icono en un círculo tiene +sentido; un botón con texto en forma de cápsula es otra cosa. + ## Tarjetas Las tres clases —estilo, álbum y canción— comparten `.tarjeta` en `app.css` y diff --git a/despliegue/senzapaura.nginx.conf b/despliegue/senzapaura.nginx.conf index 35ead3b..411d06b 100644 --- a/despliegue/senzapaura.nginx.conf +++ b/despliegue/senzapaura.nginx.conf @@ -1,35 +1,28 @@ # Nginx por delante del servidor de Node. # -# Copiar a /etc/nginx/sites-available/senzapaura, sustituir DOMINIO, y luego: +# sudo cp senzapaura.nginx.conf /etc/nginx/sites-available/senzapaura +# sudo ln -sf /etc/nginx/sites-available/senzapaura /etc/nginx/sites-enabled/ +# sudo rm -f /etc/nginx/sites-enabled/default +# sudo nginx -t && sudo systemctl reload nginx # -# sudo ln -s /etc/nginx/sites-available/senzapaura /etc/nginx/sites-enabled/ -# sudo nginx -t # comprueba la sintaxis antes de recargar -# sudo systemctl reload nginx +# Ese `rm` del sitio por defecto importa: mientras esté, es él quien contesta a +# quien entra por IP, y se ve la página de bienvenida de Nginx en lugar de la +# web. # -# El certificado lo pone certbot, que reescribe este archivo al ejecutarse: -# -# sudo certbot --nginx -d DOMINIO -d www.DOMINIO -# -# Sin HTTPS no se puede publicar: aquí se escriben correos y se paga. - -# Todo lo que llegue sin cifrar se manda a la versión segura. Certbot añade -# este bloque por su cuenta, pero se deja escrito para que no dependa de ello. -server { - listen 80; - listen [::]:80; - server_name DOMINIO www.DOMINIO; - return 301 https://$host$request_uri; -} +# Tal como está, sirve por IP y sin cifrar, que es lo que se puede hacer en una +# red local sin dominio. Al final del archivo está lo que hay que cambiar +# cuando lo haya. server { - listen 443 ssl; - listen [::]:443 ssl; - http2 on; - server_name DOMINIO www.DOMINIO; + # `default_server` es lo que hace que este sitio conteste a quien entra por + # la IP o por un nombre que no coincida con ninguno. Sin esto, Nginx elige + # el sitio `default` y enseña su bienvenida. + listen 80 default_server; + listen [::]:80 default_server; - # certbot rellena estas dos. - # ssl_certificate /etc/letsencrypt/live/DOMINIO/fullchain.pem; - # ssl_certificate_key /etc/letsencrypt/live/DOMINIO/privkey.pem; + # `_` no casa con ningún nombre real: es la forma de decir «cualquiera». + # Cuando haya dominio, aquí va el dominio. + server_name _; # Los mp3 completos pesan varios megas; los fragmentos, cientos de kilos. client_max_body_size 20m; @@ -46,7 +39,7 @@ server { # # Todo lo de `_app/immutable` lleva el hash del contenido en el nombre: si # cambia el archivo, cambia la URL. Por eso se puede cachear para siempre y - # servirlo desde nginx sin molestar a Node, que es lo que hace que abrir la + # servirlo desde Nginx sin molestar a Node, que es lo que hace que abrir la # web no despierte al proceso para devolver un CSS. # ----------------------------------------------------------------------- location /_app/immutable/ { @@ -57,9 +50,9 @@ server { try_files $uri =404; } - # Imágenes, fuentes, vídeo y los fragmentos de muestra. No llevan huella, así - # que se cachean un día: lo justo para no pedirlos en cada visita sin que un - # cambio tarde una semana en verse. + # Imágenes, fuentes, vídeo, los fragmentos de muestra y el panel. No llevan + # huella, así que se cachean un día: lo justo para no pedirlos en cada + # visita sin que un cambio tarde una semana en verse. location ~* ^/(images|background|video|muestras|admin)/ { root /var/www/senzapaura/build/client; access_log off; @@ -73,7 +66,7 @@ server { # # Incluidas /descargas/… —que comprueban la compra antes de entregar el # archivo— y el webhook de Stripe. Ojo: las descargas NO se pueden servir - # desde nginx; el audio completo vive fuera de `build/` justamente para que + # desde Nginx; el audio completo vive fuera de `build/` justamente para que # no haya forma de pedirlo por URL sin haberlo comprado. # ----------------------------------------------------------------------- location / { @@ -93,7 +86,26 @@ server { # Un mp3 completo tarda en salir por una conexión lenta. proxy_read_timeout 300s; # Sin buffer: la descarga empieza a fluir en cuanto Node la manda, en - # vez de esperar a que nginx tenga el archivo entero. + # vez de esperar a que Nginx tenga el archivo entero. proxy_buffering off; } } + +# --------------------------------------------------------------------------- +# Cuando haya dominio +# +# 1. Cambiar `server_name _;` por `server_name DOMINIO www.DOMINIO;` y quitar +# `default_server` de las dos líneas `listen` de arriba. +# 2. Poner el certificado: +# +# sudo apt install -y certbot python3-certbot-nginx +# sudo certbot --nginx -d DOMINIO -d www.DOMINIO +# +# Certbot añade solo el bloque de 443 y la redirección desde el 80. +# 3. Cambiar `ORIGIN` en /var/www/senzapaura/.env a `https://DOMINIO` y +# reiniciar el servicio. Sin eso, SvelteKit rechaza todos los formularios +# con un 403 por comprobación de origen, y parece que la web está rota. +# +# Sin HTTPS no se puede publicar de cara al exterior: aquí se escriben correos +# y se paga. +# --------------------------------------------------------------------------- diff --git a/src/app.css b/src/app.css index 45732e7..2c28535 100644 --- a/src/app.css +++ b/src/app.css @@ -94,9 +94,18 @@ var(--c-accent-sombra) 100% ); - --radius: 4px; - /* Radio propio: 4px en una tarjeta grande se lee como un recorte, no como una pieza. */ - --radius-tarjeta: 14px; + /* + * Dos radios y no más, para que nada se vea de otra familia: + * + * - `--radius-boton` para lo que se pulsa o se escribe: botones, campos, + * pastillas. + * - `--radius-tarjeta` para las superficies: tarjetas, paneles, portadas. + * + * Un canto muy redondo tira a aplicación de móvil, y esto es una web de + * letras: la esquina corta acompaña mejor a una serif. + */ + --radius-boton: 6px; + --radius-tarjeta: 8px; /* * Curvas del movimiento. `--sal` sale rápido y frena largo, que es como se @@ -319,7 +328,7 @@ button { gap: var(--sp-2); padding: var(--sp-3) var(--sp-5); border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); /* * `background-color` y no el atajo `background`: el atajo reinicia también * `background-image`, así que una variante con degradado —el botón dorado— @@ -406,7 +415,7 @@ button { display: inline-block; padding: 0.2em 0.7em; border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); font-size: var(--fs-xs); color: var(--c-text-muted); text-decoration: none; diff --git a/src/lib/components/BotonCarrito.svelte b/src/lib/components/BotonCarrito.svelte index d7d8a7e..9f60272 100644 --- a/src/lib/components/BotonCarrito.svelte +++ b/src/lib/components/BotonCarrito.svelte @@ -57,7 +57,7 @@ .boton { padding: var(--sp-1) var(--sp-3); border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); background: transparent; font-size: var(--fs-sm); cursor: pointer; diff --git a/src/lib/components/BotonFavorito.svelte b/src/lib/components/BotonFavorito.svelte index 79704c3..81b9982 100644 --- a/src/lib/components/BotonFavorito.svelte +++ b/src/lib/components/BotonFavorito.svelte @@ -81,7 +81,7 @@ gap: var(--sp-2); padding: var(--sp-1) var(--sp-3); border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); background: transparent; color: var(--c-text-muted); font: inherit; diff --git a/src/lib/components/BotonReproducir.svelte b/src/lib/components/BotonReproducir.svelte index bb31acc..094bbb7 100644 --- a/src/lib/components/BotonReproducir.svelte +++ b/src/lib/components/BotonReproducir.svelte @@ -109,6 +109,8 @@ } .solido { + /* Con texto ya no es un icono en un círculo, sino un botón. */ + border-radius: var(--radius-boton); padding: var(--sp-2) var(--sp-4); background-color: var(--c-accent); background-image: var(--oro); diff --git a/src/lib/components/Cabecera.svelte b/src/lib/components/Cabecera.svelte index 0c3e3eb..c6f3083 100644 --- a/src/lib/components/Cabecera.svelte +++ b/src/lib/components/Cabecera.svelte @@ -7,6 +7,34 @@ import { navegacion, site } from '$lib/site'; let menuAbierto = $state(false); + let cabecera = $state(null); + + /** + * La cabecera publica su altura real en `--altura-cabecera`. + * + * Ese valor lo usan tres cosas: el desplazamiento a un ancla, las barras + * laterales pegajosas y la portada, que se mete por debajo de la cabecera y + * necesita saber cuánto para compensarlo. Estaba escrito a mano en + * `app.css` y no coincidía —4,75rem contra los 70px reales—, así que todo + * lo que dependía de él quedaba desplazado. + * + * Se mide en vez de calcularse porque el menú móvil la hace crecer al + * desplegarse. + */ + $effect(() => { + if (!cabecera) return; + + const observador = new ResizeObserver(([entrada]) => { + const alto = entrada.borderBoxSize?.[0]?.blockSize ?? entrada.contentRect.height; + document.documentElement.style.setProperty('--altura-cabecera', `${Math.ceil(alto)}px`); + }); + observador.observe(cabecera); + + return () => { + observador.disconnect(); + document.documentElement.style.removeProperty('--altura-cabecera'); + }; + }); /** Una sección está activa también en sus páginas hijas. */ function activa(href: string): boolean { @@ -23,7 +51,7 @@ }); -
+
{site.nombre} @@ -184,7 +212,7 @@ min-width: 1.4em; height: 1.4em; padding-inline: 0.35em; - border-radius: 999px; + border-radius: var(--radius-boton); background: var(--c-accent); color: var(--c-accent-text); font-size: var(--fs-xs); @@ -196,7 +224,7 @@ display: none; padding: var(--sp-2); border: 1px solid var(--c-border); - border-radius: var(--radius); + border-radius: var(--radius-boton); background: transparent; cursor: pointer; } diff --git a/src/lib/components/EnlacesPlataformas.svelte b/src/lib/components/EnlacesPlataformas.svelte index 8762254..ecbff61 100644 --- a/src/lib/components/EnlacesPlataformas.svelte +++ b/src/lib/components/EnlacesPlataformas.svelte @@ -44,7 +44,7 @@ gap: var(--sp-2); padding: var(--sp-1) var(--sp-3); border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); font-size: var(--fs-sm); text-decoration: none; } diff --git a/src/lib/components/Heroe.svelte b/src/lib/components/Heroe.svelte index f60415e..c4b116f 100644 --- a/src/lib/components/Heroe.svelte +++ b/src/lib/components/Heroe.svelte @@ -120,7 +120,8 @@ display: flex; align-items: center; min-height: min(88vh, 46rem); - padding-block: var(--sp-8); + /* El alto de la cabecera, que flota encima, se devuelve aquí. */ + padding-block: calc(var(--sp-8) + var(--altura-cabecera)) var(--sp-8); overflow: hidden; isolation: isolate; } diff --git a/src/lib/components/LaminaCta.svelte b/src/lib/components/LaminaCta.svelte index 7f8ff83..0ec5272 100644 --- a/src/lib/components/LaminaCta.svelte +++ b/src/lib/components/LaminaCta.svelte @@ -43,7 +43,8 @@ display: flex; align-items: center; min-height: min(88vh, 46rem); - padding-block: var(--sp-8); + /* El alto de la cabecera, que flota encima, se devuelve aquí. */ + padding-block: calc(var(--sp-8) + var(--altura-cabecera)) var(--sp-8); overflow: hidden; isolation: isolate; } diff --git a/src/lib/components/ListaCanciones.svelte b/src/lib/components/ListaCanciones.svelte index 3246f7d..d312b22 100644 --- a/src/lib/components/ListaCanciones.svelte +++ b/src/lib/components/ListaCanciones.svelte @@ -125,7 +125,7 @@ .marca { border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); padding: 0 0.4em; margin-right: var(--sp-1); } diff --git a/src/lib/components/Valoracion.svelte b/src/lib/components/Valoracion.svelte index 29504ab..37ed63b 100644 --- a/src/lib/components/Valoracion.svelte +++ b/src/lib/components/Valoracion.svelte @@ -173,7 +173,7 @@ .estrellas label:has(input:focus-visible) { outline: 2px solid var(--c-accent); outline-offset: 2px; - border-radius: var(--radius); + border-radius: var(--radius-boton); } .retirar { diff --git a/src/lib/components/VideoYoutube.svelte b/src/lib/components/VideoYoutube.svelte index 14348c1..98d7afc 100644 --- a/src/lib/components/VideoYoutube.svelte +++ b/src/lib/components/VideoYoutube.svelte @@ -65,7 +65,7 @@ aspect-ratio: 16 / 9; overflow: hidden; border: 1px solid var(--c-border); - border-radius: var(--radius); + border-radius: var(--radius-tarjeta); background: var(--c-bg-sunken); } diff --git a/src/lib/components/player/BarraReproductor.svelte b/src/lib/components/player/BarraReproductor.svelte index 3463097..8786526 100644 --- a/src/lib/components/player/BarraReproductor.svelte +++ b/src/lib/components/player/BarraReproductor.svelte @@ -258,7 +258,7 @@ flex: none; padding: 0 0.4em; border: 1px solid var(--c-border); - border-radius: 999px; + border-radius: var(--radius-boton); font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.06em; diff --git a/src/lib/components/player/ControlVolumen.svelte b/src/lib/components/player/ControlVolumen.svelte index 8f8548a..2383ee2 100644 --- a/src/lib/components/player/ControlVolumen.svelte +++ b/src/lib/components/player/ControlVolumen.svelte @@ -43,7 +43,7 @@ display: inline-flex; padding: var(--sp-2); border: none; - border-radius: var(--radius); + border-radius: var(--radius-boton); background: transparent; cursor: pointer; } diff --git a/src/lib/components/player/PanelCola.svelte b/src/lib/components/player/PanelCola.svelte index ead567c..4c396e9 100644 --- a/src/lib/components/player/PanelCola.svelte +++ b/src/lib/components/player/PanelCola.svelte @@ -64,7 +64,7 @@ background: var(--c-bg-raised); border: 1px solid var(--c-border); border-bottom: 0; - border-radius: var(--radius) 0 0 0; + border-radius: var(--radius-tarjeta) 0 0 0; } .cabecera { @@ -88,7 +88,7 @@ border: none; background: transparent; cursor: pointer; - border-radius: var(--radius); + border-radius: var(--radius-boton); } .cerrar:hover { @@ -115,7 +115,7 @@ width: 100%; padding: var(--sp-2); border: none; - border-radius: var(--radius); + border-radius: var(--radius-boton); background: transparent; text-align: left; cursor: pointer; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 23d876d..b0da955 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -64,7 +64,7 @@ padding: var(--sp-2) var(--sp-4); background: var(--c-accent); color: var(--c-accent-text); - border-radius: var(--radius); + border-radius: var(--radius-boton); transition: top 0.15s ease; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2402962..b419a8e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -30,41 +30,52 @@ - - {#snippet lamina(i)} - {#if i === 0} - - {:else if i === 1} - - {#snippet acciones()} - Entrar al taller - - Empezar por el principio - - {/snippet} - - {:else} - - {#snippet acciones()} - Elegir canciones - Ver el carrito - {/snippet} - - {/if} - {/snippet} - + +
+ + {#snippet lamina(i)} + {#if i === 0} + + {:else if i === 1} + + {#snippet acciones()} + Entrar al taller + + Empezar por el principio + + {/snippet} + + {:else} + + {#snippet acciones()} + Elegir canciones + Ver el carrito + {/snippet} + + {/if} + {/snippet} + +
{#if principal}