Skip to main content

max / makeover-webview

Key the invalid field on aria-invalid, and keep the well under it Release 0.5.0. Two corrections, both found by adopting into goingson, and goingson was right on both counts. The invalid state keys on [aria-invalid="true"] rather than on a .invalid class. One fact, read by the visual state and the accessible state alike, so they cannot drift; a class is a second place to forget. GO already drove its invalid styling this way and can now delete its own rules rather than ignore ours. The danger ring composes after the bevel instead of replacing it. box-shadow is not additive, so the lone ring this emitted before now silently dropped the well out from under an invalid field -- the fill and the edge disagreeing, which is the one thing this stack exists to prevent.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-29 23:34 UTC
Signed with PGP, not checked
Commit: 661de6e7ef743d31e88bcf7dc4f924506b9c9277
Parent: 483d805
2 files changed, +38 insertions, -8 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.4.0"
3 + version = "0.5.0"
4 4 edition = "2024"
5 5 description = "The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser."
6 6 license = "MIT"
M src/lib.rs +37 -7
@@ -261,13 +261,21 @@
261 261
262 262 let field = class("field", opts);
263 263 css.push_str(&depth_rule(&field, Depth::Well));
264 - // `Field::invalid` marks the whole group rather than only the message, so
265 - // the ring goes on the control. Flat, not a two-tone bevel: this edge is
266 - // saying "wrong", and lighting one side of it would have it say "raised"
267 - // at the same time.
264 +
265 + // Keyed on the ARIA attribute rather than on a class, so the visual state
266 + // and the accessible state cannot drift apart: there is one fact and both
267 + // read it. goingson already drove its invalid styling this way and was
268 + // right to; the `.invalid` class this emitted before 0.5.0 was a second
269 + // place to forget.
270 + //
271 + // The ring composes *after* the bevel rather than replacing it. box-shadow
272 + // is not additive, so a lone ring silently dropped the well out from under
273 + // an invalid field. Flat and unlit: this edge is saying "wrong", and
274 + // lighting one side would have it say "raised" at the same time.
268 275 let _ = writeln!(
269 276 css,
270 - ".{field}.invalid {{\n box-shadow: inset 0 0 0 {} var(--danger);\n}}",
277 + ".{field}[aria-invalid=\"true\"] {{\n box-shadow: var({}), 0 0 0 {} var(--danger);\n}}",
278 + bevel_var(Bevel::Inset),
271 279 opts.border_width
272 280 );
273 281 css
@@ -728,10 +736,32 @@
728 736 fn an_invalid_field_is_ringed_without_being_lit() {
729 737 let css = surface_rules(&Emit::default());
730 738 assert!(css.contains(".field {"));
731 - assert!(css.contains(".field.invalid {"));
739 + // The ARIA attribute, not a class: one fact, read by both the visual
740 + // and the accessible state, so they cannot drift.
741 + assert!(css.contains(".field[aria-invalid=\"true\"] {"));
742 + assert!(!css.contains(".field.invalid"));
732 743 // A flat ring: this edge says "wrong", and a two-tone bevel would have
733 744 // it say "raised" at the same time.
734 - assert!(css.contains("inset 0 0 0 1px var(--danger)"));
745 + assert!(css.contains("0 0 0 1px var(--danger)"));
746 + }
747 +
748 + #[test]
749 + fn an_invalid_field_keeps_the_well_underneath_it() {
750 + // box-shadow is not additive. A lone ring replaces the bevel and drops
751 + // the well out from under the field, which is what this emitted before
752 + // 0.5.0 and is the whole reason the rule composes.
753 + let css = surface_rules(&Emit::default());
754 + let invalid = css
755 + .lines()
756 + .skip_while(|l| !l.starts_with(".field[aria-invalid"))
757 + .take_while(|l| !l.starts_with('}'))
758 + .collect::<Vec<_>>()
759 + .join("\n");
760 + assert!(
761 + invalid.contains("var(--bevel-inset)"),
762 + "the well was dropped: {invalid}"
763 + );
764 + assert!(invalid.contains("var(--danger)"));
735 765 }
736 766
737 767 #[test]