use icons::{Check, ChevronDown, ChevronUp}; use leptos::context::Provider; use leptos::prelude::*; use leptos_ui::clx; use strum::{AsRefStr, Display}; 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; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Display, AsRefStr)] pub enum SelectPosition { #[default] Below, Above, } mod components { use super::*; clx! {SelectLabel, span, "px-2 py-1.5 text-sm font-medium data-inset:pl-8", "mb-1"} clx! {SelectItem, li, "inline-flex gap-2 items-center w-full rounded-sm px-2 py-1.5 text-sm no-underline transition-colors duration-200 text-popover-foreground hover:bg-accent hover:text-accent-foreground [&_svg:not([class*='size-'])]:size-4"} } pub use components::*; #[component] pub fn SelectGroup( children: Children, #[prop(optional, into)] class: String, #[prop(default = "Select options".into(), into)] aria_label: String, ) -> impl IntoView { let merged_class = tw_merge!("group", class); view! { } } #[component] pub fn SelectValue(#[prop(optional, into)] placeholder: String) -> impl IntoView { let select_ctx = expect_context::(); view! { {move || { select_ctx.value_signal.get().unwrap_or_else(|| placeholder.clone()) }} } } #[component] pub fn SelectOption( children: Children, #[prop(optional, into)] class: String, #[prop(default = false.into(), into)] aria_selected: Signal, #[prop(optional, into)] value: Option, ) -> impl IntoView { let ctx = expect_context::(); let merged_class = tw_merge!( "group inline-flex gap-2 items-center w-full rounded-sm px-2 py-1.5 text-sm cursor-pointer no-underline transition-colors duration-200 text-popover-foreground hover:bg-accent hover:text-accent-foreground [&_svg:not([class*='size-'])]:size-4", class ); let value_for_check = value.clone(); let is_selected = move || aria_selected.get() || ctx.value_signal.get() == value_for_check; view! {
  • {children()}
  • } } #[derive(Clone)] struct SelectContext { target_id: String, value_signal: RwSignal>, on_change: Option>>, } #[component] pub fn Select( children: Children, #[prop(optional, into)] class: String, #[prop(optional, into)] default_value: Option, #[prop(optional)] on_change: Option>>, ) -> impl IntoView { let select_target_id = use_random_id_for("select"); let value_signal = RwSignal::new(default_value); let ctx = SelectContext { target_id: select_target_id.clone(), value_signal, on_change }; let merged_class = tw_merge!("relative w-fit", class); view! {
    {children()}
    } } #[component] pub fn SelectTrigger( children: Children, #[prop(optional, into)] class: String, #[prop(optional, into)] id: String, ) -> impl IntoView { let 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_{}", ctx.target_id) }; view! { } } #[component] pub fn SelectContent( children: Children, #[prop(optional, into)] class: String, #[prop(default = SelectPosition::default())] position: SelectPosition, #[prop(optional)] on_close: Option>, ) -> impl IntoView { let ctx = expect_context::(); let merged_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)] left-0 data-[position=Above]:top-auto data-[position=Above]:bottom-[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-[state=closed]:data-[position=Below]:origin-top data-[state=open]:data-[position=Below]:origin-top data-[state=closed]:data-[position=Above]:origin-bottom data-[state=open]:data-[position=Above]:origin-bottom [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", class ); let target_id_for_script = ctx.target_id.clone(); let target_id_for_script_2 = 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() }