use crate::tree::Color;
#[derive(Clone, Debug)]
pub struct Palette {
pub background: Color,
pub foreground: Color,
pub card: Color,
pub card_foreground: Color,
pub popover: Color,
pub popover_foreground: Color,
pub primary: Color,
pub primary_foreground: Color,
pub secondary: Color,
pub secondary_foreground: Color,
pub muted: Color,
pub muted_foreground: Color,
pub accent: Color,
pub accent_foreground: Color,
pub destructive: Color,
pub destructive_foreground: Color,
pub border: Color,
pub input: Color,
pub ring: Color,
pub success: Color,
pub success_foreground: Color,
pub warning: Color,
pub warning_foreground: Color,
pub info: Color,
pub info_foreground: Color,
pub overlay_scrim: Color,
pub link_foreground: Color,
pub scrollbar_thumb_fill: Color,
pub scrollbar_thumb_fill_active: Color,
pub selection_bg: Color,
pub selection_bg_unfocused: Color,
}
impl Palette {
pub const fn aetna_dark() -> Self {
Self::shadcn_zinc_dark()
}
pub const fn aetna_light() -> Self {
Self::shadcn_zinc_light()
}
pub const fn shadcn_zinc_dark() -> Self {
Self {
background: Color::token("background", 9, 9, 11, 255),
foreground: Color::token("foreground", 250, 250, 250, 255),
card: Color::token("card", 9, 9, 11, 255),
card_foreground: Color::token("card-foreground", 250, 250, 250, 255),
popover: Color::token("popover", 9, 9, 11, 255),
popover_foreground: Color::token("popover-foreground", 250, 250, 250, 255),
primary: Color::token("primary", 250, 250, 250, 255),
primary_foreground: Color::token("primary-foreground", 24, 24, 27, 255),
secondary: Color::token("secondary", 39, 39, 42, 255),
secondary_foreground: Color::token("secondary-foreground", 250, 250, 250, 255),
muted: Color::token("muted", 39, 39, 42, 255),
muted_foreground: Color::token("muted-foreground", 161, 161, 170, 255),
accent: Color::token("accent", 39, 39, 42, 255),
accent_foreground: Color::token("accent-foreground", 250, 250, 250, 255),
destructive: Color::token("destructive", 127, 29, 29, 255),
destructive_foreground: Color::token("destructive-foreground", 250, 250, 250, 255),
border: Color::token("border", 39, 39, 42, 255),
input: Color::token("input", 39, 39, 42, 255),
ring: Color::token("ring", 212, 212, 216, 255),
success: Color::token("success", 16, 185, 129, 255),
success_foreground: Color::token("success-foreground", 5, 46, 22, 255),
warning: Color::token("warning", 245, 158, 11, 255),
warning_foreground: Color::token("warning-foreground", 69, 26, 3, 255),
info: Color::token("info", 59, 130, 246, 255),
info_foreground: Color::token("info-foreground", 239, 246, 255, 255),
overlay_scrim: Color::token("overlay-scrim", 0, 0, 0, 204),
link_foreground: Color::token("link-foreground", 96, 165, 250, 255),
scrollbar_thumb_fill: Color::token("scrollbar-thumb", 113, 113, 122, 120),
scrollbar_thumb_fill_active: Color::token("scrollbar-thumb-active", 161, 161, 170, 220),
selection_bg: Color::token("selection-bg", 96, 165, 250, 96),
selection_bg_unfocused: Color::token("selection-bg-unfocused", 113, 113, 122, 64),
}
}
pub const fn shadcn_zinc_light() -> Self {
Self {
background: Color::token("background", 255, 255, 255, 255),
foreground: Color::token("foreground", 9, 9, 11, 255),
card: Color::token("card", 255, 255, 255, 255),
card_foreground: Color::token("card-foreground", 9, 9, 11, 255),
popover: Color::token("popover", 255, 255, 255, 255),
popover_foreground: Color::token("popover-foreground", 9, 9, 11, 255),
primary: Color::token("primary", 24, 24, 27, 255),
primary_foreground: Color::token("primary-foreground", 250, 250, 250, 255),
secondary: Color::token("secondary", 244, 244, 245, 255),
secondary_foreground: Color::token("secondary-foreground", 24, 24, 27, 255),
muted: Color::token("muted", 244, 244, 245, 255),
muted_foreground: Color::token("muted-foreground", 113, 113, 122, 255),
accent: Color::token("accent", 244, 244, 245, 255),
accent_foreground: Color::token("accent-foreground", 24, 24, 27, 255),
destructive: Color::token("destructive", 239, 68, 68, 255),
destructive_foreground: Color::token("destructive-foreground", 250, 250, 250, 255),
border: Color::token("border", 228, 228, 231, 255),
input: Color::token("input", 228, 228, 231, 255),
ring: Color::token("ring", 24, 24, 27, 255),
success: Color::token("success", 16, 185, 129, 255),
success_foreground: Color::token("success-foreground", 5, 46, 22, 255),
warning: Color::token("warning", 245, 158, 11, 255),
warning_foreground: Color::token("warning-foreground", 69, 26, 3, 255),
info: Color::token("info", 37, 99, 235, 255),
info_foreground: Color::token("info-foreground", 239, 246, 255, 255),
overlay_scrim: Color::token("overlay-scrim", 0, 0, 0, 128),
link_foreground: Color::token("link-foreground", 37, 99, 235, 255),
scrollbar_thumb_fill: Color::token("scrollbar-thumb", 113, 113, 122, 90),
scrollbar_thumb_fill_active: Color::token("scrollbar-thumb-active", 82, 82, 91, 220),
selection_bg: Color::token("selection-bg", 37, 99, 235, 64),
selection_bg_unfocused: Color::token("selection-bg-unfocused", 113, 113, 122, 56),
}
}
pub const fn shadcn_neutral_dark() -> Self {
let mut palette = Self::shadcn_zinc_dark();
palette.background = Color::token("background", 10, 10, 10, 255);
palette.foreground = Color::token("foreground", 250, 250, 250, 255);
palette.card = Color::token("card", 10, 10, 10, 255);
palette.card_foreground = Color::token("card-foreground", 250, 250, 250, 255);
palette.popover = Color::token("popover", 10, 10, 10, 255);
palette.popover_foreground = Color::token("popover-foreground", 250, 250, 250, 255);
palette.primary = Color::token("primary", 250, 250, 250, 255);
palette.primary_foreground = Color::token("primary-foreground", 23, 23, 23, 255);
palette.secondary = Color::token("secondary", 38, 38, 38, 255);
palette.secondary_foreground = Color::token("secondary-foreground", 250, 250, 250, 255);
palette.muted = Color::token("muted", 38, 38, 38, 255);
palette.muted_foreground = Color::token("muted-foreground", 163, 163, 163, 255);
palette.accent = Color::token("accent", 38, 38, 38, 255);
palette.accent_foreground = Color::token("accent-foreground", 250, 250, 250, 255);
palette.border = Color::token("border", 38, 38, 38, 255);
palette.input = Color::token("input", 38, 38, 38, 255);
palette.ring = Color::token("ring", 212, 212, 212, 255);
palette.scrollbar_thumb_fill = Color::token("scrollbar-thumb", 115, 115, 115, 120);
palette.scrollbar_thumb_fill_active =
Color::token("scrollbar-thumb-active", 163, 163, 163, 220);
palette.selection_bg_unfocused = Color::token("selection-bg-unfocused", 115, 115, 115, 64);
palette
}
pub const fn shadcn_neutral_light() -> Self {
let mut palette = Self::shadcn_zinc_light();
palette.background = Color::token("background", 255, 255, 255, 255);
palette.foreground = Color::token("foreground", 10, 10, 10, 255);
palette.card = Color::token("card", 255, 255, 255, 255);
palette.card_foreground = Color::token("card-foreground", 10, 10, 10, 255);
palette.popover = Color::token("popover", 255, 255, 255, 255);
palette.popover_foreground = Color::token("popover-foreground", 10, 10, 10, 255);
palette.primary = Color::token("primary", 23, 23, 23, 255);
palette.primary_foreground = Color::token("primary-foreground", 250, 250, 250, 255);
palette.secondary = Color::token("secondary", 245, 245, 245, 255);
palette.secondary_foreground = Color::token("secondary-foreground", 23, 23, 23, 255);
palette.muted = Color::token("muted", 245, 245, 245, 255);
palette.muted_foreground = Color::token("muted-foreground", 115, 115, 115, 255);
palette.accent = Color::token("accent", 245, 245, 245, 255);
palette.accent_foreground = Color::token("accent-foreground", 23, 23, 23, 255);
palette.border = Color::token("border", 229, 229, 229, 255);
palette.input = Color::token("input", 229, 229, 229, 255);
palette.ring = Color::token("ring", 10, 10, 10, 255);
palette.scrollbar_thumb_fill = Color::token("scrollbar-thumb", 115, 115, 115, 90);
palette.scrollbar_thumb_fill_active =
Color::token("scrollbar-thumb-active", 82, 82, 82, 220);
palette.selection_bg_unfocused = Color::token("selection-bg-unfocused", 115, 115, 115, 56);
palette
}
pub const fn shadcn_neutral(is_dark: bool) -> Self {
if is_dark {
Self::shadcn_neutral_dark()
} else {
Self::shadcn_neutral_light()
}
}
pub const fn radix_slate_blue_dark() -> Self {
Self {
background: Color::token("background", 17, 17, 19, 255),
foreground: Color::token("foreground", 237, 238, 240, 255),
card: Color::token("card", 24, 25, 27, 255),
card_foreground: Color::token("card-foreground", 237, 238, 240, 255),
popover: Color::token("popover", 24, 25, 27, 255),
popover_foreground: Color::token("popover-foreground", 237, 238, 240, 255),
primary: Color::token("primary", 0, 144, 255, 255),
primary_foreground: Color::token("primary-foreground", 255, 255, 255, 255),
secondary: Color::token("secondary", 33, 34, 37, 255),
secondary_foreground: Color::token("secondary-foreground", 237, 238, 240, 255),
muted: Color::token("muted", 33, 34, 37, 255),
muted_foreground: Color::token("muted-foreground", 176, 180, 186, 255),
accent: Color::token("accent", 13, 40, 71, 255),
accent_foreground: Color::token("accent-foreground", 112, 184, 255, 255),
destructive: Color::token("destructive", 229, 72, 77, 255),
destructive_foreground: Color::token("destructive-foreground", 255, 255, 255, 255),
border: Color::token("border", 54, 58, 63, 255),
input: Color::token("input", 54, 58, 63, 255),
ring: Color::token("ring", 0, 144, 255, 255),
success: Color::token("success", 48, 164, 108, 255),
success_foreground: Color::token("success-foreground", 14, 21, 18, 255),
warning: Color::token("warning", 255, 197, 61, 255),
warning_foreground: Color::token("warning-foreground", 79, 52, 34, 255),
info: Color::token("info", 0, 144, 255, 255),
info_foreground: Color::token("info-foreground", 255, 255, 255, 255),
overlay_scrim: Color::token("overlay-scrim", 0, 0, 0, 204),
link_foreground: Color::token("link-foreground", 112, 184, 255, 255),
scrollbar_thumb_fill: Color::token("scrollbar-thumb", 105, 110, 119, 120),
scrollbar_thumb_fill_active: Color::token("scrollbar-thumb-active", 176, 180, 186, 220),
selection_bg: Color::token("selection-bg", 0, 144, 255, 96),
selection_bg_unfocused: Color::token("selection-bg-unfocused", 105, 110, 119, 64),
}
}
pub const fn radix_slate_blue_light() -> Self {
Self {
background: Color::token("background", 252, 252, 253, 255),
foreground: Color::token("foreground", 28, 32, 36, 255),
card: Color::token("card", 255, 255, 255, 255),
card_foreground: Color::token("card-foreground", 28, 32, 36, 255),
popover: Color::token("popover", 255, 255, 255, 255),
popover_foreground: Color::token("popover-foreground", 28, 32, 36, 255),
primary: Color::token("primary", 0, 144, 255, 255),
primary_foreground: Color::token("primary-foreground", 255, 255, 255, 255),
secondary: Color::token("secondary", 240, 240, 243, 255),
secondary_foreground: Color::token("secondary-foreground", 28, 32, 36, 255),
muted: Color::token("muted", 240, 240, 243, 255),
muted_foreground: Color::token("muted-foreground", 96, 100, 108, 255),
accent: Color::token("accent", 230, 244, 254, 255),
accent_foreground: Color::token("accent-foreground", 13, 116, 206, 255),
destructive: Color::token("destructive", 229, 72, 77, 255),
destructive_foreground: Color::token("destructive-foreground", 255, 255, 255, 255),
border: Color::token("border", 217, 217, 224, 255),
input: Color::token("input", 205, 206, 214, 255),
ring: Color::token("ring", 0, 144, 255, 255),
success: Color::token("success", 48, 164, 108, 255),
success_foreground: Color::token("success-foreground", 25, 59, 45, 255),
warning: Color::token("warning", 255, 197, 61, 255),
warning_foreground: Color::token("warning-foreground", 79, 52, 34, 255),
info: Color::token("info", 0, 144, 255, 255),
info_foreground: Color::token("info-foreground", 255, 255, 255, 255),
overlay_scrim: Color::token("overlay-scrim", 0, 0, 0, 128),
link_foreground: Color::token("link-foreground", 13, 116, 206, 255),
scrollbar_thumb_fill: Color::token("scrollbar-thumb", 139, 141, 152, 90),
scrollbar_thumb_fill_active: Color::token("scrollbar-thumb-active", 96, 100, 108, 220),
selection_bg: Color::token("selection-bg", 0, 144, 255, 64),
selection_bg_unfocused: Color::token("selection-bg-unfocused", 139, 141, 152, 56),
}
}
pub const fn radix_slate_blue(is_dark: bool) -> Self {
if is_dark {
Self::radix_slate_blue_dark()
} else {
Self::radix_slate_blue_light()
}
}
pub fn resolve(&self, c: Color) -> Color {
match c.token.and_then(|name| self.lookup(name)) {
Some(swap) => Color {
r: swap.r,
g: swap.g,
b: swap.b,
a: c.a,
token: c.token,
},
None => c,
}
}
pub fn lookup(&self, token: &str) -> Option<Color> {
Some(match token {
"background" => self.background,
"foreground" => self.foreground,
"card" => self.card,
"card-foreground" => self.card_foreground,
"popover" => self.popover,
"popover-foreground" => self.popover_foreground,
"primary" => self.primary,
"primary-foreground" => self.primary_foreground,
"secondary" => self.secondary,
"secondary-foreground" => self.secondary_foreground,
"muted" => self.muted,
"muted-foreground" => self.muted_foreground,
"accent" => self.accent,
"accent-foreground" => self.accent_foreground,
"destructive" => self.destructive,
"destructive-foreground" => self.destructive_foreground,
"border" => self.border,
"input" => self.input,
"ring" => self.ring,
"success" => self.success,
"success-foreground" => self.success_foreground,
"warning" => self.warning,
"warning-foreground" => self.warning_foreground,
"info" => self.info,
"info-foreground" => self.info_foreground,
"overlay-scrim" => self.overlay_scrim,
"link-foreground" => self.link_foreground,
"scrollbar-thumb" => self.scrollbar_thumb_fill,
"scrollbar-thumb-active" => self.scrollbar_thumb_fill_active,
"selection-bg" => self.selection_bg,
"selection-bg-unfocused" => self.selection_bg_unfocused,
_ => return None,
})
}
}
impl Default for Palette {
fn default() -> Self {
Self::aetna_dark()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tokens;
#[test]
fn dark_lookup_round_trips() {
let p = Palette::aetna_dark();
let bg = p.lookup("background").expect("background present");
assert_eq!(bg, p.background);
}
#[test]
fn aetna_dark_matches_token_fallbacks() {
let palette = Palette::aetna_dark();
for token in [
tokens::BACKGROUND,
tokens::FOREGROUND,
tokens::CARD,
tokens::CARD_FOREGROUND,
tokens::POPOVER,
tokens::POPOVER_FOREGROUND,
tokens::PRIMARY,
tokens::PRIMARY_FOREGROUND,
tokens::SECONDARY,
tokens::SECONDARY_FOREGROUND,
tokens::MUTED,
tokens::MUTED_FOREGROUND,
tokens::ACCENT,
tokens::ACCENT_FOREGROUND,
tokens::DESTRUCTIVE,
tokens::DESTRUCTIVE_FOREGROUND,
tokens::BORDER,
tokens::INPUT,
tokens::RING,
tokens::SUCCESS,
tokens::SUCCESS_FOREGROUND,
tokens::WARNING,
tokens::WARNING_FOREGROUND,
tokens::INFO,
tokens::INFO_FOREGROUND,
tokens::OVERLAY_SCRIM,
tokens::LINK_FOREGROUND,
tokens::SCROLLBAR_THUMB_FILL,
tokens::SCROLLBAR_THUMB_FILL_ACTIVE,
tokens::SELECTION_BG,
tokens::SELECTION_BG_UNFOCUSED,
] {
assert_eq!(palette.resolve(token), token);
}
}
#[test]
fn lookup_unknown_returns_none() {
let p = Palette::aetna_dark();
assert!(p.lookup("not-a-token").is_none());
}
#[test]
fn removed_legacy_tokens_not_in_palette() {
let p = Palette::aetna_dark();
assert!(p.lookup("bg-app").is_none());
assert!(p.lookup("bg-card").is_none());
assert!(p.lookup("bg-muted").is_none());
assert!(p.lookup("text-on-solid-dark").is_none());
assert!(p.lookup("text-on-solid-light").is_none());
}
#[test]
fn resolve_passes_through_unrecognized() {
let p = Palette::aetna_dark();
let raw = Color::rgba(1, 2, 3, 4);
assert_eq!(p.resolve(raw), raw);
let invariant = Color::token("text-on-solid-dark", 8, 16, 25, 255);
assert_eq!(p.resolve(invariant), invariant);
}
#[test]
fn resolve_preserves_alpha_override() {
let p = Palette::aetna_dark();
let translucent = p.card.with_alpha(120);
let resolved = p.resolve(translucent);
assert_eq!(
(resolved.r, resolved.g, resolved.b),
(p.card.r, p.card.g, p.card.b)
);
assert_eq!(resolved.a, 120);
assert_eq!(resolved.token, Some("card"));
}
#[test]
fn aetna_light_differs_from_aetna_dark() {
let dark = Palette::aetna_dark();
let light = Palette::aetna_light();
assert_ne!(
(dark.background.r, dark.background.g, dark.background.b),
(light.background.r, light.background.g, light.background.b),
);
assert_ne!(
(dark.foreground.r, dark.foreground.g, dark.foreground.b),
(light.foreground.r, light.foreground.g, light.foreground.b),
);
assert_eq!(dark.background.token, light.background.token);
}
#[test]
fn resolve_against_light_swaps_rgb() {
let light = Palette::aetna_light();
let dark_card = Color::token("card", 23, 26, 33, 255);
let resolved = light.resolve(dark_card);
assert_eq!(
(resolved.r, resolved.g, resolved.b),
(light.card.r, light.card.g, light.card.b),
);
}
#[test]
fn rgb_ops_strip_token_so_resolve_passes_through() {
let p = Palette::aetna_dark();
let darkened = p.muted.darken(0.5);
assert_eq!(darkened.token, None);
let resolved = p.resolve(darkened);
assert_eq!(resolved, darkened);
}
}