logicaffeine_web/ui/components/
theme_picker.rs1use 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#[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#[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}