fix(header): keep open Tiles and Split pressed in the boxed header styles

#538's Compact and Tiles header styles give every header icon button a box
through `html[data-header-stats] .header-right > .btn-icon-header` (0,3,1,
hover 0,4,1), which sets background, border and colour. That outranks the
open-state accent of #561's Tiles button (`.tiles-open`, 0,3,0) and of the
Split button (`.split-open`), hover included, so under the default Tiles
style an open grid or split looked exactly like a closed one and nothing
said the next click would close it.

The boxed styles now restate the pressed look for both buttons, hover
included (accent fill, accent border, accent ink), as comma-grouped
selectors built on the box rule's own selector so they always outrank it.
Classic is untouched and keeps the buttons' own rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-08 21:36:04 +02:00
parent 42147d30c0
commit bd3c368512
2 changed files with 51 additions and 0 deletions
+16
View File
@@ -21354,6 +21354,22 @@ html[data-header-stats='compact'] .header-right > .btn-icon-header:hover {
transform: none; transform: none;
} }
/* An open grid or split keeps its pressed look in the box: the rules above
outrank the buttons' own accent (.tiles-open, .split-open), and that accent
is the only thing saying the next click closes the grid or the split. */
html[data-header-stats='tiles'] .header-right > .btn-icon-header.btn-tile-grid.tiles-open,
html[data-header-stats='tiles'] .header-right > .btn-icon-header.btn-tile-grid.tiles-open:hover,
html[data-header-stats='tiles'] .header-right > .btn-icon-header.btn-split.split-open,
html[data-header-stats='tiles'] .header-right > .btn-icon-header.btn-split.split-open:hover,
html[data-header-stats='compact'] .header-right > .btn-icon-header.btn-tile-grid.tiles-open,
html[data-header-stats='compact'] .header-right > .btn-icon-header.btn-tile-grid.tiles-open:hover,
html[data-header-stats='compact'] .header-right > .btn-icon-header.btn-split.split-open,
html[data-header-stats='compact'] .header-right > .btn-icon-header.btn-split.split-open:hover {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-ink);
}
/* Before the first plan reading the chip holds a bare em dash; give it a tile /* Before the first plan reading the chip holds a bare em dash; give it a tile
of its own so it does not read as a stray mark between the tiles. */ of its own so it does not read as a stray mark between the tiles. */
html[data-header-stats='tiles'] .header-plan-usage:not(:has(.pu-row)) { html[data-header-stats='tiles'] .header-plan-usage:not(:has(.pu-row)) {
+35
View File
@@ -23,6 +23,7 @@
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import postcss from 'postcss';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const PUBLIC = join(process.cwd(), 'src/web/public'); const PUBLIC = join(process.cwd(), 'src/web/public');
@@ -225,6 +226,40 @@ describe('header stats wiring (static)', () => {
expect(css).toMatch(/html\[data-header-stats='tiles'\] \.header-right > \.btn-icon-header > svg \{\s*width: 18px;/); expect(css).toMatch(/html\[data-header-stats='tiles'\] \.header-right > \.btn-icon-header > svg \{\s*width: 18px;/);
}); });
it('keeps the open Tiles and Split buttons pressed (accent) in the boxed styles, hover included', () => {
// The box rule (html[data-header-stats] .header-right > .btn-icon-header)
// outranks the buttons' own open-state accent, so nothing would show that
// the next click closes the grid or the split.
const found = new Map<string, Record<string, string>>();
postcss.parse(css).walkRules((rule) => {
const decls: Record<string, string> = {};
rule.walkDecls((d) => {
decls[d.prop] = d.value;
});
for (const sel of rule.selectors) found.set(sel, { ...found.get(sel), ...decls });
});
const missing: string[] = [];
for (const style of ['tiles', 'compact']) {
for (const open of ['btn-tile-grid.tiles-open', 'btn-split.split-open']) {
for (const hover of ['', ':hover']) {
// Starting from the box rule's own selector keeps it the more specific one.
const sel = `html[data-header-stats='${style}'] .header-right > .btn-icon-header.${open}${hover}`;
const d = found.get(sel);
if (
d?.background !== 'var(--accent)' ||
d['border-color'] !== 'var(--accent)' ||
d.color !== 'var(--accent-ink)'
)
missing.push(sel);
}
}
}
expect(missing).toEqual([]);
// Classic keeps the buttons' own rules: nothing scoped to it touches them.
expect([...found.keys()].filter((s) => s.includes("'classic'") && /tiles-open|split-open/.test(s))).toEqual([]);
expect([...found.keys()].filter((s) => /(tiles|split)-open/.test(s) && s.includes(':is('))).toEqual([]);
});
it('stamps data-header-stats before first paint, tiles by default and classic on narrow screens', () => { it('stamps data-header-stats before first paint, tiles by default and classic on narrow screens', () => {
expect(INDEX).toContain( expect(INDEX).toContain(
"dataset.headerStats=(window.innerWidth<768||solo)?'classic':(H==='classic'||H==='compact')?H:'tiles'" "dataset.headerStats=(window.innerWidth<768||solo)?'classic':(H==='classic'||H==='compact')?H:'tiles'"