Skip to main content

logicaffeine_web/ui/components/
editor.rs

1//! Multi-line text editor component.
2//!
3//! Provides a styled textarea for entering English sentences. Supports
4//! controlled input with external value management.
5//!
6//! # Components
7//!
8//! - [`Editor`] - Controlled textarea with value prop
9//! - [`LiveEditor`] - Uncontrolled textarea with real-time updates
10//!
11//! # Props
12//!
13//! - `value` - Current editor content
14//! - `on_change` - Callback with new content on each keystroke
15//! - `placeholder` - Optional placeholder text
16
17use dioxus::prelude::*;
18
19const EDITOR_STYLE: &str = r#"
20.editor-container {
21    flex: 1;
22    display: flex;
23    flex-direction: column;
24    min-height: 200px;
25    padding: 16px;
26}
27
28.editor-wrapper {
29    flex: 1;
30    background: rgba(255, 255, 255, 0.08);
31    border: 1px solid rgba(255, 255, 255, 0.2);
32    border-radius: 12px;
33    overflow: hidden;
34}
35
36.editor-fallback {
37    width: 100%;
38    height: 100%;
39    min-height: 150px;
40    background: rgba(255, 255, 255, 0.08);
41    border: 1px solid rgba(255, 255, 255, 0.2);
42    border-radius: 12px;
43    padding: 16px;
44    font-size: 16px;
45    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
46    color: #e8e8e8;
47    resize: none;
48    outline: none;
49    -webkit-overflow-scrolling: touch;
50}
51
52.editor-fallback:focus {
53    border-color: #667eea;
54    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
55}
56
57.editor-fallback::placeholder {
58    color: #666;
59}
60
61/* Mobile optimizations */
62@media (max-width: 768px) {
63    .editor-container {
64        padding: 12px;
65        min-height: unset;
66    }
67
68    .editor-fallback {
69        /* 16px minimum prevents iOS zoom on focus */
70        font-size: 16px;
71        padding: 14px;
72        border-radius: 10px;
73        min-height: 120px;
74        /* Allow textarea to grow with content */
75        resize: vertical;
76    }
77
78    .editor-fallback:focus {
79        /* Slightly stronger focus for mobile visibility */
80        box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.25);
81    }
82
83    .editor-fallback::placeholder {
84        color: #777;
85        font-size: 15px;
86    }
87}
88
89/* Extra small screens */
90@media (max-width: 480px) {
91    .editor-container {
92        padding: 10px;
93    }
94
95    .editor-fallback {
96        padding: 12px;
97        font-size: 16px; /* Keep at 16px to prevent zoom */
98        min-height: 100px;
99    }
100}
101
102/* Landscape mobile - more horizontal space for input */
103@media (max-height: 500px) and (orientation: landscape) {
104    .editor-container {
105        padding: 8px;
106    }
107
108    .editor-fallback {
109        min-height: 80px;
110        padding: 10px 12px;
111    }
112}
113"#;
114
115#[component]
116pub fn Editor(
117    value: String,
118    on_change: EventHandler<String>,
119    placeholder: Option<String>,
120) -> Element {
121    let placeholder_text = placeholder.unwrap_or_else(|| "Type an English sentence...".to_string());
122
123    rsx! {
124        style { "{EDITOR_STYLE}" }
125
126        div { class: "editor-container",
127            textarea {
128                class: "editor-fallback",
129                placeholder: "{placeholder_text}",
130                value: "{value}",
131                oninput: move |evt| on_change.call(evt.value()),
132            }
133        }
134    }
135}
136
137#[component]
138pub fn LiveEditor(
139    on_change: EventHandler<String>,
140    placeholder: Option<String>,
141    #[props(default)] value: String,
142) -> Element {
143    let placeholder_text = placeholder.unwrap_or_else(|| "Type an English sentence...".to_string());
144
145    let handle_input = move |evt: Event<FormData>| {
146        let new_value = evt.value();
147        on_change.call(new_value);
148    };
149
150    rsx! {
151        style { "{EDITOR_STYLE}" }
152
153        div { class: "editor-container",
154            textarea {
155                class: "editor-fallback",
156                placeholder: "{placeholder_text}",
157                value: "{value}",
158                oninput: handle_input,
159                spellcheck: "false",
160                autocomplete: "off",
161                autocapitalize: "off",
162            }
163        }
164    }
165}