logicaffeine_web/ui/components/
symbol_pad.rs1use dioxus::prelude::*;
10use crate::composer::symbol_buttons;
11
12const SYMBOL_PAD_STYLE: &str = r#"
13.symbol-pad {
14 display: grid;
15 grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
16 gap: var(--spacing-sm);
17 margin-top: var(--spacing-md);
18}
19
20.symbol-key {
21 display: flex;
22 flex-direction: column;
23 align-items: center;
24 justify-content: center;
25 gap: 2px;
26 min-height: 48px;
27 padding: 6px 5px 5px;
28 border-radius: var(--radius-md);
29 border: 1px solid rgba(var(--accent-primary-rgb), 0.22);
30 background: rgba(var(--accent-primary-rgb), 0.05);
31 color: var(--text-primary);
32 font: inherit;
33 cursor: pointer;
34 touch-action: manipulation;
35 transition: background 0.15s ease, border-color 0.15s ease;
36}
37
38.symbol-key:hover {
39 background: rgba(var(--accent-primary-rgb), 0.12);
40 border-color: rgba(var(--accent-primary-rgb), 0.5);
41}
42
43.symbol-key:active {
44 background: rgba(var(--accent-primary-rgb), 0.2);
45}
46
47.symbol-key:focus-visible {
48 outline: 2px solid var(--accent-primary);
49 outline-offset: 2px;
50}
51
52.symbol-key-glyph {
53 font-family: var(--font-mono);
54 font-size: 20px;
55 line-height: 1;
56 color: var(--accent-primary);
57}
58
59.symbol-key-name {
60 font-size: 11px;
61 line-height: 1.2;
62 color: var(--text-tertiary);
63 text-align: center;
64}
65
66@media (max-width: 640px) {
67 .symbol-pad {
68 grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
69 }
70 .symbol-key {
71 min-height: 52px;
72 }
73}
74"#;
75
76#[cfg(target_arch = "wasm32")]
78fn type_into(target: &str, symbol: &str) -> Option<String> {
79 use wasm_bindgen::JsCast;
80 let field = web_sys::window()?.document()?.get_element_by_id(target)?.dyn_into::<web_sys::HtmlInputElement>().ok()?;
81 let text = field.value();
82 let end_of_text = text.encode_utf16().count() as u32;
83 let start = field.selection_start().ok().flatten().unwrap_or(end_of_text);
84 let end = field.selection_end().ok().flatten().unwrap_or(start);
85 let (typed, caret) = crate::composer::insert_at(&text, start as usize, end as usize, symbol);
86 field.set_value(&typed);
87 let _ = field.focus();
88 let _ = field.set_selection_range(caret as u32, caret as u32);
91 Some(typed)
92}
93
94#[cfg(not(target_arch = "wasm32"))]
95fn type_into(target: &str, symbol: &str) -> Option<String> {
96 let _ = (target, symbol);
97 None
98}
99
100#[component]
103pub fn SymbolPad(target: String, on_input: EventHandler<String>) -> Element {
104 rsx! {
105 style { "{SYMBOL_PAD_STYLE}" }
106 div { class: "symbol-pad", role: "group", aria_label: "Symbols",
107 for button in symbol_buttons() {
108 {
109 let field = target.clone();
110 rsx! {
111 button {
112 key: "{button.symbol}",
113 class: "symbol-key",
114 r#type: "button",
115 "data-symbol": "{button.symbol}",
116 aria_label: "{button.label()}",
117 aria_controls: "{target}",
118 title: "{button.meaning}",
119 onmousedown: move |e: Event<MouseData>| e.prevent_default(),
120 onclick: move |_| {
121 if let Some(text) = type_into(&field, button.symbol) {
122 on_input.call(text);
123 }
124 },
125 span { class: "symbol-key-glyph", aria_hidden: "true", "{button.symbol}" }
126 span { class: "symbol-key-name", aria_hidden: "true", "{button.name}" }
127 }
128 }
129 }
130 }
131 }
132 }
133}