Skip to main content

hypersig_ui/
card.rs

1//! Cards: a bordered surface with a header, content, and a footer.
2
3use hypersig::html::{El, div, h2, h3, h4, p};
4use hypersig::style::Style;
5use hypersig::style::pattern::{hstack, stack};
6use hypersig::style::value::{BorderStyle, FontWeight, LineHeight, px, raw, rem};
7
8use crate::look;
9use crate::token;
10
11/// The level of a heading, which follows the headings around it.
12///
13/// A screen reader lists a page by its headings, so a card title one level
14/// below the section it sits in keeps that list in order.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum Level {
18    /// `<h2>`.
19    H2,
20    /// `<h3>`.
21    H3,
22    /// `<h4>`.
23    H4,
24}
25
26impl Level {
27    pub(crate) fn heading<S>(self) -> El<S> {
28        match self {
29            Self::H2 => h2(),
30            Self::H3 => h3(),
31            Self::H4 => h4(),
32        }
33    }
34}
35
36/// A `<div>` with a border, a shadow, and [`token::card`] behind it.
37///
38/// Put a [`card_header`], a [`card_content`], and a [`card_footer`] in it.
39///
40/// ```
41/// use hypersig::Signals;
42/// use hypersig_ui::{Level, card, card_content, card_description, card_header, card_title};
43/// use serde::{Deserialize, Serialize};
44///
45/// #[derive(Signals, Serialize, Deserialize)]
46/// struct App {}
47///
48/// let plan = card::<App>()
49///     .child(
50///         card_header()
51///             .child(card_title(Level::H3).text("Pro"))
52///             .child(card_description().text("For teams of up to ten.")),
53///     )
54///     .child(card_content().text("$20 a month"));
55/// assert!(plan.render().contains("<h3"));
56/// ```
57#[must_use]
58pub fn card<S>() -> El<S> {
59    div().css(
60        stack(rem(1.5))
61            .padding_block(rem(1.5))
62            .border_width(px(1))
63            .border_style(BorderStyle::Solid)
64            .border_color(token::border().var())
65            .border_radius(raw(format!("calc({} + 4px)", token::radius().var())))
66            .background_color(token::card().var())
67            .color(token::card_foreground().var())
68            .box_shadow(raw(
69                "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
70            )),
71    )
72}
73
74/// The top of a [`card`], holding its title and description.
75#[must_use]
76pub fn card_header<S>() -> El<S> {
77    div().css(stack(rem(0.375)).padding_inline(rem(1.5)))
78}
79
80/// The title of a [`card`], as a heading of `level`.
81#[must_use]
82pub fn card_title<S>(level: Level) -> El<S> {
83    level.heading().css(
84        Style::new()
85            .font_size(rem(1))
86            .line_height(LineHeight::Number(1.0))
87            .font_weight(FontWeight::Number(600.0)),
88    )
89}
90
91/// A line under the title of a [`card`], in [`token::muted_foreground`].
92#[must_use]
93pub fn card_description<S>() -> El<S> {
94    p().css(look::small_text().color(token::muted_foreground().var()))
95}
96
97/// The body of a [`card`].
98#[must_use]
99pub fn card_content<S>() -> El<S> {
100    div().css(Style::new().padding_inline(rem(1.5)))
101}
102
103/// The bottom of a [`card`], laying its children out in a row.
104#[must_use]
105pub fn card_footer<S>() -> El<S> {
106    div().css(hstack(rem(0.5)).padding_inline(rem(1.5)))
107}