Skip to main content

max / makeover-webview

0.13.0: gate the row-action hide, not only its reveal 0.10.0 put the hover reveal inside the capability query and left the hide outside it. That is an incomplete capability answer rather than a deliberate one: on a device with no hover the actions were hidden with nothing to bring them back, and only :focus-within could reach them. Both webview apps had already patched it the same way, which is the evidence that the answer belongs here. goingson undoes the hide in its touch block and Balanced Breakfast in its own, and the header comment written yesterday claiming "the app owes those rows another way in" was an assumption, not a measurement. The measurement says both consumers reached the same answer, and that answer is not to hide at all. A primitive that owns the hiding owes the answer for the device that cannot unhide. So the hide moves inside the query with the reveal, and on touch the rule simply never applies. Found by adopting into goingson, which is the second thing that adoption has sent back this session.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-02 17:11 UTC
Signed with PGP, not checked
Commit: dbaf590d35429041526e73f808cb3207670c98b8
Parent: f432213
2 files changed, +41 insertions, -9 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.12.0"
3 + version = "0.13.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 +40 -8
@@ -660,10 +660,18 @@
660 660 // `pointer-events` rides along because opacity leaves the hit area
661 661 // behind: without it a renderer with no hover carries an invisible
662 662 // tappable control. Keyboard focus is unaffected by it.
663 - let _ = writeln!(
664 - css,
665 - ".{c} {{\n opacity: 0;\n pointer-events: none;\n}}"
666 - );
663 + // The hide is gated too, which it was not in 0.10.0, and that was
664 + // an incomplete capability answer rather than a deliberate one.
665 + // Ungated, a fingertip got actions hidden with no hover to bring
666 + // them back, so both webview apps hand-wrote the same
667 + // `opacity: 1` to undo it: goingson in its touch block and
668 + // Balanced Breakfast in its own. A primitive that owns the hiding
669 + // owes the answer for the device that cannot unhide, and the
670 + // answer both consumers already reached is not to hide at all.
671 + css.push_str(&gated(
672 + hover_condition(),
673 + &format!(".{c} {{\n opacity: 0;\n pointer-events: none;\n}}\n"),
674 + ));
667 675
668 676 // The two halves split here, where they used to be one selector
669 677 // list. Hover-to-reveal is the literal case `Affordance::Hover`
@@ -1028,6 +1036,22 @@
1028 1036 };
1029 1037 let hover = indent(".row:hover .row-actions");
1030 1038 let keyboard = indent(".row:focus-within .row-actions");
1039 + // The hide is gated with the reveal. Ungated it leaves a fingertip
1040 + // with actions it cannot bring back, which is what both webview apps
1041 + // were undoing by hand.
1042 + let hide = css
1043 + .lines()
1044 + .position(|l| l.trim() == ".row-actions {")
1045 + .expect("hide rule");
1046 + let query = css
1047 + .lines()
1048 + .take(hide)
1049 + .enumerate()
1050 + .filter(|(_, l)| l.trim_start().starts_with("@media"))
1051 + .map(|(i, _)| i)
1052 + .last()
1053 + .expect("a query precedes it");
1054 + assert!(hide - query < 3, "the hide is not inside the query");
1031 1055 assert!(
1032 1056 hover > keyboard,
1033 1057 "hover reveal must be nested inside the capability query and the \
@@ -1152,7 +1176,7 @@
1152 1176 #[test]
1153 1177 fn row_actions_are_revealed_without_moving_the_row() {
1154 1178 let css = row_rules(&Emit::default());
1155 - assert!(css.contains(".row-actions {\n opacity: 0;"));
1179 + assert!(css.contains(".row-actions {\n opacity: 0;"));
1156 1180 // Not display:none, which would reflow the row under the pointer.
1157 1181 assert!(!css.contains("display: none"));
1158 1182 // Hover alone would lock the keyboard out.
@@ -1166,10 +1190,18 @@
1166 1190 // `visibility: hidden` takes the actions out of the focus order, so the
1167 1191 // `focus-within` reveal above could never fire from an action itself.
1168 1192 assert!(!css.contains("visibility:"));
1169 - // Opacity leaves the hit area behind; the row must not carry an
1170 - // invisible tappable control where there is no hover to reveal it.
1171 - assert!(css.contains(".row-actions {\n opacity: 0;\n pointer-events: none;\n}"));
1193 + // Opacity leaves the hit area behind, so the pair travels together or
1194 + // the row carries an invisible tappable control.
1195 + assert!(css.contains(".row-actions {\n opacity: 0;\n pointer-events: none;\n }"));
1172 1196 assert!(css.contains("opacity: 1;\n pointer-events: auto;"));
1197 +
1198 + // And on a device with no hover the row carries no such control at
1199 + // all, because the hide never applies there. That is what the comment
1200 + // above used to be asking for and could not get: the hide is inside
1201 + // the capability query with the reveal.
1202 + let hide = css.find(".row-actions {").expect("hide rule");
1203 + let query = css[..hide].rfind("@media").expect("a query precedes it");
1204 + assert!(css[query..hide].find('}').is_none(), "the hide escaped the query");
1173 1205 }
1174 1206
1175 1207 #[test]