1use dioxus::prelude::*;
7use crate::ui::components::icon::{Icon, IconVariant, IconSize};
8
9const VOCAB_REFERENCE_STYLE: &str = r#"
10.vocab-reference-toggle {
11 position: fixed;
12 bottom: 24px;
13 right: 24px;
14 width: 48px;
15 height: 48px;
16 border-radius: 50%;
17 background: #667eea;
18 border: none;
19 color: #fff;
20 font-size: 24px;
21 cursor: pointer;
22 display: flex;
23 align-items: center;
24 justify-content: center;
25 box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
26 transition: transform 0.2s ease, box-shadow 0.2s ease;
27 z-index: 1000;
28}
29
30.vocab-reference-toggle:hover {
31 transform: scale(1.05);
32 box-shadow: 0 6px 24px rgba(96, 165, 250, 0.4);
33}
34
35.vocab-reference-toggle.active {
36 background: rgba(255, 255, 255, 0.1);
37 color: var(--text-primary);
38}
39
40/* Attention animation - pulses after delay */
41.vocab-reference-toggle.attention {
42 animation: attentionPulse 2s ease-in-out 3;
43}
44
45@keyframes attentionPulse {
46 0%, 100% {
47 transform: scale(1);
48 box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
49 }
50 50% {
51 transform: scale(1.15);
52 box-shadow: 0 6px 30px rgba(102, 126, 234, 0.6);
53 }
54}
55
56.vocab-reference-panel {
57 position: fixed;
58 bottom: 84px;
59 right: 24px;
60 width: 360px;
61 max-height: 70vh;
62 background: #12161c;
63 border: 1px solid rgba(255, 255, 255, 0.12);
64 border-radius: 12px;
65 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
66 z-index: 999;
67 overflow: hidden;
68 animation: slideUp 0.2s ease;
69}
70
71@keyframes slideUp {
72 from {
73 opacity: 0;
74 transform: translateY(16px);
75 }
76 to {
77 opacity: 1;
78 transform: translateY(0);
79 }
80}
81
82.vocab-panel-header {
83 display: flex;
84 align-items: center;
85 justify-content: space-between;
86 padding: var(--spacing-lg);
87 border-bottom: 1px solid rgba(255, 255, 255, 0.08);
88 background: rgba(255, 255, 255, 0.03);
89}
90
91.vocab-panel-title {
92 font-size: var(--font-body-lg);
93 font-weight: 700;
94 color: var(--text-primary);
95 display: flex;
96 align-items: center;
97 gap: var(--spacing-sm);
98}
99
100.vocab-panel-close {
101 width: 28px;
102 height: 28px;
103 border-radius: 50%;
104 background: rgba(255, 255, 255, 0.08);
105 border: 1px solid rgba(255, 255, 255, 0.12);
106 color: var(--text-secondary);
107 font-size: 14px;
108 cursor: pointer;
109 display: flex;
110 align-items: center;
111 justify-content: center;
112 transition: all 0.15s ease;
113}
114
115.vocab-panel-close:hover {
116 background: rgba(255, 255, 255, 0.15);
117 color: var(--text-primary);
118}
119
120.vocab-panel-content {
121 padding: var(--spacing-lg);
122 max-height: calc(70vh - 60px);
123 overflow-y: auto;
124}
125
126.vocab-section {
127 margin-bottom: var(--spacing-xl);
128}
129
130.vocab-section:last-child {
131 margin-bottom: 0;
132}
133
134.vocab-section-title {
135 font-size: var(--font-caption-md);
136 font-weight: 600;
137 text-transform: uppercase;
138 letter-spacing: 0.5px;
139 margin-bottom: var(--spacing-md);
140 display: flex;
141 align-items: center;
142 gap: var(--spacing-xs);
143}
144
145.vocab-section-title.quantifiers {
146 color: #60a5fa;
147}
148
149.vocab-section-title.connectives {
150 color: #f472b6;
151}
152
153.vocab-section-title.predicates {
154 color: #4ade80;
155}
156
157.vocab-section-title.terms {
158 color: #a78bfa;
159}
160
161.vocab-items {
162 display: flex;
163 flex-direction: column;
164 gap: var(--spacing-xs);
165}
166
167.vocab-item {
168 display: flex;
169 align-items: center;
170 gap: var(--spacing-md);
171 padding: var(--spacing-sm) var(--spacing-md);
172 background: rgba(255, 255, 255, 0.03);
173 border-radius: var(--radius-md);
174 transition: background 0.15s ease;
175}
176
177.vocab-item:hover {
178 background: rgba(255, 255, 255, 0.06);
179}
180
181.vocab-symbol {
182 font-family: var(--font-mono);
183 font-size: var(--font-body-lg);
184 min-width: 36px;
185 text-align: center;
186 font-weight: 600;
187}
188
189.vocab-symbol.quantifier {
190 color: #60a5fa;
191}
192
193.vocab-symbol.connective {
194 color: #f472b6;
195}
196
197.vocab-symbol.predicate {
198 color: #4ade80;
199}
200
201.vocab-info {
202 flex: 1;
203}
204
205.vocab-name {
206 font-size: var(--font-body-sm);
207 font-weight: 600;
208 color: var(--text-primary);
209}
210
211.vocab-meaning {
212 font-size: var(--font-caption-md);
213 color: var(--text-tertiary);
214}
215
216.vocab-term-item {
217 padding: var(--spacing-md);
218 background: rgba(255, 255, 255, 0.03);
219 border-radius: var(--radius-md);
220 margin-bottom: var(--spacing-sm);
221}
222
223.vocab-term-name {
224 font-weight: 600;
225 color: var(--color-accent-purple);
226 margin-bottom: var(--spacing-xs);
227}
228
229.vocab-term-def {
230 font-size: var(--font-body-sm);
231 color: var(--text-secondary);
232 line-height: 1.5;
233}
234
235/* Search box */
236.vocab-search {
237 margin-bottom: var(--spacing-lg);
238}
239
240.vocab-search-input {
241 width: 100%;
242 padding: var(--spacing-sm) var(--spacing-md);
243 background: rgba(255, 255, 255, 0.06);
244 border: 1px solid rgba(255, 255, 255, 0.12);
245 border-radius: var(--radius-md);
246 color: var(--text-primary);
247 font-size: var(--font-body-sm);
248 outline: none;
249 transition: border-color 0.2s ease;
250}
251
252.vocab-search-input:focus {
253 border-color: var(--color-accent-blue);
254}
255
256.vocab-search-input::placeholder {
257 color: var(--text-tertiary);
258}
259"#;
260
261struct SymbolRef {
263 symbol: &'static str,
264 name: &'static str,
265 meaning: &'static str,
266 category: &'static str,
267}
268
269struct VocabTerm {
271 term: &'static str,
272 definition: &'static str,
273}
274
275fn get_symbols() -> Vec<SymbolRef> {
277 vec![
278 SymbolRef { symbol: "∀", name: "Universal", meaning: "for all", category: "quantifier" },
280 SymbolRef { symbol: "∃", name: "Existential", meaning: "there exists", category: "quantifier" },
281 SymbolRef { symbol: "∃!", name: "Unique", meaning: "there exists exactly one", category: "quantifier" },
282 SymbolRef { symbol: "∧", name: "Conjunction", meaning: "and", category: "connective" },
284 SymbolRef { symbol: "∨", name: "Disjunction", meaning: "or", category: "connective" },
285 SymbolRef { symbol: "→", name: "Implication", meaning: "if...then", category: "connective" },
286 SymbolRef { symbol: "↔", name: "Biconditional", meaning: "if and only if", category: "connective" },
287 SymbolRef { symbol: "¬", name: "Negation", meaning: "not", category: "connective" },
288 SymbolRef { symbol: "□", name: "Necessity", meaning: "necessarily", category: "connective" },
290 SymbolRef { symbol: "◇", name: "Possibility", meaning: "possibly", category: "connective" },
291 SymbolRef { symbol: "=", name: "Identity", meaning: "equals", category: "predicate" },
293 SymbolRef { symbol: "≠", name: "Non-identity", meaning: "not equal to", category: "predicate" },
294 ]
295}
296
297fn get_vocab_terms() -> Vec<VocabTerm> {
299 vec![
300 VocabTerm {
301 term: "Predicate",
302 definition: "A property or relation, written as a capitalized name with arguments in parentheses. E.g., Happy(alice)"
303 },
304 VocabTerm {
305 term: "Constant",
306 definition: "A name for a specific individual, written in lowercase. E.g., alice, bob, socrates"
307 },
308 VocabTerm {
309 term: "Variable",
310 definition: "A placeholder that can refer to any individual, typically x, y, z"
311 },
312 VocabTerm {
313 term: "Valid Argument",
314 definition: "An argument where the conclusion necessarily follows from the premises"
315 },
316 VocabTerm {
317 term: "Sound Argument",
318 definition: "A valid argument with premises that are actually true"
319 },
320 VocabTerm {
321 term: "Domain",
322 definition: "The set of all objects that variables can refer to"
323 },
324 VocabTerm {
325 term: "Scope",
326 definition: "The part of a formula to which a quantifier or connective applies"
327 },
328 ]
329}
330
331#[derive(Props, Clone, PartialEq)]
333pub struct VocabReferenceProps {
334 #[props(default = false)]
336 pub initial_open: bool,
337}
338
339#[component]
341pub fn VocabReference(props: VocabReferenceProps) -> Element {
342 let mut is_open = use_signal(|| props.initial_open);
343 let mut search_query = use_signal(|| String::new());
344 let mut show_attention = use_signal(|| false);
345
346 use_effect(move || {
348 spawn(async move {
349 gloo_timers::future::TimeoutFuture::new(10_000).await;
351 if !*is_open.peek() {
353 show_attention.set(true);
354 }
355 });
356 });
357
358 let symbols = get_symbols();
359 let vocab_terms = get_vocab_terms();
360
361 let query = search_query.read().to_lowercase();
363 let filtered_symbols: Vec<_> = if query.is_empty() {
364 symbols.iter().collect()
365 } else {
366 symbols.iter().filter(|s| {
367 s.symbol.to_lowercase().contains(&query) ||
368 s.name.to_lowercase().contains(&query) ||
369 s.meaning.to_lowercase().contains(&query)
370 }).collect()
371 };
372
373 let filtered_terms: Vec<_> = if query.is_empty() {
374 vocab_terms.iter().collect()
375 } else {
376 vocab_terms.iter().filter(|t| {
377 t.term.to_lowercase().contains(&query) ||
378 t.definition.to_lowercase().contains(&query)
379 }).collect()
380 };
381
382 let quantifiers: Vec<_> = filtered_symbols.iter().filter(|s| s.category == "quantifier").collect();
384 let connectives: Vec<_> = filtered_symbols.iter().filter(|s| s.category == "connective").collect();
385 let predicates: Vec<_> = filtered_symbols.iter().filter(|s| s.category == "predicate").collect();
386
387 rsx! {
388 style { "{VOCAB_REFERENCE_STYLE}" }
389
390 button {
392 class: {
393 let open = *is_open.read();
394 let attention = *show_attention.read();
395 if open {
396 "vocab-reference-toggle active"
397 } else if attention {
398 "vocab-reference-toggle attention"
399 } else {
400 "vocab-reference-toggle"
401 }
402 },
403 onclick: move |_| {
404 let current = *is_open.read();
405 is_open.set(!current);
406 show_attention.set(false);
408 },
409 title: "Symbol & Vocabulary Reference",
410 if *is_open.read() {
411 Icon { variant: IconVariant::Close, size: IconSize::Large }
412 } else {
413 Icon { variant: IconVariant::Book, size: IconSize::Large }
414 }
415 }
416
417 if *is_open.read() {
419 div { class: "vocab-reference-panel",
420 div { class: "vocab-panel-header",
422 div { class: "vocab-panel-title",
423 Icon { variant: IconVariant::Book, size: IconSize::Medium }
424 "Reference"
425 }
426 button {
427 class: "vocab-panel-close",
428 onclick: move |_| is_open.set(false),
429 "×"
430 }
431 }
432
433 div { class: "vocab-panel-content",
435 div { class: "vocab-search",
437 input {
438 class: "vocab-search-input",
439 r#type: "text",
440 placeholder: "Search symbols or terms...",
441 value: "{search_query}",
442 oninput: move |e| search_query.set(e.value()),
443 }
444 }
445
446 if !quantifiers.is_empty() {
448 div { class: "vocab-section",
449 div { class: "vocab-section-title quantifiers", "Quantifiers" }
450 div { class: "vocab-items",
451 for sym in quantifiers {
452 div { class: "vocab-item",
453 span { class: "vocab-symbol quantifier", "{sym.symbol}" }
454 div { class: "vocab-info",
455 div { class: "vocab-name", "{sym.name}" }
456 div { class: "vocab-meaning", "{sym.meaning}" }
457 }
458 }
459 }
460 }
461 }
462 }
463
464 if !connectives.is_empty() {
466 div { class: "vocab-section",
467 div { class: "vocab-section-title connectives", "Connectives" }
468 div { class: "vocab-items",
469 for sym in connectives {
470 div { class: "vocab-item",
471 span { class: "vocab-symbol connective", "{sym.symbol}" }
472 div { class: "vocab-info",
473 div { class: "vocab-name", "{sym.name}" }
474 div { class: "vocab-meaning", "{sym.meaning}" }
475 }
476 }
477 }
478 }
479 }
480 }
481
482 if !predicates.is_empty() {
484 div { class: "vocab-section",
485 div { class: "vocab-section-title predicates", "Predicates & Relations" }
486 div { class: "vocab-items",
487 for sym in predicates {
488 div { class: "vocab-item",
489 span { class: "vocab-symbol predicate", "{sym.symbol}" }
490 div { class: "vocab-info",
491 div { class: "vocab-name", "{sym.name}" }
492 div { class: "vocab-meaning", "{sym.meaning}" }
493 }
494 }
495 }
496 }
497 }
498 }
499
500 if !filtered_terms.is_empty() {
502 div { class: "vocab-section",
503 div { class: "vocab-section-title terms", "Key Terms" }
504 for term in filtered_terms {
505 div { class: "vocab-term-item",
506 div { class: "vocab-term-name", "{term.term}" }
507 div { class: "vocab-term-def", "{term.definition}" }
508 }
509 }
510 }
511 }
512 }
513 }
514 }
515 }
516}