Skip to main content

tablo_ui/components/composites/
stat.rs

1use topcoat::{
2    Result,
3    // Renamed: the tile's `icon` prop takes the name.
4    icon::{IconData, icon as icon_view},
5    view::{Attributes, StaticClass, View, class, component, view},
6};
7
8const STAT: StaticClass = class!(
9    "flex flex-col gap-3 rounded-xl border border-border bg-card p-5 text-card-foreground",
10    "shadow-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
11);
12/// A linked tile answers the pointer, as the list rows it leads to do.
13const STAT_LINK: StaticClass = class!("hover:border-foreground/20 hover:bg-muted/40");
14const STAT_LABEL: StaticClass = class!(
15    "flex items-center gap-2 text-sm font-medium text-muted-foreground",
16    "[&>svg]:size-4 [&>svg]:shrink-0",
17);
18const STAT_VALUE: StaticClass =
19    class!("text-3xl leading-none font-semibold tracking-tight text-foreground tabular-nums");
20const STAT_DETAIL: StaticClass = class!("text-sm text-muted-foreground");
21
22/// One figure for a dashboard: what it counts, the value, and an optional line under it. With an
23/// `href` the whole tile is a link, such as to the list the figure counts.
24///
25/// Lay several out in a grid of your own:
26///
27/// ```rust
28/// # use tablo_ui::{icons, stat};
29/// # use topcoat::{context::Cx, view::{BoxView, ViewExt, view}};
30/// # fn stats(cx: &Cx) -> BoxView<'_> {
31/// view! {
32///     cx =>
33///     <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
34///         stat(
35///             label: "Posts",
36///             value: "66",
37///             detail: "65 drafts",
38///             icon: icons::FILE_TEXT,
39///             href: "/admin/posts".to_string(),
40///         )
41///         stat(label: "Comments", value: "4")
42///     </div>
43/// }
44/// # .boxed()
45/// # }
46/// ```
47#[component]
48pub async fn stat(
49    /// What the value counts, e.g. `"Posts"`.
50    #[into]
51    label: String,
52    /// The figure, formatted, e.g. `"1,204"` or `"98%"`.
53    #[into]
54    value: String,
55    /// Optional muted line under the value, e.g. `"12 this week"`.
56    #[into]
57    #[default]
58    detail: String,
59    /// Optional icon before the label.
60    #[into]
61    #[default]
62    icon: Option<IconData>,
63    /// Optional URL the tile links to.
64    #[into]
65    #[default]
66    href: Option<String>,
67    /// Extra attributes for the tile.
68    #[default]
69    mut attrs: Attributes,
70) -> Result<impl View> {
71    let body = view! {
72        <p class=(STAT_LABEL)>
73            if let Some(glyph) = icon {
74                icon_view(
75                    data: glyph,
76                    attrs: topcoat::view::attributes! { aria-hidden="true" }
77                )
78            }
79            (label)
80        </p>
81        <p class=(STAT_VALUE)>(value)</p>
82        if !detail.is_empty() {
83            <p class=(STAT_DETAIL)>(detail)</p>
84        }
85    };
86    Ok(view! {
87        if let Some(href) = href {
88            <a
89                href=(href)
90                class=(class!(STAT, STAT_LINK, attrs.remove("class")))
91                (attrs)
92            >
93                (body)
94            </a>
95        } else {
96            <div class=(class!(STAT, attrs.remove("class"))) (attrs)>(body)</div>
97        }
98    })
99}