Skip to main content

tablo_ui/components/composites/
page.rs

1use topcoat::{
2    Result,
3    view::{Attributes, Child, StaticClass, View, class, component, view},
4};
5
6const PAGE: StaticClass = class!("mx-auto flex w-full max-w-7xl flex-col gap-6 p-4 md:p-8");
7/// The title and the actions share the first line; the description always
8/// takes a line of its own below them, whatever order the children come in.
9const PAGE_HEADER: StaticClass = class!(
10    "flex flex-wrap items-center gap-x-4 gap-y-1.5 \
11     [&>[data-page-description]]:order-last [&>[data-page-description]]:basis-full"
12);
13// The basis keeps a title readable: past it, the actions wrap below instead of squeezing it.
14const PAGE_TITLE: StaticClass = class!(
15    "min-w-0 flex-1 basis-64 truncate text-2xl font-semibold tracking-tight text-foreground"
16);
17const PAGE_DESCRIPTION: StaticClass = class!("text-sm text-muted-foreground");
18// Capped at the header's width, so on a narrow screen the buttons wrap among themselves rather
19// than run off the page.
20const PAGE_ACTIONS: StaticClass = class!("flex max-w-full shrink-0 flex-wrap items-center gap-2");
21const PAGE_CONTENT: StaticClass = class!("flex flex-col gap-6");
22
23/// Standard container for a panel page, owning its max width, padding, and vertical rhythm.
24///
25/// ```rust
26/// # use tablo_ui::{page, page_actions, page_content, page_description, page_header, page_title};
27/// # use topcoat::{context::Cx, view::{BoxView, ViewExt, view}};
28/// # fn users<'a>(cx: &'a Cx, create_button: BoxView<'a>, table_view: BoxView<'a>) -> BoxView<'a> {
29/// # view! { cx =>
30/// page(
31///     page_header(
32///         page_title("Users")
33///         page_description("Manage your users.")
34///         page_actions((create_button))
35///     )
36///     page_content((table_view))
37/// )
38/// # }
39/// # .boxed()
40/// # }
41/// ```
42#[component]
43pub async fn page(
44    #[default] mut attrs: Attributes,
45    #[default] child: Child<'_>,
46) -> Result<impl View> {
47    Ok(view! { <div class=(class!(PAGE, attrs.remove("class"))) (attrs)>(child)</div> })
48}
49
50/// The page's heading row: a [`page_title`], an optional [`page_description`]
51/// under it, and optional [`page_actions`] at the end of the title's line.
52#[component]
53pub async fn page_header(
54    #[default] mut attrs: Attributes,
55    #[default] child: Child<'_>,
56) -> Result<impl View> {
57    Ok(view! {
58        <div class=(class!(PAGE_HEADER, attrs.remove("class"))) (attrs)>(child)</div>
59    })
60}
61
62#[component]
63pub async fn page_title(
64    #[default] mut attrs: Attributes,
65    #[default] child: Child<'_>,
66) -> Result<impl View> {
67    Ok(view! {
68        <h1 class=(class!(PAGE_TITLE, attrs.remove("class"))) (attrs)>(child)</h1>
69    })
70}
71
72#[component]
73pub async fn page_description(
74    #[default] mut attrs: Attributes,
75    #[default] child: Child<'_>,
76) -> Result<impl View> {
77    Ok(view! {
78        <p
79            class=(class!(PAGE_DESCRIPTION, attrs.remove("class")))
80            data-page-description=""
81            (attrs)
82        >
83            (child)
84        </p>
85    })
86}
87
88/// The page-level controls (Create, Edit, Back...) at the end of the title's
89/// line in a [`page_header`].
90#[component]
91pub async fn page_actions(
92    #[default] mut attrs: Attributes,
93    #[default] child: Child<'_>,
94) -> Result<impl View> {
95    Ok(view! {
96        <div class=(class!(PAGE_ACTIONS, attrs.remove("class"))) (attrs)>(child)</div>
97    })
98}
99
100#[component]
101pub async fn page_content(
102    #[default] mut attrs: Attributes,
103    #[default] child: Child<'_>,
104) -> Result<impl View> {
105    Ok(view! {
106        <div class=(class!(PAGE_CONTENT, attrs.remove("class"))) (attrs)>(child)</div>
107    })
108}