Skip to main content

maud_ui/primitives/
tabs.rs

1//! Tabs component — maud-ui Wave 2
2
3use maud::{html, Markup};
4
5#[derive(Clone, Debug)]
6pub struct Tab {
7    pub id: String,
8    pub label: String,
9    pub content: Markup,
10}
11
12#[derive(Clone, Debug)]
13pub struct Props {
14    pub tabs: Vec<Tab>,
15    pub default_active: usize,
16    pub aria_label: String,
17}
18
19impl Default for Props {
20    fn default() -> Self {
21        Self {
22            tabs: vec![],
23            default_active: 0,
24            aria_label: "Tabs".to_string(),
25        }
26    }
27}
28
29pub fn render(props: Props) -> Markup {
30    html! {
31        div class="mui-tabs" data-mui="tabs" {
32            div class="mui-tabs__list" role="tablist" aria-label=(props.aria_label) {
33                @for (i, tab) in props.tabs.iter().enumerate() {
34                    @let is_active = i == props.default_active;
35                    @let tabindex = if is_active { "0" } else { "-1" };
36                    button type="button"
37                        class="mui-tabs__trigger"
38                        role="tab"
39                        id=(format!("{}-trigger", tab.id))
40                        aria-controls=(format!("{}-panel", tab.id))
41                        aria-selected=(if is_active { "true" } else { "false" })
42                        tabindex=(tabindex) {
43                        (tab.label)
44                    }
45                }
46            }
47            @for (i, tab) in props.tabs.iter().enumerate() {
48                @let is_active = i == props.default_active;
49                div class="mui-tabs__panel"
50                    role="tabpanel"
51                    id=(format!("{}-panel", tab.id))
52                    aria-labelledby=(format!("{}-trigger", tab.id))
53                    tabindex="0"
54                    hidden[!is_active]
55                {
56                    (tab.content)
57                }
58            }
59        }
60    }
61}
62
63pub fn showcase() -> Markup {
64    use crate::primitives::{input, label, button};
65
66    let tabs = vec![
67        Tab {
68            id: "account".to_string(),
69            label: "Account".to_string(),
70            content: html! {
71                div style="padding:1rem 0;" {
72                    p style="font-size:0.875rem;color:var(--mui-text-muted);margin:0 0 1rem;" {
73                        "Make changes to your account here. Click save when you\u{2019}re done."
74                    }
75                    div style="display:flex;flex-direction:column;gap:0.75rem;max-width:24rem;" {
76                        div class="mui-field" {
77                            (label::render(label::Props {
78                                text: "Name".into(),
79                                html_for: Some("tab-account-name".into()),
80                                ..Default::default()
81                            }))
82                            (input::render(input::Props {
83                                name: "name".into(),
84                                id: "tab-account-name".into(),
85                                input_type: input::InputType::Text,
86                                value: "Pedro Duarte".into(),
87                                ..Default::default()
88                            }))
89                        }
90                        div class="mui-field" {
91                            (label::render(label::Props {
92                                text: "Email".into(),
93                                html_for: Some("tab-account-email".into()),
94                                ..Default::default()
95                            }))
96                            (input::render(input::Props {
97                                name: "email".into(),
98                                id: "tab-account-email".into(),
99                                input_type: input::InputType::Email,
100                                value: "pedro@example.com".into(),
101                                ..Default::default()
102                            }))
103                        }
104                        div style="display:flex;justify-content:flex-end;margin-top:0.5rem;" {
105                            (button::render(button::Props {
106                                label: "Save changes".into(),
107                                variant: button::Variant::Primary,
108                                size: button::Size::Md,
109                                ..Default::default()
110                            }))
111                        }
112                    }
113                }
114            },
115        },
116        Tab {
117            id: "password".to_string(),
118            label: "Password".to_string(),
119            content: html! {
120                div style="padding:1rem 0;" {
121                    p style="font-size:0.875rem;color:var(--mui-text-muted);margin:0 0 1rem;" {
122                        "Change your password here. After saving, you\u{2019}ll be logged out."
123                    }
124                    div style="display:flex;flex-direction:column;gap:0.75rem;max-width:24rem;" {
125                        div class="mui-field" {
126                            (label::render(label::Props {
127                                text: "Current password".into(),
128                                html_for: Some("tab-pw-current".into()),
129                                ..Default::default()
130                            }))
131                            (input::render(input::Props {
132                                name: "current_password".into(),
133                                id: "tab-pw-current".into(),
134                                input_type: input::InputType::Password,
135                                placeholder: "Enter current password".into(),
136                                ..Default::default()
137                            }))
138                        }
139                        div class="mui-field" {
140                            (label::render(label::Props {
141                                text: "New password".into(),
142                                html_for: Some("tab-pw-new".into()),
143                                ..Default::default()
144                            }))
145                            (input::render(input::Props {
146                                name: "new_password".into(),
147                                id: "tab-pw-new".into(),
148                                input_type: input::InputType::Password,
149                                placeholder: "Enter new password".into(),
150                                ..Default::default()
151                            }))
152                        }
153                        div style="display:flex;justify-content:flex-end;margin-top:0.5rem;" {
154                            (button::render(button::Props {
155                                label: "Change password".into(),
156                                variant: button::Variant::Primary,
157                                size: button::Size::Md,
158                                ..Default::default()
159                            }))
160                        }
161                    }
162                }
163            },
164        },
165        Tab {
166            id: "team".to_string(),
167            label: "Team".to_string(),
168            content: html! {
169                div style="padding:1rem 0;" {
170                    p style="font-size:0.875rem;color:var(--mui-text-muted);margin:0 0 1rem;" {
171                        "Invite your team members to collaborate."
172                    }
173                    div style="display:flex;flex-direction:column;gap:0.75rem;max-width:28rem;" {
174                        @for (name, email, role) in [
175                            ("Sofia Davis", "sofia@example.com", "Owner"),
176                            ("Jackson Lee", "jackson@example.com", "Member"),
177                            ("Isabella Nguyen", "isabella@example.com", "Member"),
178                        ] {
179                            div style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0;border-bottom:1px solid var(--mui-border,#e5e7eb);" {
180                                div {
181                                    p style="font-size:0.875rem;font-weight:500;margin:0;" { (name) }
182                                    p style="font-size:0.8125rem;color:var(--mui-text-muted);margin:0.125rem 0 0;" { (email) }
183                                }
184                                span style="font-size:0.75rem;color:var(--mui-text-muted);padding:0.25rem 0.5rem;border:1px solid var(--mui-border,#e5e7eb);border-radius:0.375rem;" {
185                                    (role)
186                                }
187                            }
188                        }
189                    }
190                }
191            },
192        },
193    ];
194
195    let props = Props {
196        tabs,
197        default_active: 0,
198        aria_label: "Account settings".to_string(),
199    };
200
201    render(props)
202}