Skip to main content

logicaffeine_web/ui/components/
theme_picker.rs

1//! Compact theme selector component.
2//!
3//! Provides a dropdown selector for switching between the nature-inspired themes.
4//!
5//! # Usage
6//!
7//! ```no_run
8//! # use dioxus::prelude::*;
9//! use logicaffeine_web::ui::components::theme_picker::ThemePicker;
10//!
11//! # fn Example() -> Element {
12//! rsx! {
13//!     ThemePicker {}
14//! }
15//! # }
16//! ```
17
18use dioxus::prelude::*;
19use crate::ui::theme_state::{Theme, ThemeState};
20use crate::ui::components::icon::{Icon, IconVariant, IconSize};
21
22const THEME_PICKER_STYLE: &str = r#"
23.theme-dropdown {
24    position: relative;
25    display: inline-block;
26}
27
28.theme-dropdown-btn {
29    display: flex;
30    align-items: center;
31    gap: 8px;
32    padding: 8px 12px;
33    background: rgba(255, 255, 255, 0.04);
34    border: 1px solid rgba(255, 255, 255, 0.08);
35    border-radius: 10px;
36    cursor: pointer;
37    transition: all 0.2s ease;
38    color: var(--text-secondary);
39    font-size: 14px;
40    font-weight: 500;
41    min-width: 140px;
42}
43
44.theme-dropdown-btn:hover {
45    background: rgba(255, 255, 255, 0.08);
46    border-color: rgba(255, 255, 255, 0.12);
47    color: var(--text-primary);
48}
49
50.theme-dropdown-btn .theme-icon {
51    color: var(--accent-primary);
52}
53
54.theme-dropdown-btn .theme-name {
55    flex: 1;
56    text-align: left;
57}
58
59.theme-dropdown-btn .chevron {
60    transition: transform 0.2s ease;
61}
62
63.theme-dropdown.open .chevron {
64    transform: rotate(180deg);
65}
66
67.theme-dropdown-menu {
68    position: absolute;
69    bottom: 100%;
70    left: 0;
71    right: 0;
72    margin-bottom: 4px;
73    background: rgba(15, 15, 20, 0.98);
74    border: 1px solid rgba(255, 255, 255, 0.12);
75    border-radius: 10px;
76    padding: 6px;
77    opacity: 0;
78    visibility: hidden;
79    transform: translateY(8px);
80    transition: all 0.2s ease;
81    z-index: 1000;
82    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4);
83}
84
85.theme-dropdown.open .theme-dropdown-menu {
86    opacity: 1;
87    visibility: visible;
88    transform: translateY(0);
89}
90
91.theme-option {
92    display: flex;
93    align-items: center;
94    gap: 10px;
95    padding: 10px 12px;
96    border-radius: 6px;
97    cursor: pointer;
98    transition: all 0.15s ease;
99    color: var(--text-secondary);
100    border: none;
101    background: transparent;
102    width: 100%;
103    font-size: 14px;
104    text-align: left;
105}
106
107.theme-option:hover {
108    background: rgba(255, 255, 255, 0.08);
109    color: var(--text-primary);
110}
111
112.theme-option.active {
113    background: rgba(var(--accent-primary-rgb), 0.15);
114    color: var(--accent-primary);
115}
116
117.theme-option .option-icon {
118    width: 20px;
119    display: flex;
120    justify-content: center;
121}
122
123.theme-option.sunrise .option-icon { color: #f97316; }
124.theme-option.violet .option-icon { color: #a78bfa; }
125.theme-option.ocean .option-icon { color: #22d3ee; }
126.theme-option.mountain .option-icon { color: #00d4ff; }
127.theme-option.rose .option-icon { color: #f472b6; }
128.theme-option.forest .option-icon { color: #4ade80; }
129.theme-option.ember .option-icon { color: #ef4444; }
130
131/* Mobile adjustments */
132@media (max-width: 768px) {
133    .theme-dropdown-btn {
134        padding: 10px 14px;
135        min-width: 150px;
136    }
137
138    .theme-option {
139        padding: 12px 14px;
140    }
141}
142"#;
143
144fn theme_icon(theme: Theme) -> IconVariant {
145    match theme {
146        Theme::Sunrise => IconVariant::Sunrise,
147        Theme::Violet => IconVariant::Moon,
148        Theme::Ocean => IconVariant::Wave,
149        Theme::Mountain => IconVariant::Mountain,
150        Theme::Rose => IconVariant::Flower,
151        Theme::Forest => IconVariant::Leaf,
152        Theme::Ember => IconVariant::Fire,
153    }
154}
155
156fn theme_class(theme: Theme) -> &'static str {
157    match theme {
158        Theme::Sunrise => "sunrise",
159        Theme::Violet => "violet",
160        Theme::Ocean => "ocean",
161        Theme::Mountain => "mountain",
162        Theme::Rose => "rose",
163        Theme::Forest => "forest",
164        Theme::Ember => "ember",
165    }
166}
167
168/// Compact theme picker with dropdown selector.
169#[component]
170pub fn ThemePicker() -> Element {
171    let mut theme_state = use_context::<ThemeState>();
172    let current_theme = theme_state.current();
173    let mut is_open = use_signal(|| false);
174
175    let dropdown_class = if *is_open.read() {
176        "theme-dropdown open"
177    } else {
178        "theme-dropdown"
179    };
180
181    rsx! {
182        style { "{THEME_PICKER_STYLE}" }
183
184        div {
185            class: "{dropdown_class}",
186
187            button {
188                class: "theme-dropdown-btn",
189                onclick: move |_| {
190                    let current = *is_open.read();
191                    is_open.set(!current);
192                },
193
194                span { class: "theme-icon",
195                    Icon { variant: theme_icon(current_theme), size: IconSize::Medium }
196                }
197                span { class: "theme-name", "{current_theme.name()}" }
198                span { class: "chevron",
199                    Icon { variant: IconVariant::ChevronDown, size: IconSize::Small }
200                }
201            }
202
203            div { class: "theme-dropdown-menu",
204                for theme in Theme::all() {
205                    {
206                        let is_active = theme == current_theme;
207                        let option_class = if is_active {
208                            format!("theme-option {} active", theme_class(theme))
209                        } else {
210                            format!("theme-option {}", theme_class(theme))
211                        };
212                        rsx! {
213                            button {
214                                class: "{option_class}",
215                                onclick: move |_| {
216                                    theme_state.set_theme(theme);
217                                    is_open.set(false);
218                                },
219                                span { class: "option-icon",
220                                    Icon { variant: theme_icon(theme), size: IconSize::Medium }
221                                }
222                                span { "{theme.name()}" }
223                            }
224                        }
225                    }
226                }
227            }
228        }
229    }
230}
231
232/// Simple theme cycle button for compact spaces.
233#[component]
234pub fn ThemeCycleButton() -> Element {
235    let mut theme_state = use_context::<ThemeState>();
236    let current_theme = theme_state.current();
237
238    let icon = theme_icon(current_theme);
239
240    rsx! {
241        button {
242            class: "theme-btn",
243            onclick: move |_| theme_state.cycle_theme(),
244            title: "Change theme ({current_theme.name()})",
245            Icon { variant: icon, size: IconSize::Medium }
246        }
247    }
248}