@xonic0506/sonic-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-RULES.md +1 -1
- package/DESIGN.md +355 -16
- package/README.md +2 -0
- package/dist/components/alert-dialog.d.ts +16 -0
- package/dist/components/alert-dialog.js +46 -0
- package/dist/components/badge.d.ts +8 -0
- package/dist/components/badge.js +77 -0
- package/dist/components/button.d.ts +11 -2
- package/dist/components/button.js +24 -14
- package/dist/components/checkbox.d.ts +30 -0
- package/dist/components/checkbox.js +48 -0
- package/dist/components/dialog.d.ts +11 -0
- package/dist/components/dialog.js +17 -14
- package/dist/components/empty-state.d.ts +11 -0
- package/dist/components/empty-state.js +17 -0
- package/dist/components/error-state.d.ts +12 -0
- package/dist/components/error-state.js +24 -0
- package/dist/components/field.d.ts +13 -0
- package/dist/components/field.js +33 -0
- package/dist/components/input.d.ts +1 -0
- package/dist/components/input.js +5 -5
- package/dist/components/menu.d.ts +33 -0
- package/dist/components/menu.js +44 -0
- package/dist/components/popover.d.ts +26 -0
- package/dist/components/popover.js +50 -0
- package/dist/components/popup-styles.d.ts +3 -0
- package/dist/components/popup-styles.js +5 -0
- package/dist/components/progress.d.ts +16 -0
- package/dist/components/progress.js +27 -0
- package/dist/components/radio-group.d.ts +20 -0
- package/dist/components/radio-group.js +40 -0
- package/dist/components/select.d.ts +30 -0
- package/dist/components/select.js +66 -0
- package/dist/components/separator.d.ts +7 -0
- package/dist/components/separator.js +14 -0
- package/dist/components/skeleton.d.ts +8 -0
- package/dist/components/skeleton.js +22 -0
- package/dist/components/state-layout.d.ts +11 -0
- package/dist/components/state-layout.js +31 -0
- package/dist/components/switch.d.ts +12 -0
- package/dist/components/switch.js +24 -0
- package/dist/components/tabs.d.ts +31 -0
- package/dist/components/tabs.js +41 -0
- package/dist/components/textarea.d.ts +6 -0
- package/dist/components/textarea.js +20 -0
- package/dist/components/toast.d.ts +24 -0
- package/dist/components/toast.js +62 -0
- package/dist/components/tooltip.d.ts +11 -0
- package/dist/components/tooltip.js +19 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +23 -5
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +15 -0
- package/package.json +4 -2
package/AI-RULES.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
## 규칙
|
|
11
11
|
|
|
12
12
|
1. **DESIGN.md가 기준이에요.** 색, 간격, radius, 글자, 모션은 DESIGN.md의 토큰만 써요. 임의의 값(`p-[7px]`, `#3355ff`, `text-[15px]`)과 Tailwind 기본 팔레트(`bg-blue-500`)는 쓰지 않아요.
|
|
13
|
-
2. **UI는 sonic-ui 컴포넌트로 조립해요.** Button, Input,
|
|
13
|
+
2. **UI는 sonic-ui 컴포넌트로 조립해요.** DESIGN.md §7 Core 목록(Button, Input, Select, Dialog, Menu, Toast, Badge 등)에 있는 역할은 직접 만들지 않아요. 없는 역할이 필요하면 먼저 사용자에게 알려요(라이브러리에 추가할지 판단이 필요해요).
|
|
14
14
|
3. **frontend-design은 제한해서 써요.** 레이아웃과 문구 판단에만 참고하고, 새 색·새 폰트·새 토큰 제안은 따르지 않아요. 컴포넌트를 구현할 때는 쓰지 않아요.
|
|
15
15
|
4. **데이터 화면은 4가지 상태를 모두 만들어요.** 정상, 로딩, 빈 상태, 에러. Figma에 4가지가 없으면 디자인부터 요청해요(DESIGN.md §8).
|
|
16
16
|
5. **Domain 컴포넌트는 프로젝트에 둬요.** 도메인 타입, API 호출, 비즈니스 규칙을 아는 컴포넌트는 sonic-ui에 넣지 않아요. 같은 형태를 두 번째 프로젝트에서 복사하게 되면 Pattern 승격을 제안해요(DESIGN.md §7).
|
package/DESIGN.md
CHANGED
|
@@ -10,7 +10,7 @@ Sonic(`sonic-ui`)의 디자인 규칙이에요. Figma에서 디자인할 때,
|
|
|
10
10
|
1. **토큰 밖의 값은 없다.** 색, 간격, radius, 글자 크기는 모두 아래 표에서 고릅니다. 임의의 값(`#3355ff`, `13px`, `p-[7px]`)은 쓰지 않아요.
|
|
11
11
|
2. **강조는 한 곳에만.** 한 화면에서 `primary` 버튼은 하나만 둬요. 나머지는 `secondary`나 `ghost`를 씁니다.
|
|
12
12
|
3. **숫자는 고정폭.** 수치, ID, 키 값은 `mono` 계열 스타일로 써서 자리를 맞춰요.
|
|
13
|
-
4. **그림자 대신 선.** 높낮이는 1px 테두리(`color/border/default`)와 배경 단계(canvas → surface → subtle)로 구분해요. 그림자 토큰은 없어요.
|
|
13
|
+
4. **그림자 대신 선.** 높낮이는 1px 테두리(`color/border/default`)와 배경 단계(canvas → surface → subtle)로 구분해요. Menu, Popover, Select, Toast처럼 떠 있는 요소도 `bg/surface` + 1px 테두리로 구분하고, 그림자 토큰은 없어요.
|
|
14
14
|
5. **움직임은 사용자 행동에 대한 응답만.** 열기, 닫기, hover처럼 사용자가 일으킨 변화에만 모션을 넣고, `prefers-reduced-motion`을 지켜요.
|
|
15
15
|
|
|
16
16
|
## 2. 이름 규칙
|
|
@@ -57,6 +57,21 @@ Figma Variable 이름이 그대로 CSS 변수가 돼요. `/`는 `-`로 바꾸고
|
|
|
57
57
|
| `color/border/strong` | gray/400 | gray/600 | hover 테두리 |
|
|
58
58
|
| `color/border/focus` | cobalt/600 | cobalt/400 | 포커스 링 |
|
|
59
59
|
| `color/border/danger` | red/600 | red/400 | 에러 상태 Input |
|
|
60
|
+
| `color/bg/inverse` | gray/950 | gray/100 | Tooltip, neutral Badge(solid) |
|
|
61
|
+
| `color/text/inverse` | gray/0 | gray/950 | inverse 바탕 위 글자 |
|
|
62
|
+
| `color/bg/success` | green/600 `#16804F` | green/400 `#3DB882` | success Badge(solid) |
|
|
63
|
+
| `color/text/on-success` | gray/0 | gray/950 | success 바탕 위 글자 |
|
|
64
|
+
| `color/bg/success-subtle` | green/50 `#EAF7F0` | green/950 `#0B2218` | success Badge(subtle) |
|
|
65
|
+
| `color/text/success` | green/700 `#0F6640` | green/300 `#6BD1A0` | 성공 문구, 아이콘 |
|
|
66
|
+
| `color/border/success` | green/600 | green/400 | 성공 강조 테두리 |
|
|
67
|
+
| `color/bg/warning` | amber/400 `#E3A43A` | amber/400 | warning Badge(solid) |
|
|
68
|
+
| `color/text/on-warning` | gray/950 | gray/950 | warning 바탕 위 글자 (amber 위 흰 글자는 대비 부족) |
|
|
69
|
+
| `color/bg/warning-subtle` | amber/50 `#FCF5E6` | amber/950 `#24190A` | warning Badge(subtle) |
|
|
70
|
+
| `color/text/warning` | amber/700 `#85520A` | amber/300 `#EEC46A` | 주의 문구, 아이콘 |
|
|
71
|
+
| `color/border/warning` | amber/600 | amber/400 | 주의 강조 테두리 |
|
|
72
|
+
| `color/bg/danger-subtle` | red/50 `#FCEDEC` | red/950 `#2B1311` | danger Badge(subtle) |
|
|
73
|
+
|
|
74
|
+
글자와 바탕 조합은 모두 WCAG 4.5:1 이상이에요. 토큰을 바꾸면 `npm run check:contrast`로 다시 확인해요(배포 전 검사에도 들어 있어요).
|
|
60
75
|
|
|
61
76
|
### 3.2 간격 (4px 단위)
|
|
62
77
|
|
|
@@ -102,6 +117,30 @@ Figma Variable 이름이 그대로 CSS 변수가 돼요. `/`는 `-`로 바꾸고
|
|
|
102
117
|
| `duration/fast` | 120ms (hover, 색 전환) |
|
|
103
118
|
| `duration/normal` | 200ms (Dialog 열기/닫기) |
|
|
104
119
|
| `easing/standard` | `cubic-bezier(0.2, 0, 0, 1)` |
|
|
120
|
+
| `duration/loop` | 1000ms (반복 애니메이션: Skeleton 깜빡임, 로딩 스피너 회전) |
|
|
121
|
+
|
|
122
|
+
반복 애니메이션은 `prefers-reduced-motion`이면 멈춰요.
|
|
123
|
+
|
|
124
|
+
### 3.6 아이콘 (Lucide)
|
|
125
|
+
|
|
126
|
+
아이콘은 [Lucide](https://lucide.dev)만 써요. 쓰는 쪽 프로젝트에서 `lucide-react`를 설치해요. Figma에는 Sonic이 쓰는 아이콘만 `Icon / <이름>` 컴포넌트로 있어요(Foundations › 04 Icon).
|
|
127
|
+
|
|
128
|
+
| 항목 | 규칙 |
|
|
129
|
+
|---|---|
|
|
130
|
+
| 이름 | Lucide 이름 그대로. Figma `Icon / chevron-down` = 코드 `<ChevronDown />` |
|
|
131
|
+
| 크기 | `space/4` 16 (버튼 sm·md·icon-sm·icon-md, 입력란 안, 본문 옆) · `space/5` 20 (버튼 lg·icon-lg) · `space/6` 24 (빈 상태처럼 아이콘이 주인공일 때) |
|
|
132
|
+
| 선 두께 | Lucide 기본 2 (24 기준). 크기에 비례해 줄어요. `absoluteStrokeWidth`는 쓰지 않아요. Figma에서는 Scale(K)로 줄여요 |
|
|
133
|
+
| 색 | `currentColor`. 주변 글자색 토큰을 따라가요. 아이콘에만 다른 색을 줄 때도 text 토큰만 써요 |
|
|
134
|
+
| 접근성 | 글자와 함께 쓰면 장식이라 숨겨요(lucide-react 기본 `aria-hidden`). 아이콘만 있는 버튼은 `aria-label`을 꼭 달아요 |
|
|
135
|
+
| 추가 | 새 아이콘이 필요하면 Lucide에서 가져와 Figma `Icon /` 컴포넌트로 추가하고, 이 표 규칙을 따라요. 다른 아이콘 세트는 섞지 않아요 |
|
|
136
|
+
|
|
137
|
+
Button 안에 넣은 아이콘은 크기가 자동으로 맞춰져요(sm·md 16, lg 20). 아이콘과 글자 사이 간격은 `space/2`예요. 아이콘은 글자 앞(left), 뒤(right), 양쪽(both)에 둘 수 있고, 뒤쪽 아이콘은 펼침(chevron)이나 외부 링크처럼 결과를 알려 줄 때만 써요. 아이콘만 있는 버튼은 `size="icon-*"`를 써요(§10 Button).
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { Plus } from 'lucide-react';
|
|
141
|
+
<Button><Plus />API 키 만들기</Button>
|
|
142
|
+
<Button size="icon-md" variant="ghost" aria-label="더 보기"><Ellipsis /></Button>
|
|
143
|
+
```
|
|
105
144
|
|
|
106
145
|
## 4. Auto layout 규칙 (Figma)
|
|
107
146
|
|
|
@@ -126,7 +165,7 @@ Figma Variable 이름이 그대로 CSS 변수가 돼요. `/`는 `-`로 바꾸고
|
|
|
126
165
|
| 제목 ↔ 바로 아래 설명 | `space/1` |
|
|
127
166
|
|
|
128
167
|
- 버튼 묶음은 오른쪽 정렬이고, 보조 버튼이 왼쪽, primary가 맨 오른쪽이에요. Dialog Footer도 같아요.
|
|
129
|
-
- 에러 상태: 입력란 테두리를 `color/border/danger`로 바꾸고, 입력란 바로
|
|
168
|
+
- 에러 상태: 입력란 테두리를 `color/border/danger`로 바꾸고, 입력란 바로 아래(설명 자리)에 `caption` + `color/text/danger`로 고치는 방법을 씁니다. 설명은 숨겨요. 라벨 색은 바꾸지 않아요. 이 묶음은 **Field 컴포넌트**가 처리해요(§10).
|
|
130
169
|
- 문구는 한국어 해요체예요. 취소 버튼은 "취소"로 충분해요. 무엇을 취소하는지는 바로 옆 primary 버튼 문구가 알려 줘요.
|
|
131
170
|
- Avatar 이니셜은 이름에서 1~2글자를 써요(한글은 이름 첫 글자, 영문은 이름·성 첫 글자).
|
|
132
171
|
|
|
@@ -164,17 +203,29 @@ Figma에서도 레이어 이름을 이 이름으로 맞춰요. 예: `Dialog / Ba
|
|
|
164
203
|
| `disabled=true` | `[data-disabled]` / `disabled` prop |
|
|
165
204
|
| `highlighted=true` | `[data-highlighted]` (키보드나 hover로 선택된 항목) |
|
|
166
205
|
| `invalid=true` | `[data-invalid]` (Base UI Field) |
|
|
206
|
+
| `selected=true` | `[data-selected]` (Select 항목) |
|
|
207
|
+
| `checked=indeterminate` | `indeterminate` prop, `[data-indeterminate]` (Checkbox) |
|
|
208
|
+
|
|
209
|
+
hover, focus는 **컴포넌트 variant로 만들지 않아요.** React prop이 아니라 브라우저가 정하는 상태라서예요.
|
|
210
|
+
|
|
211
|
+
| 상태 | Figma | 코드 |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| hover | Core 문서 "Interaction states" 매트릭스에 인스턴스 오버라이드로 그림 | `:hover` (disabled일 때는 적용 안 함) |
|
|
214
|
+
| focus | 같은 매트릭스. 2px `border/focus` 링, 간격 2px. invalid Input은 `border/danger` 링 | `:focus-visible` → `outline 2px`, `outline-offset 2px` (Tab만 안쪽 −2px). 포커스 링을 그리는 요소에는 `outline-none`을 같이 쓰지 않아요(Tailwind v4에서 링이 사라져요) |
|
|
215
|
+
| press | 그리지 않음 | 구현 안 함 (v0.1) |
|
|
167
216
|
|
|
168
|
-
|
|
217
|
+
상태별 토큰은 §10 컴포넌트 스펙 표가 기준이에요. 화면 디자인에서는 hover, focus를 따로 그리지 않아요. 컴포넌트가 자동으로 처리해요.
|
|
169
218
|
|
|
170
219
|
## 7. 3계층 판단 기준
|
|
171
220
|
|
|
172
221
|
| 계층 | 위치 | 기준 | 예 |
|
|
173
222
|
|---|---|---|---|
|
|
174
|
-
| **Core** | sonic-ui `src/components/` | 더 나눌 수 없고, 도메인을 전혀 모름 | Button, Input, Text, Card, Avatar, Dialog |
|
|
223
|
+
| **Core** | sonic-ui `src/components/` | 더 나눌 수 없고, 도메인을 전혀 모름 | Button, Input, Textarea, Checkbox, RadioGroup, Switch, Select, Text, Card, Avatar, Dialog, AlertDialog, Menu, Popover, Tooltip, Tabs, Separator, Toast, Badge, Progress, Field, Skeleton, EmptyState, ErrorState |
|
|
175
224
|
| **Pattern** | sonic-ui `src/patterns/` | Core 조합이고 도메인을 모르지만, **두 프로젝트 이상에서 같은 형태로 복사됨** | (아직 없음) |
|
|
176
225
|
| **Domain** | 각 프로젝트 | 도메인 타입, API, 비즈니스 로직 중 하나라도 앎 | `UserProfileCard`, `ApiKeyList` |
|
|
177
226
|
|
|
227
|
+
예외: EmptyState와 ErrorState는 Core 조합이라 원래는 Pattern 후보지만, §8 규칙이 모든 데이터 화면에 요구하는 **시스템 컴포넌트**라서 Core에 둬요.
|
|
228
|
+
|
|
178
229
|
판단 순서:
|
|
179
230
|
1. props에 도메인 타입(`User`, `Order`)이 들어가나요? → **Domain**
|
|
180
231
|
2. 데이터를 직접 불러오거나 비즈니스 규칙이 있나요? → **Domain**
|
|
@@ -183,16 +234,16 @@ hover, focus는 Figma에서 `state=hover`, `state=focus`로 그리고, 코드에
|
|
|
183
234
|
|
|
184
235
|
## 8. 데이터 화면의 4가지 상태
|
|
185
236
|
|
|
186
|
-
데이터를 보여 주는 화면은 Figma에서 **4가지 상태를 모두 그려야** 개발로 넘어가요.
|
|
237
|
+
데이터를 보여 주는 화면은 Figma에서 **4가지 상태를 모두 그려야** 개발로 넘어가요. 각 상태는 아래 컴포넌트로 그려요(예: Figma Playground › `Sample / API keys — 4 states`).
|
|
187
238
|
|
|
188
|
-
| 상태 | 그릴 것 |
|
|
189
|
-
|
|
190
|
-
| 정상 | 실제와 비슷한 양의 데이터 |
|
|
191
|
-
| 로딩 | 정상 화면과
|
|
192
|
-
| 빈 상태 | 무엇이 없는지, 다음에 할 행동 하나
|
|
193
|
-
| 에러 | 무엇이 실패했는지, 다시 시도할 방법 |
|
|
239
|
+
| 상태 | 그릴 것 | 컴포넌트 |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| 정상 | 실제와 비슷한 양의 데이터 | 화면 구성 그대로 |
|
|
242
|
+
| 로딩 | 정상 화면과 **같은 배치**에 자리만 표시 | `Skeleton` (text · rect · circle) |
|
|
243
|
+
| 빈 상태 | 무엇이 없는지, 다음에 할 행동 하나 | `EmptyState` |
|
|
244
|
+
| 에러 | 무엇이 실패했는지, 다시 시도할 방법 | `ErrorState` |
|
|
194
245
|
|
|
195
|
-
|
|
246
|
+
저장처럼 사용자가 일으킨 동작이 진행 중일 때는 화면 전체가 아니라 그 버튼에 `loading`을 켜요.
|
|
196
247
|
|
|
197
248
|
## 9. frontend-design 사용 규칙
|
|
198
249
|
|
|
@@ -213,15 +264,24 @@ Figma Core 페이지의 컴포넌트 속성 이름 = React props 이름이에요
|
|
|
213
264
|
| prop | 값 | 기본값 |
|
|
214
265
|
|---|---|---|
|
|
215
266
|
| `variant` | `primary` · `secondary` · `ghost` · `danger` | `primary` |
|
|
216
|
-
| `size` | `sm` · `md` · `lg` | `md` |
|
|
267
|
+
| `size` | `sm` · `md` · `lg` · `icon-sm` · `icon-md` · `icon-lg` | `md` |
|
|
217
268
|
| `disabled` | boolean | `false` |
|
|
218
|
-
| `children` | 버튼 문구 | — |
|
|
269
|
+
| `children` | 버튼 문구. 아이콘은 문구 앞뒤에 lucide 컴포넌트로 넣음 | — |
|
|
270
|
+
| `loading` | boolean. 스피너 + 클릭 막기 + `aria-busy` | `false` |
|
|
271
|
+
| `aria-label` | icon 크기일 때 **필수** (타입으로 강제) | — |
|
|
219
272
|
|
|
220
273
|
| | sm | md | lg |
|
|
221
274
|
|---|---|---|---|
|
|
222
275
|
| 높이 | `space/7` (28) | `space/8` (32) | `space/10` (40) |
|
|
223
276
|
| 좌우 padding | `space/2` | `space/3` | `space/4` |
|
|
224
277
|
| 글자 | `label` | `body-strong` | `body-strong` |
|
|
278
|
+
| 아이콘 | 16 | 16 | 20 |
|
|
279
|
+
|
|
280
|
+
| | icon-sm | icon-md | icon-lg |
|
|
281
|
+
|---|---|---|---|
|
|
282
|
+
| 크기 (정사각형) | `space/7` (28) | `space/8` (32) | `space/10` (40) |
|
|
283
|
+
| padding | 0 | 0 | 0 |
|
|
284
|
+
| 아이콘 | 16 | 16 | 20 |
|
|
225
285
|
|
|
226
286
|
| variant | 바탕 | 글자 | 테두리 | hover |
|
|
227
287
|
|---|---|---|---|---|
|
|
@@ -244,6 +304,8 @@ Figma Core 페이지의 컴포넌트 속성 이름 = React props 이름이에요
|
|
|
244
304
|
|
|
245
305
|
높이 `space/8`, 좌우 padding `space/3`, radius `radius/md`, 바탕 `bg/surface`, 테두리 `border/default`(hover `border/strong`, invalid `border/danger`), 입력값 `body` + `text/default`, placeholder `text/muted`, 너비는 부모를 채움.
|
|
246
306
|
|
|
307
|
+
focus 링은 2px, 간격 2px이고, 색은 기본 `border/focus`, invalid일 때 `border/danger`예요. 에러 상태에서 파란 링이 뜨면 상태 신호가 섞이기 때문이에요.
|
|
308
|
+
|
|
247
309
|
### Text
|
|
248
310
|
|
|
249
311
|
`<Text variant="caption" tone="muted" as="p">…</Text>`
|
|
@@ -297,6 +359,263 @@ radius `radius/full`, 바탕 `bg/subtle`, fallback 글자 `text/muted`. 이미
|
|
|
297
359
|
- 열기/닫기: `duration/normal` + `easing/standard`. `[data-starting-style]`과 `[data-ending-style]`에서 opacity 0, scale 0.98. Backdrop은 opacity만 바꿔요.
|
|
298
360
|
- `open`, `onOpenChange`로 제어할 수 있어요. Esc와 바깥 클릭으로 닫혀요.
|
|
299
361
|
|
|
362
|
+
#### 트리거가 밖에 있을 때 (Dialog, AlertDialog 공통)
|
|
363
|
+
|
|
364
|
+
`DialogTrigger`를 쓸 수 없으면 **제어 모드**로 열어요. Trigger 없이 `open`과 `onOpenChange`만 넘겨요.
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
// 1) 다른 곳의 버튼, Menu 항목에서 열기
|
|
368
|
+
const [confirm, setConfirm] = useState(false);
|
|
369
|
+
<Menu>
|
|
370
|
+
<MenuTrigger render={<Button size="icon-md" variant="ghost" aria-label="더 보기" />}><Ellipsis /></MenuTrigger>
|
|
371
|
+
<MenuPopup><MenuItem tone="danger" onClick={() => setConfirm(true)}><Trash2 />삭제</MenuItem></MenuPopup>
|
|
372
|
+
</Menu>
|
|
373
|
+
<AlertDialog open={confirm} onOpenChange={setConfirm}>
|
|
374
|
+
<AlertDialogPopup>…</AlertDialogPopup>
|
|
375
|
+
</AlertDialog>
|
|
376
|
+
|
|
377
|
+
// 2) 코드로 열기 (예: API 에러 뒤). 닫힌 뒤 포커스가 갈 곳을 finalFocus로 정해요
|
|
378
|
+
const retryRef = useRef<HTMLButtonElement>(null);
|
|
379
|
+
<Dialog open={hasError} onOpenChange={setHasError}>
|
|
380
|
+
<DialogPopup finalFocus={retryRef}>…</DialogPopup>
|
|
381
|
+
</Dialog>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
- 닫히면 포커스는 **열기 전에 포커스가 있던 요소**로 돌아가요(바깥 버튼, Menu 트리거). 사용자 동작 없이 코드로 열었다면 돌아갈 곳이 없으니 `finalFocus`를 꼭 지정해요.
|
|
385
|
+
- 열릴 때 포커스는 팝업 안 첫 포커스 요소예요. 다른 곳이 맞으면 `initialFocus`로 정해요(예: AlertDialog에서 위험한 확인 버튼 대신 취소 버튼).
|
|
386
|
+
- 확인 동작이 끝나면 `onOpenChange(false)` 대신 상태를 직접 `false`로 바꿔 닫아요.
|
|
387
|
+
|
|
388
|
+
### Button loading
|
|
389
|
+
|
|
390
|
+
`<Button loading>저장</Button>`
|
|
391
|
+
|
|
392
|
+
- 왼쪽에 `loader-circle`이 `duration/loop`로 회전하고, 클릭이 막혀요. 포커스는 유지되고 `aria-busy="true"`, `data-loading`이 붙어요.
|
|
393
|
+
- 로딩 중에는 왼쪽 아이콘을 넣지 않아요(스피너가 그 자리를 써요). icon 버튼은 아이콘 자리에 스피너만 보여요.
|
|
394
|
+
|
|
395
|
+
### Field
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
<Field label="이메일" description="로그인할 때 써요." error="https://로 시작하는 주소를 입력하세요." invalid>
|
|
399
|
+
<Input />
|
|
400
|
+
</Field>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
| prop | 값 |
|
|
404
|
+
|---|---|
|
|
405
|
+
| `label` | 라벨 문구 (필수) |
|
|
406
|
+
| `description` | 설명 문구 (선택) |
|
|
407
|
+
| `error` | 에러 문구. `invalid`일 때만 보이고, 그때는 **설명 자리를 대신해요** |
|
|
408
|
+
| `invalid`, `disabled` | boolean. 안의 Input에 자동으로 전달돼요 (Base UI Field) |
|
|
409
|
+
| `children` | 입력 컨트롤 (지금은 `Input`) |
|
|
410
|
+
|
|
411
|
+
간격: 라벨 ↔ 입력 ↔ 설명·에러 `space/2`. 라벨 `label` + `text/default`(invalid여도 그대로), 설명 `caption` + `text/muted`, 에러 `caption` + `text/danger`. **Field 안의 Input에는 `invalid`를 따로 주지 않아요.** 입력 컨트롤과 설명 또는 에러는 `aria-describedby`로 연결돼요.
|
|
412
|
+
|
|
413
|
+
에러는 설명과 함께 보이지 않고 **설명을 대신해요**(Material, Carbon, Spectrum과 같은 방식). 아래 줄이 늘어나지 않아 레이아웃이 밀리지 않고, 화면 낭독기도 에러 하나만 읽어요. 설명에 있던 형식 안내는 에러 문구가 대신 전해야 하니, 에러 문구에는 고치는 방법을 꼭 써요(§5).
|
|
414
|
+
|
|
415
|
+
### Skeleton
|
|
416
|
+
|
|
417
|
+
`<Skeleton shape="text" className="w-40" />`
|
|
418
|
+
|
|
419
|
+
| shape | 기본 크기 | radius |
|
|
420
|
+
|---|---|---|
|
|
421
|
+
| `text` | 높이 `space/3` (12, body 한 줄 자리), 너비 100% | `radius/sm` |
|
|
422
|
+
| `rect` | 높이 `space/16` (64), 너비 100% | `radius/md` |
|
|
423
|
+
| `circle` | `space/8` (32) | `radius/full` |
|
|
424
|
+
|
|
425
|
+
바탕 `bg/subtle`, opacity 1 ↔ 0.5를 `duration/loop`로 반복해요. 크기는 쓰는 곳에서 `className`으로 정하고, 스크린 리더에는 숨겨요(`aria-hidden`). 로딩 영역 전체에는 `aria-busy`를 둬요.
|
|
426
|
+
|
|
427
|
+
### EmptyState / ErrorState
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
<EmptyState icon={<Inbox />} title="API 키가 없어요" description="키를 만들면 여기에서 사용량을 볼 수 있어요." action={<Button>API 키 만들기</Button>} />
|
|
431
|
+
<ErrorState title="API 키를 불러오지 못했어요" description="네트워크 연결을 확인하고 다시 시도해 주세요." onRetry={refetch} />
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
| | EmptyState | ErrorState |
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| 아이콘 | `icon` prop (24, `text/muted`, `bg/subtle` 원 `space/12`) | 기본 `triangle-alert` (`text/danger`), `icon`으로 바꿀 수 있음 |
|
|
437
|
+
| 제목 / 설명 | `body-strong` / `body` + `text/muted`, 가운데 정렬 | 같음 |
|
|
438
|
+
| 행동 | `action` (보통 primary Button 하나) | `onRetry`가 있으면 secondary Button + `refresh-cw`, 문구 `retryLabel`(기본 "다시 시도") |
|
|
439
|
+
| 간격 | 요소 사이 `space/4`, 위아래 `space/12`, 좌우 `space/6` | 같음 |
|
|
440
|
+
|
|
441
|
+
에러 문구는 사과하지 않고, 무엇이 실패했고 어떻게 하면 되는지 써요.
|
|
442
|
+
|
|
443
|
+
### Textarea
|
|
444
|
+
|
|
445
|
+
`<Textarea rows={3} invalid disabled />` — Input과 같은 테두리·hover·focus·invalid 규칙이에요. 최소 높이 80(약 3줄), padding `space/3`·`space/2`, 세로로만 크기를 바꿀 수 있어요. Field 안에서 라벨·에러와 함께 써요.
|
|
446
|
+
|
|
447
|
+
### Checkbox / CheckboxGroup
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
<Checkbox checked={on} onCheckedChange={setOn}>알림 받기</Checkbox>
|
|
451
|
+
<CheckboxGroup label="알림 채널" value={channels} onValueChange={setChannels}>
|
|
452
|
+
<Checkbox value="email">이메일</Checkbox>
|
|
453
|
+
<Checkbox value="push">푸시</Checkbox>
|
|
454
|
+
</CheckboxGroup>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- 여러 개를 고를 수 있을 때 써요. 일부만 선택된 상위 항목은 `indeterminate`예요(화면 낭독기에는 "mixed").
|
|
458
|
+
- 상자 16(`space/4`), `radius/sm`. 해제: `bg/surface` + `border/strong`, 선택: `bg/primary` + check 12(`text/on-primary`). 라벨 `body`, 상자와 간격 `space/2`.
|
|
459
|
+
- 그룹은 `fieldset` + `legend`(`label` 스타일)로 묶고, 항목 사이 `space/2`, legend와 `space/3`이에요.
|
|
460
|
+
|
|
461
|
+
### RadioGroup
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
<RadioGroup label="결제 주기" value={cycle} onValueChange={setCycle}>
|
|
465
|
+
<Radio value="month">월간 결제</Radio>
|
|
466
|
+
<Radio value="year">연간 결제</Radio>
|
|
467
|
+
</RadioGroup>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
하나만 고를 때 써요. 화살표 키로 이동하면 바로 선택돼요. 원 16(`space/4`), 선택 `bg/primary` + 점 8(`space/2`). 항목이 5개를 넘으면 Select를 써요.
|
|
471
|
+
|
|
472
|
+
### Switch
|
|
473
|
+
|
|
474
|
+
`<Switch checked={on} onCheckedChange={setOn}>이메일 알림</Switch>` — 누르는 즉시 적용되는 설정에만 써요. 저장 버튼을 눌러야 반영되는 폼에서는 Checkbox를 써요. 트랙 28×16(`space/7`·`space/4`), 손잡이 12(`space/3`), 해제 `bg/subtle` + `border/strong`, 켜짐 `bg/primary`. 손잡이는 `duration/fast`로 움직여요.
|
|
475
|
+
|
|
476
|
+
### Select
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
<Select options={plans} value={plan} onValueChange={setPlan} placeholder="요금제 선택" /> // 단일: string | null
|
|
480
|
+
<Select multiple options={tags} value={selected} onValueChange={setSelected} placeholder="태그 선택" /> // 다중: string[]
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
| prop | 값 |
|
|
484
|
+
|---|---|
|
|
485
|
+
| `options` | `{ value, label, disabled? }[]` |
|
|
486
|
+
| `multiple` | 다중 선택. 켜면 `value`·`onValueChange`가 배열이에요(타입으로 강제) |
|
|
487
|
+
| `placeholder`, `invalid`, `disabled`, `name`, `aria-label` | Field 안에서는 라벨과 invalid가 자동으로 연결돼요 |
|
|
488
|
+
|
|
489
|
+
| | 단일 | 다중 |
|
|
490
|
+
|---|---|---|
|
|
491
|
+
| 항목 | 선택 시 오른쪽 `check` | 왼쪽 체크박스 상자 |
|
|
492
|
+
| 고른 뒤 | 팝업이 닫혀요 | 열린 채로 여러 개를 골라요 |
|
|
493
|
+
| 트리거 | 고른 항목 이름 | 쉼표로 이어 쓰고, 넘치면 말줄임(…) |
|
|
494
|
+
|
|
495
|
+
- 트리거는 Input과 같은 크기·테두리예요(높이 32). 오른쪽 `chevron-down` 16(`text/muted`), 열리면 180° 돌고 테두리가 `border/focus`가 돼요.
|
|
496
|
+
- 팝업은 트리거 아래 4px, 트리거 너비 이상이에요. 떠 있는 요소 공통 스타일: `bg/surface` + 1px `border/default` + `radius/lg`, 안쪽 `space/1`, 그림자 없음.
|
|
497
|
+
- 항목은 높이 32, padding `space/2`, 키보드나 마우스로 가리키면(`highlighted`) `bg/subtle`이에요.
|
|
498
|
+
- 고를 항목이 5개 이상이거나 화면이 좁을 때 써요. 그보다 적으면 RadioGroup이 한눈에 보여요.
|
|
499
|
+
|
|
500
|
+
### AlertDialog
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
<AlertDialog>
|
|
504
|
+
<AlertDialogTrigger render={<Button variant="danger" />}>삭제</AlertDialogTrigger>
|
|
505
|
+
<AlertDialogPopup>
|
|
506
|
+
<AlertDialogTitle>API 키를 삭제할까요?</AlertDialogTitle>
|
|
507
|
+
<AlertDialogDescription>이 키로 보내는 요청이 바로 실패해요. 삭제하면 되돌릴 수 없어요.</AlertDialogDescription>
|
|
508
|
+
<AlertDialogFooter>
|
|
509
|
+
<AlertDialogClose render={<Button variant="secondary" />}>취소</AlertDialogClose>
|
|
510
|
+
<Button variant="danger" onClick={remove}>삭제</Button>
|
|
511
|
+
</AlertDialogFooter>
|
|
512
|
+
</AlertDialogPopup>
|
|
513
|
+
</AlertDialog>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
되돌릴 수 없는 동작 전에만 써요. Dialog와 같은 모양(Backdrop, Popup 440, padding `space/6`)이고, 바깥을 눌러도 닫히지 않아요(Esc는 닫혀요). 제목은 질문, 설명은 결과, 확인 버튼은 동작 이름 그대로(`danger`)예요.
|
|
517
|
+
|
|
518
|
+
### Menu
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
<Menu>
|
|
522
|
+
<MenuTrigger render={<Button size="icon-md" variant="ghost" aria-label="더 보기" />}><Ellipsis /></MenuTrigger>
|
|
523
|
+
<MenuPopup>
|
|
524
|
+
<MenuItem onClick={rename}><Pencil />이름 바꾸기</MenuItem>
|
|
525
|
+
<MenuSeparator />
|
|
526
|
+
<MenuItem tone="danger" onClick={confirmDelete}><Trash2 />삭제</MenuItem>
|
|
527
|
+
</MenuPopup>
|
|
528
|
+
</Menu>
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
행이나 카드에 딸린 동작을 묶어요. 팝업은 떠 있는 요소 공통 스타일(§10 Select와 같음), 트리거 아래 4px·시작 정렬, 최소 너비 192. 항목 높이 32, 아이콘 16, `highlighted`면 `bg/subtle`. 위험한 동작은 맨 아래 구분선 뒤에 `tone="danger"`(`text/danger`)로 두고, 실행 전에 AlertDialog로 확인해요. 화살표로 이동, Enter로 실행하면 닫혀요.
|
|
532
|
+
|
|
533
|
+
### Popover
|
|
534
|
+
|
|
535
|
+
`<Popover><PopoverTrigger render={<Button variant="secondary" />}>공유</PopoverTrigger><PopoverPopup><PopoverTitle>…</PopoverTitle><PopoverDescription>…</PopoverDescription>…</PopoverPopup></Popover>`
|
|
536
|
+
|
|
537
|
+
대상 가까이에 짧은 내용이나 간단한 입력을 띄울 때 써요. 너비 320, padding `space/4`, 영역 사이 `space/3`, 제목 `body-strong`, 설명 `caption` + `text/muted`, 대상에서 8px. 확인이 필요한 결정은 Dialog, 목록형 동작은 Menu를 써요.
|
|
538
|
+
|
|
539
|
+
### Tooltip
|
|
540
|
+
|
|
541
|
+
`<Tooltip content="설정"><Button size="icon-md" variant="ghost" aria-label="설정"><Settings /></Button></Tooltip>`
|
|
542
|
+
|
|
543
|
+
아이콘만 있는 버튼의 이름을 보여 줘요. `bg/inverse` + `text/inverse`, `caption`, padding `space/2`·`space/1`, `radius/md`, 대상에서 4px. 마우스는 600ms 뒤, 키보드 포커스는 바로 보여요. 중요한 정보나 클릭할 요소는 넣지 않아요(Tooltip이 있어도 `aria-label`은 따로 달아요).
|
|
544
|
+
|
|
545
|
+
### Tabs
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
<Tabs defaultValue="overview">
|
|
549
|
+
<TabsList><Tab value="overview">개요</Tab><Tab value="usage">사용량</Tab></TabsList>
|
|
550
|
+
<TabsPanel value="overview">…</TabsPanel>
|
|
551
|
+
<TabsPanel value="usage">…</TabsPanel>
|
|
552
|
+
</Tabs>
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
같은 대상의 다른 보기를 오갈 때만 써요(페이지 이동 링크에는 쓰지 않아요). 탭 높이 32, padding `space/3`, `body-strong`. 선택 `text/default`, 나머지 `text/muted`(hover 시 `text/default`). 목록 아래 1px `border/default`, 선택 표시 막대 2px `bg/primary`가 `duration/normal`로 움직여요. 목록과 패널 사이 `space/4`예요.
|
|
556
|
+
|
|
557
|
+
키보드: 기본은 화살표 키로 이동하면 **바로 탭이 바뀌어요**(`activateOnFocus`, 기본 `true`). 탭을 바꿀 때 무거운 내용을 불러온다면 `<TabsList activateOnFocus={false}>`로 화살표는 이동만, Enter·Space로 전환하게 해요. 끝에서 처음으로 돌아가는 `loopFocus`도 기본 `true`예요. 키보드 조작 자체를 끄는 옵션은 없어요. 포커스 링은 탭끼리 겹치지 않도록 탭 **안쪽**(offset −2px)에 그려요. 패널(`TabsPanel`)은 Tab 키로 들어갈 수 있어서(WAI-ARIA Tabs 패턴), 키보드 포커스일 때 패널 **바깥**에 같은 2px 링(offset 2px)을 그려요. 마우스로 누를 때는 보이지 않아요(Figma › 10 Navigation › Tabs › Focus).
|
|
558
|
+
|
|
559
|
+
### Separator
|
|
560
|
+
|
|
561
|
+
`<Separator orientation="horizontal" />` — 1px `border/default`. 관련 없는 묶음 사이를 나눌 때만 쓰고, 카드 안 영역이나 목록 행은 간격으로 먼저 구분해요.
|
|
562
|
+
|
|
563
|
+
### Toast
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
// 앱 최상단에 한 번 (Next.js는 layout.tsx). 알림 상자를 화면 오른쪽 아래에 그려요
|
|
567
|
+
<ToastProvider>{children}</ToastProvider>
|
|
568
|
+
|
|
569
|
+
// 클라이언트 컴포넌트 어디서든
|
|
570
|
+
const toast = useToast();
|
|
571
|
+
toast.add({ title: 'API 키를 만들었어요', description: '키는 지금 한 번만 보여 줘요.', tone: 'success' });
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
| `add` 옵션 | 값 | 기본값 |
|
|
575
|
+
|---|---|---|
|
|
576
|
+
| `title` | 결과 한 줄 (필수) | — |
|
|
577
|
+
| `description` | 덧붙일 설명 | — |
|
|
578
|
+
| `tone` | `neutral` · `success` · `warning` · `danger` | `neutral` |
|
|
579
|
+
| `timeout` | 사라지기까지 ms. `0`이면 직접 닫을 때까지 남아요 | 5000 |
|
|
580
|
+
|
|
581
|
+
- 사용자 동작의 결과를 잠깐 알려 줄 때 써요. 되돌릴 수 없는 동작의 확인은 AlertDialog, 화면을 채우는 실패는 ErrorState를 써요.
|
|
582
|
+
- 너비 360(좁은 화면은 좌우 `space/4`를 뺀 너비), 화면 가장자리에서 `space/4`, 알림 사이 `space/2`. 최대 3개까지 보이고 오래된 것부터 가려져요.
|
|
583
|
+
- 상자: 떠 있는 요소 공통 스타일(`bg/surface` + 1px `border/default` + `radius/lg`, 그림자 없음), padding·간격 `space/3`.
|
|
584
|
+
- 아이콘 16: neutral `info`(`text/muted`) · success `circle-check`(`text/success`) · warning `triangle-alert`(`text/warning`) · danger `circle-alert`(`text/danger`). 제목 `body-strong`, 설명 `caption` + `text/muted`, 오른쪽에 닫기(ghost `icon-sm`, `x`, "알림 닫기").
|
|
585
|
+
- 마우스를 올리거나 포커스하면 시간이 멈춰요. 들어오고 나갈 때 opacity와 위치(`space/2`)가 `duration/normal`로 바뀌어요.
|
|
586
|
+
- 화면 낭독기: 보통은 차례가 오면 읽고, `danger`는 바로 읽어요. 알림은 F6으로 이동할 수 있어요.
|
|
587
|
+
- `useToast`는 클라이언트 컴포넌트에서만 써요(`ToastProvider` 안).
|
|
588
|
+
|
|
589
|
+
### Badge
|
|
590
|
+
|
|
591
|
+
`<Badge tone="success" variant="subtle">활성</Badge>`
|
|
592
|
+
|
|
593
|
+
| prop | 값 | 기본값 |
|
|
594
|
+
|---|---|---|
|
|
595
|
+
| `tone` | `neutral` · `success` · `warning` · `danger` | `neutral` |
|
|
596
|
+
| `variant` | `subtle` · `solid` | `subtle` |
|
|
597
|
+
|
|
598
|
+
| | subtle | solid |
|
|
599
|
+
|---|---|---|
|
|
600
|
+
| neutral | `bg/subtle` + `text/default` | `bg/inverse` + `text/inverse` |
|
|
601
|
+
| success | `bg/success-subtle` + `text/success` | `bg/success` + `text/on-success` |
|
|
602
|
+
| warning | `bg/warning-subtle` + `text/warning` | `bg/warning` + `text/on-warning` |
|
|
603
|
+
| danger | `bg/danger-subtle` + `text/danger` | `bg/danger` + `text/on-danger` |
|
|
604
|
+
|
|
605
|
+
높이 20(`space/5`), 좌우 `space/2`, `label`, `radius/sm`. 상태를 짧은 낱말로 표시해요. 목록과 표에서는 subtle을, 꼭 눈에 띄어야 하는 한 곳에만 solid를 써요. 누를 수 없는 표시라 버튼처럼 쓰지 않아요.
|
|
606
|
+
|
|
607
|
+
### Progress
|
|
608
|
+
|
|
609
|
+
`<Progress value={45} label="내보내는 중" />`
|
|
610
|
+
|
|
611
|
+
| prop | 값 |
|
|
612
|
+
|---|---|
|
|
613
|
+
| `value` | 현재 값 (필수) |
|
|
614
|
+
| `max` | 최댓값 (기본 100) |
|
|
615
|
+
| `label` | 라벨 문구. 없으면 `aria-label`이 필수예요 (타입으로 강제) |
|
|
616
|
+
|
|
617
|
+
라벨 `label` + `text/default`, 오른쪽에 백분율 `mono-caption` + `text/muted`, 막대와 사이 `space/2`. 트랙 높이 4(`space/1`) `bg/subtle` `radius/full`, 채움 `bg/primary`이 `duration/normal`로 늘어나요. 끝이 정해진 작업(업로드, 내보내기)에만 써요. 끝을 알 수 없는 기다림은 Button `loading`이나 Skeleton을 써요.
|
|
618
|
+
|
|
300
619
|
## 11. Figma ↔ 코드 매핑 (Code Connect 대체)
|
|
301
620
|
|
|
302
621
|
Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수 없어요. 아래 표로 대신합니다. Figma 컴포넌트를 코드로 옮길 때는 이 표의 컴포넌트를 그대로 써요.
|
|
@@ -304,10 +623,30 @@ Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수
|
|
|
304
623
|
| Figma 컴포넌트 (Core 페이지) | node id | 코드 | 파일 |
|
|
305
624
|
|---|---|---|---|
|
|
306
625
|
| Button | `5:52` | `<Button variant size disabled>` | `src/components/button.tsx` |
|
|
626
|
+
| Button Icon | `23:2294` | `<Button size="icon-*" variant disabled aria-label>` + 아이콘 자식 | `src/components/button.tsx` |
|
|
307
627
|
| Input | `5:61` | `<Input invalid disabled placeholder>` | `src/components/input.tsx` |
|
|
308
628
|
| Text | `5:110` | `<Text variant tone as>` | `src/components/text.tsx` |
|
|
309
629
|
| Avatar | `5:117` | `<Avatar size src alt fallback>` | `src/components/avatar.tsx` |
|
|
310
630
|
| Card | `6:2` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardBody`, `CardFooter` | `src/components/card.tsx` |
|
|
311
631
|
| Dialog | `6:13` | `Dialog`, `DialogTrigger`, `DialogPopup`, `DialogTitle`, `DialogDescription`, `DialogClose`, `DialogFooter` | `src/components/dialog.tsx` |
|
|
312
|
-
|
|
313
|
-
|
|
632
|
+
| Field | `31:1543` | `<Field label description error invalid disabled>` + `<Input />` | `src/components/field.tsx` |
|
|
633
|
+
| Skeleton | `31:1547` | `<Skeleton shape>` | `src/components/skeleton.tsx` |
|
|
634
|
+
| EmptyState | `31:1548` | `<EmptyState icon title description action>` | `src/components/empty-state.tsx` |
|
|
635
|
+
| ErrorState | `31:1561` | `<ErrorState icon title description onRetry retryLabel>` | `src/components/error-state.tsx` |
|
|
636
|
+
| Textarea | `44:2239` | `<Textarea invalid disabled placeholder>` | `src/components/textarea.tsx` |
|
|
637
|
+
| Checkbox | `44:2266` | `<Checkbox checked indeterminate disabled>` (`checked=indeterminate` → `indeterminate`) | `src/components/checkbox.tsx` |
|
|
638
|
+
| Radio | `44:2281` | `<RadioGroup label>` + `<Radio value disabled>` | `src/components/radio-group.tsx` |
|
|
639
|
+
| Switch | `44:2298` | `<Switch checked disabled>` | `src/components/switch.tsx` |
|
|
640
|
+
| Select Trigger · Item · Popup | `44:2315` · `44:2372` · `44:2373` | `<Select options multiple placeholder invalid disabled>` (Trigger `state=open`은 열린 상태, Item은 `options`에서 자동 생성) | `src/components/select.tsx` |
|
|
641
|
+
| AlertDialog | `49:4264` | `AlertDialog`, `AlertDialogTrigger`, `AlertDialogPopup`, `…Title`, `…Description`, `…Close`, `…Footer` | `src/components/alert-dialog.tsx` |
|
|
642
|
+
| Menu Item · Menu Popup | `49:3092` · `49:3093` | `<MenuItem tone disabled>` (`showIcon`/`icon` → 자식 아이콘), `MenuPopup`, `MenuSeparator` | `src/components/menu.tsx` |
|
|
643
|
+
| Popover | `49:4247` | `Popover`, `PopoverTrigger`, `PopoverPopup`, `PopoverTitle`, `PopoverDescription`, `PopoverClose` | `src/components/popover.tsx` |
|
|
644
|
+
| Tooltip | `49:3040` | `<Tooltip content>` | `src/components/tooltip.tsx` |
|
|
645
|
+
| Tab · Tabs List | `49:3050` · `49:3051` | `Tabs`, `TabsList`, `<Tab value disabled>`, `TabsPanel` (`selected` → 현재 값) | `src/components/tabs.tsx` |
|
|
646
|
+
| Separator | `49:3039` | `<Separator orientation>` | `src/components/separator.tsx` |
|
|
647
|
+
| Toast | `57:3549` | `useToast().add({ tone, title, description })` (`showDescription` → `description`을 넘길지), 앱에는 `<ToastProvider>` | `src/components/toast.tsx` |
|
|
648
|
+
| Badge | `57:3498` | `<Badge tone variant>` (`children` → 자식 문구) | `src/components/badge.tsx` |
|
|
649
|
+
| Progress | `58:5093` | `<Progress value max label>` (`value` variant는 채움 미리보기, `value text`는 코드가 자동 표시) | `src/components/progress.tsx` |
|
|
650
|
+
| Icon (32개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
|
|
651
|
+
|
|
652
|
+
Figma 속성 → props 변환 규칙: variant 속성은 같은 이름의 prop으로, 텍스트 속성 `children`은 자식으로, boolean `header`/`footer`는 해당 하위 컴포넌트를 넣을지 말지로 옮겨요. Button의 `iconLeft`/`iconRight`(+ `leftIcon`/`rightIcon` 교체)와 Button Icon의 `icon`은 prop이 아니라 **자식 순서**예요: `<Button><Plus />추가하기<ChevronDown /></Button>`.
|
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Dialog, DialogClose, Dia
|
|
|
88
88
|
|
|
89
89
|
컴포넌트별 props는 [DESIGN.md §10](./DESIGN.md#10-컴포넌트-스펙-core)에 있어요. 모든 파일에 `"use client"`가 붙어 있어서 Next.js 서버 컴포넌트에서도 바로 import할 수 있어요. 하위 컴포넌트는 `CardHeader`처럼 이름 있는 export로 써요.
|
|
90
90
|
|
|
91
|
+
Toast를 쓰려면 앱 최상단(Next.js는 `app/layout.tsx`)을 `<ToastProvider>`로 한 번 감싸고, 클라이언트 컴포넌트에서 `useToast().add({ title, tone })`로 띄워요.
|
|
92
|
+
|
|
91
93
|
## Claude Code에서 쓰기
|
|
92
94
|
|
|
93
95
|
프로젝트 CLAUDE.md에 추가해요.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type DialogButtonProps, type DialogPopupProps, type DialogProps } from './dialog';
|
|
3
|
+
/** 되돌릴 수 없는 동작(삭제 등) 확인. Dialog와 같은 모양이고 바깥을 눌러도 닫히지 않아요 */
|
|
4
|
+
export declare function AlertDialog({ onOpenChange, ...props }: DialogProps): React.JSX.Element;
|
|
5
|
+
export declare const AlertDialogTrigger: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export declare const AlertDialogClose: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
7
|
+
export declare const AlertDialogPopup: React.ForwardRefExoticComponent<DialogPopupProps & React.RefAttributes<HTMLDivElement>>;
|
|
8
|
+
export declare const AlertDialogTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
|
|
9
|
+
className?: string;
|
|
10
|
+
} & React.RefAttributes<HTMLHeadingElement>>;
|
|
11
|
+
export declare const AlertDialogDescription: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLParagraphElement>, "className"> & {
|
|
12
|
+
className?: string;
|
|
13
|
+
} & React.RefAttributes<HTMLParagraphElement>>;
|
|
14
|
+
export declare const AlertDialogFooter: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
15
|
+
className?: string;
|
|
16
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import { DialogFooter as t, backdropClass as n, dialogDescriptionClass as r, dialogPopupClass as i, dialogTitleClass as a } from "./dialog.js";
|
|
4
|
+
import * as o from "react";
|
|
5
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
|
+
import { AlertDialog as l } from "@base-ui/react/alert-dialog";
|
|
7
|
+
//#region src/components/alert-dialog.tsx
|
|
8
|
+
function u({ onOpenChange: e, ...t }) {
|
|
9
|
+
return /* @__PURE__ */ s(l.Root, {
|
|
10
|
+
...t,
|
|
11
|
+
onOpenChange: e && ((t) => e(t))
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
var d = o.forwardRef(function(e, t) {
|
|
15
|
+
return /* @__PURE__ */ s(l.Trigger, {
|
|
16
|
+
ref: t,
|
|
17
|
+
...e
|
|
18
|
+
});
|
|
19
|
+
}), f = o.forwardRef(function(e, t) {
|
|
20
|
+
return /* @__PURE__ */ s(l.Close, {
|
|
21
|
+
ref: t,
|
|
22
|
+
...e
|
|
23
|
+
});
|
|
24
|
+
}), p = o.forwardRef(function({ className: t, initialFocus: r, finalFocus: a, ...o }, u) {
|
|
25
|
+
return /* @__PURE__ */ c(l.Portal, { children: [/* @__PURE__ */ s(l.Backdrop, { className: n }), /* @__PURE__ */ s(l.Popup, {
|
|
26
|
+
ref: u,
|
|
27
|
+
initialFocus: r,
|
|
28
|
+
finalFocus: a,
|
|
29
|
+
className: e(i, t),
|
|
30
|
+
...o
|
|
31
|
+
})] });
|
|
32
|
+
}), m = o.forwardRef(function({ className: t, ...n }, r) {
|
|
33
|
+
return /* @__PURE__ */ s(l.Title, {
|
|
34
|
+
ref: r,
|
|
35
|
+
className: e(a, t),
|
|
36
|
+
...n
|
|
37
|
+
});
|
|
38
|
+
}), h = o.forwardRef(function({ className: t, ...n }, i) {
|
|
39
|
+
return /* @__PURE__ */ s(l.Description, {
|
|
40
|
+
ref: i,
|
|
41
|
+
className: e(r, t),
|
|
42
|
+
...n
|
|
43
|
+
});
|
|
44
|
+
}), g = t;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { u as AlertDialog, f as AlertDialogClose, h as AlertDialogDescription, g as AlertDialogFooter, p as AlertDialogPopup, m as AlertDialogTitle, d as AlertDialogTrigger };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'className'> {
|
|
3
|
+
tone?: 'neutral' | 'success' | 'warning' | 'danger';
|
|
4
|
+
/** 목록·표에서는 subtle, 꼭 눈에 띄어야 하는 한 곳에만 solid */
|
|
5
|
+
variant?: 'subtle' | 'solid';
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const Badge: React.ForwardRefExoticComponent<BadgeProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import * as t from "react";
|
|
4
|
+
import { cva as n } from "class-variance-authority";
|
|
5
|
+
import { jsx as r } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/badge.tsx
|
|
7
|
+
var i = n("sn:box-border sn:inline-flex sn:h-5 sn:shrink-0 sn:items-center sn:gap-1 sn:whitespace-nowrap sn:rounded-sm sn:px-2 sn:font-sans sn:text-label sn:[&_svg]:size-3 sn:[&_svg]:shrink-0", {
|
|
8
|
+
variants: {
|
|
9
|
+
tone: {
|
|
10
|
+
neutral: "",
|
|
11
|
+
success: "",
|
|
12
|
+
warning: "",
|
|
13
|
+
danger: ""
|
|
14
|
+
},
|
|
15
|
+
variant: {
|
|
16
|
+
subtle: "",
|
|
17
|
+
solid: ""
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
compoundVariants: [
|
|
21
|
+
{
|
|
22
|
+
tone: "neutral",
|
|
23
|
+
variant: "subtle",
|
|
24
|
+
className: "sn:bg-subtle sn:text-default"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
tone: "neutral",
|
|
28
|
+
variant: "solid",
|
|
29
|
+
className: "sn:bg-inverse sn:text-inverse"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
tone: "success",
|
|
33
|
+
variant: "subtle",
|
|
34
|
+
className: "sn:bg-success-subtle sn:text-success"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
tone: "success",
|
|
38
|
+
variant: "solid",
|
|
39
|
+
className: "sn:bg-success sn:text-on-success"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
tone: "warning",
|
|
43
|
+
variant: "subtle",
|
|
44
|
+
className: "sn:bg-warning-subtle sn:text-warning"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
tone: "warning",
|
|
48
|
+
variant: "solid",
|
|
49
|
+
className: "sn:bg-warning sn:text-on-warning"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
tone: "danger",
|
|
53
|
+
variant: "subtle",
|
|
54
|
+
className: "sn:bg-danger-subtle sn:text-danger"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
tone: "danger",
|
|
58
|
+
variant: "solid",
|
|
59
|
+
className: "sn:bg-danger sn:text-on-danger"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
defaultVariants: {
|
|
63
|
+
tone: "neutral",
|
|
64
|
+
variant: "subtle"
|
|
65
|
+
}
|
|
66
|
+
}), a = t.forwardRef(function({ tone: t, variant: n, className: a, ...o }, s) {
|
|
67
|
+
return /* @__PURE__ */ r("span", {
|
|
68
|
+
ref: s,
|
|
69
|
+
className: e(i({
|
|
70
|
+
tone: t,
|
|
71
|
+
variant: n
|
|
72
|
+
}), a),
|
|
73
|
+
...o
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//#endregion
|
|
77
|
+
export { a as Badge };
|