fix(ui): let a classed label own its box instead of the generic field-label rule - #329
Merged
Merged
Conversation
…-label rule
The generic field-label rule `.form label { display:flex; flex-direction:column }`
(specificity 0,1,1) outranked the component layer (0,1,0) for every label that
carries a component class, so the layout the class asked for never applied:
- `.check-line` — the settings notification checkboxes and the table-density
radios rendered stacked (control above its caption, centred). #157 renamed the
markup from `class="check"` to the prototype-canonical `.check-line` that #154
introduced, but left the stylesheet's `.form label.check` rule behind; that
rule was the only thing turning a checkbox label back into a row, and it
stopped matching anything.
- `.chip` — the share-form weekday chips were pills; #32 turned them into
`<label>`s, and the `display:inline-flex` #154 gave them was overridden.
Fix: guard the generic field-label rules with `:not([class])`, so a classed label
takes its box from its own class — the component layer wins as intended. Also
drop the now-dead `.density-options .check` rule (#157 renamed that markup too)
and give `.checkbox, .checkbox-line` back their own `display:flex`, which they
used to borrow from the generic rule.
Adds `src/layout_gate.rs` (test-only, compiled out of release builds, zero new
dependencies) with two derived rules: every class used on a `<label>` in
ui/index.html must declare its own `display`, and any rule that targets a bare
`label` and imposes a layout must carry a guard excluding classed elements.
Measured with jsdom over the whole document on both trees: check-line and chip
go from column to row, while the unclassed field labels stay column in both.
Owner
Author
|
✅ Self-review (committer, Checked:
Scope, stated honestly: the gate is lexical. It proves who may give a label its box, not what the browser paints. That half is the jsdom table in the description plus the headless-Chrome screenshots taken during recon. |
11 tasks done
argszero
added a commit
that referenced
this pull request
Sep 30, 2026
…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
added a commit
that referenced
this pull request
Sep 30, 2026
…ing rule (#330) 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`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
.form label { display: flex; flex-direction: column; … }was unconditional. At specificity (0,1,1) it outranked the component layer (0,1,0) for every<label>carrying a component class, so the box the class asked for never applied — lower specificity loses regardless of source order.Two shapes are hit, from two different origins but one root cause:
.check-line09e4121) renamed the markup fromclass="check"to the prototype-canonical.check-linethat #154 (75dcb2c) introduced — but left the stylesheet's.form label.check(0,2,1) behind. That rule was the only thing turning a checkbox row back to a row, and it stopped matching anything..chipd31641d) turned the chips from<button>into<label>; thedisplay:inline-flex#154 gave them was overridden by the generic rule..checkbox-line(login "remember me") only survived because #61 addedflex-direction: row !important; ironically the prototype-canonical.check-linehas no such protection.Related Issue
Changes
ui/css/style.css: guard the two generic field-label rules with:not([class])— a classed label takes its box from its own class, and the generic rule now serves only unclassed field labels.ui/css/style.css: drop the now-dead.density-options .checkrule (feat(ui): redesign settings/admin/ops views on the landed component layer #157 renamed that markup too) and give.checkbox, .checkbox-lineback their owndisplay: flex, which they used to borrow from the generic rule.src/layout_gate.rs(new,#[cfg(test)], zero new dependencies): two derived rules so this cannot silently recur.src/main.rs: register the gate (#[cfg(test)] mod layout_gate;).The gate
Not the two classes, but the shape they share:
ui/index.html) — every class used on a<label>must have a stylesheet rule that targets it and declaresdisplay. A label's box must come from its class, never borrowed from a generic rule.labeland that declaresdisplay/flex-directionmust carry a guard excluding classed elements (:not([class])).Together they are the invariant: a generic field-label rule serves only labels without a class.
Expected values are entirely derived (roster from
ui/index.html, rules fromui/css/style.css), so no snapshot is written and the next component class is covered on arrival.Rejected alternatives (each only plugs today's two holes): add
!importantto.check-line; add a.form label.Xrow override per victim; only delete the dead.form label.check.Tests
cargo test— 435 passed / 0 failed (baseline 429; +6 from the new gate)cargo fmt --check— cleancargo clippy --all-targets -- -D warnings— cleanA/B (same tree, only
ui/css/style.cssswapped forupstream/main's):Second instrument — jsdom, computed style per shape (whole document, both trees):
.form label.check-lineflex/columnflex/rowlabel.chipflex/columninline-flex/rowlabel.checkboxflex/rowflex/rowlabel.checkbox-lineflex/rowflex/row.form label:not([class])flex/columnflex/column.login-form label:not([class])flex/columnflex/columnThe fix changes exactly the classed labels; the unclassed field labels are untouched in both trees.
Scope (stated honestly): the gate is lexical — it proves who is allowed to give a label its box, not the painted layout. That half is covered by the jsdom probe above and by headless-Chrome before/after screenshots.
Checklist
fix/…)