Skip to main content

max / makeover-build

0.52.0: emit the time axis, so a web consumer gets the durations makeover-timing 0.1.1 has carried timing_css() since the cadence axis landed and nothing generated it, so the durations existed in the crate and reached no stylesheet. timing_css(path) writes them beside geometry.css, and tauri_frontend writes it without being asked. Its own file rather than folded into geometry.css: one generated file per crate, named for the axis it carries. Time is not a narrower claim about space the way size class is about density, so folding would leave a file whose banner names one crate and whose contents come from two. The prefers-reduced-motion block rides with the values it overrides. A sheet carrying only the values animates at every rung for a reader who asked it not to, and does it silently; the test asserts both halves and their order.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-26 15:38 UTC
Signed with PGP, not checked
Commit: 8ef725d8d22b8dc52bd1bb717d2dc7706ef7dbc3
Parent: 352d60f
2 files changed, +87 insertions, -15 deletions
M Cargo.toml +6 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-build"
3 - version = "0.51.0"
3 + version = "0.52.0"
4 4 edition = "2024"
5 5 description = "Build-script support for the make-family design system: materialise makeover's themes and makeover-webview's stylesheet into a Tauri app's frontend, once, instead of copying the same twenty lines into every consumer's build.rs."
6 6 license = "MIT"
@@ -39,6 +39,11 @@
39 39 # it that was not invented locally.
40 40 makeover-webview = "0.59.1"
41 41 makeover-geometry = "0.7"
42 + # 0.1.1, not 0.1.0: the cadence axis and `reduced_motion_css` are what
43 + # `timing_css` emits, and 0.1.0 has neither. A consumer resolving 0.1.0 would
44 + # generate a sheet with no activity mark and no motion-off block, which is the
45 + # silent half -- a reader who asked for less motion would still get it.
46 + makeover-timing = "0.1.1"
42 47
43 48 [lints.rust]
44 49 unused = "warn"
M src/lib.rs +81 -14
@@ -144,6 +144,46 @@
144 144 std::fs::write(path, css).expect("write geometry css");
145 145 }
146 146
147 + /// Write `makeover-timing`'s time axis, and the motion-off block that rides
148 + /// with it, to `path`.
149 + ///
150 + /// The third generated axis, and it arrives the same way the spacing one does:
151 + /// a consumer that calls this gets `--timing-*`, `--motion-fade` and
152 + /// `--cadence-activity` without stating a number anywhere. All this adds is the
153 + /// banner and the write; `makeover_timing::timing_css` is the whole file and
154 + /// already wraps itself in [`makeover_geometry::CSS_LAYER`].
155 + ///
156 + /// # Its own file, for the reason geometry has its own file
157 + ///
158 + /// One generated file per crate, named for the axis it carries. Time is not a
159 + /// narrower claim about space the way size class is about density, so folding
160 + /// it into `geometry.css` would leave a file whose banner names one crate and
161 + /// whose contents come from two. The cost is a fourth `<link>` in the consumer,
162 + /// which is the cost the family already pays three times.
163 + ///
164 + /// # The `prefers-reduced-motion` block is not optional
165 + ///
166 + /// `makeover_timing::timing_css` emits the `:root` values and then a media
167 + /// block overriding two of them. Both land here, in that order, because they
168 + /// are one statement: a sheet carrying only the values animates at every rung
169 + /// for a reader who asked it not to, and does it silently.
170 + ///
171 + /// # Panics
172 + ///
173 + /// If the file cannot be written.
174 + pub fn timing_css(path: impl AsRef<Path>) {
175 + let mut css = String::from(
176 + "/* Generated by makeover-build from makeover-timing. Do not edit.\n \
177 + A duration is named by what it is waiting for; the number follows.\n \
178 + Three axes: how long a state lasts, how long a change takes, and how\n \
179 + often a repeating mark repeats. The reduced-motion block below zeroes\n \
180 + the last two and leaves the waits alone. A reader asking for less\n \
181 + motion has not asked for a notice to leave early. */\n",
182 + );
183 + css.push_str(&makeover_timing::timing_css());
184 + std::fs::write(path, css).expect("write timing css");
185 + }
186 +
147 187 /// Write the house typography layer to `path`: the two `@font-face` rules and
148 188 /// the two tokens they back.
149 189 ///
@@ -215,7 +255,7 @@
215 255
216 256 /// All the generated files at the layout every Tauri consumer already uses:
217 257 /// `themes/` beside the manifest, and
218 - /// `frontend/css/{geometry,layout,typography}.css` under it.
258 + /// `frontend/css/{geometry,timing,layout,typography}.css` under it.
219 259 ///
220 260 /// Pass `env!("CARGO_MANIFEST_DIR")`. Consumers that want different paths call
221 261 /// [`themes`], [`layout_css`] and [`typography_css`] directly.
@@ -264,6 +304,9 @@
264 304 let css = root.join("frontend").join("css");
265 305 themes(root.join("themes"));
266 306 geometry_css(css.join("geometry.css"), explicit_touch);
307 + // Beside geometry rather than after layout: both are value files the
308 + // component sheet reads, and a consumer's `<link>` order follows this one.
309 + timing_css(css.join("timing.css"));
267 310 layout_css(css.join("layout.css"), opts);
268 311 typography_css_from(css.join("typography.css"), typography);
269 312 }
@@ -417,22 +460,46 @@
417 460 }
418 461
419 462 #[test]
420 - fn the_tauri_layout_puts_all_three_where_the_apps_look() {
463 + fn the_timing_file_carries_the_values_and_the_block_that_overrides_them() {
464 + // The rungs themselves are tested in makeover-timing. What is this
465 + // crate's to get wrong is dropping half the file: the values are
466 + // useless noise without the media block, and the media block on its
467 + // own overrides nothing.
468 + let dir = scratch("timing");
469 + let path = dir.join("timing.css");
470 + timing_css(&path);
471 + let css = std::fs::read_to_string(&path).unwrap();
472 + assert!(css.starts_with("/* Generated by makeover-build"));
473 + // One token per axis, so a crate that grows a fourth axis and is not
474 + // emitted here fails somewhere other than on screen.
475 + assert!(css.contains("--timing-dismiss"), "no intent tokens");
476 + assert!(css.contains("--motion-fade"), "no motion token");
477 + assert!(css.contains("--cadence-activity"), "no cadence token");
478 + assert!(
479 + css.contains("@media (prefers-reduced-motion: reduce)"),
480 + "the values shipped without the block that turns them off"
481 + );
482 + // The block comes after the values it overrides. Same specificity,
483 + // so the order is the whole of the win.
484 + assert!(
485 + css.find(":root").unwrap() < css.find("prefers-reduced-motion").unwrap(),
486 + "the motion-off block cannot override values declared after it"
487 + );
488 + // Inside the family's layer, like every other generated sheet:
489 + // unlayered declarations outrank every named layer, so a generated
490 + // file outside it beats the app's own overrides.
491 + assert!(css.contains(makeover_geometry::CSS_LAYER));
492 + }
493 +
494 + #[test]
495 + fn the_tauri_layout_puts_all_four_where_the_apps_look() {
421 496 let root = scratch("tauri");
422 497 std::fs::create_dir_all(root.join("frontend").join("css")).unwrap();
423 498 tauri_frontend(&root, &makeover_webview::Emit::default(), None);
424 - assert!(
425 - root.join("frontend")
426 - .join("css")
427 - .join("geometry.css")
428 - .exists()
429 - );
430 - assert!(
431 - root.join("frontend")
432 - .join("css")
433 - .join("layout.css")
434 - .exists()
435 - );
499 + let css = root.join("frontend").join("css");
500 + for file in ["geometry.css", "timing.css", "layout.css", "typography.css"] {
501 + assert!(css.join(file).exists(), "{file} was not written");
502 + }
436 503 assert!(root.join("themes").is_dir());
437 504 }
438 505 }