use hikari_palette::*;
use crate::prelude::*;
use crate::theme::{
css::{ComponentOverrides, ComponentPalette, PaletteOverrides, ThemePalette},
registry::{get_default_theme, get_registered_theme},
};
use tairitsu_hooks::ReactiveSignal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum LayoutDirection {
#[default]
Ltr,
Rtl,
}
impl LayoutDirection {
pub fn as_str(&self) -> &'static str {
match self {
LayoutDirection::Ltr => "ltr",
LayoutDirection::Rtl => "rtl",
}
}
pub fn is_rtl(&self) -> bool {
matches!(self, LayoutDirection::Rtl)
}
}
#[derive(Clone)]
pub struct ThemeContext {
pub palette: ReactiveSignal<String>,
pub theme_name: ReactiveSignal<String>,
pub direction: ReactiveSignal<LayoutDirection>,
pub set_theme: Callback<String>,
}
#[define_props]
pub struct ThemeProviderProps {
#[default("hikari".to_string())]
pub palette: String,
#[default("ltr".to_string())]
pub direction: String,
#[default]
pub primary: Option<String>,
#[default]
pub secondary: Option<String>,
#[default]
pub accent: Option<String>,
#[default]
pub success: Option<String>,
#[default]
pub warning: Option<String>,
#[default]
pub danger: Option<String>,
#[default]
pub background: Option<String>,
#[default]
pub surface: Option<String>,
#[default]
pub border: Option<String>,
#[default]
pub text_primary: Option<String>,
#[default]
pub text_secondary: Option<String>,
#[default]
pub component_overrides: ComponentOverrides,
pub children: Element,
}
#[allow(non_snake_case)]
#[allow(unused_braces)]
pub fn ThemeProvider(props: ThemeProviderProps) -> Element {
let current_palette = use_signal(|| props.palette.clone());
let current_theme_name = use_signal(|| props.palette.clone());
let current_direction = use_signal(|| match props.direction.as_str() {
"rtl" => LayoutDirection::Rtl,
_ => LayoutDirection::Ltr,
});
let palette_for_callback = current_palette.clone();
let theme_name_for_callback = current_theme_name.clone();
let set_theme = Callback::new(move |new_theme: String| {
palette_for_callback.set(new_theme.clone());
theme_name_for_callback.set(new_theme);
});
let palette_for_context = current_palette.clone();
let theme_name_for_context = current_theme_name.clone();
let direction_for_context = current_direction.clone();
use_context_provider(move || ThemeContext {
palette: palette_for_context,
theme_name: theme_name_for_context,
direction: direction_for_context,
set_theme,
});
let primary_override = props.primary.clone();
let secondary_override = props.secondary.clone();
let accent_override = props.accent.clone();
let success_override = props.success.clone();
let warning_override = props.warning.clone();
let danger_override = props.danger.clone();
let background_override = props.background.clone();
let surface_override = props.surface.clone();
let border_override = props.border.clone();
let text_primary_override = props.text_primary.clone();
let text_secondary_override = props.text_secondary.clone();
let component_overrides = props.component_overrides.clone();
let theme_name_for_memo = current_theme_name.clone();
let css_vars = use_memo(move || {
let theme_name = theme_name_for_memo.read();
let base_palette = match get_registered_theme(&theme_name) {
Some(palette) => palette,
None => {
let default_name = get_default_theme();
get_registered_theme(default_name).unwrap_or_else(Hikari::palette)
}
};
let overrides = PaletteOverrides {
primary: primary_override.clone(),
secondary: secondary_override.clone(),
accent: accent_override.clone(),
success: success_override.clone(),
warning: warning_override.clone(),
danger: danger_override.clone(),
background: background_override.clone(),
surface: surface_override.clone(),
border: border_override.clone(),
text_primary: text_primary_override.clone(),
text_secondary: text_secondary_override.clone(),
};
let theme_palette = ThemePalette::from_palette(&base_palette).with_overrides(overrides);
let component_palette = ComponentPalette::from_palette_with_overrides(
&base_palette,
component_overrides.clone(),
);
format!(
"{} {}",
theme_palette.css_variables(),
component_palette.css_variables()
)
});
let theme_name = current_theme_name.read();
let dir = current_direction.read().as_str();
rsx! {
div {
class: "hi-theme-provider",
"data-theme": theme_name,
dir,
style: css_vars,
{props.children}
}
}
}
pub fn use_theme() -> ThemeContext {
let ctx = use_context::<ThemeContext>().expect("ThemeContext not found");
ctx.get().clone()
}
pub fn use_layout_direction() -> LayoutDirection {
try_consume_context::<ThemeContext>()
.map(|ctx| ctx.get().direction.read())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::css::{PaletteOverrides, ThemePalette};
use hikari_palette::{Hikari, Tairitsu};
#[test]
fn test_theme_palette_from_palette() {
let palette = Hikari::palette();
let theme_palette = ThemePalette::from_palette(&palette);
assert_eq!(theme_palette.primary, palette.primary.hex());
assert_eq!(theme_palette.background, palette.background.hex());
}
#[test]
fn test_css_variables_generation() {
let palette = Hikari::palette();
let theme_palette = ThemePalette::from_palette(&palette);
let css_vars = theme_palette.css_variables();
assert!(css_vars.contains("--hi-primary:"));
assert!(css_vars.contains("--hi-background:"));
assert!(css_vars.contains("--hi-text-primary:"));
}
#[test]
fn test_color_overrides() {
let palette = Hikari::palette();
let overrides = PaletteOverrides {
primary: Some("#FF0000".to_string()),
secondary: None,
accent: None,
success: None,
warning: None,
danger: None,
background: None,
surface: None,
border: None,
text_primary: None,
text_secondary: None,
};
let theme_palette = ThemePalette::from_palette(&palette).with_overrides(overrides);
assert_eq!(theme_palette.primary, "#FF0000");
assert_eq!(theme_palette.secondary, palette.secondary.hex());
}
#[test]
fn test_all_color_overrides() {
let palette = Hikari::palette();
let overrides = PaletteOverrides {
primary: Some("#111111".to_string()),
secondary: Some("#222222".to_string()),
accent: Some("#333333".to_string()),
success: Some("#444444".to_string()),
warning: Some("#555555".to_string()),
danger: Some("#666666".to_string()),
background: Some("#777777".to_string()),
surface: Some("#888888".to_string()),
border: Some("#999999".to_string()),
text_primary: Some("#AAAAAA".to_string()),
text_secondary: Some("#BBBBBB".to_string()),
};
let theme_palette = ThemePalette::from_palette(&palette).with_overrides(overrides);
assert_eq!(theme_palette.primary, "#111111");
assert_eq!(theme_palette.secondary, "#222222");
assert_eq!(theme_palette.accent, "#333333");
assert_eq!(theme_palette.success, "#444444");
assert_eq!(theme_palette.warning, "#555555");
assert_eq!(theme_palette.danger, "#666666");
assert_eq!(theme_palette.background, "#777777");
assert_eq!(theme_palette.surface, "#888888");
assert_eq!(theme_palette.border, "#999999");
assert_eq!(theme_palette.text_primary, "#AAAAAA");
assert_eq!(theme_palette.text_secondary, "#BBBBBB");
}
#[test]
fn test_hikari_theme_colors() {
let palette = Hikari::palette();
let theme_palette = ThemePalette::from_palette(&palette);
assert!(theme_palette.primary.starts_with('#'));
assert!(theme_palette.secondary.starts_with('#'));
assert!(theme_palette.accent.starts_with('#'));
assert!(theme_palette.primary.len() == 7);
assert!(theme_palette.secondary.len() == 7);
assert!(theme_palette.accent.len() == 7);
}
#[test]
fn test_tairitsu_theme_colors() {
let palette = Tairitsu::palette();
let theme_palette = ThemePalette::from_palette(&palette);
assert!(theme_palette.background.starts_with('#'));
assert!(theme_palette.text_primary.starts_with('#'));
assert!(theme_palette.background.len() == 7);
assert!(theme_palette.text_primary.len() == 7);
}
#[test]
fn test_component_palette_css_variables() {
let palette = Hikari::palette();
let component_palette = ComponentPalette::from_palette(&palette);
let css_vars = component_palette.css_variables();
assert!(css_vars.contains("--hi-component-selection-icon:"));
assert!(css_vars.contains("--hi-component-selection-bg:"));
assert_eq!(
component_palette.selection_icon_color,
palette.background.hex()
);
assert!(
component_palette
.selection_background
.contains("linear-gradient")
);
}
#[test]
fn test_component_palette_dark_mode() {
let palette = Tairitsu::palette();
let component_palette = ComponentPalette::from_palette(&palette);
assert_eq!(
component_palette.selection_icon_color,
palette.primary.hex()
);
assert!(component_palette.selection_border.contains("255, 255, 255"));
assert!(component_palette.selection_background.contains("rgba"));
assert!(
component_palette
.selection_background
.contains("linear-gradient")
);
let css_vars = component_palette.css_variables();
assert!(css_vars.contains("--hi-component-selection-icon:"));
}
#[test]
fn test_theme_palette_color_text_aliases() {
let palette = Tairitsu::palette();
let theme_palette = ThemePalette::from_palette(&palette);
let css_vars = theme_palette.css_variables();
assert!(css_vars.contains("--hi-color-text-primary:"));
assert!(css_vars.contains("--hi-color-text-secondary:"));
assert!(css_vars.contains("--hi-color-primary:"));
assert!(css_vars.contains("--hi-color-background:"));
assert!(
theme_palette.text_primary == "#F2F2F2" || theme_palette.text_primary.starts_with("#F")
);
}
}