Visual redesign: design system, refined ops console aesthetic

- Full component library (Button, Input, Label, Select, Modal, Table, Badge, Card, etc.)
- Tailwind design tokens: IBM Plex Sans + JetBrains Mono, teal accent, semantic status colors
- NavBar with logo, responsive hamburger menu, real logout
- All pages redesigned: Login, Dashboard (KPI cards), Machines, SyncPairs, JobHistory, JobDetail, SSHKeys, Settings
- Fixed: hover:bg-gray-750 dead class, window.location.href navigation bug
- Replaced alert()/confirm() with sonner toasts and accessible modals
- Added ErrorBoundary, skip link, accessible modal dialogs (Radix)
- Icons: lucide-react throughout, copy/download buttons
- 1.0.5 → 1.0.6
This commit is contained in:
2026-07-07 23:12:34 -04:00
parent 93c22e844e
commit 9cef7173cb
36 changed files with 4069 additions and 652 deletions
+62
View File
@@ -0,0 +1,62 @@
import * as React from 'react'
import { AlertTriangle, RefreshCw } from 'lucide-react'
import { Button } from './ui/Button'
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
interface ErrorBoundaryProps {
children: React.ReactNode
fallback?: React.ReactNode
}
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo)
}
handleReload = () => {
this.setState({ hasError: false, error: null })
window.location.reload()
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4 p-8 text-center">
<div className="rounded-full bg-rose-500/10 p-4 text-rose-400">
<AlertTriangle className="h-8 w-8" />
</div>
<div className="space-y-2">
<h2 className="text-lg font-semibold text-fg">Something went wrong</h2>
<p className="text-sm text-fg-muted max-w-md">
{this.state.error?.message ?? 'An unexpected error occurred'}
</p>
</div>
<Button variant="secondary" onClick={this.handleReload} className="gap-2">
<RefreshCw className="h-4 w-4" />
Reload page
</Button>
</div>
)
}
return this.props.children
}
}
export { ErrorBoundary }
+30
View File
@@ -0,0 +1,30 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import { badgeVariants, type BadgeVariant } from '@/lib/status'
import { statusLabel } from '@/lib/status'
interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {
label?: string
}
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant, label, children, ...props }, ref) => {
const displayLabel = label ?? (typeof variant === 'string' ? statusLabel(variant) : children)
return (
<span
ref={ref}
className={cn(badgeVariants({ variant }), className)}
{...props}
>
{displayLabel}
</span>
)
}
)
Badge.displayName = 'Badge'
export { Badge, badgeVariants }
export type { BadgeVariant }
+66
View File
@@ -0,0 +1,66 @@
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'
import { Loader2 } from 'lucide-react'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-card font-medium transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]',
{
variants: {
variant: {
primary:
'bg-accent text-accent-foreground hover:bg-accent-hover shadow-sm hover:shadow-glow',
secondary:
'bg-surface-raised text-fg border border-border hover:bg-surface-hover hover:border-border/80',
ghost:
'text-fg-muted hover:text-fg hover:bg-surface-raised',
danger:
'bg-rose-500/15 text-rose-400 border border-rose-500/30 hover:bg-rose-500/25 hover:border-rose-500/50 hover:shadow-glow-error',
'danger-solid':
'bg-rose-500 text-white hover:bg-rose-600 shadow-sm',
link:
'text-accent underline-offset-4 hover:underline hover:text-accent-hover',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-9 px-4 text-sm',
lg: 'h-11 px-6 text-base',
icon: 'h-9 w-9',
'icon-sm': 'h-8 w-8',
'icon-lg': 'h-10 w-10',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, disabled, asChild = false, children, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={asChild ? undefined : (disabled || loading)}
{...props}
>
{loading && !asChild && <Loader2 className="h-4 w-4 animate-spin" />}
{children}
</Comp>
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }
+75
View File
@@ -0,0 +1,75 @@
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-card border border-border bg-surface shadow-card',
className
)}
{...props}
/>
))
Card.displayName = 'Card'
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col gap-1 p-5', className)}
{...props}
/>
))
CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-sm font-semibold text-fg', className)}
{...props}
/>
))
CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-xs text-fg-muted', className)}
{...props}
/>
))
CardDescription.displayName = 'CardDescription'
const CardBody = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
))
CardBody.displayName = 'CardBody'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center gap-3 p-5 pt-0', className)}
{...props}
/>
))
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter }
+55
View File
@@ -0,0 +1,55 @@
import * as React from 'react'
import { Check, Copy } from 'lucide-react'
import { Button } from './Button'
import { cn } from '@/lib/utils'
interface CopyButtonProps
extends Omit<React.ComponentProps<typeof Button>, 'variant' | 'size' | 'children'> {
text: string
displayText?: string
successDuration?: number
}
const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
(
{ className, text, displayText, successDuration = 2000, ...props },
ref
) => {
const [copied, setCopied] = React.useState(false)
const handleCopy = React.useCallback(async () => {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), successDuration)
} catch {
}
}, [text, successDuration])
return (
<Button
ref={ref}
variant="ghost"
size="sm"
onClick={handleCopy}
className={cn('gap-1.5 text-fg-muted hover:text-fg', className)}
{...props}
>
{copied ? (
<>
<Check className="h-3.5 w-3.5 text-emerald-400" />
<span className="text-emerald-400">Copied</span>
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
<span>{displayText ?? 'Copy'}</span>
</>
)}
</Button>
)
}
)
CopyButton.displayName = 'CopyButton'
export { CopyButton }
+38
View File
@@ -0,0 +1,38 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
icon?: React.ReactNode
title: string
description?: string
action?: React.ReactNode
}
const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
({ className, icon, title, description, action, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex flex-col items-center justify-center gap-3 py-12 text-center',
className
)}
{...props}
>
{icon && (
<div className="rounded-full bg-surface-raised p-3 text-fg-subtle">
{icon}
</div>
)}
<div className="space-y-1">
<p className="text-sm font-medium text-fg">{title}</p>
{description && (
<p className="text-xs text-fg-muted max-w-xs">{description}</p>
)}
</div>
{action && <div className="mt-1">{action}</div>}
</div>
)
)
EmptyState.displayName = 'EmptyState'
export { EmptyState }
+29
View File
@@ -0,0 +1,29 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, error, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-card border bg-surface-raised px-3 py-2 text-sm text-fg placeholder:text-fg-subtle transition-colors',
'border-border hover:border-border/80',
'focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/50',
'disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-rose-500/50 focus:ring-rose-500/30 focus:border-rose-500/50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }
+25
View File
@@ -0,0 +1,25 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
required?: boolean
}
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ className, required, children, ...props }, ref) => (
<label
ref={ref}
className={cn(
'text-sm font-medium text-fg-muted leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
{...props}
>
{children}
{required && <span className="ml-1 text-rose-400">*</span>}
</label>
)
)
Label.displayName = 'Label'
export { Label }
+132
View File
@@ -0,0 +1,132 @@
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const Modal = DialogPrimitive.Root
const ModalTrigger = DialogPrimitive.Trigger
const ModalPortal = DialogPrimitive.Portal
const ModalClose = DialogPrimitive.Close
const ModalOverlay = 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/60 backdrop-blur-sm',
'data-[state=open]:animate-fade-in',
className
)}
{...props}
/>
))
ModalOverlay.displayName = DialogPrimitive.Overlay.displayName
const ModalContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
size?: 'sm' | 'md' | 'lg' | 'xl'
}
>(({ className, children, size = 'md', ...props }, ref) => {
const sizes = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
}
return (
<ModalPortal>
<ModalOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]',
'w-full max-h-[90vh] overflow-y-auto rounded-card border border-border bg-surface shadow-card animate-scale-in',
'focus:outline-none',
sizes[size],
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-accent/40 focus:ring-offset-2 focus:ring-offset-surface disabled:pointer-events-none">
<X className="h-4 w-4 text-fg-muted" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</ModalPortal>
)
})
ModalContent.displayName = DialogPrimitive.Content.displayName
const ModalHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col gap-1.5 p-5 pb-0', className)}
{...props}
/>
))
ModalHeader.displayName = 'ModalHeader'
const ModalTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-base font-semibold text-fg', className)}
{...props}
/>
))
ModalTitle.displayName = DialogPrimitive.Title.displayName
const ModalDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-fg-muted', className)}
{...props}
/>
))
ModalDescription.displayName = DialogPrimitive.Description.displayName
const ModalBody = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-5', className)} {...props} />
))
ModalBody.displayName = 'ModalBody'
const ModalFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center justify-end gap-3 p-5 pt-0', className)}
{...props}
/>
))
ModalFooter.displayName = 'ModalFooter'
export {
Modal,
ModalPortal,
ModalOverlay,
ModalTrigger,
ModalClose,
ModalContent,
ModalHeader,
ModalTitle,
ModalDescription,
ModalBody,
ModalFooter,
}
+36
View File
@@ -0,0 +1,36 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
title: string
description?: string
actions?: React.ReactNode
}
const PageHeader = React.forwardRef<HTMLDivElement, PageHeaderProps>(
({ className, title, description, actions, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between',
className
)}
{...props}
>
<div className="flex-1 min-w-0">
<h1 className="text-2xl font-bold text-fg tracking-tight">{title}</h1>
{description && (
<p className="mt-1 text-sm text-fg-muted">{description}</p>
)}
</div>
{actions && (
<div className="flex items-center gap-2 mt-3 sm:mt-0 shrink-0">
{actions}
</div>
)}
</div>
)
)
PageHeader.displayName = 'PageHeader'
export { PageHeader }
+95
View File
@@ -0,0 +1,95 @@
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { ChevronDown, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-9 w-full items-center justify-between rounded-card border border-border bg-surface-raised px-3 py-2 text-sm text-fg placeholder:text-fg-subtle transition-colors',
'hover:border-border/80 focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/50',
'disabled:cursor-not-allowed disabled:opacity-50',
' [&>span]:line-clamp-1',
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 text-fg-subtle" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-card border border-border bg-surface shadow-card animate-scale-in',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-pointer select-none items-center rounded px-2 py-1.5 text-sm text-fg-muted outline-none',
'hover:bg-surface-raised hover:text-fg',
'focus:bg-surface-raised focus:text-fg',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4 text-accent" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectItem,
}
+34
View File
@@ -0,0 +1,34 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'text' | 'circular' | 'rectangular'
width?: string | number
height?: string | number
}
const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
({ className, variant = 'rectangular', width, height, style, ...props }, ref) => {
const baseClass =
variant === 'circular'
? 'rounded-full'
: variant === 'text'
? 'rounded'
: 'rounded-card'
return (
<div
ref={ref}
className={cn(
'animate-pulse bg-surface-raised',
baseClass,
className
)}
style={{ width, height, ...style }}
{...props}
/>
)
}
)
Skeleton.displayName = 'Skeleton'
export { Skeleton }
+46
View File
@@ -0,0 +1,46 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Spinner = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { size?: 'sm' | 'md' | 'lg' }
>(({ className, size = 'md', ...props }, ref) => {
const sizes = {
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8',
}
return (
<div
ref={ref}
role="status"
aria-label="Loading"
className={cn('animate-spin text-accent', sizes[size], className)}
{...props}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="h-full w-full"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</div>
)
})
Spinner.displayName = 'Spinner'
export { Spinner }
+30
View File
@@ -0,0 +1,30 @@
import * as React from 'react'
import * as SwitchPrimitive from '@radix-ui/react-switch'
import { cn } from '@/lib/utils'
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitive.Root
ref={ref}
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:bg-accent data-[state=unchecked]:bg-surface-raised data-[state=unchecked]:border-border',
className
)}
{...props}
>
<SwitchPrimitive.Thumb
className={cn(
'pointer-events-none block h-4 w-4 rounded-full bg-fg-subtle shadow-lg ring-0 transition-transform',
'translate-x-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>
))
Switch.displayName = SwitchPrimitive.Root.displayName
export { Switch }
+118
View File
@@ -0,0 +1,118 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
))
Table.displayName = 'Table'
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
))
TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
'border-t bg-surface-raised font-medium [&>tr]:last:border-b-0',
className
)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b border-border/50 transition-colors',
'hover:bg-surface-raised/50',
className
)}
{...props}
/>
))
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-10 px-4 text-left align-middle text-xs font-semibold text-fg-muted uppercase tracking-wider',
'[&:has([role=checkbox])]:pr-0',
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('px-4 py-3 align-middle text-sm text-fg', '[&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-fg-muted', className)}
{...props}
/>
))
TableCaption.displayName = 'TableCaption'
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
+29
View File
@@ -0,0 +1,29 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
error?: boolean
}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, error, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-card border bg-surface-raised px-3 py-2 text-sm text-fg placeholder:text-fg-subtle transition-colors resize-none',
'border-border hover:border-border/80',
'focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/50',
'disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-rose-500/50 focus:ring-rose-500/30 focus:border-rose-500/50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }
+36
View File
@@ -0,0 +1,36 @@
export { Button, buttonVariants } from './Button'
export { Badge, badgeVariants } from './Badge'
export { Spinner } from './Spinner'
export { Skeleton } from './Skeleton'
export { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter } from './Card'
export { Input } from './Input'
export { Label } from './Label'
export { Textarea } from './Textarea'
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectItem } from './Select'
export { Switch } from './Switch'
export {
Modal,
ModalPortal,
ModalOverlay,
ModalTrigger,
ModalClose,
ModalContent,
ModalHeader,
ModalTitle,
ModalDescription,
ModalBody,
ModalFooter,
} from './Modal'
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
} from './Table'
export { EmptyState } from './EmptyState'
export { PageHeader } from './PageHeader'
export { CopyButton } from './CopyButton'