use std::collections::HashSet; use icons::{Check, ChevronDown, ChevronUp}; use leptos::context::Provider; use leptos::prelude::*; use tw_merge::*; use crate::components::hooks::use_can_scroll_vertical::use_can_scroll_vertical; use crate::components::hooks::use_random::use_random_id_for; // * Reuse @select.rs pub use crate::components::ui::select::{ SelectGroup as MultiSelectGroup, SelectItem as MultiSelectItem, SelectLabel as MultiSelectLabel, }; #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum MultiSelectAlign { Start, #[default] Center, End, } /* ========================================================== */ /* ✨ FUNCTIONS ✨ */ /* ========================================================== */ #[component] pub fn MultiSelectValue(#[prop(optional, into)] placeholder: String) -> impl IntoView { let multi_select_ctx = expect_context::(); view! { {move || { let values = multi_select_ctx.values_signal.get(); if values.is_empty() { placeholder.clone() } else { let count = values.len(); if count == 1 { "1 selected".to_string() } else { format!("{} selected", count) } } }} } } #[component] pub fn MultiSelectOption( children: Children, #[prop(optional, into)] class: String, #[prop(optional, into)] value: Option, ) -> impl IntoView { let multi_select_ctx = expect_context::(); let value_clone = value.clone(); let is_selected = Signal::derive(move || { if let Some(ref val) = value_clone { multi_select_ctx.values_signal.with(|values| values.contains(val)) } else { false } }); let class = tw_merge!( "group inline-flex gap-2 items-center w-full text-sm text-left transition-colors duration-200 focus:outline-none focus-visible:outline-none text-popover-foreground [&_svg:not([class*='size-'])]:size-4 hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-50", class ); view! { } } /* ========================================================== */ /* ✨ FUNCTIONS ✨ */ /* ========================================================== */ #[derive(Clone)] struct MultiSelectContext { target_id: String, values_signal: RwSignal>, align: MultiSelectAlign, } #[component] pub fn MultiSelect( children: Children, #[prop(optional, into)] values: Option>>, #[prop(default = MultiSelectAlign::default())] align: MultiSelectAlign, ) -> impl IntoView { let multi_select_target_id = use_random_id_for("multi_select"); let values_signal = values.unwrap_or_else(|| RwSignal::new(HashSet::::new())); let multi_select_ctx = MultiSelectContext { target_id: multi_select_target_id.clone(), values_signal, align }; view! {
{children()}
} } #[component] pub fn MultiSelectTrigger( children: Children, #[prop(optional, into)] class: String, #[prop(optional, into)] id: String, ) -> impl IntoView { let multi_select_ctx = expect_context::(); let peer_class = if !id.is_empty() { format!("peer/{}", id) } else { String::new() }; let button_class = tw_merge!( "w-full p-2 h-9 inline-flex items-center justify-between text-sm font-medium whitespace-nowrap rounded-md transition-colors focus:outline-none focus:ring-1 focus:ring-ring focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&_svg:not(:last-child)]:mr-2 [&_svg:not(:first-child)]:ml-2 [&_svg:not([class*='size-'])]:size-4 border bg-background border-input hover:bg-accent hover:text-accent-foreground", &peer_class, class ); let button_id = if !id.is_empty() { id } else { format!("trigger_{}", multi_select_ctx.target_id) }; view! { } } #[component] pub fn MultiSelectContent(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { let multi_select_ctx = expect_context::(); let align_str = match multi_select_ctx.align { MultiSelectAlign::Start => "start", MultiSelectAlign::Center => "center", MultiSelectAlign::End => "end", }; let class = tw_merge!( "w-[150px] overflow-auto z-50 p-1 rounded-md border bg-card shadow-md h-fit max-h-[300px] absolute top-[calc(100%+4px)] transition-all duration-200 data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100 data-[align=start]:left-0 data-[align=center]:left-1/2 data-[align=center]:-translate-x-1/2 data-[align=end]:right-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", class ); let target_id_for_script = multi_select_ctx.target_id.clone(); let target_id_for_script_2 = multi_select_ctx.target_id.clone(); // Scroll indicator signals let (on_scroll, can_scroll_up_signal, can_scroll_down_signal) = use_can_scroll_vertical(); view! {
{children()}
}.into_any() }