diff options
| -rw-r--r-- | package-lock.json | 252 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/components/category-selector.tsx | 94 | ||||
| -rw-r--r-- | src/components/expense-form.tsx | 36 | ||||
| -rw-r--r-- | src/components/ui/command.tsx | 155 |
5 files changed, 507 insertions, 31 deletions
diff --git a/package-lock.json b/package-lock.json index fa7d4c9..1398186 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@tailwindcss/typography": "^0.5.10", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^0.2.0", "dayjs": "^1.11.10", "lucide-react": "^0.290.0", "nanoid": "^5.0.4", @@ -2489,6 +2490,252 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-0.2.0.tgz", + "integrity": "sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.0", + "command-score": "0.1.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.0.tgz", + "integrity": "sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.0", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-slot": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", + "integrity": "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz", + "integrity": "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", + "integrity": "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz", + "integrity": "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz", + "integrity": "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2507,6 +2754,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/command-score": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz", + "integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", diff --git a/package.json b/package.json index 29bb41d..3478946 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@tailwindcss/typography": "^0.5.10", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^0.2.0", "dayjs": "^1.11.10", "lucide-react": "^0.290.0", "nanoid": "^5.0.4", diff --git a/src/components/category-selector.tsx b/src/components/category-selector.tsx new file mode 100644 index 0000000..adb6582 --- /dev/null +++ b/src/components/category-selector.tsx @@ -0,0 +1,94 @@ +import { ChevronDown } from 'lucide-react' + +import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { Category } from '@prisma/client' +import { useState } from 'react' + +type Props = { + categories: Category[] + onValueChange: (categoryId: Category['id']) => void + defaultValue: Category['id'] +} + +export function CategorySelector({ + categories, + onValueChange, + defaultValue, +}: Props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState<number>(defaultValue) + + const categoriesByGroup = categories.reduce<Record<string, Category[]>>( + (acc, category) => ({ + ...acc, + [category.grouping]: [...(acc[category.grouping] ?? []), category], + }), + {}, + ) + + const selectedCategory = + categories.find((category) => category.id === value) ?? categories[0] + + return ( + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className="flex w-full justify-between" + > + <div className="flex items-center gap-3"> + <CategoryIcon category={selectedCategory} className="w-4 h-4" /> + {selectedCategory.name} + </div> + <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="p-0" align="start"> + <Command> + <CommandInput placeholder="Search category..." /> + <CommandEmpty>No category found.</CommandEmpty> + <div className="w-full max-h-[300px] overflow-y-auto"> + {Object.entries(categoriesByGroup).map( + ([group, groupCategories], index) => ( + <CommandGroup key={index} heading={group}> + {groupCategories.map((category) => ( + <CommandItem + key={category.id} + value={`${category.id} ${category.grouping} ${category.name}`} + onSelect={(currentValue) => { + const id = Number(currentValue.split(' ')[0]) + setValue(id) + onValueChange(id) + setOpen(false) + }} + > + <div className="flex items-center gap-3"> + <CategoryIcon category={category} className="w-4 h-4" /> + {category.name} + </div> + </CommandItem> + ))} + </CommandGroup> + ), + )} + </div> + </Command> + </PopoverContent> + </Popover> + ) +} diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx index cfc749a..dfaaaa0 100644 --- a/src/components/expense-form.tsx +++ b/src/components/expense-form.tsx @@ -1,6 +1,6 @@ 'use client' -import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' import { AsyncButton } from '@/components/async-button' +import { CategorySelector } from '@/components/category-selector' import { SubmitButton } from '@/components/submit-button' import { Button } from '@/components/ui/button' import { @@ -29,9 +29,7 @@ import { Input } from '@/components/ui/input' import { Select, SelectContent, - SelectGroup, SelectItem, - SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' @@ -227,35 +225,11 @@ export function ExpenseForm({ render={({ field }) => ( <FormItem className="order-3 sm:order-2"> <FormLabel>Category</FormLabel> - <Select + <CategorySelector + categories={categories} + defaultValue={field.value} onValueChange={field.onChange} - defaultValue={field.value.toString()} - > - <SelectTrigger> - <SelectValue placeholder="Select a category" /> - </SelectTrigger> - <SelectContent> - {Object.keys(categoriesByGroup).map((group) => ( - <SelectGroup key={group}> - <SelectLabel className="-ml-6">{group}</SelectLabel> - {categoriesByGroup[group].map((category) => ( - <SelectItem - key={category.id.toString()} - value={category.id.toString()} - > - <div className="flex items-center gap-3"> - <CategoryIcon - category={category} - className="w-4 h-4" - /> - {category.name} - </div> - </SelectItem> - ))} - </SelectGroup> - ))} - </SelectContent> - </Select> + /> <FormDescription> Select the expense category. </FormDescription> diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..17cc641 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef<typeof CommandPrimitive>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive> +>(({ className, ...props }, ref) => ( + <CommandPrimitive + ref={ref} + className={cn( + "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", + className + )} + {...props} + /> +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + <Dialog {...props}> + <DialogContent className="overflow-hidden p-0 shadow-lg"> + <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> + {children} + </Command> + </DialogContent> + </Dialog> + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Input>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> +>(({ className, ...props }, ref) => ( + <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> + <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + <CommandPrimitive.Input + ref={ref} + className={cn( + "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", + className + )} + {...props} + /> + </div> +)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.List>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.List + ref={ref} + className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} + {...props} + /> +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Empty>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> +>((props, ref) => ( + <CommandPrimitive.Empty + ref={ref} + className="py-6 text-center text-sm" + {...props} + /> +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Group>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Group + ref={ref} + className={cn( + "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", + className + )} + {...props} + /> +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Separator + ref={ref} + className={cn("-mx-1 h-px bg-border", className)} + {...props} + /> +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + {...props} + /> +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "ml-auto text-xs tracking-widest text-muted-foreground", + className + )} + {...props} + /> + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} |
