Skip to main content

max / makenotwork

theme-common 0.5.0: intent-based ("human design") schema + MNW adoption Themes are now authored by intent/role, not hue. Sections: [surface] page/raised/sunken/overlay, [content] primary/secondary/muted, [action] primary, [status] danger/success/warning/info, [line] border, [category] one..six (distinct decorative colors for tags/badges/charts). The crate gains the single resolver every product shares: resolve() reads the authored intents and computes the derived interactive states (action hover/active, content-on-action contrast, selection, row-stripe, status surfaces, border-strong) using the exact math the apps each re-implemented (lighten/darken/contrast/lerp), and emits them as intent CSS vars (intent_css_vars) or RGB tuples (SemanticTokens::rgb) for native consumers. - All 29 themes migrated to the intent schema (deterministic, color-preserving). - MNW: theming.rs emits the intent layer; style.css :root carries the makenotwork intent defaults + brand aliases (--detail/--highlight/--surface* over the intent tokens) so 300+ call sites stay valid; app-side literals (brown --warning, health/diff/stripe/tints/shadows) unchanged. Fixed a pre-existing dangling --bg-page ref (-> --surface-page). GoingsOn, Balanced Breakfast, and audiofiles adopt the shared resolver in follow-up commits (their repos).
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-13 01:02 UTC
Signed with PGP, not checked
Commit: 6e84ecf2bc41f80f11b78a78f9da83278ec5a22d
Parent: 8057069
36 files changed, +1340 insertions, -1105 deletions
@@ -7059,7 +7059,7 @@
7059 7059
7060 7060 [[package]]
7061 7061 name = "theme-common"
7062 - version = "0.4.0"
7062 + version = "0.5.0"
7063 7063 dependencies = [
7064 7064 "serde",
7065 7065 "toml",
@@ -7,10 +7,10 @@
7 7 //! [`DEFAULT_THEME_ID`].
8 8 //!
9 9 //! Themes are embedded at compile time so production needs no themes directory
10 - //! on disk. Each theme's primitive-layer CSS is rendered once via
11 - //! [`theme_common::to_css_vars`] and cached; page handlers inject the rendered
12 - //! string into the page `<head>`, where it overrides the default `:root`
13 - //! primitives from `static/style.css` (the semantic layer derives from there,
10 + //! on disk. Each theme is resolved to its intent tokens and rendered once via
11 + //! [`theme_common::intent_css_vars`] and cached; page handlers inject the
12 + //! rendered string into the page `<head>`, where it overrides the default
13 + //! intent `:root` from `static/style.css` (the brand aliases derive from there,
14 14 //! so the whole page re-themes). This is the single TOML -> CSS mapping shared
15 15 //! with GoingsOn and audiofiles.
16 16
@@ -18,7 +18,7 @@
18 18 use std::sync::LazyLock;
19 19
20 20 use include_dir::{Dir, include_dir};
21 - use theme_common::{ThemeMeta, parse_theme_str, to_css_vars};
21 + use theme_common::{ThemeMeta, intent_css_vars, parse_theme_str, resolve};
22 22
23 23 /// The bundled themes, embedded from the shared crate at compile time.
24 24 static THEMES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../shared/themes");
@@ -28,10 +28,10 @@
28 28 pub const DEFAULT_THEME_ID: &str = "makenotwork";
29 29
30 30 /// A built-in theme: its metadata (for the picker) and its pre-rendered
31 - /// primitive-layer CSS (for `<head>` injection).
31 + /// intent-layer CSS (for `<head>` injection).
32 32 pub struct ThemeEntry {
33 33 pub meta: ThemeMeta,
34 - /// `:root { … }` block of the 14 primitive tokens, ready to inline.
34 + /// `:root { … }` block of resolved intent tokens, ready to inline.
35 35 pub css: String,
36 36 }
37 37
@@ -51,8 +51,9 @@
51 51 };
52 52 match parse_theme_str(id, content, false) {
53 53 Ok(theme) => {
54 - let css = to_css_vars(&theme);
55 - map.insert(id.to_string(), ThemeEntry { meta: theme.meta, css });
54 + let tokens = resolve(&theme);
55 + let css = intent_css_vars(&tokens);
56 + map.insert(id.to_string(), ThemeEntry { meta: tokens.meta, css });
56 57 }
57 58 Err(e) => {
58 59 // A malformed bundled theme is a build-content bug, not a
@@ -148,13 +149,15 @@
148 149 }
149 150
150 151 #[test]
151 - fn default_css_carries_primitives_losslessly() {
152 + fn default_css_carries_intent_tokens() {
152 153 let css = theme_css(None);
153 - // The values style.css's semantic layer derives from.
154 - assert!(css.contains("--bg-primary: #ede8e1;"));
155 - assert!(css.contains("--fg-primary: #3d3530;"));
156 - assert!(css.contains("--accent-blue: #6c5ce7;"));
157 - assert!(css.contains("--border-default: #d0cbb8;"));
154 + // Intent vocabulary the page's brand aliases derive from.
155 + assert!(css.contains("--surface-page: #ede8e1;"));
156 + assert!(css.contains("--content: #3d3530;"));
157 + assert!(css.contains("--action: #6c5ce7;"));
158 + assert!(css.contains("--border: #d0cbb8;"));
159 + // A derived interactive state is present too.
160 + assert!(css.contains("--selection: "));
158 161 }
159 162
160 163 #[test]
@@ -136,63 +136,70 @@
136 136 --font-body: "Lato", sans-serif;
137 137
138 138 /* --------------------------------------------------------------------
139 - PRIMITIVE LAYER — theme-common palette (themeable)
139 + INTENT LAYER — the shared theme vocabulary (themeable)
140 140
141 - The 14 shared tokens emitted by theme_common::to_css_vars(): 4 bg,
142 - 3 fg, 6 accent, 1 border. These are the ONLY literal source for
143 - themeable color. A built-in or creator theme swaps this block; the
144 - semantic layer below derives from it, so re-theming cascades for free.
145 - Values reproduce shared/themes/makenotwork.toml exactly. Names match
146 - GoingsOn + audiofiles, so one theme ports across every product.
141 + These are the intent tokens emitted by theme_common::intent_css_vars():
142 + colors declared by ROLE (surface/content/action/status/line/category),
143 + plus the derived interactive states (hover/active/selection/row-stripe/
144 + contrast) the crate computes once for every product. The values below
145 + are the platform default (shared/themes/makenotwork.toml resolved) so
146 + non-creator pages render without injection; creator profile/project/item
147 + pages inject a <style> that supersedes this block with the chosen theme.
148 + Generated — keep in sync with `cargo run --example dump_intents makenotwork`.
147 149 -------------------------------------------------------------------- */
148 - --bg-primary: #ede8e1;
149 - --bg-secondary: #f4f0eb;
150 - --bg-tertiary: #ddd7c5;
151 - --bg-surface: #f5f0eb;
152 - --fg-primary: #3d3530;
153 - --fg-secondary: #5c554f;
154 - --fg-muted: #8a8480;
155 - --accent-red: #c0392b;
156 - --accent-green: #27ae60;
157 - --accent-blue: #6c5ce7;
158 - --accent-yellow: #f59e0b;
159 - --accent-purple: #a29bfe;
160 - --accent-cyan: #17a2b8;
161 - --border-default: #d0cbb8;
150 + --surface-page: #ede8e1;
151 + --surface-raised: #f5f0eb;
152 + --surface-sunken: #ddd7c5;
153 + --surface-overlay: #f4f0eb;
154 + --content: #3d3530;
155 + --content-secondary: #5c554f;
156 + --content-muted: #8a8480;
157 + --content-on-action: #ffffff;
158 + --action: #6c5ce7;
159 + --action-hover: #7b6ce9;
160 + --action-active: #6153d0;
161 + --danger: #c0392b;
162 + --success: #27ae60;
163 + --warning: #f59e0b;
164 + --info: #17a2b8;
165 + --danger-surface: #e8d3cb;
166 + --success-surface: #d5e1d2;
167 + --warning-surface: #eedfc7;
168 + --info-surface: #d3e0dc;
169 + --border: #d0cbb8;
170 + --border-strong: #bbb7a6;
171 + --focus-ring: #6c5ce7;
172 + --selection: #c6bee3;
173 + --row-stripe: #efeae4;
174 + --hover-surface: #ddd7c5;
175 + --category-one: #c0392b;
176 + --category-two: #27ae60;
177 + --category-three: #6c5ce7;
178 + --category-four: #f59e0b;
179 + --category-five: #a29bfe;
180 + --category-six: #17a2b8;
162 181
163 182 /* --------------------------------------------------------------------
164 - SEMANTIC LAYER — MNW roles derived from the primitives above.
183 + BRAND ALIASES — MNW's historical vocabulary over the intent layer.
165 184
166 - Components reference THESE names, never the raw palette. Each
167 - derivation currently resolves to its historical hex, so the swap to
168 - the primitive layer is visually lossless; change a theme and these
169 - follow. Brand vocabulary (--detail, --highlight, --light-background)
170 - is kept as derived aliases so 300+ existing call sites stay valid.
185 + Kept so 300+ existing call sites (--detail, --highlight, --surface-*)
186 + stay valid. Names that already match an intent token (--border,
187 + --danger, --success, --focus-ring) consume it directly above.
171 188 -------------------------------------------------------------------- */
172 - /* Surfaces + ink */
173 - --background: var(--bg-primary);
174 - --light-background: var(--bg-secondary);
175 - --secondary-bg: var(--bg-secondary);
176 - --surface: var(--bg-surface);
177 - --surface-raised: var(--bg-surface);
178 - --surface-muted: var(--bg-tertiary);
179 - --detail: var(--fg-primary);
180 - --text: var(--fg-primary);
181 - --text-muted: var(--fg-muted);
182 - --highlight: var(--accent-blue);
183 - --accent: var(--accent-blue);
184 - --border: var(--border-default);
185 - --surface-border: var(--border-default);
186 -
187 - /* Status roles */
188 - --success: var(--accent-green);
189 - --danger: var(--accent-red);
190 - --focus-ring: var(--accent-blue);
191 -
192 - /* Compatibility aliases (git browser, older inline styles) */
193 - --accent-color: var(--accent-blue);
194 - --border-color: var(--border-default);
195 - --background-color: var(--bg-primary);
189 + --background: var(--surface-page);
190 + --background-color: var(--surface-page);
191 + --light-background: var(--surface-overlay);
192 + --secondary-bg: var(--surface-overlay);
193 + --surface: var(--surface-raised);
194 + --surface-muted: var(--surface-sunken);
195 + --detail: var(--content);
196 + --text: var(--content);
197 + --text-muted: var(--content-muted);
198 + --highlight: var(--action);
199 + --accent: var(--action);
200 + --accent-color: var(--action);
201 + --surface-border: var(--border);
202 + --border-color: var(--border);
196 203
197 204 /* --------------------------------------------------------------------
198 205 APP-SIDE LITERALS — intentionally NOT in the shared palette.
@@ -4214,7 +4221,7 @@
4214 4221 }
4215 4222
4216 4223 .layer-chip-pass { background: #e6f0dc; border-color: #c4d8a8; color: #3a5a2a; }
4217 - .layer-chip-skip { background: var(--bg-page); border-color: var(--border); color: var(--text-muted); }
4224 + .layer-chip-skip { background: var(--surface-page); border-color: var(--border); color: var(--text-muted); }
4218 4225 .layer-chip-fail { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); font-weight: 600; }
4219 4226 .layer-chip-error { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning); font-weight: 600; }
4220 4227
@@ -273,7 +273,7 @@
273 273
274 274 [[package]]
275 275 name = "theme-common"
276 - version = "0.4.0"
276 + version = "0.5.0"
277 277 dependencies = [
278 278 "serde",
279 279 "tempfile",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "theme-common"
3 - version = "0.4.0"
3 + version = "0.5.0"
4 4 edition = "2024"
5 5
6 6 [dependencies]
@@ -7,24 +7,33 @@
7 7 name = "audiofiles"
8 8 variant = "light"
9 9
10 - [background]
11 - primary = "#CCDAD1" # Ash Grey — main canvas
12 - secondary = "#B4C5BB" # mid sage — headers / secondary panels
13 - tertiary = "#9CAEA9" # Ash Grey deep — selected / inset
14 - surface = "#DAE3DC" # lighter sage — raised cards/rows
10 + [surface]
11 + page = "#CCDAD1"
12 + raised = "#DAE3DC"
13 + sunken = "#9CAEA9"
14 + overlay = "#B4C5BB"
15 15
16 - [foreground]
17 - primary = "#38302E" # Deep Mocha — body text
18 - secondary = "#6F6866" # Dim Grey
19 - muted = "#788585" # Grey Olive — hints / disabled
16 + [content]
17 + primary = "#38302E"
18 + secondary = "#6F6866"
19 + muted = "#788585"
20 20
21 - [accent]
22 - red = "#B05F4E" # muted terracotta
23 - green = "#6F8A5C" # muted olive
24 - blue = "#5E7C8E" # muted slate
25 - yellow = "#C19A53" # muted ochre
26 - purple = "#836A80" # muted mauve
27 - cyan = "#5F8C82" # muted teal
21 + [action]
22 + primary = "#5E7C8E"
28 23
29 - [border]
30 - default = "#9CAEA9"
24 + [status]
25 + danger = "#B05F4E"
26 + success = "#6F8A5C"
27 + warning = "#C19A53"
28 + info = "#5F8C82"
29 +
30 + [line]
31 + border = "#9CAEA9"
32 +
33 + [category]
34 + one = "#B05F4E"
35 + two = "#6F8A5C"
36 + three = "#5E7C8E"
37 + four = "#C19A53"
38 + five = "#836A80"
39 + six = "#5F8C82"
@@ -5,24 +5,33 @@
5 5 name = "Ayu Light"
6 6 variant = "light"
7 7
8 - [background]
9 - primary = "#e7eaed"
10 - secondary = "#dde1e5"
11 - tertiary = "#d0d4d8"
12 - surface = "#f2f4f6"
8 + [surface]
9 + page = "#e7eaed"
10 + raised = "#f2f4f6"
11 + sunken = "#d0d4d8"
12 + overlay = "#dde1e5"
13 13
14 - [foreground]
15 - primary = "#5c6166"
14 + [content]
15 + primary = "#5c6166"
16 16 secondary = "#6b7580"
17 - muted = "#8b9199"
17 + muted = "#8b9199"
18 18
19 - [accent]
20 - red = "#f07171"
21 - green = "#86b300"
22 - blue = "#399ee6"
23 - yellow = "#ffaa33"
24 - purple = "#a37acc"
25 - cyan = "#55b4d4"
19 + [action]
20 + primary = "#399ee6"
26 21
27 - [border]
28 - default = "#828c9a"
22 + [status]
23 + danger = "#f07171"
24 + success = "#86b300"
25 + warning = "#ffaa33"
26 + info = "#55b4d4"
27 +
28 + [line]
29 + border = "#828c9a"
30 +
31 + [category]
32 + one = "#f07171"
33 + two = "#86b300"
34 + three = "#399ee6"
35 + four = "#ffaa33"
36 + five = "#a37acc"
37 + six = "#55b4d4"
@@ -5,24 +5,33 @@
5 5 name = "Ayu Mirage"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#1f2430"
10 - secondary = "#191e29"
11 - tertiary = "#272d38"
12 - surface = "#242936"
8 + [surface]
9 + page = "#1f2430"
10 + raised = "#242936"
11 + sunken = "#272d38"
12 + overlay = "#191e29"
13 13
14 - [foreground]
15 - primary = "#cccac2"
14 + [content]
15 + primary = "#cccac2"
16 16 secondary = "#b8b4aa"
17 - muted = "#707a8c"
17 + muted = "#707a8c"
18 18
19 - [accent]
20 - red = "#f28779"
21 - green = "#bae67e"
22 - blue = "#5ccfe6"
23 - yellow = "#ffd580"
24 - purple = "#d4bfff"
25 - cyan = "#95e6cb"
19 + [action]
20 + primary = "#5ccfe6"
26 21
27 - [border]
28 - default = "#33415e"
22 + [status]
23 + danger = "#f28779"
24 + success = "#bae67e"
25 + warning = "#ffd580"
26 + info = "#95e6cb"
27 +
28 + [line]
29 + border = "#33415e"
30 +
31 + [category]
32 + one = "#f28779"
33 + two = "#bae67e"
34 + three = "#5ccfe6"
35 + four = "#ffd580"
36 + five = "#d4bfff"
37 + six = "#95e6cb"
@@ -5,24 +5,33 @@
5 5 name = "Carbonfox"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#161616"
10 - secondary = "#101010"
11 - tertiary = "#1e1e1e"
12 - surface = "#252525"
8 + [surface]
9 + page = "#161616"
10 + raised = "#252525"
11 + sunken = "#1e1e1e"
12 + overlay = "#101010"
13 13
14 - [foreground]
15 - primary = "#f2f4f8"
14 + [content]
15 + primary = "#f2f4f8"
16 16 secondary = "#dde1e6"
17 - muted = "#878d96"
17 + muted = "#878d96"
18 18
19 - [accent]
20 - red = "#ee5396"
21 - green = "#25be6a"
22 - blue = "#78a9ff"
23 - yellow = "#08bdba"
24 - purple = "#be95ff"
25 - cyan = "#33b1ff"
19 + [action]
20 + primary = "#78a9ff"
26 21
27 - [border]
28 - default = "#393939"
22 + [status]
23 + danger = "#ee5396"
24 + success = "#25be6a"
25 + warning = "#08bdba"
26 + info = "#33b1ff"
27 +
28 + [line]
29 + border = "#393939"
30 +
31 + [category]
32 + one = "#ee5396"
33 + two = "#25be6a"
34 + three = "#78a9ff"
35 + four = "#08bdba"
36 + five = "#be95ff"
37 + six = "#33b1ff"
@@ -5,24 +5,33 @@
5 5 name = "Catppuccin Frappé"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#303446"
10 - secondary = "#292c3c"
11 - tertiary = "#414559"
12 - surface = "#232634"
8 + [surface]
9 + page = "#303446"
10 + raised = "#232634"
11 + sunken = "#414559"
12 + overlay = "#292c3c"
13 13
14 - [foreground]
15 - primary = "#c6d0f5"
14 + [content]
15 + primary = "#c6d0f5"
16 16 secondary = "#b5bfe2"
17 - muted = "#a5adce"
17 + muted = "#a5adce"
18 18
19 - [accent]
20 - red = "#e78284"
21 - green = "#a6d189"
22 - blue = "#8caaee"
23 - yellow = "#e5c890"
24 - purple = "#ca9ee6"
25 - cyan = "#81c8be"
19 + [action]
20 + primary = "#8caaee"
26 21
27 - [border]
28 - default = "#626880"
22 + [status]
23 + danger = "#e78284"
24 + success = "#a6d189"
25 + warning = "#e5c890"
26 + info = "#81c8be"
27 +
28 + [line]
29 + border = "#626880"
30 +
31 + [category]
32 + one = "#e78284"
33 + two = "#a6d189"
34 + three = "#8caaee"
35 + four = "#e5c890"
36 + five = "#ca9ee6"
37 + six = "#81c8be"
@@ -5,24 +5,33 @@
5 5 name = "Catppuccin Latte"
6 6 variant = "light"
7 7
8 - [background]
9 - primary = "#e6e9ef"
10 - secondary = "#dce0e8"
11 - tertiary = "#ccd0da"
12 - surface = "#eff1f5"
8 + [surface]
9 + page = "#e6e9ef"
10 + raised = "#eff1f5"
11 + sunken = "#ccd0da"
12 + overlay = "#dce0e8"
13 13
14 - [foreground]
15 - primary = "#4c4f69"
14 + [content]
15 + primary = "#4c4f69"
16 16 secondary = "#5c5f77"
17 - muted = "#6c6f82"
17 + muted = "#6c6f82"
18 18
19 - [accent]
20 - red = "#d20f39"
21 - green = "#40a02b"
22 - blue = "#1e66f5"
23 - yellow = "#df8e1d"
24 - purple = "#8839ef"
25 - cyan = "#04a5e5"
19 + [action]
20 + primary = "#1e66f5"
26 21
27 - [border]
28 - default = "#bcc0cc"
22 + [status]
23 + danger = "#d20f39"
24 + success = "#40a02b"
25 + warning = "#df8e1d"
26 + info = "#04a5e5"
27 +
28 + [line]
29 + border = "#bcc0cc"
30 +
31 + [category]
32 + one = "#d20f39"
33 + two = "#40a02b"
34 + three = "#1e66f5"
35 + four = "#df8e1d"
36 + five = "#8839ef"
37 + six = "#04a5e5"
@@ -5,24 +5,33 @@
5 5 name = "Catppuccin Macchiato"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#24273a"
10 - secondary = "#1e2030"
11 - tertiary = "#363a4f"
12 - surface = "#181926"
8 + [surface]
9 + page = "#24273a"
10 + raised = "#181926"
11 + sunken = "#363a4f"
12 + overlay = "#1e2030"
13 13
14 - [foreground]
15 - primary = "#cad3f5"
14 + [content]
15 + primary = "#cad3f5"
16 16 secondary = "#b8c0e0"
17 - muted = "#a5adcb"
17 + muted = "#a5adcb"
18 18
19 - [accent]
20 - red = "#ed8796"
21 - green = "#a6da95"
22 - blue = "#8aadf4"
23 - yellow = "#eed49f"
24 - purple = "#c6a0f6"
25 - cyan = "#8bd5ca"
19 + [action]
20 + primary = "#8aadf4"
26 21
27 - [border]
28 - default = "#5b6078"
22 + [status]
23 + danger = "#ed8796"
24 + success = "#a6da95"
25 + warning = "#eed49f"
26 + info = "#8bd5ca"
27 +
28 + [line]
29 + border = "#5b6078"
30 +
31 + [category]
32 + one = "#ed8796"
33 + two = "#a6da95"
34 + three = "#8aadf4"
35 + four = "#eed49f"
36 + five = "#c6a0f6"
37 + six = "#8bd5ca"
@@ -5,24 +5,33 @@
5 5 name = "Catppuccin Mocha"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#181825"
10 - secondary = "#11111b"
11 - tertiary = "#313244"
12 - surface = "#1e1e2e"
8 + [surface]
9 + page = "#181825"
10 + raised = "#1e1e2e"
11 + sunken = "#313244"
12 + overlay = "#11111b"
13 13
14 - [foreground]
15 - primary = "#cdd6f4"
14 + [content]
15 + primary = "#cdd6f4"
16 16 secondary = "#bac2de"
17 - muted = "#9399b2"
17 + muted = "#9399b2"
18 18
19 - [accent]
20 - red = "#f38ba8"
21 - green = "#a6e3a1"
22 - blue = "#89b4fa"
23 - yellow = "#f9e2af"
24 - purple = "#cba6f7"
25 - cyan = "#89dceb"
19 + [action]
20 + primary = "#89b4fa"
26 21
27 - [border]
28 - default = "#45475a"
22 + [status]
23 + danger = "#f38ba8"
24 + success = "#a6e3a1"
25 + warning = "#f9e2af"
26 + info = "#89dceb"
27 +
28 + [line]
29 + border = "#45475a"
30 +
31 + [category]
32 + one = "#f38ba8"
33 + two = "#a6e3a1"
34 + three = "#89b4fa"
35 + four = "#f9e2af"
36 + five = "#cba6f7"
37 + six = "#89dceb"