Skip to main content

orbital_core_components/card/
header.rs

1use leptos::prelude::*;
2use orbital_macros::component_doc;
3
4use super::styles::card_header_styles;
5
6/// Card header with title row and optional description and action slots.
7///
8/// # Examples
9///
10/// ## Title only
11/// Header row with a single title line.
12/// <!-- default -->
13/// <!-- preview -->
14/// ```rust
15/// use crate::{Card, CardHeader, Subtitle1};
16/// view! {
17///     <div data-testid="card-header-preview">
18///         <div data-testid="card-header-title" style="max-width: 360px;">
19///             <Card>
20///                 <CardHeader>
21///                     <Subtitle1>"Overview"</Subtitle1>
22///                 </CardHeader>
23///             </Card>
24///         </div>
25///     </div>
26/// }
27/// ```
28///
29/// ## Title and description
30/// Header with a secondary description row below the title.
31/// <!-- preview -->
32/// ```rust
33/// use crate::{Card, CardHeader, CardHeaderDescription, Subtitle1};
34/// view! {
35///     <div data-testid="card-header-description" style="max-width: 360px;">
36///         <Card>
37///             <CardHeader>
38///                 <Subtitle1>"Team workspace"</Subtitle1>
39///                 <CardHeaderDescription slot>
40///                     <span>"Shared files and tasks"</span>
41///                 </CardHeaderDescription>
42///             </CardHeader>
43///         </Card>
44///     </div>
45/// }
46/// ```
47///
48/// ## Title, description, and action
49/// Header with description and a trailing action control.
50/// <!-- preview -->
51/// ```rust
52/// use crate::{Button, ButtonAppearance, Card, CardHeader, CardHeaderAction, CardHeaderDescription, Subtitle1};
53/// view! {
54///     <div data-testid="card-header-action" style="max-width: 360px;">
55///         <Card>
56///             <CardHeader>
57///                 <Subtitle1>"Team workspace"</Subtitle1>
58///                 <CardHeaderDescription slot>
59///                     <span>"Shared files and tasks"</span>
60///                 </CardHeaderDescription>
61///                 <CardHeaderAction slot>
62///                     <Button appearance=ButtonAppearance::Transparent icon=icondata::AiMoreOutlined />
63///                 </CardHeaderAction>
64///             </CardHeader>
65///         </Card>
66///     </div>
67/// }
68/// ```
69///
70/// ## Header with media
71/// Title and description above a hero image — images belong in [`CardMedia`](crate::CardMedia), not inside the header.
72/// <!-- preview -->
73/// ```rust
74/// use crate::{Card, CardContent, CardHeader, CardHeaderDescription, CardMedia, Subtitle1};
75/// view! {
76///     <div data-testid="card-header-media" style="max-width: 360px;">
77///         <Card>
78///             <CardHeader>
79///                 <Subtitle1>"Mountain trail"</Subtitle1>
80///                 <CardHeaderDescription slot>
81///                     <span>"Photo by Picsum"</span>
82///                 </CardHeaderDescription>
83///             </CardHeader>
84///             <CardMedia
85///                 src="https://picsum.photos/seed/orbital-card/360/140"
86///                 alt="Sample card illustration"
87///                 height=140
88///             />
89///             <CardContent>"Card body copy below the hero image."</CardContent>
90///         </Card>
91///     </div>
92/// }
93/// ```
94#[component_doc(
95    category = "Surfaces",
96    preview_slug = "card-header",
97    preview_label = "Card Header",
98    preview_icon = icondata::AiLayoutOutlined,
99)]
100#[component]
101pub fn CardHeader(
102    /// Extra CSS class names merged onto the header row container.
103    #[prop(optional, into)]
104    class: MaybeProp<String>,
105    /// Optional subtitle slot rendered below the title row via [`CardHeaderDescription`].
106    #[prop(optional)]
107    card_header_description: Option<CardHeaderDescription>,
108    /// Optional trailing action slot (icon button, menu) via [`CardHeaderAction`].
109    #[prop(optional)]
110    card_header_action: Option<CardHeaderAction>,
111    /// Title row content — typically [`Subtitle1`] or a heading preset.
112    children: Children,
113) -> impl IntoView {
114    let style_sheet = card_header_styles();
115    let has_description = card_header_description.is_some();
116    let root_class = Memo::new(move |_| {
117        let extra = class.get().unwrap_or_default();
118        let base = if has_description {
119            "orbital-card-header orbital-card-header-with-description"
120        } else {
121            "orbital-card-header"
122        };
123        if extra.is_empty() {
124            base.to_string()
125        } else {
126            format!("{base} {extra}")
127        }
128    });
129
130    view! {
131        <style>{style_sheet}</style>
132        <div class=root_class>
133            <div class="orbital-card-header__header">{children()}</div>
134            {card_header_description.map(|desc| {
135                view! {
136                    <div class="orbital-card-header__description">{(desc.children)()}</div>
137                }
138            })}
139            {card_header_action.map(|action| {
140                view! {
141                    <div class="orbital-card-header__action">{(action.children)()}</div>
142                }
143            })}
144        </div>
145    }
146}
147
148#[slot]
149pub struct CardHeaderDescription {
150    children: Children,
151}
152
153#[slot]
154pub struct CardHeaderAction {
155    children: Children,
156}