max / balanced_breakfast
5 files changed,
+125 insertions,
-245 deletions
| @@ -5115,6 +5115,7 @@ dependencies = [ | |||
| 5115 | 5115 | "reqwest 0.12.28", | |
| 5116 | 5116 | "serde", | |
| 5117 | 5117 | "serde_json", | |
| 5118 | + | "sha2", | |
| 5118 | 5119 | "thiserror 2.0.18", | |
| 5119 | 5120 | "tokio", | |
| 5120 | 5121 | "tokio-stream", | |
| @@ -5619,7 +5620,7 @@ dependencies = [ | |||
| 5619 | 5620 | ||
| 5620 | 5621 | [[package]] | |
| 5621 | 5622 | name = "theme-common" | |
| 5622 | - | version = "0.3.1" | |
| 5623 | + | version = "0.6.0" | |
| 5623 | 5624 | dependencies = [ | |
| 5624 | 5625 | "serde", | |
| 5625 | 5626 | "toml 0.8.2", |
| @@ -117,30 +117,71 @@ | |||
| 117 | 117 | token audit. Phase F1 cleanup: consume or remove. | |
| 118 | 118 | ========================================================================== */ | |
| 119 | 119 | :root { | |
| 120 | - | --bg-primary: #faf8f5; /* themed: background.primary */ | |
| 121 | - | --bg-secondary: #f5f0e8; /* themed: background.secondary */ | |
| 122 | - | --bg-tertiary: #ebe4d8; /* themed: background.tertiary */ | |
| 123 | - | --text-primary: #3d3225; /* themed: foreground.primary */ | |
| 124 | - | --text-secondary: #6b5d4d; /* themed: foreground.secondary */ | |
| 125 | - | --text-muted: #9a8b78; /* themed: foreground.muted */ | |
| 126 | - | --accent-red: #c94b4b; /* themed: accent.red */ | |
| 127 | - | --accent-red-hover: #d65d5d; /* derived in themes.js — consumed by .btn-danger:hover (F2) */ | |
| 128 | - | --accent-green: #6b9b5a; /* themed: accent.green */ | |
| 129 | - | --accent-blue: #4a8ebd; /* themed: accent.blue */ | |
| 130 | - | --accent-blue-hover: #5e9dca; /* derived in themes.js */ | |
| 131 | - | --accent-yellow: #e8a841; /* themed: accent.yellow */ | |
| 132 | - | --accent-yellow-light: #f4c56d; /* derived in themes.js */ | |
| 133 | - | --accent-purple: #8b6bbf; /* themed: accent.purple — consumed by .tag/.badge[data-color="purple"] (F2) */ | |
| 134 | - | --accent-cyan: #5ab5b5; /* themed: accent.cyan — consumed by .tag/.badge[data-color="cyan"] (F2) */ | |
| 135 | - | --border: #e0d6c8; /* themed: border.default */ | |
| 136 | - | --border-dark: #d4c8b5; /* derived in themes.js */ | |
| 137 | - | --text-on-accent: #ffffff; /* derived in themes.js (contrast vs accent.blue) */ | |
| 120 | + | /* === INTENT LAYER (themeable) === | |
| 121 | + | Resolved intent tokens applied at runtime by js/themes.js (setProperty on | |
| 122 | + | :root). Defaults below are "flatwhite" (BB's light default) for first | |
| 123 | + | paint; theme-common resolves every theme incl. the OKLab-derived states, | |
| 124 | + | shared with audiofiles / GoingsOn / MNW. */ | |
| 125 | + | --surface-page: #f1ece4; | |
| 126 | + | --surface-raised: #f7f3ee; | |
| 127 | + | --surface-sunken: #dcd3c6; | |
| 128 | + | --surface-overlay: #e4ddd2; | |
| 129 | + | --content: #605a52; | |
| 130 | + | --content-secondary: #786d5e; | |
| 131 | + | --content-muted: #9a8b78; | |
| 132 | + | --content-on-action: #ffffff; | |
| 133 | + | --action: #4c5361; | |
| 134 | + | --action-hover: #5a616f; | |
| 135 | + | --action-active: #3f4553; | |
| 136 | + | --danger: #ff1414; | |
| 137 | + | --success: #2db448; | |
| 138 | + | --warning: #f2a60d; | |
| 139 | + | --info: #465953; | |
| 140 | + | --danger-surface: #f9d4c8; | |
| 141 | + | --success-surface: #d8e4cd; | |
| 142 | + | --warning-surface: #f2e2cc; | |
| 143 | + | --info-surface: #d5d4cd; | |
| 144 | + | --border: #93836c; | |
| 145 | + | --border-strong: #84745e; | |
| 146 | + | --focus-ring: #4c5361; | |
| 147 | + | --selection: #cecbc8; | |
| 148 | + | --row-stripe: #eae4db; | |
| 149 | + | --hover-surface: #dcd3c6; | |
| 150 | + | --category-one: #ff1414; | |
| 151 | + | --category-two: #2db448; | |
| 152 | + | --category-three: #4c5361; | |
| 153 | + | --category-four: #f2a60d; | |
| 154 | + | --category-five: #614c61; | |
| 155 | + | --category-six: #465953; | |
| 156 | + | ||
| 157 | + | /* === BRAND ALIASES — BB vocabulary over the intent layer === | |
| 158 | + | Accent hues map to the categorical ramp (preserves the [data-color] | |
| 159 | + | badge colors). Per-status hovers BB needs that aren't universal intents | |
| 160 | + | are derived here via color-mix in OKLCH (perceptual, matches the crate's | |
| 161 | + | approach) rather than recomputed in JS. */ | |
| 162 | + | --bg-primary: var(--surface-page); | |
| 163 | + | --bg-secondary: var(--surface-overlay); | |
| 164 | + | --bg-tertiary: var(--surface-sunken); | |
| 165 | + | --text-primary: var(--content); | |
| 166 | + | --text-secondary: var(--content-secondary); | |
| 167 | + | --text-muted: var(--content-muted); | |
| 168 | + | --accent-red: var(--category-one); | |
| 169 | + | --accent-green: var(--category-two); | |
| 170 | + | --accent-blue: var(--category-three); | |
| 171 | + | --accent-yellow: var(--category-four); | |
| 172 | + | --accent-purple: var(--category-five); | |
| 173 | + | --accent-cyan: var(--category-six); | |
| 174 | + | --accent-blue-hover: var(--action-hover); | |
| 175 | + | --accent-red-hover: color-mix(in oklch, var(--accent-red) 90%, white); /* .btn-danger:hover */ | |
| 176 | + | --accent-yellow-light: color-mix(in oklch, var(--accent-yellow) 82%, white); | |
| 177 | + | --border-dark: var(--border-strong); | |
| 178 | + | --text-on-accent: var(--content-on-action); | |
| 179 | + | --shadow-color: color-mix(in oklch, var(--content-muted) 88%, black); | |
| 138 | 180 | --border-width: 2px; /* invariant */ | |
| 139 | 181 | --radius-sm: 5px; /* invariant */ | |
| 140 | 182 | --radius-lg: 10px; /* invariant */ | |
| 141 | 183 | --radius-xl: 20px; /* invariant */ | |
| 142 | 184 | --shadow-offset: 3px; /* invariant — BB neobrute signature */ | |
| 143 | - | --shadow-color: #6b5d4d; /* derived in themes.js (darken --text-muted) */ | |
| 144 | 185 | --shadow-brutal: var(--shadow-offset) var(--shadow-offset) 0 var(--shadow-color); /* invariant composition */ | |
| 145 | 186 | } | |
| 146 | 187 | /* F5 cleanup (2026-06-02): dropped --bg-surface (themed but never consumed), |
| @@ -12,115 +12,18 @@ | |||
| 12 | 12 | let themeCache = {}; | |
| 13 | 13 | ||
| 14 | 14 | /** | |
| 15 | - | * Parse a hex color string to {r, g, b}. | |
| 16 | - | * @param {string} hex - Hex color string (e.g. '#ff0000' or 'ff0000'). | |
| 17 | - | * @returns {{r: number, g: number, b: number}} RGB components (0-255). | |
| 15 | + | * Apply a theme's resolved intent tokens as CSS variables on :root. Each | |
| 16 | + | * intent key becomes `--{key}` (e.g. `surface-page` -> `--surface-page`); | |
| 17 | + | * styles.css aliases BB's brand vocabulary over them. The interactive | |
| 18 | + | * states (hover/active/selection/contrast) are resolved by theme-common in | |
| 19 | + | * OKLab — no JS color math here anymore. | |
| 20 | + | * @param {Object<string, string>} intents - Map of intent token -> hex value. | |
| 18 | 21 | */ | |
| 19 | - | function hexToRgb(hex) { | |
| 20 | - | const h = hex.replace('#', ''); | |
| 21 | - | const r = parseInt(h.substring(0, 2), 16) || 0; | |
| 22 | - | const g = parseInt(h.substring(2, 4), 16) || 0; | |
| 23 | - | const b = parseInt(h.substring(4, 6), 16) || 0; | |
| 24 | - | return { r, g, b }; | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | /** | |
| 28 | - | * Lighten a hex color by a percentage (0-100). | |
| 29 | - | * @param {string} hex - Hex color string. | |
| 30 | - | * @param {number} pct - Lightening percentage (0-100). | |
| 31 | - | * @returns {string} Lightened hex color string. | |
| 32 | - | */ | |
| 33 | - | function lighten(hex, pct) { | |
| 34 | - | const { r, g, b } = hexToRgb(hex); | |
| 35 | - | const f = pct / 100; | |
| 36 | - | return '#' + [ | |
| 37 | - | Math.min(255, Math.round(r + (255 - r) * f)), | |
| 38 | - | Math.min(255, Math.round(g + (255 - g) * f)), | |
| 39 | - | Math.min(255, Math.round(b + (255 - b) * f)), | |
| 40 | - | ].map(c => c.toString(16).padStart(2, '0')).join(''); | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | /** | |
| 44 | - | * Darken a hex color by a percentage (0-100). | |
| 45 | - | * @param {string} hex - Hex color string. | |
| 46 | - | * @param {number} pct - Darkening percentage (0-100). | |
| 47 | - | * @returns {string} Darkened hex color string. | |
| 48 | - | */ | |
| 49 | - | function darken(hex, pct) { | |
| 50 | - | const { r, g, b } = hexToRgb(hex); | |
| 51 | - | const f = 1 - pct / 100; | |
| 52 | - | return '#' + [ | |
| 53 | - | Math.round(r * f), | |
| 54 | - | Math.round(g * f), | |
| 55 | - | Math.round(b * f), | |
| 56 | - | ].map(c => c.toString(16).padStart(2, '0')).join(''); | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | /** | |
| 60 | - | * Return white or black depending on which contrasts better against `hex`. | |
| 61 | - | * @param {string} hex - Hex color string. | |
| 62 | - | * @returns {string} '#000000' or '#ffffff'. | |
| 63 | - | */ | |
| 64 | - | function contrastColor(hex) { | |
| 65 | - | const { r, g, b } = hexToRgb(hex); | |
| 66 | - | const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; | |
| 67 | - | return luminance > 0.5 ? '#000000' : '#ffffff'; | |
| 68 | - | } | |
| 69 | - | ||
| 70 | - | // Direct mappings: TOML dot-path → CSS custom property. | |
| 71 | - | // F5 (2026-06-02): dropped 'background.surface' → '--bg-surface' (no CSS | |
| 72 | - | // rule consumed it). TOML files may still declare background.surface; it's | |
| 73 | - | // silently ignored here. Down to 13 mapped slots. | |
| 74 | - | const COLOR_MAP = { | |
| 75 | - | 'background.primary': '--bg-primary', | |
| 76 | - | 'background.secondary': '--bg-secondary', | |
| 77 | - | 'background.tertiary': '--bg-tertiary', | |
| 78 | - | 'foreground.primary': '--text-primary', | |
| 79 | - | 'foreground.secondary': '--text-secondary', | |
| 80 | - | 'foreground.muted': '--text-muted', | |
| 81 | - | 'accent.red': '--accent-red', | |
| 82 | - | 'accent.green': '--accent-green', | |
| 83 | - | 'accent.blue': '--accent-blue', | |
| 84 | - | 'accent.yellow': '--accent-yellow', | |
| 85 | - | 'accent.purple': '--accent-purple', | |
| 86 | - | 'accent.cyan': '--accent-cyan', | |
| 87 | - | 'border.default': '--border', | |
| 88 | - | }; | |
| 89 | - | ||
| 90 | - | /** | |
| 91 | - | * Apply a theme's colors to CSS variables, deriving BB-specific vars. | |
| 92 | - | * @param {Object<string, string>} colors - Map of TOML dot-path keys to hex color values. | |
| 93 | - | */ | |
| 94 | - | function applyTheme(colors) { | |
| 22 | + | function applyTheme(intents) { | |
| 95 | 23 | const root = document.documentElement; | |
| 96 | - | ||
| 97 | - | // Direct mappings (14 slots) | |
| 98 | - | for (const [tomlKey, cssVar] of Object.entries(COLOR_MAP)) { | |
| 99 | - | if (colors[tomlKey]) { | |
| 100 | - | root.style.setProperty(cssVar, colors[tomlKey]); | |
| 101 | - | } | |
| 24 | + | for (const [token, value] of Object.entries(intents)) { | |
| 25 | + | root.style.setProperty(`--${token}`, value); | |
| 102 | 26 | } | |
| 103 | - | ||
| 104 | - | // Derived: accent hover states | |
| 105 | - | root.style.setProperty('--accent-red-hover', lighten(colors['accent.red'], 10)); | |
| 106 | - | root.style.setProperty('--accent-blue-hover', lighten(colors['accent.blue'], 10)); | |
| 107 | - | ||
| 108 | - | // Derived: yellow highlight — 15% lighten (vs 10% for accent) because | |
| 109 | - | // yellow needs more contrast shift against light backgrounds | |
| 110 | - | root.style.setProperty('--accent-yellow-light', lighten(colors['accent.yellow'], 15)); | |
| 111 | - | ||
| 112 | - | // Derived: border | |
| 113 | - | root.style.setProperty('--border-dark', darken(colors['border.default'], 10)); | |
| 114 | - | ||
| 115 | - | // F5 (2026-06-02): dropped --shadow and --shadow-hover derivations | |
| 116 | - | // (no CSS rule consumed them). --shadow-color (used by --shadow-brutal) | |
| 117 | - | // stays. | |
| 118 | - | ||
| 119 | - | // Derived: neobrute shadow color from muted foreground | |
| 120 | - | root.style.setProperty('--shadow-color', darken(colors['foreground.muted'], 10)); | |
| 121 | - | ||
| 122 | - | // Derived: text color for use on accent backgrounds (buttons, badges) | |
| 123 | - | root.style.setProperty('--text-on-accent', contrastColor(colors['accent.blue'])); | |
| 124 | 27 | } | |
| 125 | 28 | ||
| 126 | 29 | /** | |
| @@ -140,7 +43,7 @@ | |||
| 140 | 43 | } | |
| 141 | 44 | } | |
| 142 | 45 | ||
| 143 | - | applyTheme(theme.colors); | |
| 46 | + | applyTheme(theme.intents); | |
| 144 | 47 | currentThemeId = themeId; | |
| 145 | 48 | invoke('set_config', { key: 'bb-theme', value: themeId }); | |
| 146 | 49 |
| @@ -6,7 +6,7 @@ use tracing::instrument; | |||
| 6 | 6 | ||
| 7 | 7 | use super::error::ApiError; | |
| 8 | 8 | ||
| 9 | - | pub use theme_common::{ThemeColors, ThemeMeta}; | |
| 9 | + | pub use theme_common::{SemanticTokens, ThemeMeta}; | |
| 10 | 10 | ||
| 11 | 11 | /// Returns theme directories in priority order (later overrides earlier by ID). | |
| 12 | 12 | /// Each entry is `(path, is_custom)`. | |
| @@ -46,9 +46,9 @@ pub fn list_themes(app: AppHandle) -> Result<Vec<ThemeMeta>, ApiError> { | |||
| 46 | 46 | ||
| 47 | 47 | #[tauri::command] | |
| 48 | 48 | #[instrument(skip_all)] | |
| 49 | - | pub fn get_theme(app: AppHandle, id: String) -> Result<ThemeColors, ApiError> { | |
| 49 | + | pub fn get_theme(app: AppHandle, id: String) -> Result<SemanticTokens, ApiError> { | |
| 50 | 50 | let dirs = theme_dirs(&app); | |
| 51 | - | theme_common::load_theme(&dirs, &id).map_err(ApiError::internal) | |
| 51 | + | theme_common::load_semantic(&dirs, &id).map_err(ApiError::internal) | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | 54 | #[tauri::command] |
| @@ -1,136 +1,71 @@ | |||
| 1 | - | //! Theme TOML parsing integration tests (unit-level, no AppHandle needed). | |
| 2 | - | ||
| 3 | - | // ── Theme Parsing ──────────────────────────────────────────────────── | |
| 1 | + | //! Theme parsing/resolution integration tests against the shared crate. | |
| 4 | 2 | ||
| 5 | 3 | #[test] | |
| 6 | - | fn theme_toml_parse_full() { | |
| 7 | - | let toml_str = r##" | |
| 4 | + | fn theme_resolves_intents() { | |
| 5 | + | let toml = r##" | |
| 8 | 6 | [meta] | |
| 9 | 7 | name = "Ocean Dark" | |
| 10 | 8 | variant = "dark" | |
| 11 | 9 | ||
| 12 | - | [background] | |
| 13 | - | primary = "#1a1a2e" | |
| 14 | - | secondary = "#16213e" | |
| 10 | + | [surface] | |
| 11 | + | page = "#1a1a2e" | |
| 12 | + | raised = "#16213e" | |
| 13 | + | sunken = "#0f1626" | |
| 15 | 14 | ||
| 16 | - | [foreground] | |
| 15 | + | [content] | |
| 17 | 16 | primary = "#e0e0e0" | |
| 18 | 17 | secondary = "#a0a0a0" | |
| 18 | + | muted = "#707070" | |
| 19 | 19 | ||
| 20 | - | [accent] | |
| 20 | + | [action] | |
| 21 | 21 | primary = "#0f3460" | |
| 22 | - | highlight = "#e94560" | |
| 23 | 22 | ||
| 24 | - | [border] | |
| 25 | - | default = "#2a2a4a" | |
| 23 | + | [status] | |
| 24 | + | danger = "#e94560" | |
| 25 | + | success = "#3ad29f" | |
| 26 | + | warning = "#f5c451" | |
| 27 | + | info = "#4cc9f0" | |
| 28 | + | ||
| 29 | + | [line] | |
| 30 | + | border = "#2a2a4a" | |
| 31 | + | ||
| 32 | + | [category] | |
| 33 | + | one = "#e94560" | |
| 34 | + | two = "#3ad29f" | |
| 35 | + | three = "#0f3460" | |
| 36 | + | four = "#f5c451" | |
| 37 | + | five = "#9d4edd" | |
| 38 | + | six = "#4cc9f0" | |
| 26 | 39 | "##; | |
| 27 | 40 | ||
| 28 | - | let table: toml::Table = toml_str.parse().unwrap(); | |
| 29 | - | ||
| 30 | - | // Verify meta parsing | |
| 31 | - | let meta = table.get("meta").unwrap().as_table().unwrap(); | |
| 32 | - | assert_eq!(meta.get("name").unwrap().as_str().unwrap(), "Ocean Dark"); | |
| 33 | - | assert_eq!(meta.get("variant").unwrap().as_str().unwrap(), "dark"); | |
| 34 | - | ||
| 35 | - | // Verify color sections can be extracted (replicates get_theme logic) | |
| 36 | - | let mut colors = std::collections::HashMap::new(); | |
| 37 | - | for section in &["background", "foreground", "accent", "border"] { | |
| 38 | - | if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) { | |
| 39 | - | for (key, val) in sect { | |
| 40 | - | if let Some(color) = val.as_str() { | |
| 41 | - | colors.insert(format!("{}.{}", section, key), color.to_string()); | |
| 42 | - | } | |
| 43 | - | } | |
| 44 | - | } | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | assert_eq!(colors.get("background.primary").unwrap(), "#1a1a2e"); | |
| 48 | - | assert_eq!(colors.get("foreground.primary").unwrap(), "#e0e0e0"); | |
| 49 | - | assert_eq!(colors.get("accent.highlight").unwrap(), "#e94560"); | |
| 50 | - | assert_eq!(colors.get("border.default").unwrap(), "#2a2a4a"); | |
| 51 | - | assert_eq!(colors.len(), 7); | |
| 41 | + | let theme = theme_common::parse_theme_str("ocean-dark", toml, false).unwrap(); | |
| 42 | + | assert_eq!(theme.meta.name, "Ocean Dark"); | |
| 43 | + | assert_eq!(theme.meta.variant, "dark"); | |
| 44 | + | ||
| 45 | + | let t = theme_common::resolve(&theme); | |
| 46 | + | // Base intents pass through. | |
| 47 | + | assert_eq!(t.hex("surface-page"), Some("#1a1a2e")); | |
| 48 | + | assert_eq!(t.hex("content"), Some("#e0e0e0")); | |
| 49 | + | assert_eq!(t.hex("action"), Some("#0f3460")); | |
| 50 | + | assert_eq!(t.hex("danger"), Some("#e94560")); | |
| 51 | + | assert_eq!(t.hex("border"), Some("#2a2a4a")); | |
| 52 | + | assert_eq!(t.hex("category-five"), Some("#9d4edd")); | |
| 53 | + | // Derived states are present. | |
| 54 | + | assert!(t.hex("action-hover").is_some()); | |
| 55 | + | assert!(t.hex("selection").is_some()); | |
| 56 | + | assert!(t.hex("content-on-action").is_some()); | |
| 52 | 57 | } | |
| 53 | 58 | ||
| 54 | 59 | #[test] | |
| 55 | - | fn theme_toml_parse_minimal() { | |
| 56 | - | // A theme with only meta and one color section | |
| 57 | - | let toml_str = r##" | |
| 58 | - | [meta] | |
| 59 | - | name = "Minimal" | |
| 60 | - | ||
| 61 | - | [background] | |
| 62 | - | primary = "#fff" | |
| 63 | - | "##; | |
| 64 | - | ||
| 65 | - | let table: toml::Table = toml_str.parse().unwrap(); | |
| 66 | - | ||
| 67 | - | let meta = table.get("meta").unwrap().as_table().unwrap(); | |
| 68 | - | assert_eq!(meta.get("name").unwrap().as_str().unwrap(), "Minimal"); | |
| 69 | - | // Variant not specified — consumer should default to "dark" | |
| 70 | - | assert!(meta.get("variant").is_none()); | |
| 71 | - | ||
| 72 | - | let mut colors = std::collections::HashMap::new(); | |
| 73 | - | for section in &["background", "foreground", "accent", "border"] { | |
| 74 | - | if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) { | |
| 75 | - | for (key, val) in sect { | |
| 76 | - | if let Some(color) = val.as_str() { | |
| 77 | - | colors.insert(format!("{}.{}", section, key), color.to_string()); | |
| 78 | - | } | |
| 79 | - | } | |
| 80 | - | } | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | assert_eq!(colors.len(), 1); | |
| 84 | - | assert_eq!(colors.get("background.primary").unwrap(), "#fff"); | |
| 60 | + | fn theme_variant_defaults_to_dark() { | |
| 61 | + | let theme = theme_common::parse_theme_str("m", "[meta]\nname = \"M\"\n", false).unwrap(); | |
| 62 | + | assert_eq!(theme.meta.variant, "dark"); | |
| 85 | 63 | } | |
| 86 | 64 | ||
| 87 | 65 | #[test] | |
| 88 | - | fn theme_toml_parse_empty_is_valid() { | |
| 89 | - | // An empty TOML should parse without error | |
| 90 | - | let table: toml::Table = "".parse().unwrap(); | |
| 91 | - | ||
| 92 | - | let mut colors = std::collections::HashMap::new(); | |
| 93 | - | for section in &["background", "foreground", "accent", "border"] { | |
| 94 | - | if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) { | |
| 95 | - | for (key, val) in sect { | |
| 96 | - | if let Some(color) = val.as_str() { | |
| 97 | - | colors.insert(format!("{}.{}", section, key), color.to_string()); | |
| 98 | - | } | |
| 99 | - | } | |
| 100 | - | } | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | assert!(colors.is_empty()); | |
| 104 | - | } | |
| 105 | - | ||
| 106 | - | #[test] | |
| 107 | - | fn theme_toml_ignores_unknown_sections() { | |
| 108 | - | let toml_str = r##" | |
| 109 | - | [meta] | |
| 110 | - | name = "Custom" | |
| 111 | - | ||
| 112 | - | [background] | |
| 113 | - | primary = "#000" | |
| 114 | - | ||
| 115 | - | [custom_section] | |
| 116 | - | foo = "bar" | |
| 117 | - | "##; | |
| 118 | - | ||
| 119 | - | let table: toml::Table = toml_str.parse().unwrap(); | |
| 120 | - | ||
| 121 | - | // The standard extraction logic only reads known sections | |
| 122 | - | let mut colors = std::collections::HashMap::new(); | |
| 123 | - | for section in &["background", "foreground", "accent", "border"] { | |
| 124 | - | if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) { | |
| 125 | - | for (key, val) in sect { | |
| 126 | - | if let Some(color) = val.as_str() { | |
| 127 | - | colors.insert(format!("{}.{}", section, key), color.to_string()); | |
| 128 | - | } | |
| 129 | - | } | |
| 130 | - | } | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | // custom_section should not appear in colors | |
| 134 | - | assert_eq!(colors.len(), 1); | |
| 135 | - | assert!(!colors.contains_key("custom_section.foo")); | |
| 66 | + | fn unknown_sections_are_ignored() { | |
| 67 | + | let toml = "[surface]\npage = \"#000\"\n[custom_section]\nfoo = \"bar\"\n"; | |
| 68 | + | let theme = theme_common::parse_theme_str("c", toml, false).unwrap(); | |
| 69 | + | assert_eq!(theme.colors.get("surface.page").unwrap(), "#000"); | |
| 70 | + | assert!(!theme.colors.contains_key("custom_section.foo")); | |
| 136 | 71 | } |