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/// ```rust
22/// # use tablo_ui::error_state;
23/// # use topcoat::{Result, context::Cx, view::{BoxView, ViewExt, view}};
24/// # fn failed<'a>(cx: &'a Cx, list_url: &'a str) -> Result<BoxView<'a>> {
25/// let action = view! { cx => <a href=(list_url)>"Retry"</a> }.boxed();
26/// Ok(view! { cx =>
27///     error_state(
28///         title: "Couldn't load Users",
29///         detail: "Something went wrong while loading the records.",
30///         action: Some(action.into()),
31///     )
32/// }
33/// .boxed())
34/// # }
35/// ```
36#[component]
37pub async fn error_state(
38    /// Short heading naming what failed, e.g. `"Couldn't load Users"`.
39    #[into]
40    title: String,
41    /// Optional muted detail line. Keep it generic — no error internals.
42    #[into]
43    #[default]
44    detail: String,
45    /// Optional action under the text (e.g. a retry link). It inherits the
46    /// component's action styling; pass a plain `<a>` (or button) without
47    /// your own color classes.
48    #[default]
49    action: Option<Child<'_>>,
50    /// Extra attributes for the container. Add `role="alert"` here when the
51    /// region swaps in during a visit and the failure should be announced.
52    #[default]
53    mut attrs: Attributes,
54) -> Result<impl View> {
55    Ok(view! {
56        <div class=(class!(ERROR_STATE, attrs.remove("class"))) (attrs)>
57            icon(data: iconify_icon!("lucide:circle-alert"))
58            <p class=(class!(ERROR_STATE_TITLE))>(title)</p>
59            if !detail.is_empty() {
60                <p class=(class!(ERROR_STATE_DETAIL))>(detail)</p>
61            }
62            if let Some(action) = action {
63                <div class=(class!(ERROR_STATE_ACTION))>(action)</div>
64            }
65        </div>
66    })
67}