Skip to main content

max / makeover-webview

0.17.0: the depth classes are surfaces, not controls .raised carried hover, press, focus and disabled. A depth says how a surface sits, not what happens when a pointer arrives, so that left the vocabulary with no raised object that is merely an object, and an app wanting one had to take a control class and cancel the control half. goingson did exactly that in three variants over sixteen elements. Measured before changing it: .raised is emitted into goingson, Balanced Breakfast and the MNW server, and none of the three has a single call site, so this costs no migration anywhere. .card and .button are untouched. They are the same depth and controls, and their states come from surface_rules, which is where a state belongs.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 16:56 UTC
Signed with PGP, not checked
Commit: d8d8e0fbb0a9d501400c1c98bdc72131c61d7bd7
Parent: a7713fc
3 files changed, +62 insertions, -14 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.16.1"
3 + version = "0.17.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 README.md +9 -4
@@ -30,16 +30,21 @@
30 30
31 31 .raised { background: var(--surface-raised); box-shadow: var(--bevel-raised); }
32 32 .well { background: var(--surface-well, var(--surface-page)); box-shadow: var(--bevel-inset); }
33 - .raised:active { box-shadow: var(--bevel-inset); }
33 + .card:active { background: var(--surface-well, var(--surface-page)); box-shadow: var(--bevel-inset); }
34 34 ```
35 35
36 36 Those two custom properties are byte-identical to what both apps already
37 37 hand-write, which is asserted in the tests. Adoption removes duplicated lines
38 38 rather than changing a pixel.
39 39
40 - `.raised:active` is the one thing this renderer gets for free: the cascade
41 - carries a pressed state that an immediate-mode renderer resolves per call site,
42 - eighteen of them in audiofiles.
40 + The depth classes are surfaces and nothing more. A raised object that is not
41 + pressable is spelled `.raised`, and the states belong to the named surfaces:
42 + `.card` and `.button` are that same depth *and* controls, so they carry hover,
43 + press, focus and disabled.
44 +
45 + `.card:active` is the one thing this renderer gets for free: the cascade carries
46 + a pressed state that an immediate-mode renderer resolves per call site, eighteen
47 + of them in audiofiles.
43 48
44 49 ## Phase B: form markup
45 50
M src/lib.rs +52 -9
@@ -155,6 +155,25 @@
155 155 //! the same magnitude with its sign off the depth. Both values are the measured
156 156 //! consensus rather than a new opinion.
157 157 //!
158 + //! # 0.17.0: the depth classes stop being controls
159 + //!
160 + //! [`depth_rules`] gave `.raised` the whole interactive set. A depth is a
161 + //! statement about shape, so that left the vocabulary with no raised surface
162 + //! that is merely an object, and an app wanting one had two moves: write its
163 + //! own class from tokens, or take a control class and cancel the control half.
164 + //! goingson took the second, in three variants over sixteen elements
165 + //! (`.card--static` at 14 call sites, `.card--muted` at 2, `.card--shell` at 1),
166 + //! each re-asserting the resting fill and bevel on `:hover` and `:active`.
167 + //!
168 + //! Measured before changing it: `.raised` is emitted into goingson, Balanced
169 + //! Breakfast and the MNW server, and none of the three has a single call site.
170 + //! The states were unasked-for everywhere at once, and dropping them costs no
171 + //! migration anywhere.
172 + //!
173 + //! `.card` and `.button` are unchanged. They are the same depth *and* controls,
174 + //! and they take their states from [`surface_rules`], which is where a state
175 + //! belongs: on the thing that claims to answer a pointer.
176 + //!
158 177 //! # Substitution, three ways
159 178 //!
160 179 //! `Fill::Well` has no colour on makeover before 2.3.0, and each renderer
@@ -482,9 +501,19 @@
482 501
483 502 /// One rule per depth: its fill and its edge, together.
484 503 ///
485 - /// A pressed rule rides along with the raised one, because the cascade can
486 - /// carry a state that an immediate-mode renderer has to resolve per call site.
487 - /// That is the one thing this renderer gets for free and the others do not.
504 + /// A depth and nothing else. `.raised` says a surface sits on what is behind
505 + /// it, which is a statement about the shape and not about what happens when a
506 + /// pointer arrives, so it emits no hover, press, focus or disabled rule. The
507 + /// named surfaces are where interaction lives: `.card` and `.button` are the
508 + /// same depth *and* controls, and they get their states from
509 + /// [`surface_rules`].
510 + ///
511 + /// This class carried the interactive set until 0.17.0, which left the
512 + /// vocabulary with no raised surface that is merely an object. Consumers that
513 + /// needed one took a control class and cancelled half of it instead: sixteen
514 + /// elements in goingson across three `.card--*` variants, each re-asserting the
515 + /// resting fill and bevel on `:hover` and `:active`. Nothing anywhere used
516 + /// `.raised` itself, so the states were unasked-for in every consumer at once.
488 517 #[must_use]
489 518 pub fn depth_rules(opts: &Emit) -> String {
490 519 let mut css = String::new();
@@ -494,9 +523,6 @@
494 523 };
495 524 css.push_str(&depth_rule(&class, depth));
496 525 }
497 - if let Some(raised) = depth_class(Depth::Raised, opts) {
498 - css.push_str(&interactive_rules(&raised, Depth::Raised, opts));
499 - }
500 526 css
501 527 }
502 528
@@ -832,10 +858,27 @@
832 858
833 859 #[test]
834 860 fn the_cascade_carries_the_pressed_state() {
835 - let css = depth_rules(&Emit::default());
861 + let css = surface_rules(&Emit::default());
836 862 // The one thing this renderer gets free that the other two resolve by
837 - // hand, eighteen call sites deep in audiofiles' case.
838 - assert!(css.contains(".raised:active {"));
863 + // hand, eighteen call sites deep in audiofiles' case. Asserted on a
864 + // named surface: pressing belongs to the control, not to the depth.
865 + assert!(css.contains(".card:active {"));
866 + assert!(css.contains(".button:active {"));
867 + }
868 +
869 + #[test]
870 + fn the_depth_class_is_a_surface_and_not_a_control() {
871 + let css = depth_rules(&Emit::default());
872 + // The static surface the vocabulary was missing. Sixteen goingson
873 + // elements wore .card and cancelled its hover and press to get this,
874 + // because a raised object that is not pressable had no other spelling.
875 + for state in [":hover", ":active", ":focus-visible", ":disabled"] {
876 + assert!(
877 + !css.contains(&format!(".raised{state}")),
878 + "the depth class claimed {state}: {css}"
879 + );
880 + }
881 + assert!(css.contains("var(--bevel-raised)"), "still raised: {css}");
839 882 }
840 883
841 884 #[test]