Files
Commander-Deck-App-backup/frontend/src/components/ui/index.tsx
T

298 lines
10 KiB
TypeScript

import React from 'react'
import { Loader2 } from 'lucide-react'
// ─── Button ───────────────────────────────────────────────────────────────────
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
type ButtonSize = 'sm' | 'md' | 'lg'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
loading?: boolean
icon?: React.ReactNode
}
const variantClasses: Record<ButtonVariant, string> = {
primary:
'bg-accent-violet hover:bg-accent-violet-light text-white shadow-glow hover:shadow-lg transition-all',
secondary:
'bg-bg-elevated border border-bg-border text-text-primary hover:border-accent-violet hover:text-accent-violet-light transition-all',
ghost: 'text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-all',
danger: 'bg-danger/20 border border-danger/50 text-danger hover:bg-danger/30 transition-all',
}
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
}
export function Button({
variant = 'primary',
size = 'md',
loading,
icon,
children,
disabled,
className = '',
...props
}: ButtonProps) {
return (
<button
{...props}
disabled={disabled || loading}
className={`
inline-flex items-center gap-2 rounded-md font-medium
disabled:opacity-50 disabled:cursor-not-allowed
${variantClasses[variant]} ${sizeClasses[size]} ${className}
`}
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : icon}
{children}
</button>
)
}
// ─── Input ────────────────────────────────────────────────────────────────────
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string
error?: string
hint?: string
}
export function Input({ label, error, hint, className = '', ...props }: InputProps) {
return (
<div className="flex flex-col gap-1">
{label && (
<label className="text-xs font-medium text-text-secondary uppercase tracking-wider">
{label}
</label>
)}
<input
{...props}
className={`
bg-bg-elevated border rounded-md px-3 py-2 text-sm text-text-primary
placeholder:text-text-muted
focus:outline-none focus:ring-2 focus:ring-accent-violet focus:border-transparent
${error ? 'border-danger' : 'border-bg-border'}
${className}
`}
/>
{error && <p className="text-xs text-danger">{error}</p>}
{hint && !error && <p className="text-xs text-text-muted">{hint}</p>}
</div>
)
}
// ─── Textarea ─────────────────────────────────────────────────────────────────
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string
error?: string
hint?: string
}
export function Textarea({ label, error, hint, className = '', ...props }: TextareaProps) {
return (
<div className="flex flex-col gap-1">
{label && (
<label className="text-xs font-medium text-text-secondary uppercase tracking-wider">
{label}
</label>
)}
<textarea
{...props}
className={`
bg-bg-elevated border rounded-md px-3 py-2 text-sm text-text-primary
placeholder:text-text-muted resize-none
focus:outline-none focus:ring-2 focus:ring-accent-violet focus:border-transparent
${error ? 'border-danger' : 'border-bg-border'}
${className}
`}
/>
{error && <p className="text-xs text-danger">{error}</p>}
{hint && !error && <p className="text-xs text-text-muted">{hint}</p>}
</div>
)
}
// ─── Select ───────────────────────────────────────────────────────────────────
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
label?: string
error?: string
}
export function Select({ label, error, className = '', children, ...props }: SelectProps) {
return (
<div className="flex flex-col gap-1">
{label && (
<label className="text-xs font-medium text-text-secondary uppercase tracking-wider">
{label}
</label>
)}
<select
{...props}
className={`
bg-bg-elevated border rounded-md px-3 py-2 text-sm text-text-primary
focus:outline-none focus:ring-2 focus:ring-accent-violet focus:border-transparent
${error ? 'border-danger' : 'border-bg-border'}
${className}
`}
>
{children}
</select>
{error && <p className="text-xs text-danger">{error}</p>}
</div>
)
}
// ─── Toggle ───────────────────────────────────────────────────────────────────
interface ToggleProps {
checked: boolean
onChange: (checked: boolean) => void
label?: string
description?: string
disabled?: boolean
}
export function Toggle({ checked, onChange, label, description, disabled }: ToggleProps) {
return (
<label className="flex items-start gap-3 cursor-pointer select-none group">
<div className="relative mt-0.5 flex-shrink-0">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
className="sr-only"
/>
<div
className={`
w-10 h-6 rounded-full transition-colors duration-200
${checked ? 'bg-accent-violet' : 'bg-bg-border'}
${disabled ? 'opacity-50' : ''}
`}
/>
<div
className={`
absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform duration-200
${checked ? 'translate-x-4' : 'translate-x-0'}
`}
/>
</div>
{(label || description) && (
<div className="flex flex-col">
{label && <span className="text-sm font-medium text-text-primary">{label}</span>}
{description && <span className="text-xs text-text-muted">{description}</span>}
</div>
)}
</label>
)
}
// ─── Badge ────────────────────────────────────────────────────────────────────
type BadgeVariant = 'owned' | 'unowned' | 'default' | 'mode' | 'slot'
interface BadgeProps {
variant?: BadgeVariant
children: React.ReactNode
className?: string
}
const badgeVariants: Record<BadgeVariant, string> = {
owned: 'bg-owned/20 text-owned border border-owned/30',
unowned: 'bg-unowned/20 text-unowned border border-unowned/30',
default: 'bg-bg-elevated text-text-secondary border border-bg-border',
mode: 'bg-accent-violet/20 text-accent-violet-light border border-accent-violet/30',
slot: 'bg-bg-surface text-text-secondary border border-bg-border',
}
export function Badge({ variant = 'default', children, className = '' }: BadgeProps) {
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${badgeVariants[variant]} ${className}`}
>
{children}
</span>
)
}
// ─── Card (panel) ─────────────────────────────────────────────────────────────
interface PanelProps {
children: React.ReactNode
className?: string
glow?: boolean
}
export function Panel({ children, className = '', glow }: PanelProps) {
return (
<div
className={`
bg-bg-surface rounded-lg border border-bg-border
${glow ? 'shadow-glow' : 'shadow-card'}
${className}
`}
>
{children}
</div>
)
}
// ─── Spinner ──────────────────────────────────────────────────────────────────
export function Spinner({ size = 'md', className = '' }: { size?: 'sm' | 'md' | 'lg'; className?: string }) {
const s = { sm: 'w-4 h-4', md: 'w-6 h-6', lg: 'w-10 h-10' }[size]
return <Loader2 className={`${s} animate-spin text-accent-violet ${className}`} />
}
// ─── Empty state ──────────────────────────────────────────────────────────────
export function EmptyState({
icon,
title,
description,
action,
}: {
icon?: React.ReactNode
title: string
description?: string
action?: React.ReactNode
}) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-4 text-center">
{icon && <div className="text-text-muted w-12 h-12">{icon}</div>}
<div className="flex flex-col gap-1">
<p className="text-text-primary font-medium">{title}</p>
{description && <p className="text-text-muted text-sm max-w-sm">{description}</p>}
</div>
{action}
</div>
)
}
// ─── Error message ────────────────────────────────────────────────────────────
export function ErrorMessage({ message }: { message: string }) {
return (
<div className="bg-danger/10 border border-danger/30 rounded-md p-3 text-sm text-danger">
{message}
</div>
)
}
// ─── Skeleton loader ──────────────────────────────────────────────────────────
export function Skeleton({ className = '' }: { className?: string }) {
return (
<div
className={`rounded bg-gradient-to-r from-bg-elevated via-bg-border to-bg-elevated bg-[length:200%_100%] animate-shimmer ${className}`}
/>
)
}