add new components

This commit is contained in:
2026-02-19 05:57:31 +01:00
parent 316055652f
commit 2933465ba9
32 changed files with 3543 additions and 6916 deletions

View File

@@ -1,15 +1,3 @@
{ {
"hooks": { "hooks": {}
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "pnpm dlx ultracite fix"
}
]
}
]
}
} }

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
**/routeTree.gen.ts
pnpm-lock.yaml

View File

@@ -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": {}
}

View File

@@ -6,6 +6,7 @@
"build": "vite build", "build": "vite build",
"serve": "vite preview", "serve": "vite preview",
"dev": "vite dev", "dev": "vite dev",
"check-types": "tsc --noEmit",
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
@@ -20,7 +21,6 @@
"@zendegi/auth": "workspace:*", "@zendegi/auth": "workspace:*",
"@zendegi/env": "workspace:*", "@zendegi/env": "workspace:*",
"better-auth": "catalog:", "better-auth": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "catalog:", "dotenv": "catalog:",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
@@ -30,6 +30,7 @@
"shadcn": "^3.6.2", "shadcn": "^3.6.2",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.3", "tailwindcss": "^4.1.3",
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",

View File

@@ -1,12 +1,10 @@
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import z from "zod";
import Loader from "./loader"; import Loader from "./loader";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Input } from "./ui/input"; import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
import { Label } from "./ui/label";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
export default function SignInForm({ 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) { if (isPending) {
@@ -67,51 +59,48 @@ export default function SignInForm({
}} }}
className="space-y-4" className="space-y-4"
> >
<div>
<form.Field name="email"> <form.Field name="email">
{(field) => ( {(field) => (
<div className="space-y-2"> <FieldRoot>
<Label htmlFor={field.name}>Email</Label> <FieldLabel>Email</FieldLabel>
<Input <FieldControl
id={field.name}
name={field.name}
type="email" type="email"
required
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
}
/> />
{field.state.meta.errors.map((error) => ( <FieldError match="valueMissing">Email is required</FieldError>
<p key={error?.message} className="text-red-500"> <FieldError match="typeMismatch">
{error?.message} Please enter a valid email address
</p> </FieldError>
))} </FieldRoot>
</div>
)} )}
</form.Field> </form.Field>
</div>
<div>
<form.Field name="password"> <form.Field name="password">
{(field) => ( {(field) => (
<div className="space-y-2"> <FieldRoot>
<Label htmlFor={field.name}>Password</Label> <FieldLabel>Password</FieldLabel>
<Input <FieldControl
id={field.name}
name={field.name}
type="password" type="password"
required
minLength={8}
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
}
/> />
{field.state.meta.errors.map((error) => ( <FieldError match="valueMissing">Password is required</FieldError>
<p key={error?.message} className="text-red-500"> <FieldError match="tooShort">
{error?.message} Password must be at least 8 characters
</p> </FieldError>
))} </FieldRoot>
</div>
)} )}
</form.Field> </form.Field>
</div>
<form.Subscribe> <form.Subscribe>
{(state) => ( {(state) => (

View File

@@ -1,12 +1,10 @@
import { useForm } from "@tanstack/react-form"; import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import z from "zod";
import Loader from "./loader"; import Loader from "./loader";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Input } from "./ui/input"; import { FieldControl, FieldError, FieldLabel, FieldRoot } from "./ui/field";
import { Label } from "./ui/label";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
export default function SignUpForm({ 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) { if (isPending) {
@@ -70,73 +61,69 @@ export default function SignUpForm({
}} }}
className="space-y-4" className="space-y-4"
> >
<div>
<form.Field name="name"> <form.Field name="name">
{(field) => ( {(field) => (
<div className="space-y-2"> <FieldRoot>
<Label htmlFor={field.name}>Name</Label> <FieldLabel>Name</FieldLabel>
<Input <FieldControl
id={field.name} required
name={field.name} minLength={2}
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
}
/> />
{field.state.meta.errors.map((error) => ( <FieldError match="valueMissing">Name is required</FieldError>
<p key={error?.message} className="text-red-500"> <FieldError match="tooShort">
{error?.message} Name must be at least 2 characters
</p> </FieldError>
))} </FieldRoot>
</div>
)} )}
</form.Field> </form.Field>
</div>
<div>
<form.Field name="email"> <form.Field name="email">
{(field) => ( {(field) => (
<div className="space-y-2"> <FieldRoot>
<Label htmlFor={field.name}>Email</Label> <FieldLabel>Email</FieldLabel>
<Input <FieldControl
id={field.name}
name={field.name}
type="email" type="email"
required
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
}
/> />
{field.state.meta.errors.map((error) => ( <FieldError match="valueMissing">Email is required</FieldError>
<p key={error?.message} className="text-red-500"> <FieldError match="typeMismatch">
{error?.message} Please enter a valid email address
</p> </FieldError>
))} </FieldRoot>
</div>
)} )}
</form.Field> </form.Field>
</div>
<div>
<form.Field name="password"> <form.Field name="password">
{(field) => ( {(field) => (
<div className="space-y-2"> <FieldRoot>
<Label htmlFor={field.name}>Password</Label> <FieldLabel>Password</FieldLabel>
<Input <FieldControl
id={field.name}
name={field.name}
type="password" type="password"
required
minLength={8}
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
}
/> />
{field.state.meta.errors.map((error) => ( <FieldError match="valueMissing">Password is required</FieldError>
<p key={error?.message} className="text-red-500"> <FieldError match="tooShort">
{error?.message} Password must be at least 8 characters
</p> </FieldError>
))} </FieldRoot>
</div>
)} )}
</form.Field> </form.Field>
</div>
<form.Subscribe> <form.Subscribe>
{(state) => ( {(state) => (

View File

@@ -1,57 +1,45 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"; import { Button as BaseButton } from "@base-ui/react/button";
import { cva } from "class-variance-authority"; import { tv } from "tailwind-variants";
import type { VariantProps } from "class-variance-authority"; import type { VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils"; 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",
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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", default:
"bg-primary text-primary-foreground shadow-md hover:bg-primary/90",
outline: 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", "border border-border bg-transparent shadow-sm hover:bg-accent hover:text-accent-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", link: "text-primary underline-offset-4 hover:underline",
destructive:
"bg-destructive text-white shadow-sm hover:bg-destructive/90",
}, },
size: { size: {
default: default: "h-10 px-4 py-2 text-sm",
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", sm: "h-8 px-3 text-xs",
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", lg: "h-12 px-6 text-base",
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", icon: "h-10 w-10",
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",
}, },
}, },
defaultVariants: { defaultVariants: { variant: "default", size: "default" },
variant: "default", });
size: "default",
},
}
);
function Button({ type ButtonProps = React.ComponentProps<typeof BaseButton> &
className, VariantProps<typeof buttonStyles>;
variant = "default",
size = "default", function Button({ variant, size, className, ...props }: ButtonProps) {
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return ( return (
<ButtonPrimitive <BaseButton
data-slot="button" className={(state) =>
className={cn(buttonVariants({ variant, size, className }))} buttonStyles({
variant,
size,
class: typeof className === "function" ? className(state) : className,
})
}
{...props} {...props}
/> />
); );
} }
export { Button, buttonVariants }; export { Button, buttonStyles };

View File

@@ -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 (
<div
data-slot="card"
data-size={size}
className={cn(
"ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-none py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>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 (
<div
data-slot="card-header"
className={cn(
"gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"text-sm font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-xs/relaxed", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"rounded-none border-t p-4 group-data-[size=sm]/card:p-3 flex items-center",
className
)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -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 (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-none border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-1 aria-invalid:ring-1 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -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 <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-none shadow-md ring-1 duration-100 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
className
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"text-muted-foreground px-2 py-2 text-xs data-[inset]:pl-8",
className
)}
{...props}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"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 not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
/>
);
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-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 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-none shadow-lg ring-1 duration-100 w-auto",
className
)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: MenuPrimitive.CheckboxItem.Props) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: MenuPrimitive.RadioItem.Props) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
}
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@@ -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<typeof BaseField.Root>;
function FieldRoot({ className, ...props }: FieldRootProps) {
return (
<BaseField.Root
className={(state) =>
root({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type FieldLabelProps = React.ComponentProps<typeof BaseField.Label>;
function FieldLabel({ className, ...props }: FieldLabelProps) {
return (
<BaseField.Label
className={(state) =>
label({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type FieldControlProps = React.ComponentProps<typeof BaseField.Control>;
function FieldControl({ className, ...props }: FieldControlProps) {
return (
<BaseField.Control
className={(state) =>
control({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type FieldDescriptionProps = React.ComponentProps<typeof BaseField.Description>;
function FieldDescription({ className, ...props }: FieldDescriptionProps) {
return (
<BaseField.Description
className={(state) =>
description({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type FieldErrorProps = React.ComponentProps<typeof BaseField.Error>;
function FieldError({ className, ...props }: FieldErrorProps) {
return (
<BaseField.Error
className={(state) =>
error({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
export {
FieldRoot,
FieldLabel,
FieldControl,
FieldDescription,
FieldError,
fieldStyles,
};

View File

@@ -1,20 +1,26 @@
import { Input as InputPrimitive } from "@base-ui/react/input"; import { Input as BaseInput } from "@base-ui/react/input";
import type * as React from "react"; 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<typeof BaseInput>;
function Input({ className, ...props }: InputProps) {
return ( return (
<InputPrimitive <BaseInput
type={type} className={(state) =>
data-slot="input" inputStyles({
className={cn( class: typeof className === "function" ? className(state) : className,
"dark:bg-input/30 border-input 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 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-none border bg-transparent px-2.5 py-1 text-xs transition-colors file:h-6 file:text-xs file:font-medium focus-visible:ring-1 aria-invalid:ring-1 md:text-xs file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", })
className }
)}
{...props} {...props}
/> />
); );
} }
export { Input }; export { Input, inputClasses, inputStyles };

View File

@@ -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
<label
data-slot="label"
className={cn(
"gap-2 text-xs leading-none group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
className
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,31 @@
import { Separator as BaseSeparator } from "@base-ui/react/separator";
import { tv } from "tailwind-variants";
import type { VariantProps } from "tailwind-variants";
const separatorStyles = tv({
base: "shrink-0 bg-border",
variants: {
orientation: {
horizontal: "h-px w-full",
vertical: "h-full w-px",
},
},
defaultVariants: { orientation: "horizontal" },
});
type SeparatorProps = Omit<
React.ComponentProps<typeof BaseSeparator>,
"className"
> &
VariantProps<typeof separatorStyles> & { className?: string };
function Separator({ orientation, className, ...props }: SeparatorProps) {
return (
<BaseSeparator
className={separatorStyles({ orientation, class: className })}
{...props}
/>
);
}
export { Separator, separatorStyles };

View File

@@ -1,16 +1,11 @@
import { cn } from "@/lib/utils"; import { tv } from "tailwind-variants";
function Skeleton({ const skeletonStyles = tv({
className, base: "animate-pulse rounded-md bg-muted",
...props });
}: React.HTMLAttributes<HTMLDivElement>) {
return ( function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
<div return <div className={skeletonStyles({ class: className })} {...props} />;
data-slot="skeleton"
className={cn("bg-muted rounded-none animate-pulse", className)}
{...props}
/>
);
} }
export { Skeleton }; export { Skeleton, skeletonStyles };

View File

@@ -1,44 +1,9 @@
import { import { Toaster as SonnerToaster } from "sonner";
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner } from "sonner";
import type { ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { type ToasterProps = React.ComponentProps<typeof SonnerToaster>;
const { theme = "system" } = useTheme();
return ( function Toaster(props: ToasterProps) {
<Sonner return <SonnerToaster theme="dark" {...props} />;
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
} }
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
);
};
export { Toaster }; export { Toaster };

View File

@@ -1,18 +1,23 @@
import { Link, useNavigate } from "@tanstack/react-router"; import { Link, useNavigate } from "@tanstack/react-router";
import { Menu } from "@base-ui/react/menu";
import { tv } from "tailwind-variants";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton"; import { Skeleton } from "./ui/skeleton";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
const menuStyles = tv({
slots: {
popup:
"z-50 min-w-48 rounded-xl border border-border bg-card p-1 shadow-lg",
item: "flex cursor-pointer select-none items-center rounded-lg px-3 py-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
label: "px-3 py-2 text-xs font-semibold text-muted-foreground",
separator: "mx-1 my-1 h-px bg-border",
},
});
const { popup, item, label, separator } = menuStyles();
export default function UserMenu() { export default function UserMenu() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession(); const { data: session, isPending } = authClient.useSession();
@@ -30,33 +35,38 @@ export default function UserMenu() {
} }
return ( return (
<DropdownMenu> <Menu.Root>
<DropdownMenuTrigger render={<Button variant="outline" />}> <Menu.Trigger
{session.user.name} render={<Button variant="outline">{session.user.name}</Button>}
</DropdownMenuTrigger> />
<DropdownMenuContent className="bg-card"> <Menu.Portal>
<DropdownMenuGroup> <Menu.Positioner sideOffset={4}>
<DropdownMenuLabel>My Account</DropdownMenuLabel> <Menu.Popup className={popup()}>
<DropdownMenuSeparator /> <Menu.Group>
<DropdownMenuItem>{session.user.email}</DropdownMenuItem> <Menu.GroupLabel className={label()}>My Account</Menu.GroupLabel>
<DropdownMenuItem <Menu.Separator className={separator()} />
variant="destructive" <Menu.Item className={item()}>{session.user.email}</Menu.Item>
<Menu.Item
className={item({
class:
"text-destructive data-[highlighted]:bg-destructive/10",
})}
onClick={() => { onClick={() => {
authClient.signOut({ authClient.signOut({
fetchOptions: { fetchOptions: {
onSuccess: () => { onSuccess: () => {
navigate({ navigate({ to: "/" });
to: "/",
});
}, },
}, },
}); });
}} }}
> >
Sign Out Sign Out
</DropdownMenuItem> </Menu.Item>
</DropdownMenuGroup> </Menu.Group>
</DropdownMenuContent> </Menu.Popup>
</DropdownMenu> </Menu.Positioner>
</Menu.Portal>
</Menu.Root>
); );
} }

View File

@@ -4,6 +4,6 @@ import { authMiddleware } from "@/middleware/auth";
export const getUser = createServerFn({ method: "GET" }) export const getUser = createServerFn({ method: "GET" })
.middleware([authMiddleware]) .middleware([authMiddleware])
.handler(async ({ context }) => { .handler(({ context }) => {
return context.session; return context.session;
}); });

View File

@@ -1,6 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));

View File

@@ -1,7 +0,0 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import type { ClassValue } from "clsx";
export function cn(...inputs: Array<ClassValue>) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,140 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as DemoRouteImport } from './routes/demo'
import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const DemoRoute = DemoRouteImport.update({
id: '/demo',
path: '/demo',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardRoute = DashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/demo': typeof DemoRoute
'/login': typeof LoginRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/demo': typeof DemoRoute
'/login': typeof LoginRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/demo': typeof DemoRoute
'/login': typeof LoginRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/dashboard' | '/demo' | '/login' | '/api/auth/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/dashboard' | '/demo' | '/login' | '/api/auth/$'
id: '__root__' | '/' | '/dashboard' | '/demo' | '/login' | '/api/auth/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRoute
DemoRoute: typeof DemoRoute
LoginRoute: typeof LoginRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/demo': {
id: '/demo'
path: '/demo'
fullPath: '/demo'
preLoaderRoute: typeof DemoRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof DashboardRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
fullPath: '/api/auth/$'
preLoaderRoute: typeof ApiAuthSplatRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRoute,
DemoRoute: DemoRoute,
LoginRoute: LoginRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View File

@@ -8,7 +8,7 @@ export const Route = createFileRoute("/dashboard")({
const session = await getUser(); const session = await getUser();
return { session }; return { session };
}, },
loader: async ({ context }) => { loader: ({ context }) => {
if (!context.session) { if (!context.session) {
throw redirect({ throw redirect({
to: "/login", to: "/login",

View File

@@ -0,0 +1,171 @@
import { createFileRoute } from "@tanstack/react-router";
import { Heart, Search } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
FieldControl,
FieldDescription,
FieldError,
FieldLabel,
FieldRoot,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
export const Route = createFileRoute("/demo")({
component: DemoPage,
});
function Section({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold">{title}</h2>
<Separator />
{children}
</section>
);
}
function DemoPage() {
return (
<div className="container mx-auto max-w-3xl space-y-10 px-4 py-8">
<h1 className="text-2xl font-bold">Component Demo</h1>
{/* Button */}
<Section title="Button">
<div className="space-y-4">
<div>
<p className="mb-2 text-sm text-muted-foreground">Variants</p>
<div className="flex flex-wrap items-center gap-3">
<Button variant="default">Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Destructive</Button>
</div>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">Sizes</p>
<div className="flex flex-wrap items-center gap-3">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon" aria-label="Search">
<Search className="h-4 w-4" />
</Button>
</div>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">States</p>
<div className="flex flex-wrap items-center gap-3">
<Button disabled>Disabled</Button>
<Button>
<Heart className="h-4 w-4" />
With Icon
</Button>
</div>
</div>
</div>
</Section>
{/* Field */}
<Section title="Field">
<form
className="grid gap-4 sm:max-w-sm"
onSubmit={(e) => e.preventDefault()}
>
<FieldRoot>
<FieldLabel>Email</FieldLabel>
<FieldControl
type="email"
required
placeholder="you@example.com"
/>{" "}
<FieldDescription>
We&apos;ll never share your email.
</FieldDescription>
<FieldError match="valueMissing">Email is required</FieldError>
<FieldError match="typeMismatch">
Please enter a valid email
</FieldError>
</FieldRoot>
<FieldRoot>
<FieldLabel>Bio</FieldLabel>
<FieldControl placeholder="Tell us about yourself" />
<FieldDescription>Optional</FieldDescription>
</FieldRoot>
<Button type="submit" className="w-fit">
Submit
</Button>
</form>
</Section>
{/* Input (standalone) */}
<Section title="Input">
<div className="grid gap-3 sm:max-w-sm">
<Input placeholder="Default input" />
<Input placeholder="Disabled input" disabled />
</div>
</Section>
{/* Separator */}
<Section title="Separator">
<div className="space-y-4">
<div>
<p className="mb-2 text-sm text-muted-foreground">Horizontal</p>
<Separator orientation="horizontal" />
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">Vertical</p>
<div className="flex h-10 items-center gap-4">
<span className="text-sm">Left</span>
<Separator orientation="vertical" />
<span className="text-sm">Right</span>
</div>
</div>
</div>
</Section>
{/* Skeleton */}
<Section title="Skeleton">
<div className="space-y-3">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-10 w-10 rounded-full" />
<Skeleton className="h-24 w-full" />
</div>
</Section>
{/* Sonner / Toast */}
<Section title="Sonner (Toast)">
<div className="flex flex-wrap gap-3">
<Button
variant="outline"
onClick={() => toast("This is a default toast")}
>
Default Toast
</Button>
<Button
variant="outline"
onClick={() => toast.success("Operation completed")}
>
Success Toast
</Button>
<Button
variant="outline"
onClick={() => toast.error("Something went wrong")}
>
Error Toast
</Button>
</div>
</Section>
</div>
);
}

View File

@@ -21,7 +21,7 @@
"db:stop": "turbo -F @zendegi/db db:stop", "db:stop": "turbo -F @zendegi/db db:stop",
"db:down": "turbo -F @zendegi/db db:down", "db:down": "turbo -F @zendegi/db db:down",
"check": "turbo run lint check-types && prettier --check .", "check": "turbo run lint check-types && prettier --check .",
"fix": "turbo run lint -- --fix && prettier --write ." "fix": "turbo run lint -- --fix && prettier --write --list-different ."
}, },
"dependencies": { "dependencies": {
"@zendegi/env": "workspace:*", "@zendegi/env": "workspace:*",
@@ -33,7 +33,6 @@
"@zendegi/config": "workspace:*", "@zendegi/config": "workspace:*",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"prettier": "latest", "prettier": "latest",
"stylelint": "latest",
"turbo": "^2.6.3", "turbo": "^2.6.3",
"typescript": "catalog:" "typescript": "catalog:"
}, },

View File

@@ -10,6 +10,7 @@
} }
}, },
"scripts": { "scripts": {
"check-types": "tsc --noEmit",
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {

View File

@@ -14,6 +14,7 @@
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"check-types": "tsc --noEmit",
"lint": "eslint .", "lint": "eslint .",
"db:start": "docker compose up -d", "db:start": "docker compose up -d",
"db:watch": "docker compose up", "db:watch": "docker compose up",

View File

@@ -8,6 +8,7 @@
"./web": "./src/web.ts" "./web": "./src/web.ts"
}, },
"scripts": { "scripts": {
"check-types": "tsc --noEmit",
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {

View File

@@ -1,5 +1,4 @@
import { createEnv } from "@t3-oss/env-core"; import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
export const env = createEnv({ export const env = createEnv({
clientPrefix: "VITE_", clientPrefix: "VITE_",

View File

@@ -1,3 +1,4 @@
import { tanstackConfig } from "@tanstack/eslint-config"; import { tanstackConfig } from "@tanstack/eslint-config";
import eslintConfigPrettier from "eslint-config-prettier/flat";
export default [...tanstackConfig]; export default [...tanstackConfig, eslintConfigPrettier];

View File

@@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@tanstack/eslint-config": "^0.3.4", "@tanstack/eslint-config": "^0.3.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.0", "eslint-plugin-react": "^7.37.0",
"eslint-plugin-react-hooks": "^5.1.0" "eslint-plugin-react-hooks": "^5.1.0"

8930
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
export { default } from "ultracite/stylelint";