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 select;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
pub mod toast;
|
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