logicaffeine_web/ui/components/
ast_tree.rs1use dioxus::prelude::*;
20use crate::AstNode;
21
22const TREE_STYLE: &str = r#"
23.ast-tree-container {
24 display: flex;
25 flex-direction: column;
26 height: 100%;
27 overflow: auto;
28 padding: 16px;
29 -webkit-overflow-scrolling: touch;
30}
31
32.ast-tree-empty {
33 color: #666;
34 font-style: italic;
35 text-align: center;
36 padding: 40px 20px;
37}
38
39.ast-node {
40 margin-left: 16px;
41 position: relative;
42}
43
44.ast-node:before {
45 content: '';
46 position: absolute;
47 left: -12px;
48 top: 0;
49 height: 100%;
50 width: 1px;
51 background: rgba(255, 255, 255, 0.1);
52}
53
54.ast-node:last-child:before {
55 height: 12px;
56}
57
58.ast-node-label {
59 display: flex;
60 align-items: center;
61 gap: 6px;
62 padding: 4px 8px;
63 border-radius: 4px;
64 cursor: pointer;
65 transition: background 0.15s ease;
66 position: relative;
67 -webkit-tap-highlight-color: transparent;
68}
69
70.ast-node-label:hover {
71 background: rgba(255, 255, 255, 0.05);
72}
73
74.ast-node-label:before {
75 content: '';
76 position: absolute;
77 left: -12px;
78 top: 50%;
79 width: 8px;
80 height: 1px;
81 background: rgba(255, 255, 255, 0.1);
82}
83
84.ast-node-toggle {
85 width: 16px;
86 height: 16px;
87 display: flex;
88 align-items: center;
89 justify-content: center;
90 font-size: 10px;
91 color: #666;
92 transition: transform 0.15s ease;
93}
94
95.ast-node-toggle.expanded {
96 transform: rotate(90deg);
97}
98
99.ast-node-text {
100 font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
101 font-size: 13px;
102}
103
104.ast-node-type {
105 font-size: 10px;
106 padding: 2px 6px;
107 border-radius: 3px;
108 background: rgba(255, 255, 255, 0.08);
109 color: #888;
110 white-space: nowrap;
111}
112
113.ast-node-type.quantifier { background: rgba(198, 120, 221, 0.2); color: #c678dd; }
114.ast-node-type.predicate { background: rgba(152, 195, 121, 0.2); color: #98c379; }
115.ast-node-type.binary_op { background: rgba(198, 120, 221, 0.2); color: #c678dd; }
116.ast-node-type.unary_op { background: rgba(224, 108, 117, 0.2); color: #e06c75; }
117.ast-node-type.constant { background: rgba(229, 192, 123, 0.2); color: #e5c07b; }
118.ast-node-type.variable { background: rgba(97, 175, 239, 0.2); color: #61afef; }
119.ast-node-type.modal { background: rgba(86, 182, 194, 0.2); color: #56b6c2; }
120.ast-node-type.lambda { background: rgba(224, 108, 117, 0.2); color: #e06c75; }
121
122.ast-children {
123 display: none;
124}
125
126.ast-children.expanded {
127 display: block;
128}
129
130.ast-root {
131 margin-left: 0;
132}
133
134.ast-root:before {
135 display: none;
136}
137
138.ast-root > .ast-node-label:before {
139 display: none;
140}
141
142/* Mobile optimizations */
143@media (max-width: 768px) {
144 .ast-tree-container {
145 padding: 12px;
146 }
147
148 .ast-tree-empty {
149 padding: 30px 16px;
150 font-size: 14px;
151 }
152
153 /* Larger touch targets for tree nodes */
154 .ast-node {
155 margin-left: 14px;
156 }
157
158 .ast-node-label {
159 padding: 8px 10px;
160 gap: 8px;
161 min-height: 40px;
162 border-radius: 6px;
163 }
164
165 .ast-node-label:active {
166 background: rgba(255, 255, 255, 0.1);
167 }
168
169 .ast-node-toggle {
170 width: 24px;
171 height: 24px;
172 font-size: 12px;
173 }
174
175 .ast-node-text {
176 font-size: 14px;
177 word-break: break-word;
178 }
179
180 .ast-node-type {
181 font-size: 11px;
182 padding: 3px 8px;
183 border-radius: 4px;
184 }
185
186 .ast-node:before {
187 left: -10px;
188 }
189
190 .ast-node-label:before {
191 left: -10px;
192 width: 6px;
193 }
194}
195
196/* Extra small screens */
197@media (max-width: 480px) {
198 .ast-tree-container {
199 padding: 10px;
200 }
201
202 .ast-node {
203 margin-left: 12px;
204 }
205
206 .ast-node-label {
207 padding: 6px 8px;
208 min-height: 36px;
209 }
210
211 .ast-node-text {
212 font-size: 13px;
213 }
214
215 .ast-node-type {
216 font-size: 10px;
217 padding: 2px 6px;
218 }
219}
220"#;
221
222#[component]
223pub fn AstTree(ast: Option<AstNode>) -> Element {
224 rsx! {
225 style { "{TREE_STYLE}" }
226
227 div { class: "ast-tree-container",
228 if let Some(node) = ast {
229 AstNodeView { node: node, is_root: true }
230 } else {
231 div { class: "ast-tree-empty",
232 "Parse a sentence to see its AST..."
233 }
234 }
235 }
236 }
237}
238
239#[component]
240fn AstNodeView(node: AstNode, is_root: bool) -> Element {
241 let mut expanded = use_signal(|| true);
242 let has_children = !node.children.is_empty();
243
244 let node_class = if is_root { "ast-node ast-root" } else { "ast-node" };
245 let toggle_class = if *expanded.read() { "ast-node-toggle expanded" } else { "ast-node-toggle" };
246 let children_class = if *expanded.read() { "ast-children expanded" } else { "ast-children" };
247 let type_class = format!("ast-node-type {}", node.node_type);
248
249 rsx! {
250 div { class: "{node_class}",
251 div {
252 class: "ast-node-label",
253 onclick: move |_| {
254 if has_children {
255 let current = *expanded.read();
256 expanded.set(!current);
257 }
258 },
259 if has_children {
260 span { class: "{toggle_class}", "\u{25B6}" }
261 } else {
262 span { class: "ast-node-toggle", "\u{2022}" }
263 }
264 span { class: "ast-node-text", "{node.label}" }
265 span { class: "{type_class}", "{node.node_type}" }
266 }
267
268 if has_children {
269 div { class: "{children_class}",
270 for child in node.children.iter() {
271 AstNodeView { node: child.clone(), is_root: false }
272 }
273 }
274 }
275 }
276 }
277}