Skip to main content

tablo_ui/components/composites/
error_state.rs

1use topcoat::{
2    Result,
3    icon::{icon, iconify::iconify_icon},
4    view::{Attributes, Child, StaticClass, View, class, component, view},
5};
6
7const ERROR_STATE: StaticClass = class!(
8    "flex flex-col items-center gap-2 rounded-xl border border-destructive/30 \
9     bg-card px-6 py-12 text-center text-card-foreground \
10     [&>svg]:mb-1 [&>svg]:size-6 [&>svg]:text-destructive"
11);
12const ERROR_STATE_TITLE: StaticClass = class!("text-sm font-medium text-destructive");
13const ERROR_STATE_DETAIL: StaticClass = class!("text-sm text-muted-foreground");
14const ERROR_STATE_ACTION: StaticClass = class!("text-sm font-medium text-primary hover:underline");
15
16/// Renders a failed load inside the content region that failed.
17///
18/// Keeps `detail` generic: never renders error internals (driver messages, SQL, paths) into the
19/// page.
20///
21/// ```text
22/// let action = view! { cx => <a href=(list_url)>"Retry"</a> }.boxed();
23/// Ok(view! { cx =>
24///     error_state(
25///         title: "Couldn't load Users",
26///         detail: "Something went wrong while loading the records.",
27///         action: Some(action.into()),
28///     )
29/// }.boxed())
30/// ```
31#[component]
32pub async fn error_state(
33    /// Short heading naming what failed, e.g. `"Couldn't load Users"`.
34    #[into]
35    title: String,
36    /// Optional muted detail line. Keep it generic — no error internals.
37    #[into]
38    #[default]
39    detail: String,
40    /// Optional action under the text (e.g. a retry link). It inherits the
41    /// component's action styling; pass a plain `<a>` (or button) without
42    /// your own color classes.
43    #[default]
44    action: Option<Child<'_>>,
45    /// Extra attributes for the container. Add `role="alert"` here when the
46    /// region swaps in during a visit and the failure should be announced.
47    #[default]
48    mut attrs: Attributes,
49) -> Result<impl View> {
50    Ok(view! {
51        <div class=(class!(ERROR_STATE, attrs.remove("class"))) (attrs)>
52            icon(data: iconify_icon!("lucide:circle-alert"))
53            <p class=(class!(ERROR_STATE_TITLE))>(title)</p>
54            if !detail.is_empty() {
55                <p class=(class!(ERROR_STATE_DETAIL))>(detail)</p>
56            }
57            if let Some(action) = action {
58                <div class=(class!(ERROR_STATE_ACTION))>(action)</div>
59            }
60        </div>
61    })
62}
63
64#[cfg(test)]
65mod tests;