From 62a8d7986670619bd036c6f50c610a9ca49c1b5e Mon Sep 17 00:00:00 2001
From: dev
Date: Tue, 8 Sep 2026 19:14:48 +0200
Subject: [PATCH] =?UTF-8?q?La=20barra=20de=20m=C3=BAsica=20es=20una=20piez?=
=?UTF-8?q?a,=20y=20su=20dorado=20es=20el=20de=20los=20botones?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Eran dos cajas con un hueco entre ellas: el campo de búsqueda por un
lado y el conmutador de vista por otro, cada uno con su canto. Buscar y
elegir cómo se ve son el mismo gesto —acotar lo que hay en pantalla— y
separados parecían dos controles sin relación. Ahora el borde es de la
barra y dentro no hay más línea que la que separa lo que se escribe de
lo que se pulsa; el foco lo acusa la pieza entera.
Y el activo llevaba un dorado plano al lado de un botón dorado de
verdad, así que se notaba que no era el mismo. El aspecto sale de
`.btn--primary` a una clase `.oro` que comparten los dos: degradado,
filo de luz arriba y el barrido al pasar el puntero. Copiarlo habría
sido garantizar que un día uno de los dos se quedara atrás.
Dos cosas que aparecieron al montarlo:
- Svelte añade su clase de ámbito a todo lo que escribe en el
componente, así que un `.vista` de la página le gana por especificidad
al `.oro` general y el botón activo se quedaba con la letra gris
encima del oro. Va con `:not(.oro)`.
- En columna, `flex-basis` mide el alto: las doce rem que dan ancho al
campo lo convertían, apilado en un móvil, en un cajón de casi
doscientos píxeles.
Co-Authored-By: Claude Opus 5 (1M context)
---
e2e/musica.e2e.ts | 68 ++++++++++++++++
src/app.css | 23 +++++-
src/routes/musica/+page.svelte | 138 ++++++++++++++++++++++++---------
3 files changed, 187 insertions(+), 42 deletions(-)
diff --git a/e2e/musica.e2e.ts b/e2e/musica.e2e.ts
index 34ca3d6..e0eeb96 100644
--- a/e2e/musica.e2e.ts
+++ b/e2e/musica.e2e.ts
@@ -10,6 +10,74 @@ test.describe('Página de música', () => {
await expect(lateral.getByRole('link', { name: /Clásica/ })).toBeVisible();
});
+ test('buscar y elegir vista son una sola pieza', async ({ page }) => {
+ /*
+ * Eran dos cajas con un hueco entre ellas y parecían dos controles sin
+ * relación. El borde es de la barra: dentro, el campo y los botones no
+ * tienen canto propio y llegan al mismo alto.
+ */
+ await page.setViewportSize({ width: 1100, height: 800 });
+ await page.goto('/musica');
+
+ const medidas = await page.evaluate(() => {
+ const campo = document.querySelector('input[type="search"]')!;
+ const activo = document.querySelector('.vista.oro')!;
+ const barra = document.querySelector('.barra')!.getBoundingClientRect();
+ return {
+ bordeDelCampo: getComputedStyle(campo).borderTopWidth,
+ altoCampo: Math.round(campo.getBoundingClientRect().height),
+ altoBoton: Math.round(activo.getBoundingClientRect().height),
+ altoBarra: Math.round(barra.height),
+ dentro: activo.getBoundingClientRect().right <= barra.right + 1
+ };
+ });
+
+ expect(medidas.bordeDelCampo).toBe('0px');
+ expect(medidas.altoCampo).toBe(medidas.altoBoton);
+ // Sin hueco: la barra mide lo que miden sus piezas, más su propio canto.
+ expect(medidas.altoBarra).toBeLessThanOrEqual(medidas.altoCampo + 2);
+ expect(medidas.dentro).toBe(true);
+ });
+
+ test('la vista activa lleva el oro de los botones, no un color plano', async ({ page }) => {
+ await page.goto('/musica');
+
+ const oro = page.locator('.vista.oro');
+ await expect(oro).toHaveText('Tarjetas');
+
+ // El degradado es lo que se pedía: al lado hay un botón dorado de verdad
+ // y se notaba que el conmutador no era el mismo oro.
+ const pintura = await oro.evaluate((e) => {
+ const s = getComputedStyle(e);
+ return { imagen: s.backgroundImage, filo: s.boxShadow, color: s.color };
+ });
+ expect(pintura.imagen).toContain('linear-gradient');
+ expect(pintura.filo).toContain('inset');
+
+ // Y la letra es la del botón dorado, no el gris del inactivo: el `.vista`
+ // de la página le ganaba por especificidad al `.oro` general.
+ const inactivo = await page
+ .getByRole('button', { name: 'Tabla' })
+ .evaluate((e) => getComputedStyle(e).color);
+ expect(pintura.color).not.toBe(inactivo);
+
+ // Y cambia con la vista.
+ await page.getByRole('button', { name: 'Tabla' }).click();
+ await expect(page.locator('.vista.oro')).toHaveText('Tabla');
+ });
+
+ test('en un móvil la barra se apila sin estirar el campo', async ({ page }) => {
+ // En columna, `flex-basis` mide el alto: las doce rem que dan ancho al
+ // campo lo convertían en un cajón de doscientos píxeles.
+ await page.setViewportSize({ width: 360, height: 800 });
+ await page.goto('/musica');
+
+ const alto = await page
+ .locator('input[type="search"]')
+ .evaluate((e) => Math.round(e.getBoundingClientRect().height));
+ expect(alto).toBeLessThan(60);
+ });
+
test('la tabla ordena por la columna que se pulsa', async ({ page }) => {
await page.goto('/musica');
await page.getByRole('button', { name: 'Tabla' }).click();
diff --git a/src/app.css b/src/app.css
index 62a1286..a102913 100644
--- a/src/app.css
+++ b/src/app.css
@@ -379,14 +379,22 @@ button {
color: inherit;
}
-.btn--primary {
+/*
+ * El aspecto dorado, aparte del botón que lo estrenó.
+ *
+ * Lo lleva también el conmutador de vista de la página de música, que no es un
+ * `.btn` —va dentro de una barra y no puede tener canto ni esquinas propias—
+ * pero sí tiene que ser el mismo oro. Copiarlo era garantizar que un día uno de
+ * los dos se quedara plano.
+ */
+.btn--primary,
+.oro {
/* El color plano queda de reserva: si el degradado no se pintara, el botón
sigue siendo dorado y legible, no transparente. */
background-color: var(--c-accent-superficie);
background-image: var(--oro);
background-size: 100% 240%;
background-position: 0 22%;
- border-color: var(--c-accent-sombra);
color: var(--c-accent-text);
/* El filo de luz de arriba. Sin él, el degradado se ve pintado, no biselado. */
box-shadow: inset 0 1px 0 color-mix(in oklab, white 45%, transparent);
@@ -395,12 +403,18 @@ button {
border-color 0.2s var(--suave);
}
+/* El canto sí es del botón: dentro de una barra no hay borde que teñir. */
+.btn--primary {
+ border-color: var(--c-accent-sombra);
+}
+
/*
* Al acercar el puntero, la luz recorre la pieza. El color de fondo se repite
* aquí porque `.btn:hover` lo cambia por el gris de los botones normales: sin
* esto, un dorado que no llegara a pintar el degradado se vería oscuro.
*/
-.btn--primary:hover {
+.btn--primary:hover,
+.oro:hover {
background-color: var(--c-accent-superficie);
background-position: 0 78%;
border-color: var(--c-accent);
@@ -414,7 +428,8 @@ button {
}
@media (prefers-reduced-motion: reduce) {
- .btn--primary {
+ .btn--primary,
+ .oro {
transition: none;
}
}
diff --git a/src/routes/musica/+page.svelte b/src/routes/musica/+page.svelte
index d363ea9..e9dc7ad 100644
--- a/src/routes/musica/+page.svelte
+++ b/src/routes/musica/+page.svelte
@@ -70,23 +70,39 @@
+
-
-
-
-
+
+
-
@@ -134,63 +150,109 @@
.barra {
display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: var(--sp-3);
+ 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;
}
- .buscador {
- flex: 1 1 18rem;
+ /*
+ * 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'] {
- width: 100%;
+ flex: 1 1 12rem;
+ min-width: 0;
padding: var(--boton-relleno);
- border: 1px solid var(--c-border);
- border-radius: var(--radius-boton);
- background: var(--c-bg-sunken);
+ border: 0;
+ background: transparent;
color: inherit;
font: inherit;
font-size: var(--boton-texto);
}
input[type='search']:focus-visible {
- outline: 2px solid var(--c-accent);
- outline-offset: 1px;
+ outline: none;
}
.vistas {
display: flex;
- border: 1px solid var(--c-border);
- border-radius: var(--radius-boton);
- overflow: hidden;
+ /* 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);
}
- .vistas button {
+ .vista {
padding: var(--boton-relleno);
- border: none;
- background: transparent;
- color: var(--c-text-muted);
+ 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);
}
- .vistas button:hover:not([aria-pressed='true']) {
- background: var(--c-bg-raised);
+ /*
+ * 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);
}
- .vistas button[aria-pressed='true'] {
- background: var(--c-accent-superficie);
- color: var(--c-accent-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);
}
-
- @media (prefers-reduced-motion: reduce) {
- }