fix(dashboard): #1858 a badge-shaped button drops the UA chrome that ignores the theme - #1951
Conversation
…ignores the theme A badge renders as a span, an anchor or a button, and only the button arrives with UA chrome: Chromium paints it with the ButtonFace system colour and an outset border, neither of which follows the theme. In dark, badge-outline's --text-muted on that fill measures 2.67:1 against WCAG AA's 4.5:1, so the "OS updates" badge alone was a light grey box in a dark header. The reset goes on .badge itself, above the variants: those selectors all tie on specificity, so source order lets .badge-outline re-add its border and every coloured variant re-add its fill, while the button's UA fill and border are gone. With the fill cleared the badge sits on --bg: 6.15:1 dark, 6.11:1 light. #59's button.upgrade-btn was the single-site version of this same reset, so it generalises to button.badge, which now carries only the pointer cursor -- the one reset that must not reach a span or anchor badge. The upgrade-btn class was a hook for that rule alone and is dropped from the markup with it. The class sweep found four badge-shaped buttons, not the one the issue names: osupdate.mjs (the reported site), configview.mjs's upgrade button, and two in components.mjs -- including the "not adopted" button from #1857, which carries the same badge-outline pair and so had the same 2.67:1 defect. A class-level reset reaches all of them; a fix at the named site would not have. Not done: the issue suggests `font: inherit`, which is wrong here. button.badge out-specifies .version-badge, so the shorthand would strip monospace from the version badges. The button-vs-span font difference is left alone. Tier: frontend unit (node), the same tier and idiom as #1232's contrast test. The WCAG helpers move to tests/frontend/helpers/contrast.mjs so #1232 and #1858 share one implementation instead of two copies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
F1: the extraction left half its own duplication behind. workerview.test.mjs still re-spelled the dark and light theme-block regexes four times instead of importing DARK_BLOCK / LIGHT_BLOCK, which helpers/contrast.mjs already exports and which badgebutton.test.mjs already imports. Two copies of a regex are exactly what the extraction was for, so it takes the constants now. F2: the test that reads --text-muted on --bg does not read the third palette -- the prefers-color-scheme auto block, whose tokens are byte-identical to the light block's today. Named in the test's own comment rather than looped over, so a future divergence there is a known gap rather than a silent one. The budget row for workerview.test.mjs goes 488 -> 494: biome wraps the five-name import (the single-line form is over lineWidth 100). Still a downward move from the 518 the base carries, which is what check_monotonic compares against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
Non-author review: PASS at
|
Non-author review: PASS at
|
| # | mutation | result |
|---|---|---|
| M1 | drop background: transparent from .badge |
caught — reddens the reset test |
| M2 | drop border: none from .badge |
caught — reddens the reset test |
| M3 | move the .badge rule below the variants |
caught — reddens the source-order test |
| M4 | drop badge from osupdate.mjs:392's class string |
SURVIVED — 583 pass, 0 fail |
M4 is the finding. The fourth test exists to catch "a variant used without .badge silently misses the fix". Its scan reads the literal text of class= attributes, so for class=${"badge version-badge ml-2" + attention} it captures only badge version-badge ml-2 — which contains no variant, so the element is skipped before the assertion is reached. Removing badge from that string leaves version-badge ml-2 badge-outline on a <button>: the UA fill comes back and the dark-theme contrast returns to the value in the issue. Every test stays green.
That element is not an edge case — it is the element #1858 was reported on. The one site whose variant arrives by concatenation is the one site the guard cannot see.
The comment is also wrong, and that is the half I would fix first. badgebutton.test.mjs says of that path: "that path is covered by the source-order test above". It is not — the source-order test asserts declaration order within the stylesheet and says nothing about which classes any element carries. A reader who trusts that sentence will believe the gap is closed. A half-true coverage note reads as checked.
Why this is non-blocking: the shipped behaviour is correct today, M1-M3 prove the reset itself is guarded, and this is a future-regression hole rather than a live defect. Cheap to close if you want it in this PR — either widen the scan to also split the concatenated tail, or assert directly that the OS-update button's class string contains badge. Your call; I am not moving the head or voiding this pass over it. If you would rather not touch a passed head, say so and I will file it.
What I did not check
Shell tests (shellcheck + pithead suite)was stillin_progressat my read (19 success, 2 skipped by design, that one running). This PR touches no shell surface, but I am not calling a running context green — whoever merges re-reads the head and its contexts in the merging call, which is the standing rule for every row on the merge list.- The
prefers-color-schemeauto block is out of scope here and the test says so itself; its tokens are byte-identical to the light block's today, so a divergence there would pass unnoticed. Noted, not charged to this PR.
|
Condition closed: MERGE-READY: PASS at Standing caveat, not specific to this PR: re-read the head and its contexts in the call that merges. A pass is a measurement of one sha, and a head can move minutes after a list is published. |
Closes #1858.
What the operator sees
In the dark theme, the OS updates control in the dashboard header stops being a light grey box and reads as a badge like the ones beside it. Its label goes from 2.67:1 to 6.15:1 against the header background — WCAG AA is 4.5:1. The light theme already passed (5.32:1) and improves to 6.11:1. Nothing else about the control changes; the same badge now also gets a pointer cursor on hover.
Mechanism
A badge renders as a
<span>, an<a>or a<button>, and only the<button>arrives with UA chrome: Chromium paints it with theButtonFacesystem colour (#efefef) and an outset border, neither of which follows our theme..badgeset nobackground, and.badge-outlinesets only a border andcolor: var(--text-muted)— so that one badge alone painted a light box in a dark header.The reset goes on
.badgeitself, above the variants..badgeand every.badge-*are single-class selectors, so specificity ties and source order decides:.badge-outlinere-adds its border and each coloured variant re-adds its fill, while the button's UA fill and border are gone. With the fill cleared the badge sits on--bg.#59'sbutton.upgrade-btnwas the single-site version of this same reset, so it generalises tobutton.badge, which now carries onlycursor: pointer— the one reset that must not reach a<span>/<a>badge.upgrade-btnwas a class hook for that rule alone, so it is dropped from the markup with it.The issue names one site; the class sweep found four
osupdate.mjs(the reported site),configview.mjs's upgrade button, and two incomponents.mjs— including the "not adopted" button from #1857, which carries the samebadge badge-outlinepair and therefore had the same 2.67:1 defect. That site is on #1948's branch, not here, so it is not visible in this diff; a class-level reset covers it the moment #1948 merges, where a fix at the named site would not have.What I did not do
font: inherit. That is wrong here and is not applied:button.badgeout-specifies.version-badge, so the shorthand would strip monospace from the version badges. The<button>-vs-<span>font-family difference is left alone.docs/dashboard.md:1292anddocs/appliance.md:343describe the OS-update control functionally; neither states a colour, so neither goes stale.border: noneis not strictly required by the contrast defect — it is what makesbutton.upgrade-btnredundant, and it also clears the UA outset border oncomponents.mjs's Inspect button. Called out so a reviewer can veto it.Tier and evidence
Frontend unit (node), the same tier and idiom as #1232's contrast test. The WCAG helpers move to
tests/frontend/helpers/contrast.mjsso #1232 and #1858 share one implementation instead of two copies; that shrinksworkerview.test.mjs518 -> 488.Every one of the four new tests was shown able to fail, each on its own seeded control, each reddening only its own test:
badgeclass--text-muted.badge-outlinemoved above.badgeBoth seeded files were restored and verified by
sha256sum -c.RAN at this head, each rc captured to a file rather than off a pipeline tail:
node --test dashboard/tests/frontend/— 583 pass, 0 fail, rc 0make test-dashboard— 2572 passed, rc 0make test-fakes— 31 passed, rc 0make lint-py— rc 0lint-docs-voice lint-operator-strings lint-topology lint-file-budget lint-trivy-parity lint-js lint-yaml lint-md lint-proto lint-tomllint-jscaught the first form of thebutton.badgecomment (biome moves a comment placed after{onto its own line, which would have broken line-neutrality); the comment sits on the declaration line instead.Shared file
docs/dev/file-budget.tsv:dashboard/mining_dashboard/web/static/dashboard.csswas at 1579/1579 with zero headroom, so the change is line-neutral by construction — the two resets on.badge(+2) are paid for by generalising#59's five-line block (-5) into a three-linebutton.badge. The file is still 1579. The only row edited isworkerview.test.mjs518 -> 488, lowered because this diff shrank that file; an un-rebased branch touching it may red until it rebases.Over-engineering pass (by hand, on merits)
.badgebelow the variants would silently blank every coloured badge. Kept..badge. It asserts its own needle matched at least once, so it cannot pass vacuously. Kept.badgerule exists anywhere indashboard.cssbefore.badge,version-devhas exactly one producer (a<span>, always paired withbadge-outline), and the wizard markup uses no badges — so nothing else can be blanked by the reset.MERGE-READY: not yet — needs a recorded non-author PASS. I do not merge my own.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR