@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.
Files changed (54) hide show
  1. package/AI-RULES.md +1 -1
  2. package/DESIGN.md +355 -16
  3. package/README.md +2 -0
  4. package/dist/components/alert-dialog.d.ts +16 -0
  5. package/dist/components/alert-dialog.js +46 -0
  6. package/dist/components/badge.d.ts +8 -0
  7. package/dist/components/badge.js +77 -0
  8. package/dist/components/button.d.ts +11 -2
  9. package/dist/components/button.js +24 -14
  10. package/dist/components/checkbox.d.ts +30 -0
  11. package/dist/components/checkbox.js +48 -0
  12. package/dist/components/dialog.d.ts +11 -0
  13. package/dist/components/dialog.js +17 -14
  14. package/dist/components/empty-state.d.ts +11 -0
  15. package/dist/components/empty-state.js +17 -0
  16. package/dist/components/error-state.d.ts +12 -0
  17. package/dist/components/error-state.js +24 -0
  18. package/dist/components/field.d.ts +13 -0
  19. package/dist/components/field.js +33 -0
  20. package/dist/components/input.d.ts +1 -0
  21. package/dist/components/input.js +5 -5
  22. package/dist/components/menu.d.ts +33 -0
  23. package/dist/components/menu.js +44 -0
  24. package/dist/components/popover.d.ts +26 -0
  25. package/dist/components/popover.js +50 -0
  26. package/dist/components/popup-styles.d.ts +3 -0
  27. package/dist/components/popup-styles.js +5 -0
  28. package/dist/components/progress.d.ts +16 -0
  29. package/dist/components/progress.js +27 -0
  30. package/dist/components/radio-group.d.ts +20 -0
  31. package/dist/components/radio-group.js +40 -0
  32. package/dist/components/select.d.ts +30 -0
  33. package/dist/components/select.js +66 -0
  34. package/dist/components/separator.d.ts +7 -0
  35. package/dist/components/separator.js +14 -0
  36. package/dist/components/skeleton.d.ts +8 -0
  37. package/dist/components/skeleton.js +22 -0
  38. package/dist/components/state-layout.d.ts +11 -0
  39. package/dist/components/state-layout.js +31 -0
  40. package/dist/components/switch.d.ts +12 -0
  41. package/dist/components/switch.js +24 -0
  42. package/dist/components/tabs.d.ts +31 -0
  43. package/dist/components/tabs.js +41 -0
  44. package/dist/components/textarea.d.ts +6 -0
  45. package/dist/components/textarea.js +20 -0
  46. package/dist/components/toast.d.ts +24 -0
  47. package/dist/components/toast.js +62 -0
  48. package/dist/components/tooltip.d.ts +11 -0
  49. package/dist/components/tooltip.js +19 -0
  50. package/dist/index.d.ts +18 -0
  51. package/dist/index.js +23 -5
  52. package/dist/styles.css +1 -1
  53. package/dist/tailwind.css +15 -0
  54. 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, Text, Card, Avatar, Dialog가 있는 역할은 직접 만들지 않아요. 없는 역할이 필요하면 먼저 사용자에게 알려요(라이브러리에 추가할지 판단이 필요해요).
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`로 바꾸고, 입력란 바로 아래에 `caption` + `color/text/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
- hover, focus는 Figma에서 `state=hover`, `state=focus`로 그리고, 코드에서는 CSS의 `:hover`, `:focus-visible`로 처리해요.
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
- | 로딩 | 정상 화면과 같은 배치에 자리만 표시(`color/bg/subtle` 블록) |
192
- | 빈 상태 | 무엇이 없는지, 다음에 할 행동 하나(버튼) |
193
- | 에러 | 무엇이 실패했는지, 다시 시도할 방법 |
239
+ | 상태 | 그릴 것 | 컴포넌트 |
240
+ |---|---|---|
241
+ | 정상 | 실제와 비슷한 양의 데이터 | 화면 구성 그대로 |
242
+ | 로딩 | 정상 화면과 **같은 배치**에 자리만 표시 | `Skeleton` (text · rect · circle) |
243
+ | 빈 상태 | 무엇이 없는지, 다음에 할 행동 하나 | `EmptyState` |
244
+ | 에러 | 무엇이 실패했는지, 다시 시도할 방법 | `ErrorState` |
194
245
 
195
- Skeleton, EmptyState, ErrorState 컴포넌트는 첫 실제 프로젝트에서 필요할 때 Core에 추가해요. 그 전에는 Core 조합으로 그립니다.
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
- Figma 속성 → props 변환 규칙: variant 속성은 같은 이름의 prop으로, 텍스트 속성 `children`은 자식으로, boolean `header`/`footer`는 해당 하위 컴포넌트를 넣을지 말지로 옮겨요.
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 };