La barra de música es una pieza, y su dorado es el de los botones

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) <noreply@anthropic.com>
master
dev 4 weeks ago
parent 91bfd48c0d
commit 62a8d79866

@ -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();

@ -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;
}
}

@ -70,23 +70,39 @@
</p>
</header>
<!--
Una sola pieza y no dos cajas con un hueco entre ellas: buscar y elegir cómo
se ve son el mismo gesto —acotar lo que hay en pantalla— y presentarlos
separados los hacía parecer dos controles sin relación. Por eso el borde es de
la barra y dentro no hay más línea que la que separa el campo de los botones.
-->
<div class="barra">
<div class="buscador">
<label class="visually-hidden" for="buscar-tema">Buscar un tema</label>
<input
id="buscar-tema"
type="search"
bind:value={consulta}
placeholder="Buscar por título, estilo, disco, año o etiqueta"
autocomplete="off"
/>
</div>
<label class="visually-hidden" for="buscar-tema">Buscar un tema</label>
<input
id="buscar-tema"
type="search"
bind:value={consulta}
placeholder="Buscar por título, estilo, disco, año o etiqueta"
autocomplete="off"
/>
<div class="vistas" role="group" aria-label="Cómo ver la música">
<button type="button" aria-pressed={vista === 'tarjetas'} onclick={() => (vista = 'tarjetas')}>
<button
type="button"
class="vista"
class:oro={vista === 'tarjetas'}
aria-pressed={vista === 'tarjetas'}
onclick={() => (vista = 'tarjetas')}
>
Tarjetas
</button>
<button type="button" aria-pressed={vista === 'tabla'} onclick={() => (vista = 'tabla')}>
<button
type="button"
class="vista"
class:oro={vista === 'tabla'}
aria-pressed={vista === 'tabla'}
onclick={() => (vista = 'tabla')}
>
Tabla
</button>
</div>
@ -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) {
}
</style>

Loading…
Cancel
Save

Powered by TurnKey Linux.