diff options
Diffstat (limited to 'frontend/components/ui')
| -rw-r--r-- | frontend/components/ui/badge.tsx | 46 | ||||
| -rw-r--r-- | frontend/components/ui/button.tsx | 59 | ||||
| -rw-r--r-- | frontend/components/ui/checkbox.tsx | 47 | ||||
| -rw-r--r-- | frontend/components/ui/dialog.tsx | 143 | ||||
| -rw-r--r-- | frontend/components/ui/dropdown-menu.tsx | 257 | ||||
| -rw-r--r-- | frontend/components/ui/input.tsx | 21 | ||||
| -rw-r--r-- | frontend/components/ui/label.tsx | 24 | ||||
| -rw-r--r-- | frontend/components/ui/progress.tsx | 31 | ||||
| -rw-r--r-- | frontend/components/ui/table.tsx | 116 | ||||
| -rw-r--r-- | frontend/components/ui/toast.tsx | 129 | ||||
| -rw-r--r-- | frontend/components/ui/toaster.tsx | 35 |
11 files changed, 908 insertions, 0 deletions
diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/frontend/components/ui/badge.tsx | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | import * as React from "react" | ||
| 2 | import { Slot } from "@radix-ui/react-slot" | ||
| 3 | import { cva, type VariantProps } from "class-variance-authority" | ||
| 4 | |||
| 5 | import { cn } from "@/lib/utils" | ||
| 6 | |||
| 7 | const badgeVariants = cva( | ||
| 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", | ||
| 9 | { | ||
| 10 | variants: { | ||
| 11 | variant: { | ||
| 12 | default: | ||
| 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", | ||
| 14 | secondary: | ||
| 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", | ||
| 16 | destructive: | ||
| 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", | ||
| 18 | outline: | ||
| 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", | ||
| 20 | }, | ||
| 21 | }, | ||
| 22 | defaultVariants: { | ||
| 23 | variant: "default", | ||
| 24 | }, | ||
| 25 | } | ||
| 26 | ) | ||
| 27 | |||
| 28 | function Badge({ | ||
| 29 | className, | ||
| 30 | variant, | ||
| 31 | asChild = false, | ||
| 32 | ...props | ||
| 33 | }: React.ComponentProps<"span"> & | ||
| 34 | VariantProps<typeof badgeVariants> & { asChild?: boolean }) { | ||
| 35 | const Comp = asChild ? Slot : "span" | ||
| 36 | |||
| 37 | return ( | ||
| 38 | <Comp | ||
| 39 | data-slot="badge" | ||
| 40 | className={cn(badgeVariants({ variant }), className)} | ||
| 41 | {...props} | ||
| 42 | /> | ||
| 43 | ) | ||
| 44 | } | ||
| 45 | |||
| 46 | export { Badge, badgeVariants } | ||
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/frontend/components/ui/button.tsx | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | import * as React from "react" | ||
| 2 | import { Slot } from "@radix-ui/react-slot" | ||
| 3 | import { cva, type VariantProps } from "class-variance-authority" | ||
| 4 | |||
| 5 | import { cn } from "@/lib/utils" | ||
| 6 | |||
| 7 | const buttonVariants = cva( | ||
| 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||
| 9 | { | ||
| 10 | variants: { | ||
| 11 | variant: { | ||
| 12 | default: | ||
| 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", | ||
| 14 | destructive: | ||
| 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", | ||
| 16 | outline: | ||
| 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", | ||
| 18 | secondary: | ||
| 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", | ||
| 20 | ghost: | ||
| 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", | ||
| 22 | link: "text-primary underline-offset-4 hover:underline", | ||
| 23 | }, | ||
| 24 | size: { | ||
| 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", | ||
| 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", | ||
| 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", | ||
| 28 | icon: "size-9", | ||
| 29 | }, | ||
| 30 | }, | ||
| 31 | defaultVariants: { | ||
| 32 | variant: "default", | ||
| 33 | size: "default", | ||
| 34 | }, | ||
| 35 | } | ||
| 36 | ) | ||
| 37 | |||
| 38 | function Button({ | ||
| 39 | className, | ||
| 40 | variant, | ||
| 41 | size, | ||
| 42 | asChild = false, | ||
| 43 | ...props | ||
| 44 | }: React.ComponentProps<"button"> & | ||
| 45 | VariantProps<typeof buttonVariants> & { | ||
| 46 | asChild?: boolean | ||
| 47 | }) { | ||
| 48 | const Comp = asChild ? Slot : "button" | ||
| 49 | |||
| 50 | return ( | ||
| 51 | <Comp | ||
| 52 | data-slot="button" | ||
| 53 | className={cn(buttonVariants({ variant, size, className }))} | ||
| 54 | {...props} | ||
| 55 | /> | ||
| 56 | ) | ||
| 57 | } | ||
| 58 | |||
| 59 | export { Button, buttonVariants } | ||
diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx new file mode 100644 index 0000000..b0f1ccf --- /dev/null +++ b/frontend/components/ui/checkbox.tsx | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" | ||
| 5 | import { CheckIcon } from "lucide-react" | ||
| 6 | |||
| 7 | import { cn } from "@/lib/utils" | ||
| 8 | |||
| 9 | const Checkbox = React.forwardRef< | ||
| 10 | React.ElementRef<typeof CheckboxPrimitive.Root>, | ||
| 11 | React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & { | ||
| 12 | indeterminate?: boolean | ||
| 13 | } | ||
| 14 | >(({ className, indeterminate, ...props }, ref) => { | ||
| 15 | const checkboxRef = React.useRef<HTMLButtonElement>(null) | ||
| 16 | |||
| 17 | React.useImperativeHandle(ref, () => checkboxRef.current!) | ||
| 18 | |||
| 19 | React.useEffect(() => { | ||
| 20 | if (checkboxRef.current) { | ||
| 21 | checkboxRef.current.indeterminate = indeterminate ?? false | ||
| 22 | } | ||
| 23 | }, [indeterminate]) | ||
| 24 | |||
| 25 | return ( | ||
| 26 | <CheckboxPrimitive.Root | ||
| 27 | ref={checkboxRef} | ||
| 28 | data-slot="checkbox" | ||
| 29 | className={cn( | ||
| 30 | "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", | ||
| 31 | className | ||
| 32 | )} | ||
| 33 | {...props} | ||
| 34 | > | ||
| 35 | <CheckboxPrimitive.Indicator | ||
| 36 | data-slot="checkbox-indicator" | ||
| 37 | className="flex items-center justify-center text-current transition-none" | ||
| 38 | > | ||
| 39 | <CheckIcon className="size-3.5" /> | ||
| 40 | </CheckboxPrimitive.Indicator> | ||
| 41 | </CheckboxPrimitive.Root> | ||
| 42 | ) | ||
| 43 | }) | ||
| 44 | |||
| 45 | Checkbox.displayName = "Checkbox" | ||
| 46 | |||
| 47 | export { Checkbox } | ||
diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/frontend/components/ui/dialog.tsx | |||
| @@ -0,0 +1,143 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" | ||
| 5 | import { XIcon } from "lucide-react" | ||
| 6 | |||
| 7 | import { cn } from "@/lib/utils" | ||
| 8 | |||
| 9 | function Dialog({ | ||
| 10 | ...props | ||
| 11 | }: React.ComponentProps<typeof DialogPrimitive.Root>) { | ||
| 12 | return <DialogPrimitive.Root data-slot="dialog" {...props} /> | ||
| 13 | } | ||
| 14 | |||
| 15 | function DialogTrigger({ | ||
| 16 | ...props | ||
| 17 | }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { | ||
| 18 | return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> | ||
| 19 | } | ||
| 20 | |||
| 21 | function DialogPortal({ | ||
| 22 | ...props | ||
| 23 | }: React.ComponentProps<typeof DialogPrimitive.Portal>) { | ||
| 24 | return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> | ||
| 25 | } | ||
| 26 | |||
| 27 | function DialogClose({ | ||
| 28 | ...props | ||
| 29 | }: React.ComponentProps<typeof DialogPrimitive.Close>) { | ||
| 30 | return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> | ||
| 31 | } | ||
| 32 | |||
| 33 | function DialogOverlay({ | ||
| 34 | className, | ||
| 35 | ...props | ||
| 36 | }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { | ||
| 37 | return ( | ||
| 38 | <DialogPrimitive.Overlay | ||
| 39 | data-slot="dialog-overlay" | ||
| 40 | className={cn( | ||
| 41 | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", | ||
| 42 | className | ||
| 43 | )} | ||
| 44 | {...props} | ||
| 45 | /> | ||
| 46 | ) | ||
| 47 | } | ||
| 48 | |||
| 49 | function DialogContent({ | ||
| 50 | className, | ||
| 51 | children, | ||
| 52 | showCloseButton = true, | ||
| 53 | ...props | ||
| 54 | }: React.ComponentProps<typeof DialogPrimitive.Content> & { | ||
| 55 | showCloseButton?: boolean | ||
| 56 | }) { | ||
| 57 | return ( | ||
| 58 | <DialogPortal data-slot="dialog-portal"> | ||
| 59 | <DialogOverlay /> | ||
| 60 | <DialogPrimitive.Content | ||
| 61 | data-slot="dialog-content" | ||
| 62 | className={cn( | ||
| 63 | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", | ||
| 64 | className | ||
| 65 | )} | ||
| 66 | {...props} | ||
| 67 | > | ||
| 68 | {children} | ||
| 69 | {showCloseButton && ( | ||
| 70 | <DialogPrimitive.Close | ||
| 71 | data-slot="dialog-close" | ||
| 72 | className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" | ||
| 73 | > | ||
| 74 | <XIcon /> | ||
| 75 | <span className="sr-only">Close</span> | ||
| 76 | </DialogPrimitive.Close> | ||
| 77 | )} | ||
| 78 | </DialogPrimitive.Content> | ||
| 79 | </DialogPortal> | ||
| 80 | ) | ||
| 81 | } | ||
| 82 | |||
| 83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { | ||
| 84 | return ( | ||
| 85 | <div | ||
| 86 | data-slot="dialog-header" | ||
| 87 | className={cn("flex flex-col gap-2 text-center sm:text-left", className)} | ||
| 88 | {...props} | ||
| 89 | /> | ||
| 90 | ) | ||
| 91 | } | ||
| 92 | |||
| 93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { | ||
| 94 | return ( | ||
| 95 | <div | ||
| 96 | data-slot="dialog-footer" | ||
| 97 | className={cn( | ||
| 98 | "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", | ||
| 99 | className | ||
| 100 | )} | ||
| 101 | {...props} | ||
| 102 | /> | ||
| 103 | ) | ||
| 104 | } | ||
| 105 | |||
| 106 | function DialogTitle({ | ||
| 107 | className, | ||
| 108 | ...props | ||
| 109 | }: React.ComponentProps<typeof DialogPrimitive.Title>) { | ||
| 110 | return ( | ||
| 111 | <DialogPrimitive.Title | ||
| 112 | data-slot="dialog-title" | ||
| 113 | className={cn("text-lg leading-none font-semibold", className)} | ||
| 114 | {...props} | ||
| 115 | /> | ||
| 116 | ) | ||
| 117 | } | ||
| 118 | |||
| 119 | function DialogDescription({ | ||
| 120 | className, | ||
| 121 | ...props | ||
| 122 | }: React.ComponentProps<typeof DialogPrimitive.Description>) { | ||
| 123 | return ( | ||
| 124 | <DialogPrimitive.Description | ||
| 125 | data-slot="dialog-description" | ||
| 126 | className={cn("text-muted-foreground text-sm", className)} | ||
| 127 | {...props} | ||
| 128 | /> | ||
| 129 | ) | ||
| 130 | } | ||
| 131 | |||
| 132 | export { | ||
| 133 | Dialog, | ||
| 134 | DialogClose, | ||
| 135 | DialogContent, | ||
| 136 | DialogDescription, | ||
| 137 | DialogFooter, | ||
| 138 | DialogHeader, | ||
| 139 | DialogOverlay, | ||
| 140 | DialogPortal, | ||
| 141 | DialogTitle, | ||
| 142 | DialogTrigger, | ||
| 143 | } | ||
diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..ec51e9c --- /dev/null +++ b/frontend/components/ui/dropdown-menu.tsx | |||
| @@ -0,0 +1,257 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" | ||
| 5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" | ||
| 6 | |||
| 7 | import { cn } from "@/lib/utils" | ||
| 8 | |||
| 9 | function DropdownMenu({ | ||
| 10 | ...props | ||
| 11 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { | ||
| 12 | return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> | ||
| 13 | } | ||
| 14 | |||
| 15 | function DropdownMenuPortal({ | ||
| 16 | ...props | ||
| 17 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { | ||
| 18 | return ( | ||
| 19 | <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> | ||
| 20 | ) | ||
| 21 | } | ||
| 22 | |||
| 23 | function DropdownMenuTrigger({ | ||
| 24 | ...props | ||
| 25 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { | ||
| 26 | return ( | ||
| 27 | <DropdownMenuPrimitive.Trigger | ||
| 28 | data-slot="dropdown-menu-trigger" | ||
| 29 | {...props} | ||
| 30 | /> | ||
| 31 | ) | ||
| 32 | } | ||
| 33 | |||
| 34 | function DropdownMenuContent({ | ||
| 35 | className, | ||
| 36 | sideOffset = 4, | ||
| 37 | ...props | ||
| 38 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { | ||
| 39 | return ( | ||
| 40 | <DropdownMenuPrimitive.Portal> | ||
| 41 | <DropdownMenuPrimitive.Content | ||
| 42 | data-slot="dropdown-menu-content" | ||
| 43 | sideOffset={sideOffset} | ||
| 44 | className={cn( | ||
| 45 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", | ||
| 46 | className | ||
| 47 | )} | ||
| 48 | {...props} | ||
| 49 | /> | ||
| 50 | </DropdownMenuPrimitive.Portal> | ||
| 51 | ) | ||
| 52 | } | ||
| 53 | |||
| 54 | function DropdownMenuGroup({ | ||
| 55 | ...props | ||
| 56 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { | ||
| 57 | return ( | ||
| 58 | <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> | ||
| 59 | ) | ||
| 60 | } | ||
| 61 | |||
| 62 | function DropdownMenuItem({ | ||
| 63 | className, | ||
| 64 | inset, | ||
| 65 | variant = "default", | ||
| 66 | ...props | ||
| 67 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { | ||
| 68 | inset?: boolean | ||
| 69 | variant?: "default" | "destructive" | ||
| 70 | }) { | ||
| 71 | return ( | ||
| 72 | <DropdownMenuPrimitive.Item | ||
| 73 | data-slot="dropdown-menu-item" | ||
| 74 | data-inset={inset} | ||
| 75 | data-variant={variant} | ||
| 76 | className={cn( | ||
| 77 | "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | ||
| 78 | className | ||
| 79 | )} | ||
| 80 | {...props} | ||
| 81 | /> | ||
| 82 | ) | ||
| 83 | } | ||
| 84 | |||
| 85 | function DropdownMenuCheckboxItem({ | ||
| 86 | className, | ||
| 87 | children, | ||
| 88 | checked, | ||
| 89 | ...props | ||
| 90 | }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { | ||
| 91 | return ( | ||
| 92 | <DropdownMenuPrimitive.CheckboxItem | ||
| 93 | data-slot="dropdown-menu-checkbox-item" | ||
| 94 | className={cn( | ||
| 95 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | ||
| 96 | className | ||
| 97 | )} | ||
| 98 | checked={checked} | ||
| 99 | {...props} | ||
| 100 | > | ||
| 101 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> | ||
| 102 | <DropdownMenuPrimitive.ItemIndicator> | ||
| 103 | <CheckIcon className="size-4" /> | ||
| 104 | </DropdownMenuPrimitive.ItemIndicator> | ||
| 105 | </span> | ||
| 106 | {children} | ||
| 107 | </DropdownMenuPrimitive.CheckboxItem> | ||
| 108 | ) | ||
| 109 | } | ||
| 110 | |||
| 111 | function DropdownMenuRadioGroup({ | ||
| 112 | ...props | ||
| 113 | }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { | ||
| 114 | return ( | ||
| 115 | <DropdownMenuPrimitive.RadioGroup | ||
| 116 | data-slot="dropdown-menu-radio-group" | ||
| 117 | {...props} | ||
| 118 | /> | ||
| 119 | ) | ||
| 120 | } | ||
| 121 | |||
| 122 | function DropdownMenuRadioItem({ | ||
| 123 | className, | ||
| 124 | children, | ||
| 125 | ...props | ||
| 126 | }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { | ||
| 127 | return ( | ||
| 128 | <DropdownMenuPrimitive.RadioItem | ||
| 129 | data-slot="dropdown-menu-radio-item" | ||
| 130 | className={cn( | ||
| 131 | "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | ||
| 132 | className | ||
| 133 | )} | ||
| 134 | {...props} | ||
| 135 | > | ||
| 136 | <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> | ||
| 137 | <DropdownMenuPrimitive.ItemIndicator> | ||
| 138 | <CircleIcon className="size-2 fill-current" /> | ||
| 139 | </DropdownMenuPrimitive.ItemIndicator> | ||
| 140 | </span> | ||
| 141 | {children} | ||
| 142 | </DropdownMenuPrimitive.RadioItem> | ||
| 143 | ) | ||
| 144 | } | ||
| 145 | |||
| 146 | function DropdownMenuLabel({ | ||
| 147 | className, | ||
| 148 | inset, | ||
| 149 | ...props | ||
| 150 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { | ||
| 151 | inset?: boolean | ||
| 152 | }) { | ||
| 153 | return ( | ||
| 154 | <DropdownMenuPrimitive.Label | ||
| 155 | data-slot="dropdown-menu-label" | ||
| 156 | data-inset={inset} | ||
| 157 | className={cn( | ||
| 158 | "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", | ||
| 159 | className | ||
| 160 | )} | ||
| 161 | {...props} | ||
| 162 | /> | ||
| 163 | ) | ||
| 164 | } | ||
| 165 | |||
| 166 | function DropdownMenuSeparator({ | ||
| 167 | className, | ||
| 168 | ...props | ||
| 169 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { | ||
| 170 | return ( | ||
| 171 | <DropdownMenuPrimitive.Separator | ||
| 172 | data-slot="dropdown-menu-separator" | ||
| 173 | className={cn("bg-border -mx-1 my-1 h-px", className)} | ||
| 174 | {...props} | ||
| 175 | /> | ||
| 176 | ) | ||
| 177 | } | ||
| 178 | |||
| 179 | function DropdownMenuShortcut({ | ||
| 180 | className, | ||
| 181 | ...props | ||
| 182 | }: React.ComponentProps<"span">) { | ||
| 183 | return ( | ||
| 184 | <span | ||
| 185 | data-slot="dropdown-menu-shortcut" | ||
| 186 | className={cn( | ||
| 187 | "text-muted-foreground ml-auto text-xs tracking-widest", | ||
| 188 | className | ||
| 189 | )} | ||
| 190 | {...props} | ||
| 191 | /> | ||
| 192 | ) | ||
| 193 | } | ||
| 194 | |||
| 195 | function DropdownMenuSub({ | ||
| 196 | ...props | ||
| 197 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { | ||
| 198 | return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> | ||
| 199 | } | ||
| 200 | |||
| 201 | function DropdownMenuSubTrigger({ | ||
| 202 | className, | ||
| 203 | inset, | ||
| 204 | children, | ||
| 205 | ...props | ||
| 206 | }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { | ||
| 207 | inset?: boolean | ||
| 208 | }) { | ||
| 209 | return ( | ||
| 210 | <DropdownMenuPrimitive.SubTrigger | ||
| 211 | data-slot="dropdown-menu-sub-trigger" | ||
| 212 | data-inset={inset} | ||
| 213 | className={cn( | ||
| 214 | "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", | ||
| 215 | className | ||
| 216 | )} | ||
| 217 | {...props} | ||
| 218 | > | ||
| 219 | {children} | ||
| 220 | <ChevronRightIcon className="ml-auto size-4" /> | ||
| 221 | </DropdownMenuPrimitive.SubTrigger> | ||
| 222 | ) | ||
| 223 | } | ||
| 224 | |||
| 225 | function DropdownMenuSubContent({ | ||
| 226 | className, | ||
| 227 | ...props | ||
| 228 | }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { | ||
| 229 | return ( | ||
| 230 | <DropdownMenuPrimitive.SubContent | ||
| 231 | data-slot="dropdown-menu-sub-content" | ||
| 232 | className={cn( | ||
| 233 | "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", | ||
| 234 | className | ||
| 235 | )} | ||
| 236 | {...props} | ||
| 237 | /> | ||
| 238 | ) | ||
| 239 | } | ||
| 240 | |||
| 241 | export { | ||
| 242 | DropdownMenu, | ||
| 243 | DropdownMenuPortal, | ||
| 244 | DropdownMenuTrigger, | ||
| 245 | DropdownMenuContent, | ||
| 246 | DropdownMenuGroup, | ||
| 247 | DropdownMenuLabel, | ||
| 248 | DropdownMenuItem, | ||
| 249 | DropdownMenuCheckboxItem, | ||
| 250 | DropdownMenuRadioGroup, | ||
| 251 | DropdownMenuRadioItem, | ||
| 252 | DropdownMenuSeparator, | ||
| 253 | DropdownMenuShortcut, | ||
| 254 | DropdownMenuSub, | ||
| 255 | DropdownMenuSubTrigger, | ||
| 256 | DropdownMenuSubContent, | ||
| 257 | } | ||
diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/frontend/components/ui/input.tsx | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | import * as React from "react" | ||
| 2 | |||
| 3 | import { cn } from "@/lib/utils" | ||
| 4 | |||
| 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { | ||
| 6 | return ( | ||
| 7 | <input | ||
| 8 | type={type} | ||
| 9 | data-slot="input" | ||
| 10 | className={cn( | ||
| 11 | "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", | ||
| 12 | "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", | ||
| 13 | "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||
| 14 | className | ||
| 15 | )} | ||
| 16 | {...props} | ||
| 17 | /> | ||
| 18 | ) | ||
| 19 | } | ||
| 20 | |||
| 21 | export { Input } | ||
diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/frontend/components/ui/label.tsx | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as LabelPrimitive from "@radix-ui/react-label" | ||
| 5 | |||
| 6 | import { cn } from "@/lib/utils" | ||
| 7 | |||
| 8 | function Label({ | ||
| 9 | className, | ||
| 10 | ...props | ||
| 11 | }: React.ComponentProps<typeof LabelPrimitive.Root>) { | ||
| 12 | return ( | ||
| 13 | <LabelPrimitive.Root | ||
| 14 | data-slot="label" | ||
| 15 | className={cn( | ||
| 16 | "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", | ||
| 17 | className | ||
| 18 | )} | ||
| 19 | {...props} | ||
| 20 | /> | ||
| 21 | ) | ||
| 22 | } | ||
| 23 | |||
| 24 | export { Label } | ||
diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx new file mode 100644 index 0000000..e7a416c --- /dev/null +++ b/frontend/components/ui/progress.tsx | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" | ||
| 5 | |||
| 6 | import { cn } from "@/lib/utils" | ||
| 7 | |||
| 8 | function Progress({ | ||
| 9 | className, | ||
| 10 | value, | ||
| 11 | ...props | ||
| 12 | }: React.ComponentProps<typeof ProgressPrimitive.Root>) { | ||
| 13 | return ( | ||
| 14 | <ProgressPrimitive.Root | ||
| 15 | data-slot="progress" | ||
| 16 | className={cn( | ||
| 17 | "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", | ||
| 18 | className | ||
| 19 | )} | ||
| 20 | {...props} | ||
| 21 | > | ||
| 22 | <ProgressPrimitive.Indicator | ||
| 23 | data-slot="progress-indicator" | ||
| 24 | className="bg-primary h-full w-full flex-1 transition-all" | ||
| 25 | style={{ transform: `translateX(-${100 - (value || 0)}%)` }} | ||
| 26 | /> | ||
| 27 | </ProgressPrimitive.Root> | ||
| 28 | ) | ||
| 29 | } | ||
| 30 | |||
| 31 | export { Progress } | ||
diff --git a/frontend/components/ui/table.tsx b/frontend/components/ui/table.tsx new file mode 100644 index 0000000..51b74dd --- /dev/null +++ b/frontend/components/ui/table.tsx | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | |||
| 5 | import { cn } from "@/lib/utils" | ||
| 6 | |||
| 7 | function Table({ className, ...props }: React.ComponentProps<"table">) { | ||
| 8 | return ( | ||
| 9 | <div | ||
| 10 | data-slot="table-container" | ||
| 11 | className="relative w-full overflow-x-auto" | ||
| 12 | > | ||
| 13 | <table | ||
| 14 | data-slot="table" | ||
| 15 | className={cn("w-full caption-bottom text-sm", className)} | ||
| 16 | {...props} | ||
| 17 | /> | ||
| 18 | </div> | ||
| 19 | ) | ||
| 20 | } | ||
| 21 | |||
| 22 | function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { | ||
| 23 | return ( | ||
| 24 | <thead | ||
| 25 | data-slot="table-header" | ||
| 26 | className={cn("[&_tr]:border-b", className)} | ||
| 27 | {...props} | ||
| 28 | /> | ||
| 29 | ) | ||
| 30 | } | ||
| 31 | |||
| 32 | function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { | ||
| 33 | return ( | ||
| 34 | <tbody | ||
| 35 | data-slot="table-body" | ||
| 36 | className={cn("[&_tr:last-child]:border-0", className)} | ||
| 37 | {...props} | ||
| 38 | /> | ||
| 39 | ) | ||
| 40 | } | ||
| 41 | |||
| 42 | function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { | ||
| 43 | return ( | ||
| 44 | <tfoot | ||
| 45 | data-slot="table-footer" | ||
| 46 | className={cn( | ||
| 47 | "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", | ||
| 48 | className | ||
| 49 | )} | ||
| 50 | {...props} | ||
| 51 | /> | ||
| 52 | ) | ||
| 53 | } | ||
| 54 | |||
| 55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) { | ||
| 56 | return ( | ||
| 57 | <tr | ||
| 58 | data-slot="table-row" | ||
| 59 | className={cn( | ||
| 60 | "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", | ||
| 61 | className | ||
| 62 | )} | ||
| 63 | {...props} | ||
| 64 | /> | ||
| 65 | ) | ||
| 66 | } | ||
| 67 | |||
| 68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) { | ||
| 69 | return ( | ||
| 70 | <th | ||
| 71 | data-slot="table-head" | ||
| 72 | className={cn( | ||
| 73 | "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", | ||
| 74 | className | ||
| 75 | )} | ||
| 76 | {...props} | ||
| 77 | /> | ||
| 78 | ) | ||
| 79 | } | ||
| 80 | |||
| 81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) { | ||
| 82 | return ( | ||
| 83 | <td | ||
| 84 | data-slot="table-cell" | ||
| 85 | className={cn( | ||
| 86 | "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", | ||
| 87 | className | ||
| 88 | )} | ||
| 89 | {...props} | ||
| 90 | /> | ||
| 91 | ) | ||
| 92 | } | ||
| 93 | |||
| 94 | function TableCaption({ | ||
| 95 | className, | ||
| 96 | ...props | ||
| 97 | }: React.ComponentProps<"caption">) { | ||
| 98 | return ( | ||
| 99 | <caption | ||
| 100 | data-slot="table-caption" | ||
| 101 | className={cn("text-muted-foreground mt-4 text-sm", className)} | ||
| 102 | {...props} | ||
| 103 | /> | ||
| 104 | ) | ||
| 105 | } | ||
| 106 | |||
| 107 | export { | ||
| 108 | Table, | ||
| 109 | TableHeader, | ||
| 110 | TableBody, | ||
| 111 | TableFooter, | ||
| 112 | TableHead, | ||
| 113 | TableRow, | ||
| 114 | TableCell, | ||
| 115 | TableCaption, | ||
| 116 | } | ||
diff --git a/frontend/components/ui/toast.tsx b/frontend/components/ui/toast.tsx new file mode 100644 index 0000000..6d2e12f --- /dev/null +++ b/frontend/components/ui/toast.tsx | |||
| @@ -0,0 +1,129 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as ToastPrimitives from "@radix-ui/react-toast" | ||
| 5 | import { cva, type VariantProps } from "class-variance-authority" | ||
| 6 | import { X } from "lucide-react" | ||
| 7 | |||
| 8 | import { cn } from "@/lib/utils" | ||
| 9 | |||
| 10 | const ToastProvider = ToastPrimitives.Provider | ||
| 11 | |||
| 12 | const ToastViewport = React.forwardRef< | ||
| 13 | React.ElementRef<typeof ToastPrimitives.Viewport>, | ||
| 14 | React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> | ||
| 15 | >(({ className, ...props }, ref) => ( | ||
| 16 | <ToastPrimitives.Viewport | ||
| 17 | ref={ref} | ||
| 18 | className={cn( | ||
| 19 | "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", | ||
| 20 | className | ||
| 21 | )} | ||
| 22 | {...props} | ||
| 23 | /> | ||
| 24 | )) | ||
| 25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName | ||
| 26 | |||
| 27 | const toastVariants = cva( | ||
| 28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", | ||
| 29 | { | ||
| 30 | variants: { | ||
| 31 | variant: { | ||
| 32 | default: "border bg-background text-foreground", | ||
| 33 | destructive: | ||
| 34 | "destructive border-destructive bg-destructive text-destructive-foreground", | ||
| 35 | }, | ||
| 36 | }, | ||
| 37 | defaultVariants: { | ||
| 38 | variant: "default", | ||
| 39 | }, | ||
| 40 | } | ||
| 41 | ) | ||
| 42 | |||
| 43 | const Toast = React.forwardRef< | ||
| 44 | React.ElementRef<typeof ToastPrimitives.Root>, | ||
| 45 | React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & | ||
| 46 | VariantProps<typeof toastVariants> | ||
| 47 | >(({ className, variant, ...props }, ref) => { | ||
| 48 | return ( | ||
| 49 | <ToastPrimitives.Root | ||
| 50 | ref={ref} | ||
| 51 | className={cn(toastVariants({ variant }), className)} | ||
| 52 | {...props} | ||
| 53 | /> | ||
| 54 | ) | ||
| 55 | }) | ||
| 56 | Toast.displayName = ToastPrimitives.Root.displayName | ||
| 57 | |||
| 58 | const ToastAction = React.forwardRef< | ||
| 59 | React.ElementRef<typeof ToastPrimitives.Action>, | ||
| 60 | React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> | ||
| 61 | >(({ className, ...props }, ref) => ( | ||
| 62 | <ToastPrimitives.Action | ||
| 63 | ref={ref} | ||
| 64 | className={cn( | ||
| 65 | "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", | ||
| 66 | className | ||
| 67 | )} | ||
| 68 | {...props} | ||
| 69 | /> | ||
| 70 | )) | ||
| 71 | ToastAction.displayName = ToastPrimitives.Action.displayName | ||
| 72 | |||
| 73 | const ToastClose = React.forwardRef< | ||
| 74 | React.ElementRef<typeof ToastPrimitives.Close>, | ||
| 75 | React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> | ||
| 76 | >(({ className, ...props }, ref) => ( | ||
| 77 | <ToastPrimitives.Close | ||
| 78 | ref={ref} | ||
| 79 | className={cn( | ||
| 80 | "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", | ||
| 81 | className | ||
| 82 | )} | ||
| 83 | toast-close="" | ||
| 84 | {...props} | ||
| 85 | > | ||
| 86 | <X className="h-4 w-4" /> | ||
| 87 | </ToastPrimitives.Close> | ||
| 88 | )) | ||
| 89 | ToastClose.displayName = ToastPrimitives.Close.displayName | ||
| 90 | |||
| 91 | const ToastTitle = React.forwardRef< | ||
| 92 | React.ElementRef<typeof ToastPrimitives.Title>, | ||
| 93 | React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> | ||
| 94 | >(({ className, ...props }, ref) => ( | ||
| 95 | <ToastPrimitives.Title | ||
| 96 | ref={ref} | ||
| 97 | className={cn("text-sm font-semibold", className)} | ||
| 98 | {...props} | ||
| 99 | /> | ||
| 100 | )) | ||
| 101 | ToastTitle.displayName = ToastPrimitives.Title.displayName | ||
| 102 | |||
| 103 | const ToastDescription = React.forwardRef< | ||
| 104 | React.ElementRef<typeof ToastPrimitives.Description>, | ||
| 105 | React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> | ||
| 106 | >(({ className, ...props }, ref) => ( | ||
| 107 | <ToastPrimitives.Description | ||
| 108 | ref={ref} | ||
| 109 | className={cn("text-sm opacity-90", className)} | ||
| 110 | {...props} | ||
| 111 | /> | ||
| 112 | )) | ||
| 113 | ToastDescription.displayName = ToastPrimitives.Description.displayName | ||
| 114 | |||
| 115 | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> | ||
| 116 | |||
| 117 | type ToastActionElement = React.ReactElement<typeof ToastAction> | ||
| 118 | |||
| 119 | export { | ||
| 120 | type ToastProps, | ||
| 121 | type ToastActionElement, | ||
| 122 | ToastProvider, | ||
| 123 | ToastViewport, | ||
| 124 | Toast, | ||
| 125 | ToastTitle, | ||
| 126 | ToastDescription, | ||
| 127 | ToastClose, | ||
| 128 | ToastAction, | ||
| 129 | } \ No newline at end of file | ||
diff --git a/frontend/components/ui/toaster.tsx b/frontend/components/ui/toaster.tsx new file mode 100644 index 0000000..b5b97f6 --- /dev/null +++ b/frontend/components/ui/toaster.tsx | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import { | ||
| 4 | Toast, | ||
| 5 | ToastClose, | ||
| 6 | ToastDescription, | ||
| 7 | ToastProvider, | ||
| 8 | ToastTitle, | ||
| 9 | ToastViewport, | ||
| 10 | } from "@/components/ui/toast" | ||
| 11 | import { useToast } from "@/hooks/use-toast" | ||
| 12 | |||
| 13 | export function Toaster() { | ||
| 14 | const { toasts } = useToast() | ||
| 15 | |||
| 16 | return ( | ||
| 17 | <ToastProvider> | ||
| 18 | {toasts.map(function ({ id, title, description, action, ...props }) { | ||
| 19 | return ( | ||
| 20 | <Toast key={id} {...props}> | ||
| 21 | <div className="grid gap-1"> | ||
| 22 | {title && <ToastTitle>{title}</ToastTitle>} | ||
| 23 | {description && ( | ||
| 24 | <ToastDescription>{description}</ToastDescription> | ||
| 25 | )} | ||
| 26 | </div> | ||
| 27 | {action} | ||
| 28 | <ToastClose /> | ||
| 29 | </Toast> | ||
| 30 | ) | ||
| 31 | })} | ||
| 32 | <ToastViewport /> | ||
| 33 | </ToastProvider> | ||
| 34 | ) | ||
| 35 | } \ No newline at end of file | ||
