aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components/ui
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/ui')
-rw-r--r--frontend/src/components/ui/button.jsx55
-rw-r--r--frontend/src/components/ui/form.jsx144
-rw-r--r--frontend/src/components/ui/input.jsx24
-rw-r--r--frontend/src/components/ui/label.jsx23
-rw-r--r--frontend/src/components/ui/sonner.jsx26
5 files changed, 272 insertions, 0 deletions
diff --git a/frontend/src/components/ui/button.jsx b/frontend/src/components/ui/button.jsx
new file mode 100644
index 0000000..69ad71f
--- /dev/null
+++ b/frontend/src/components/ui/button.jsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva } 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-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",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "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",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ 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 gap-1.5 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
+}) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ <Comp
+ data-slot="button"
+ className={cn(buttonVariants({ variant, size, className }))}
+ {...props} />
+ );
+}
+
+export { Button, buttonVariants }
diff --git a/frontend/src/components/ui/form.jsx b/frontend/src/components/ui/form.jsx
new file mode 100644
index 0000000..728ea87
--- /dev/null
+++ b/frontend/src/components/ui/form.jsx
@@ -0,0 +1,144 @@
+"use client";
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { Controller, FormProvider, useFormContext, useFormState } from "react-hook-form";
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+const FormFieldContext = React.createContext({})
+
+const FormField = (
+ {
+ ...props
+ }
+) => {
+ 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,
+ }
+}
+
+const FormItemContext = React.createContext({})
+
+function FormItem({
+ className,
+ ...props
+}) {
+ 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
+}) {
+ 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
+}) {
+ 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
+}) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+ <p
+ data-slot="form-description"
+ id={formDescriptionId}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props} />
+ );
+}
+
+function FormMessage({
+ className,
+ ...props
+}) {
+ 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", className)}
+ {...props}>
+ {body}
+ </p>
+ );
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/src/components/ui/input.jsx b/frontend/src/components/ui/input.jsx
new file mode 100644
index 0000000..1e9bbd1
--- /dev/null
+++ b/frontend/src/components/ui/input.jsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({
+ className,
+ type,
+ ...props
+}) {
+ return (
+ <input
+ type={type}
+ data-slot="input"
+ className={cn(
+ "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",
+ "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",
+ className
+ )}
+ {...props} />
+ );
+}
+
+export { Input }
diff --git a/frontend/src/components/ui/label.jsx b/frontend/src/components/ui/label.jsx
new file mode 100644
index 0000000..1002a4f
--- /dev/null
+++ b/frontend/src/components/ui/label.jsx
@@ -0,0 +1,23 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}) {
+ return (
+ <LabelPrimitive.Root
+ data-slot="label"
+ className={cn(
+ "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",
+ className
+ )}
+ {...props} />
+ );
+}
+
+export { Label }
diff --git a/frontend/src/components/ui/sonner.jsx b/frontend/src/components/ui/sonner.jsx
new file mode 100644
index 0000000..8079d58
--- /dev/null
+++ b/frontend/src/components/ui/sonner.jsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner";
+
+const Toaster = ({
+ ...props
+}) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ <Sonner
+ theme={theme}
+ className="toaster group"
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)"
+ }
+ }
+ {...props} />
+ );
+}
+
+export { Toaster }