Skip to main content

max / makeover-layout

0.13.0: a figure carries how its value moved, and revealed_on_hover retires
Author: Max Johnson <me@maxj.phd> · 2026-08-11 01:35 UTC
Signed with PGP, not checked
Commit: e5e9aa8cced64e3431f70d6433db904873d7f93d
Parent: 05b82fa
2 files changed, +46 insertions, -26 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.12.0"
3 + version = "0.13.0"
4 4 edition = "2024"
5 5 description = "The renderer-agnostic half of the make-family design system: what a thing IS, named as intents and relationships and never as values. Colour defers to makeover, spacing to makeover-geometry; what is left is composition."
6 6 license = "MIT"
M src/lib.rs +45 -25
@@ -684,19 +684,6 @@
684 684 }
685 685
686 686 impl RowPart {
687 - /// Whether the part stays hidden until the row is hovered or focused.
688 - ///
689 - /// Behaviour of the part, not app policy: Balanced Breakfast and goingson
690 - /// grew the same hover-reveal on their actions independently and neither
691 - /// applies it to anything else.
692 - ///
693 - /// A renderer with no hover shows it always. That is a renderer decision
694 - /// and this returning `true` does not forbid it.
695 - #[must_use]
696 - pub const fn revealed_on_hover(self) -> bool {
697 - matches!(self, Self::Actions)
698 - }
699 -
700 687 /// The content intent the part takes.
701 688 #[must_use]
702 689 pub const fn intent(self) -> &'static str {
@@ -1064,7 +1051,23 @@
1064 1051 pub value: &'a str,
1065 1052 /// What it counts. The caption under the value.
1066 1053 pub caption: &'a str,
1054 + /// How the value has moved, if the app is tracking that.
1055 + ///
1056 + /// Added 0.13.0. Text, for [`value`](Self::value)'s reason: only the app
1057 + /// knows whether a move reads as `+12.5%`, `+3` or `2x`, and a renderer
1058 + /// handed a number would have to guess.
1059 + ///
1060 + /// This is what [`tone`](Self::tone) was for and had no consumer of. The MNW
1061 + /// server has four screens whose stat card is a label, a value and a delta,
1062 + /// and the delta is the toned part: the figure itself is an ordinary fact
1063 + /// and it is the movement that reads as good or bad. Without this the delta
1064 + /// has to be folded into the caption, which loses the tone and reads as a
1065 + /// longer caption rather than as a second, smaller line.
1066 + pub change: Option<&'a str>,
1067 1067 /// What the figure means right now. [`Tone::Neutral`] is an ordinary fact.
1068 + ///
1069 + /// Applies to [`change`](Self::change) where there is one, since that is the
1070 + /// part that carries the judgement, and to the value where there is not.
1068 1071 pub tone: Tone,
1069 1072 }
1070 1073
@@ -1075,10 +1078,18 @@
1075 1078 Self {
1076 1079 value,
1077 1080 caption,
1081 + change: None,
1078 1082 tone: Tone::Neutral,
1079 1083 }
1080 1084 }
1081 1085
1086 + /// How the value has moved.
1087 + #[must_use]
1088 + pub const fn change(mut self, change: &'a str) -> Self {
1089 + self.change = Some(change);
1090 + self
1091 + }
1092 +
1082 1093 /// What the figure means.
1083 1094 #[must_use]
1084 1095 pub const fn tone(mut self, tone: Tone) -> Self {
@@ -1708,6 +1719,22 @@
1708 1719 assert_eq!(Figure::new("17", "Total").tone, Tone::Neutral);
1709 1720 }
1710 1721
1722 + #[test]
1723 + fn a_figures_change_is_the_toned_part_and_is_absent_by_default() {
1724 + // 0.13.0. The MNW server's stat card is a label, a value and a delta,
1725 + // across four screens, and the delta is what reads as good or bad. Tone
1726 + // had no consumer before this: the figure itself is an ordinary fact.
1727 + let views = Figure::new("1,204", "Views")
1728 + .change("+12.5%")
1729 + .tone(Tone::Success);
1730 + assert_eq!(views.change, Some("+12.5%"));
1731 + assert_eq!(views.tone, Tone::Success);
1732 +
1733 + // A figure with nothing to compare against says so by having no change,
1734 + // rather than by carrying an empty string a renderer has to test for.
1735 + assert_eq!(Figure::new("3.1%", "Conversion").change, None);
1736 + }
1737 +
1711 1738 #[test]
1712 1739 fn a_figures_value_is_text_because_only_the_app_knows_what_it_is() {
1713 1740 // "84%", "12/30", "3d". A figure is whatever the app computed, already
@@ -1723,7 +1750,6 @@
1723 1750 // The meter carries the tone, so a part-level intent underneath would
1724 1751 // fight it. Same answer `Tokens` needed, for the same reason.
1725 1752 assert_eq!(RowPart::Proportion.intent(), RowPart::Tokens.intent());
1726 - assert!(!RowPart::Proportion.revealed_on_hover());
1727 1753 }
1728 1754
1729 1755 #[test]
@@ -1989,17 +2015,11 @@
1989 2015 }
1990 2016
1991 2017 #[test]
1992 - fn only_the_actions_part_hides_until_hovered() {
1993 - for p in [
1994 - RowPart::Primary,
1995 - RowPart::Secondary,
1996 - RowPart::Meta,
1997 - RowPart::Tokens,
1998 - ] {
1999 - assert!(!p.revealed_on_hover(), "{p:?} should always be visible");
2000 - }
2001 - assert!(RowPart::Actions.revealed_on_hover());
2002 - // Emphasis falls off down the row, and never rises again.
2018 + fn emphasis_falls_off_down_the_row() {
2019 + // `revealed_on_hover` was asserted here until 0.13.0 retired it. It said
2020 + // a row's actions stay hidden until hover, which stopped being true when
2021 + // makeover-webview 0.23.0 showed them at rest, and nothing had consumed
2022 + // it for a release either way.
2003 2023 assert_eq!(RowPart::Primary.intent(), "content");
2004 2024 assert_eq!(RowPart::Secondary.intent(), "content-secondary");
2005 2025 assert_eq!(RowPart::Meta.intent(), "content-muted");