add new components
This commit is contained in:
@@ -1,15 +1,3 @@
|
|||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {}
|
||||||
"PostToolUse": [
|
|
||||||
{
|
|
||||||
"matcher": "Write|Edit",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "pnpm dlx ultracite fix"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
**/routeTree.gen.ts
|
||||||
|
pnpm-lock.yaml
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
100
apps/web/src/components/ui/field.tsx
Normal file
100
apps/web/src/components/ui/field.tsx
Normal 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,
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
31
apps/web/src/components/ui/separator.tsx
Normal file
31
apps/web/src/components/ui/separator.tsx
Normal 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 };
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 *));
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
140
apps/web/src/routeTree.gen.ts
Normal file
140
apps/web/src/routeTree.gen.ts
Normal 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>>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
171
apps/web/src/routes/demo.tsx
Normal file
171
apps/web/src/routes/demo.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
1
packages/env/package.json
vendored
1
packages/env/package.json
vendored
@@ -8,6 +8,7 @@
|
|||||||
"./web": "./src/web.ts"
|
"./web": "./src/web.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
1
packages/env/src/web.ts
vendored
1
packages/env/src/web.ts
vendored
@@ -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_",
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
9044
pnpm-lock.yaml
generated
9044
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
export { default } from "ultracite/stylelint";
|
|
||||||
Reference in New Issue
Block a user