Skip to main content

max / quasi

A figure carries how its value moved
Author: Max Johnson <me@maxj.phd> · 2026-08-11 01:41 UTC
Signed with PGP, not checked
Commit: 53e4ccc3c6ccbf214745cb2ce4b7e118cce26542
Parent: 4058403
5 files changed, +76 insertions, -17 deletions
M Cargo.lock +20 -14
@@ -1918,6 +1918,12 @@
1918 1918 source = "registry+https://github.com/rust-lang/crates.io-index"
1919 1919 checksum = "58edd16523115ed4c9ca6de016693300ac95cf1bb0bd8ccf7fd246213102a7ff"
1920 1920
1921 + [[package]]
1922 + name = "makeover-layout"
1923 + version = "0.13.0"
1924 + source = "registry+https://github.com/rust-lang/crates.io-index"
1925 + checksum = "3e503a11485fe2e17d9e7b0326cb7fef8a9ff0968e1d37d101b45ea08298b0b9"
1926 +
1921 1927 [[package]]
1922 1928 name = "makeover-touch"
1923 1929 version = "0.8.1"
@@ -1925,17 +1931,17 @@
1925 1931 checksum = "d9427a7a6890849f5c815a0e77a63faa4791d7a37e7efaff4802bcafa3b8235a"
1926 1932 dependencies = [
1927 1933 "makeover-geometry",
1928 - "makeover-layout",
1934 + "makeover-layout 0.12.0",
1929 1935 ]
1930 1936
1931 1937 [[package]]
1932 1938 name = "makeover-webview"
1933 - version = "0.23.0"
1939 + version = "0.24.0"
1934 1940 source = "registry+https://github.com/rust-lang/crates.io-index"
1935 - checksum = "0303225f8e8be6ef237ed268652e7fbfd501bd17843323680a18a9df30b38fe3"
1941 + checksum = "6e88bf1eafa0806bbef3002435b91bfb28ce25a989fe484f19eb1e16c58eafc6"
1936 1942 dependencies = [
1937 1943 "makeover-geometry",
1938 - "makeover-layout",
1944 + "makeover-layout 0.13.0",
1939 1945 "makeover-touch",
1940 1946 ]
1941 1947
@@ -2616,7 +2622,7 @@
2616 2622 name = "quasi-router"
2617 2623 version = "0.1.0"
2618 2624 dependencies = [
2619 - "makeover-layout",
2625 + "makeover-layout 0.13.0",
2620 2626 ]
2621 2627
2622 2628 [[package]]
@@ -2645,7 +2651,7 @@
2645 2651 version = "0.1.0"
2646 2652 dependencies = [
2647 2653 "docengine",
2648 - "makeover-layout",
2654 + "makeover-layout 0.13.0",
2649 2655 "makeover-webview",
2650 2656 "quasi-http",
2651 2657 "quasi-router",
@@ -4888,6 +4894,14 @@
4888 4894 source = "registry+https://github.com/rust-lang/crates.io-index"
4889 4895 checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
4890 4896
4897 + [[patch.unused]]
4898 + name = "synckit-client"
4899 + version = "0.8.0"
4900 +
4901 + [[patch.unused]]
4902 + name = "synckit-config"
4903 + version = "0.2.0"
4904 +
4891 4905 [[patch.unused]]
4892 4906 name = "kberg"
4893 4907 version = "0.1.0"
@@ -4899,11 +4913,3 @@
4899 4913 [[patch.unused]]
4900 4914 name = "tagtree"
4901 4915 version = "0.4.0"
4902 -
4903 - [[patch.unused]]
4904 - name = "synckit-client"
4905 - version = "0.8.0"
4906 -
4907 - [[patch.unused]]
4908 - name = "synckit-config"
4909 - version = "0.2.0"
@@ -13,4 +13,4 @@
13 13 workspace = true
14 14
15 15 [dependencies]
16 - makeover-layout = "0.12.0"
16 + makeover-layout = "0.13.0"
@@ -15,8 +15,8 @@
15 15 [dependencies]
16 16 quasi-router = { path = "../quasi-router", version = "0.1.0" }
17 17 quasi-http = { path = "../quasi-http", version = "0.1.0" }
18 - makeover-layout = "0.12.0"
19 - makeover-webview = "0.23.0"
18 + makeover-layout = "0.13.0"
19 + makeover-webview = "0.24.0"
20 20 # `Node::Rich` carries markdown source and this is what turns it into markup.
21 21 # Sanitising comes with it, which is why the node can carry what a user typed.
22 22 #
@@ -372,7 +372,23 @@
372 372 pub value: String,
373 373 /// What it counts. The caption under the value.
374 374 pub caption: String,
375 + /// How the value has moved, if the app is tracking that.
376 + ///
377 + /// Mirrors `layout::Figure::change`, added there at 0.13.0. Text for the
378 + /// same reason [`value`](Self::value) is: only the app knows whether a move
379 + /// reads as `+12.5%`, `+3` or `2x`.
380 + ///
381 + /// This is what [`tone`](Self::tone) was for. Counted before adding it: the
382 + /// MNW server has four screens whose stat card is a label, a value and a
383 + /// delta, and on all four the delta is the toned part while the number
384 + /// itself is an ordinary fact. Without it the delta folds into the caption,
385 + /// which loses the tone and turns a second smaller line into a longer first
386 + /// one.
387 + pub change: Option<String>,
375 388 /// What the figure means. [`layout::Tone::Neutral`] is an ordinary fact.
389 + ///
390 + /// Applies to [`change`](Self::change) where there is one, and to the value
391 + /// where there is not. The renderer decides which element that lands on.
376 392 pub tone: layout::Tone,
377 393 }
378 394
@@ -382,10 +398,18 @@
382 398 Self {
383 399 value: value.into(),
384 400 caption: caption.into(),
401 + change: None,
385 402 tone: layout::Tone::Neutral,
386 403 }
387 404 }
388 405
406 + /// How the value has moved.
407 + #[must_use]
408 + pub fn change(mut self, change: impl Into<String>) -> Self {
409 + self.change = Some(change.into());
410 + self
411 + }
412 +
389 413 /// What the figure means.
390 414 #[must_use]
391 415 pub const fn tone(mut self, tone: layout::Tone) -> Self {
@@ -399,6 +423,7 @@
399 423 layout::Figure {
400 424 value: &self.value,
401 425 caption: &self.caption,
426 + change: self.change.as_deref(),
402 427 tone: self.tone,
403 428 }
404 429 }
@@ -731,6 +731,34 @@
731 731 assert!(!html.contains("row-actions"));
732 732 }
733 733
734 + #[test]
735 + fn a_figure_carries_its_delta_through_to_the_strip() {
736 + // makeover-layout 0.13.0. Four of MNW's screens put a label, a value and a
737 + // delta in one stat card, and the delta is the toned part. A description
738 + // that dropped it left the tone with nothing to colour.
739 + let html = fragment(&Node::Stats {
740 + figures: vec![(
741 + Figure::new("1,204", "Views")
742 + .change("+12.5%")
743 + .tone(layout::Tone::Success),
744 + None,
745 + )],
746 + });
747 +
748 + assert!(html.contains("+12.5%"), "{html}");
749 + assert!(html.contains("figure-change"), "{html}");
750 + assert!(html.contains("data-tone=\"success\""), "{html}");
751 + // The accessible name carries it too, since every span in a figure is
752 + // `aria-hidden` and a delta outside the name would reach a reader not at all.
753 + assert!(html.contains("Views: 1,204, +12.5%"), "{html}");
754 +
755 + // A figure with nothing to compare against emits no empty delta.
756 + let plain = fragment(&Node::Stats {
757 + figures: vec![(Figure::new("3.1%", "Conversion"), None)],
758 + });
759 + assert!(!plain.contains("figure-change"), "{plain}");
760 + }
761 +
734 762 #[test]
735 763 fn a_cell_value_that_is_a_link_is_the_link() {
736 764 // 35 cells across 18 of MNW's templates are a title that goes somewhere.