1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Collapsible component — minimal expandable section with trigger and animated content.
//! Unlike accordion, this is a standalone toggle — no card wrapping, just trigger + content.
use maud::{html, Markup};
/// Collapsible rendering properties
#[derive(Clone, Debug)]
pub struct Props {
/// Label text displayed in the trigger button
pub trigger_label: String,
/// Markup content displayed when expanded
pub content: Markup,
/// Initial open state (default false)
pub open: bool,
/// Unique identifier for aria-controls and content linking
pub id: String,
}
impl Default for Props {
fn default() -> Self {
Self {
trigger_label: "Toggle".to_string(),
content: html! {},
open: false,
id: "collapsible".to_string(),
}
}
}
/// Render a single collapsible with the given properties
pub fn render(props: Props) -> Markup {
let content_id = format!("{}-content", props.id);
let aria_expanded = if props.open { "true" } else { "false" };
html! {
div class="mui-collapsible" data-mui="collapsible" {
button type="button"
class="mui-collapsible__trigger"
aria-expanded=(aria_expanded)
aria-controls=(content_id)
{
span class="mui-collapsible__label" { (props.trigger_label) }
span class="mui-collapsible__chevron" aria-hidden="true" { "\u{25BE}" }
}
div class="mui-collapsible__content"
id=(content_id)
hidden[!props.open]
{
(props.content)
}
}
}
}
/// Showcase all collapsible use cases
pub fn showcase() -> Markup {
html! {
div.mui-showcase__grid {
// Closed collapsible
(render(Props {
trigger_label: "What is maud-ui?".to_string(),
content: html! { p { "Headless accessible UI components for maud + htmx." } },
open: false,
id: "demo-col-1".to_string(),
}))
// Open collapsible
(render(Props {
trigger_label: "Is it production-ready?".to_string(),
content: html! { p { "Currently in active development. APIs may change." } },
open: true,
id: "demo-col-2".to_string(),
}))
// Nested content with list
(render(Props {
trigger_label: "Show me more".to_string(),
content: html! {
ul {
li { "Item A" }
li { "Item B" }
}
},
open: false,
id: "demo-col-3".to_string(),
}))
}
}
}