Files
studio-admin-fe/src/components/ui.tsx
T
2026-07-01 17:34:15 +09:00

144 lines
4.3 KiB
TypeScript

import { X } from 'lucide-react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { cn } from '../lib/utils';
export function Card({
children,
className
}: {
children: ReactNode;
className?: string;
}) {
return <section className={cn('rounded-lg border border-border bg-card shadow-sm', className)}>{children}</section>;
}
export function CardHeader({
title,
description,
action,
className
}: {
title: string;
description?: string;
action?: ReactNode;
className?: string;
}) {
return (
<div className={cn('flex items-start justify-between gap-4 border-b border-border px-5 py-4', className)}>
<div className="min-w-0">
<h2 className="truncate text-sm font-semibold text-foreground">{title}</h2>
{description ? <p className="mt-1 text-xs leading-5 text-muted-foreground">{description}</p> : null}
</div>
{action ? <div className="shrink-0">{action}</div> : null}
</div>
);
}
export function Button({
children,
variant = 'default',
className,
...props
}: {
children: ReactNode;
variant?: 'default' | 'secondary' | 'ghost' | 'outline';
className?: string;
} & ButtonHTMLAttributes<HTMLButtonElement>) {
const variants = {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground'
};
return (
<button
type="button"
{...props}
className={cn(
'ui-button inline-flex h-8 items-center justify-center gap-1.5 rounded-md px-2.5 text-[13px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-45',
variants[variant],
className
)}
>
{children}
</button>
);
}
export function Badge({
children,
tone = 'neutral'
}: {
children: ReactNode;
tone?: 'neutral' | 'green' | 'amber' | 'red' | 'blue' | 'violet';
}) {
const tones = {
neutral: 'border-border bg-muted text-muted-foreground',
green: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300',
amber: 'border-amber-500/30 bg-amber-500/10 text-amber-300',
red: 'border-rose-500/30 bg-rose-500/10 text-rose-300',
blue: 'border-sky-500/30 bg-sky-500/10 text-sky-300',
violet: 'border-violet-500/30 bg-violet-500/10 text-violet-300'
};
return (
<span className={cn('inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium', tones[tone])}>
{children}
</span>
);
}
export function Progress({ value, tone = 'primary' }: { value: number; tone?: 'primary' | 'green' | 'amber' | 'red' }) {
const tones = {
primary: 'bg-primary',
green: 'bg-emerald-400',
amber: 'bg-amber-400',
red: 'bg-rose-400'
};
return (
<div className="h-2 overflow-hidden rounded-full bg-muted">
<div className={cn('h-full rounded-full', tones[tone])} style={{ width: `${Math.min(value, 100)}%` }} />
</div>
);
}
export function Modal({
open,
title,
children,
footer,
onClose,
className
}: {
open: boolean;
title: ReactNode;
children: ReactNode;
footer?: ReactNode;
onClose: () => void;
className?: string;
}) {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/55 px-4 py-8 backdrop-blur-[2px]">
<div className={cn('w-full max-w-lg overflow-hidden rounded-lg border border-border bg-card shadow-xl', className)}>
<div className="flex items-center justify-between border-b border-border px-5 py-4">
<h2 className="text-sm font-semibold text-foreground">{title}</h2>
<button
type="button"
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onClick={onClose}
aria-label="닫기"
>
<X className="size-4" />
</button>
</div>
<div className="max-h-[70vh] overflow-y-auto px-5 py-5">{children}</div>
{footer ? <div className="flex justify-end gap-2 border-t border-border px-5 py-4">{footer}</div> : null}
</div>
</div>
);
}