| 1 | "use client"; |
| 2 | import { mergeProps } from "@base-ui/react/merge-props"; |
| 3 | import { Select as SelectPrimitive } from "@base-ui/react/select"; |
| 4 | import { useRender } from "@base-ui/react/use-render"; |
| 5 | import { cva, type VariantProps } from "class-variance-authority"; |
| 6 | import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; |
| 7 | import type * as React from "react"; |
| 8 | |
| 9 | import { cn } from "~/lib/utils"; |
| 10 | |
| 11 | const Select = SelectPrimitive.Root; |
| 12 | |
| 13 | const selectTriggerVariants = cva( |
| 14 | "relative inline-flex cursor-pointer select-none items-center justify-between gap-2 border rounded-lg text-left text-base outline-none transition-[color,box-shadow,background-color] data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4", |
| 15 | { |
| 16 | defaultVariants: { |
| 17 | size: "default", |
| 18 | variant: "default", |
| 19 | }, |
| 20 | variants: { |
| 21 | variant: { |
| 22 | default: |
| 23 | "w-full min-w-36 border-input bg-background not-dark:bg-clip-padding text-foreground shadow-xs/5 ring-ring/24 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-inset aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 dark:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='text-'])]:text-icon-muted [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none", |
| 24 | ghost: |
| 25 | "border-transparent text-secondary-label focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground", |
| 26 | }, |
| 27 | size: { |
| 28 | compact: |
| 29 | "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-xs before:rounded-[calc(var(--radius-md)-1px)] [&_svg:not([class*='size-'])]:size-3.5", |
| 30 | default: "min-h-9 px-[calc(--spacing(3)-1px)] sm:min-h-8", |
| 31 | lg: "min-h-10 px-[calc(--spacing(3)-1px)] sm:min-h-9", |
| 32 | sm: "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7", |
| 33 | xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5", |
| 34 | }, |
| 35 | }, |
| 36 | }, |
| 37 | ); |
| 38 | |
| 39 | function SelectTrigger({ |
| 40 | className, |
| 41 | size = "default", |
| 42 | variant = "default", |
| 43 | children, |
| 44 | icon, |
| 45 | ...props |
| 46 | }: SelectPrimitive.Trigger.Props & |
| 47 | VariantProps<typeof selectTriggerVariants> & { icon?: React.ReactNode }) { |
| 48 | return ( |
| 49 | <SelectPrimitive.Trigger |
| 50 | className={cn(selectTriggerVariants({ size, variant }), className)} |
| 51 | data-slot="select-trigger" |
| 52 | {...props} |
| 53 | > |
| 54 | {children} |
| 55 | <SelectPrimitive.Icon data-slot="select-icon"> |
| 56 | {icon ?? <ChevronDownIcon className="-me-1 size-3 opacity-50" />} |
| 57 | </SelectPrimitive.Icon> |
| 58 | </SelectPrimitive.Trigger> |
| 59 | ); |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * The select-field look for a picker that is not a Select, such as a Menu or |
| 64 | * Combobox trigger. Render it as that trigger: `<MenuTrigger render={<SelectButton />}>`. |
| 65 | */ |
| 66 | function SelectButton({ |
| 67 | className, |
| 68 | size = "default", |
| 69 | children, |
| 70 | render, |
| 71 | ...props |
| 72 | }: useRender.ComponentProps<"button"> & Pick<VariantProps<typeof selectTriggerVariants>, "size">) { |
| 73 | const defaultProps = { |
| 74 | className: cn(selectTriggerVariants({ size }), className), |
| 75 | "data-slot": "select-trigger", |
| 76 | type: render ? undefined : ("button" as const), |
| 77 | }; |
| 78 | return useRender({ |
| 79 | defaultTagName: "button", |
| 80 | props: { |
| 81 | ...mergeProps<"button">(defaultProps, props), |
| 82 | children: ( |
| 83 | <> |
| 84 | <span className="min-w-0 flex-1 truncate text-left">{children}</span> |
| 85 | <ChevronDownIcon aria-hidden className="-me-1 size-3 shrink-0 opacity-50" /> |
| 86 | </> |
| 87 | ), |
| 88 | }, |
| 89 | render, |
| 90 | }); |
| 91 | } |
| 92 | |
| 93 | function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { |
| 94 | return ( |
| 95 | <SelectPrimitive.Value |
| 96 | className={cn("flex-1 truncate data-placeholder:text-placeholder", className)} |
| 97 | data-slot="select-value" |
| 98 | {...props} |
| 99 | /> |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | function SelectPopup({ |
| 104 | className, |
| 105 | children, |
| 106 | side = "bottom", |
| 107 | sideOffset = 4, |
| 108 | align = "start", |
| 109 | alignOffset = 0, |
| 110 | alignItemWithTrigger = true, |
| 111 | matchTriggerWidth = true, |
| 112 | anchor, |
| 113 | ...props |
| 114 | }: SelectPrimitive.Popup.Props & { |
| 115 | side?: SelectPrimitive.Positioner.Props["side"]; |
| 116 | sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"]; |
| 117 | align?: SelectPrimitive.Positioner.Props["align"]; |
| 118 | alignOffset?: SelectPrimitive.Positioner.Props["alignOffset"]; |
| 119 | alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"]; |
| 120 | matchTriggerWidth?: boolean; |
| 121 | anchor?: SelectPrimitive.Positioner.Props["anchor"]; |
| 122 | }) { |
| 123 | return ( |
| 124 | <SelectPrimitive.Portal> |
| 125 | <SelectPrimitive.Positioner |
| 126 | align={align} |
| 127 | alignItemWithTrigger={alignItemWithTrigger} |
| 128 | alignOffset={alignOffset} |
| 129 | anchor={anchor} |
| 130 | className="z-[130] select-none" |
| 131 | data-slot="select-positioner" |
| 132 | side={side} |
| 133 | sideOffset={sideOffset} |
| 134 | > |
| 135 | <SelectPrimitive.Popup |
| 136 | className="origin-(--transform-origin) rounded-lg text-foreground outline-none" |
| 137 | data-slot="select-popup" |
| 138 | {...props} |
| 139 | > |
| 140 | <SelectPrimitive.ScrollUpArrow |
| 141 | className="top-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:top-px before:h-[200%] before:rounded-t-[calc(var(--radius-lg)-1px)] before:bg-linear-to-b before:from-50% before:from-popover" |
| 142 | data-slot="select-scroll-up-arrow" |
| 143 | > |
| 144 | <ChevronUpIcon className="relative size-4.5 sm:size-4" /> |
| 145 | </SelectPrimitive.ScrollUpArrow> |
| 146 | <div |
| 147 | className={cn( |
| 148 | "dropdown-glass relative h-full rounded-lg shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]", |
| 149 | matchTriggerWidth && "min-w-(--anchor-width)", |
| 150 | )} |
| 151 | > |
| 152 | <SelectPrimitive.List |
| 153 | className={cn("max-h-(--available-height) overflow-y-auto p-1", className)} |
| 154 | data-slot="select-list" |
| 155 | > |
| 156 | {children} |
| 157 | </SelectPrimitive.List> |
| 158 | </div> |
| 159 | <SelectPrimitive.ScrollDownArrow |
| 160 | className="bottom-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:bottom-px before:h-[200%] before:rounded-b-[calc(var(--radius-lg)-1px)] before:bg-linear-to-t before:from-50% before:from-popover" |
| 161 | data-slot="select-scroll-down-arrow" |
| 162 | > |
| 163 | <ChevronDownIcon className="relative size-4.5 sm:size-4" /> |
| 164 | </SelectPrimitive.ScrollDownArrow> |
| 165 | </SelectPrimitive.Popup> |
| 166 | </SelectPrimitive.Positioner> |
| 167 | </SelectPrimitive.Portal> |
| 168 | ); |
| 169 | } |
| 170 | |
| 171 | function SelectItem({ |
| 172 | className, |
| 173 | children, |
| 174 | hideIndicator: _hideIndicator = false, |
| 175 | ...props |
| 176 | }: SelectPrimitive.Item.Props & { |
| 177 | hideIndicator?: boolean; |
| 178 | }) { |
| 179 | return ( |
| 180 | <SelectPrimitive.Item |
| 181 | className={cn( |
| 182 | "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none data-selected:bg-foreground/[0.08] data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", |
| 183 | className, |
| 184 | )} |
| 185 | data-slot="select-item" |
| 186 | {...props} |
| 187 | > |
| 188 | <SelectPrimitive.ItemText |
| 189 | className="min-w-0 flex-1 [&_svg:not([class*='text-'])]:text-muted-foreground" |
| 190 | data-slot="select-item-text" |
| 191 | > |
| 192 | {children} |
| 193 | </SelectPrimitive.ItemText> |
| 194 | </SelectPrimitive.Item> |
| 195 | ); |
| 196 | } |
| 197 | |
| 198 | function SelectGroup(props: SelectPrimitive.Group.Props) { |
| 199 | return <SelectPrimitive.Group data-slot="select-group" {...props} />; |
| 200 | } |
| 201 | |
| 202 | function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) { |
| 203 | return ( |
| 204 | <SelectPrimitive.GroupLabel |
| 205 | className="px-2 py-1.5 font-medium text-muted-foreground text-xs" |
| 206 | data-slot="select-group-label" |
| 207 | {...props} |
| 208 | /> |
| 209 | ); |
| 210 | } |
| 211 | |
| 212 | export { |
| 213 | Select, |
| 214 | SelectTrigger, |
| 215 | SelectButton, |
| 216 | SelectValue, |
| 217 | SelectPopup, |
| 218 | SelectPopup as SelectContent, |
| 219 | SelectItem, |
| 220 | SelectGroup, |
| 221 | SelectGroupLabel, |
| 222 | }; |