logicaffeine_web/ui/components/
symbol_dictionary.rs1use dioxus::prelude::*;
7use logicaffeine_language::symbol_dict::{extract_symbols, group_symbols_by_kind, SymbolKind};
8
9const SYMBOL_DICT_STYLE: &str = r#"
10.symbol-dictionary {
11 background: rgba(255, 255, 255, 0.03);
12 border: 1px solid rgba(255, 255, 255, 0.08);
13 border-radius: 12px;
14 padding: 16px;
15 margin-top: 16px;
16}
17
18.symbol-dictionary.collapsed {
19 padding: 12px 16px;
20 cursor: pointer;
21}
22
23.symbol-dictionary.collapsed:hover {
24 background: rgba(255, 255, 255, 0.05);
25}
26
27.symbol-dict-header {
28 display: flex;
29 align-items: center;
30 justify-content: space-between;
31 font-size: 12px;
32 font-weight: 600;
33 text-transform: uppercase;
34 letter-spacing: 0.5px;
35 color: rgba(229, 231, 235, 0.56);
36 margin-bottom: 12px;
37}
38
39.symbol-dictionary.collapsed .symbol-dict-header {
40 margin-bottom: 0;
41}
42
43.symbol-dict-toggle {
44 color: rgba(229, 231, 235, 0.4);
45 font-size: 14px;
46 cursor: pointer;
47 transition: transform 0.2s ease;
48}
49
50.symbol-dictionary.collapsed .symbol-dict-toggle {
51 transform: rotate(-90deg);
52}
53
54.symbol-dict-content {
55 display: flex;
56 flex-direction: column;
57 gap: 16px;
58}
59
60.symbol-group {
61 margin-bottom: 0;
62}
63
64.symbol-group-title {
65 font-size: 11px;
66 font-weight: 600;
67 color: #a78bfa;
68 margin-bottom: 8px;
69 text-transform: uppercase;
70 letter-spacing: 0.5px;
71}
72
73.symbol-group-title.quantifier {
74 color: #60a5fa;
75}
76
77.symbol-group-title.connective {
78 color: #f472b6;
79}
80
81.symbol-group-title.modal {
82 color: #c084fc;
83}
84
85.symbol-group-title.predicate {
86 color: #4ade80;
87}
88
89.symbol-group-title.variable {
90 color: #fbbf24;
91}
92
93.symbol-group-title.constant {
94 color: #fb923c;
95}
96
97.symbol-entries {
98 display: flex;
99 flex-direction: column;
100 gap: 4px;
101}
102
103.symbol-entry {
104 display: flex;
105 align-items: center;
106 gap: 12px;
107 padding: 8px 10px;
108 border-radius: 6px;
109 transition: background 0.15s ease;
110}
111
112.symbol-entry:hover {
113 background: rgba(255, 255, 255, 0.05);
114}
115
116.symbol-glyph {
117 font-size: 18px;
118 font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
119 color: #60a5fa;
120 min-width: 32px;
121 text-align: center;
122}
123
124.symbol-glyph.quantifier {
125 color: #60a5fa;
126}
127
128.symbol-glyph.connective {
129 color: #f472b6;
130}
131
132.symbol-glyph.modal {
133 color: #c084fc;
134}
135
136.symbol-glyph.predicate {
137 color: #4ade80;
138}
139
140.symbol-glyph.variable {
141 color: #fbbf24;
142}
143
144.symbol-glyph.constant {
145 color: #fb923c;
146}
147
148.symbol-desc {
149 font-size: 13px;
150 color: rgba(229, 231, 235, 0.72);
151}
152
153.symbol-dictionary-empty {
154 color: rgba(229, 231, 235, 0.4);
155 font-size: 13px;
156 text-align: center;
157 padding: 8px;
158}
159
160/* Compact inline variant */
161.symbol-dictionary.inline {
162 padding: 12px;
163 margin-top: 8px;
164}
165
166.symbol-dictionary.inline .symbol-dict-header {
167 margin-bottom: 8px;
168}
169
170.symbol-dictionary.inline .symbol-entries {
171 flex-direction: row;
172 flex-wrap: wrap;
173 gap: 8px;
174}
175
176.symbol-dictionary.inline .symbol-entry {
177 padding: 4px 8px;
178 background: rgba(255, 255, 255, 0.03);
179 border-radius: 4px;
180}
181
182.symbol-dictionary.inline .symbol-glyph {
183 font-size: 14px;
184 min-width: 20px;
185}
186
187.symbol-dictionary.inline .symbol-desc {
188 font-size: 11px;
189}
190"#;
191
192#[derive(Props, Clone, PartialEq)]
194pub struct SymbolDictionaryProps {
195 logic: String,
197 #[props(default = false)]
199 collapsed: bool,
200 #[props(default = false)]
202 inline: bool,
203}
204
205#[component]
207pub fn SymbolDictionary(props: SymbolDictionaryProps) -> Element {
208 let mut is_collapsed = use_signal(|| props.collapsed);
209
210 let symbols = extract_symbols(&props.logic);
211
212 if symbols.is_empty() {
213 return rsx! { "" };
214 }
215
216 let grouped = group_symbols_by_kind(&symbols);
217
218 let container_class = format!(
219 "symbol-dictionary{}{}",
220 if *is_collapsed.read() { " collapsed" } else { "" },
221 if props.inline { " inline" } else { "" }
222 );
223
224 rsx! {
225 style { "{SYMBOL_DICT_STYLE}" }
226 div {
227 class: "{container_class}",
228 onclick: move |_| {
229 if *is_collapsed.read() {
230 is_collapsed.set(false);
231 }
232 },
233
234 div { class: "symbol-dict-header",
235 span { "Symbol Dictionary" }
236 span {
237 class: "symbol-dict-toggle",
238 onclick: move |e| {
239 e.stop_propagation();
240 let current = *is_collapsed.read();
241 is_collapsed.set(!current);
242 },
243 "▼"
244 }
245 }
246
247 if !*is_collapsed.read() {
248 div { class: "symbol-dict-content",
249 for (kind, entries) in grouped {
250 {
251 let kind_class = match kind {
252 SymbolKind::Quantifier => "quantifier",
253 SymbolKind::Connective => "connective",
254 SymbolKind::Modal => "modal",
255 SymbolKind::Predicate => "predicate",
256 SymbolKind::Variable => "variable",
257 SymbolKind::Constant => "constant",
258 SymbolKind::Temporal => "temporal",
259 SymbolKind::Identity => "connective",
260 SymbolKind::Punctuation => "variable",
261 };
262
263 rsx! {
264 div { class: "symbol-group",
265 key: "{kind_class}",
266 div { class: "symbol-group-title {kind_class}",
267 "{kind.label()}"
268 }
269 div { class: "symbol-entries",
270 for entry in entries {
271 div { class: "symbol-entry",
272 key: "{entry.symbol}",
273 span { class: "symbol-glyph {kind_class}",
274 "{entry.symbol}"
275 }
276 span { class: "symbol-desc",
277 "{entry.description}"
278 }
279 }
280 }
281 }
282 }
283 }
284 }
285 }
286 }
287 }
288 }
289 }
290}
291
292#[component]
294pub fn SymbolLegend(logic: String) -> Element {
295 rsx! {
296 SymbolDictionary {
297 logic: logic,
298 inline: true,
299 collapsed: false,
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_symbol_kind_class_names() {
310 assert_eq!(SymbolKind::Quantifier.label(), "Quantifier");
311 assert_eq!(SymbolKind::Connective.label(), "Connective");
312 }
313}