use icons::{Check, ChevronRight}; use leptos::context::Provider; use leptos::prelude::*; use leptos_ui::clx; use tw_merge::*; use crate::components::hooks::use_random::use_random_id_for; pub use crate::components::ui::separator::Separator as DropdownMenuSeparator; mod components { use super::*; clx! {DropdownMenuLabel, span, "px-2 py-1.5 text-sm font-medium data-inset:pl-8", "mb-1"} clx! {DropdownMenuGroup, ul, "group"} clx! {DropdownMenuItem, 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"} clx! {DropdownMenuSubContent, ul, "dropdown__menu_sub_content", "rounded-md border bg-card shadow-lg p-1 absolute z-[100] min-w-[160px] opacity-0 invisible translate-x-[-8px] transition-all duration-200 ease-out pointer-events-none"} clx! {DropdownMenuLink, a, "w-full inline-flex gap-2 items-center"} } pub use components::*; /* ========================================================== */ /* RADIO GROUP */ /* ========================================================== */ #[derive(Clone)] struct DropdownMenuRadioContext { value_signal: RwSignal, } /// A group of radio items where only one can be selected at a time. #[component] pub fn DropdownMenuRadioGroup( children: Children, /// The signal holding the current selected value value: RwSignal, ) -> impl IntoView where T: Clone + PartialEq + Send + Sync + 'static, { let ctx = DropdownMenuRadioContext { value_signal: value }; view! {
    {children()}
} } /// A radio item that shows a checkmark when selected. #[component] pub fn DropdownMenuRadioItem( children: Children, /// The value this item represents value: T, #[prop(optional, into)] class: String, ) -> impl IntoView where T: Clone + PartialEq + Send + Sync + 'static, { let ctx = expect_context::>(); let value_for_check = value.clone(); let value_for_click = value.clone(); let is_selected = move || ctx.value_signal.get() == value_for_check; let merged_class = tw_merge!( "group inline-flex gap-2 items-center w-full rounded-sm pl-2 pr-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 ); view! {
  • {children()}
  • } } /// An action item in a dropdown menu (no checkmark, just triggers an action). #[component] pub fn DropdownMenuAction( children: Children, #[prop(optional, into)] class: String, #[prop(optional, into)] href: Option, ) -> impl IntoView { let _ctx = expect_context::(); let class = tw_merge!( "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", class ); if let Some(href) = href { // Render as tag when href is provided view! { {children()} } .into_any() } else { // Render as } .into_any() } } /* ========================================================== */ /* ✨ FUNCTIONS ✨ */ /* ========================================================== */ #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum DropdownMenuAlign { #[default] Start, StartOuter, End, EndOuter, Center, } #[derive(Clone)] struct DropdownMenuContext { target_id: String, align: DropdownMenuAlign, } #[component] pub fn DropdownMenu( children: Children, #[prop(default = DropdownMenuAlign::default())] align: DropdownMenuAlign, ) -> impl IntoView { let dropdown_target_id = use_random_id_for("dropdown"); let ctx = DropdownMenuContext { target_id: dropdown_target_id.clone(), align }; view! {
    {children()}
    } } #[component] pub fn DropdownMenuTrigger(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { let ctx = expect_context::(); let button_class = tw_merge!( "px-4 py-2 h-9 inline-flex justify-center items-center text-sm font-medium whitespace-nowrap rounded-md transition-colors w-fit 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([class*='size-'])]:size-4 border bg-background border-input hover:bg-accent hover:text-accent-foreground", class ); view! { } } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum DropdownMenuPosition { #[default] Auto, Top, Bottom, } #[component] pub fn DropdownMenuContent( children: Children, #[prop(optional, into)] class: String, #[prop(default = DropdownMenuPosition::default())] position: DropdownMenuPosition, ) -> impl IntoView { let ctx = expect_context::(); let base_classes = "z-50 p-1 rounded-md border bg-card shadow-md h-fit fixed transition-all duration-200 data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100"; let width_class = match ctx.align { DropdownMenuAlign::Center => "min-w-full", _ => "w-[180px]", }; let class = tw_merge!(width_class, base_classes, class); let target_id_for_script = ctx.target_id.clone(); let align_for_script = match ctx.align { DropdownMenuAlign::Start => "start", DropdownMenuAlign::StartOuter => "start-outer", DropdownMenuAlign::End => "end", DropdownMenuAlign::EndOuter => "end-outer", DropdownMenuAlign::Center => "center", }; let position_for_script = match position { DropdownMenuPosition::Auto => "auto", DropdownMenuPosition::Top => "top", DropdownMenuPosition::Bottom => "bottom", }; view! {
    {children()}
    } } #[component] pub fn DropdownMenuSub(children: Children) -> impl IntoView { // TODO. Find a better way for dropdown__menu_sub_trigger. clx! {DropdownMenuSubRoot, li, "dropdown__menu_sub_trigger", " relative inline-flex relative gap-2 items-center py-1.5 px-2 w-full text-sm no-underline rounded-sm transition-colors duration-200 cursor-pointer text-popover-foreground [&_svg:not([class*='size-'])]:size-4 hover:bg-accent hover:text-accent-foreground"} view! { {children()} } } #[component] pub fn DropdownMenuSubTrigger(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { let class = tw_merge!("flex items-center justify-between w-full", class); view! { {children()} } } #[component] pub fn DropdownMenuSubItem(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { let class = tw_merge!( "inline-flex gap-2 items-center w-full rounded-sm px-3 py-2 text-sm transition-all duration-150 ease text-popover-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer hover:translate-x-[2px]", class ); view! {
  • {children()}
  • } }