feat(ui): add shadcn/ui base components

- Add clsx, tailwind-merge, class-variance-authority dependencies
- Add Radix UI primitives (dialog, dropdown-menu, label, select, tabs, toast, slot)
- Add lucide-react for icons
- Create components.json for shadcn/ui configuration
- Create lib/utils.ts with cn(), formatCurrency(), formatDate(), formatTime()
- Create Button component with variants (default, destructive, outline, secondary, ghost, link, accent)
- Create Input component with focus ring styling
- Create Card components (Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ivan
2026-02-01 06:26:25 +00:00
parent a7284925e6
commit d1cc0c0b58
7 changed files with 3212 additions and 1 deletions

20
apps/web/components.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@@ -0,0 +1,61 @@
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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-white hover:bg-primary-700 active:bg-primary-800",
destructive:
"bg-red-500 text-white hover:bg-red-600 active:bg-red-700",
outline:
"border border-primary-200 bg-white text-primary hover:bg-primary-50 active:bg-primary-100",
secondary:
"bg-primary-100 text-primary-800 hover:bg-primary-200 active:bg-primary-300",
ghost:
"text-primary hover:bg-primary-50 active:bg-primary-100",
link:
"text-primary underline-offset-4 hover:underline",
accent:
"bg-accent text-white hover:bg-accent-600 active:bg-accent-700",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
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,79 @@
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-lg border border-primary-200 bg-white text-primary-900 shadow-sm",
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-2xl font-semibold leading-none tracking-tight text-primary-800",
className
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-primary-500", 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,25 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-primary-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = "Input";
export { Input };

45
apps/web/lib/utils.ts Normal file
View File

@@ -0,0 +1,45 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Merge Tailwind CSS classes with proper precedence
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Format a number as Mexican peso currency
*/
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat("es-MX", {
style: "currency",
currency: "MXN",
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(amount);
}
/**
* Format a date in Spanish format (e.g., "15 de enero de 2024")
*/
export function formatDate(date: Date | string): string {
const dateObj = typeof date === "string" ? new Date(date) : date;
return new Intl.DateTimeFormat("es-MX", {
day: "numeric",
month: "long",
year: "numeric",
}).format(dateObj);
}
/**
* Format time in 12-hour format with AM/PM
*/
export function formatTime(date: Date | string): string {
const dateObj = typeof date === "string" ? new Date(date) : date;
return new Intl.DateTimeFormat("es-MX", {
hour: "numeric",
minute: "2-digit",
hour12: true,
}).format(dateObj);
}

2970
apps/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,11 +14,22 @@
},
"dependencies": {
"@prisma/client": "^5.10.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.330.0",
"next": "14.2.0",
"next-auth": "^4.24.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",