diff --git a/.claude/settings.json b/.claude/settings.json index 92e425b..deffac9 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,15 +1,3 @@ { - "hooks": { - "PostToolUse": [ - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "pnpm dlx ultracite fix" - } - ] - } - ] - } + "hooks": {} } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..38702ef --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +**/routeTree.gen.ts +pnpm-lock.yaml diff --git a/apps/web/components.json b/apps/web/components.json deleted file mode 100644 index d94dbff..0000000 --- a/apps/web/components.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-lyra", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "menuColor": "default", - "menuAccent": "subtle", - "registries": {} -} diff --git a/apps/web/package.json b/apps/web/package.json index 3e6af97..7f46f69 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,6 +6,7 @@ "build": "vite build", "serve": "vite preview", "dev": "vite dev", + "check-types": "tsc --noEmit", "lint": "eslint ." }, "dependencies": { @@ -20,7 +21,6 @@ "@zendegi/auth": "workspace:*", "@zendegi/env": "workspace:*", "better-auth": "catalog:", - "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "catalog:", "lucide-react": "^0.525.0", @@ -30,6 +30,7 @@ "shadcn": "^3.6.2", "sonner": "^2.0.3", "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.2.2", "tailwindcss": "^4.1.3", "tw-animate-css": "^1.2.5", "vite-tsconfig-paths": "^5.1.4", diff --git a/apps/web/src/components/sign-in-form.tsx b/apps/web/src/components/sign-in-form.tsx index 1153e38..ae34991 100644 --- a/apps/web/src/components/sign-in-form.tsx +++ b/apps/web/src/components/sign-in-form.tsx @@ -1,12 +1,10 @@ import { useForm } from "@tanstack/react-form"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; -import z from "zod"; import Loader from "./loader"; import { Button } from "./ui/button"; -import { Input } from "./ui/input"; -import { Label } from "./ui/label"; +import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field"; import { authClient } from "@/lib/auth-client"; export default function SignInForm({ @@ -43,12 +41,6 @@ export default function SignInForm({ } ); }, - validators: { - onSubmit: z.object({ - email: z.email("Invalid email address"), - password: z.string().min(8, "Password must be at least 8 characters"), - }), - }, }); if (isPending) { @@ -67,51 +59,48 @@ export default function SignInForm({ }} className="space-y-4" > -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + {(field) => ( + + Email + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + Email is required + + Please enter a valid email address + + + )} + -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + {(field) => ( + + Password + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + Password is required + + Password must be at least 8 characters + + + )} + {(state) => ( diff --git a/apps/web/src/components/sign-up-form.tsx b/apps/web/src/components/sign-up-form.tsx index 358487a..da05bf5 100644 --- a/apps/web/src/components/sign-up-form.tsx +++ b/apps/web/src/components/sign-up-form.tsx @@ -1,12 +1,10 @@ import { useForm } from "@tanstack/react-form"; import { useNavigate } from "@tanstack/react-router"; import { toast } from "sonner"; -import z from "zod"; import Loader from "./loader"; import { Button } from "./ui/button"; -import { Input } from "./ui/input"; -import { Label } from "./ui/label"; +import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field"; import { authClient } from "@/lib/auth-client"; export default function SignUpForm({ @@ -45,13 +43,6 @@ export default function SignUpForm({ } ); }, - validators: { - onSubmit: z.object({ - name: z.string().min(2, "Name must be at least 2 characters"), - email: z.email("Invalid email address"), - password: z.string().min(8, "Password must be at least 8 characters"), - }), - }, }); if (isPending) { @@ -70,73 +61,69 @@ export default function SignUpForm({ }} className="space-y-4" > -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + {(field) => ( + + Name + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + Name is required + + Name must be at least 2 characters + + + )} + -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + {(field) => ( + + Email + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + Email is required + + Please enter a valid email address + + + )} + -
- - {(field) => ( -
- - field.handleChange(e.target.value)} - /> - {field.state.meta.errors.map((error) => ( -

- {error?.message} -

- ))} -
- )} -
-
+ + {(field) => ( + + Password + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + Password is required + + Password must be at least 8 characters + + + )} + {(state) => ( diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 811ab52..ba8f679 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -1,57 +1,45 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva } from "class-variance-authority"; -import type { VariantProps } from "class-variance-authority"; +import { Button as BaseButton } from "@base-ui/react/button"; +import { tv } from "tailwind-variants"; +import type { VariantProps } from "tailwind-variants"; -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", - destructive: - "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", - icon: "size-8", - "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-none", - "icon-lg": "size-9", - }, +const buttonStyles = tv({ + base: "inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50", + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-md hover:bg-primary/90", + outline: + "border border-border bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + destructive: + "bg-destructive text-white shadow-sm hover:bg-destructive/90", }, - defaultVariants: { - variant: "default", - size: "default", + size: { + default: "h-10 px-4 py-2 text-sm", + sm: "h-8 px-3 text-xs", + lg: "h-12 px-6 text-base", + icon: "h-10 w-10", }, - } -); + }, + defaultVariants: { variant: "default", size: "default" }, +}); -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps) { +type ButtonProps = React.ComponentProps & + VariantProps; + +function Button({ variant, size, className, ...props }: ButtonProps) { return ( - + buttonStyles({ + variant, + size, + class: typeof className === "function" ? className(state) : className, + }) + } {...props} /> ); } -export { Button, buttonVariants }; +export { Button, buttonStyles }; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx deleted file mode 100644 index baa9fca..0000000 --- a/apps/web/src/components/ui/card.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Card({ - className, - size = "default", - ...props -}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { - return ( -
img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col", - className - )} - {...props} - /> - ); -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -}; diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx deleted file mode 100644 index 0357443..0000000 --- a/apps/web/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; -import { CheckIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { - return ( - - - - - - ); -} - -export { Checkbox }; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index d392c5c..0000000 --- a/apps/web/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { Menu as MenuPrimitive } from "@base-ui/react/menu"; -import { CheckIcon, ChevronRightIcon } from "lucide-react"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { - return ; -} - -function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { - return ; -} - -function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { - return ; -} - -function DropdownMenuContent({ - align = "start", - alignOffset = 0, - side = "bottom", - sideOffset = 4, - className, - ...props -}: MenuPrimitive.Popup.Props & - Pick< - MenuPrimitive.Positioner.Props, - "align" | "alignOffset" | "side" | "sideOffset" - >) { - return ( - - - - - - ); -} - -function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { - return ; -} - -function DropdownMenuLabel({ - className, - inset, - ...props -}: MenuPrimitive.GroupLabel.Props & { - inset?: boolean; -}) { - return ( - - ); -} - -function DropdownMenuItem({ - className, - inset, - variant = "default", - ...props -}: MenuPrimitive.Item.Props & { - inset?: boolean; - variant?: "default" | "destructive"; -}) { - return ( - - ); -} - -function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { - return ; -} - -function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: MenuPrimitive.SubmenuTrigger.Props & { - inset?: boolean; -}) { - return ( - - {children} - - - ); -} - -function DropdownMenuSubContent({ - align = "start", - alignOffset = -3, - side = "right", - sideOffset = 0, - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuCheckboxItem({ - className, - children, - checked, - ...props -}: MenuPrimitive.CheckboxItem.Props) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { - return ( - - ); -} - -function DropdownMenuRadioItem({ - className, - children, - ...props -}: MenuPrimitive.RadioItem.Props) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuSeparator({ - className, - ...props -}: MenuPrimitive.Separator.Props) { - return ( - - ); -} - -function DropdownMenuShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { - return ( - - ); -} - -export { - DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuSubContent, -}; diff --git a/apps/web/src/components/ui/field.tsx b/apps/web/src/components/ui/field.tsx new file mode 100644 index 0000000..3d2be8a --- /dev/null +++ b/apps/web/src/components/ui/field.tsx @@ -0,0 +1,100 @@ +import { Field as BaseField } from "@base-ui/react/field"; +import { tv } from "tailwind-variants"; + +import { inputClasses } from "./input"; + +const fieldStyles = tv({ + slots: { + root: "flex flex-col gap-1", + label: "text-sm font-medium leading-none data-[invalid]:text-destructive", + control: inputClasses, + description: "text-sm text-muted-foreground", + error: "text-sm text-destructive", + }, +}); + +const { root, label, control, description, error } = fieldStyles(); + +type FieldRootProps = React.ComponentProps; + +function FieldRoot({ className, ...props }: FieldRootProps) { + return ( + + root({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type FieldLabelProps = React.ComponentProps; + +function FieldLabel({ className, ...props }: FieldLabelProps) { + return ( + + label({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type FieldControlProps = React.ComponentProps; + +function FieldControl({ className, ...props }: FieldControlProps) { + return ( + + control({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type FieldDescriptionProps = React.ComponentProps; + +function FieldDescription({ className, ...props }: FieldDescriptionProps) { + return ( + + description({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type FieldErrorProps = React.ComponentProps; + +function FieldError({ className, ...props }: FieldErrorProps) { + return ( + + error({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +export { + FieldRoot, + FieldLabel, + FieldControl, + FieldDescription, + FieldError, + fieldStyles, +}; diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index bcad978..aae5f13 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -1,20 +1,26 @@ -import { Input as InputPrimitive } from "@base-ui/react/input"; -import type * as React from "react"; +import { Input as BaseInput } from "@base-ui/react/input"; +import { tv } from "tailwind-variants"; -import { cn } from "@/lib/utils"; +const inputClasses = + "h-10 w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[invalid]:border-destructive"; -function Input({ className, type, ...props }: React.ComponentProps<"input">) { +const inputStyles = tv({ + base: inputClasses, +}); + +type InputProps = React.ComponentProps; + +function Input({ className, ...props }: InputProps) { return ( - + inputStyles({ + class: typeof className === "function" ? className(state) : className, + }) + } {...props} /> ); } -export { Input }; +export { Input, inputClasses, inputStyles }; diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx deleted file mode 100644 index dd3488a..0000000 --- a/apps/web/src/components/ui/label.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Label({ className, ...props }: React.ComponentProps<"label">) { - return ( - // eslint-disable-next-line jsx-a11y/label-has-associated-control -- htmlFor is passed via ...props -