mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(header): keep the header stats styles out of the glyph motion
#538's Compact and Tiles header styles carried two glyph rules of their own: a `transform var(--transition-smooth)` transition on every header button's svg, and `.btn-settings:hover > svg { rotate(45deg) }`. #561 had meanwhile dropped the global button rotate and moved the hover motion onto the glyphs (gear turn, folder, Tiles squares) behind `(hover: hover)` and a prefers-reduced-motion off switch. The #538 rules out-specified those guarded rules (they reach the glyph through `html[data-header-stats] .header-right`), so under the default Tiles style a reduced-motion user still saw the gear turn, a touch tap left it stuck at 45deg, and the 0.2s ease replaced #561's spring. Both rules go; #538 keeps only its glyph sizes, and the guarded motion now applies the same in all three header styles. The hover test scanned only selectors naming .btn-icon-header, which is how this slipped through: it now scans every rule that reaches a header glyph and pins the one spring transition and the hover guard. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21311,8 +21311,11 @@ html[data-header-stats='tiles'] .header-plan-usage .pu-win-idle {
|
||||
/* Bare 16px glyphs beside 36px tiles read as a different design. In Tiles the
|
||||
header's icon buttons take the tile box (same height, border and fill, a
|
||||
larger icon); in Compact they become round chips the height of the pills.
|
||||
Only the box changes: the marker classes still hide them, and the gear
|
||||
keeps its quarter turn on hover, on the glyph rather than the whole box. */
|
||||
Only the box changes: the marker classes still hide them, and the glyph
|
||||
motion (the gear's quarter turn, the folder, the Tiles squares) stays with
|
||||
the guarded rules beside .btn-icon-header:hover. A glyph transform or
|
||||
transition here would out-specify them and slip past their (hover: hover)
|
||||
and reduced-motion guards. */
|
||||
html[data-header-stats='tiles'] .header-right > .btn-icon-header,
|
||||
html[data-header-stats='compact'] .header-right > .btn-icon-header {
|
||||
box-sizing: border-box;
|
||||
@@ -21351,16 +21354,6 @@ html[data-header-stats='compact'] .header-right > .btn-icon-header:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
html[data-header-stats='tiles'] .header-right > .btn-icon-header > svg,
|
||||
html[data-header-stats='compact'] .header-right > .btn-icon-header > svg {
|
||||
transition: transform var(--transition-smooth);
|
||||
}
|
||||
|
||||
html[data-header-stats='tiles'] .header-right > .btn-settings:hover > svg,
|
||||
html[data-header-stats='compact'] .header-right > .btn-settings:hover > svg {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
html[data-header-stats='tiles'] .header-plan-usage:not(:has(.pu-row)) {
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
* 3. Those motions live inside `@media (hover: hover)`, so a tap on a touch
|
||||
* screen cannot leave an icon stuck mid-motion, and reduced motion turns the
|
||||
* transitions off.
|
||||
* 4. That holds for EVERY rule that reaches a header glyph, not only the ones
|
||||
* naming `.btn-icon-header`: the header stats styles (#538) restyle the
|
||||
* buttons through `.header-right > .btn-settings > svg`, and a rotate or a
|
||||
* transition there out-specified the guarded rules (a reduced-motion user
|
||||
* still saw the gear turn, a tap left it turned, and #561's spring was
|
||||
* replaced by a plain ease).
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -49,6 +55,15 @@ function rules(): FoundRule[] {
|
||||
|
||||
const all = rules();
|
||||
const iconRules = all.filter((r) => r.selector.includes('btn-icon-header'));
|
||||
/**
|
||||
* Every rule that reaches a header button's glyph, whatever names the button:
|
||||
* the class list, a per-button class, or the header's right side.
|
||||
*/
|
||||
const glyphRules = all.filter(
|
||||
(r) =>
|
||||
/btn-icon-header|\.header-right|btn-settings|btn-tile-grid|btn-file-viewer/.test(r.selector) &&
|
||||
/\bsvg\b/.test(r.selector)
|
||||
);
|
||||
|
||||
function buttonTag(cls: string): string {
|
||||
const i = html.indexOf(`class="btn-icon-header ${cls}`);
|
||||
@@ -123,3 +138,39 @@ describe('header icon hover', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('header glyph motion, whatever selector reaches it (header stats styles included)', () => {
|
||||
it('finds the header stats styles glyph rules (the scan is not vacuous)', () => {
|
||||
// Their 18px / 15px glyph sizes are reached through .header-right.
|
||||
const sized = glyphRules.filter((r) => r.selector.startsWith('html[data-header-stats=') && r.decls.width);
|
||||
expect(sized.map((r) => r.decls.width).sort()).toEqual(['15px', '18px']);
|
||||
});
|
||||
|
||||
it('turns a glyph on hover only inside the pointer guard', () => {
|
||||
const offenders = glyphRules.filter(
|
||||
(r) =>
|
||||
r.selector.includes(':hover') &&
|
||||
r.decls.transform &&
|
||||
r.decls.transform !== 'none' &&
|
||||
!r.media.includes('(hover: hover)')
|
||||
);
|
||||
expect(offenders.map((r) => `${r.selector} { transform: ${r.decls.transform} }`)).toEqual([]);
|
||||
const rotations = glyphRules.filter((r) => /rotate\(/.test(r.decls.transform || ''));
|
||||
expect(rotations.map((r) => r.selector)).toEqual(['.btn-icon-header.btn-settings:hover svg']);
|
||||
});
|
||||
|
||||
it('animates the glyphs with the one spring transition, which reduced motion turns off', () => {
|
||||
// A second transition on the same glyphs (a header-wide restyle, say)
|
||||
// would out-specify the spring and the reduced-motion `none` alike.
|
||||
const animated = glyphRules.filter((r) => 'transition' in r.decls || 'animation' in r.decls);
|
||||
const glyphs = [
|
||||
'.btn-icon-header.btn-settings svg',
|
||||
'.btn-icon-header.btn-tile-grid svg rect',
|
||||
'.btn-icon-header.btn-file-viewer svg .icon-folder-closed',
|
||||
'.btn-icon-header.btn-file-viewer svg .icon-folder-open',
|
||||
];
|
||||
expect(animated.map((r) => `${r.media.join(' ')} ${r.selector}`.trim()).sort()).toEqual(
|
||||
[...glyphs, ...glyphs.map((g) => `(prefers-reduced-motion: reduce) ${g}`)].sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user