snp-connection-monitoring/docs/design/components.md
HYOJIN c2a71c1b77 feat(design): 디자인 시스템 적용 (CSS 토큰, Button/Badge, 차트, 다크모드) (#48)
- 디자인 시스템 가이드 문서 11개 생성 (docs/design/)
- CSS 변수 토큰 시스템 (@theme + :root/.dark 전환)
- cn() 유틸리티 (clsx + tailwind-merge)
- Button/Badge 공통 컴포넌트 (variant/size, 다크모드 대응)
- 하드코딩 Tailwind 색상 → CSS 변수 토큰 리팩토링 (30개 파일)
- 차트 팔레트 다크모드 색상 업데이트 (CHART_COLORS_HEX)
- 버튼 다크모드 채도/대비 강화 (primary-600 기반)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:38:00 +09:00

237 lines
6.6 KiB
Markdown

# 컴포넌트 스펙
모든 컴포넌트는 `cn()` 유틸리티(`src/utils/cn.ts`)를 사용하고, CSS 변수 토큰을 통해 색상을 지정한다.
---
## Button
### 변형 (Variant)
| Variant | 배경 | 텍스트 | 테두리 | 용도 |
|---------|------|--------|--------|------|
| `primary` | `--color-primary` | white | none | 주요 CTA |
| `secondary` | `--color-secondary` | `--color-secondary-text` | none | 보조 액션 |
| `accent` | `--color-accent` | `--color-accent-text` | none | 포인트 액션 |
| `outline` | transparent | `--color-primary` | `--color-primary` | 조용한 강조 |
| `ghost` | transparent | `--color-text-secondary` | none | 최소 강조 |
| `danger` | `--color-danger` | white | none | 삭제, 경고 |
### 사이즈
| Size | padding | font-size | height | border-radius |
|------|---------|-----------|--------|---------------|
| `sm` | `px-3 py-1.5` | `text-sm` | 32px | `rounded-md` |
| `md` | `px-4 py-2` | `text-base` | 40px | `rounded-lg` |
| `lg` | `px-6 py-3` | `text-lg` | 48px | `rounded-lg` |
### 상태
- **disabled**: `opacity-50 cursor-not-allowed pointer-events-none`
- **loading**: 스피너 아이콘 표시, 텍스트 유지, disabled 처리
- **hover**: 각 variant에 맞는 `-hover` 토큰 적용
- **active**: 각 variant에 맞는 `-active` 토큰 적용
- **focus-visible**: `ring-2 ring-[var(--color-primary)] ring-offset-2`
### 구현 예시
```tsx
const buttonVariants = {
primary: 'bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)] active:bg-[var(--color-primary-active)]',
secondary: 'bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:bg-[var(--color-secondary-hover)]',
accent: 'bg-[var(--color-accent)] text-[var(--color-accent-text)] hover:bg-[var(--color-accent-hover)]',
outline: 'border border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary-subtle)]',
ghost: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] hover:text-[var(--color-text-primary)]',
danger: 'bg-[var(--color-danger)] text-white hover:opacity-90',
};
```
---
## Badge
상태, 카테고리, 태그 표시에 사용한다.
### 변형
| Variant | 배경 | 텍스트 |
|---------|------|--------|
| `default` | `--color-bg-elevated` | `--color-text-primary` |
| `primary` | `--color-primary-subtle` | `--color-primary-text` |
| `success` | #DCFCE7 | #166534 |
| `warning` | #FEF9C3 | #854D0E |
| `danger` | #FEE2E2 | #991B1B |
| `info` | #E0F2FE | #075985 |
### 스펙
- 패딩: `px-2 py-0.5`
- 폰트: `text-xs font-medium`
- 모양: `rounded-full` (pill) 또는 `rounded-md` (square)
---
## Card
콘텐츠를 담는 기본 컨테이너.
### 구조
```html
<div class="rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-surface)] shadow-sm">
<!-- Card Header -->
<div class="border-b border-[var(--color-border)] px-6 py-4">
<h3 class="text-base font-semibold text-[var(--color-text-primary)]">제목</h3>
<p class="text-sm text-[var(--color-text-secondary)]">부제목</p>
</div>
<!-- Card Body -->
<div class="p-6">
<!-- 콘텐츠 -->
</div>
<!-- Card Footer (선택) -->
<div class="border-t border-[var(--color-border)] px-6 py-4">
<!-- 액션 버튼 등 -->
</div>
</div>
```
### 변형
| Variant | 설명 |
|---------|------|
| `default` | border + shadow-sm |
| `elevated` | shadow-md, 모달 내부 카드 |
| `flat` | border만, shadow 없음 |
| `interactive` | hover 시 shadow-md + scale-[1.01] |
---
## Input
### 기본 스펙
```html
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-[var(--color-text-primary)]">레이블</label>
<input
class="
w-full rounded-lg border border-[var(--color-border)]
bg-[var(--color-bg-surface)] px-3 py-2
text-base text-[var(--color-text-primary)]
placeholder:text-[var(--color-text-tertiary)]
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed
"
placeholder="입력하세요"
/>
<p class="text-xs text-[var(--color-text-tertiary)]">도움말 텍스트</p>
</div>
```
### 상태
| 상태 | 테두리 색상 |
|------|-------------|
| 기본 | `--color-border` |
| 포커스 | `--color-primary` (ring) |
| 오류 | `--color-danger` |
| 성공 | `--color-success` |
| 비활성 | `--color-border` + opacity-50 |
### 사이즈
| Size | padding | font-size | height |
|------|---------|-----------|--------|
| `sm` | `px-3 py-1.5` | `text-sm` | 32px |
| `md` | `px-3 py-2` | `text-base` | 40px |
| `lg` | `px-4 py-3` | `text-lg` | 48px |
---
## Modal
### 구조
```html
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<!-- Dialog -->
<div class="
relative w-full max-w-md mx-4 rounded-2xl
bg-[var(--color-bg-elevated)] shadow-xl
">
<!-- Header -->
<div class="flex items-center justify-between border-b border-[var(--color-border)] px-6 py-4">
<h2 class="text-lg font-semibold text-[var(--color-text-primary)]">제목</h2>
<button class="..."></button>
</div>
<!-- Body -->
<div class="px-6 py-6">...</div>
<!-- Footer -->
<div class="flex justify-end gap-3 border-t border-[var(--color-border)] px-6 py-4">
<Button variant="ghost">취소</Button>
<Button variant="primary">확인</Button>
</div>
</div>
</div>
```
### 사이즈
| Size | max-width |
|------|-----------|
| `sm` | `max-w-sm` (384px) |
| `md` | `max-w-md` (448px) |
| `lg` | `max-w-lg` (512px) |
| `xl` | `max-w-xl` (576px) |
### 접근성
- `role="dialog"`, `aria-modal="true"`, `aria-labelledby` 필수
- 열릴 때 첫 번째 포커스 가능 요소로 포커스 이동
- Escape 키로 닫기
- Backdrop 클릭 시 닫기 (선택적)
- 열린 동안 body 스크롤 잠금
---
## Toast
### 위치
`fixed bottom-4 right-4 z-[60]` (Modal보다 위)
### 변형
| Variant | 아이콘 | 색상 |
|---------|--------|------|
| `success` | CheckCircle | `--color-success` |
| `warning` | AlertTriangle | `--color-warning` |
| `error` | XCircle | `--color-danger` |
| `info` | Info | `--color-info` |
### 구조
```html
<div class="
flex items-start gap-3 w-80 rounded-xl p-4
bg-[var(--color-bg-elevated)] shadow-lg
border border-[var(--color-border)]
">
<!-- 아이콘 -->
<!-- 텍스트 -->
<!-- 닫기 버튼 -->
</div>
```
### 동작
- 기본 자동 닫힘: 4000ms
- 진입: `translate-y-2 opacity-0``translate-y-0 opacity-100`
- 퇴장: `translate-y-0 opacity-100``translate-y-2 opacity-0`
- 트랜지션: `duration-200 ease-out`