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) { - }