logicaffeine_web/ui/components/
guide_code_block.rs1use dioxus::prelude::*;
11use crate::ui::pages::guide::content::ExampleMode;
12use logicaffeine_compile::{compile_for_ui, interpret_for_ui};
13
14const CODE_BLOCK_STYLE: &str = r#"
15.guide-code-block {
16 border-radius: 16px;
17 border: 1px solid rgba(255,255,255,0.10);
18 background: rgba(0,0,0,0.35);
19 overflow: hidden;
20 margin: 20px 0;
21 box-shadow: 0 8px 32px rgba(0,0,0,0.3);
22}
23
24.guide-code-header {
25 display: flex;
26 justify-content: space-between;
27 align-items: center;
28 padding: 12px 16px;
29 background: rgba(255,255,255,0.04);
30 border-bottom: 1px solid rgba(255,255,255,0.08);
31}
32
33.guide-code-label {
34 display: flex;
35 align-items: center;
36 gap: 10px;
37}
38
39.guide-code-title {
40 font-size: 13px;
41 font-weight: 600;
42 color: rgba(229,231,235,0.9);
43}
44
45.guide-code-mode {
46 font-size: 11px;
47 padding: 4px 10px;
48 border-radius: 999px;
49 font-weight: 600;
50 text-transform: uppercase;
51 letter-spacing: 0.5px;
52}
53
54.guide-code-mode.logic {
55 background: rgba(167,139,250,0.2);
56 color: #a78bfa;
57 border: 1px solid rgba(167,139,250,0.3);
58}
59
60.guide-code-mode.imperative {
61 background: rgba(34,197,94,0.2);
62 color: #22c55e;
63 border: 1px solid rgba(34,197,94,0.3);
64}
65
66.guide-code-actions {
67 display: flex;
68 gap: 8px;
69}
70
71.guide-code-btn {
72 padding: 8px 14px;
73 border-radius: 8px;
74 border: 1px solid rgba(255,255,255,0.12);
75 background: rgba(255,255,255,0.06);
76 color: rgba(229,231,235,0.8);
77 font-size: 12px;
78 font-weight: 600;
79 cursor: pointer;
80 transition: all 0.2s ease;
81 display: flex;
82 align-items: center;
83 gap: 6px;
84}
85
86.guide-code-btn:hover {
87 background: rgba(255,255,255,0.10);
88 border-color: rgba(255,255,255,0.20);
89 color: #fff;
90}
91
92.guide-code-btn:active {
93 transform: scale(0.97);
94}
95
96.guide-code-btn.primary {
97 background: linear-gradient(135deg, rgba(96,165,250,0.9), rgba(167,139,250,0.9));
98 border-color: rgba(255,255,255,0.2);
99 color: #060814;
100}
101
102.guide-code-btn.primary:hover {
103 background: linear-gradient(135deg, #60a5fa, #a78bfa);
104}
105
106.guide-code-btn.running {
107 opacity: 0.7;
108 cursor: wait;
109}
110
111.guide-code-editor {
112 position: relative;
113}
114
115.guide-code-textarea {
116 width: 100%;
117 min-height: 80px;
118 padding: 16px;
119 background: transparent;
120 border: none;
121 color: rgba(229,231,235,0.95);
122 font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, 'Cascadia Code', monospace;
123 font-size: 14px;
124 line-height: 1.7;
125 resize: vertical;
126 outline: none;
127 tab-size: 4;
128}
129
130.guide-code-textarea::placeholder {
131 color: rgba(229,231,235,0.3);
132}
133
134.guide-code-output {
135 border-top: 1px solid rgba(255,255,255,0.08);
136 background: rgba(0,0,0,0.25);
137}
138
139.guide-code-output-header {
140 display: flex;
141 align-items: center;
142 justify-content: space-between;
143 padding: 10px 16px;
144 font-size: 11px;
145 font-weight: 600;
146 text-transform: uppercase;
147 letter-spacing: 0.5px;
148 color: rgba(229,231,235,0.5);
149 border-bottom: 1px solid rgba(255,255,255,0.06);
150}
151
152.guide-code-output-content {
153 padding: 16px;
154 font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, monospace;
155 font-size: 14px;
156 line-height: 1.6;
157 white-space: pre-wrap;
158 word-break: break-word;
159}
160
161.guide-code-output-content.success {
162 color: #a78bfa;
163}
164
165.guide-code-output-content.error {
166 color: #f87171;
167}
168
169.guide-code-output-content.info {
170 color: rgba(229,231,235,0.7);
171}
172
173.guide-code-copied {
174 position: absolute;
175 top: 50%;
176 left: 50%;
177 transform: translate(-50%, -50%);
178 padding: 8px 16px;
179 background: rgba(34,197,94,0.9);
180 color: #fff;
181 border-radius: 8px;
182 font-size: 13px;
183 font-weight: 600;
184 animation: fadeInOut 1.5s ease forwards;
185 pointer-events: none;
186}
187
188@keyframes fadeInOut {
189 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
190 15% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
191 85% { opacity: 1; }
192 100% { opacity: 0; }
193}
194
195.guide-code-placeholder {
196 padding: 24px 16px;
197 text-align: center;
198 color: rgba(229,231,235,0.4);
199 font-size: 13px;
200}
201"#;
202
203#[derive(Props, Clone, PartialEq)]
204pub struct GuideCodeBlockProps {
205 pub id: String,
206 pub label: String,
207 pub mode: ExampleMode,
208 pub initial_code: String,
209}
210
211#[component]
212pub fn GuideCodeBlock(props: GuideCodeBlockProps) -> Element {
213 let mut code = use_signal(|| props.initial_code.clone());
214 let mut output = use_signal(String::new);
215 let mut output_type = use_signal(|| "info".to_string());
216 let mut is_running = use_signal(|| false);
217 let mut show_copied = use_signal(|| false);
218 let mut has_run = use_signal(|| false);
219
220 let initial_code = props.initial_code.clone();
221 let mode = props.mode;
222 let id = props.id.clone();
223
224 let handle_run = move |_| {
226 is_running.set(true);
227 has_run.set(true);
228
229 let current_code = code.read().clone();
230
231 match mode {
232 ExampleMode::Logic => {
233 let result = compile_for_ui(¤t_code);
235 if let Some(logic) = result.logic {
236 output.set(logic);
237 output_type.set("success".to_string());
238 } else if let Some(err) = result.error {
239 output.set(err);
240 output_type.set("error".to_string());
241 } else {
242 output.set("No output".to_string());
243 output_type.set("info".to_string());
244 }
245 }
246 ExampleMode::Imperative => {
247 spawn(async move {
249 let result = interpret_for_ui(¤t_code).await;
250 if let Some(err) = result.error {
251 output.set(err);
252 output_type.set("error".to_string());
253 } else if result.lines.is_empty() {
254 output.set("(no output)".to_string());
255 output_type.set("info".to_string());
256 } else {
257 output.set(result.lines.join("\n"));
258 output_type.set("success".to_string());
259 }
260 is_running.set(false);
261 });
262 return;
263 }
264 }
265
266 is_running.set(false);
267 };
268
269 let handle_copy = move |_| {
271 let code_to_copy = code.read().clone();
272
273 #[cfg(target_arch = "wasm32")]
274 {
275 if let Some(window) = web_sys::window() {
276 let clipboard = window.navigator().clipboard();
277 let _ = clipboard.write_text(&code_to_copy);
278 show_copied.set(true);
279
280 spawn(async move {
282 gloo_timers::future::TimeoutFuture::new(1500).await;
283 show_copied.set(false);
284 });
285 }
286 }
287
288 #[cfg(not(target_arch = "wasm32"))]
289 {
290 let _ = code_to_copy;
291 show_copied.set(true);
292 }
293 };
294
295 let handle_reset = {
297 let initial = initial_code.clone();
298 move |_| {
299 code.set(initial.clone());
300 output.set(String::new());
301 has_run.set(false);
302 }
303 };
304
305 let mode_class = match mode {
306 ExampleMode::Logic => "logic",
307 ExampleMode::Imperative => "imperative",
308 };
309
310 let mode_label = match mode {
311 ExampleMode::Logic => "Logic",
312 ExampleMode::Imperative => "Run",
313 };
314
315 let line_count = code.read().lines().count();
317 let rows = (line_count + 2).max(4) as i64; rsx! {
320 style { "{CODE_BLOCK_STYLE}" }
321
322 div {
323 class: "guide-code-block",
324 id: "{id}",
325
326 div { class: "guide-code-header",
328 div { class: "guide-code-label",
329 span { class: "guide-code-title", "{props.label}" }
330 span { class: "guide-code-mode {mode_class}",
331 match mode {
332 ExampleMode::Logic => "Logic Mode",
333 ExampleMode::Imperative => "Imperative",
334 }
335 }
336 }
337
338 div { class: "guide-code-actions",
339 button {
340 class: if *is_running.read() { "guide-code-btn primary running" } else { "guide-code-btn primary" },
341 onclick: handle_run,
342 disabled: *is_running.read(),
343 if *is_running.read() {
344 "Running..."
345 } else {
346 "{mode_label}"
347 }
348 }
349 button {
350 class: "guide-code-btn",
351 onclick: handle_copy,
352 "Copy"
353 }
354 button {
355 class: "guide-code-btn",
356 onclick: handle_reset,
357 "Reset"
358 }
359 }
360 }
361
362 div { class: "guide-code-editor",
364 textarea {
365 class: "guide-code-textarea",
366 rows: rows,
367 value: "{code}",
368 oninput: move |evt| code.set(evt.value()),
369 spellcheck: "false",
370 autocomplete: "off",
371 autocapitalize: "off",
372 }
373
374 if *show_copied.read() {
375 div { class: "guide-code-copied", "Copied!" }
376 }
377 }
378
379 if *has_run.read() {
381 div { class: "guide-code-output",
382 div { class: "guide-code-output-header",
383 span { "Output" }
384 }
385 div {
386 class: "guide-code-output-content {output_type}",
387 "{output}"
388 }
389 }
390 }
391 }
392 }
393}