State-Fidelity Primitives
Every list view renders three states beyond the happy path: loading, empty, and error. The state-fidelity primitives give you drop-in components for each, tuned for mobile (Apple HIG type scale, 44pt touch targets) and density-preserving on desktop.
Skeleton
Section titled “Skeleton”Animated shimmer block for loading placeholders. Honors prefers-reduced-motion: reduce.
File: src/ui/skeleton_ui.rs
#[component]pub fn Skeleton( #[props(default = "h-4 w-full".to_string())] class: String,) -> ElementPass Tailwind size/shape utilities to control dimensions:
// Single text line (default)Skeleton {}
// Avatar circleSkeleton { class: "h-10 w-10 rounded-full shrink-0".to_string() }
// Wide blockSkeleton { class: "h-7 w-2/3".to_string() }The .skeleton utility in tailwind.css applies a gradient shimmer over --color-muted. Reduced-motion users see a static gray fill.
EmptyState
Section titled “EmptyState”Centered column with icon, title, description, and optional CTA. Mobile-first layout with md: pairings for desktop density.
File: src/ui/empty_state_ui.rs
#[component]pub fn EmptyState( icon: Option<Element>, // decorative Lucide icon title: String, // one-line headline description: Option<String>, // supporting copy cta: Option<Element>, // primary action (typically a Button) compact: bool, // trims padding for in-panel use class: Option<String>, // extra utilities) -> Elementuse crate::ui::{Button, EmptyState};use lucide_dioxus::FileText;
EmptyState { icon: rsx! { FileText { size: 48 } }, title: "No reports yet".to_string(), description: "Enable daily reports in Settings to start receiving them.".to_string(), cta: rsx! { Link { to: Route::SettingsSubsectionView { section: "reporting".to_string(), subsection: "daily-report".to_string(), }, Button { Settings { size: 16, class: "mr-2" } "Open Settings" } } },}Use compact: true for sidebars and dropdowns (e.g. notification panel). Standard mode uses the iOS type scale (text-headline, text-body) with generous padding.
ErrorState
Section titled “ErrorState”Auto-detects 403 errors and renders AccessDenied. All other errors show a centered card with a TriangleAlert icon, friendly message, and optional Retry button.
File: src/shared/components/error_state_component.rs
#[component]pub fn ErrorState( error: String, // raw ServerFnError string resource_name: String, // e.g. "contacts", "calls" on_retry: Option<EventHandler<()>>, // retry callback compact: bool, // in-panel mode class: Option<String>, // extra utilities) -> ElementResourceError bridge
Section titled “ResourceError bridge”ResourceError wraps ErrorState with the same props, keeping the existing API back-compatible:
Some(Err(e)) => rsx! { ResourceError { error: e.to_string(), resource_name: "contacts".to_string(), on_retry: move |_| contacts_resource.restart(), }},Skeleton compositions
Section titled “Skeleton compositions”Three layout-matching skeleton components replace ViewLoader during initial fetch.
ListRowSkeleton
Section titled “ListRowSkeleton”Mirrors a vertical list of rows with optional avatar, two text lines, and a trailing stub.
File: src/shared/components/list_row_skeleton_component.rs
| Prop | Type | Default | Description |
|---|---|---|---|
count | u8 | 6 | Number of stub rows |
with_avatar | bool | true | Show a leading avatar circle |
// Calls list loading stateNone => rsx! { ListRowSkeleton {} },
// Reports — no avatars, fewer rowsNone => rsx! { ListRowSkeleton { count: 5, with_avatar: false } },CardGridSkeleton
Section titled “CardGridSkeleton”Mirrors EntityGrid’s responsive 1/2/3-column card layout.
File: src/shared/components/card_grid_skeleton_component.rs
| Prop | Type | Default | Description |
|---|---|---|---|
count | u8 | 6 | Number of stub cards |
// Agents grid loading stateNone => rsx! { CardGridSkeleton {} },KpiSkeleton
Section titled “KpiSkeleton”Stat-card row for dashboard-style KPI blocks (grid-cols-2 md:grid-cols-4).
File: src/shared/components/kpi_skeleton_component.rs
| Prop | Type | Default | Description |
|---|---|---|---|
count | u8 | 4 | Number of stat-card stubs |
View migration pattern
Section titled “View migration pattern”Replace ViewLoader / inline error divs with the three-branch match:
match &*resource.read() { // Loading → layout-matching skeleton None => rsx! { ListRowSkeleton {} },
// Error → ResourceError with retry Some(Err(e)) => rsx! { ResourceError { error: e.to_string(), resource_name: "calls".to_string(), on_retry: move |_| resource.restart(), } },
// Success → data or EmptyState Some(Ok(data)) => { if data.items.is_empty() { rsx! { EmptyState { icon: rsx! { Phone { size: 48 } }, title: "No calls yet".to_string(), description: "Make your first call to get started.".to_string(), cta: rsx! { Button { "Make a call" } }, } } } else { rsx! { /* render items */ } } }}Migrated views
Section titled “Migrated views”Eight list views use this pattern:
| View | Skeleton | Empty CTA |
|---|---|---|
| Calls | ListRowSkeleton | Make a call / Clear filters |
| Contacts | ListRowSkeleton | Add contact / Clear filters |
| Tasks | ListRowSkeleton | New task |
| Messaging | ListRowSkeleton (compact) | “All caught up!” |
| Agents | CardGridSkeleton | Create agent |
| Knowledge | CardGridSkeleton | Create knowledge base |
| Plans | ListRowSkeleton | Reset filters |
| Reports | ListRowSkeleton | Open Settings |
CTAs are permission-gated — they render only when can_create (or equivalent) is true.