1use maud::{html, Markup, PreEscaped};
4
5fn icon_plus() -> Markup {
7 PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>"#.to_string())
8}
9
10fn icon_github() -> Markup {
12 PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.4-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>"#.to_string())
13}
14
15fn icon_spinner() -> Markup {
17 PreEscaped(r#"<svg xmlns="http://www.w3.org/2000/svg" class="mui-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>"#.to_string())
18}
19
20#[derive(Clone, Debug)]
21pub struct Props {
22 pub label: String,
23 pub variant: Variant,
24 pub size: Size,
25 pub disabled: bool,
26 pub button_type: &'static str,
27 pub leading_icon: Option<Markup>,
31 pub aria_label: Option<String>,
34}
35
36impl Default for Props {
37 fn default() -> Self {
38 Self {
39 label: "Button".to_string(),
40 variant: Variant::Default,
41 size: Size::Md,
42 disabled: false,
43 button_type: "button",
44 leading_icon: None,
45 aria_label: None,
46 }
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Variant {
52 Default,
53 Primary,
54 Secondary,
55 Outline,
56 Ghost,
57 Danger,
58 Link,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum Size {
63 Sm,
64 Md,
65 Lg,
66 Icon,
67}
68
69impl Variant {
70 fn class_name(self) -> &'static str {
71 match self {
72 Variant::Default => "mui-btn--default",
73 Variant::Primary => "mui-btn--primary",
74 Variant::Secondary => "mui-btn--secondary",
75 Variant::Outline => "mui-btn--outline",
76 Variant::Ghost => "mui-btn--ghost",
77 Variant::Danger => "mui-btn--danger",
78 Variant::Link => "mui-btn--link",
79 }
80 }
81}
82
83impl Size {
84 fn class_name(self) -> &'static str {
85 match self {
86 Size::Sm => "mui-btn--sm",
87 Size::Md => "mui-btn--md",
88 Size::Lg => "mui-btn--lg",
89 Size::Icon => "mui-btn--icon",
90 }
91 }
92}
93
94pub fn render(props: Props) -> Markup {
95 let disabled_attr = if props.disabled {
96 "true"
97 } else {
98 "false"
99 };
100
101 let class = format!(
102 "mui-btn {} {}",
103 props.variant.class_name(),
104 props.size.class_name()
105 );
106
107 html! {
108 @if let Some(label) = &props.aria_label {
109 button class=(class) type=(props.button_type) aria-disabled=(disabled_attr) aria-label=(label) {
110 @if let Some(icon) = &props.leading_icon {
111 span.mui-btn__icon aria-hidden="true" { (icon) }
112 }
113 (props.label)
114 }
115 } @else {
116 button class=(class) type=(props.button_type) aria-disabled=(disabled_attr) {
117 @if let Some(icon) = &props.leading_icon {
118 span.mui-btn__icon aria-hidden="true" { (icon) }
119 }
120 (props.label)
121 }
122 }
123 }
124}
125
126pub fn showcase() -> Markup {
127 html! {
128 div.mui-showcase__grid {
129 section {
130 h2 { "Form actions" }
131 p.mui-showcase__caption { "Primary/secondary pairing for settings, onboarding, checkout." }
132 div.mui-showcase__row {
133 (render(Props {
134 label: "Save changes".to_string(),
135 variant: Variant::Primary,
136 size: Size::Md,
137 disabled: false,
138 button_type: "submit",
139 leading_icon: None,
140 aria_label: None,
141 }))
142 (render(Props {
143 label: "Continue to billing".to_string(),
144 variant: Variant::Primary,
145 size: Size::Md,
146 disabled: false,
147 button_type: "button",
148 leading_icon: None,
149 aria_label: None,
150 }))
151 (render(Props {
152 label: "Cancel".to_string(),
153 variant: Variant::Outline,
154 size: Size::Md,
155 disabled: false,
156 button_type: "button",
157 leading_icon: None,
158 aria_label: None,
159 }))
160 }
161 }
162 section {
163 h2 { "Destructive" }
164 p.mui-showcase__caption { "Irreversible actions — only after a confirm dialog." }
165 div.mui-showcase__row {
166 (render(Props {
167 label: "Delete account".to_string(),
168 variant: Variant::Danger,
169 size: Size::Md,
170 disabled: false,
171 button_type: "button",
172 leading_icon: None,
173 aria_label: None,
174 }))
175 (render(Props {
176 label: "Revoke API key".to_string(),
177 variant: Variant::Danger,
178 size: Size::Sm,
179 disabled: false,
180 button_type: "button",
181 leading_icon: None,
182 aria_label: None,
183 }))
184 }
185 }
186 section {
187 h2 { "Loading state" }
188 p.mui-showcase__caption { "Disabled + spinner icon while awaiting a response." }
189 div.mui-showcase__row {
190 (render(Props {
191 label: "Signing in\u{2026}".to_string(),
192 variant: Variant::Primary,
193 size: Size::Md,
194 disabled: true,
195 button_type: "button",
196 leading_icon: Some(icon_spinner()),
197 aria_label: None,
198 }))
199 (render(Props {
200 label: "Deploying\u{2026}".to_string(),
201 variant: Variant::Secondary,
202 size: Size::Md,
203 disabled: true,
204 button_type: "button",
205 leading_icon: Some(icon_spinner()),
206 aria_label: None,
207 }))
208 }
209 }
210 section {
211 h2 { "Icon + text" }
212 p.mui-showcase__caption { "Leading glyph for recognition at a glance." }
213 div.mui-showcase__row {
214 (render(Props {
215 label: "Invite teammate".to_string(),
216 variant: Variant::Primary,
217 size: Size::Md,
218 disabled: false,
219 button_type: "button",
220 leading_icon: Some(icon_plus()),
221 aria_label: None,
222 }))
223 (render(Props {
224 label: "GitHub".to_string(),
225 variant: Variant::Outline,
226 size: Size::Md,
227 disabled: false,
228 button_type: "button",
229 leading_icon: Some(icon_github()),
230 aria_label: None,
231 }))
232 (render(Props {
233 label: String::new(),
234 variant: Variant::Outline,
235 size: Size::Icon,
236 disabled: false,
237 button_type: "button",
238 leading_icon: Some(icon_plus()),
239 aria_label: Some("Add item".to_string()),
240 }))
241 }
242 }
243 }
244 }
245}