import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { cva } from "class-variance-authority"; import * as React from "react"; import { useRef } from "react"; import { Button } from "@sparkle/components/Button"; import { Chip } from "@sparkle/components/Chip"; import { Icon } from "@sparkle/components/Icon"; import { LinkWrapper, LinkWrapperProps } from "@sparkle/components/LinkWrapper"; import { ScrollArea } from "@sparkle/components/ScrollArea"; import { SearchInput, SearchInputProps } from "@sparkle/components/SearchInput"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "@sparkle/icons/app"; import { cn } from "@sparkle/lib/utils"; const ITEM_VARIANTS = ["default", "warning"] as const; type ItemVariantType = (typeof ITEM_VARIANTS)[number]; export const menuStyleClasses = { inset: "s-pl-8", container: cn( "s-rounded-xl s-border-hovering s-p-1", "s-border s-border-border dark:s-border-border-night", "s-bg-background dark:s-bg-muted-background-night", "s-text-foreground dark:s-text-foreground-night", "s-z-50 s-min-w-[8rem]", "data-[state=open]:s-animate-in data-[state=closed]:s-animate-out data-[state=closed]:s-fade-out-0 data-[state=open]:s-fade-in-0 data-[state=closed]:s-zoom-out-95 data-[state=open]:s-zoom-in-95 data-[side=bottom]:s-slide-in-from-top-2 data-[side=left]:s-slide-in-from-right-2 data-[side=right]:s-slide-in-from-left-2 data-[side=top]:s-slide-in-from-bottom-2" ), item: cva( cn( "s-relative s-flex s-gap-2 s-cursor-pointer s-select-none s-items-center s-outline-none s-rounded-md s-heading-sm s-transition-colors s-duration-300 data-[disabled]:s-pointer-events-none", "data-[disabled]:s-text-primary-400 dark:data-[disabled]:s-text-primary-400-night" ), { variants: { variant: { default: cn( "s-p-2", "hover:s-bg-muted-background dark:hover:s-bg-muted-night", "focus:s-text-foreground dark:focus:s-text-foreground-night", "focus:s-bg-muted-background dark:focus:s-bg-muted-night" ), tags: cn( "s-p-0.5", "hover:s-bg-muted-background dark:hover:s-bg-muted-night", "focus:s-text-foreground dark:focus:s-text-foreground-night", "focus:s-bg-muted-background dark:focus:s-bg-muted-night" ), warning: cn( "s-p-2", "s-text-warning-500 dark:s-text-warning-500-night", "hover:s-bg-warning-50 dark:hover:s-bg-warning-50-night", "focus:s-bg-warning-50 dark:focus:s-bg-warning-50-night", "active:s-bg-warning-100 dark:active:s-bg-warning-100-night" ), }, }, defaultVariants: { variant: "default", }, } ), subTrigger: { default: cn( "s-mr-1 s-ml-auto s-tracking-widest", "s-text-primary-400 dark:s-text-primary-400-night" ), span: "s-absolute s-left-2 s-flex s-h-3.5 s-w-3.5 s-items-center s-justify-center", }, label: cn( "s-px-2 s-py-2 s-heading-xs", "s-text-muted-foreground dark:s-text-muted-foreground-night" ), description: cn( "s-grow s-truncate s-text-xs s-font-normal", "s-text-muted-foreground dark:s-text-muted-foreground-night" ), separator: cn( "-s-mx-1 s-my-1 s-h-px", "s-bg-separator dark:s-bg-separator-night" ), shortcut: cn( "s-ml-auto s-text-xs s-tracking-widest", "s-text-primary-400 dark:s-text-primary-400-night" ), }; const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenuGroup = DropdownMenuPrimitive.Group; const DropdownMenuPortal = DropdownMenuPrimitive.Portal; const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; interface LabelAndIconProps { label: string; icon?: React.ComponentType | React.ReactNode; } type Simplify = { [K in keyof T]: T[K] }; type EitherChildrenOrProps = | (BaseProps & ExtraProps & { children?: never }) | (BaseProps & { [K in keyof ExtraProps]?: never }); type MutuallyExclusiveProps = Simplify< EitherChildrenOrProps >; interface ItemWithLabelIconAndDescriptionProps { label?: string; icon?: React.ComponentType | React.ReactNode; description?: string; children?: React.ReactNode; truncate?: boolean; endComponent?: React.ReactNode; } const renderIcon = ( icon: React.ComponentType | React.ReactNode, size: "xs" | "sm" = "xs" ) => { // If it's a React element (already rendered), return it as is if (React.isValidElement(icon)) { return icon; } // For any component type (including exotic components), render it with Icon if (typeof icon === "function" || typeof icon === "object") { return ; } // For primitive values, return null return null; }; const ItemWithLabelIconAndDescription = < T extends ItemWithLabelIconAndDescriptionProps, >({ label, icon, description, truncate, children, endComponent, }: T) => { return ( <> {label && (
{renderIcon(icon, "sm")}
{label} {description && ( {description} )}
{endComponent}
)} {children} ); }; const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, MutuallyExclusiveProps< React.ComponentPropsWithoutRef, LabelAndIconProps > & { inset?: boolean; } >(({ className, label, icon, children, inset, ...props }, ref) => ( } > {children} )); DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )); DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; interface DropdownMenuContentProps extends React.ComponentPropsWithoutRef< typeof DropdownMenuPrimitive.Content > { mountPortal?: boolean; mountPortalContainer?: HTMLElement; dropdownHeaders?: React.ReactNode; preventAutoFocusOnClose?: boolean; onOpenAutoFocus?: (e: React.FocusEvent) => void; } const DropdownMenuContent = React.forwardRef< React.ElementRef, DropdownMenuContentProps >( ( { className, sideOffset = 4, mountPortal = true, mountPortalContainer, dropdownHeaders, preventAutoFocusOnClose = true, onCloseAutoFocus, children, ...props }, ref ) => { const handleCloseAutoFocus = React.useCallback( (event: Event) => { if (preventAutoFocusOnClose) { event.preventDefault(); } onCloseAutoFocus?.(event); }, [preventAutoFocusOnClose, onCloseAutoFocus] ); const content = (
{dropdownHeaders && dropdownHeaders}
{children}
); const [container, setContainer] = React.useState( mountPortalContainer ); React.useEffect(() => { if (mountPortal && !container) { const dialogElements = document.querySelectorAll( ".s-sheet[role=dialog][data-state=open]" ); const defaultContainer = dialogElements[dialogElements.length - 1]; setContainer(defaultContainer); } }, []); return mountPortal ? ( {content} ) : ( content ); } ); DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; export type DropdownMenuItemProps = MutuallyExclusiveProps< React.ComponentPropsWithoutRef & { inset?: boolean; variant?: ItemVariantType; } & Omit, LabelAndIconProps & { description?: string; truncateText?: boolean; endComponent?: React.ReactNode; } >; const DropdownMenuItem = React.forwardRef< React.ElementRef, DropdownMenuItemProps >( ( { children, variant, description, className, inset, icon, truncateText, label, href, target, rel, asChild, replace, shallow, prefetch, endComponent, ...props }, ref ) => { return (
{children}
); } ); DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; export type DropdownMenuCheckboxItemProps = React.ComponentPropsWithoutRef< typeof DropdownMenuPrimitive.CheckboxItem > & { label?: React.ComponentProps["label"]; icon?: React.ComponentProps["icon"]; description?: React.ComponentProps["description"]; truncateText?: React.ComponentProps["truncateText"]; }; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, DropdownMenuCheckboxItemProps >( ( { className, children, description, label, icon, truncateText, ...props }, ref ) => ( {children} ) ); DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, MutuallyExclusiveProps< React.ComponentPropsWithoutRef, LabelAndIconProps & { description?: string } > >(({ className, children, description, label, icon, ...props }, ref) => ( {children} )); DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; interface DropdownMenuTagItemProps extends Omit< DropdownMenuItemProps, "label" | "icon" | "onClick" > { label: string; size?: React.ComponentProps["size"]; color?: React.ComponentProps["color"]; icon?: React.ComponentProps["icon"]; onRemove?: () => void; onClick?: () => void; } const DropdownMenuTagItem = React.forwardRef< HTMLDivElement, DropdownMenuTagItemProps >( ( { label, size = "xs", color = "primary", icon, onRemove, className, onClick, ...props }, ref ) => { return ( ); } ); DropdownMenuTagItem.displayName = "DropdownMenuTagItem"; interface DropdownMenuTagListProps { children: React.ReactNode; className?: string; } const DropdownMenuTagList = React.forwardRef< HTMLDivElement, DropdownMenuTagListProps >(({ children, className }, ref) => { return (
{children}
); }); DropdownMenuTagList.displayName = "DropdownMenuTagList"; const DropdownMenuLabel = React.forwardRef< React.ElementRef, MutuallyExclusiveProps< React.ComponentPropsWithoutRef & { inset?: boolean; }, LabelAndIconProps > >(({ children, className, inset, label, ...props }, ref) => ( {label && <>{label}} {children} )); DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ); }; DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; interface DropdownMenuSearchbarProps extends SearchInputProps { button?: React.ReactNode; autoFocus?: boolean; } const DropdownMenuSearchbar = React.forwardRef< HTMLInputElement, DropdownMenuSearchbarProps >( ( { placeholder, value, onChange, onKeyDown, name, className, disabled = false, button, autoFocus, }, ref ) => { const internalRef = useRef(null); React.useImperativeHandle( ref, () => internalRef.current ); React.useEffect(() => { if (autoFocus) { setTimeout(() => { internalRef.current?.focus(); }, 0); } }, [autoFocus]); const handleKeyDown = (e: React.KeyboardEvent) => { e.stopPropagation(); onKeyDown?.(e); if (!e.defaultPrevented) { if (e.key === "Enter") { e.preventDefault(); const firstItem = document.querySelector( '[data-radix-menu-content][data-state=open] [role="menuitem"]' ); if (firstItem instanceof HTMLElement) { firstItem.click(); } } if (e.key === "Tab" || e.key === "ArrowDown") { e.preventDefault(); const firstItem = document.querySelector( '[data-radix-menu-content][data-state=open] [role="menuitem"]' ); if (firstItem instanceof HTMLElement) { firstItem.focus(); } } } }; return (
{button}
); } ); DropdownMenuSearchbar.displayName = "DropdownMenuSearchbar"; export interface DropdownMenuFilterOption { label: string; value: string; } interface DropdownMenuFiltersProps { filters: DropdownMenuFilterOption[]; selectedValues: string[]; onSelectFilter: (value: string) => void; className?: string; } const DropdownMenuFilters = React.forwardRef< HTMLDivElement, DropdownMenuFiltersProps >(({ filters, selectedValues = [], onSelectFilter, className }, ref) => { const multiSelectionValues = Array.isArray(selectedValues) ? selectedValues : []; return (
{filters.map((filter) => { const isSelected = multiSelectionValues.includes(filter.value); return (
); }); DropdownMenuFilters.displayName = "DropdownMenuFilters"; // DropdownTooltip: Simple tooltip with consistent layout: optional media at top, description below. export interface DropdownTooltipProps { description: string; media?: React.ReactNode; } const DropdownTooltip = ({ description, media }: DropdownTooltipProps) => (
{/* Media at top */} {media &&
{media}
} {/* Description */}

{description}

); DropdownTooltip.displayName = "DropdownTooltip"; export interface DropdownTooltipTriggerProps { children: React.ReactElement; className?: string; description: string; media?: React.ReactNode; mountPortal?: boolean; mountPortalContainer?: HTMLElement; onVisibilityChange?: (visible: boolean) => void; side?: "left" | "right" | "top" | "bottom"; sideOffset?: number; } const DropdownTooltipTrigger = React.forwardRef< React.ElementRef, DropdownTooltipTriggerProps >( ( { children, className, description, media, mountPortal = true, mountPortalContainer, onVisibilityChange, side = "right", sideOffset = 8, }, ref ) => { const tooltipContent = ( ); const [container, setContainer] = React.useState( mountPortalContainer ); React.useEffect(() => { if (mountPortal && !container) { const dialogElements = document.querySelectorAll( ".s-sheet[role=dialog][data-state=open]" ); const defaultContainer = dialogElements[dialogElements.length - 1]; setContainer(defaultContainer); } }, [mountPortal, container]); return ( {children} {mountPortal ? ( {tooltipContent} ) : ( tooltipContent )} ); } ); DropdownTooltipTrigger.displayName = "DropdownTooltipTrigger"; interface DropdownMenuStaticItemProps { label: string; value?: string; children?: React.ReactNode; className?: string; } const DropdownMenuStaticItem = React.forwardRef< HTMLDivElement, DropdownMenuStaticItemProps >(({ label, value, children, className }, ref) => (
{label} {value && ( {value} )} {children &&
{children}
}
)); DropdownMenuStaticItem.displayName = "DropdownMenuStaticItem"; export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuFilters, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSearchbar, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuStaticItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTagItem, DropdownMenuTagList, DropdownMenuTrigger, DropdownTooltipTrigger, };