Skip to main content

fc_dioxus/components/
card.rs

1//! Composable surfaces matching card.css and its reference fixtures.
2use crate::{
3    core::{assets, attrs::component_attributes},
4    prelude::*,
5    theme::use_stylesheets,
6};
7const BEM: Bem = Bem::new("fc-card");
8const FOOTER: Bem = Bem::new("fc-card__footer");
9fc_modifier! {
10    /// Upstream card surface variants.
11    pub enum CardVariant for BEM {
12        /// Standard bordered surface.
13        Default => None,
14        /// Raised surface.
15        Elevated => "elevated",
16        /// Muted surface.
17        Subtle => "subtle",
18        /// Theme-specific feature surface.
19        Feature => "feature",
20    }
21}
22fc_modifier! { enum FooterDivider for FOOTER { None => None, Divided => "divided" } }
23/// Native meaning of a card container.
24#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
25#[non_exhaustive]
26pub enum CardTag {
27    /// A self-contained item.
28    #[default]
29    Article,
30    /// A generic surface.
31    Div,
32    /// A section, normally with a heading.
33    Section,
34    /// An item inside a list.
35    Li,
36    /// A selectable card wrapping a native input and its visible label.
37    Label,
38}
39fn stylesheet() -> Element {
40    rsx! { if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_CARD_CSS } } }
41}
42/// A card container; compose the parts to retain the native document structure.
43#[component]
44pub fn Card(
45    #[props(default)] variant: CardVariant,
46    #[props(default)] as_: CardTag,
47    #[props(default)] class: Option<String>,
48    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
49    children: Element,
50) -> Element {
51    let attributes = component_attributes(
52        attributes,
53        ClassList::new(BEM.block())
54            .with_modifier(BEM, variant)
55            .extend(class.as_deref()),
56        &[],
57    );
58    rsx! { {stylesheet()} match as_ {
59        CardTag::Article => rsx! { article { ..attributes, {children} } },
60        CardTag::Div => rsx! { div { ..attributes, {children} } },
61        CardTag::Section => rsx! { section { ..attributes, {children} } },
62        CardTag::Li => rsx! { li { ..attributes, {children} } },
63        CardTag::Label => rsx! { label { ..attributes, {children} } },
64    } }
65}
66/// A row for the heading and supporting controls.
67#[component]
68pub fn CardHeader(
69    #[props(default)] class: Option<String>,
70    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
71    children: Element,
72) -> Element {
73    let attributes = component_attributes(
74        attributes,
75        ClassList::new(&BEM.element("header")).extend(class.as_deref()),
76        &[],
77    );
78    rsx! { {stylesheet()} div { ..attributes, {children} } }
79}
80/// A group for a card title and subtitle.
81#[component]
82pub fn CardHeading(
83    #[props(default)] class: Option<String>,
84    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
85    children: Element,
86) -> Element {
87    let attributes = component_attributes(
88        attributes,
89        ClassList::new(&BEM.element("heading")).extend(class.as_deref()),
90        &[],
91    );
92    rsx! { {stylesheet()} div { ..attributes, {children} } }
93}
94/// The content area of a card.
95#[component]
96pub fn CardBody(
97    #[props(default)] class: Option<String>,
98    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
99    children: Element,
100) -> Element {
101    let attributes = component_attributes(
102        attributes,
103        ClassList::new(&BEM.element("body")).extend(class.as_deref()),
104        &[],
105    );
106    rsx! { {stylesheet()} div { ..attributes, {children} } }
107}
108/// Container-responsive layout for media beside content.
109#[component]
110pub fn CardLayout(
111    #[props(default)] class: Option<String>,
112    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
113    children: Element,
114) -> Element {
115    let attributes = component_attributes(
116        attributes,
117        ClassList::new(&BEM.element("layout")).extend(class.as_deref()),
118        &[],
119    );
120    rsx! { {stylesheet()} div { ..attributes, {children} } }
121}
122/// A semantic heading, or a span when used inside a selectable label card.
123#[component]
124pub fn CardTitle(
125    #[props(default = Some(HeadingLevel::H3))] level: Option<HeadingLevel>,
126    #[props(default)] class: Option<String>,
127    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
128    children: Element,
129) -> Element {
130    let attributes = component_attributes(
131        attributes,
132        ClassList::new(&BEM.element("title")).extend(class.as_deref()),
133        &[],
134    );
135    rsx! { {stylesheet()} match level {
136        Some(HeadingLevel::H1) => rsx! { h1 { ..attributes, {children} } },
137        Some(HeadingLevel::H2) => rsx! { h2 { ..attributes, {children} } },
138        Some(HeadingLevel::H3) => rsx! { h3 { ..attributes, {children} } },
139        Some(HeadingLevel::H4) => rsx! { h4 { ..attributes, {children} } },
140        Some(HeadingLevel::H5) => rsx! { h5 { ..attributes, {children} } },
141        Some(HeadingLevel::H6) => rsx! { h6 { ..attributes, {children} } },
142        None => rsx! { span { ..attributes, {children} } },
143    } }
144}
145/// Secondary description, with an inline mode for selectable label cards.
146#[component]
147pub fn CardSubtitle(
148    #[props(default)] inline: bool,
149    #[props(default)] class: Option<String>,
150    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
151    children: Element,
152) -> Element {
153    let attributes = component_attributes(
154        attributes,
155        ClassList::new(&BEM.element("subtitle")).extend(class.as_deref()),
156        &[],
157    );
158    rsx! { {stylesheet()} if inline { span { ..attributes, {children} } } else { p { ..attributes, {children} } } }
159}
160/// A card image with required alternative text, or an upstream media placeholder.
161#[component]
162pub fn CardMedia(
163    alt: Alt,
164    #[props(default)] src: Option<String>,
165    #[props(default)] class: Option<String>,
166    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
167) -> Element {
168    let attributes = component_attributes(
169        attributes,
170        ClassList::new(&BEM.element("media")).extend(class.as_deref()),
171        &["alt", "src", "aria-hidden", "aria-label", "role"],
172    );
173    let label = match &alt {
174        Alt::Text(name) => name.to_string(),
175        Alt::Decorative => String::new(),
176    };
177    rsx! { {stylesheet()}
178        if let Some(src) = src { img { src, alt: label, ..attributes } }
179        else if alt == Alt::Decorative { div { aria_hidden: "true", ..attributes } }
180        else { div { role: "img", aria_label: label, ..attributes } }
181    }
182}
183/// Card actions, optionally separated by an upstream rule.
184#[component]
185pub fn CardFooter(
186    #[props(default)] divided: bool,
187    #[props(default)] class: Option<String>,
188    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
189    children: Element,
190) -> Element {
191    let classes = ClassList::new(FOOTER.block())
192        .with_modifier(
193            FOOTER,
194            if divided {
195                FooterDivider::Divided
196            } else {
197                FooterDivider::None
198            },
199        )
200        .extend(class.as_deref());
201    let attributes = component_attributes(attributes, classes, &[]);
202    rsx! { {stylesheet()} div { ..attributes, {children} } }
203}
204/// A stretched native title link; footer controls retain their own hit areas.
205#[component]
206pub fn CardLink(
207    href: String,
208    #[props(default)] class: Option<String>,
209    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
210    children: Element,
211) -> Element {
212    let attributes = component_attributes(
213        attributes,
214        ClassList::new(&BEM.element("link")).extend(class.as_deref()),
215        &["href", "role"],
216    );
217    rsx! { {stylesheet()} a { href, ..attributes, {children} } }
218}