feat: implement naming convention, deployment automation, and infrastructure updates

- Add comprehensive naming convention (provider-region-resource-env-purpose)
- Implement Terraform locals for centralized naming
- Update all Terraform resources to use new naming convention
- Create deployment automation framework (18 phase scripts)
- Add Azure setup scripts (provider registration, quota checks)
- Update deployment scripts config with naming functions
- Create complete deployment documentation (guide, steps, quick reference)
- Add frontend portal implementations (public and internal)
- Add UI component library (18 components)
- Enhance Entra VerifiedID integration with file utilities
- Add API client package for all services
- Create comprehensive documentation (naming, deployment, next steps)

Infrastructure:
- Resource groups, storage accounts with new naming
- Terraform configuration updates
- Outputs with naming convention examples

Deployment:
- Automated deployment scripts for all 15 phases
- State management and logging
- Error handling and validation

Documentation:
- Naming convention guide and implementation summary
- Complete deployment guide (296 steps)
- Next steps and quick start guides
- Azure prerequisites and setup completion docs

Note: ESLint warnings present - will be addressed in follow-up commit
This commit is contained in:
defiQUG
2025-11-12 08:22:51 -08:00
parent 9e46f3f316
commit 8649ad4124
136 changed files with 17251 additions and 147 deletions

View File

@@ -13,7 +13,10 @@
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0",
"class-variance-authority": "^0.7.0"
},
"devDependencies": {
"@types/react": "^18.2.45",

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'destructive' | 'success' | 'warning';
}
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
({ className, variant = 'default', ...props }, ref) => {
const variantClasses = {
default: 'bg-background text-foreground border-border',
destructive: 'bg-destructive/10 text-destructive border-destructive/20',
success: 'bg-green-50 text-green-800 border-green-200',
warning: 'bg-yellow-50 text-yellow-800 border-yellow-200',
};
return (
<div
ref={ref}
role="alert"
className={cn(
'relative w-full rounded-lg border p-4',
variantClasses[variant],
className
)}
{...props}
/>
);
}
);
Alert.displayName = 'Alert';
export const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => {
return <h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />;
}
);
AlertTitle.displayName = 'AlertTitle';
export const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
return <div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />;
}
);
AlertDescription.displayName = 'AlertDescription';

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { cn } from '../lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
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',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
success: 'border-transparent bg-green-500 text-white hover:bg-green-600',
warning: 'border-transparent bg-yellow-500 text-white hover:bg-yellow-600',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
export function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import Link from 'next/link';
import { cn } from '../lib/utils';
export interface BreadcrumbItem {
label: string;
href?: string;
}
export interface BreadcrumbsProps {
items: BreadcrumbItem[];
className?: string;
}
export function Breadcrumbs({ items, className }: BreadcrumbsProps) {
return (
<nav aria-label="Breadcrumb" className={cn('flex', className)}>
<ol className="flex items-center space-x-2 text-sm">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<li key={index} className="flex items-center">
{index > 0 && <span className="text-gray-400 mx-2">/</span>}
{isLast ? (
<span className="text-gray-900 font-medium" aria-current="page">
{item.label}
</span>
) : item.href ? (
<Link href={item.href} className="text-gray-600 hover:text-gray-900">
{item.label}
</Link>
) : (
<span className="text-gray-600">{item.label}</span>
)}
</li>
);
})}
</ol>
</nav>
);
}

View File

@@ -1,36 +1,40 @@
import React from 'react';
import { cn } from '../lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
}
const buttonVariants = cva(
'inline-flex items-center justify-center 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: {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
},
size: {
sm: 'h-9 px-3',
md: 'h-10 px-4 py-2',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
className = '',
...props
}) => {
const baseClasses = 'font-medium rounded-lg transition-colors';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-600 text-white hover:bg-gray-700',
outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
>
{children}
</button>
);
};
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
}
);
Button.displayName = 'Button';

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'outline';
}
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, variant = 'default', ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
variant === 'outline' && 'border-2',
className
)}
{...props}
/>
);
}
);
Card.displayName = 'Card';
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
return <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
}
);
CardHeader.displayName = 'CardHeader';
export const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => {
return <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />;
}
);
CardTitle.displayName = 'CardTitle';
export const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
return <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />;
}
);
CardDescription.displayName = 'CardDescription';
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
return <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />;
}
);
CardContent.displayName = 'CardContent';
export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
return <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />;
}
);
CardFooter.displayName = 'CardFooter';

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ className, label, ...props }, ref) => {
return (
<div className="flex items-center space-x-2">
<input
type="checkbox"
className={cn(
'h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2',
className
)}
ref={ref}
{...props}
/>
{label && (
<label htmlFor={props.id} className="text-sm font-medium text-gray-700">
{label}
</label>
)}
</div>
);
}
);
Checkbox.displayName = 'Checkbox';

View File

