Restructure into full project layout
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user