Skip to content

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.

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,
) -> Element

Pass Tailwind size/shape utilities to control dimensions:

// Single text line (default)
Skeleton {}
// Avatar circle
Skeleton { class: "h-10 w-10 rounded-full shrink-0".to_string() }
// Wide block
Skeleton { 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.

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
) -> Element
use 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.

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
) -> Element

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(),
}
},

Three layout-matching skeleton components replace ViewLoader during initial fetch.

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

PropTypeDefaultDescription
countu86Number of stub rows
with_avatarbooltrueShow a leading avatar circle
// Calls list loading state
None => rsx! { ListRowSkeleton {} },
// Reports — no avatars, fewer rows
None => rsx! { ListRowSkeleton { count: 5, with_avatar: false } },

Mirrors EntityGrid’s responsive 1/2/3-column card layout.

File: src/shared/components/card_grid_skeleton_component.rs

PropTypeDefaultDescription
countu86Number of stub cards
// Agents grid loading state
None => rsx! { CardGridSkeleton {} },

Stat-card row for dashboard-style KPI blocks (grid-cols-2 md:grid-cols-4).

File: src/shared/components/kpi_skeleton_component.rs

PropTypeDefaultDescription
countu84Number of stat-card stubs

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 */ }
}
}
}

Eight list views use this pattern:

ViewSkeletonEmpty CTA
CallsListRowSkeletonMake a call / Clear filters
ContactsListRowSkeletonAdd contact / Clear filters
TasksListRowSkeletonNew task
MessagingListRowSkeleton (compact)“All caught up!”
AgentsCardGridSkeletonCreate agent
KnowledgeCardGridSkeletonCreate knowledge base
PlansListRowSkeletonReset filters
ReportsListRowSkeletonOpen Settings

CTAs are permission-gated — they render only when can_create (or equivalent) is true.