feat(leptos): UI primitives — Skeleton, StatusBadge, EmptyState, Modal
This commit is contained in:
@@ -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>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user