@@ -0,0 +1,83 @@
import React, { useState, useRef, useEffect } from 'react';
import { cn } from '../lib/utils';
export interface DropdownItem {
label: string;
value: string;
onClick?: () => void;
disabled?: boolean;
divider?: boolean;
}
export interface DropdownProps {
trigger: React.ReactNode;
items: DropdownItem[];
align?: 'left' | 'right';
className?: string;
}
export function Dropdown({ trigger, items, align = 'left', className }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const handleItemClick = (item: DropdownItem) => {
if (item.disabled) return;
item.onClick?.();
setIsOpen(false);
};
return (
<div className={cn('relative inline-block', className)} ref={dropdownRef}>
<div onClick={() => setIsOpen(!isOpen)} className="cursor-pointer">
{trigger}
</div>
{isOpen && (
<div
className={cn(
'absolute z-50 mt-2 min-w-[200px] rounded-md border bg-white shadow-lg',
align === 'right' ? 'right-0' : 'left-0'
)}
>
<div className="py-1">
{items.map((item, index) => {
if (item.divider) {
return <div key={index} className="my-1 border-t" />;
}
return (
<button
key={index}
type="button"
onClick={() => handleItemClick(item)}
disabled={item.disabled}
className={cn(
'w-full text-left px-4 py-2 text-sm hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed',
item.disabled && 'text-gray-400'
)}
>
{item.label}
</button>
);
})}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export 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-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ className, ...props }, ref) => {
return (
<label
ref={ref}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
{...props}
/>
);
}
);
Label.displayName = 'Label';

View File

@@ -0,0 +1,125 @@
import React, { useEffect } from 'react';
import { cn } from '../lib/utils';
import { Button } from './Button';
export interface ModalProps {
open: boolean;
onClose: () => void;
title?: string;
description?: string;
children: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
showCloseButton?: boolean;
}
export function Modal({
open,
onClose,
title,
description,
children,
size = 'md',
showCloseButton = true,
}: ModalProps) {
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [open]);
if (!open) return null;
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={(e) => {
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div className="fixed inset-0 bg-black/50" />
<div
className={cn(
'relative bg-white rounded-lg shadow-xl w-full mx-4',
sizeClasses[size],
'animate-in fade-in zoom-in-95'
)}
onClick={(e) => e.stopPropagation()}
>
{(title || showCloseButton) && (
<div className="flex items-center justify-between p-6 border-b">
<div>
{title && <h2 className="text-xl font-semibold">{title}</h2>}
{description && <p className="text-sm text-gray-600 mt-1">{description}</p>}
</div>
{showCloseButton && (
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
aria-label="Close"
>
×
</button>
)}
</div>
)}
<div className="p-6">{children}</div>
</div>
</div>
);
}
export interface ConfirmModalProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: 'default' | 'destructive';
}
export function ConfirmModal({
open,
onClose,
onConfirm,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'default',
}: ConfirmModalProps) {
const handleConfirm = () => {
onConfirm();
onClose();
};
return (
<Modal open={open} onClose={onClose} title={title} size="sm">
<p className="text-gray-700 mb-6">{message}</p>
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={onClose}>
{cancelText}
</Button>
<Button variant={variant === 'destructive' ? 'destructive' : 'primary'} onClick={handleConfirm}>
{confirmText}
</Button>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface RadioProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
}
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
({ className, label, ...props }, ref) => {
return (
<div className="flex items-center space-x-2">
<input
type="radio"
className={cn(
'h-4 w-4 border-gray-300 text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2',
className
)}
ref={ref}
{...props}
/>
{label && (
<label htmlFor={props.id} className="text-sm font-medium text-gray-700">
{label}
</label>
)}
</div>
);
}
);
Radio.displayName = 'Radio';

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
>
{children}
</select>
);
}
);
Select.displayName = 'Select';

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
export function Skeleton({ className, ...props }: SkeletonProps) {
return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />;
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: string;
}
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
({ className, label, ...props }, ref) => {
return (
<div className="flex items-center space-x-2">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
ref={ref}
{...props}
/>
<div
className={cn(
"w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary",
className
)}
/>
</label>
{label && (
<span className="text-sm font-medium text-gray-700">{label}</span>
)}
</div>
);
}
);
Switch.displayName = 'Switch';

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { cn } from '../lib/utils';
export const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => {
return (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
);
}
);
Table.displayName = 'Table';
export const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => {
return <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />;
});
TableHeader.displayName = 'TableHeader';
export const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => {
return <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />;
});
TableBody.displayName = 'TableBody';
export const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => {
return (
<tr
ref={ref}
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
{...props}
/>
);
}
);
TableRow.displayName = 'TableRow';
export const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => {
return (
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
);
});
TableHead.displayName = 'TableHead';
export const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => {
return (
<td ref={ref} className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)} {...props} />
);
});
TableCell.displayName = 'TableCell';

View File

