// SPDX-FileCopyrightText: 2026 Proa Labs, Inc. // SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT //! Theme Toggle recipe — named slot classes for the theme toggle button. use crate::theme::rounded; use crate::tw_join; use proa_core::shared::attr_value::AttrValue; use proa_core::{DataLoader, WebContext, WriteBuf, WriteError}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct ThemeToggleRecipeProps; /// Static named classes for every Theme Toggle part. #[derive(Debug, Clone, Copy)] pub struct ThemeToggleRecipe { pub root: &'static str, pub moon_icon: &'static str, pub sun_icon: &'static str, } impl ThemeToggleRecipe { pub const fn resolve(&'static self, _props: ThemeToggleRecipeProps) -> ThemeToggleStyle { ThemeToggleStyle { recipe: self } } } #[must_use] #[derive(Debug, Clone, Copy)] pub struct ThemeToggleStyle { recipe: &'static ThemeToggleRecipe, } impl AttrValue for ThemeToggleStyle { #[inline(always)] fn should_render(&self) -> bool { true } #[inline(always)] fn render_attr_value( &self, cx: &mut WebContext, ) -> Result<(), WriteError> { cx.out.extend_static(self.recipe.root.as_bytes()) } } /// Ghost icon-button treatment matching shadcn's mode toggle. const THEME_TOGGLE_ROOT: &str = tw_join!( "inline-flex", "h-9", "w-9", "shrink-0", "items-center", "justify-center", "text-muted-foreground", "transition-colors", "hover:bg-accent", "hover:text-accent-foreground", "focus-visible:outline-none", "focus-visible:ring-2", "focus-visible:ring-ring", "focus-visible:ring-offset-2", "disabled:pointer-events-none", "disabled:opacity-50", rounded::MD ); /// The moon invites dark mode, so it shows while the page is light; the sun /// takes over under the root `.dark` class. Pure CSS — the runtime only flips /// the class, never the icons. const THEME_TOGGLE_MOON_ICON: &str = tw_join!("dark:hidden"); const THEME_TOGGLE_SUN_ICON: &str = tw_join!("hidden", "dark:block"); pub const THEME_TOGGLE_RECIPE: ThemeToggleRecipe = ThemeToggleRecipe { root: THEME_TOGGLE_ROOT, moon_icon: THEME_TOGGLE_MOON_ICON, sun_icon: THEME_TOGGLE_SUN_ICON, }; pub const fn theme_toggle_recipe() -> &'static ThemeToggleRecipe { &THEME_TOGGLE_RECIPE } impl crate::variant_spec::ComponentSpec for ThemeToggleRecipe { const NAME: &'static str = "ThemeToggle"; fn base_classes(&self) -> &'static str { self.root } fn variant_metadata(&self) -> Vec { Vec::new() } fn anatomy(&self) -> Option { Some(super::THEME_TOGGLE_ANATOMY) } } #[cfg(test)] mod tests { use super::*; #[test] fn recipe_exposes_named_slots() { let recipe = theme_toggle_recipe(); assert!(recipe.root.contains("inline-flex")); assert!(recipe.moon_icon.contains("dark:hidden")); assert!(recipe.sun_icon.contains("dark:block")); } #[test] fn component_metadata_uses_canonical_anatomy() { use crate::variant_spec::ComponentSpec; assert_eq!( THEME_TOGGLE_RECIPE.anatomy(), Some(super::super::THEME_TOGGLE_ANATOMY) ); assert!(THEME_TOGGLE_RECIPE.variant_metadata().is_empty()); } }