@ -2,23 +2,24 @@
/**
* Las páginas de error, 404 y 500.
*
* Cada una lleva su lámina y su tema. La lámina trae el número y el rótulo
* dibujados dentro, así que la página **no** los repite: escribe lo que la
* imagen no puede decir —qué ha pasado y por dónde se sale— y deja el titular
* a la ilustración. Aun así hay un `h1` de verdad, porque una página cuyo
* único título viva dentro de un `.webp` no la lee un lector de pantalla ni
* sobrevive a que la imagen no cargue.
* Van **a pantalla completa y desnudas**: sin cabecera, sin pie y sin barra de
* reproducción. La lámina es el fondo, y encima va lo que hay que leer y
* pulsar, centrado.
*
* El fondo se recorta al ancho (`cover`), así que el color de relleno de
* `FONDOS` casi nunca se ve; está para el instante antes de que cargue la
* imagen y para las proporciones raras, y es el del borde de cada lámina
* —medido sobre el archivo—, de modo que no hay salto de color.
*
* ## La música
*
* Suena con un botón, no sola. Los navegadores cortan el audio que empieza
* sin que el usuario haya tocado nada, y quien llega a un 404 desde un enlace
* roto no ha tocado nada todavía: arrancarla al cargar no fallaría, es que no
* Suena con un botón, no sola. Los navegadores cortan el audio que empieza sin
* que el usuario haya tocado nada, y quien llega a un 404 desde un enlace roto
* no ha tocado nada todavía: arrancarla al cargar no fallaría, es que no
* sonaría, y en silencio.
*
* Y suena una cosa cada vez. Si había un tema en la barra de reproducción, se
* pausa al dar aquí; si desde la barra se vuelve a dar al play, esto se para.
* Manda el último que haya pulsado, que es lo que uno espera.
* Al entrar se para lo que hubiera sonando. Sin barra no hay con qué pararlo, y
* un tema que sigue sonando sin controles a la vista es peor que un silencio.
*
* El `preload="none"` importa: sin él, el navegador se traería los 686 KB del
* mp3 a todo el que aterrice en un 404, lo vaya a escuchar o no.
@ -29,12 +30,17 @@
import Meta from '$lib/components/Meta.svelte';
import { reproductor } from '$lib/player/player.svelte';
/** El color del borde de cada lámina, medido sobre el archivo. */
const FONDOS: Record< string , string > = { '404' : '#00122a' , '500' : '#000719' } ;
/** El de la banda lisa donde va el texto. Es el mismo en las dos. */
const BANDA = '#00213c';
/**
* Lo que pone SvelteKit cuando no hay mensaje nuestro. Están en inglés y no
* dicen nada —«Internal Error» en un sitio en español es peor que no poner
* nada—, así que se descartan y se escribe la frase a mano. Los que sí son
* nuestros vienen redactados: «Ese tema no existe.», «Esta cuenta ya no
* administra el sitio.»
* nuestros vienen redactados: «Ese tema no existe.»
*/
const GENERICOS = new Set([
'Not Found',
@ -49,15 +55,13 @@
);
/**
* Solo hay lámina para dos casos, y son los dos que tienen dibujo: la que dice
* «página no encontrada» y la que dice «algo ha salido mal». Un 403 no es
* ninguna de las dos cosas —la página existe y el sitio funciona, es que no
* te toca—, así que sale sin ilustración en vez de con una que miente.
* Solo hay lámina para dos casos, que son los dos que tienen dibujo. Un 403 no
* es ninguno: la página existe y el sitio funciona, es que no te toca. Ese
* sale sin ilustración en vez de con una que miente.
*/
const caso = $derived(page.status === 404 ? 'perdida' : page.status >= 500 ? 'rota' : 'otra');
const lamina = $derived(caso === 'perdida' ? '404' : caso === 'rota' ? '500' : null);
/** El de la pestaña y el buscador; el de la página va aparte, más abajo. */
const titulo = $derived(
caso === 'perdida'
? 'Página no encontrada'
@ -66,11 +70,7 @@
: (mensaje ?? `Error ${ page . status } `)
);
/**
* El encabezado no repite lo que la lámina ya lleva dibujado —«404 · PÁGINA
* NO ENCONTRADA»—, dice lo mismo con otras palabras. Así no se lee dos veces
* seguidas, y si la imagen no carga la página sigue teniendo su titular.
*/
/** No repite lo que la lámina lleva dibujado: lo dice con otras palabras. */
const encabezado = $derived(
caso === 'perdida'
? 'Esta dirección no lleva a ninguna parte'
@ -90,47 +90,40 @@
let audio = $state< HTMLAudioElement | null > (null);
let sonando = $state(false);
/* Al llegar aquí ya no hay barra con la que pararlo. */
$effect(() => {
if (reproductor.sonando) reproductor.pausar();
});
function alternar(): void {
if (!audio) return;
if (sonando) {
audio.pause();
return;
}
// Una cosa cada vez: lo que hubiera en la barra se calla.
if (reproductor.sonando) reproductor.pausar();
void audio.play();
if (sonando) audio.pause();
else void audio.play();
}
/*
* El otro lado del trato: si desde la barra vuelven a dar al play, esto se
* calla. `untrack` no hace falta —solo se lee `reproductor.sonando`—, pero sí
* la guarda de `sonando`, o el efecto pausaría un audio ya parado en cada
* cambio del reproductor.
*/
$effect(() => {
if (reproductor.sonando & & sonando) audio?.pause();
});
< / script >
< Meta { titulo } sinIndexar />
< div class = "container error" class:sola = { ! lamina } >
{ #if lamina }
< figure class = "lamina" >
< img
src="/errores/{ lamina } .webp"
alt={ caso === 'perdida'
? 'Una chica sentada junto a la funda de una guitarra, mirando hacia arriba, bajo un 404 dorado.'
: 'Un 500 dorado sobre el fondo marino, entre líneas y notas sueltas.'}
width="1200"
height="1364"
fetchpriority="high"
/>
< / figure >
{ /if }
< div
class="pantalla"
class:con-lamina={ Boolean ( lamina )}
style={ lamina
? `--fondo: ${ FONDOS [ lamina ]} ; --banda: ${ BANDA } ;` +
` --lamina: url('/errores/${ lamina } .webp');` +
` --lamina-alta: url('/errores/${ lamina } -vertical.webp')`
: undefined}
>
< div class = "dicho" >
< p class = "codigo muted" > { page . status } </ p >
<!--
El código solo cuando no hay lámina. Con ella, el «404» ya está escrito
en letras de un palmo a la izquierda, y repetirlo en pequeño al lado no
añade nada.
-->
{ #if ! lamina }
< p class = "codigo" > { page . status } </ p >
{ : else }
< span class = "filete" aria-hidden = "true" > < / span >
{ /if }
< h1 > { encabezado } </ h1 >
{ #if detalle }
< p class = "detalle" > { detalle } </ p >
@ -160,7 +153,7 @@
< Icono nombre = { sonando ? 'pausa' : 'play' } tamano= { 18 } />
{ sonando ? 'Parar' : 'Poner música' }
< / button >
< p class = "mientras muted ">
< p class = "mientras ">
{ caso === 'perdida' ? 'Ya que has llegado hasta aquí.' : 'Mientras lo arreglamos.' }
< / p >
< / div >
@ -170,50 +163,101 @@
< style >
/*
* La lámina es vertical (0,88:1) y manda el alto; el texto se centra a su
* lado en vez de colgar de arriba, que con una imagen tan alta dejaría el
* bloque flotando en la esquina.
*
* El bloque se limita a 64rem y se centra en vez de estirarse a lo ancho del
* contenedor: las dos columnas suman justo eso —30rem de lámina y las 44ch
* del texto—, y dejarlas sueltas en un monitor ancho abriría un hueco a la
* derecha del texto que no hay con qué llenar.
* `100dvh` y no `100vh`: en el móvil, con la barra del navegador desplegada,
* `vh` cuenta una pantalla que no se ve entera y el bloque queda descolocado.
*/
.error {
.pantalla {
position: relative;
display: grid;
grid-template-columns: minmax(0, 30rem) minmax(0, 1fr);
gap: var(--sp-7);
align-items: center;
max-width: 64rem;
margin-inline: auto;
padding-block: var(--sp-7);
place-items: center;
min-height: 100dvh;
padding: var(--sp-6) var(--sp-5);
background: var(--c-bg);
}
/* Sin lámina —un 403, un 401— no hay dos columnas que repartir. */
.error.sola {
grid-template-columns: minmax(0, 1fr);
max-width: 44rem;
/*
* La lámina viene armonizada: el dibujo ocupa el 62,6 % de la izquierda y el
* resto es una banda lisa, que es el sitio del texto.
*
* No se recorta. Se pega a la izquierda con el alto exacto de la pantalla
* (`auto 100dvh`) y lo que sobre a la derecha se pinta del color de la propia
* banda, que es plano: en un monitor ancho la banda simplemente se alarga y
* no se ve dónde acaba la imagen. Recortar al ancho —`cover`— se comía la
* línea del dominio de arriba, y eso es parte del cartel.
*
* La columna del texto empieza donde empieza la banda. Como el dibujo mide
* 1,406 de ancho por cada 1 de alto y la banda arranca en su 62,6 %, ese
* punto está siempre a 0,88 × el alto de la pantalla, sea cual sea el ancho.
*/
.pantalla.con-lamina {
grid-template-columns: 88dvh 1fr;
place-items: center;
padding: 0;
background:
var(--lamina) left center / auto 100dvh no-repeat,
var(--banda);
}
.lamina {
margin: 0;
/*
* `justify-self: stretch` a propósito: en una rejilla, un elemento centrado se
* encoge a su contenido, y el titular se partía en cuatro renglones sueltos
* dentro de una banda que tenía sitio de sobra. Ocupa la banda y se centra por
* dentro con el margen automático.
*/
.pantalla.con-lamina .dicho {
grid-column: 2;
justify-self: stretch;
width: auto;
max-width: none;
padding: var(--sp-6) var(--sp-5);
}
.codigo {
margin-bottom: var(--sp-2);
font-size: var(--fs-sm);
font-variant-numeric: tabular-nums;
.dicho {
position: relative;
max-width: 46ch;
text-align: center;
}
.con-lamina .dicho > * {
max-width: 34rem;
margin-inline: auto;
}
.lamina img {
/*
* Un filete dorado, corto, encima del titular. Es el mismo gesto que el
* cartel hace arriba a la izquierda junto a «senzapaura.es»: sin él, la banda
* es texto suelto sobre un plano y no parece la otra mitad de la lámina.
*/
.filete {
display: block;
width: 100%;
height: auto;
border-radius: var(--radius-tarjeta);
width: 3.5rem;
height: 1px;
margin: 0 auto var(--sp-5);
background: var(--c-accent);
opacity: 0.7;
}
.dicho {
max-width: 44ch;
/*
* A la altura del cartel: al lado de un «404» de un palmo, un titular de
* cuerpo mediano parecía un pie de foto. Sube al mismo escalón que los de las
* páginas grandes del sitio.
*/
.con-lamina h1 {
font-size: var(--fs-3xl);
text-wrap: balance;
}
.con-lamina .detalle {
font-size: var(--fs-lg);
text-wrap: pretty;
}
.codigo {
margin-bottom: var(--sp-2);
font-size: var(--fs-sm);
font-variant-numeric: tabular-nums;
letter-spacing: 0.12em;
color: var(--c-text-muted);
}
h1 {
@ -228,15 +272,18 @@
.acciones {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--sp-3);
margin-top: var(--sp-5);
}
/* Separada de los botones de salir: esto no es una forma de irse. */
/* En fila. Apilados parecían una lista de opciones, no dos salidas. */
.con-lamina .acciones {
max-width: none;
}
.musica {
margin-top: var(--sp-7);
padding-top: var(--sp-5);
border-top: 1px solid var(--c-border);
margin-top: var(--sp-6);
}
.poner {
@ -261,6 +308,21 @@
.mientras {
margin: var(--sp-2) 0 0;
font-size: var(--fs-xs);
color: var(--c-text-muted);
}
/*
* Sobre la lámina el gris apagado del sitio se pierde: el fondo de aquí es
* mucho más oscuro que el del sitio y el contraste se cae.
*/
.con-lamina .codigo,
.con-lamina .detalle,
.con-lamina .mientras {
color: #c2cbdb;
}
.con-lamina .poner {
border-color: #4a5a76;
}
@media (prefers-reduced-motion: reduce) {
@ -270,28 +332,43 @@
}
/*
* Apiladas, la lámina se limita: a ancho completo en un móvil ocuparía la
* pantalla entera y el texto quedaría por debajo del pliegue, que es justo lo
* que hace falta leer.
* En vertical el reparto de arriba no vale: el cartel es apaisado y, al alto
* de un móvil, se quedaría en una franja del centro con el número cortado por
* los dos lados.
*
* Aquí se apila, y con la otra versión del archivo: solo el dibujo, sin la
* banda. Poner el cartel entero dejaba su banda como un rectángulo más claro
* pegado a la derecha de la imagen, sin relación con el panel de abajo.
*
* El panel va del color del borde del dibujo —`--fondo`, no `--banda`—, y por
* eso no se ve la juntura: lo que hay debajo de la imagen es exactamente el
* color con el que la imagen termina.
*
* El texto se baja con un `padding-top` de la altura que ocupa el dibujo —el
* ancho partido por su proporción, 0,880— porque una fila de rejilla no puede
* medirse contra el ancho del elemento.
*/
@media (max-width: 52rem) {
.error {
@media (max-aspect-ratio: 1.41 / 1), (max- width: 52rem) {
.pantalla.con-lamina {
grid-template-columns: minmax(0, 1fr);
gap: var(--sp-5);
justify-items: center;
text-align: center;
align-content: start;
padding: 0;
background:
var(--lamina-alta) top center / 100% auto no-repeat,
var(--fondo);
}
.lamina {
max-width: 20rem;
.pantalla.con-lamina .dicho {
grid-column: 1;
padding: calc(100vw / 0.88 + var(--sp-6)) var(--sp-5) var(--sp-6);
}
.acciones {
justify-content: center ;
.con-lamina .dicho > * {
max-width: 34rem ;
}
.musica {
width: 100% ;
.con-lamina h1 {
font-size: var(--fs-2xl) ;
}
}
< / style >