Skip to main content

tablo_ui/components/primitives/
card.rs

1// SYNC: topcoat-ui-registry@0.11.0 sha256:9c753a11ad86ad31db0c1a06584d9b1814ee1326c1e862295d508f0a7baee95d — do not hand-edit. Sync via `cargo xtask sync-topcoat-ui` (ADR-0007).
2use topcoat::{
3    Result,
4    view::{Attributes, Child, StaticClass, View, class, component, view},
5};
6
7/// Classes for a card with vertically stacked sections. Each section supplies its own
8/// horizontal padding so other content can span the full width.
9const CARD: StaticClass = class!(
10    "flex flex-col gap-5 rounded-xl border border-border bg-card py-6",
11    "text-card-foreground shadow-sm",
12    "has-[>[data-slot=table]:first-child]:overflow-hidden",
13    "has-[>[data-slot=table]:first-child]:pt-0",
14    "has-[>[data-slot=table]:last-child]:overflow-hidden",
15    "has-[>[data-slot=table]:last-child]:pb-0",
16);
17
18/// A bordered panel that groups related content.
19///
20/// Pass a header, body, footer, or table as children. A table at either end of the
21/// card sits against that edge, without padding. `attrs` adds attributes and classes
22/// to the card's `<div>`.
23///
24/// ```ignore
25/// view! {
26///     card(
27///         attrs: attributes! { class="max-w-sm" },
28///         card_header(
29///             card_title("Delete workspace")
30///             card_description("This cannot be undone.")
31///         )
32///         card_footer(
33///             attrs: attributes! { class="justify-end" },
34///             button(variant: ButtonVariant::Destructive, "Delete")
35///         )
36///     )
37/// }
38/// ```
39#[component]
40pub async fn card(
41    #[default] mut attrs: Attributes,
42    #[default] child: Child<'_>,
43) -> Result<impl View> {
44    Ok(view! { <div class=(class!(CARD, attrs.remove("class"))) (attrs)>(child)</div> })
45}
46
47/// The opening section of a [`card`], stacking a [`card_title`] and an
48/// optional [`card_description`].
49#[component]
50pub async fn card_header(
51    #[default] mut attrs: Attributes,
52    #[default] child: Child<'_>,
53) -> Result<impl View> {
54    Ok(view! {
55        <div
56            class=(class!("flex flex-col gap-1.5 px-6", attrs.remove("class")))
57            (attrs)
58        >
59            (child)
60        </div>
61    })
62}
63
64/// The heading of a [`card`], rendered as an `<h3>`.
65#[component]
66pub async fn card_title(
67    #[default] mut attrs: Attributes,
68    #[default] child: Child<'_>,
69) -> Result<impl View> {
70    Ok(view! {
71        <h3
72            class=(class!("text-base leading-none font-semibold", attrs.remove("class")))
73            (attrs)
74        >
75            (child)
76        </h3>
77    })
78}
79
80/// The supporting text under a [`card_title`].
81#[component]
82pub async fn card_description(
83    #[default] mut attrs: Attributes,
84    #[default] child: Child<'_>,
85) -> Result<impl View> {
86    Ok(view! {
87        <p
88            class=(class!("text-sm text-muted-foreground", attrs.remove("class")))
89            (attrs)
90        >
91            (child)
92        </p>
93    })
94}
95
96/// The main body of a [`card`].
97#[component]
98pub async fn card_content(
99    #[default] mut attrs: Attributes,
100    #[default] child: Child<'_>,
101) -> Result<impl View> {
102    Ok(view! { <div class=(class!("px-6", attrs.remove("class"))) (attrs)>(child)</div> })
103}
104
105/// The closing section of a [`card`], a horizontal row for actions.
106#[component]
107pub async fn card_footer(
108    #[default] mut attrs: Attributes,
109    #[default] child: Child<'_>,
110) -> Result<impl View> {
111    Ok(view! {
112        <div
113            class=(class!("flex items-center gap-2 px-6", attrs.remove("class")))
114            (attrs)
115        >
116            (child)
117        </div>
118    })
119}