From b9e3a255bb9e94d0339279828dbb7df34cfa64b9 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 3 Jul 2026 17:47:21 +0700 Subject: [PATCH] =?UTF-8?q?feat(leptos):=20UI=20primitives=20=E2=80=94=20I?= =?UTF-8?q?nput,=20Select,=20Tabs,=20ScrollArea,=20Toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend-leptos/frontend/src/ui/input.rs | 47 ++++++++++ .../frontend-leptos/frontend/src/ui/mod.rs | 5 ++ .../frontend/src/ui/scroll_area.rs | 15 ++++ .../frontend-leptos/frontend/src/ui/select.rs | 30 +++++++ .../frontend-leptos/frontend/src/ui/tabs.rs | 66 ++++++++++++++ .../frontend-leptos/frontend/src/ui/toast.rs | 85 +++++++++++++++++++ 6 files changed, 248 insertions(+) create mode 100644 services/frontend-leptos/frontend/src/ui/input.rs create mode 100644 services/frontend-leptos/frontend/src/ui/scroll_area.rs create mode 100644 services/frontend-leptos/frontend/src/ui/select.rs create mode 100644 services/frontend-leptos/frontend/src/ui/tabs.rs create mode 100644 services/frontend-leptos/frontend/src/ui/toast.rs diff --git a/services/frontend-leptos/frontend/src/ui/input.rs b/services/frontend-leptos/frontend/src/ui/input.rs new file mode 100644 index 0000000..809aa61 --- /dev/null +++ b/services/frontend-leptos/frontend/src/ui/input.rs @@ -0,0 +1,47 @@ +// services/frontend-leptos/frontend/src/ui/input.rs +use leptos::prelude::*; + +#[component] +pub fn Input( + #[prop(optional)] input_type: &'static str, + #[prop(optional)] placeholder: &'static str, + #[prop(optional)] value: RwSignal, + #[prop(optional)] soft: bool, + #[prop(optional)] error: bool, + #[prop(optional)] class: &'static str, + #[prop(optional)] on_input: Option>, +) -> impl IntoView { + view! { + + } +} + +#[component] +pub fn TextArea( + #[prop(optional)] placeholder: &'static str, + #[prop(optional)] value: RwSignal, + #[prop(optional)] rows: u32, + #[prop(optional)] class: &'static str, +) -> impl IntoView { + view! { + + } +} diff --git a/services/frontend-leptos/frontend/src/ui/mod.rs b/services/frontend-leptos/frontend/src/ui/mod.rs index cf0c674..d67bd50 100644 --- a/services/frontend-leptos/frontend/src/ui/mod.rs +++ b/services/frontend-leptos/frontend/src/ui/mod.rs @@ -2,3 +2,8 @@ pub mod badge; pub mod button; pub mod card; +pub mod input; +pub mod scroll_area; +pub mod select; +pub mod tabs; +pub mod toast; diff --git a/services/frontend-leptos/frontend/src/ui/scroll_area.rs b/services/frontend-leptos/frontend/src/ui/scroll_area.rs new file mode 100644 index 0000000..22f8f30 --- /dev/null +++ b/services/frontend-leptos/frontend/src/ui/scroll_area.rs @@ -0,0 +1,15 @@ +// services/frontend-leptos/frontend/src/ui/scroll_area.rs +use leptos::prelude::*; + +#[component] +pub fn ScrollArea( + #[prop(optional)] class: &'static str, + #[prop(optional)] style: &'static str, + children: Children, +) -> impl IntoView { + view! { +
+ {children()} +
+ } +} diff --git a/services/frontend-leptos/frontend/src/ui/select.rs b/services/frontend-leptos/frontend/src/ui/select.rs new file mode 100644 index 0000000..24119b8 --- /dev/null +++ b/services/frontend-leptos/frontend/src/ui/select.rs @@ -0,0 +1,30 @@ +// services/frontend-leptos/frontend/src/ui/select.rs +use leptos::prelude::*; + +/// Simple select — values and labels are the same +/// For options with different value/label, use `SelectOptions` +#[component] +pub fn Select( + #[prop(optional)] value: RwSignal, + options: Vec<(&'static str, &'static str)>, // (value, label) + #[prop(optional)] placeholder: &'static str, + #[prop(optional)] class: &'static str, + #[prop(optional)] on_change: Option>, +) -> impl IntoView { + view! { + + } +} diff --git a/services/frontend-leptos/frontend/src/ui/tabs.rs b/services/frontend-leptos/frontend/src/ui/tabs.rs new file mode 100644 index 0000000..f1916a1 --- /dev/null +++ b/services/frontend-leptos/frontend/src/ui/tabs.rs @@ -0,0 +1,66 @@ +// services/frontend-leptos/frontend/src/ui/tabs.rs +use leptos::prelude::*; + +#[component] +pub fn Tabs( + active: RwSignal, + #[prop(optional)] class: &'static str, + children: Children, +) -> impl IntoView { + view! { +
+ {children()} +
+ } +} + +#[component] +pub fn TabList( + #[prop(optional)] class: &'static str, + children: Children, +) -> impl IntoView { + view! { +
+ {children()} +
+ } +} + +#[component] +pub fn TabTrigger( + value: String, + active: RwSignal, + children: Children, +) -> impl IntoView { + let v1 = value.clone(); + let v2 = value.clone(); + view! { + + } +} + +#[component] +pub fn TabContent( + value: String, + active: RwSignal, + children: Children, +) -> impl IntoView { + let is_selected = move || active.get() == value; + view! { +
+ {children()} +
+ } +} diff --git a/services/frontend-leptos/frontend/src/ui/toast.rs b/services/frontend-leptos/frontend/src/ui/toast.rs new file mode 100644 index 0000000..ac47d49 --- /dev/null +++ b/services/frontend-leptos/frontend/src/ui/toast.rs @@ -0,0 +1,85 @@ +// services/frontend-leptos/frontend/src/ui/toast.rs +use leptos::prelude::*; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub enum ToastType { + Info, + Success, + Error, + Warning, +} + +#[derive(Clone)] +pub struct ToastMessage { + pub id: u64, + pub message: String, + pub toast_type: ToastType, +} + +#[derive(Clone)] +pub struct ToastContext { + pub toasts: RwSignal>, + next_id: Arc>, +} + +impl ToastContext { + pub fn new() -> Self { + Self { + toasts: create_rw_signal(vec![]), + next_id: Arc::new(Mutex::new(0)), + } + } + + pub fn show(&self, message: &str, toast_type: ToastType) { + let id = { + let mut n = self.next_id.lock().unwrap(); + *n += 1; + *n + }; + let msg = ToastMessage { + id, + message: message.to_string(), + toast_type, + }; + self.toasts.update(|t| t.push(msg)); + + // Auto-dismiss after 4 seconds + let toasts = self.toasts; + leptos::prelude::set_timeout( + move || { + toasts.update(|t| t.retain(|m| m.id != id)); + }, + std::time::Duration::from_secs(4), + ); + } +} + +#[component] +pub fn ToastProvider(children: Children) -> impl IntoView { + let ctx = ToastContext::new(); + provide_context(ctx.clone()); + + view! { + {children()} +
+ {move || ctx.toasts.get().into_iter().map(|msg| { + let type_class = match msg.toast_type { + ToastType::Info => "toast-info", + ToastType::Success => "toast-success", + ToastType::Error => "toast-error", + ToastType::Warning => "toast-warning", + }; + let toasts = ctx.toasts; + view! { +
+ {msg.message} + +
+ } + }).collect::>()} +
+ } +}