Skip to main content

tablo_ui/components/composites/
toast.rs

1//! Provides the toast surface and stack.
2
3use topcoat::{
4    Result,
5    icon::icon,
6    view::{Attributes, Child, StaticClass, View, attributes, class, component, view},
7};
8
9use crate::icons;
10
11/// Positions the toast stack bottom-right.
12const TOASTER: StaticClass = class!(
13    "pointer-events-none fixed right-4 bottom-4 z-50 flex w-[356px] \
14     max-w-[calc(100vw-2rem)] flex-col gap-3.5 sm:right-6 sm:bottom-6",
15);
16
17/// Styles the toast surface.
18///
19/// The toast slides in, then fades out 4s later; hovering or focusing it holds it, and the
20/// countdown restarts once it is left. Its close control hides it at once.
21const TOAST: StaticClass = class!(
22    "pointer-events-auto relative flex w-full items-center gap-1.5 rounded-lg \
23     border border-border bg-background p-4 text-[13px] text-foreground shadow-lg \
24     [overflow-wrap:anywhere] \
25     invisible opacity-0 starting:visible starting:translate-y-full starting:opacity-100 \
26     [transition:translate_400ms_cubic-bezier(0.25,0.1,0.25,1),opacity_400ms_ease_4s,visibility_0s_linear_4.4s] \
27     hover:visible hover:opacity-100 hover:[transition-delay:0s] \
28     focus-within:visible focus-within:opacity-100 focus-within:[transition-delay:0s] \
29     has-[[data-dismiss]:checked]:hidden \
30     focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none",
31);
32
33/// Styles the icon slot.
34const ICON: StaticClass = class!("flex size-4 shrink-0 items-center justify-center");
35
36/// Styles the content column.
37const CONTENT: StaticClass = class!("flex min-w-0 flex-1 flex-col gap-0.5");
38
39/// Styles the toast title.
40const TITLE: StaticClass = class!("font-medium leading-normal");
41
42/// Styles the toast description.
43const DESCRIPTION: StaticClass = class!("leading-snug text-muted-foreground");
44
45/// Styles the close button.
46const CLOSE: StaticClass = class!(
47    "absolute top-0 left-0 flex size-5 -translate-x-[35%] -translate-y-[35%] \
48     cursor-pointer items-center justify-center rounded-full border border-border \
49     bg-background text-foreground transition-colors hover:bg-muted",
50);
51
52/// Renders the toast stack.
53///
54/// ```rust
55/// # use tablo_ui::{icons, toast, toast_close, toast_content, toast_description, toast_icon, toast_title, toaster};
56/// # use topcoat::{context::Cx, icon::icon, view::{BoxView, ViewExt, attributes, view}};
57/// # fn created(cx: &Cx) -> BoxView<'_> {
58/// # view! { cx =>
59/// toaster(label: "Notifications",
60///     toast(attrs: attributes! { data-type="success" },
61///         toast_icon(icon(data: icons::CIRCLE_CHECK))
62///         toast_content(
63///             toast_title("Created")
64///             toast_description("The record is live.")
65///         )
66///         toast_close(label: "Close")
67///     )
68/// )
69/// # }
70/// # .boxed()
71/// # }
72/// ```
73#[component]
74pub async fn toaster(
75    /// The region's accessible name, such as `"Notifications"`.
76    #[into]
77    label: String,
78    #[default] mut attrs: Attributes,
79    #[default] child: Child<'_>,
80) -> Result<impl View> {
81    Ok(view! {
82        <section
83            aria-label=(label)
84            tabindex="-1"
85            aria-live="polite"
86            aria-relevant="additions text"
87            aria-atomic="false"
88        >
89            <ol
90                class=(class!(TOASTER, attrs.remove("class")))
91                data-sonner-toaster=""
92                data-y-position="bottom"
93                data-x-position="right"
94                (attrs)
95            >
96                (child)
97            </ol>
98        </section>
99    })
100}
101
102/// Renders a toast surface.
103#[component]
104pub async fn toast(
105    #[default] mut attrs: Attributes,
106    #[default] child: Child<'_>,
107) -> Result<impl View> {
108    Ok(view! {
109        <li
110            tabindex="0"
111            class=(class!(TOAST, attrs.remove("class")))
112            data-sonner-toast=""
113            data-styled="true"
114            data-visible="true"
115            data-y-position="bottom"
116            data-x-position="right"
117            data-front="true"
118            (attrs)
119        >
120            (child)
121        </li>
122    })
123}
124
125/// Renders the status icon slot.
126#[component]
127pub async fn toast_icon(
128    #[default] mut attrs: Attributes,
129    #[default] child: Child<'_>,
130) -> Result<impl View> {
131    Ok(view! {
132        <div
133            class=(class!(ICON, attrs.remove("class")))
134            data-icon=""
135            aria-hidden="true"
136            (attrs)
137        >
138            (child)
139        </div>
140    })
141}
142
143/// Renders the content column.
144#[component]
145pub async fn toast_content(
146    #[default] mut attrs: Attributes,
147    #[default] child: Child<'_>,
148) -> Result<impl View> {
149    Ok(view! {
150        <div class=(class!(CONTENT, attrs.remove("class"))) data-content="" (attrs)>
151            (child)
152        </div>
153    })
154}
155
156/// Renders the toast title.
157#[component]
158pub async fn toast_title(
159    #[default] mut attrs: Attributes,
160    #[default] child: Child<'_>,
161) -> Result<impl View> {
162    Ok(view! {
163        <div class=(class!(TITLE, attrs.remove("class"))) data-title="" (attrs)>
164            (child)
165        </div>
166    })
167}
168
169/// Renders the toast description.
170#[component]
171pub async fn toast_description(
172    #[default] mut attrs: Attributes,
173    #[default] child: Child<'_>,
174) -> Result<impl View> {
175    Ok(view! {
176        <div
177            class=(class!(DESCRIPTION, attrs.remove("class")))
178            data-description=""
179            (attrs)
180        >
181            (child)
182        </div>
183    })
184}
185
186/// Renders the close control: a checkbox whose checked state hides its toast.
187#[component]
188pub async fn toast_close(
189    /// The checkbox's accessible name, such as `"Close"`.
190    #[into]
191    label: String,
192    #[default] mut attrs: Attributes,
193) -> Result<impl View> {
194    Ok(view! {
195        <label
196            class=(class!(
197                CLOSE,
198                "has-focus-visible:ring-2 has-focus-visible:ring-ring/50",
199                attrs.remove("class"),
200            ))
201            data-close-button=""
202            (attrs)
203        >
204            <input type="checkbox" class="sr-only" aria-label=(label) data-dismiss="">
205            icon(data: icons::X, attrs: attributes! { class="size-3" })
206        </label>
207    })
208}