@@ -0,0 +1,118 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { cn } from '../lib/utils';
interface TabsContextType {
value: string;
onValueChange: (value: string) => void;
}
const TabsContext = createContext<TabsContextType | undefined>(undefined);
export interface TabsProps {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
children: ReactNode;
className?: string;
}
export function Tabs({ defaultValue, value: controlledValue, onValueChange, children, className }: TabsProps) {
const [internalValue, setInternalValue] = useState(defaultValue || '');
const isControlled = controlledValue !== undefined;
const currentValue = isControlled ? controlledValue : internalValue;
const handleValueChange = (newValue: string) => {
if (!isControlled) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
};
return (
<TabsContext.Provider value={{ value: currentValue, onValueChange: handleValueChange }}>
<div className={cn('w-full', className)}>{children}</div>
</TabsContext.Provider>
);
}
export interface TabsListProps {
children: ReactNode;
className?: string;
}
export function TabsList({ children, className }: TabsListProps) {
return (
<div
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className
)}
role="tablist"
>
{children}
</div>
);
}
export interface TabsTriggerProps {
value: string;
children: ReactNode;
className?: string;
}
export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
const context = useContext(TabsContext);
if (!context) {
throw new Error('TabsTrigger must be used within Tabs');
}
const { value: currentValue, onValueChange } = context;
const isActive = currentValue === value;
return (
<button
type="button"
role="tab"
aria-selected={isActive}
onClick={() => onValueChange(value)}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
isActive
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:bg-background/50',
className
)}
>
{children}
</button>
);
}
export interface TabsContentProps {
value: string;
children: ReactNode;
className?: string;
}
export function TabsContent({ value, children, className }: TabsContentProps) {
const context = useContext(TabsContext);
if (!context) {
throw new Error('TabsContent must be used within Tabs');
}
const { value: currentValue } = context;
if (currentValue !== value) return null;
return (
<div
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
role="tabpanel"
>
{children}
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { cn } from '../lib/utils';
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = 'Textarea';

View File

@@ -0,0 +1,144 @@
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { cn } from '../lib/utils';
import { Alert, AlertDescription } from './Alert';
export type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
title?: string;
message: string;
variant: ToastVariant;
duration?: number;
}
interface ToastContextType {
toasts: Toast[];
addToast: (toast: Omit<Toast, 'id'>) => void;
removeToast: (id: string) => void;
success: (message: string, title?: string) => void;
error: (message: string, title?: string) => void;
warning: (message: string, title?: string) => void;
info: (message: string, title?: string) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export function useToast() {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
const addToast = useCallback(
(toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substring(2, 9);
const newToast: Toast = {
...toast,
id,
duration: toast.duration || 5000,
};
setToasts((prev) => [...prev, newToast]);
if (newToast.duration && newToast.duration > 0) {
setTimeout(() => {
removeToast(id);
}, newToast.duration);
}
},
[removeToast]
);
const success = useCallback(
(message: string, title?: string) => {
addToast({ message, title, variant: 'success' });
},
[addToast]
);
const error = useCallback(
(message: string, title?: string) => {
addToast({ message, title, variant: 'error' });
},
[addToast]
);
const warning = useCallback(
(message: string, title?: string) => {
addToast({ message, title, variant: 'warning' });
},
[addToast]
);
const info = useCallback(
(message: string, title?: string) => {
addToast({ message, title, variant: 'info' });
},
[addToast]
);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast, success, error, warning, info }}>
{children}
<ToastContainer toasts={toasts} removeToast={removeToast} />
</ToastContext.Provider>
);
}
function ToastContainer({ toasts, removeToast }: { toasts: Toast[]; removeToast: (id: string) => void }) {
if (toasts.length === 0) return null;
return (
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 w-full max-w-sm">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onRemove={removeToast} />
))}
</div>
);
}
function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) {
const variantMap: Record<ToastVariant, { alert: 'default' | 'destructive' | 'success' | 'warning'; icon: string }> = {
default: { alert: 'default', icon: '' },
success: { alert: 'success', icon: '✓' },
error: { alert: 'destructive', icon: '✗' },
warning: { alert: 'warning', icon: '⚠' },
info: { alert: 'default', icon: '' },
};
const variant = variantMap[toast.variant];
return (
<Alert
variant={variant.alert}
className="animate-in slide-in-from-top-5 fade-in shadow-lg"
>
<div className="flex items-start justify-between">
<div className="flex-1">
{toast.title && (
<AlertDescription className="font-semibold mb-1">{toast.title}</AlertDescription>
)}
<AlertDescription>{toast.message}</AlertDescription>
</div>
<button
onClick={() => onRemove(toast.id)}
className="ml-4 text-gray-500 hover:text-gray-700 text-xl leading-none"
aria-label="Close"
>
×
</button>
</div>
</Alert>
);
}

View File

@@ -4,4 +4,21 @@
// Export components here as they are created
export { Button } from './Button';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
export { Input } from './Input';
export { Label } from './Label';
export { Select } from './Select';
export { Textarea } from './Textarea';
export { Alert, AlertTitle, AlertDescription } from './Alert';
export { Badge } from './Badge';
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './Table';
export { Skeleton } from './Skeleton';
export { ToastProvider, useToast } from './Toast';
export { Modal, ConfirmModal } from './Modal';
export { Breadcrumbs } from './Breadcrumbs';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs';
export { Checkbox } from './Checkbox';
export { Radio } from './Radio';
export { Switch } from './Switch';
export { Dropdown } from './Dropdown';

View File

@@ -3,4 +3,5 @@
*/
export * from './components';
export * from './lib/utils';

View File

@@ -0,0 +1,7 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}