Skip to main content

maud_ui/primitives/
table.rs

1//! Table component.
2use maud::{html, Markup};
3
4#[derive(Clone, Debug)]
5pub struct CellMarkup {
6    pub content: Markup,
7    pub align_right: bool,
8}
9
10impl CellMarkup {
11    pub fn text(s: &str) -> Self {
12        Self {
13            content: html! { (s) },
14            align_right: false,
15        }
16    }
17    pub fn right(s: &str) -> Self {
18        Self {
19            content: html! { (s) },
20            align_right: true,
21        }
22    }
23    pub fn markup(m: Markup, align_right: bool) -> Self {
24        Self {
25            content: m,
26            align_right,
27        }
28    }
29}
30
31#[derive(Clone, Debug)]
32pub struct Props {
33    pub headers: Vec<String>,
34    pub rows: Vec<Vec<String>>,
35    pub rich_rows: Vec<Vec<CellMarkup>>,
36    pub footer_row: Vec<CellMarkup>,
37    pub striped: bool,
38    pub hoverable: bool,
39    pub compact: bool,
40    pub caption: Option<String>,
41    /// Column indices that should be right-aligned in headers
42    pub right_align_cols: Vec<usize>,
43}
44
45impl Default for Props {
46    fn default() -> Self {
47        Self {
48            headers: vec![],
49            rows: vec![],
50            rich_rows: vec![],
51            footer_row: vec![],
52            striped: false,
53            hoverable: false,
54            compact: false,
55            caption: None,
56            right_align_cols: vec![],
57        }
58    }
59}
60
61pub fn render(props: Props) -> Markup {
62    let mut modifiers = String::new();
63
64    if props.striped {
65        modifiers.push_str(" mui-table--striped");
66    }
67    if props.hoverable {
68        modifiers.push_str(" mui-table--hoverable");
69    }
70    if props.compact {
71        modifiers.push_str(" mui-table--compact");
72    }
73
74    let class = format!("mui-table{}", modifiers);
75    let has_rich = !props.rich_rows.is_empty();
76    let has_footer = !props.footer_row.is_empty();
77
78    html! {
79        div.mui-table-wrapper {
80            table class=(class) {
81                @if let Some(caption_text) = props.caption {
82                    caption.mui-table__caption { (caption_text) }
83                }
84                thead {
85                    tr {
86                        @for (i, header) in props.headers.iter().enumerate() {
87                            @if props.right_align_cols.contains(&i) {
88                                th.mui-table__th style="text-align:right;" { (header) }
89                            } @else {
90                                th.mui-table__th { (header) }
91                            }
92                        }
93                    }
94                }
95                tbody {
96                    @if has_rich {
97                        @for row in &props.rich_rows {
98                            tr.mui-table__row {
99                                @for cell in row {
100                                    @if cell.align_right {
101                                        td.mui-table__td style="text-align:right;" { (cell.content) }
102                                    } @else {
103                                        td.mui-table__td { (cell.content) }
104                                    }
105                                }
106                            }
107                        }
108                    } @else {
109                        @for row in &props.rows {
110                            tr.mui-table__row {
111                                @for cell in row {
112                                    td.mui-table__td { (cell) }
113                                }
114                            }
115                        }
116                    }
117                }
118                @if has_footer {
119                    tfoot {
120                        tr.mui-table__row {
121                            @for cell in &props.footer_row {
122                                @if cell.align_right {
123                                    td.mui-table__td style="text-align:right;font-weight:600;" { (cell.content) }
124                                } @else {
125                                    td.mui-table__td style="font-weight:600;" { (cell.content) }
126                                }
127                            }
128                        }
129                    }
130                }
131            }
132        }
133    }
134}
135
136pub fn showcase() -> Markup {
137    use crate::primitives::badge;
138
139    let headers = vec![
140        "Invoice".to_string(),
141        "Status".to_string(),
142        "Method".to_string(),
143        "Amount".to_string(),
144    ];
145
146    // Status badge helper
147    let status_badge = |label: &str| -> Markup {
148        let variant = match label {
149            "Paid" => badge::Variant::Success,
150            "Pending" => badge::Variant::Warning,
151            "Unpaid" => badge::Variant::Danger,
152            _ => badge::Variant::Default,
153        };
154        badge::render(badge::Props {
155            label: label.to_string(),
156            variant,
157        })
158    };
159
160    let rich_rows = vec![
161        vec![
162            CellMarkup::text("INV001"),
163            CellMarkup::markup(status_badge("Paid"), false),
164            CellMarkup::text("Credit Card"),
165            CellMarkup::right("$250.00"),
166        ],
167        vec![
168            CellMarkup::text("INV002"),
169            CellMarkup::markup(status_badge("Pending"), false),
170            CellMarkup::text("PayPal"),
171            CellMarkup::right("$150.00"),
172        ],
173        vec![
174            CellMarkup::text("INV003"),
175            CellMarkup::markup(status_badge("Unpaid"), false),
176            CellMarkup::text("Bank Transfer"),
177            CellMarkup::right("$350.00"),
178        ],
179        vec![
180            CellMarkup::text("INV004"),
181            CellMarkup::markup(status_badge("Paid"), false),
182            CellMarkup::text("Credit Card"),
183            CellMarkup::right("$450.00"),
184        ],
185        vec![
186            CellMarkup::text("INV005"),
187            CellMarkup::markup(status_badge("Paid"), false),
188            CellMarkup::text("PayPal"),
189            CellMarkup::right("$550.00"),
190        ],
191    ];
192
193    let footer_row = vec![
194        CellMarkup::text("Total"),
195        CellMarkup::text(""),
196        CellMarkup::text(""),
197        CellMarkup::right("$1,750.00"),
198    ];
199
200    // Plain rows for the simpler variants
201    let plain_rows = vec![
202        vec!["INV001".into(), "Paid".into(), "Credit Card".into(), "$250.00".into()],
203        vec!["INV002".into(), "Pending".into(), "PayPal".into(), "$150.00".into()],
204        vec!["INV003".into(), "Unpaid".into(), "Bank Transfer".into(), "$350.00".into()],
205        vec!["INV004".into(), "Paid".into(), "Credit Card".into(), "$450.00".into()],
206        vec!["INV005".into(), "Paid".into(), "PayPal".into(), "$550.00".into()],
207    ];
208
209    html! {
210        div.mui-showcase__grid {
211            div {
212                p.mui-showcase__caption { "With badges, right-aligned amounts, and footer total" }
213                (render(Props {
214                    headers: headers.clone(),
215                    rich_rows,
216                    footer_row,
217                    hoverable: true,
218                    right_align_cols: vec![3],
219                    caption: Some("A list of your recent invoices.".to_string()),
220                    ..Default::default()
221                }))
222            }
223            div {
224                p.mui-showcase__caption { "Striped + hoverable" }
225                (render(Props {
226                    headers: headers.clone(),
227                    rows: plain_rows.clone(),
228                    striped: true,
229                    hoverable: true,
230                    right_align_cols: vec![3],
231                    ..Default::default()
232                }))
233            }
234            div {
235                p.mui-showcase__caption { "Compact" }
236                (render(Props {
237                    headers,
238                    rows: plain_rows,
239                    compact: true,
240                    right_align_cols: vec![3],
241                    ..Default::default()
242                }))
243            }
244        }
245    }
246}