From 984f1aeb8d82adf0945fc9253cecca55b3d75450 Mon Sep 17 00:00:00 2001 From: Consultoria AS Date: Thu, 22 Jan 2026 02:00:33 +0000 Subject: [PATCH] feat(web): add base UI components - Button with variants (default, destructive, outline, secondary, ghost, link, success) - Input with consistent styling - Card with Header, Title, Description, Content, Footer - Label using Radix UI primitives - Index file for centralized exports Co-Authored-By: Claude Opus 4.5 --- apps/web/components/ui/button.tsx | 53 +++++++++++++++++++++ apps/web/components/ui/card.tsx | 78 +++++++++++++++++++++++++++++++ apps/web/components/ui/index.ts | 4 ++ apps/web/components/ui/input.tsx | 24 ++++++++++ apps/web/components/ui/label.tsx | 25 ++++++++++ 5 files changed, 184 insertions(+) create mode 100644 apps/web/components/ui/button.tsx create mode 100644 apps/web/components/ui/card.tsx create mode 100644 apps/web/components/ui/index.ts create mode 100644 apps/web/components/ui/input.tsx create mode 100644 apps/web/components/ui/label.tsx diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx new file mode 100644 index 0000000..0c0d311 --- /dev/null +++ b/apps/web/components/ui/button.tsx @@ -0,0 +1,53 @@ +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-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + success: 'bg-success text-success-foreground hover:bg-success/90', + }, + 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, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx new file mode 100644 index 0000000..81bd82d --- /dev/null +++ b/apps/web/components/ui/card.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = 'CardFooter'; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/apps/web/components/ui/index.ts b/apps/web/components/ui/index.ts new file mode 100644 index 0000000..cddb527 --- /dev/null +++ b/apps/web/components/ui/index.ts @@ -0,0 +1,4 @@ +export { Button, buttonVariants, type ButtonProps } from './button'; +export { Input, type InputProps } from './input'; +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './card'; +export { Label } from './label'; diff --git a/apps/web/components/ui/input.tsx b/apps/web/components/ui/input.tsx new file mode 100644 index 0000000..b651d6b --- /dev/null +++ b/apps/web/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/apps/web/components/ui/label.tsx b/apps/web/components/ui/label.tsx new file mode 100644 index 0000000..9bb4934 --- /dev/null +++ b/apps/web/components/ui/label.tsx @@ -0,0 +1,25 @@ +'use client'; + +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label };