#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Light,
Dark,
}
#[derive(Debug, Clone, PartialEq)]
pub struct UiTheme {
pub background: String,
pub foreground: String,
pub card: String,
pub card_foreground: String,
pub popover: String,
pub popover_foreground: String,
pub primary: String,
pub primary_foreground: String,
pub secondary: String,
pub secondary_foreground: String,
pub muted: String,
pub muted_foreground: String,
pub accent: String,
pub accent_foreground: String,
pub destructive: String,
pub destructive_foreground: String,
pub border: String,
pub input: String,
pub ring: String,
pub radius: f32,
pub font_family: Option<String>,
}
impl UiTheme {
#[must_use]
pub fn light() -> Self {
Self {
background: "#faf9f6".to_owned(),
foreground: "#141414".to_owned(),
card: "#ffffff".to_owned(),
card_foreground: "#141414".to_owned(),
popover: "#ffffff".to_owned(),
popover_foreground: "#141414".to_owned(),
primary: "#3072db".to_owned(),
primary_foreground: "#ffffff".to_owned(),
secondary: "#f2f0eb".to_owned(),
secondary_foreground: "#141414".to_owned(),
muted: "#f2f0eb".to_owned(),
muted_foreground: "#6e6a63".to_owned(),
accent: "#f2f0eb".to_owned(),
accent_foreground: "#141414".to_owned(),
destructive: "#d92d20".to_owned(),
destructive_foreground: "#ffffff".to_owned(),
border: "#d9d9d9".to_owned(),
input: "#d9d9d9".to_owned(),
ring: "#3072db".to_owned(),
radius: 10.0,
font_family: None,
}
}
#[must_use]
pub fn dark() -> Self {
Self {
background: "#0a0a0b".to_owned(),
foreground: "#fafafa".to_owned(),
card: "#141417".to_owned(),
card_foreground: "#fafafa".to_owned(),
popover: "#141417".to_owned(),
popover_foreground: "#fafafa".to_owned(),
primary: "#3072db".to_owned(),
primary_foreground: "#ffffff".to_owned(),
secondary: "#1d1d21".to_owned(),
secondary_foreground: "#fafafa".to_owned(),
muted: "#1b1b1f".to_owned(),
muted_foreground: "#a1a1aa".to_owned(),
accent: "#1d1d21".to_owned(),
accent_foreground: "#fafafa".to_owned(),
destructive: "#f97066".to_owned(),
destructive_foreground: "#fafafa".to_owned(),
border: "#26272b".to_owned(),
input: "#26272b".to_owned(),
ring: "#3072db".to_owned(),
radius: 10.0,
font_family: None,
}
}
#[must_use]
pub fn for_mode(mode: Mode) -> Self {
match mode {
Mode::Light => Self::light(),
Mode::Dark => Self::dark(),
}
}
}
impl Default for UiTheme {
fn default() -> Self {
Self::light()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn light_and_dark_are_distinct_but_share_the_accent() {
let light = UiTheme::light();
let dark = UiTheme::dark();
assert_ne!(light.background, dark.background);
assert_eq!(light.primary, dark.primary);
assert_eq!(light.radius, dark.radius);
}
#[test]
fn the_default_is_the_light_palette() {
assert_eq!(UiTheme::default(), UiTheme::light());
assert_eq!(UiTheme::for_mode(Mode::default()), UiTheme::light());
assert_eq!(Mode::default(), Mode::Light);
}
#[test]
fn every_token_is_a_concrete_colour() {
for theme in [UiTheme::light(), UiTheme::dark()] {
for token in [
&theme.background,
&theme.foreground,
&theme.card,
&theme.primary,
&theme.muted,
&theme.border,
&theme.ring,
] {
assert!(
token.starts_with('#'),
"{token} should be a concrete hex colour"
);
assert!(
!token.contains("var("),
"{token} must not be a CSS variable"
);
}
}
}
}