Skip to main content

maud_ui/primitives/
input.rs

1//! Input component — text, email, password, and other input types.
2
3use maud::{html, Markup};
4
5/// Input type variants
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum InputType {
8    Text,
9    Email,
10    Password,
11    Url,
12    Tel,
13    Search,
14    Number,
15}
16
17impl InputType {
18    fn html_type(&self) -> &'static str {
19        match self {
20            Self::Text => "text",
21            Self::Email => "email",
22            Self::Password => "password",
23            Self::Url => "url",
24            Self::Tel => "tel",
25            Self::Search => "search",
26            Self::Number => "number",
27        }
28    }
29}
30
31/// Input rendering properties
32#[derive(Debug, Clone)]
33pub struct Props {
34    /// HTML name attribute for form submission
35    pub name: String,
36    /// Input type (text, email, password, etc.)
37    pub input_type: InputType,
38    /// Placeholder text displayed when empty
39    pub placeholder: String,
40    /// Current value
41    pub value: String,
42    /// HTML id attribute for label linkage
43    pub id: String,
44    /// Whether the input is disabled
45    pub disabled: bool,
46    /// Whether the input is required
47    pub required: bool,
48    /// Whether the input is in an invalid state
49    pub invalid: bool,
50    /// Whether the input is readonly
51    pub readonly: bool,
52}
53
54impl Default for Props {
55    fn default() -> Self {
56        Self {
57            name: String::new(),
58            input_type: InputType::Text,
59            placeholder: String::new(),
60            value: String::new(),
61            id: String::new(),
62            disabled: false,
63            required: false,
64            invalid: false,
65            readonly: false,
66        }
67    }
68}
69
70/// Render a single input with the given properties
71pub fn render(props: Props) -> Markup {
72    use maud::PreEscaped;
73
74    let input_type = props.input_type.html_type();
75    let mut html_str = format!(
76        r#"<input class="mui-input" type="{}" name="{}" id="{}" placeholder="{}" value="{}""#,
77        input_type,
78        &props.name,
79        &props.id,
80        &props.placeholder,
81        &props.value
82    );
83
84    if props.required {
85        html_str.push_str(" required");
86    }
87    if props.disabled {
88        html_str.push_str(" disabled");
89    }
90    if props.readonly {
91        html_str.push_str(" readonly");
92    }
93    if props.invalid {
94        html_str.push_str(r#" aria-invalid="true""#);
95    }
96
97    html_str.push_str(" />");
98
99    PreEscaped(html_str).into()
100}
101
102/// Showcase all input types and states
103pub fn showcase() -> Markup {
104    html! {
105        div.mui-showcase__grid {
106            // Realistic form section
107            section {
108                h2 { "Profile" }
109                p.mui-showcase__caption { "A typical sign-up form using text, email, and password inputs." }
110                div style="display:flex;flex-direction:column;gap:0.75rem;max-width:24rem;" {
111                    label style="display:flex;flex-direction:column;gap:0.25rem;font-size:0.875rem;font-weight:500;" {
112                        "Full Name"
113                        (render(Props {
114                            name: "fullname".into(),
115                            id: "demo-fullname".into(),
116                            input_type: InputType::Text,
117                            placeholder: "Jane Smith".into(),
118                            required: true,
119                            ..Default::default()
120                        }))
121                    }
122                    label style="display:flex;flex-direction:column;gap:0.25rem;font-size:0.875rem;font-weight:500;" {
123                        "Email Address"
124                        (render(Props {
125                            name: "email".into(),
126                            id: "demo-email".into(),
127                            input_type: InputType::Email,
128                            placeholder: "jane@example.com".into(),
129                            required: true,
130                            ..Default::default()
131                        }))
132                    }
133                    label style="display:flex;flex-direction:column;gap:0.25rem;font-size:0.875rem;font-weight:500;" {
134                        "Password"
135                        (render(Props {
136                            name: "password".into(),
137                            id: "demo-password".into(),
138                            input_type: InputType::Password,
139                            placeholder: "At least 8 characters".into(),
140                            required: true,
141                            ..Default::default()
142                        }))
143                    }
144                }
145            }
146
147            // All input types reference
148            section {
149                h2 { "Input Types" }
150                p.mui-showcase__caption { "Each HTML input type rendered with a contextual placeholder." }
151                div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:0.75rem;" {
152                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
153                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Text" }
154                        (render(Props {
155                            name: "type-text".into(),
156                            input_type: InputType::Text,
157                            placeholder: "Your name".into(),
158                            ..Default::default()
159                        }))
160                    }
161                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
162                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Email" }
163                        (render(Props {
164                            name: "type-email".into(),
165                            input_type: InputType::Email,
166                            placeholder: "you@example.com".into(),
167                            ..Default::default()
168                        }))
169                    }
170                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
171                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Password" }
172                        (render(Props {
173                            name: "type-password".into(),
174                            input_type: InputType::Password,
175                            placeholder: "Secret".into(),
176                            ..Default::default()
177                        }))
178                    }
179                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
180                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "URL" }
181                        (render(Props {
182                            name: "type-url".into(),
183                            input_type: InputType::Url,
184                            placeholder: "https://example.com".into(),
185                            ..Default::default()
186                        }))
187                    }
188                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
189                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Phone" }
190                        (render(Props {
191                            name: "type-tel".into(),
192                            input_type: InputType::Tel,
193                            placeholder: "+1 (555) 000-0100".into(),
194                            ..Default::default()
195                        }))
196                    }
197                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
198                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Search" }
199                        (render(Props {
200                            name: "type-search".into(),
201                            input_type: InputType::Search,
202                            placeholder: "Search articles...".into(),
203                            ..Default::default()
204                        }))
205                    }
206                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
207                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Number" }
208                        (render(Props {
209                            name: "type-number".into(),
210                            input_type: InputType::Number,
211                            placeholder: "42".into(),
212                            ..Default::default()
213                        }))
214                    }
215                }
216            }
217
218            // States
219            section {
220                h2 { "States" }
221                p.mui-showcase__caption { "Default, populated, invalid, disabled, and read-only." }
222                div style="display:flex;flex-direction:column;gap:0.75rem;max-width:24rem;" {
223                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
224                        span style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Default" }
225                        (render(Props {
226                            name: "state-default".into(),
227                            placeholder: "Enter a value...".into(),
228                            ..Default::default()
229                        }))
230                    }
231                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
232                        label for="state-value" style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "With value" }
233                        (render(Props {
234                            name: "state-value".into(),
235                            id: "state-value".into(),
236                            value: "jane@example.com".into(),
237                            input_type: InputType::Email,
238                            ..Default::default()
239                        }))
240                    }
241                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
242                        label for="state-invalid" style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Invalid" }
243                        (render(Props {
244                            name: "state-invalid".into(),
245                            id: "state-invalid".into(),
246                            invalid: true,
247                            value: "not-an-email".into(),
248                            input_type: InputType::Email,
249                            ..Default::default()
250                        }))
251                        span style="font-size:0.75rem;color:var(--mui-destructive,#ef4444);" { "Please enter a valid email address." }
252                    }
253                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
254                        label for="state-disabled" style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Disabled" }
255                        (render(Props {
256                            name: "state-disabled".into(),
257                            id: "state-disabled".into(),
258                            disabled: true,
259                            value: "Cannot edit".into(),
260                            ..Default::default()
261                        }))
262                    }
263                    div style="display:flex;flex-direction:column;gap:0.25rem;" {
264                        label for="state-readonly" style="font-size:0.75rem;color:var(--mui-muted-fg,#888);" { "Read-only" }
265                        (render(Props {
266                            name: "state-readonly".into(),
267                            id: "state-readonly".into(),
268                            readonly: true,
269                            value: "user-9a3f2b".into(),
270                            ..Default::default()
271                        }))
272                    }
273                }
274            }
275        }
276    }
277}