feat: add reusable UI components and install dependencies

This commit is contained in:
rajun 2026-07-28 10:57:27 +05:30
parent 8b0c699d76
commit e9aad1afe3
10 changed files with 1299 additions and 57 deletions

View File

@ -0,0 +1,37 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none uppercase tracking-wider',
{
variants: {
variant: {
default:
'border-transparent bg-neutral-800 text-white hover:bg-neutral-700',
secondary:
'border-transparent bg-neutral-900 text-neutral-400 hover:bg-neutral-850',
destructive:
'border-transparent bg-red-500/10 text-red-500 border-red-500/20',
outline: 'border-neutral-800 text-neutral-400',
orange:
'border-orange-500/20 bg-orange-500/5 text-orange-500 font-bold',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@ -0,0 +1,52 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold transition-all duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-700 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98] cursor-pointer',
{
variants: {
variant: {
default: 'bg-orange-500 text-black shadow-[0_0_24px_rgba(249,115,22,0.15)] hover:bg-orange-400 hover:shadow-[0_0_32px_rgba(249,115,22,0.3)]',
destructive: 'bg-red-500/10 text-red-500 border border-red-500/20 hover:bg-red-500 hover:text-white',
outline: 'border border-neutral-800 bg-[#121212] text-neutral-400 hover:bg-neutral-900 hover:text-white hover:border-orange-500/40',
secondary: 'bg-neutral-800 text-neutral-300 hover:bg-neutral-750 hover:text-white',
ghost: 'hover:bg-neutral-850 hover:text-white text-neutral-400',
link: 'text-orange-500 underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-5 py-2.5',
sm: 'h-8 rounded-lg px-3 text-xs',
lg: 'h-12 rounded-xl px-8 text-base',
icon: 'h-9 w-9 rounded-lg',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };

View File

@ -0,0 +1,78 @@
import * as React from 'react';
import { cn } from '~/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-2xl border border-neutral-900 bg-[#121212] text-white shadow-2xl transition-all duration-300',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
'text-lg font-bold tracking-tight text-white uppercase',
className
)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-xs text-neutral-500 uppercase tracking-wider', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '~/components/ui/dialog';
import { Button } from '~/components/ui/button';
import { AlertTriangle, RefreshCw } from 'lucide-react';
interface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmText?: string;
cancelText?: string;
variant?: 'destructive' | 'default';
loading?: boolean;
onConfirm: () => void;
}
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'destructive',
loading = false,
onConfirm,
}: ConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md bg-[#121212] border-neutral-850 p-6">
<DialogHeader className="space-y-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-red-500/10 border border-red-500/20 flex items-center justify-center text-red-500 shrink-0">
<AlertTriangle size={20} />
</div>
<div>
<DialogTitle className="text-base font-bold text-white uppercase tracking-wider">
{title}
</DialogTitle>
<DialogDescription className="text-xs text-neutral-400 mt-1">
{description}
</DialogDescription>
</div>
</div>
</DialogHeader>
<DialogFooter className="mt-6 flex flex-row items-center justify-end gap-3">
<Button
type="button"
variant="outline"
size="sm"
disabled={loading}
onClick={() => onOpenChange(false)}
className="h-9"
>
{cancelText}
</Button>
<Button
type="button"
variant={variant}
size="sm"
disabled={loading}
onClick={onConfirm}
className="h-9"
>
{loading ? (
<RefreshCw size={14} className="animate-spin" />
) : (
confirmText
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,119 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '~/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/85 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 w-full translate-x-[-50%] translate-y-[-50%] border border-neutral-850 bg-[#121212] shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-1/2 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-1/2 rounded-2xl',
!className?.includes('flex') && !className?.includes('grid') && 'grid gap-4',
!className?.includes('max-w-') && 'max-w-lg',
!className?.includes('p-') && 'p-0 p-6', // default padding if not overridden
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-full p-2 bg-black/60 border border-neutral-850 text-neutral-400 hover:text-white hover:bg-neutral-800 transition-all cursor-pointer opacity-70 hover:opacity-100 focus:outline-none disabled:pointer-events-none z-20">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-1.5 text-center sm:text-left',
className
)}
{...props}
/>
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold leading-none tracking-tight text-white',
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-neutral-400', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import { cn } from '~/lib/utils';
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-xl border border-neutral-800 bg-[#181818] px-4 py-3 text-sm text-white placeholder-neutral-600 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-white focus-visible:outline-none focus-visible:border-orange-500 disabled:cursor-not-allowed disabled:opacity-50 transition-colors duration-200',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
export { Input };

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/lib/utils';
const labelVariants = cva(
'text-xs font-bold text-neutral-400 uppercase tracking-wider select-none leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
);
const Label = React.forwardRef<
HTMLLabelElement,
React.LabelHTMLAttributes<HTMLLabelElement> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<label ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = 'Label';
export { Label };

View File

@ -0,0 +1,15 @@
import { cn } from '~/lib/utils';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-xl bg-neutral-900', className)}
{...props}
/>
);
}
export { Skeleton };

922
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,15 +9,24 @@
"typecheck": "react-router typegen && tsc" "typecheck": "react-router typegen && tsc"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.4.0",
"@radix-ui/react-dialog": "^1.1.20",
"@radix-ui/react-slot": "^1.3.0",
"@react-router/node": "^7.9.2", "@react-router/node": "^7.9.2",
"@react-router/serve": "^7.9.2", "@react-router/serve": "^7.9.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"isbot": "^5.1.31", "isbot": "^5.1.31",
"lucide-react": "^0.555.0", "lucide-react": "^0.555.0",
"pocketbase": "^0.26.8", "pocketbase": "^0.26.8",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router": "^7.9.2" "react-hook-form": "^7.82.0",
"react-router": "^7.9.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@react-router/dev": "^7.9.2", "@react-router/dev": "^7.9.2",