maud_ui/primitives/
textarea.rs1use maud::{html, Markup, PreEscaped};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Resize {
8 None,
9 Vertical,
10 Horizontal,
11 Both,
12}
13
14impl Resize {
15 fn class(&self) -> &'static str {
16 match self {
17 Self::None => "mui-textarea--resize-none",
18 Self::Vertical => "mui-textarea--resize-vertical",
19 Self::Horizontal => "mui-textarea--resize-horizontal",
20 Self::Both => "mui-textarea--resize-both",
21 }
22 }
23}
24
25#[derive(Debug, Clone)]
27pub struct Props {
28 pub name: String,
30 pub placeholder: String,
32 pub value: String,
34 pub rows: u32,
36 pub id: String,
38 pub disabled: bool,
40 pub required: bool,
42 pub invalid: bool,
44 pub readonly: bool,
46 pub resize: Resize,
48}
49
50impl Default for Props {
51 fn default() -> Self {
52 Self {
53 name: String::new(),
54 placeholder: String::new(),
55 value: String::new(),
56 rows: 4,
57 id: String::new(),
58 disabled: false,
59 required: false,
60 invalid: false,
61 readonly: false,
62 resize: Resize::Vertical,
63 }
64 }
65}
66
67pub fn render(props: Props) -> Markup {
69 let mut attrs = String::new();
70
71 if props.required {
72 attrs.push_str(" required");
73 }
74 if props.disabled {
75 attrs.push_str(" disabled");
76 }
77 if props.readonly {
78 attrs.push_str(" readonly");
79 }
80 if props.invalid {
81 attrs.push_str(r#" aria-invalid="true""#);
82 }
83
84 let class = format!("mui-textarea {}", props.resize.class());
85 let html_string = format!(
86 r#"<textarea class="{}" name="{}" id="{}" placeholder="{}" rows="{}"{}>{}</textarea>"#,
87 escape_html(&class),
88 escape_html(&props.name),
89 escape_html(&props.id),
90 escape_html(&props.placeholder),
91 props.rows,
92 attrs,
93 escape_html(&props.value)
94 );
95
96 PreEscaped(html_string)
97}
98
99fn escape_html(s: &str) -> String {
100 s.replace('&', "&")
101 .replace('<', "<")
102 .replace('>', ">")
103 .replace('"', """)
104 .replace('\'', "'")
105}
106
107pub fn showcase() -> Markup {
109 html! {
110 div.mui-showcase__grid {
111 section {
113 h2 { "Share your feedback" }
114 p.mui-showcase__caption { "Tell us what worked and what didn't. We read every response." }
115 div style="display:flex;flex-direction:column;gap:0.5rem;max-width:28rem;" {
116 label for="feedback-message" style="font-size:0.875rem;font-weight:500;" { "Your feedback" }
117 (render(Props {
118 name: "feedback".into(),
119 id: "feedback-message".into(),
120 placeholder: "What's on your mind?".into(),
121 rows: 5,
122 ..Default::default()
123 }))
124 div style="display:flex;justify-content:space-between;font-size:0.75rem;color:var(--mui-text-muted);" {
125 span { "Min 20 characters" }
126 span { "0 / 500" }
127 }
128 }
129 }
130
131 section {
133 h2 { "Profile" }
134 p.mui-showcase__caption { "Shown on your public profile and attribution lines." }
135 div style="display:flex;flex-direction:column;gap:0.5rem;max-width:28rem;" {
136 label for="profile-bio" style="font-size:0.875rem;font-weight:500;" { "Bio" }
137 (render(Props {
138 name: "bio".into(),
139 id: "profile-bio".into(),
140 placeholder: "Write a short intro about yourself".into(),
141 rows: 4,
142 ..Default::default()
143 }))
144 p style="font-size:0.75rem;color:var(--mui-text-muted);margin:0;" {
145 "Tip: mention where you work, what you build, and where folks can find you."
146 }
147 }
148 }
149
150 section {
152 h2 { "Admin notes" }
153 p.mui-showcase__caption { "Read-only. Changes require a support ticket." }
154 div style="display:flex;flex-direction:column;gap:0.5rem;max-width:28rem;" {
155 label for="admin-notes" style="font-size:0.875rem;font-weight:500;color:var(--mui-text-muted);" {
156 "Admin notes \u{2014} read only"
157 }
158 (render(Props {
159 name: "admin-notes".into(),
160 id: "admin-notes".into(),
161 value: "Account flagged for manual review on 2026-04-10 by Sofia M. (billing). Reason: chargeback window open until 2026-05-10. Do not issue refunds without approval from #finance-ops.".into(),
162 readonly: true,
163 rows: 4,
164 ..Default::default()
165 }))
166 p style="font-size:0.75rem;color:var(--mui-text-muted);margin:0;" {
167 "Last updated by Sofia M. \u{00B7} 6 days ago"
168 }
169 }
170 }
171 }
172 }
173}