logicaffeine_web/ui/components/
module_tabs.rs1use dioxus::prelude::*;
7use crate::learn_state::TabMode;
8
9const MODULE_TABS_STYLE: &str = r#"
10.module-tabs {
11 display: flex;
12 gap: 4px;
13 padding: 4px;
14 background: rgba(255, 255, 255, 0.03);
15 border-radius: 12px;
16 border: 1px solid rgba(255, 255, 255, 0.08);
17 width: fit-content;
18}
19
20.module-tab {
21 padding: 10px 18px;
22 border-radius: 8px;
23 border: none;
24 background: transparent;
25 color: rgba(229, 231, 235, 0.56);
26 font-size: 12px;
27 font-weight: 600;
28 text-transform: uppercase;
29 letter-spacing: 0.5px;
30 cursor: pointer;
31 transition: all 0.2s ease;
32 white-space: nowrap;
33}
34
35.module-tab:hover:not(.locked):not(.active) {
36 color: rgba(229, 231, 235, 0.85);
37 background: rgba(255, 255, 255, 0.05);
38}
39
40.module-tab.active {
41 background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(167, 139, 250, 0.2));
42 color: #e5e7eb;
43 box-shadow: 0 2px 8px rgba(96, 165, 250, 0.15);
44}
45
46.module-tab.locked {
47 opacity: 0.4;
48 cursor: not-allowed;
49}
50
51.module-tab.lesson.active {
52 background: linear-gradient(135deg, rgba(96, 165, 250, 0.25), rgba(96, 165, 250, 0.15));
53}
54
55.module-tab.examples.active {
56 background: linear-gradient(135deg, rgba(167, 139, 250, 0.25), rgba(167, 139, 250, 0.15));
57}
58
59.module-tab.practice.active {
60 background: linear-gradient(135deg, rgba(74, 222, 128, 0.25), rgba(74, 222, 128, 0.15));
61}
62
63.module-tab.test.active {
64 background: linear-gradient(135deg, rgba(251, 146, 60, 0.25), rgba(251, 146, 60, 0.15));
65}
66
67/* Compact variant for inline use */
68.module-tabs.compact {
69 padding: 2px;
70 gap: 2px;
71}
72
73.module-tabs.compact .module-tab {
74 padding: 6px 12px;
75 font-size: 11px;
76}
77"#;
78
79#[derive(Props, Clone, PartialEq)]
81pub struct ModuleTabsProps {
82 current: TabMode,
84 on_change: EventHandler<TabMode>,
86 #[props(default)]
88 locked_tabs: Vec<TabMode>,
89 #[props(default = false)]
91 compact: bool,
92}
93
94#[component]
96pub fn ModuleTabs(props: ModuleTabsProps) -> Element {
97 let container_class = if props.compact {
98 "module-tabs compact"
99 } else {
100 "module-tabs"
101 };
102
103 rsx! {
104 style { "{MODULE_TABS_STYLE}" }
105 div { class: "{container_class}",
106 for tab in TabMode::all() {
107 {
108 let is_active = tab == props.current;
109 let is_locked = props.locked_tabs.contains(&tab);
110 let tab_class_name = match tab {
111 TabMode::Lesson => "lesson",
112 TabMode::Examples => "examples",
113 TabMode::Practice => "practice",
114 TabMode::Test => "test",
115 };
116 let class = format!(
117 "module-tab {}{}{}",
118 tab_class_name,
119 if is_active { " active" } else { "" },
120 if is_locked { " locked" } else { "" }
121 );
122
123 rsx! {
124 button {
125 key: "{tab_class_name}",
126 class: "{class}",
127 disabled: is_locked,
128 onclick: {
129 let on_change = props.on_change.clone();
130 move |_| {
131 if !is_locked {
132 on_change.call(tab);
133 }
134 }
135 },
136 "{tab.label()}"
137 }
138 }
139 }
140 }
141 }
142 }
143}
144
145#[component]
147pub fn TabButton(
148 tab: TabMode,
149 is_active: bool,
150 #[props(default = false)] is_locked: bool,
151 on_click: EventHandler<TabMode>,
152) -> Element {
153 let tab_class_name = match tab {
154 TabMode::Lesson => "lesson",
155 TabMode::Examples => "examples",
156 TabMode::Practice => "practice",
157 TabMode::Test => "test",
158 };
159 let class = format!(
160 "module-tab {}{}{}",
161 tab_class_name,
162 if is_active { " active" } else { "" },
163 if is_locked { " locked" } else { "" }
164 );
165
166 rsx! {
167 style { "{MODULE_TABS_STYLE}" }
168 button {
169 class: "{class}",
170 disabled: is_locked,
171 onclick: move |_| {
172 if !is_locked {
173 on_click.call(tab);
174 }
175 },
176 "{tab.label()}"
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_tab_class_names() {
187 assert!(TabMode::Lesson.label().contains("LESSON"));
189 assert!(TabMode::Practice.label().contains("PRACTICE"));
190 }
191}