Skip to main content

tablo_ui/components/composites/
error_state.rs

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