feat: Tailwind CSS v4 + @wing/ui 디자인 시스템 #21
27
packages/ui/src/components/Badge.tsx
Normal file
27
packages/ui/src/components/Badge.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
variant?: 'default' | 'accent' | 'danger' | 'warning' | 'success' | 'muted';
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Badge({ variant = 'default', className, children, ...props }: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-[5px] border px-1.5 py-px text-[8px] font-extrabold leading-none tracking-wide',
|
||||
variant === 'default' && 'border-white/8 bg-wing-muted/22 text-white',
|
||||
variant === 'accent' && 'border-wing-accent/70 bg-wing-accent/22 text-white',
|
||||
variant === 'danger' && 'border-wing-danger/70 bg-wing-danger/22 text-white',
|
||||
variant === 'warning' && 'border-wing-warning/70 bg-wing-warning/22 text-white',
|
||||
variant === 'success' && 'border-wing-success/70 bg-wing-success/22 text-white',
|
||||
variant === 'muted' && 'border-wing-border/90 bg-wing-card/55 text-wing-muted font-bold',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
30
packages/ui/src/components/Button.tsx
Normal file
30
packages/ui/src/components/Button.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'ghost' | 'primary' | 'danger';
|
||||
size?: 'sm' | 'md';
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Button({ variant = 'ghost', size = 'sm', className, children, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'cursor-pointer rounded border transition-all duration-150 select-none whitespace-nowrap',
|
||||
size === 'sm' && 'px-1.5 py-0.5 text-[9px]',
|
||||
size === 'md' && 'px-2.5 py-1.5 text-xs rounded-lg',
|
||||
variant === 'ghost' &&
|
||||
'border-wing-border bg-transparent text-wing-muted hover:text-wing-text hover:border-wing-accent',
|
||||
variant === 'primary' &&
|
||||
'border-wing-accent bg-wing-accent text-white hover:brightness-110',
|
||||
variant === 'danger' &&
|
||||
'border-wing-danger bg-transparent text-wing-danger hover:bg-wing-danger/10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
27
packages/ui/src/components/IconButton.tsx
Normal file
27
packages/ui/src/components/IconButton.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
active?: boolean;
|
||||
size?: 'sm' | 'md';
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function IconButton({ active, size = 'md', className, children, ...props }: IconButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex shrink-0 cursor-pointer items-center justify-center rounded border p-0 transition-all duration-150 select-none',
|
||||
'border-wing-border bg-wing-glass text-wing-muted backdrop-blur-lg',
|
||||
'hover:text-wing-text hover:border-wing-accent',
|
||||
size === 'sm' && 'h-[22px] w-[22px] rounded text-sm',
|
||||
size === 'md' && 'h-[29px] w-[29px] rounded text-base',
|
||||
active && 'text-wing-accent border-wing-accent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
26
packages/ui/src/components/ListItem.tsx
Normal file
26
packages/ui/src/components/ListItem.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface ListItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
selected?: boolean;
|
||||
highlighted?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ListItem({ selected, highlighted, className, children, ...props }: ListItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-1.5 rounded px-1.5 py-1 text-[10px] transition-colors duration-100 select-none',
|
||||
'hover:bg-wing-card',
|
||||
selected && 'bg-[rgba(14,234,255,0.16)] border-[rgba(14,234,255,0.55)]',
|
||||
highlighted && !selected && 'bg-[rgba(245,158,11,0.16)] border border-[rgba(245,158,11,0.4)]',
|
||||
selected && highlighted && 'bg-gradient-to-r from-[rgba(14,234,255,0.16)] to-[rgba(245,158,11,0.16)] border-[rgba(14,234,255,0.7)]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
packages/ui/src/components/Panel.tsx
Normal file
24
packages/ui/src/components/Panel.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface PanelProps extends HTMLAttributes<HTMLDivElement> {
|
||||
glass?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Panel({ glass = true, className, children, ...props }: PanelProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border border-wing-border p-3',
|
||||
glass
|
||||
? 'bg-wing-glass backdrop-blur-lg'
|
||||
: 'bg-wing-surface',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
packages/ui/src/components/Section.tsx
Normal file
29
packages/ui/src/components/Section.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface SectionProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
||||
title: ReactNode;
|
||||
actions?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Section({ title, actions, defaultOpen = true, className, children, ...props }: SectionProps) {
|
||||
return (
|
||||
<details open={defaultOpen || undefined} className={cn('border-b border-wing-border', className)} {...props}>
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2.5 select-none [&::-webkit-details-marker]:hidden">
|
||||
<span className="text-[9px] font-bold uppercase tracking-[1.5px] text-wing-muted">
|
||||
{title}
|
||||
</span>
|
||||
{actions && (
|
||||
<span onClick={(e) => e.preventDefault()} className="flex items-center gap-1.5">
|
||||
{actions}
|
||||
</span>
|
||||
)}
|
||||
</summary>
|
||||
<div className="px-3 pb-2.5">
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
18
packages/ui/src/components/TextInput.tsx
Normal file
18
packages/ui/src/components/TextInput.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import type { InputHTMLAttributes } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
type TextInputProps = InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export function TextInput({ className, ...props }: TextInputProps) {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-wing-border bg-wing-card/75 px-2 py-1.5 text-[10px] text-wing-text outline-none',
|
||||
'placeholder:text-wing-muted/90',
|
||||
'focus:border-wing-accent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
24
packages/ui/src/components/ToggleButton.tsx
Normal file
24
packages/ui/src/components/ToggleButton.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../utils/cn.ts';
|
||||
|
||||
interface ToggleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
on?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ToggleButton({ on, className, children, ...props }: ToggleButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'cursor-pointer rounded border px-1.5 py-0.5 text-[8px] transition-all duration-150 select-none',
|
||||
on
|
||||
? 'border-wing-accent bg-wing-accent text-white'
|
||||
: 'border-wing-border bg-wing-card text-wing-muted',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -1 +1,8 @@
|
||||
// Components will be added in Step 2
|
||||
export { Button } from './Button.tsx';
|
||||
export { IconButton } from './IconButton.tsx';
|
||||
export { ToggleButton } from './ToggleButton.tsx';
|
||||
export { Badge } from './Badge.tsx';
|
||||
export { Panel } from './Panel.tsx';
|
||||
export { Section } from './Section.tsx';
|
||||
export { ListItem } from './ListItem.tsx';
|
||||
export { TextInput } from './TextInput.tsx';
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user