Skip to main content

max / makeover-webview

0.25.0: a table cell says what it holds, so a control in one is not painted as text 0.23.0 gave a table its layout and left every cell the same. One `.cell` for the whole thing, so a cell holding text and a cell holding a button took one class and one content colour. That is exactly the drift RowPart::intent has prevented for list rows since 0.2.0, unprevented for tables. makeover-layout 0.14.0 names the four parts and this emits them: .cell-value, .cell-tokens, .cell-actions, .cell-link. Only the value takes a colour. A token carries its own tone, an action is a control rather than text, and a link takes the action colour from the anchor it is, so pinning any of the three would be louder than saying nothing -- the reasoning row_rules already carries, reached per part for its own reason. The colour goes on .cell-value and never on .cell. On the container it cascades into the three parts that are not text, which is the bug written as one rule. Asserted both ways. list::Cell::part is Option<CellPart> where it was Option<RowPart>. That field was the drift with a type on it: a table cell borrowing the list row's vocabulary because the table side had none. No in-tree consumer sets it -- quasi-webview builds its cells through Cell::new -- so the retype costs no migration. The rules are emitted as a skip-list rather than a match on Value, so a member added upstream lands with its intent rather than silently without.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-11 18:21 UTC
Signed with PGP, not checked
Commit: f8f1ab5266b10d8e1b66e19894e8c6950fc3d16c
Parent: 2a95c2a
3 files changed, +134 insertions, -14 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.24.0"
3 + version = "0.25.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"
@@ -12,7 +12,7 @@
12 12 # patch satisfy the requirement and still fail to compile. That happened once
13 13 # with `form::radio_html` calling `FieldKind::Radio`, and makeover-build is where
14 14 # it surfaced, one release later.
15 - makeover-layout = "0.13.0"
15 + makeover-layout = "0.14.0"
16 16 # The capability axis. `makeover-touch` decides whether a hover rule should be
17 17 # gated at all; `makeover-geometry` spells the gate as a media condition. Both
18 18 # answers are owned elsewhere and neither is re-derived here.
M src/lib.rs +94 -2
@@ -159,6 +159,29 @@
159 159 //! the same magnitude with its sign off the depth. Both values are the measured
160 160 //! consensus rather than a new opinion.
161 161 //!
162 + //! # 0.25.0: a cell says what it holds
163 + //!
164 + //! 0.23.0 gave a table its layout and left every cell the same. One `.cell`
165 + //! carried the whole thing, so a cell holding text and a cell holding a button
166 + //! were one class and one content colour, and a control in a cell was painted
167 + //! as text. That is the drift [`RowPart::intent`](makeover_layout::RowPart)
168 + //! has prevented for list rows since 0.2.0 and prevented for nothing here.
169 + //!
170 + //! makeover-layout 0.14.0's [`CellPart`](makeover_layout::CellPart) names the
171 + //! four things a cell holds, and [`table_rules`] turns them into
172 + //! `.cell-value`, `.cell-tokens`, `.cell-actions` and `.cell-link`. Only the
173 + //! first takes a colour: a token carries its own tone, an action is a control
174 + //! rather than text, and a link takes the action colour from the anchor it is.
175 + //!
176 + //! The colour going on `.cell-value` rather than on `.cell` is the fix rather
177 + //! than an implementation detail. On the container it cascades into the parts
178 + //! that are not text, which is the bug said in one rule.
179 + //!
180 + //! [`list::Cell::part`] is `Option<CellPart>` here, where it was
181 + //! `Option<RowPart>`. A table cell borrowing the list row's vocabulary was the
182 + //! drift with a type on it: the two answer different questions, and only one of
183 + //! them was ever about a cell.
184 + //!
162 185 //! # 0.23.0: a table lays itself out, and a row shows its controls
163 186 //!
164 187 //! Three things a description could say and this renderer had no rule for,
@@ -237,7 +260,7 @@
237 260 pub mod meter;
238 261 pub mod placeholder;
239 262
240 - use crate::list::part_class;
263 + use crate::list::{cell_part_class, part_class};
241 264 use makeover_geometry::{Density, SizeClass};
242 265 // Re-exported rather than redefined. An app assembling its own stylesheet out
243 266 // of this crate's pieces needs the same layer name, and most such apps depend
@@ -245,7 +268,9 @@
245 268 // `tables.css` in its own build.rs from [`list::narrowing_css`], and those
246 269 // rules are as generated as the ones here.
247 270 pub use makeover_geometry::{CSS_LAYER, in_css_layer};
248 - use makeover_layout::{Bevel, Depth, Fill, Intent, RowPart, Selector, State, Token, Tone};
271 + use makeover_layout::{
272 + Bevel, CellPart, Depth, Fill, Intent, RowPart, Selector, State, Token, Tone,
273 + };
249 274 use makeover_touch::Affordance;
250 275 use std::fmt::Write as _;
251 276
@@ -1027,6 +1052,42 @@
1027 1052 ));
1028 1053 }
1029 1054
1055 + // What is inside a cell, which the table side could not say until
1056 + // makeover-layout 0.14.0. Every cell was one `.cell` and one content
1057 + // colour, so a button in a cell was painted as text -- the drift
1058 + // `RowPart::intent` has prevented for list rows since 0.2.0 and prevented
1059 + // for nothing here.
1060 + //
1061 + // The colour goes on `.cell-value` rather than on `.cell`, and that
1062 + // placement is the whole fix. On the container it would cascade into the
1063 + // tokens and the controls sitting beside the text, which is the bug said
1064 + // in one rule; on the part that is text, it reaches text and stops.
1065 + for part in [
1066 + CellPart::Value,
1067 + CellPart::Tokens,
1068 + CellPart::Actions,
1069 + CellPart::Link,
1070 + ] {
1071 + // Three of the four inherit, each for its own reason: a token carries
1072 + // its own tone, an action is a control rather than text, and a link
1073 + // takes the action colour from the anchor it is. `CellPart::intent`
1074 + // says so by answering with the intent inheriting already gives, and
1075 + // pinning that would be louder than saying nothing.
1076 + //
1077 + // Written as a skip-list rather than as a match on Value, so a member
1078 + // added upstream gets its intent emitted rather than being silently
1079 + // dropped. That is the same trade `part_class`'s fallback makes: land
1080 + // plainly, never land as nothing.
1081 + if !matches!(part, CellPart::Tokens | CellPart::Actions | CellPart::Link) {
1082 + let _ = writeln!(
1083 + css,
1084 + ".{} {{\n color: var(--{});\n}}",
1085 + class(cell_part_class(part), opts),
1086 + part.intent()
1087 + );
1088 + }
1089 + }
1090 +
1030 1091 css
1031 1092 }
1032 1093
@@ -1710,6 +1771,37 @@
1710 1771 assert!(!css.contains("grid-template-columns"), "{css}");
1711 1772 }
1712 1773
1774 + #[test]
1775 + fn a_control_in_a_cell_is_not_painted_as_text() {
1776 + // The point of makeover-layout 0.14.0's CellPart, and the table-side
1777 + // twin of `the_three_text_parts_take_their_intents_and_actions_inherits`
1778 + // above. One `.cell` and one content colour meant a button in a cell
1779 + // inherited it.
1780 + let css = table_rules(&Emit::default());
1781 +
1782 + assert!(
1783 + css.contains(".cell-value {\n color: var(--content);"),
1784 + "{css}"
1785 + );
1786 + assert!(!css.contains(".cell-actions {\n color:"), "{css}");
1787 + assert!(!css.contains(".cell-tokens {\n color:"), "{css}");
1788 + assert!(!css.contains(".cell-link {\n color:"), "{css}");
1789 +
1790 + // The colour is on the part that is text, never on the container. On
1791 + // `.cell` it would cascade into the three parts that are not text,
1792 + // which is the bug written as one rule.
1793 + assert!(!css.contains(".cell {\n color:"), "{css}");
1794 + }
1795 +
1796 + #[test]
1797 + fn an_unknown_cell_part_renders_plainly_rather_than_failing_to_build() {
1798 + // `part_class`'s obligation, taken on for the table side too. CellPart
1799 + // is `#[non_exhaustive]`, so a member added upstream must land as a
1800 + // bare class rather than as a build that stops.
1801 + assert_eq!(cell_part_class(CellPart::Value), "cell-value");
1802 + assert_eq!(cell_part_class(CellPart::Actions), "cell-actions");
1803 + }
1804 +
1713 1805 #[test]
1714 1806 fn a_column_drops_by_its_priority_and_never_by_its_position() {
1715 1807 let css = component_rules(&Emit::default());
M src/list.rs +38 -10
@@ -32,7 +32,7 @@
32 32
33 33 use crate::form::Markup;
34 34 use crate::{Emit, class};
35 - use makeover_layout::{Column, Priority, RowPart, Width};
35 + use makeover_layout::{CellPart, Column, Priority, RowPart, Width};
36 36 use std::fmt::Write as _;
37 37
38 38 /// The lengths the description deferred.
@@ -214,18 +214,29 @@
214 214 pub struct Cell<'a> {
215 215 /// Which column this fills, by name.
216 216 pub column: &'a str,
217 - /// What kind of text it is, when it is text.
217 + /// What the cell holds, when the whole cell is one thing.
218 218 ///
219 - /// Carries the row-part class the stylesheet half already emits, so a
220 - /// secondary cell says it is secondary in the description's own words
221 - /// rather than in the app's.
222 - pub part: Option<RowPart>,
219 + /// Carries the cell-part class the stylesheet half emits, so a cell that is
220 + /// nothing but controls says so in the description's own words rather than
221 + /// in the app's.
222 + ///
223 + /// This was `Option<RowPart>` until 0.25.0, which was the drift
224 + /// `makeover-layout` 0.14.0 named: a table cell borrowing the list row's
225 + /// vocabulary, because the table side had none. A row's parts answer a
226 + /// different question (which of six emphases this run of text takes) from a
227 + /// cell's (whether this is text, tokens, controls or a link).
228 + ///
229 + /// `None` for a cell mixing parts. A cell holding a value *and* a strip of
230 + /// tokens *and* a control is three parts in one container, and each one
231 + /// wears its own class inside — this field is for the single-part case,
232 + /// where a wrapper span would say nothing the cell has not already said.
233 + pub part: Option<CellPart>,
223 234 /// The contents. Trusted app markup.
224 235 pub content: Markup<'a>,
225 236 }
226 237
227 238 impl<'a> Cell<'a> {
228 - /// A cell with no row part.
239 + /// A cell with no cell part.
229 240 #[must_use]
230 241 pub const fn new(column: &'a str, content: Markup<'a>) -> Self {
231 242 Self {
@@ -260,6 +271,23 @@
260 271 }
261 272 }
262 273
274 + /// The class for a cell part.
275 + ///
276 + /// [`part_class`]'s table half, added with `makeover-layout` 0.14.0's
277 + /// [`CellPart`]. The fallback is there for the same reason and costs the same
278 + /// thing: a member added upstream lands as a bare `cell-part` with no rule of
279 + /// 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 {
282 + match part {
283 + CellPart::Value => "cell-value",
284 + CellPart::Tokens => "cell-tokens",
285 + CellPart::Actions => "cell-actions",
286 + CellPart::Link => "cell-link",
287 + _ => "cell-part",
288 + }
289 + }
290 +
263 291 /// A row's cells, in column order.
264 292 ///
265 293 /// Ordered by the columns and not by the cells, so a row cannot silently
@@ -293,7 +321,7 @@
293 321 let found = cells.iter().find(|cell| cell.column == column.name);
294 322 let mut classes = format!("{cell_class} {}", column_classes(column, opts));
295 323 if let Some(part) = found.and_then(|cell| cell.part) {
296 - let _ = write!(classes, " {}", class(part_class(part), opts));
324 + let _ = write!(classes, " {}", class(cell_part_class(part), opts));
297 325 }
298 326 let _ = write!(
299 327 html,
@@ -424,7 +452,7 @@
424 452 let cells = [
425 453 Cell {
426 454 column: "due",
427 - part: Some(RowPart::Meta),
455 + part: Some(CellPart::Value),
428 456 content: Markup("tomorrow"),
429 457 },
430 458 Cell::new("description", Markup("<span>Ship it</span>")),
@@ -444,7 +472,7 @@
444 472 "{html}"
445 473 );
446 474 assert!(
447 - html.contains(r#"<div class="cell col-due cell-fixed cell-drops-next row-meta">"#),
475 + html.contains(r#"<div class="cell col-due cell-fixed cell-drops-next cell-value">"#),
448 476 "{html}"
449 477 );
450 478 // progress had no cell, so it is present and empty rather than absent,