Skip to main content

logicaffeine_web/ui/components/
symbol_palette.rs

1//! Symbol palette for Math mode.
2//!
3//! Provides a searchable grid of mathematical symbols organized by category
4//! (Greek, Logic, Sets, Relations, Operators). Clicking inserts the LaTeX code.
5//!
6//! # Props
7//!
8//! - `on_insert` - Callback with the LaTeX command to insert
9//!
10//! # Categories
11//!
12//! - **Greek**: α, β, γ... and uppercase variants
13//! - **Logic**: ∀, ∃, ∧, ∨, →, ↔, ⊢, ⊨, □, ◇
14//! - **Sets**: ∈, ⊆, ∪, ∩, ∅, ℕ, ℤ, ℚ, ℝ, ℂ
15//! - **Relations**: ≠, ≤, ≥, ≈, ≡, ∼
16//! - **Operators**: ×, ÷, ±, ·, ∘, √, ∞, ∂, ∇, ∫, Σ
17
18use dioxus::prelude::*;
19
20const SYMBOL_PALETTE_STYLE: &str = r#"
21.symbol-palette {
22    display: flex;
23    flex-direction: column;
24    height: 100%;
25    background: #12161c;
26    overflow: hidden;
27}
28
29.symbol-palette-header {
30    padding: 12px 14px;
31    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
32    background: rgba(255, 255, 255, 0.02);
33}
34
35.symbol-palette-title {
36    font-size: 14px;
37    font-weight: 600;
38    color: rgba(255, 255, 255, 0.9);
39}
40
41.symbol-search {
42    margin-top: 8px;
43}
44
45.symbol-search input {
46    width: 100%;
47    padding: 8px 10px;
48    background: rgba(255, 255, 255, 0.04);
49    border: 1px solid rgba(255, 255, 255, 0.08);
50    border-radius: 6px;
51    color: rgba(255, 255, 255, 0.9);
52    font-size: 13px;
53}
54
55.symbol-search input::placeholder {
56    color: rgba(255, 255, 255, 0.3);
57}
58
59.symbol-search input:focus {
60    outline: none;
61    border-color: rgba(102, 126, 234, 0.5);
62}
63
64.symbol-categories {
65    flex: 1;
66    overflow: auto;
67    padding: 8px;
68}
69
70.symbol-category {
71    margin-bottom: 16px;
72}
73
74.symbol-category-header {
75    font-size: 11px;
76    font-weight: 600;
77    text-transform: uppercase;
78    letter-spacing: 0.5px;
79    color: rgba(255, 255, 255, 0.4);
80    margin-bottom: 8px;
81    padding: 0 4px;
82}
83
84.symbol-grid {
85    display: grid;
86    grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
87    gap: 4px;
88}
89
90.symbol-btn {
91    aspect-ratio: 1;
92    display: flex;
93    align-items: center;
94    justify-content: center;
95    background: rgba(255, 255, 255, 0.04);
96    border: 1px solid rgba(255, 255, 255, 0.06);
97    border-radius: 6px;
98    color: rgba(255, 255, 255, 0.8);
99    font-size: 16px;
100    cursor: pointer;
101    transition: all 0.15s ease;
102}
103
104.symbol-btn:hover {
105    background: rgba(102, 126, 234, 0.15);
106    border-color: rgba(102, 126, 234, 0.3);
107    color: #667eea;
108}
109
110.symbol-btn:active {
111    transform: scale(0.95);
112}
113
114.symbol-btn[title]:hover::after {
115    content: attr(title);
116    position: absolute;
117    bottom: 100%;
118    left: 50%;
119    transform: translateX(-50%);
120    padding: 4px 8px;
121    background: #1a1f27;
122    border: 1px solid rgba(255, 255, 255, 0.1);
123    border-radius: 4px;
124    font-size: 11px;
125    white-space: nowrap;
126    z-index: 10;
127}
128
129/* Mobile adjustments */
130@media (max-width: 768px) {
131    .symbol-grid {
132        grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
133    }
134
135    .symbol-btn {
136        font-size: 18px;
137    }
138}
139"#;
140
141/// A symbol category with a name and list of symbols.
142struct SymbolCategory {
143    name: &'static str,
144    symbols: &'static [(&'static str, &'static str)], // (symbol, latex)
145}
146
147const SYMBOL_CATEGORIES: &[SymbolCategory] = &[
148    SymbolCategory {
149        name: "Greek",
150        symbols: &[
151            ("\u{03B1}", "\\alpha"),
152            ("\u{03B2}", "\\beta"),
153            ("\u{03B3}", "\\gamma"),
154            ("\u{03B4}", "\\delta"),
155            ("\u{03B5}", "\\epsilon"),
156            ("\u{03B6}", "\\zeta"),
157            ("\u{03B7}", "\\eta"),
158            ("\u{03B8}", "\\theta"),
159            ("\u{03BB}", "\\lambda"),
160            ("\u{03BC}", "\\mu"),
161            ("\u{03C0}", "\\pi"),
162            ("\u{03C3}", "\\sigma"),
163            ("\u{03C6}", "\\phi"),
164            ("\u{03C8}", "\\psi"),
165            ("\u{03C9}", "\\omega"),
166            ("\u{0393}", "\\Gamma"),
167            ("\u{0394}", "\\Delta"),
168            ("\u{03A3}", "\\Sigma"),
169            ("\u{03A6}", "\\Phi"),
170            ("\u{03A9}", "\\Omega"),
171        ],
172    },
173    SymbolCategory {
174        name: "Logic",
175        symbols: &[
176            ("\u{2200}", "\\forall"),
177            ("\u{2203}", "\\exists"),
178            ("\u{2204}", "\\nexists"),
179            ("\u{00AC}", "\\neg"),
180            ("\u{2227}", "\\land"),
181            ("\u{2228}", "\\lor"),
182            ("\u{2192}", "\\to"),
183            ("\u{2194}", "\\leftrightarrow"),
184            ("\u{21D2}", "\\Rightarrow"),
185            ("\u{21D4}", "\\Leftrightarrow"),
186            ("\u{22A2}", "\\vdash"),
187            ("\u{22A8}", "\\models"),
188            ("\u{22A4}", "\\top"),
189            ("\u{22A5}", "\\bot"),
190            ("\u{25A1}", "\\Box"),
191            ("\u{25C7}", "\\Diamond"),
192        ],
193    },
194    SymbolCategory {
195        name: "Sets",
196        symbols: &[
197            ("\u{2208}", "\\in"),
198            ("\u{2209}", "\\notin"),
199            ("\u{2286}", "\\subseteq"),
200            ("\u{2287}", "\\supseteq"),
201            ("\u{2282}", "\\subset"),
202            ("\u{2283}", "\\supset"),
203            ("\u{222A}", "\\cup"),
204            ("\u{2229}", "\\cap"),
205            ("\u{2205}", "\\emptyset"),
206            ("\u{2119}", "\\mathbb{P}"),
207            ("\u{2115}", "\\mathbb{N}"),
208            ("\u{2124}", "\\mathbb{Z}"),
209            ("\u{211A}", "\\mathbb{Q}"),
210            ("\u{211D}", "\\mathbb{R}"),
211            ("\u{2102}", "\\mathbb{C}"),
212        ],
213    },
214    SymbolCategory {
215        name: "Relations",
216        symbols: &[
217            ("\u{2260}", "\\neq"),
218            ("\u{2264}", "\\leq"),
219            ("\u{2265}", "\\geq"),
220            ("\u{226A}", "\\ll"),
221            ("\u{226B}", "\\gg"),
222            ("\u{2248}", "\\approx"),
223            ("\u{2261}", "\\equiv"),
224            ("\u{223C}", "\\sim"),
225            ("\u{2245}", "\\cong"),
226            ("\u{221D}", "\\propto"),
227        ],
228    },
229    SymbolCategory {
230        name: "Operators",
231        symbols: &[
232            ("\u{00D7}", "\\times"),
233            ("\u{00F7}", "\\div"),
234            ("\u{00B1}", "\\pm"),
235            ("\u{2213}", "\\mp"),
236            ("\u{22C5}", "\\cdot"),
237            ("\u{2218}", "\\circ"),
238            ("\u{2295}", "\\oplus"),
239            ("\u{2297}", "\\otimes"),
240            ("\u{221A}", "\\sqrt{}"),
241            ("\u{221E}", "\\infty"),
242            ("\u{2202}", "\\partial"),
243            ("\u{2207}", "\\nabla"),
244            ("\u{222B}", "\\int"),
245            ("\u{220F}", "\\prod"),
246            ("\u{2211}", "\\sum"),
247        ],
248    },
249];
250
251#[component]
252pub fn SymbolPalette(
253    on_insert: EventHandler<String>,
254) -> Element {
255    let mut search = use_signal(String::new);
256
257    let search_lower = search.read().to_lowercase();
258    let filtered_categories: Vec<_> = if search_lower.is_empty() {
259        SYMBOL_CATEGORIES.iter().collect()
260    } else {
261        SYMBOL_CATEGORIES
262            .iter()
263            .filter(|cat| {
264                cat.name.to_lowercase().contains(&search_lower)
265                    || cat.symbols.iter().any(|(_, latex)| latex.contains(&search_lower))
266            })
267            .collect()
268    };
269
270    rsx! {
271        style { "{SYMBOL_PALETTE_STYLE}" }
272
273        div { class: "symbol-palette",
274            // Header with search
275            div { class: "symbol-palette-header",
276                span { class: "symbol-palette-title", "Symbols" }
277                div { class: "symbol-search",
278                    input {
279                        r#type: "text",
280                        placeholder: "Search symbols...",
281                        value: "{search}",
282                        oninput: move |e| search.set(e.value()),
283                    }
284                }
285            }
286
287            // Symbol categories
288            div { class: "symbol-categories",
289                for category in filtered_categories {
290                    div { class: "symbol-category", key: "{category.name}",
291                        div { class: "symbol-category-header", "{category.name}" }
292                        div { class: "symbol-grid",
293                            for (symbol, latex) in category.symbols.iter() {
294                                button {
295                                    class: "symbol-btn",
296                                    key: "{latex}",
297                                    title: "{latex}",
298                                    onclick: move |_| on_insert.call(latex.to_string()),
299                                    "{symbol}"
300                                }
301                            }
302                        }
303                    }
304                }
305            }
306        }
307    }
308}