Files
stats/apps/start/src/components/ui/combobox.tsx
Carl-Gerhard Lindesvärd 81a7e5d62e feat: dashboard v2, esm, upgrades (#211)
* esm

* wip

* wip

* wip

* wip

* wip

* wip

* subscription notice

* wip

* wip

* wip

* fix envs

* fix: update docker build

* fix

* esm/types

* delete dashboard :D

* add patches to dockerfiles

* update packages + catalogs + ts

* wip

* remove native libs

* ts

* improvements

* fix redirects and fetching session

* try fix favicon

* fixes

* fix

* order and resize reportds within a dashboard

* improvements

* wip

* added userjot to dashboard

* fix

* add op

* wip

* different cache key

* improve date picker

* fix table

* event details loading

* redo onboarding completely

* fix login

* fix

* fix

* extend session, billing and improve bars

* fix

* reduce price on 10M
2025-10-16 12:27:44 +02:00

166 lines
4.6 KiB
TypeScript

import type { ButtonProps } from '@/components/ui/button';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
} from '@/components/ui/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/utils/cn';
import { PopoverPortal } from '@radix-ui/react-popover';
import type { LucideIcon } from 'lucide-react';
import { Check, ChevronsUpDown } from 'lucide-react';
import VirtualList from 'rc-virtual-list';
import * as React from 'react';
export interface ComboboxProps<T> {
placeholder: string;
items: {
value: T;
label: string;
disabled?: boolean;
}[];
value: T | null | undefined;
onChange: (value: T) => void;
children?: React.ReactNode;
onCreate?: (value: T) => void;
className?: string;
searchable?: boolean;
icon?: LucideIcon;
size?: ButtonProps['size'];
label?: string;
align?: 'start' | 'end' | 'center';
portal?: boolean;
error?: string;
disabled?: boolean;
}
export type ExtendedComboboxProps<T> = Omit<
ComboboxProps<T>,
'items' | 'placeholder'
> & {
placeholder?: string;
};
export function Combobox<T extends string>({
placeholder,
items,
value,
onChange,
children,
onCreate,
className,
searchable,
icon: Icon,
size,
align = 'start',
portal,
error,
disabled,
}: ComboboxProps<T>) {
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
function find(value: string) {
return items.find(
(item) => item.value.toLowerCase() === value.toLowerCase(),
);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{children ?? (
<Button
disabled={disabled}
size={size}
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
'justify-between',
!!error && 'border-destructive',
className,
)}
>
<div className="flex min-w-0 items-center">
{Icon ? <Icon size={16} className="mr-2 shrink-0" /> : null}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{value ? (find(value)?.label ?? 'No match') : placeholder}
</span>
</div>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
)}
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
className="w-full max-w-md p-0"
align={align}
portal={portal}
>
<Command shouldFilter={false}>
{searchable === true && (
<CommandInput
placeholder="Search item..."
value={search}
onValueChange={setSearch}
/>
)}
{typeof onCreate === 'function' && search ? (
<CommandEmpty className="p-2">
<Button
onClick={() => {
onCreate(search as T);
setSearch('');
setOpen(false);
}}
>
Create &quot;{search}&quot;
</Button>
</CommandEmpty>
) : (
<CommandEmpty>Nothing selected</CommandEmpty>
)}
<VirtualList
height={Math.min(items.length * 32, 300)}
data={items.filter((item) => {
if (search === '') return true;
return item.label.toLowerCase().includes(search.toLowerCase());
})}
itemHeight={32}
itemKey="value"
className="min-w-60"
>
{(item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={(currentValue) => {
const value = find(currentValue)?.value ?? currentValue;
onChange(value as T);
setOpen(false);
}}
{...(item.disabled && { disabled: true })}
>
<Check
className={cn(
'mr-2 h-4 w-4 flex-shrink-0',
value === item.value ? 'opacity-100' : 'opacity-0',
)}
/>
{item.label}
</CommandItem>
)}
</VirtualList>
</Command>
</PopoverContent>
</PopoverPortal>
</Popover>
);
}