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}