Skip to main content

max / makeover-build

Emit the typography layer, and put it in the Tauri bundle typography_css writes the @font-face rules and the two tokens they back, the same way geometry_css writes the spacing layer: the facts are the crates' and stating them per app is how three apps came to hold three answers to --font-mono. Separate file from the layout stylesheet because @font-face takes no part in the cascade, and a consumer may need the faces ahead of a layer order it declares elsewhere.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-17 16:07 UTC
Signed with PGP, not checked
Commit: cad4b30b0d89318a788aa5e58c882ae43f9b783a
Parent: 5926dd6
2 files changed, +88 insertions, -15 deletions
M Cargo.toml +14 -9
@@ -1,20 +1,25 @@
1 1 [package]
2 2 name = "makeover-build"
3 - version = "0.39.0"
3 + version = "0.40.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"
7 7 repository = "https://makenot.work/git/max/makeover-build"
8 8
9 9 [dependencies]
10 - # 2.5 and not 2.4, because makeover-webview 0.22 emits `--elevation-overlay`
11 - # referring to `var(--elevation)`, and 2.5 is the release that derives that
12 - # token. The two crates cannot state the coupling themselves: makeover-webview
13 - # has no dependency on makeover, deliberately, so this is the only manifest that
14 - # sees both halves. A consumer resolving 2.4 here would generate a stylesheet
15 - # naming a token nothing defines, and a custom property whose value fails to
16 - # substitute takes the whole declaration with it rather than degrading.
17 - makeover = "2.5"
10 + # 2.7 for the typography layer: `font_face_css` and the two font tokens, which
11 + # `typography_css` writes and which do not exist before it.
12 + #
13 + # The floor was 2.5 before that, and its reason still holds underneath: 2.5 is
14 + # the release that derives `--elevation-overlay`, which makeover-webview 0.22
15 + # emits a `var()` reference to. The two crates cannot state that coupling
16 + # themselves — makeover-webview has no dependency on makeover, deliberately —
17 + # so this is the only manifest that sees both halves, and a consumer resolving
18 + # 2.4 here would generate a stylesheet naming a token nothing defines. A custom
19 + # property whose value fails to substitute takes the whole declaration with it
20 + # rather than degrading, so that failure is invisible until something looks
21 + # wrong on screen.
22 + makeover = "2.7"
18 23 # The exact patch, not the minor: the same discipline 0.13.1 was released for.
19 24 # An earlier patch in a minor can pin an older makeover-touch, which drags a
20 25 # second makeover-geometry into the graph alongside the one below and fails to
M src/lib.rs +74 -6
@@ -130,16 +130,58 @@
130 130 std::fs::write(path, css).expect("write geometry css");
131 131 }
132 132
133 - /// All three generated files at the layout every Tauri consumer already uses:
134 - /// `themes/` beside the manifest, and `frontend/css/{geometry,layout}.css`
135 - /// under it.
133 + /// Write the house typography layer to `path`: the two `@font-face` rules and
134 + /// the two tokens they back.
136 135 ///
137 - /// Pass `env!("CARGO_MANIFEST_DIR")`. Consumers that want different paths call
138 - /// [`themes`] and [`layout_css`] directly.
136 + /// `font_url` is the directory the consumer serves its fonts from, without a
137 + /// trailing slash — `/static/fonts` on the MNW server, `fonts` for a Tauri
138 + /// frontend loading relative to its index.
139 + ///
140 + /// Generated rather than hand-written for the same reason the spacing layer is:
141 + /// the facts are the crates' and stating them per app is how three apps came to
142 + /// hold three different answers to `--font-mono`. It is a separate file from
143 + /// the layout stylesheet because `@font-face` rules take no part in the
144 + /// cascade and a consumer may need to load them ahead of a layer order it
145 + /// declares elsewhere.
146 + ///
147 + /// # The consumer still has to put the faces there
148 + ///
149 + /// This writes the CSS that fetches `QuasiMono.woff2` and `QuasiBody.woff2`; it
150 + /// does not write the fonts. It cannot: they are cut by `quasi-type`, which is
151 + /// `publish = false`, and this crate is on crates.io. A consumer takes
152 + /// quasi-type as a git dependency in its own `build.rs` and calls
153 + /// `quasi_type::cut`, the way `shop-font` does, writing each slot's woff2 under
154 + /// [`makeover::WEBFONT_MONO_FILE`] and [`makeover::WEBFONT_SANS_FILE`].
139 155 ///
140 156 /// # Panics
141 157 ///
142 - /// If either file cannot be written.
158 + /// If the file cannot be written.
159 + pub fn typography_css(path: impl AsRef<Path>, font_url: &str) {
160 + let mut css = String::from(
161 + "/* Generated by makeover-build from makeover. Do not edit.\n \
162 + Two needs, two names, then a system generic. The faces are cut by\n \
163 + quasi-type from Atkinson Hyperlegible plus the house glyph set, and\n \
164 + both are variable over wght 200-800 in one file — which is why the\n \
165 + @font-face rules name the range. The mono face opens at ExtraLight. */\n\n",
166 + );
167 + css.push_str(&makeover::font_face_css(font_url));
168 + css.push_str(&makeover::typography_css_vars());
169 + std::fs::write(path, css).expect("write typography css");
170 + }
171 +
172 + /// All the generated files at the layout every Tauri consumer already uses:
173 + /// `themes/` beside the manifest, and
174 + /// `frontend/css/{geometry,layout,typography}.css` under it.
175 + ///
176 + /// Pass `env!("CARGO_MANIFEST_DIR")`. Consumers that want different paths call
177 + /// [`themes`], [`layout_css`] and [`typography_css`] directly.
178 + ///
179 + /// The font URL is `fonts`, relative to the frontend's index — the one layout
180 + /// a Tauri app has, since its frontend is served from its own directory.
181 + ///
182 + /// # Panics
183 + ///
184 + /// If any file cannot be written.
143 185 pub fn tauri_frontend(
144 186 manifest_dir: impl AsRef<Path>,
145 187 opts: &makeover_webview::Emit,
@@ -150,6 +192,7 @@
150 192 themes(root.join("themes"));
151 193 geometry_css(css.join("geometry.css"), explicit_touch);
152 194 layout_css(css.join("layout.css"), opts);
195 + typography_css(css.join("typography.css"), "../fonts");
153 196 }
154 197
155 198 #[cfg(test)]
@@ -208,6 +251,31 @@
208 251 );
209 252 }
210 253
254 + #[test]
255 + fn the_typography_file_declares_the_faces_before_the_tokens_that_name_them() {
256 + // The vocabulary itself is tested in makeover. What is this crate's
257 + // job is that both halves reach one file, in an order that works: a
258 + // `@font-face` may follow its use in the cascade, but reading the file
259 + // is how anyone finds out a face is fetched at all.
260 + let dir = scratch("typography");
261 + let path = dir.join("typography.css");
262 + typography_css(&path, "/static/fonts");
263 + let css = std::fs::read_to_string(&path).unwrap();
264 +
265 + assert!(css.starts_with("/* Generated by makeover-build"));
266 + assert!(
267 + css.find("@font-face").unwrap() < css.find(":root").unwrap(),
268 + "the tokens come first, so the file reads as a stack with no ground"
269 + );
270 + assert!(css.contains("url(\"/static/fonts/QuasiMono.woff2\")"));
271 + assert!(css.contains("--font-sans: \"Quasi Body\", sans-serif;"));
272 +
273 + // Not a cascade layer. `@font-face` takes no part in the cascade and a
274 + // consumer may need these rules ahead of a layer order it declares
275 + // elsewhere, so wrapping this file in one would be a silent trap.
276 + assert!(!css.contains("@layer"));
277 + }
278 +
211 279 #[test]
212 280 fn the_geometry_file_carries_the_crates_policy_and_a_banner() {
213 281 // The policy itself is tested in makeover-geometry. What is this