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
Commit: 6e84ecf2bc41f80f11b78a78f9da83278ec5a22d
Parent: 8057069
36 files changed, +1118 insertions, -687 deletions
@@ -7059,7 +7059,7 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
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 @@ use std::collections::BTreeMap;
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 @@ static THEMES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../shared/themes"
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 @@ static REGISTRY: LazyLock<BTreeMap<String, ThemeEntry>> = LazyLock::new(|| {
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 @@ mod tests {
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)
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.
139 + INTENT LAYER — the shared theme vocabulary (themeable)
140 +
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 @@ form button:active {
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 @@ dependencies = [
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]
@@ -0,0 +1,8 @@
1 + fn main() {
2 + let id = std::env::args().nth(1).unwrap_or_else(|| "makenotwork".into());
3 + let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("themes");
4 + let t = theme_common::load_semantic(&[(dir, false)], &id).unwrap();
5 + for (k, v) in &t.intents {
6 + println!(" --{k}: {v};");
7 + }
8 + }
@@ -1,32 +1,46 @@
1 - //! Shared theme loading logic for TOML-based theme files.
1 + //! Shared theme loading + intent resolution for TOML-based theme files.
2 2 //!
3 - //! Used by GoingsOn, Balanced Breakfast (Tauri apps), and audiofiles (egui).
4 - //! audiofiles embeds themes at compile time but uses `ThemeMeta`, `parse_meta`,
5 - //! and `extract_colors` from this crate.
3 + //! Used by GoingsOn, Balanced Breakfast (Tauri apps), audiofiles (egui), and the
4 + //! MNW web server. Themes are authored by **intent** ("human design"): colors are
5 + //! declared by role (surface / content / action / status / line / category), not
6 + //! by hue. This crate is the single place that resolves an authored theme into a
7 + //! full set of intent tokens — including the derived interactive states
8 + //! (hover/active/selection/row-stripe/contrast) that each app used to recompute
9 + //! itself — and emits them as CSS variables or RGB tuples.
6 10 //!
7 - //! Theme files are TOML with this structure:
11 + //! Theme file shape:
8 12 //! ```text
9 13 //! [meta]
10 - //! name = "Theme Name"
11 - //! variant = "dark" # or "light"
14 + //! name = "Nord"
15 + //! variant = "dark" # or "light"
12 16 //!
13 - //! [background]
14 - //! primary = "#1e1e2e"
17 + //! [surface] # container backgrounds by role/elevation
18 + //! page = "#2e3440"; raised = "#3b4252"; sunken = "#434c5e"; overlay = "#3b4252"
15 19 //!
16 - //! [foreground]
17 - //! primary = "#cdd6f4"
20 + //! [content] # text/ink by emphasis
21 + //! primary = "#d8dee9"; secondary = "#e5e9f0"; muted = "#616e88"
18 22 //!
19 - //! [accent]
20 - //! primary = "#89b4fa"
23 + //! [action] # interactive / brand color
24 + //! primary = "#81a1c1"
21 25 //!
22 - //! [border]
23 - //! primary = "#45475a"
26 + //! [status] # state semantics
27 + //! danger = "#bf616a"; success = "#a3be8c"; warning = "#ebcb8b"; info = "#88c0d0"
28 + //!
29 + //! [line]
30 + //! border = "#4c566a"
31 + //!
32 + //! [category] # distinct decorative colors for tags/badges/charts
33 + //! one = "#bf616a"; two = "#a3be8c"; three = "#81a1c1"
34 + //! four = "#ebcb8b"; five = "#b48ead"; six = "#88c0d0"
24 35 //! ```
25 36
26 37 use serde::Serialize;
27 - use std::collections::HashMap;
38 + use std::collections::{BTreeMap, HashMap};
28 39 use std::path::{Path, PathBuf};
29 40
41 + /// The color sections an authored theme may declare.
42 + pub const COLOR_SECTIONS: &[&str] = &["surface", "content", "action", "status", "line", "category"];
43 +
30 44 /// Theme metadata parsed from the `[meta]` section.
31 45 #[derive(Debug, Clone, Serialize)]
32 46 #[serde(rename_all = "camelCase")]
@@ -37,7 +51,8 @@ pub struct ThemeMeta {
37 51 pub is_custom: bool,
38 52 }
39 53
40 - /// A fully loaded theme: metadata plus flattened color map.
54 + /// A loaded theme: metadata plus the authored colors, flattened to dotted keys
55 + /// (e.g. `"surface.page"`, `"status.danger"`, `"category.one"`).
41 56 #[derive(Debug, Serialize)]
42 57 #[serde(rename_all = "camelCase")]
43 58 pub struct ThemeColors {
@@ -45,6 +60,210 @@ pub struct ThemeColors {
45 60 pub colors: HashMap<String, String>,
46 61 }
47 62
63 + // ============================================================================
64 + // Color math (single source of truth for the four derivation ops)
65 + // ============================================================================
66 +
67 + /// An sRGB color. Hex round-trips losslessly.
68 + #[derive(Clone, Copy, Debug, PartialEq, Eq)]
69 + pub struct Rgb {
70 + pub r: u8,
71 + pub g: u8,
72 + pub b: u8,
73 + }
74 +
75 + impl Rgb {
76 + /// Parse `#rgb` or `#rrggbb` (case-insensitive). Returns `None` otherwise.
77 + pub fn from_hex(s: &str) -> Option<Rgb> {
78 + let h = s.strip_prefix('#')?;
79 + let (r, g, b) = match h.len() {
80 + 6 => (
81 + u8::from_str_radix(&h[0..2], 16).ok()?,
82 + u8::from_str_radix(&h[2..4], 16).ok()?,
83 + u8::from_str_radix(&h[4..6], 16).ok()?,
84 + ),
85 + 3 => {
86 + let d = |c: &str| u8::from_str_radix(c, 16).ok().map(|v| v * 17);
87 + (d(&h[0..1])?, d(&h[1..2])?, d(&h[2..3])?)
88 + }
89 + _ => return None,
90 + };
91 + Some(Rgb { r, g, b })
92 + }
93 +
94 + /// Lowercase `#rrggbb`.
95 + pub fn to_hex(self) -> String {
96 + format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
97 + }
98 +
99 + pub fn tuple(self) -> (u8, u8, u8) {
100 + (self.r, self.g, self.b)
101 + }
102 + }
103 +
104 + /// ITU-R BT.601 relative luminance in [0,1].
105 + fn luminance(c: Rgb) -> f32 {
106 + (0.299 * c.r as f32 + 0.587 * c.g as f32 + 0.114 * c.b as f32) / 255.0
107 + }
108 +
109 + /// Black or white, whichever contrasts with `bg`. Matches BB `contrastColor`
110 + /// and AF `contrast_color` (luminance > 0.5 → black).
111 + pub fn contrast_color(bg: Rgb) -> Rgb {
112 + if luminance(bg) > 0.5 {
113 + Rgb { r: 0, g: 0, b: 0 }
114 + } else {
115 + Rgb { r: 255, g: 255, b: 255 }
116 + }
117 + }
118 +
119 + /// Lighten each channel toward white by `pct` percent. Matches BB `lighten`.
120 + pub fn lighten(c: Rgb, pct: f32) -> Rgb {
121 + let f = |ch: u8| (ch as f32 + (255.0 - ch as f32) * (pct / 100.0)).round().clamp(0.0, 255.0) as u8;
122 + Rgb { r: f(c.r), g: f(c.g), b: f(c.b) }
123 + }
124 +
125 + /// Darken each channel toward black by `pct` percent. Matches BB `darken`.
126 + pub fn darken(c: Rgb, pct: f32) -> Rgb {
127 + let f = |ch: u8| (ch as f32 * (1.0 - pct / 100.0)).round().clamp(0.0, 255.0) as u8;
128 + Rgb { r: f(c.r), g: f(c.g), b: f(c.b) }
129 + }
130 +
131 + /// Linear blend from `a` to `b` by `t` in [0,1], per channel. Matches AF `lerp_color`.
132 + pub fn lerp(a: Rgb, b: Rgb, t: f32) -> Rgb {
133 + let f = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round().clamp(0.0, 255.0) as u8;
134 + Rgb { r: f(a.r, b.r), g: f(a.g, b.g), b: f(a.b, b.b) }
135 + }
136 +
137 + // ============================================================================
138 + // Intent resolution
139 + // ============================================================================
140 +
141 + /// Authored base intents: (TOML dotted source key, canonical token key).
142 + /// These are read straight from the theme; the token key is the CSS-var stem
143 + /// (`--{token}`) and the `rgb()` lookup key.
144 + pub const BASE_INTENTS: &[(&str, &str)] = &[
145 + ("surface.page", "surface-page"),
146 + ("surface.raised", "surface-raised"),
147 + ("surface.sunken", "surface-sunken"),
148 + ("surface.overlay", "surface-overlay"),
149 + ("content.primary", "content"),
150 + ("content.secondary", "content-secondary"),
151 + ("content.muted", "content-muted"),
152 + ("action.primary", "action"),
153 + ("status.danger", "danger"),
154 + ("status.success", "success"),
155 + ("status.warning", "warning"),
156 + ("status.info", "info"),
157 + ("line.border", "border"),
158 + ("category.one", "category-one"),
159 + ("category.two", "category-two"),
160 + ("category.three", "category-three"),
161 + ("category.four", "category-four"),
162 + ("category.five", "category-five"),
163 + ("category.six", "category-six"),
164 + ];
165 +
166 + /// A fully resolved intent layer: every token key → concrete `#rrggbb`.
167 + /// Includes both authored base intents and the computed derived intents.
168 + #[derive(Debug, Clone, Serialize)]
169 + #[serde(rename_all = "camelCase")]
170 + pub struct SemanticTokens {
171 + pub meta: ThemeMeta,
172 + /// token-key → resolved hex. Stable, deterministic ordering.
173 + pub intents: BTreeMap<String, String>,
174 + }
175 +
176 + impl SemanticTokens {
177 + /// Resolved hex for a token key, if present.
178 + pub fn hex(&self, key: &str) -> Option<&str> {
179 + self.intents.get(key).map(String::as_str)
180 + }
181 +
182 + /// Resolved RGB tuple for a token key (for egui / native consumers).
183 + pub fn rgb(&self, key: &str) -> Option<(u8, u8, u8)> {
184 + self.intents.get(key).and_then(|h| Rgb::from_hex(h)).map(Rgb::tuple)
185 + }
186 + }
187 +
188 + /// Resolve an authored theme into the full intent token set.
189 + ///
190 + /// 1. Copy each present base intent from the authored colors.
191 + /// 2. Compute the derived interactive states from the base intents, using the
192 + /// same math the apps used to apply individually (so output is identical).
193 + /// Each derived token is emitted only when its source intents exist, mirroring
194 + /// the skip-missing behavior of the rest of the crate.
195 + pub fn resolve(theme: &ThemeColors) -> SemanticTokens {
196 + let mut intents: BTreeMap<String, String> = BTreeMap::new();
197 +
198 + // 1. Base intents (authored).
199 + for (src, token) in BASE_INTENTS {
200 + if let Some(v) = theme.colors.get(*src) {
201 + intents.insert((*token).to_string(), v.clone());
202 + }
203 + }
204 +
205 + // Helper: parse an already-resolved token to Rgb.
206 + let get = |m: &BTreeMap<String, String>, k: &str| m.get(k).and_then(|h| Rgb::from_hex(h));
207 +
208 + // 2. Derived intents.
209 + let mut derived: Vec<(String, Rgb)> = Vec::new();
210 + if let Some(action) = get(&intents, "action") {
211 + derived.push(("action-hover".into(), lighten(action, 10.0)));
212 + derived.push(("action-active".into(), darken(action, 10.0)));
213 + derived.push(("content-on-action".into(), contrast_color(action)));
214 + derived.push(("focus-ring".into(), action));
215 + if let Some(page) = get(&intents, "surface-page") {
216 + derived.push(("selection".into(), lerp(page, action, 0.3)));
217 + }
218 + }
219 + if let Some(page) = get(&intents, "surface-page") {
220 + if let Some(overlay) = get(&intents, "surface-overlay") {
221 + derived.push(("row-stripe".into(), lerp(page, overlay, 0.3)));
222 + }
223 + for status in ["danger", "success", "warning", "info"] {
224 + if let Some(c) = get(&intents, status) {
225 + derived.push((format!("{status}-surface"), lerp(page, c, 0.12)));
226 + }
227 + }
228 + }
229 + if let Some(sunken) = get(&intents, "surface-sunken") {
230 + derived.push(("hover-surface".into(), sunken));
231 + }
232 + if let Some(border) = get(&intents, "border") {
233 + derived.push(("border-strong".into(), darken(border, 10.0)));
234 + }
235 +
236 + for (token, rgb) in derived {
237 + intents.insert(token, rgb.to_hex());
238 + }
239 +
240 + SemanticTokens { meta: theme.meta.clone(), intents }
241 + }
242 +
243 + /// Emit the resolved intent layer as CSS declarations (no selector), one
244 + /// ` --token: #hex;` line each, in deterministic (BTreeMap) order.
245 + pub fn intent_css_declarations(tokens: &SemanticTokens) -> String {
246 + let mut out = String::new();
247 + for (token, hex) in &tokens.intents {
248 + out.push_str(" --");
249 + out.push_str(token);
250 + out.push_str(": ");
251 + out.push_str(hex);
252 + out.push_str(";\n");
253 + }
254 + out
255 + }
256 +
257 + /// Emit the resolved intent layer as a `:root { … }` block — the single TOML →
258 + /// CSS mapping every web surface injects.
259 + pub fn intent_css_vars(tokens: &SemanticTokens) -> String {
260 + format!(":root {{\n{}}}\n", intent_css_declarations(tokens))
261 + }
262 +
263 + // ============================================================================
264 + // Loading / parsing
265 + // ============================================================================
266 +
48 267 /// Validate a theme ID contains only safe characters (alphanumeric, hyphens, underscores).
49 268 pub fn validate_theme_id(id: &str) -> Result<(), String> {
50 269 if !id
@@ -56,7 +275,7 @@ pub fn validate_theme_id(id: &str) -> Result<(), String> {
56 275 Ok(())
57 276 }
58 277
59 - /// Parse the `[meta]` section from a TOML table into `ThemeMeta`.
278 + /// Parse the `[meta]` section into `ThemeMeta`.
60 279 ///
61 280 /// Falls back to the file ID as the name and `"dark"` as the variant.
62 281 pub fn parse_meta(id: &str, table: &toml::Table, is_custom: bool) -> ThemeMeta {
@@ -72,19 +291,14 @@ pub fn parse_meta(id: &str, table: &toml::Table, is_custom: bool) -> ThemeMeta {
72 291 .unwrap_or("dark")
73 292 .to_string();
74 293
75 - ThemeMeta {
76 - id: id.to_string(),
77 - name,
78 - variant,
79 - is_custom,
80 - }
294 + ThemeMeta { id: id.to_string(), name, variant, is_custom }
81 295 }
82 296
83 - /// Extract color sections (background, foreground, accent, border) from a TOML
84 - /// table into a flat `HashMap` with keys like `"background.primary"`.
297 + /// Extract the intent color sections into a flat `HashMap` with dotted keys
298 + /// like `"surface.page"`, `"status.danger"`, `"category.one"`.
85 299 pub fn extract_colors(table: &toml::Table) -> HashMap<String, String> {
86 300 let mut colors = HashMap::new();
87 - for section in &["background", "foreground", "accent", "border"] {
301 + for section in COLOR_SECTIONS {
88 302 if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) {
89 303 for (key, val) in sect {
90 304 if let Some(color) = val.as_str() {
@@ -161,11 +375,7 @@ pub fn find_theme_path(dirs: &[(PathBuf, bool)], id: &str) -> Option<(PathBuf, b
161 375 }
162 376
163 377 /// Parse a complete theme (metadata + colors) from raw TOML content, with no
164 - /// filesystem access.
165 - ///
166 - /// For callers that embed themes at compile time (e.g. the MNW server bundles
167 - /// `shared/themes/*.toml` into the binary) rather than reading a directory.
168 - /// Keeps TOML parsing inside this crate so consumers need only `theme_common`.
378 + /// filesystem access. For callers that embed themes at compile time.
169 379 pub fn parse_theme_str(id: &str, content: &str, is_custom: bool) -> Result<ThemeColors, String> {
170 380 validate_theme_id(id)?;
171 381 let table: toml::Table = content
@@ -196,11 +406,15 @@ pub fn load_theme(dirs: &[(PathBuf, bool)], id: &str) -> Result<ThemeColors, Str
196 406 Ok(ThemeColors { meta, colors })
197 407 }
198 408
409 + /// Load a theme and resolve it to the full intent token set in one step.
410 + pub fn load_semantic(dirs: &[(PathBuf, bool)], id: &str) -> Result<SemanticTokens, String> {
411 + Ok(resolve(&load_theme(dirs, id)?))
412 + }
413 +
199 414 /// Import a theme TOML file into the custom themes directory.
200 415 ///
201 - /// Validates that the file is parseable TOML with the expected color sections,
202 - /// then copies it to `custom_dir/{id}.toml` where `id` is the file stem.
203 - /// Creates `custom_dir` if it doesn't exist. Returns the theme metadata.
416 + /// Validates that the file is parseable TOML with at least one intent color
417 + /// section, then copies it to `custom_dir/{id}.toml`. Returns the theme metadata.
204 418 pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta, String> {
205 419 let content = std::fs::read_to_string(source_path)
206 420 .map_err(|e| format!("Failed to read {}: {}", source_path.display(), e))?;
@@ -209,12 +423,14 @@ pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta,
209 423 .parse()
210 424 .map_err(|e| format!("Invalid TOML: {}", e))?;
211 425
212 - // Verify it has at least one color section
213 - let has_colors = ["background", "foreground", "accent", "border"]
426 + let has_colors = COLOR_SECTIONS
214 427 .iter()
215 428 .any(|s| table.get(*s).and_then(|v| v.as_table()).is_some());
216 429 if !has_colors {
217 - return Err("Theme file must have at least one color section (background, foreground, accent, or border)".to_string());
430 + return Err(format!(
431 + "Theme file must have at least one color section ({})",
432 + COLOR_SECTIONS.join(", ")
433 + ));
218 434 }
219 435
220 436 let id = source_path
@@ -237,8 +453,7 @@ pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta,
237 453 /// Delete a custom theme by ID.
238 454 ///
239 455 /// Only operates on `custom_dir` — bundled themes are not deletable through
240 - /// this entry point. Returns `Err` if the ID is invalid, the file does not
241 - /// exist, or the underlying remove fails.
456 + /// this entry point.
242 457 pub fn delete_theme(custom_dir: &Path, id: &str) -> Result<(), String> {
243 458 validate_theme_id(id)?;
244 459
@@ -251,31 +466,32 @@ pub fn delete_theme(custom_dir: &Path, id: &str) -> Result<(), String> {
251 466 .map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
252 467 }
253 468
254 - /// A four-color palette for preview chips, swatches, etc.
255 - ///
256 - /// Smaller than `ThemeColors`: only the `primary` value from each of the four
257 - /// canonical sections, so callers rendering a list of theme thumbnails don't
258 - /// need to allocate a full `HashMap` per row.
469 + /// A four-color preview for theme thumbnails: the representative swatch from
470 + /// each of the principal roles.
259 471 #[derive(Debug, Clone, Serialize)]
260 472 #[serde(rename_all = "camelCase")]
261 473 pub struct ThemePreview {
262 474 pub meta: ThemeMeta,
475 + /// Page background (`surface.page`).
263 476 pub background: Option<String>,
477 + /// Body text (`content.primary`).
264 478 pub foreground: Option<String>,
479 + /// Brand/interactive color (`action.primary`).
265 480 pub accent: Option<String>,
481 + /// Divider/outline color (`line.border`).
266 482 pub border: Option<String>,
267 483 }
268 484
269 - fn primary_color(table: &toml::Table, section: &str) -> Option<String> {
485 + fn color_at(table: &toml::Table, section: &str, key: &str) -> Option<String> {
270 486 table
271 487 .get(section)
272 488 .and_then(|s| s.as_table())
273 - .and_then(|s| s.get("primary"))
489 + .and_then(|s| s.get(key))
274 490 .and_then(|v| v.as_str())
275 491 .map(|s| s.to_string())
276 492 }
277 493
278 - /// Load just the primary colors for a theme — for UI previews / thumbnails.
494 + /// Load just the preview swatches for a theme — for UI thumbnails.
279 495 pub fn load_theme_preview(dirs: &[(PathBuf, bool)], id: &str) -> Result<ThemePreview, String> {
280 496 validate_theme_id(id)?;
281 497
@@ -291,17 +507,14 @@ pub fn load_theme_preview(dirs: &[(PathBuf, bool)], id: &str) -> Result<ThemePre
291 507
292 508 Ok(ThemePreview {
293 509 meta: parse_meta(id, &table, is_custom),
294 - background: primary_color(&table, "background"),
295 - foreground: primary_color(&table, "foreground"),
296 - accent: primary_color(&table, "accent"),
297 - border: primary_color(&table, "border"),
510 + background: color_at(&table, "surface", "page"),
511 + foreground: color_at(&table, "content", "primary"),
512 + accent: color_at(&table, "action", "primary"),
513 + border: color_at(&table, "line", "border"),
298 514 })
299 515 }
300 516
301 517 /// Export a theme to a user-chosen path.
302 - ///
303 - /// Finds the theme by ID in the given directories and copies the TOML file
304 - /// to `dest_path`.
305 518 pub fn export_theme(dirs: &[(PathBuf, bool)], id: &str, dest_path: &Path) -> Result<(), String> {
306 519 validate_theme_id(id)?;
307 520
@@ -314,67 +527,10 @@ pub fn export_theme(dirs: &[(PathBuf, bool)], id: &str, dest_path: &Path) -> Res
314 527 Ok(())
315 528 }
316 529
317 - /// The canonical TOML-key -> CSS-custom-property mapping for the primitive layer.
318 - ///
319 - /// This is the single source of truth for how a theme-common palette becomes CSS
320 - /// variables. Every web surface (MNW server-rendered pages, and eventually
321 - /// GoingsOn — which today hand-maintains an equivalent `COLOR_MAP` in JS) emits
322 - /// the *same* variable names from this table, so adding a token is one edit here.
323 - ///
324 - /// The 14 primitive tokens: 4 backgrounds, 3 foregrounds, 6 accents, 1 border.
325 - /// Apps derive their semantic roles (`--surface`, `--danger`, diff/health, …)
326 - /// from these in their own base CSS; the crate stays a pure palette.
327 - pub const PRIMITIVE_VARS: &[(&str, &str)] = &[
328 - ("background.primary", "--bg-primary"),
329 - ("background.secondary", "--bg-secondary"),
330 - ("background.tertiary", "--bg-tertiary"),
331 - ("background.surface", "--bg-surface"),
332 - ("foreground.primary", "--fg-primary"),
333 - ("foreground.secondary", "--fg-secondary"),
334 - ("foreground.muted", "--fg-muted"),
335 - ("accent.red", "--accent-red"),
336 - ("accent.green", "--accent-green"),
337 - ("accent.blue", "--accent-blue"),
338 - ("accent.yellow", "--accent-yellow"),
339 - ("accent.purple", "--accent-purple"),
340 - ("accent.cyan", "--accent-cyan"),
341 - ("border.default", "--border-default"),
342 - ];
343 -
344 - /// Emit the primitive layer as CSS declarations (no selector wrapper).
345 - ///
346 - /// One ` --name: value;` line per token that the theme actually defines, in the
347 - /// canonical [`PRIMITIVE_VARS`] order. Missing tokens are skipped rather than
348 - /// emitted empty, mirroring how the desktop apps apply only present keys. The
349 - /// caller chooses the selector — use [`to_css_vars`] for a ready `:root` block,
350 - /// or wrap these declarations in any scope (e.g. a creator canvas class).
351 - pub fn to_css_declarations(theme: &ThemeColors) -> String {
352 - let mut out = String::new();
353 - for (toml_key, css_var) in PRIMITIVE_VARS {
354 - if let Some(value) = theme.colors.get(*toml_key) {
355 - out.push_str(" ");
356 - out.push_str(css_var);
357 - out.push_str(": ");
358 - out.push_str(value);
359 - out.push_str(";\n");
360 - }
361 - }
362 - out
363 - }
364 -
365 - /// Emit the primitive layer as a CSS `:root { … }` block.
366 - ///
367 - /// This is the single TOML -> CSS mapping for the web. Inject the result into a
368 - /// page `<head>` (or serve it as a stylesheet) to apply a theme's primitive
369 - /// palette; the app's semantic layer derives from these variables.
370 - pub fn to_css_vars(theme: &ThemeColors) -> String {
371 - format!(":root {{\n{}}}\n", to_css_declarations(theme))
372 - }
373 -
374 530 /// Construct a dev fallback theme directory path.
375 531 ///
376 532 /// Given a `CARGO_MANIFEST_DIR`, walks up `levels` parent directories and
377 - /// appends `"themes"`. Returns the path if the directory exists.
533 + /// appends `MNW/shared/themes`. Returns the path if the directory exists.
378 534 pub fn dev_themes_dir(manifest_dir: &Path, levels: usize) -> Option<PathBuf> {
379 535 let mut path = manifest_dir.to_path_buf();
380 536 for _ in 0..levels {
@@ -393,6 +549,8 @@ mod tests {
393 549 use super::*;
394 550 use std::fs;
395 551
Lines truncated
@@ -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"
@@ -5,24 +5,33 @@
5 5 name = "Dawnfox"
6 6 variant = "light"
7 7
8 - [background]
9 - primary = "#faf4ed"
10 - secondary = "#ebe5df"
11 - tertiary = "#ebe0df"
12 - surface = "#f5efe8"
8 + [surface]
9 + page = "#faf4ed"
10 + raised = "#f5efe8"
11 + sunken = "#ebe0df"
12 + overlay = "#ebe5df"
13 13
14 - [foreground]
15 - primary = "#575279"
14 + [content]
15 + primary = "#575279"
16 16 secondary = "#625c87"
17 - muted = "#a8a3b3"
17 + muted = "#a8a3b3"
18 18
19 - [accent]
20 - red = "#b4637a"
21 - green = "#618774"
22 - blue = "#286983"
23 - yellow = "#ea9d34"
24 - purple = "#907aa9"
25 - cyan = "#56949f"
19 + [action]
20 + primary = "#286983"
26 21
27 - [border]
28 - default = "#bdbfc9"
22 + [status]
23 + danger = "#b4637a"
24 + success = "#618774"
25 + warning = "#ea9d34"
26 + info = "#56949f"
27 +
28 + [line]
29 + border = "#bdbfc9"
30 +
31 + [category]
32 + one = "#b4637a"
33 + two = "#618774"
34 + three = "#286983"
35 + four = "#ea9d34"
36 + five = "#907aa9"
37 + six = "#56949f"
@@ -5,24 +5,33 @@
5 5 name = "Dracula"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#222430"
10 - secondary = "#191A21"
11 - tertiary = "#44475a"
12 - surface = "#282A36"
8 + [surface]
9 + page = "#222430"
10 + raised = "#282A36"
11 + sunken = "#44475a"
12 + overlay = "#191A21"
13 13
14 - [foreground]
15 - primary = "#f8f8f2"
14 + [content]
15 + primary = "#f8f8f2"
16 16 secondary = "#f8f8f2"
17 - muted = "#6272A4"
17 + muted = "#6272A4"
18 18
19 - [accent]
20 - red = "#ff5555"
21 - green = "#50fa7b"
22 - blue = "#8be9fd"
23 - yellow = "#f1fa8c"
24 - purple = "#BD93F9"
25 - cyan = "#8be9fd"
19 + [action]
20 + primary = "#8be9fd"
26 21
27 - [border]
28 - default = "#44475a"
22 + [status]
23 + danger = "#ff5555"
24 + success = "#50fa7b"
25 + warning = "#f1fa8c"
26 + info = "#8be9fd"
27 +
28 + [line]
29 + border = "#44475a"
30 +
31 + [category]
32 + one = "#ff5555"
33 + two = "#50fa7b"
34 + three = "#8be9fd"
35 + four = "#f1fa8c"
36 + five = "#BD93F9"
37 + six = "#8be9fd"
@@ -5,24 +5,33 @@
5 5 name = "Everforest"
6 6 variant = "dark"
7 7
8 - [background]
9 - primary = "#2d353b"
10 - secondary = "#272e33"
11 - tertiary = "#3d484d"
12 - surface = "#343f44"
8 + [surface]
9 + page = "#2d353b"
10 + raised = "#343f44"
11 + sunken = "#3d484d"
12 + overlay = "#272e33"
13 13
14 - [foreground]
15 - primary = "#d3c6aa"
14 + [content]
15 + primary = "#d3c6aa"
16 16 secondary = "#9da9a0"
17 - muted = "#7a8478"
17 + muted = "#7a8478"
18 18
19 - [accent]
20 - red = "#e67e80"
21 - green = "#a7c080"
22 - blue = "#7fbbb3"
23 - yellow = "#dbbc7f"
24 - purple = "#d699b6"
25 - cyan = "#83c092"
19 + [action]
20 + primary = "#7fbbb3"
26 21
27 - [border]
28 - default = "#3d484d"
22 + [status]
23 + danger = "#e67e80"
24 + success = "#a7c080"
25 + warning = "#dbbc7f"
26 + info = "#83c092"
27 +
28 + [line]
29 + border = "#3d484d"
30 +
31 + [category]
32 + one = "#e67e80"
33 + two = "#a7c080"
34 + three = "#7fbbb3"
35 + four = "#dbbc7f"
36 + five = "#d699b6"
37 + six = "#83c092"
@@ -5,24 +5,33 @@
5 5 name = "Flatwhite"
6 6 variant = "light"
7 7
8 - [background]
9 - primary = "#f1ece4"
10 - secondary = "#e4ddd2"
11 - tertiary = "#dcd3c6"
12 - surface = "#f7f3ee"
8 + [surface]
9 + page = "#f1ece4"
10 + raised = "#f7f3ee"
11 + sunken = "#dcd3c6"
12 + overlay = "#e4ddd2"
13 13
14 - [foreground]
15 - primary = "#605a52"
14 + [content]
15 + primary = "#605a52"
16 16 secondary = "#786d5e"
17 - muted = "#9a8b78"
17 + muted = "#9a8b78"
18 18
19 - [accent]
20 - red = "#ff1414"
21 - green = "#2db448"
22 - blue = "#4c5361"
23 - yellow = "#f2a60d"
24 - purple = "#614c61"
25 - cyan = "#465953"
19 + [action]
20 + primary = "#4c5361"
26 21
27 - [border]
28 - default = "#93836c"
22 + [status]
23 + danger = "#ff1414"
24 + success = "#2db448"
25 + warning = "#f2a60d"
26 + info = "#465953"
27 +
28 + [line]
29 + border = "#93836c"
30 +
31 + [category]
32 + one = "#ff1414"
33 + two = "#2db448"
34 + three = "#4c5361"
35 + four = "#f2a60d"
36 + five = "#614c61"
37 + six = "#465953"
@@ -5,24 +5,33 @@
5 5 name = "GoingsOn"
6 6 variant = "light"
7 7
8 - [background]
9 - primary = "#E0E4FA"
10 - secondary = "#CDD3F0"
11 - tertiary = "#BAC2E6"
12 - surface = "#FFFFFF"
8 + [surface]
9 + page = "#E0E4FA"
10 + raised = "#FFFFFF"
11 + sunken = "#BAC2E6"
12 + overlay = "#CDD3F0"
13 13
14 - [foreground]
15 - primary = "#000000"
14 + [content]
15 + primary = "#000000"
16 16 secondary = "#2D2D2D"
17 - muted = "#6B6B6B"
17 + muted = "#6B6B6B"
18 18
19 - [accent]
20 - red = "#DC3545"
21 - green = "#5CB85C"
22 - blue = "#6196FF"
23 - yellow = "#F7D154"
24 - purple = "#7B68EE"
25 - cyan = "#17A2B8"
19 + [action]
20 + primary = "#6196FF"
26 21
27 - [border]
28 - default = "#000000"
22 + [status]
23 + danger = "#DC3545"
24 + success = "#5CB85C"
25 + warning = "#F7D154"
26 + info = "#17A2B8"
27 +
28 + [line]
29 + border = "#000000"
30 +
31 + [category]
32 + one = "#DC3545"
33 + two = "#5CB85C"
34 + three = "#6196FF"
35 + four = "#F7D154"
36 + five = "#7B68EE"
37 + six = "#17A2B8"