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}