1use dioxus::prelude::*;
31use serde::{Deserialize, Serialize};
32#[cfg(target_arch = "wasm32")]
33use gloo_storage::{LocalStorage, Storage};
34
35const THEME_STORAGE_KEY: &str = "logicaffeine-theme";
36
37#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
39pub enum Theme {
40 Sunrise,
42 Violet,
44 Ocean,
46 #[default]
48 Mountain,
49 Rose,
51 Forest,
53 Ember,
55}
56
57impl Theme {
58 pub fn name(&self) -> &'static str {
60 match self {
61 Theme::Sunrise => "Sunrise",
62 Theme::Violet => "Violet",
63 Theme::Ocean => "Ocean",
64 Theme::Mountain => "Mountain",
65 Theme::Rose => "Rose",
66 Theme::Forest => "Forest",
67 Theme::Ember => "Ember",
68 }
69 }
70
71 pub fn data_attr(&self) -> &'static str {
73 match self {
74 Theme::Sunrise => "sunrise",
75 Theme::Violet => "violet",
76 Theme::Ocean => "ocean",
77 Theme::Mountain => "mountain",
78 Theme::Rose => "rose",
79 Theme::Forest => "forest",
80 Theme::Ember => "ember",
81 }
82 }
83
84 pub fn all() -> impl Iterator<Item = Theme> {
86 [
87 Theme::Mountain,
88 Theme::Sunrise,
89 Theme::Violet,
90 Theme::Ocean,
91 Theme::Rose,
92 Theme::Forest,
93 Theme::Ember,
94 ].into_iter()
95 }
96
97 pub fn css_variables(&self) -> &'static str {
99 match self {
100 Theme::Sunrise => r#"
102 --bg-gradient-start: #0c0a09;
103 --bg-gradient-mid: #1c1410;
104 --bg-gradient-end: #0c0a09;
105 --accent-primary: #f97316;
106 --accent-secondary: #fbbf24;
107 --accent-tertiary: #fef3c7;
108 --accent-primary-rgb: 249, 115, 22;
109 --accent-secondary-rgb: 251, 191, 36;
110 --accent-border: rgba(249, 115, 22, 0.3);
111 --accent-glow: rgba(249, 115, 22, 0.2);
112 "#,
113 Theme::Violet => r#"
115 --bg-gradient-start: #0f0d13;
116 --bg-gradient-mid: #1a1520;
117 --bg-gradient-end: #0f0d13;
118 --accent-primary: #a78bfa;
119 --accent-secondary: #f0abfc;
120 --accent-tertiary: #e9d5ff;
121 --accent-primary-rgb: 167, 139, 250;
122 --accent-secondary-rgb: 240, 171, 252;
123 --accent-border: rgba(167, 139, 250, 0.3);
124 --accent-glow: rgba(167, 139, 250, 0.2);
125 "#,
126 Theme::Ocean => r#"
128 --bg-gradient-start: #0a0f14;
129 --bg-gradient-mid: #0c1820;
130 --bg-gradient-end: #0a0f14;
131 --accent-primary: #22d3ee;
132 --accent-secondary: #2dd4bf;
133 --accent-tertiary: #a5f3fc;
134 --accent-primary-rgb: 34, 211, 238;
135 --accent-secondary-rgb: 45, 212, 191;
136 --accent-border: rgba(34, 211, 238, 0.3);
137 --accent-glow: rgba(34, 211, 238, 0.2);
138 "#,
139 Theme::Mountain => r#"
141 --bg-gradient-start: #09090b;
142 --bg-gradient-mid: #0c0c10;
143 --bg-gradient-end: #09090b;
144 --accent-primary: #00d4ff;
145 --accent-secondary: #818cf8;
146 --accent-tertiary: #f0f0f0;
147 --accent-primary-rgb: 0, 212, 255;
148 --accent-secondary-rgb: 129, 140, 248;
149 --accent-border: rgba(0, 212, 255, 0.3);
150 --accent-glow: rgba(0, 212, 255, 0.2);
151 "#,
152 Theme::Rose => r#"
154 --bg-gradient-start: #0d0909;
155 --bg-gradient-mid: #1a1015;
156 --bg-gradient-end: #0d0909;
157 --accent-primary: #f472b6;
158 --accent-secondary: #fb7185;
159 --accent-tertiary: #fce7f3;
160 --accent-primary-rgb: 244, 114, 182;
161 --accent-secondary-rgb: 251, 113, 133;
162 --accent-border: rgba(244, 114, 182, 0.3);
163 --accent-glow: rgba(244, 114, 182, 0.2);
164 "#,
165 Theme::Forest => r#"
167 --bg-gradient-start: #090d09;
168 --bg-gradient-mid: #0f1a12;
169 --bg-gradient-end: #090d09;
170 --accent-primary: #4ade80;
171 --accent-secondary: #86efac;
172 --accent-tertiary: #dcfce7;
173 --accent-primary-rgb: 74, 222, 128;
174 --accent-secondary-rgb: 134, 239, 172;
175 --accent-border: rgba(74, 222, 128, 0.3);
176 --accent-glow: rgba(74, 222, 128, 0.2);
177 "#,
178 Theme::Ember => r#"
180 --bg-gradient-start: #0d0808;
181 --bg-gradient-mid: #1a0f0c;
182 --bg-gradient-end: #0d0808;
183 --accent-primary: #ef4444;
184 --accent-secondary: #f97316;
185 --accent-tertiary: #fef2f2;
186 --accent-primary-rgb: 239, 68, 68;
187 --accent-secondary-rgb: 249, 115, 22;
188 --accent-border: rgba(239, 68, 68, 0.3);
189 --accent-glow: rgba(239, 68, 68, 0.2);
190 "#,
191 }
192 }
193}
194
195#[derive(Clone, Copy)]
197pub struct ThemeState {
198 current: Signal<Theme>,
199}
200
201impl ThemeState {
202 pub fn new() -> Self {
206 #[cfg(target_arch = "wasm32")]
207 let initial = LocalStorage::get::<Theme>(THEME_STORAGE_KEY).ok().unwrap_or_default();
208 #[cfg(not(target_arch = "wasm32"))]
209 let initial = Theme::default();
210
211 Self {
212 current: Signal::new(initial),
213 }
214 }
215
216 pub fn current(&self) -> Theme {
218 *self.current.read()
219 }
220
221 pub fn set_theme(&mut self, theme: Theme) {
223 self.current.set(theme);
224 #[cfg(target_arch = "wasm32")]
225 let _ = LocalStorage::set(THEME_STORAGE_KEY, theme);
226 }
227
228 pub fn cycle_theme(&mut self) {
230 let next = match self.current() {
231 Theme::Mountain => Theme::Sunrise,
232 Theme::Sunrise => Theme::Violet,
233 Theme::Violet => Theme::Ocean,
234 Theme::Ocean => Theme::Rose,
235 Theme::Rose => Theme::Forest,
236 Theme::Forest => Theme::Ember,
237 Theme::Ember => Theme::Mountain,
238 };
239 self.set_theme(next);
240 }
241}
242
243impl Default for ThemeState {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249pub fn theme_css(theme: Theme) -> String {
251 let vars = theme.css_variables();
252 format!(
253 r#":root {{
254 {vars}
255 }}
256
257 html, body {{
258 background: linear-gradient(
259 135deg,
260 var(--bg-gradient-start) 0%,
261 var(--bg-gradient-mid) 50%,
262 var(--bg-gradient-end) 100%
263 );
264 }}
265
266 /* Theme-aware accent colors */
267 .accent-primary {{
268 color: var(--accent-primary);
269 }}
270
271 .accent-secondary {{
272 color: var(--accent-secondary);
273 }}
274
275 .bg-accent-primary {{
276 background-color: var(--accent-primary);
277 }}
278
279 .bg-accent-secondary {{
280 background-color: var(--accent-secondary);
281 }}
282
283 .border-accent {{
284 border-color: var(--accent-primary);
285 }}
286
287 /* Override color-accent-blue/purple with theme colors */
288 .main-nav-link.active::after {{
289 background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)) !important;
290 }}
291
292 .btn-primary,
293 .main-nav-btn.primary {{
294 background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important;
295 box-shadow: 0 12px 30px rgba(var(--accent-primary-rgb), 0.18) !important;
296 }}
297
298 .btn-primary:hover,
299 .main-nav-btn.primary:hover {{
300 box-shadow: 0 16px 40px rgba(var(--accent-primary-rgb), 0.25) !important;
301 }}
302
303 /* Gradient buttons with theme colors */
304 .btn-gradient {{
305 background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
306 }}
307
308 .btn-gradient:hover {{
309 box-shadow: 0 8px 24px rgba(var(--accent-primary-rgb), 0.3);
310 }}
311
312 /* Theme-aware link colors */
313 a.accent-link {{
314 color: var(--accent-primary);
315 transition: color 0.2s ease;
316 }}
317
318 a.accent-link:hover {{
319 color: var(--accent-secondary);
320 }}
321
322 /* Progress bars */
323 .progress-accent,
324 .progress-fill {{
325 background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
326 }}
327
328 /* Active nav items */
329 .mobile-nav-item.active,
330 .mobile-nav-link.active {{
331 background: rgba(var(--accent-primary-rgb), 0.12) !important;
332 color: var(--accent-primary) !important;
333 }}
334
335 .learn-sidebar-module.active {{
336 background: rgba(var(--accent-primary-rgb), 0.15) !important;
337 color: var(--accent-primary) !important;
338 border-left-color: var(--accent-primary) !important;
339 }}
340
341 /* Focus rings */
342 .focus-accent:focus,
343 input:focus,
344 textarea:focus {{
345 box-shadow: 0 0 0 3px rgba(var(--accent-primary-rgb), 0.3);
346 border-color: var(--accent-primary);
347 }}
348
349 /* Badge/pill accents */
350 .badge .dot {{
351 background: var(--accent-primary) !important;
352 box-shadow: 0 0 0 6px rgba(var(--accent-primary-rgb), 0.12) !important;
353 }}
354
355 /* Card hover accents */
356 .card:hover {{
357 border-color: rgba(var(--accent-secondary-rgb), 0.28) !important;
358 }}
359
360 .card:hover::before {{
361 background: linear-gradient(135deg, rgba(var(--accent-primary-rgb), 0.12), rgba(var(--accent-secondary-rgb), 0.12)) !important;
362 }}
363
364 /* Icon box accents */
365 .icon-box {{
366 background: rgba(var(--accent-primary-rgb), 0.15) !important;
367 }}
368
369 /* Step numbers */
370 .step-num {{
371 background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)) !important;
372 }}
373
374 /* Code/logic accents */
375 .code.logic {{
376 color: var(--accent-secondary) !important;
377 }}
378 "#
379 )
380}