summaryrefslogtreecommitdiff
path: root/src/components/ui
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-15 10:13:00 +0700
committerpolwex <polwex@sortug.com>2025-05-15 10:13:00 +0700
commitd56594d3289002566f4653d607f0837befd65109 (patch)
treef69685b458419566a78727ce6a8cecd0cdc269a5 /src/components/ui
parent04509d9207603d9055cf022051763ec05c9214d6 (diff)
wtf man
Diffstat (limited to 'src/components/ui')
-rw-r--r--src/components/ui/button.tsx58
-rw-r--r--src/components/ui/card.tsx68
-rw-r--r--src/components/ui/form.tsx165
-rw-r--r--src/components/ui/input.tsx19
-rw-r--r--src/components/ui/label.tsx24
-rw-r--r--src/components/ui/select.tsx179
-rw-r--r--src/components/ui/sonner.tsx23
-rw-r--r--src/components/ui/spinner.tsx8
8 files changed, 544 insertions, 0 deletions
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..61875fc
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps<typeof buttonVariants> & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ <Comp
+ data-slot="button"
+ className={cn(buttonVariants({ variant, size, className }))}
+ {...props}
+ />
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..3ff199b
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card"
+ className={cn(
+ "bg-card text-card-foreground rounded-xl border shadow-sm",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-header"
+ className={cn("flex flex-col gap-1.5 p-6", className)}
+ {...props}
+ />
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-title"
+ className={cn("leading-none font-semibold tracking-tight", className)}
+ {...props}
+ />
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-description"
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-content"
+ className={cn("p-6 pt-0", className)}
+ {...props}
+ />
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-footer"
+ className={cn("flex items-center p-6 pt-0", className)}
+ {...props}
+ />
+ )
+}
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
new file mode 100644
index 0000000..8a83b32
--- /dev/null
+++ b/src/components/ui/form.tsx
@@ -0,0 +1,165 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+ useFormState,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext<FormFieldContextValue>(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+>({
+ ...props
+}: ControllerProps<TFieldValues, TName>) => {
+ return (
+ <FormFieldContext.Provider value={{ name: props.name }}>
+ <Controller {...props} />
+ </FormFieldContext.Provider>
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState } = useFormContext()
+ const formState = useFormState({ name: fieldContext.name })
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within <FormField>")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext<FormItemContextValue>(
+ {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+ const id = React.useId()
+
+ return (
+ <FormItemContext.Provider value={{ id }}>
+ <div
+ data-slot="form-item"
+ className={cn("grid gap-2", className)}
+ {...props}
+ />
+ </FormItemContext.Provider>
+ )
+}
+
+function FormLabel({
+ className,
+ ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+ const { error, formItemId } = useFormField()
+
+ return (
+ <Label
+ data-slot="form-label"
+ data-error={!!error}
+ className={cn("data-[error=true]:text-destructive", className)}
+ htmlFor={formItemId}
+ {...props}
+ />
+ )
+}
+
+function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+ <Slot
+ data-slot="form-control"
+ id={formItemId}
+ aria-describedby={
+ !error
+ ? `${formDescriptionId}`
+ : `${formDescriptionId} ${formMessageId}`
+ }
+ aria-invalid={!!error}
+ {...props}
+ />
+ )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+ <p
+ data-slot="form-description"
+ id={formDescriptionId}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
+ )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : props.children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+ <p
+ data-slot="form-message"
+ id={formMessageId}
+ className={cn("text-destructive text-sm font-medium", className)}
+ {...props}
+ >
+ {body}
+ </p>
+ )
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..596f806
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+ <input
+ type={type}
+ data-slot="input"
+ className={cn(
+ "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive 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] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..f948bc3
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+ return (
+ <LabelPrimitive.Root
+ data-slot="label"
+ className={cn(
+ "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",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Label }
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..b624a5b
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,179 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Root>) {
+ return <SelectPrimitive.Root data-slot="select" {...props} />
+}
+
+function SelectGroup({
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Group>) {
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Value>) {
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
+}
+
+function SelectTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
+ return (
+ <SelectPrimitive.Trigger
+ data-slot="select-trigger"
+ className={cn(
+ "border-input data-[placeholder]:text-muted-foreground aria-invalid:border-destructive ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-9 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-0 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <SelectPrimitive.Icon asChild>
+ <ChevronDownIcon className="size-4 opacity-50" />
+ </SelectPrimitive.Icon>
+ </SelectPrimitive.Trigger>
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "popper",
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+ return (
+ <SelectPrimitive.Portal>
+ <SelectPrimitive.Content
+ data-slot="select-content"
+ className={cn(
+ "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 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md",
+ position === "popper" &&
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+ className
+ )}
+ position={position}
+ {...props}
+ >
+ <SelectScrollUpButton />
+ <SelectPrimitive.Viewport
+ className={cn(
+ "p-1",
+ position === "popper" &&
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
+ )}
+ >
+ {children}
+ </SelectPrimitive.Viewport>
+ <SelectScrollDownButton />
+ </SelectPrimitive.Content>
+ </SelectPrimitive.Portal>
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Label>) {
+ return (
+ <SelectPrimitive.Label
+ data-slot="select-label"
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
+ {...props}
+ />
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Item>) {
+ return (
+ <SelectPrimitive.Item
+ data-slot="select-item"
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+ className
+ )}
+ {...props}
+ >
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
+ <SelectPrimitive.ItemIndicator>
+ <CheckIcon className="size-4" />
+ </SelectPrimitive.ItemIndicator>
+ </span>
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+ </SelectPrimitive.Item>
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
+ return (
+ <SelectPrimitive.Separator
+ data-slot="select-separator"
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+ {...props}
+ />
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
+ return (
+ <SelectPrimitive.ScrollUpButton
+ data-slot="select-scroll-up-button"
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className
+ )}
+ {...props}
+ >
+ <ChevronUpIcon className="size-4" />
+ </SelectPrimitive.ScrollUpButton>
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
+ return (
+ <SelectPrimitive.ScrollDownButton
+ data-slot="select-scroll-down-button"
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className
+ )}
+ {...props}
+ >
+ <ChevronDownIcon className="size-4" />
+ </SelectPrimitive.ScrollDownButton>
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..cd62aff
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,23 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ <Sonner
+ theme={theme as ToasterProps["theme"]}
+ className="toaster group"
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000..d1511ad
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -0,0 +1,8 @@
+export const Spinner = ({ className }: { className?: string }) => (
+ <div
+ className={
+ "w-7 h-7 border-[3px] border-transparent border-t-primary rounded-full animate-spin" +
+ ` ${className}`
+ }
+ />
+);