Skip to main content

max / makeover-webview

0.27.0: a renderer can ask for the class the rules key off quasi-webview spelled the selector classes `tabs`, `segmented` and `option`, and put `toggle` on the group rather than on the buttons in it, so every described selector rendered flat and a toggle group took the bevel meant for its options. It could not have known better: the mapping was three string literals inside `selector_rules`. So export what a screen renderer has to agree with. `option_class` is the new one and `selector_rules` now reads from it, which is what makes the two halves one fact. `class`, `part_class` and `cell_part_class` were already the agreement and were private, leaving quasi-webview to carry copies -- an identical prefix helper, and a second spelling of a list whose own doc comment says to grep it on upgrade.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-12 00:51 UTC
Signed with PGP, not checked
Commit: 1751c2379ff698bd7597df3dcbe7e95149789268
Parent: ceed741
3 files changed, +62 insertions, -11 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.26.0"
3 + version = "0.27.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 +48 -7
@@ -403,10 +403,40 @@
403 403 }
404 404
405 405 /// A prefixed class name.
406 - fn class(name: &str, opts: &Emit) -> String {
406 + ///
407 + /// Public since 0.27.0, for the renderers that emit markup this crate does not.
408 + /// A screen renderer writing `class="row"` has to prefix it the way the
409 + /// stylesheet half does or a prefixed app gets rules matching everything except
410 + /// the elements that renderer wrote, and the failure is invisible: the CSS
411 + /// stays valid and one element is unstyled. quasi-webview carried a byte
412 + /// identical copy of this function until it could call this one.
413 + #[must_use]
414 + pub fn class(name: &str, opts: &Emit) -> String {
407 415 format!("{}{name}", opts.class_prefix)
408 416 }
409 417
418 + /// The class an option of a selector carries, which is what the rules key off.
419 + ///
420 + /// Named for the option and not for the group: [`selector_rules`] styles the
421 + /// thing that gets picked, so `Selector::Tabs` is `tab` and not `tabs`. The
422 + /// distinction is not pedantry. quasi-webview spelled these `tabs`, `segmented`
423 + /// and `option`, put `toggle` on the wrapping element rather than on the
424 + /// buttons inside it, and every described selector in that renderer came out
425 + /// with no depth, no focus ring and no chosen state, while the toggle group got
426 + /// a bevel meant for its buttons.
427 + ///
428 + /// The chosen option additionally carries `chosen`, the same way a latched chip
429 + /// carries `latched`. That name is this crate's too; there is no reason for a
430 + /// caller to spell it, and [`selector_rules`] is where it is written down.
431 + #[must_use]
432 + pub fn option_class(selector: Selector) -> &'static str {
433 + match selector {
434 + Selector::Tabs => "tab",
435 + Selector::Segmented => "segment",
436 + Selector::Toggle => "toggle",
437 + }
438 + }
439 +
410 440 /// The fill and edge declarations for a depth, as a rule body.
411 441 ///
412 442 /// Empty for [`Depth::Flat`], which has neither and inherits what it sits on.
@@ -738,12 +768,8 @@
738 768 /// strip hand-writing the recess that makes its chosen tab read as forward.
739 769 fn selector_rules(opts: &Emit) -> String {
740 770 let mut css = String::new();
741 - for (selector, name) in [
742 - (Selector::Tabs, "tab"),
743 - (Selector::Segmented, "segment"),
744 - (Selector::Toggle, "toggle"),
745 - ] {
746 - let c = class(name, opts);
771 + for selector in [Selector::Tabs, Selector::Segmented, Selector::Toggle] {
772 + let c = class(option_class(selector), opts);
747 773 css.push_str(&depth_rule(&c, selector.unchosen()));
748 774 css.push_str(&interactive_rules(&c, selector.unchosen(), opts));
749 775 css.push_str(&depth_rule(&format!("{c}.chosen"), selector.chosen()));
@@ -1738,6 +1764,21 @@
1738 1764 assert!(!css.contains(".badge {"));
1739 1765 }
1740 1766
1767 + #[test]
1768 + fn the_class_a_renderer_puts_on_an_option_is_the_one_the_rules_key_off() {
1769 + // `option_class` is the contract a screen renderer writes markup
1770 + // against, and the rules below are the other half of it. They come off
1771 + // one mapping now, so this asserts the mapping is the one that reaches
1772 + // the stylesheet rather than that two lists still agree.
1773 + let css = stylesheet(&Emit::default());
1774 + for selector in [Selector::Tabs, Selector::Segmented, Selector::Toggle] {
1775 + let name = option_class(selector);
1776 + assert!(css.contains(&format!(".{name} {{")), "{name}: {css}");
1777 + assert!(css.contains(&format!(".{name}.chosen {{")), "{name}: {css}");
1778 + }
1779 + assert_eq!(option_class(Selector::Tabs), "tab");
1780 + }
1781 +
1741 1782 #[test]
1742 1783 fn a_destructive_button_has_somewhere_for_its_tone_to_land() {
1743 1784 let css = component_rules(&Emit::default());
M src/list.rs +13 -3
@@ -259,7 +259,15 @@
259 259 /// bare `row-part` with no rule of its own, which is a thing rendering plainly
260 260 /// rather than a build that stops. Grep this function when adopting a new
261 261 /// makeover-layout.
262 - pub(crate) fn part_class(part: RowPart) -> &'static str {
262 + ///
263 + /// Public since 0.27.0. A row's parts are emitted by whoever builds the row
264 + /// element, and that is not always this crate: `cells_html` emits a table's
265 + /// cells, but a list row carries the app's identity and hooks, so a screen
266 + /// renderer writes it. quasi-webview wrote this list out a second time to do
267 + /// that, which made the obligation in the paragraph above land on a function
268 + /// its author would not think to grep.
269 + #[must_use]
270 + pub fn part_class(part: RowPart) -> &'static str {
263 271 match part {
264 272 RowPart::Primary => "row-primary",
265 273 RowPart::Secondary => "row-secondary",
@@ -277,8 +285,10 @@
277 285 /// [`CellPart`]. The fallback is there for the same reason and costs the same
278 286 /// thing: a member added upstream lands as a bare `cell-part` with no rule of
279 287 /// its own, rather than as a build that stops. Grep this function too when
280 - /// adopting a new makeover-layout.
281 - pub(crate) fn cell_part_class(part: CellPart) -> &'static str {
288 + /// adopting a new makeover-layout, and public since 0.27.0 for the reason
289 + /// [`part_class`] is.
290 + #[must_use]
291 + pub fn cell_part_class(part: CellPart) -> &'static str {
282 292 match part {
283 293 CellPart::Value => "cell-value",
284 294 CellPart::Tokens => "cell-tokens",