Skip to content

fix(ui): let a classed h3 own its appearance instead of the card-heading rule - #330

Merged
argszero merged 1 commit into
mainfrom
fix/wallet-hero-label-cascade
Sep 30, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/wallet-hero-label-cascade

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The card-heading rule .card h3 { font-size: 15px; margin-bottom: 14px; color: var(--text) } (specificity 0,1,1) outranks the component class .wallet-hero-label (0,1,0), so every declaration that class makes loses: the wallet page's balance caption has always rendered as an ordinary card heading — computed style 15px / var(--text) / 14px, while the class asks for 12px / var(--text-mute) / 0 (the prototype writes <h3 class="muted" style="font-size:12px"> at line 588). The class's margin-bottom: 0 — the property that pulls the big number up against its caption — never applied either.

A class whose every declaration loses is a class= that was never honoured. So the fix is the same invariant #329 applied to the field-label rules: guard the generic rule with :not([class]), and let a classed <h3> keep its own look.

Related Issue

(none)

Changes

The new rule (R3)

For every <element class=...> site in ui/index.html, the class must not have every declaration it makes beaten by a rule that names the element type without using the element's own classes. R3:

  • compares specificity and source order (the cascade);
  • counts only rules that really match — markup nesting is tracked, without which a single .page-head p produced 7 false dead classes;
  • skips @media rules and state rules such as [hidden] (a rule that only applies at one breakpoint does not make a class dead; a hidden element is not a component-style contest).

It is deliberately lexical: +/~ combinators, pseudo-classes, attribute selectors and !important are not modelled, and the judgement is a necessary condition (all declarations must lose), so it under-reports rather than producing false reds. The pre-existing R1/R2 rules are untouched.

Measured with jsdom over the whole document: the pre-fix tree has exactly one dead class site in the entire document — .wallet-hero-label on <h3> — and the fixed tree has zero, while the other 27 .card h3 headings stay 15px / var(--text) / 14px in both trees.

Tests

  • cargo test — 436 passed, 0 failed (baseline 435)
  • cargo fmt --check — clean
  • cargo clippy --all-targets -- -D warnings — clean
  • new/updated unit tests: the R3 axis assertion; a variant that reverts the guard and must report exactly one dead site while R1/R2 stay quiet; a synthetic test for ancestry, :not([class]) guards, @media, state rules and specificity.

Checklist

  • branch name follows the convention (fix/)
  • commit message uses Conventional Commits
  • single responsibility, minimal change

…ing rule

The card-heading rule `.card h3 { font-size: 15px; margin-bottom: 14px;
color: var(--text) }` (specificity 0,1,1) outranked the component class
`.wallet-hero-label` (0,1,0), so all three of that class's declarations
lost.  The wallet page's balance caption has therefore always rendered as
an ordinary card heading — computed style before the fix: `15px /
var(--text) / 14px`, while the class asks for `12px / var(--text-mute) /
0` (the prototype writes `<h3 class="muted" style="font-size:12px">` at
line 588).  The class's `margin-bottom: 0` — the thing that pulls the big
number up against its caption — never applied either.  A whole class
whose every declaration loses is a `class=` that was never honoured.

Fix: guard the generic card-heading rule with `:not([class])`, so a h3
that carries a class keeps its own look and the generic rule serves only
unclassed headings — the same invariant #329 applied to the field-label
rules.

The gate in src/layout_gate.rs grows a third rule (R3) that makes this
shape derivable rather than a one-off: for every `<element class=...>`
site in ui/index.html, the class must not have *every* declaration it
makes be beaten by a rule that names the element type without using the
element's own classes.  R3 compares specificity and source order, counts
only rules that really match (markup nesting is tracked — without it a
single `.page-head p` produced 7 false dead classes), and skips `@media`
rules and state rules such as `[hidden]`.

Measured with jsdom over the whole document: the pre-fix tree has exactly
one dead class site in the entire document (`.wallet-hero-label` on
`<h3>`), the fixed tree has zero, and the other 27 `.card h3` headings are
unchanged at `15px / var(--text) / 14px`.
@argszero
argszero force-pushed the fix/wallet-hero-label-cascade branch from dd084db to 5a8129e Compare September 30, 2026 02:15
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (the author is also the committer on this repo, allow_self_merge: true, so this is left as an ordinary comment rather than a formal review).

What I re-verified on the pushed head 5a8129e:

  • The defect is real and reachable. ui/index.html has exactly one site where a class loses every declaration it makes: <h3 class="wallet-hero-label">点数余额</h3> inside .card.wallet-hero. .card h3 (0,1,1) beats .wallet-hero-label (0,1,0), so the caption's font-size:12px / color:var(--text-mute) / margin-bottom:0 never applied — computed style before the fix was 15px / var(--text) / 14px, i.e. identical to an unclassed .card h3. A whole class with no effect is a class= the markup never honoured.
  • A/B on the gate. Temporarily reverting only ui/css/style.css to upstream/main while keeping the new gate makes layout_gate::tests::an_element_appearance_comes_from_its_own_class fail with exactly R3:…[".wallet-hero-label on <h3>"], plus the variant test's base-line assertion. With the fix in place the layout gate is 7/7 and the other 27 .card h3 headings are unchanged at 15px / var(--text) / 14px.
  • The fix keeps the invariant, not the instance. :not([class]) on .card h3 is the same idiom fix(ui): let a classed label own its box instead of the generic field-label rule #329 applied to the two generic field-label rules; a classed <h3> now owns its own look, and R3 makes the shape derivable so the next dead class is caught rather than this one patched.
  • Full suite + linters on the pushed tree: cargo test 436/0 (baseline on main is 435), cargo fmt --check clean, cargo clippy --all-targets -- -D warnings clean.
  • Honest scope. The gate is lexical: it decides who owns the appearance and whether a class left anything behind; it does not render pixels and does not model !important, inline styles or full cascade semantics. The pixel half lives in a jsdom computed-style probe (kept outside the repo, and named as such — no instrument filenames are cited in-tree).

One note on process, in case a reviewer wonders about the extra file: besides ui/css/style.css and src/layout_gate.rs, the commit moves the style.css?v= cache-bust token in ui/index.html from 20260924-3 to -4, because the stylesheet changed. Nothing else in the markup changed.

@argszero
argszero merged commit 4973e4a into main Sep 30, 2026
2 checks passed
@argszero
argszero deleted the fix/wallet-hero-label-cascade branch September 30, 2026 02:18
@argszero argszero mentioned this pull request Sep 30, 2026
12 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant