182 lines
5.4 KiB
Markdown
182 lines
5.4 KiB
Markdown
|
|
# WACE PLM — Modal Design System
|
||
|
|
|
||
|
|
> 이 파일은 Claude Code(`CLAUDE.md`)와 Cursor(`.cursor/rules/modal-design.mdc`)에서 자동 참조됩니다.
|
||
|
|
> **모달 패턴만** 정의합니다. Designer 페이지 레이아웃은 변경하지 않습니다.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 모달 Shell 구조
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
|
|
<DialogContent className="max-w-[{SIZE}] h-[80vh] overflow-hidden flex flex-col p-0 [&>button]:hidden">
|
||
|
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||
|
|
<DialogDescription className="sr-only">{description}</DialogDescription>
|
||
|
|
|
||
|
|
{/* Header */}
|
||
|
|
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<{Icon} className="w-4 h-4 text-blue-600" />
|
||
|
|
<h2 className="text-base font-semibold text-foreground">{title}</h2>
|
||
|
|
</div>
|
||
|
|
<Button variant="ghost" size="icon" onClick={() => onOpenChange(false)} className="w-8 h-8">
|
||
|
|
<X className="w-4 h-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Tab (선택) */}
|
||
|
|
{/* → Section 2 참조 */}
|
||
|
|
|
||
|
|
{/* Content */}
|
||
|
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||
|
|
{/* 콘텐츠 */}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Footer */}
|
||
|
|
<div className="px-6 py-4 border-t border-border flex items-center justify-end gap-2">
|
||
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>취소</Button>
|
||
|
|
<Button className="bg-blue-600 hover:bg-blue-700">저장</Button>
|
||
|
|
</div>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 사이즈 변형
|
||
|
|
|
||
|
|
| 용도 | className |
|
||
|
|
|------|-----------|
|
||
|
|
| 기본 (512px) | `max-w-lg` |
|
||
|
|
| 중간 (672px) | `max-w-2xl` |
|
||
|
|
| 넓음 (800px) | `max-w-[800px]` |
|
||
|
|
| 최대 (1024px) | `max-w-5xl` |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 탭 패턴 (모달 내부)
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<div className="mx-6 mt-3">
|
||
|
|
<div className="h-9 bg-muted/30 rounded-lg p-0.5 inline-flex">
|
||
|
|
{tabs.map(tab => (
|
||
|
|
<button
|
||
|
|
key={tab.value}
|
||
|
|
onClick={() => setActiveTab(tab.value)}
|
||
|
|
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||
|
|
activeTab === tab.value
|
||
|
|
? "bg-blue-50 text-blue-700 shadow-sm"
|
||
|
|
: "bg-transparent text-foreground hover:text-foreground/80"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<tab.Icon className="w-3.5 h-3.5" />
|
||
|
|
{tab.label}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
- shadcn `<Tabs>` 컴포넌트 **사용 금지** — 위 커스텀 버튼 패턴 사용
|
||
|
|
- 활성 탭: `bg-blue-50 text-blue-700 shadow-sm`
|
||
|
|
- 비활성 탭: `bg-transparent text-foreground`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 섹션 패턴
|
||
|
|
|
||
|
|
### 정보 섹션 (강조, Teal)
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<div className="bg-teal-50 border border-teal-200 rounded-xl p-4">
|
||
|
|
{/* 데이터 소스, 정보 입력 등 주요 섹션 */}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 일반 섹션 (흰색)
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<div className="bg-white border border-border rounded-xl p-4 space-y-4">
|
||
|
|
{/* 설정, 목록 등 */}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 폼 필드 패턴
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
{/* Label + Input */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label className="text-xs font-medium text-foreground">라벨</Label>
|
||
|
|
<Input className="h-9 text-sm" placeholder="..." />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Label + Select */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label className="text-xs font-medium text-foreground">라벨</Label>
|
||
|
|
<Select>
|
||
|
|
<SelectTrigger className="h-9 text-sm">
|
||
|
|
<SelectValue placeholder="선택하세요" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>...</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 읽기 전용 Input */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label className="text-xs font-medium text-muted-foreground">라벨 (자동 감지)</Label>
|
||
|
|
<Input className="h-9 text-sm bg-muted" readOnly />
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
- 필드 간격: `space-y-3`
|
||
|
|
- Label: `text-xs font-medium`
|
||
|
|
- Input/Select 높이: `h-9`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 버튼 규칙 (모달 내)
|
||
|
|
|
||
|
|
| 위치 | variant | 용도 |
|
||
|
|
|------|---------|------|
|
||
|
|
| Footer 취소 | `outline` | 닫기, 취소 |
|
||
|
|
| Footer 저장 | `className="bg-blue-600 hover:bg-blue-700"` | 저장, 확인 |
|
||
|
|
| Footer 삭제 | `variant="destructive"` | 삭제 확인 |
|
||
|
|
| 콘텐츠 내 추가 | `variant="outline" size="sm" className="w-full gap-2"` | 행/항목 추가 |
|
||
|
|
| 콘텐츠 내 아이콘 | `variant="ghost" size="sm"` | 인라인 액션 |
|
||
|
|
| 삭제 아이콘 | `variant="ghost" size="sm" className="text-destructive hover:text-destructive hover:bg-destructive/10"` | 인라인 삭제 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 오버레이 & 애니메이션
|
||
|
|
|
||
|
|
- 오버레이: `rgba(0, 0, 0, 0.6)` — globals.css에 이미 전역 설정됨
|
||
|
|
- 모달 열기: `fade-in 200ms + zoom-in-95` (shadcn 기본)
|
||
|
|
- 모달 닫기: `fade-out 150ms + zoom-out-95` (shadcn 기본)
|
||
|
|
- 탭 전환: 애니메이션 없음 (즉시)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 아이콘 규칙 (모달 내)
|
||
|
|
|
||
|
|
| 위치 | 크기 | 색상 |
|
||
|
|
|------|------|------|
|
||
|
|
| 헤더 타이틀 아이콘 | `w-4 h-4` (16px) | `text-blue-600` |
|
||
|
|
| 탭 내부 아이콘 | `w-3.5 h-3.5` (14px) | 탭 상태에 따라 자동 |
|
||
|
|
| 닫기 버튼 X | `w-4 h-4` (16px) | 기본 foreground |
|
||
|
|
| 콘텐츠 내 액션 아이콘 | `w-3 h-3` (12px) | `text-muted-foreground` |
|
||
|
|
| 행 추가 버튼 아이콘 | `w-3.5 h-3.5` (14px) | 기본 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 접근성 필수 사항
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
{/* DialogContent 내부 반드시 포함 */}
|
||
|
|
<DialogTitle className="sr-only">{모달 목적 설명}</DialogTitle>
|
||
|
|
<DialogDescription className="sr-only">{상세 설명}</DialogDescription>
|
||
|
|
```
|
||
|
|
|
||
|
|
- Escape 키로 닫기: shadcn Dialog 기본 제공
|
||
|
|
- 포커스 트랩: shadcn Dialog 기본 제공
|
||
|
|
- 닫기 버튼에 `aria-label` 불필요 (shadcn 처리)
|