feat(leptos): UI primitives — Skeleton, StatusBadge, EmptyState, Modal

This commit is contained in:
asepharyana
2026-07-03 17:59:38 +07:00
parent b9e3a255bb
commit 5832ca7cd4
5 changed files with 120 additions and 0 deletions
@@ -0,0 +1,19 @@
// services/frontend-leptos/frontend/src/ui/empty_state.rs
use leptos::prelude::*;
#[component]
pub fn EmptyState(
#[prop(optional)] icon: Option<AnyView>,
title: &'static str,
#[prop(optional)] description: Option<&'static str>,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
view! {
<div class="empty-state">
{icon.map(|i| view! { <div class="empty-state-icon">{i}</div> })}
<div class="empty-state-title">{title}</div>
{description.map(|d| view! { <p class="empty-state-description">{d}</p> })}
{children.map(|c| c())}
</div>
}
}
@@ -7,3 +7,7 @@ pub mod scroll_area;
pub mod select;
pub mod tabs;
pub mod toast;
pub mod skeleton;
pub mod status_badge;
pub mod empty_state;
pub mod modal;
@@ -0,0 +1,46 @@
// services/frontend-leptos/frontend/src/ui/modal.rs
use std::sync::Arc;
use leptos::prelude::*;
#[component]
pub fn Modal(
is_open: RwSignal<bool>,
#[prop(optional)] title: Option<&'static str>,
#[prop(optional)] on_close: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
children: Children,
) -> impl IntoView {
let oc1 = on_close.clone();
let oc2 = on_close;
view! {
<div
class="modal-overlay"
style=move || {
if is_open.get() {
String::new()
} else {
"display: none;".to_string()
}
}
on:click=move |_| {
is_open.set(false);
if let Some(ref cb) = oc1 { cb(); }
}
>
<div class="modal-content" on:click=|ev| ev.stop_propagation()>
{title.map(|t| view! {
<div class="modal-header">
<h3>{t}</h3>
<button class="btn btn-ghost btn-icon-sm" on:click=move |_| {
is_open.set(false);
if let Some(ref cb) = oc2 { cb(); }
}>"×"</button>
</div>
})}
<div class="modal-body">
{children()}
</div>
</div>
</div>
}
}
@@ -0,0 +1,30 @@
// services/frontend-leptos/frontend/src/ui/skeleton.rs
use leptos::prelude::*;
#[derive(Clone, Default)]
pub enum SkeletonShape {
#[default]
Rounded,
Circular,
Rectangular,
}
#[component]
pub fn Skeleton(
#[prop(optional)] width: &'static str,
#[prop(optional)] height: &'static str,
#[prop(optional)] shape: SkeletonShape,
) -> impl IntoView {
let shape_class = match shape {
SkeletonShape::Rounded => "",
SkeletonShape::Circular => "skeleton-circular",
SkeletonShape::Rectangular => "skeleton-rectangular",
};
let combined = format!("skeleton {}", shape_class);
view! {
<div
class=combined
style=format!("width: {}; height: {};", width, height)
></div>
}
}
@@ -0,0 +1,21 @@
// services/frontend-leptos/frontend/src/ui/status_badge.rs
use leptos::prelude::*;
use shared_types::message::AiStatus;
#[component]
pub fn StatusBadge(status: AiStatus) -> impl IntoView {
let (class, label) = match status {
AiStatus::Flagged => ("status-badge-flagged", "Flagged"),
AiStatus::Clean => ("status-badge-clean", "Clean"),
AiStatus::Warn => ("status-badge-warn", "Warned"),
AiStatus::Pending => ("status-badge-pending", "Pending"),
AiStatus::Processing => ("status-badge-processing", "Processing"),
AiStatus::Error => ("status-badge-error", "Error"),
};
let combined = format!("status-badge {}", class);
view! {
<span class=combined>
{label}
</span>
}
}