@nexus-cross/design-system 1.0.0 → 1.0.2

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 (82) hide show
  1. package/cursor-rules/nexus-project-setup.mdc +150 -150
  2. package/cursor-rules/nexus-ui-api.mdc +659 -316
  3. package/cursor-rules/nexus-ui-components.mdc +162 -96
  4. package/dist/chunks/chunk-55IEEVNR.js +7 -0
  5. package/dist/chunks/{chunk-D6FII7HW.js → chunk-BBLBTOP4.js} +8 -5
  6. package/dist/chunks/{chunk-5JHN4FCY.mjs → chunk-K2TBLM3F.mjs} +1 -4
  7. package/dist/chunks/{chunk-MTX7GD3H.js → chunk-PEIEVKD5.js} +1 -4
  8. package/dist/chunks/{chunk-54RBL7J4.mjs → chunk-UKRU46PH.mjs} +8 -5
  9. package/dist/chunks/chunk-XMG7ZEYY.mjs +5 -0
  10. package/dist/data-list.js +2 -2
  11. package/dist/data-list.mjs +1 -1
  12. package/dist/error-boundary.d.mts +1 -1
  13. package/dist/error-boundary.d.ts +1 -1
  14. package/dist/index.js +5 -5
  15. package/dist/index.mjs +2 -2
  16. package/dist/schemas/_all.json +870 -373
  17. package/dist/schemas/accordion.json +12 -12
  18. package/dist/schemas/avatar.json +9 -9
  19. package/dist/schemas/button.json +27 -9
  20. package/dist/schemas/carousel.json +6 -6
  21. package/dist/schemas/carouselButton.json +3 -3
  22. package/dist/schemas/carouselDots.json +2 -2
  23. package/dist/schemas/carouselSlide.json +3 -3
  24. package/dist/schemas/checkBox.json +28 -10
  25. package/dist/schemas/chip.json +13 -7
  26. package/dist/schemas/clientOnly.json +3 -3
  27. package/dist/schemas/countdown.json +8 -8
  28. package/dist/schemas/counter.json +13 -10
  29. package/dist/schemas/dataList.json +10 -10
  30. package/dist/schemas/divider.json +8 -5
  31. package/dist/schemas/drawer.json +22 -3
  32. package/dist/schemas/drawerClose.json +24 -0
  33. package/dist/schemas/drawerContent.json +7 -7
  34. package/dist/schemas/drawerDescription.json +20 -0
  35. package/dist/schemas/drawerTitle.json +20 -0
  36. package/dist/schemas/drawerTrigger.json +24 -0
  37. package/dist/schemas/ellipsis.json +9 -9
  38. package/dist/schemas/errorBoundary.json +4 -4
  39. package/dist/schemas/infiniteScroll.json +12 -12
  40. package/dist/schemas/marquee.json +10 -7
  41. package/dist/schemas/modalCall.json +81 -3
  42. package/dist/schemas/modalTemplate.json +28 -25
  43. package/dist/schemas/numberInput.json +32 -14
  44. package/dist/schemas/pagination.json +8 -8
  45. package/dist/schemas/popover.json +12 -12
  46. package/dist/schemas/radioGroup.json +17 -10
  47. package/dist/schemas/radioItem.json +12 -5
  48. package/dist/schemas/select.json +11 -11
  49. package/dist/schemas/selectItem.json +5 -5
  50. package/dist/schemas/skeleton.json +10 -7
  51. package/dist/schemas/spinner.json +11 -4
  52. package/dist/schemas/switch.json +18 -7
  53. package/dist/schemas/tab.json +15 -15
  54. package/dist/schemas/table.json +14 -14
  55. package/dist/schemas/tableRow.json +5 -5
  56. package/dist/schemas/tdColumn.json +17 -17
  57. package/dist/schemas/textArea.json +42 -9
  58. package/dist/schemas/textInput.json +55 -15
  59. package/dist/schemas/themeProvider.json +10 -10
  60. package/dist/schemas/toastOptions.json +81 -0
  61. package/dist/schemas/toaster.json +48 -3
  62. package/dist/schemas/tooltip.json +10 -10
  63. package/dist/schemas/virtualGrid.json +19 -16
  64. package/dist/schemas/virtualList.json +12 -9
  65. package/dist/schemas.d.mts +420 -56
  66. package/dist/schemas.d.ts +420 -56
  67. package/dist/schemas.js +502 -367
  68. package/dist/schemas.mjs +498 -368
  69. package/dist/styles/layer.js +2 -2
  70. package/dist/styles/layer.mjs +1 -1
  71. package/dist/styles.css +56 -45
  72. package/dist/styles.js +2 -2
  73. package/dist/styles.layered.css +56 -45
  74. package/dist/styles.mjs +1 -1
  75. package/dist/text-input.d.mts +1 -1
  76. package/dist/text-input.d.ts +1 -1
  77. package/dist/text-input.js +3 -3
  78. package/dist/text-input.mjs +1 -1
  79. package/package.json +8 -6
  80. package/scripts/setup-cursor-rules.cjs +6 -6
  81. package/dist/chunks/chunk-7AISZYWL.js +0 -7
  82. package/dist/chunks/chunk-V5OTJP6H.mjs +0 -5
@@ -1,41 +1,57 @@
1
1
  ---
2
- description: "@nexus-cross/design-system 컴포넌트 API 레퍼런스모든 공용 컴포넌트의 props와 사용 예시"
2
+ description: "@nexus-cross/design-system component API referenceprops and usage examples for all shared components"
3
3
  globs: "**/*.tsx,**/*.jsx,**/*.ts"
4
4
  alwaysApply: false
5
5
  ---
6
6
 
7
7
  # @nexus-cross/design-system — Component API Reference
8
8
 
9
- 모든 컴포넌트는 `@nexus-cross/design-system`에서 import.
9
+ All components are imported from `@nexus-cross/design-system`.
10
10
 
11
11
  ---
12
12
 
13
13
  ## Button
14
14
 
15
- 인터랙티브 버튼. `asChild`로 렌더링 요소 변경 가능.
15
+ Interactive button. semantic(color) x variant(style) 2-axis system. Rendering element changeable via asChild.
16
16
 
17
17
  | Prop | Type | Default | Description |
18
18
  |---|---|---|---|
19
- | `variant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'danger'` | `'primary'` | 시각 스타일 |
20
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 크기 |
21
- | `asChild` | `boolean` | `false` | 자식 요소로 렌더링 |
22
- | `detectDoubleClick` | `boolean` | `false` | 500ms 더블 클릭 방지 |
23
- | `disabled` | `boolean` | - | 비활성 (`aria-disabled` 자동) |
24
- | `className` | `string` | - | 스타일 오버라이드 |
19
+ | `semantic` | `'primary'` \| `'secondary'` \| `'normal'` \| `'danger'` | `"primary"` | Color theme (primary=main, secondary=sub, normal=neutral, danger=danger) |
20
+ | `variant` | `'contained'` \| `'outlined'` \| `'subtle'` \| `'ghost'` | `"contained"` | Visual style (contained=filled, outlined=border, subtle=light bg, ghost=transparent) |
21
+ | `size` | `'xl'` \| `'lg'` \| `'md'` \| `'sm'` | `"md"` | Size |
22
+ | `radius` | `'default'` \| `'circle'` | `"default"` | Corner radius (default=size-based radius, circle=pill shape) |
23
+ | `asChild` | `boolean` | - | If true, renders as child element (Slot pattern) |
24
+ | `detectDoubleClick` | `boolean` | - | Prevent double click within 500ms |
25
+ | `disabled` | `boolean` | - | Disabled (auto aria-disabled) |
26
+ | `children` | `ReactNode` | - | Button content (ReactNode) |
27
+ | `onClick` | `ReactNode` | - | Click event handler |
28
+ | `type` | `'button'` \| `'submit'` \| `'reset'` | - | HTML button type (default: button) |
29
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
30
+ | `className` | `string` | - | Style override |
31
+
32
+ 2-axis variant: semantic (color) × variant (style). sizes: xl, lg, md, sm. radius: default, circle.
25
33
 
26
34
  ```tsx
27
- <Button variant="primary" size="lg">확인</Button>
35
+ // Default (primary + contained)
36
+ <Button>Confirm</Button>
28
37
 
29
- <Button variant="outline" disabled>비활성</Button>
38
+ // semantic × variant combinations
39
+ <Button semantic="primary" variant="contained" size="lg">Primary Contained</Button>
40
+ <Button semantic="secondary" variant="outlined" size="md">Secondary Outlined</Button>
41
+ <Button semantic="normal" variant="subtle" size="sm">Normal Subtle</Button>
42
+ <Button semantic="danger" variant="ghost">Danger Ghost</Button>
30
43
 
31
- // 로딩 상태 (Spinner를 children에 직접 배치)
44
+ // Pill shape (radius="circle")
45
+ <Button semantic="primary" variant="contained" radius="circle">Pill</Button>
46
+
47
+ // Loading state
32
48
  <Button disabled>
33
- <Spinner size={16} /> 처리 중...
49
+ <Spinner size={16} /> Processing...
34
50
  </Button>
35
51
 
36
- // <a> 태그로 렌더링
37
- <Button asChild variant="ghost">
38
- <a href="/settings">설정</a>
52
+ // Render as <a> tag
53
+ <Button asChild semantic="normal" variant="ghost">
54
+ <a href="/settings">Settings</a>
39
55
  </Button>
40
56
  ```
41
57
 
@@ -43,42 +59,91 @@ alwaysApply: false
43
59
 
44
60
  ## TextInput
45
61
 
46
- 텍스트 입력 필드. prefix/suffix 아이콘 지원.
62
+ Text input field. Supports label, description, prefix/suffix icons, clearable, character counter.
47
63
 
48
64
  | Prop | Type | Default | Description |
49
65
  |---|---|---|---|
50
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 크기 |
51
- | `error` | `boolean` | `false` | 에러 상태 (`aria-invalid` 자동) |
52
- | `prefixIcon` | `ReactNode` | - | 앞쪽 아이콘 |
53
- | `suffixIcon` | `ReactNode` | - | 뒤쪽 아이콘 |
54
- | `onValueChange` | `(value: string) => void` | - | 변경 콜백 |
66
+ | `size` | `'md'` \| `'lg'` \| `'xl'` | `"md"` | Size |
67
+ | `error` | `boolean` | - | Error state (auto aria-invalid) |
68
+ | `prefixIcon` | `ReactNode` | - | Prefix icon (ReactNode) |
69
+ | `suffixIcon` | `ReactNode` | - | Suffix icon (ReactNode) |
70
+ | `label` | `ReactNode` | - | Label above input field (ReactNode) |
71
+ | `description` | `ReactNode` | - | Description below input field (ReactNode, red on error) |
72
+ | `showCount` | `boolean` | - | Show character count (requires maxLength) |
73
+ | `maxLength` | `number` | - | Maximum character count |
74
+ | `clearable` | `boolean` | - | Clear input via X button |
75
+ | `placeholder` | `string` | - | Placeholder |
76
+ | `disabled` | `boolean` | - | Disabled |
77
+ | `readOnly` | `boolean` | - | Read-only |
78
+ | `value` | `string` | - | Input value (controlled mode) |
79
+ | `defaultValue` | `string` | - | Initial value (uncontrolled mode) |
80
+ | `type` | `string` | - | Input type (text, password, email, url, etc.) |
81
+ | `name` | `string` | - | Form field name |
82
+ | `id` | `string` | - | Element ID (for label htmlFor binding) |
83
+ | `autoFocus` | `boolean` | - | Auto focus |
84
+ | `autoComplete` | `string` | - | Autocomplete hint (on, off, email, etc.) |
85
+ | `onValueChange` | `ReactNode` | - | Value change callback (value: string) => void |
86
+ | `onChange` | `ReactNode` | - | Native change event handler |
87
+ | `onBlur` | `ReactNode` | - | Blur callback |
88
+ | `onFocus` | `ReactNode` | - | Focus callback |
89
+ | `className` | `string` | - | Style override |
55
90
 
56
91
  ```tsx
57
- <TextInput placeholder="이메일" size="md" />
92
+ <TextInput placeholder="Email" size="md" />
58
93
 
59
94
  <TextInput
60
- error
95
+ label="Email"
96
+ description="Enter your email address"
97
+ placeholder="example@email.com"
98
+ clearable
99
+ />
100
+
101
+ <TextInput
102
+ label="Search"
61
103
  prefixIcon={<SearchIcon />}
104
+ clearable
62
105
  onValueChange={(v) => setQuery(v)}
63
106
  />
107
+
108
+ <TextInput
109
+ label="Nickname"
110
+ description="2-20 characters"
111
+ showCount
112
+ maxLength={20}
113
+ clearable
114
+ error
115
+ />
64
116
  ```
65
117
 
66
118
  ---
67
119
 
68
120
  ## TextArea
69
121
 
70
- 여러 텍스트 입력. 글자 수 카운터 내장.
122
+ Multi-line text input. Built-in character counter.
71
123
 
72
124
  | Prop | Type | Default | Description |
73
125
  |---|---|---|---|
74
- | `error` | `boolean` | `false` | 에러 상태 (`aria-invalid` 자동) |
75
- | `showCount` | `boolean` | `false` | 글자 표시 (`maxLength` 필요) |
76
- | `maxLength` | `number` | - | 최대 글자 |
77
- | `onValueChange` | `(value: string) => void` | - | 변경 콜백 |
126
+ | `error` | `boolean` | - | Error state (auto aria-invalid) |
127
+ | `showCount` | `boolean` | - | Show character count (requires maxLength) |
128
+ | `maxLength` | `number` | - | Maximum character count |
129
+ | `placeholder` | `string` | - | Placeholder |
130
+ | `rows` | `number` | - | Visible row count |
131
+ | `disabled` | `boolean` | - | Disabled |
132
+ | `readOnly` | `boolean` | - | Read-only |
133
+ | `value` | `string` | - | Input value (controlled mode) |
134
+ | `defaultValue` | `string` | - | Initial value (uncontrolled mode) |
135
+ | `name` | `string` | - | Form field name |
136
+ | `id` | `string` | - | Element ID |
137
+ | `autoFocus` | `boolean` | - | Auto focus |
138
+ | `onValueChange` | `ReactNode` | - | Value change callback (value: string) => void |
139
+ | `onChange` | `ReactNode` | - | Native change event handler |
140
+ | `onBlur` | `ReactNode` | - | Blur callback |
141
+ | `onFocus` | `ReactNode` | - | Focus callback |
142
+ | `className` | `string` | - | Style override |
78
143
 
79
144
  ```tsx
80
145
  <TextArea
81
- placeholder="내용을 입력하세요"
146
+ placeholder="Enter your content"
82
147
  maxLength={500}
83
148
  showCount
84
149
  rows={4}
@@ -89,23 +154,37 @@ alwaysApply: false
89
154
 
90
155
  ## Select
91
156
 
92
- 드롭다운 선택. Radix Select 기반.
157
+ Dropdown select. Based on Radix Select. Used with SelectItem.
158
+
159
+ | Prop | Type | Default | Description |
160
+ |---|---|---|---|
161
+ | `value` | `string` | - | Selected value |
162
+ | `placeholder` | `string` | - | Placeholder |
163
+ | `variant` | `'default'` \| `'outline'` | `"default"` | Trigger style |
164
+ | `size` | `'sm'` \| `'md'` \| `'lg'` \| `'full'` | `"full"` | Width |
165
+ | `disabled` | `boolean` | - | Disabled |
166
+ | `onValueChange` | `ReactNode` | - | Value change callback (value: string) => void |
167
+ | `displayComponent` | `ReactNode` | - | Custom display in trigger (ReactNode) |
168
+ | `children` | `ReactNode` | - | SelectItem list (ReactNode, required) |
169
+ | `className` | `string` | - | Wrapper style |
170
+ | `triggerClassName` | `string` | - | Trigger style override |
171
+
172
+ ### SelectItem
173
+
174
+ Individual option within Select.
93
175
 
94
176
  | Prop | Type | Default | Description |
95
177
  |---|---|---|---|
96
- | `value` | `string` | - | 선택된 |
97
- | `onValueChange` | `(value: string) => void` | - | 변경 콜백 |
98
- | `placeholder` | `string` | - | 플레이스홀더 |
99
- | `variant` | `'default' \| 'outline'` | `'default'` | 트리거 스타일 |
100
- | `size` | `'sm' \| 'md' \| 'lg' \| 'full'` | `'full'` | 너비 |
101
- | `disabled` | `boolean` | - | 비활성 |
102
- | `displayComponent` | `ReactNode` | - | 트리거에 커스텀 표시 |
178
+ | `value` | `string` | - | Item value |
179
+ | `children` | `ReactNode` | - | Item content (ReactNode, required) |
180
+ | `disabled` | `boolean` | - | Disabled |
181
+ | `className` | `string` | - | Style override |
103
182
 
104
183
  ```tsx
105
- <Select value={lang} onValueChange={setLang} placeholder="언어 선택">
106
- <SelectItem value="ko">한국어</SelectItem>
184
+ <Select value={lang} onValueChange={setLang} placeholder="Select language">
185
+ <SelectItem value="ko">Korean</SelectItem>
107
186
  <SelectItem value="en">English</SelectItem>
108
- <SelectItem value="ja">日本語</SelectItem>
187
+ <SelectItem value="ja">Japanese</SelectItem>
109
188
  </Select>
110
189
  ```
111
190
 
@@ -113,22 +192,30 @@ alwaysApply: false
113
192
 
114
193
  ## CheckBox
115
194
 
116
- 체크박스. 네이티브 `<input>` 기반, square/round 형태 지원.
195
+ Checkbox. Native input-based, supports square/round shapes.
117
196
 
118
197
  | Prop | Type | Default | Description |
119
198
  |---|---|---|---|
120
- | `size` | `'sm' \| 'md'` | `'md'` | 크기 |
121
- | `shape` | `'square' \| 'round'` | `'square'` | 형태 |
122
- | `checked` | `boolean` | - | 체크 상태 |
123
- | `indeterminate` | `boolean` | `false` | 불확정 상태 (`aria-checked="mixed"`) |
124
- | `onCheckedChange` | `(checked: boolean) => void` | - | 체크 변경 콜백 |
125
- | `label` | `ReactNode` | - | 라벨 텍스트 |
199
+ | `size` | `'sm'` \| `'md'` | `"md"` | Size |
200
+ | `shape` | `'square'` \| `'round'` | `"square"` | Shape |
201
+ | `checked` | `boolean` | - | Checked state |
202
+ | `indeterminate` | `boolean` | - | Indeterminate state (aria-checked="mixed") |
203
+ | `disabled` | `boolean` | - | Disabled |
204
+ | `readOnly` | `boolean` | - | Read-only |
205
+ | `label` | `ReactNode` | - | Label text (ReactNode) |
206
+ | `children` | `ReactNode` | - | Label alternative content (ReactNode) |
207
+ | `name` | `string` | - | Form field name |
208
+ | `id` | `string` | - | Element ID |
209
+ | `value` | `string` | - | Value for form submission |
210
+ | `onCheckedChange` | `ReactNode` | - | Checked state change callback (checked: boolean) => void |
211
+ | `onChange` | `ReactNode` | - | Native change event handler |
212
+ | `className` | `string` | - | Style override |
126
213
 
127
214
  ```tsx
128
215
  <CheckBox
129
216
  checked={agreed}
130
217
  onCheckedChange={setAgreed}
131
- label="이용약관에 동의합니다"
218
+ label="I agree to the terms of service"
132
219
  />
133
220
 
134
221
  <CheckBox shape="round" size="sm" checked indeterminate />
@@ -136,23 +223,36 @@ alwaysApply: false
136
223
 
137
224
  ---
138
225
 
139
- ## RadioGroup / RadioItem
226
+ ## RadioGroup
140
227
 
141
- 라디오 그룹. 네이티브 `<input type="radio">` 기반.
228
+ Radio group. Used with RadioItem.
142
229
 
143
- | RadioGroup Prop | Type | Default | Description |
230
+ | Prop | Type | Default | Description |
144
231
  |---|---|---|---|
145
- | `name` | `string` (필수) | - | form name |
146
- | `value` | `string` | - | 선택된 |
147
- | `onValueChange` | `(value: string) => void` | - | 변경 콜백 |
148
- | `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | 배치 방향 |
149
- | `size` | `'sm' \| 'md'` | `'md'` | 크기 |
150
- | `aria-label` | `string` | - | 접근성 라벨 |
232
+ | `name` | `string` | - | Form name (required) |
233
+ | `value` | `string` | - | Selected value (controlled) |
234
+ | `defaultValue` | `string` | - | Initial value (uncontrolled) |
235
+ | `size` | `'sm'` \| `'md'` | `"md"` | Size |
236
+ | `orientation` | `'horizontal'` \| `'vertical'` | `"vertical"` | Layout direction |
237
+ | `disabled` | `boolean` | - | Disabled |
238
+ | `children` | `ReactNode` | - | RadioItem list (ReactNode, required) |
239
+ | `aria-label` | `string` | - | Accessibility label |
240
+ | `aria-labelledby` | `string` | - | Accessibility label reference ID |
241
+ | `onValueChange` | `ReactNode` | - | Value change callback (value: string) => void |
242
+ | `className` | `string` | - | Style override |
243
+
244
+ ### RadioItem
245
+
246
+ Individual option within RadioGroup.
151
247
 
152
- | RadioItem Prop | Type | Description |
153
- |---|---|---|
154
- | `value` | `string` (필수) | 항목 |
155
- | `label` | `ReactNode` | 라벨 텍스트 |
248
+ | Prop | Type | Default | Description |
249
+ |---|---|---|---|
250
+ | `value` | `string` | - | Item value (required) |
251
+ | `size` | `'sm'` \| `'md'` | - | Size (overrides group) |
252
+ | `label` | `ReactNode` | - | Label text (ReactNode) |
253
+ | `children` | `ReactNode` | - | Label alternative content (ReactNode) |
254
+ | `disabled` | `boolean` | - | Disabled |
255
+ | `className` | `string` | - | Style override |
156
256
 
157
257
  ```tsx
158
258
  <RadioGroup
@@ -160,11 +260,11 @@ alwaysApply: false
160
260
  value={plan}
161
261
  onValueChange={setPlan}
162
262
  orientation="horizontal"
163
- aria-label="요금제 선택"
263
+ aria-label="Select plan"
164
264
  >
165
- <RadioItem value="free" label="무료" />
166
- <RadioItem value="pro" label="프로" />
167
- <RadioItem value="enterprise" label="엔터프라이즈" />
265
+ <RadioItem value="free" label="Free" />
266
+ <RadioItem value="pro" label="Pro" />
267
+ <RadioItem value="enterprise" label="Enterprise" />
168
268
  </RadioGroup>
169
269
  ```
170
270
 
@@ -172,14 +272,19 @@ alwaysApply: false
172
272
 
173
273
  ## Switch
174
274
 
175
- 토글 스위치. 네이티브 checkbox 기반, `role="switch"`.
275
+ Toggle switch. Native checkbox-based, role="switch".
176
276
 
177
277
  | Prop | Type | Default | Description |
178
278
  |---|---|---|---|
179
- | `size` | `'sm' \| 'md'` | `'md'` | 크기 |
180
- | `checked` | `boolean` | - | on/off 상태 |
181
- | `onCheckedChange` | `(checked: boolean) => void` | - | 변경 콜백 |
182
- | `disabled` | `boolean` | - | 비활성 |
279
+ | `size` | `'sm'` \| `'md'` | `"md"` | Size |
280
+ | `checked` | `boolean` | - | On/off state |
281
+ | `disabled` | `boolean` | - | Disabled |
282
+ | `readOnly` | `boolean` | - | Read-only |
283
+ | `name` | `string` | - | Form field name |
284
+ | `id` | `string` | - | Element ID |
285
+ | `onCheckedChange` | `ReactNode` | - | Toggle state change callback (checked: boolean) => void |
286
+ | `onChange` | `ReactNode` | - | Native change event handler (ChangeEvent) |
287
+ | `className` | `string` | - | Style override |
183
288
 
184
289
  ```tsx
185
290
  <Switch checked={darkMode} onCheckedChange={setDarkMode} />
@@ -189,24 +294,27 @@ alwaysApply: false
189
294
 
190
295
  ## Chip
191
296
 
192
- 칩/태그/뱃지. `asChild`로 렌더링 요소 변경 가능.
297
+ Chip/tag/badge. Close button displayed via onClose prop.
193
298
 
194
299
  | Prop | Type | Default | Description |
195
300
  |---|---|---|---|
196
- | `variant` | `'default' \| 'filled' \| 'outline' \| 'accent'` | `'default'` | 스타일 |
197
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 크기 |
198
- | `asChild` | `boolean` | `false` | 자식 요소로 렌더링 |
199
- | `disabled` | `boolean` | - | 비활성 (`aria-disabled` 자동) |
200
- | `onClose` | `(e: MouseEvent) => void` | - | 닫기 버튼 표시 및 콜백 |
301
+ | `variant` | `'default'` \| `'filled'` \| `'outline'` \| `'accent'` | `"default"` | Style |
302
+ | `size` | `'sm'` \| `'md'` \| `'lg'` | `"md"` | Size |
303
+ | `asChild` | `boolean` | - | If true, renders as child element (Slot pattern) |
304
+ | `disabled` | `boolean` | - | Disabled (auto aria-disabled) |
305
+ | `children` | `ReactNode` | - | Chip content (ReactNode) |
306
+ | `onClose` | `ReactNode` | - | Close button click callback (e: MouseEvent) => void. Shows X button when provided |
307
+ | `onClick` | `ReactNode` | - | Click event handler |
308
+ | `className` | `string` | - | Style override |
201
309
 
202
310
  ```tsx
203
311
  <Chip variant="accent" size="sm">New</Chip>
204
312
 
205
313
  <Chip onClose={() => removeTag(id)}>React</Chip>
206
314
 
207
- // <li>로 렌더링
315
+ // Render as <li>
208
316
  <Chip asChild variant="filled">
209
- <li>리스트 칩</li>
317
+ <li>List chip</li>
210
318
  </Chip>
211
319
  ```
212
320
 
@@ -214,31 +322,73 @@ alwaysApply: false
214
322
 
215
323
  ## Spinner
216
324
 
217
- 로딩 인디케이터. SVG 기반. `role="status"` 내장.
325
+ Loading indicator. SVG-based. Built-in role="status".
218
326
 
219
327
  | Prop | Type | Default | Description |
220
328
  |---|---|---|---|
221
- | `size` | `number` | `20` | px 크기 |
222
- | `className` | `string` | - | 색상 오버라이드 |
223
- | `aria-label` | `string` | `'Loading'` | 접근성 라벨 |
329
+ | `size` | `number` | `20` | Size in px |
330
+ | `color` | `string` | - | Color (CSS color value, default currentColor) |
331
+ | `aria-label` | `string` | `"Loading"` | Accessibility label |
332
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
333
+ | `className` | `string` | - | Color override etc. |
224
334
 
225
335
  ```tsx
226
336
  <Spinner size={24} />
227
337
 
228
- <Spinner size={14} className="text-white" aria-label="로딩 중" />
338
+ <Spinner size={14} className="text-white" aria-label="Loading" />
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Skeleton
344
+
345
+ Skeleton loading placeholder. Size/shape via className. With children, wraps transparently to maintain actual size.
346
+
347
+ | Prop | Type | Default | Description |
348
+ |---|---|---|---|
349
+ | `as` | `'div'` \| `'span'` | `"div"` | Rendered tag |
350
+ | `circle` | `boolean` | `false` | Circle skeleton (rounded-full) |
351
+ | `width` | `string` \| `number` | - | Width (e.g. '100px', '50%', 200) |
352
+ | `height` | `string` \| `number` | - | Height (e.g. '16px', 40) |
353
+ | `children` | `ReactNode` | - | Inner content (shown when loaded, maintains actual size) |
354
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
355
+ | `className` | `string` | - | Style override |
356
+
357
+ ```tsx
358
+ // Basic usage (size via className)
359
+ <Skeleton className="h-4 w-48" />
360
+ <Skeleton className="h-3 w-32" />
361
+
362
+ // Circular avatar skeleton
363
+ <Skeleton circle width={40} height={40} />
364
+
365
+ // width/height props
366
+ <Skeleton width="100%" height={120} className="rounded-lg" />
367
+
368
+ // Match children size
369
+ <Skeleton>
370
+ <p>Skeleton will match this text size</p>
371
+ </Skeleton>
372
+
373
+ // With DataList
374
+ <DataList list={data} skeletonElement={<MySkeleton />} skeletonCount={5}>
375
+ {({ item }) => <Card key={item.id} {...item} />}
376
+ </DataList>
229
377
  ```
230
378
 
231
379
  ---
232
380
 
233
381
  ## Divider
234
382
 
235
- 구분선. 수평/수직 방향, 실선/점선/파선 지원.
383
+ Divider. Supports horizontal/vertical, solid/dashed/dotted.
236
384
 
237
385
  | Prop | Type | Default | Description |
238
386
  |---|---|---|---|
239
- | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | 방향 |
240
- | `variant` | `'solid' \| 'dashed' \| 'dotted'` | `'solid'` | 스타일 |
241
- | `color` | `string` | - | 커스텀 색상 (CSS ) |
387
+ | `orientation` | `'horizontal'` \| `'vertical'` | `"horizontal"` | Direction |
388
+ | `variant` | `'solid'` \| `'dashed'` \| `'dotted'` | `"solid"` | Line style |
389
+ | `color` | `string` | - | Custom color (CSS value) |
390
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
391
+ | `className` | `string` | - | Style override |
242
392
 
243
393
  ```tsx
244
394
  <Divider />
@@ -249,19 +399,22 @@ alwaysApply: false
249
399
 
250
400
  ## Tooltip
251
401
 
252
- 툴팁. Radix Tooltip 기반. 단독 사용 가능 (Provider 내장).
402
+ Tooltip. Based on Radix Tooltip. Built-in Provider.
253
403
 
254
404
  | Prop | Type | Default | Description |
255
405
  |---|---|---|---|
256
- | `content` | `ReactNode` | - | 툴팁 내용 |
257
- | `variant` | `'dark' \| 'light'` | `'dark'` | 스타일 |
258
- | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'top'` | 위치 |
259
- | `align` | `'start' \| 'center' \| 'end'` | `'center'` | 정렬 |
260
- | `delayDuration` | `number` | `200` | 표시 지연 (ms) |
261
- | `disabled` | `boolean` | `false` | 비활성 |
406
+ | `children` | `ReactNode` | - | Trigger element (ReactNode, required) |
407
+ | `content` | `ReactNode` | - | Tooltip content (ReactNode, required) |
408
+ | `variant` | `'dark'` \| `'light'` | `"dark"` | Style |
409
+ | `side` | `'top'` \| `'right'` \| `'bottom'` \| `'left'` | `"top"` | Position |
410
+ | `align` | `'start'` \| `'center'` \| `'end'` | `"center"` | Alignment |
411
+ | `delayDuration` | `number` | `200` | Show delay (ms) |
412
+ | `disabled` | `boolean` | `false` | Disabled |
413
+ | `className` | `string` | - | Content style |
414
+ | `triggerClassName` | `string` | - | Trigger style |
262
415
 
263
416
  ```tsx
264
- <Tooltip content="복사됨!" side="bottom">
417
+ <Tooltip content="Copied!" side="bottom">
265
418
  <button>📋</button>
266
419
  </Tooltip>
267
420
  ```
@@ -270,21 +423,25 @@ alwaysApply: false
270
423
 
271
424
  ## Popover
272
425
 
273
- 팝오버. Radix Popover 기반. 컴포저블 패턴과 단일 패턴 모두 지원.
426
+ Popover. Based on Radix Popover.
274
427
 
275
428
  | Prop | Type | Default | Description |
276
429
  |---|---|---|---|
277
- | `trigger` | `ReactNode` | - | 트리거 요소 |
278
- | `children` | `ReactNode` | - | 팝오버 내용 |
279
- | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | 위치 |
280
- | `align` | `'start' \| 'center' \| 'end'` | `'center'` | 정렬 |
281
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 너비 |
282
- | `open` | `boolean` | - | 제어 모드 |
283
- | `onOpenChange` | `(open: boolean) => void` | - | 열림/닫힘 콜백 |
430
+ | `trigger` | `ReactNode` | - | Trigger element (ReactNode, required) |
431
+ | `side` | `'top'` \| `'right'` \| `'bottom'` \| `'left'` | `"bottom"` | Position |
432
+ | `align` | `'start'` \| `'center'` \| `'end'` | `"center"` | Alignment |
433
+ | `sideOffset` | `number` | `4` | Position offset (px) |
434
+ | `alignOffset` | `number` | - | Alignment offset (px) |
435
+ | `open` | `boolean` | - | Controlled mode |
436
+ | `onOpenChange` | `ReactNode` | - | Open/close state change callback (open: boolean) => void |
437
+ | `onClickTrigger` | `ReactNode` | - | Callback on trigger element click. Passed to PopoverPrimitive.Trigger onClick. |
438
+ | `children` | `ReactNode` | - | Popover body (ReactNode) |
439
+ | `className` | `string` | - | Content style |
440
+ | `arrowClassName` | `string` | - | Arrow style |
284
441
 
285
442
  ```tsx
286
- <Popover trigger={<Button variant="outline">메뉴</Button>}>
287
- <div className="p-4">팝오버 내용</div>
443
+ <Popover trigger={<Button variant="outline">Menu</Button>}>
444
+ <div className="p-4">Popover content</div>
288
445
  </Popover>
289
446
  ```
290
447
 
@@ -292,27 +449,29 @@ alwaysApply: false
292
449
 
293
450
  ## Accordion
294
451
 
295
- 아코디언. 단순 `items` 배열 방식과 컴포저블 방식 모두 지원.
452
+ Accordion. Supports both items array and composable patterns.
296
453
 
297
454
  | Prop | Type | Default | Description |
298
455
  |---|---|---|---|
299
- | `items` | `AccordionItemData[]` | - | 항목 배열 |
300
- | `type` | `'single' \| 'multiple'` | `'single'` | 단일/다중 열기 |
301
- | `collapsible` | `boolean` | `true` | 전부 접기 가능 |
302
- | `value` / `defaultValue` | `string \| string[]` | - | 제어/비제어 |
303
- | `onValueChange` | `(value) => void` | - | 변경 콜백 |
456
+ | `items` | `object`[] | - | Accordion item array (required) |
457
+ | `type` | `'single'` \| `'multiple'` | `"single"` | Single/multiple open mode |
458
+ | `collapsible` | `boolean` | `true` | Allow collapsing all |
459
+ | `value` | `string` \| `string`[] | - | Controlled mode |
460
+ | `defaultValue` | `string` \| `string`[] | - | Uncontrolled initial value |
461
+ | `onValueChange` | `ReactNode` | - | Open item change callback (value: string | string[]) => void |
462
+ | `className` | `string` | - | Root style |
304
463
 
305
464
  ```tsx
306
465
  <Accordion items={[
307
- { id: '1', trigger: 'FAQ 1', content: '답변 1' },
308
- { id: '2', trigger: 'FAQ 2', content: '답변 2' },
466
+ { id: '1', trigger: 'FAQ 1', content: 'Answer 1' },
467
+ { id: '2', trigger: 'FAQ 2', content: 'Answer 2' },
309
468
  ]} />
310
469
 
311
- // 컴포저블 방식
470
+ // Composable pattern
312
471
  <AccordionRoot type="single" collapsible>
313
472
  <AccordionItem value="item-1">
314
- <AccordionTrigger>제목</AccordionTrigger>
315
- <AccordionContent>내용</AccordionContent>
473
+ <AccordionTrigger>Title</AccordionTrigger>
474
+ <AccordionContent>Content</AccordionContent>
316
475
  </AccordionItem>
317
476
  </AccordionRoot>
318
477
  ```
@@ -321,24 +480,41 @@ alwaysApply: false
321
480
 
322
481
  ## Drawer
323
482
 
324
- 드로어/바텀시트. Vaul 기반. 4방향 지원.
483
+ Drawer/bottom sheet. Based on Vaul. Compound component pattern.
484
+
485
+ | Prop | Type | Default | Description |
486
+ |---|---|---|---|
487
+ | `direction` | `'bottom'` \| `'top'` \| `'left'` \| `'right'` | `"bottom"` | Direction |
488
+ | `open` | `boolean` | - | Open state (controlled mode) |
489
+ | `onOpenChange` | `ReactNode` | - | Open state change callback (open: boolean) => void |
490
+ | `dismissible` | `boolean` | - | Allow close via swipe/outside click (default true) |
491
+ | `modal` | `boolean` | - | Modal mode (default true). If false, background is interactive |
492
+ | `shouldScaleBackground` | `boolean` | - | Background scale effect (default false) |
493
+ | `children` | `ReactNode` | - | Drawer sub-components (ReactNode, required) |
494
+
495
+ ### DrawerContent
496
+
497
+ Drawer.Content area.
325
498
 
326
499
  | Prop | Type | Default | Description |
327
500
  |---|---|---|---|
328
- | `direction` | `'bottom' \| 'top' \| 'left' \| 'right'` | `'bottom'` | 방향 |
329
- | `showHandle` | `boolean` | `true` | 핸들 표시 (top/bottom만) |
330
- | `blur` | `'none' \| 'sm' \| 'md'` | `'none'` | 오버레이 블러 |
501
+ | `direction` | `'bottom'` \| `'top'` \| `'left'` \| `'right'` | `"bottom"` | Direction (Context takes priority) |
502
+ | `blur` | `'none'` \| `'sm'` \| `'md'` | `"none"` | Overlay blur |
503
+ | `showHandle` | `boolean` | `true` | Show handle bar |
504
+ | `children` | `ReactNode` | - | Content area (ReactNode) |
505
+ | `overlayClassName` | `string` | - | Overlay style |
506
+ | `className` | `string` | - | Panel style |
331
507
 
332
508
  ```tsx
333
509
  <Drawer direction="bottom">
334
510
  <Drawer.Trigger asChild>
335
- <Button>열기</Button>
511
+ <Button>Open</Button>
336
512
  </Drawer.Trigger>
337
513
  <Drawer.Content>
338
- <Drawer.Title>제목</Drawer.Title>
339
- <Drawer.Description>설명</Drawer.Description>
514
+ <Drawer.Title>Title</Drawer.Title>
515
+ <Drawer.Description>Description</Drawer.Description>
340
516
  <Drawer.Close asChild>
341
- <Button variant="ghost">닫기</Button>
517
+ <Button variant="ghost">Close</Button>
342
518
  </Drawer.Close>
343
519
  </Drawer.Content>
344
520
  </Drawer>
@@ -346,353 +522,520 @@ alwaysApply: false
346
522
 
347
523
  ---
348
524
 
349
- ## Modal (함수형 API)
525
+ ## Modal
350
526
 
351
- 명령형 모달. 함수 호출로 열고 닫는다.
527
+ Modal template. All modal components must be wrapped with ModalTemplate.
352
528
 
353
- **중요: 모달 컴포넌트는 반드시 `ModalTemplate`으로 감싸야 한다.**
529
+ | Prop | Type | Default | Description |
530
+ |---|---|---|---|
531
+ | `title` | `ReactNode` | - | Header title (ReactNode) |
532
+ | `desc` | `ReactNode` | - | Header description (ReactNode) |
533
+ | `layout` | `'default'` \| `'bottom-sheet'` \| `'slide-left'` \| `'slide-right'` \| `'full-page'` \| `'full-page-reverse'` \| `'draggable'` | `"default"` | Layout |
534
+ | `showDim` | `boolean` | `true` | Show dim overlay |
535
+ | `dimClose` | `boolean` | `true` | Close on dim click |
536
+ | `hideHeader` | `boolean` | `false` | Hide header |
537
+ | `hideFooter` | `boolean` | `true` | Hide footer |
538
+ | `footer` | `ReactNode` | - | Custom footer (ReactElement) |
539
+ | `animation` | `object` | - | Modal animation |
540
+ | `enableDrag` | `boolean` | `true` | Enable drag (bottom-sheet/draggable layouts) |
541
+ | `dragPersistKey` | `string` | - | Drag position persistence key |
542
+ | `close` | `ReactNode` | - | Modal close function (isAnimation?: boolean) => void (auto-injected) |
543
+ | `children` | `ReactNode` | - | Modal body (ReactNode, required) |
544
+ | `className` | `string` | - | Root wrapper style |
545
+ | `innerClassName` | `string` | - | Modal body style |
546
+ | `bodyClassName` | `string` | - | Body area style |
547
+ | `footerClassName` | `string` | - | Footer area style |
548
+ | `dimClassName` | `string` | - | Dim overlay style |
549
+ | `headerClassName` | `string` | - | Header area style |
550
+
551
+ ### modal()
552
+
553
+ modal() function call options. component automatically receives close/resolve as props.
354
554
 
355
- ### 모달 컴포넌트 작성법
555
+ | Prop | Type | Default | Description |
556
+ |---|---|---|---|
557
+ | `component` | `ReactNode` | - | Modal component (required). Automatically receives close/resolve as props |
558
+ | `props` | `Record<string, any>` | - | Props to pass to component |
559
+ | `id` | `string` | - | Modal ID (used for duplicate check) |
560
+ | `layout` | `'default'` \| `'bottom-sheet'` \| `'slide-left'` \| `'slide-right'` \| `'full-page'` \| `'full-page-reverse'` \| `'draggable'` | - | Layout |
561
+ | `animation` | `object` | - | Modal animation |
562
+ | `scrollEnable` | `boolean` | - | Allow background scroll |
563
+ | `isToggle` | `boolean` | - | Toggle mode (close on re-call of same modal) |
564
+ | `isAlone` | `boolean` | - | Alone mode (close all existing modals before opening) |
565
+ | `duplicateCheck` | `boolean` | - | Prevent duplicate opening of same component |
566
+ | `disableEscapeKeyPress` | `boolean` | - | Disable close via ESC key |
567
+ | `componentName` | `string` | - | Modal identifier name (used for duplicate check, modal search) |
568
+ | `onOpen` | `ReactNode` | - | Callback when modal opens |
569
+ | `onClose` | `ReactNode` | - | Callback when modal closes |
570
+
571
+ **IMPORTANT: Modal components MUST be wrapped with `ModalTemplate`.**
356
572
 
357
573
  ```tsx
358
574
  import { ModalTemplate } from '@nexus-cross/design-system/modal';
359
575
 
360
- // 모달 컴포넌트는 반드시 close와 resolve를 props로 받는다
361
576
  function MyModal({ close, resolve }: { close: () => void; resolve: (value: any) => void }) {
362
577
  return (
363
578
  <ModalTemplate
364
- title="모달 제목"
365
- desc="모달 설명 (선택)"
579
+ title="Modal Title"
580
+ desc="Modal description (optional)"
366
581
  close={close}
367
- layout="default" // 'default' | 'bottom-sheet' | 'slide-left' | 'slide-right' | 'full-page' | 'draggable'
368
- hideFooter // footer 없으면 하단 패딩 자동
582
+ layout="default"
583
+ hideFooter
369
584
  >
370
585
  <div className="space-y-4">
371
- <p className="text-text-secondary">모달 내용</p>
372
- <Button onClick={() => resolve({ confirmed: true })}>확인</Button>
586
+ <p className="text-text-secondary">Modal content</p>
587
+ <Button onClick={() => resolve({ confirmed: true })}>Confirm</Button>
373
588
  </div>
374
589
  </ModalTemplate>
375
590
  );
376
591
  }
377
592
  ```
378
593
 
379
- ### ModalTemplate Props
380
-
381
- | Prop | Type | Default | Description |
382
- |---|---|---|---|
383
- | `title` | `ReactNode` | - | 헤더 제목 |
384
- | `desc` | `ReactNode` | - | 헤더 설명 |
385
- | `close` | `() => void` (필수) | - | 닫기 함수 (props로 자동 주입됨) |
386
- | `layout` | `'default' \| 'bottom-sheet' \| 'slide-left' \| 'slide-right' \| 'full-page' \| 'full-page-reverse' \| 'draggable'` | `'default'` | 레이아웃 |
387
- | `showDim` | `boolean` | `true` | 딤 배경 표시 |
388
- | `dimClose` | `boolean` | `true` | 딤 클릭 시 닫기 |
389
- | `hideHeader` | `boolean` | `false` | 헤더 숨김 |
390
- | `hideFooter` | `boolean` | `true` | 푸터 숨김 |
391
- | `footer` | `ReactElement` | - | 커스텀 푸터 |
392
- | `enableDrag` | `boolean` | `false` | 드래그 활성화 (draggable 레이아웃) |
393
- | `dragPersistKey` | `string` | - | 드래그 위치 저장 키 |
394
- | `innerClassName` | `string` | - | 모달 본체 스타일 오버라이드 |
395
- | `bodyClassName` | `string` | - | 바디 영역 스타일 오버라이드 |
396
-
397
- ### 모달 호출
594
+ ### Calling a Modal
398
595
 
399
596
  ```tsx
400
597
  import { modal, useModal, ModalContainer } from '@nexus-cross/design-system/modal';
401
598
 
402
- // 루트에 ModalContainer 배치 필수
599
+ // ModalContainer MUST be placed at the app root
403
600
  <ModalContainer />
404
601
 
405
- // 방법 1: modal() 함수
602
+ // Method 1: modal() function
406
603
  const result = await modal({
407
604
  component: MyModal,
408
- props: { /* 추가 props */ },
605
+ props: { /* additional props */ },
409
606
  });
410
607
 
411
- // 방법 2: useModal()
608
+ // Method 2: useModal() hook
412
609
  const { modal: openModal } = useModal();
413
610
  openModal({
414
611
  component: MyModal,
415
- options: { isAlone: true }, // 단독 모달 (다른 모달 위에 안 쌓임)
612
+ options: { isAlone: true },
416
613
  });
417
614
  ```
418
615
 
419
- ### 금지 사항
616
+ ### Prohibited
420
617
 
421
- - ModalTemplate 없이 `<div>`만으로 모달 컴포넌트를 만들지 않는다
422
- - `close` prop 직접 정의하지 않는다 (시스템이 자동 주입)
423
- - 모달 내에서 별도 dim/overlay 구현하지 않는다
618
+ - Do NOT create modal components without ModalTemplate (using plain `<div>`)
619
+ - Do NOT define the `close` prop manually (the system injects it automatically)
620
+ - Do NOT implement a separate dim/overlay inside the modal
424
621
 
425
622
  ---
426
623
 
427
- ## Toast (함수형 API)
428
-
429
- 토스트 알림. Sonner 기반.
624
+ ## Tab
430
625
 
431
- ```tsx
432
- import { toast, Toaster } from '@nexus-cross/design-system';
626
+ Tab navigation. line/pill variants.
433
627
 
434
- // 루트에 Toaster 배치
435
- <Toaster position="top-right" />
628
+ | Prop | Type | Default | Description |
629
+ |---|---|---|---|
630
+ | `items` | `object`[] | - | Tab item array (required) |
631
+ | `activeKey` | `string` | - | Controlled mode active key |
632
+ | `defaultActiveKey` | `string` | - | Uncontrolled initial key |
633
+ | `variant` | `'line'` \| `'pill'` | `"line"` | Style |
634
+ | `size` | `'sm'` \| `'md'` | `"md"` | Size |
635
+ | `destroyInactive` | `boolean` | `false` | Unmount inactive panels |
636
+ | `onTabChange` | `ReactNode` | - | Tab change callback (key: string) => void |
637
+ | `className` | `string` | - | Root style |
638
+ | `tabListClassName` | `string` | - | Tab list style |
639
+ | `tabPanelClassName` | `string` | - | Tab panel style |
436
640
 
437
- // 사용
438
- toast('저장되었습니다');
439
- toast.success('성공!');
440
- toast.error('오류가 발생했습니다');
441
- toast.loading('처리 중...');
641
+ ```tsx
642
+ <Tab
643
+ items={[
644
+ { key: 'a', label: 'Tab A', children: <p>A</p> },
645
+ { key: 'b', label: 'Tab B', children: <p>B</p> },
646
+ ]}
647
+ defaultActiveKey="a"
648
+ variant="pill"
649
+ />
442
650
  ```
443
651
 
444
652
  ---
445
653
 
446
- ## InfiniteScroll
654
+ ## Carousel
447
655
 
448
- 무한 스크롤. IntersectionObserver 기반.
656
+ Carousel. Based on Embla Carousel. Sub-components: CarouselSlide, CarouselPrev, CarouselNext, CarouselDots.
449
657
 
450
- | Prop | Type | Description |
451
- |---|---|---|
452
- | `list` | `unknown[]` | 현재 데이터 배열 |
453
- | `totalCount` | `number` | 전체 개수 (또는 `hasMore` 사용) |
454
- | `hasMore` | `boolean` | 있는지 (또는 `totalCount` 사용) |
455
- | `handleLoadMore` | `() => void` | 추가 로드 콜백 |
456
- | `loading` | `boolean` | 로딩 상태 |
658
+ | Prop | Type | Default | Description |
659
+ |---|---|---|---|
660
+ | `opts` | `Record<string, any>` | - | Embla options (loop, align, etc.) |
661
+ | `plugins` | `ReactNode`[] | - | Embla plugins |
662
+ | `onApiChange` | `ReactNode` | - | Embla API change callback (api: CarouselApi) => void |
663
+ | `children` | `ReactNode` | - | Carousel slides and sub-components (ReactNode) |
664
+ | `className` | `string` | - | Style override |
665
+
666
+ Sub-components: `CarouselSlide`, `CarouselPrev`, `CarouselNext`, `CarouselDots`
457
667
 
458
668
  ```tsx
459
- <InfiniteScroll
460
- list={items}
461
- totalCount={100}
462
- loading={isLoading}
463
- handleLoadMore={fetchMore}
464
- >
465
- {items.map(item => <Card key={item.id} {...item} />)}
466
- </InfiniteScroll>
669
+ <Carousel opts={{ loop: true }}>
670
+ <CarouselSlide>Slide 1</CarouselSlide>
671
+ <CarouselSlide>Slide 2</CarouselSlide>
672
+ <CarouselPrev />
673
+ <CarouselNext />
674
+ <CarouselDots />
675
+ </Carousel>
467
676
  ```
468
677
 
469
678
  ---
470
679
 
471
- ## Ellipsis
680
+ ## Pagination
472
681
 
473
- 텍스트 말줄임. 더보기/접기 토글 내장.
682
+ Pagination. Previous/next + page number buttons.
474
683
 
475
684
  | Prop | Type | Default | Description |
476
685
  |---|---|---|---|
477
- | `content` | `ReactNode` | - | 내용 |
478
- | `lineClamp` | `number` | `2` | 제한 |
479
- | `triggerMore` | `ReactNode` | `'more'` | 더보기 텍스트 |
480
- | `triggerLess` | `ReactNode` | `'less'` | 접기 텍스트 |
686
+ | `currentPage` | `number` | - | Current page (1-based, required) |
687
+ | `totalPages` | `number` | - | Total page count (required) |
688
+ | `siblingCount` | `number` | `1` | Number of pages shown on each side of current |
689
+ | `showEdges` | `boolean` | - | Always show first/last page |
690
+ | `size` | `'sm'` \| `'md'` | `"md"` | Size |
691
+ | `onPageChange` | `ReactNode` | - | Page change callback (page: number) => void, required |
692
+ | `className` | `string` | - | <nav> style |
481
693
 
482
694
  ```tsx
483
- <Ellipsis content={longText} lineClamp={3} triggerMore="더보기" triggerLess="접기" />
695
+ <Pagination currentPage={2} totalPages={10} onPageChange={setPage} />
484
696
  ```
485
697
 
486
698
  ---
487
699
 
488
- ## Hooks
489
-
490
- ### useModal
700
+ ## Avatar
491
701
 
492
- ```tsx
493
- const { open, close } = useModal();
494
- open(MyComponent, { title: '제목' });
495
- ```
702
+ Avatar. Supports image, fallback text, and children.
496
703
 
497
- ### useInView
704
+ | Prop | Type | Default | Description |
705
+ |---|---|---|---|
706
+ | `src` | `string` | - | Image URL |
707
+ | `alt` | `string` | - | Alt text |
708
+ | `fallback` | `ReactNode` | - | Displayed on image load failure (ReactNode) |
709
+ | `size` | `'xs'` \| `'sm'` \| `'md'` \| `'lg'` \| `'xl'` | `"md"` | Size |
710
+ | `shape` | `'circle'` \| `'square'` | `"circle"` | Shape |
711
+ | `children` | `ReactNode` | - | Custom image element (e.g. Next.js Image) |
712
+ | `onImageError` | `ReactNode` | - | Image load error callback () => void |
713
+ | `className` | `string` | - | Style override |
498
714
 
499
715
  ```tsx
500
- const { ref, inView } = useInView({ threshold: 0.5 });
501
- <div ref={ref}>{inView && <LazyContent />}</div>
716
+ <Avatar src="/user.png" alt="User" size="lg" />
717
+ <Avatar fallback="JD" shape="square" size="sm" />
502
718
  ```
503
719
 
504
- ### useCheckDevice
720
+ ---
505
721
 
506
- ```tsx
507
- const { isMobile, isTablet, isDesktop } = useCheckDevice();
508
- ```
722
+ ## Counter
509
723
 
510
- ### useClickOutside
724
+ Number count animation.
725
+
726
+ | Prop | Type | Default | Description |
727
+ |---|---|---|---|
728
+ | `endValue` | `number` | - | Target value (required) |
729
+ | `startValue` | `number` | `0` | Start value |
730
+ | `duration` | `number` | `1500` | Animation duration (ms) |
731
+ | `delay` | `number` | `0` | Start delay (ms) |
732
+ | `separator` | `boolean` | `true` | Thousands separator |
733
+ | `digits` | `number` | `0` | Decimal places |
734
+ | `triggerOnView` | `boolean` | `false` | Start on viewport entry |
735
+ | `onEnd` | `ReactNode` | - | Count complete callback () => void |
736
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
737
+ | `className` | `string` | - | Style override |
511
738
 
512
739
  ```tsx
513
- const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
514
- <div ref={ref}>드롭다운 내용</div>
740
+ <Counter endValue={1234} duration={2000} separator />
515
741
  ```
516
742
 
517
743
  ---
518
744
 
519
- ## Pagination
745
+ ## Countdown
520
746
 
521
- 페이지네이션. 이전/다음 + 번호 버튼.
747
+ Countdown timer.
522
748
 
523
749
  | Prop | Type | Default | Description |
524
750
  |---|---|---|---|
525
- | `currentPage` | `number` | - | 현재 페이지 (1부터) |
526
- | `totalPages` | `number` | - | 전체 페이지 |
527
- | `siblingCount` | `number` | `1` | 현재 페이지 양옆 표시 개수 |
528
- | `onPageChange` | `(page: number) => void` | - | 페이지 변경 콜백 |
529
- | `size` | `'sm' \| 'md'` | `'md'` | 크기 |
751
+ | `endTimestamp` | `number` | - | End time (Unix ms, required) |
752
+ | `separator` | `ReactNode` | `":"` | Separator (ReactNode) |
753
+ | `showDays` | `boolean` | `true` | Show days unit |
754
+ | `labels` | `object` | - | Unit labels |
755
+ | `render` | `ReactNode` | - | Custom render function |
756
+ | `onEnd` | `ReactNode` | - | Countdown end callback () => void |
757
+ | `className` | `string` | - | Style override |
530
758
 
531
759
  ```tsx
532
- <Pagination currentPage={2} totalPages={10} onPageChange={setPage} />
760
+ <Countdown endTimestamp={Date.now() + 60_000} showDays={false} onEnd={handleEnd} />
533
761
  ```
534
762
 
535
763
  ---
536
764
 
537
- ## Avatar
765
+ ## Marquee
538
766
 
539
- 아바타. 이미지, 폴백 텍스트, children 지원.
767
+ Marquee (scrolling text/elements).
540
768
 
541
769
  | Prop | Type | Default | Description |
542
770
  |---|---|---|---|
543
- | `src` | `string` | - | 이미지 URL |
544
- | `alt` | `string` | - | 대체 텍스트 |
545
- | `fallback` | `ReactNode` | - | 이미지 로드 실패 시 표시 |
546
- | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | 크기 |
547
- | `shape` | `'circle' \| 'square'` | `'circle'` | 형태 |
548
- | `onImageError` | `() => void` | - | 이미지 에러 콜백 |
771
+ | `direction` | `'left'` \| `'right'` \| `'up'` \| `'down'` | `"left"` | Direction |
772
+ | `speed` | `number` | `40` | Animation speed (seconds) |
773
+ | `pauseOnHover` | `boolean` | `false` | Pause on hover |
774
+ | `gap` | `number` | `16` | Item gap (px) |
775
+ | `children` | `ReactNode` | - | Content to repeat (ReactNode, required) |
776
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
777
+ | `className` | `string` | - | Style override |
549
778
 
550
779
  ```tsx
551
- <Avatar src="/user.png" alt="User" size="lg" />
552
- <Avatar fallback="JD" shape="square" size="sm" />
780
+ <Marquee direction="left" speed={30} pauseOnHover>
781
+ <span>Scrolling text</span>
782
+ </Marquee>
553
783
  ```
554
784
 
555
785
  ---
556
786
 
557
- ## Counter
787
+ ## VirtualList
788
+
789
+ Virtual scroll list. Based on @tanstack/react-virtual.
790
+
791
+ | Prop | Type | Default | Description |
792
+ |---|---|---|---|
793
+ | `items` | `ReactNode`[] | - | Data array (required) |
794
+ | `estimateSize` | `number` \| `ReactNode` | - | Estimated item height (number or (index) => number, required) |
795
+ | `renderItem` | `ReactNode` | - | Item renderer (item, index, virtualItem) => ReactNode (required) |
796
+ | `overscan` | `number` | `5` | Overscan count |
797
+ | `gap` | `number` | `0` | Item gap (px) |
798
+ | `className` | `string` | - | Scroll container style |
799
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
800
+ | `endReachedThreshold` | `number` | `200` | End detection threshold (px) |
801
+ | `onEndReached` | `ReactNode` | - | End reached callback () => void |
802
+
803
+ ### VirtualGrid
558
804
 
559
- 숫자 카운트 애니메이션.
805
+ Virtual scroll grid. Based on @tanstack/react-virtual.
560
806
 
561
807
  | Prop | Type | Default | Description |
562
808
  |---|---|---|---|
563
- | `endValue` | `number` (필수) | - | 목표 |
564
- | `startValue` | `number` | `0` | 시작 |
565
- | `duration` | `number` | `1500` | 애니메이션 시간 (ms) |
566
- | `delay` | `number` | `0` | 시작 지연 (ms) |
567
- | `separator` | `boolean` | `true` | 단위 구분 |
568
- | `digits` | `number` | `0` | 소수 자릿수 |
569
- | `triggerOnView` | `boolean` | `false` | 뷰포트 진입 시작 |
570
- | `onEnd` | `() => void` | - | 완료 콜백 |
809
+ | `items` | `ReactNode`[] | - | Data array (required) |
810
+ | `columns` | `number` | - | Column count (required) |
811
+ | `estimateSize` | `number` \| `ReactNode` | - | Estimated item height (required) |
812
+ | `renderItem` | `ReactNode` | - | Item renderer (item, index) => ReactNode (required) |
813
+ | `overscan` | `number` | `3` | Overscan count |
814
+ | `gap` | `number` | `0` | Item gap (px) |
815
+ | `className` | `string` | - | Scroll container style |
816
+ | `style` | `ReactNode` | - | Inline style (CSSProperties) |
817
+ | `endReachedThreshold` | `number` | `200` | End detection threshold (px) |
818
+ | `onEndReached` | `ReactNode` | - | End reached callback () => void |
571
819
 
572
820
  ```tsx
573
- <Counter endValue={1234} duration={2000} separator />
821
+ <VirtualList
822
+ items={data}
823
+ estimateSize={48}
824
+ renderItem={(item) => <div>{item.name}</div>}
825
+ onEndReached={loadMore}
826
+ />
827
+ ```
828
+
829
+ ### VirtualGrid
830
+
831
+ Same as VirtualList + `columns: number` (required).
832
+
833
+ ```tsx
834
+ <VirtualGrid items={data} estimateSize={120} columns={3} renderItem={(item) => <Card {...item} />} />
574
835
  ```
575
836
 
576
837
  ---
577
838
 
578
- ## Countdown
839
+ ## DataList
579
840
 
580
- 카운트다운 타이머.
841
+ Data list. Automatically handles loading/skeleton/empty/data states based on list. Built-in ErrorBoundary.
581
842
 
582
843
  | Prop | Type | Default | Description |
583
844
  |---|---|---|---|
584
- | `endTimestamp` | `number` (필수) | - | 종료 시각 (Unix ms) |
585
- | `separator` | `ReactNode` | `':'` | 구분자 |
586
- | `showDays` | `boolean` | `true` | 단위 표시 |
587
- | `labels` | `{ days?, hours?, minutes?, seconds? }` | - | 단위 라벨 |
588
- | `onEnd` | `() => void` | - | 완료 콜백 |
589
- | `render` | `(timeLeft: TimeLeft) => ReactNode` | - | 커스텀 렌더링 |
845
+ | `list` | `ReactNode`[] | - | Data array to render. null = loading state (required) |
846
+ | `noDataMessage` | `ReactNode` | - | Message for empty array (string | ReactElement) |
847
+ | `errorFallback` | `ReactNode` | - | Fallback on error (ReactNode) |
848
+ | `loadingElement` | `ReactNode` | - | Custom loading element (default: Spinner) |
849
+ | `skeletonElement` | `ReactNode` | - | Skeleton element during loading (ReactElement) |
850
+ | `skeletonCount` | `number` | `3` | Skeleton repeat count |
851
+ | `loading` | `boolean` | `false` | Force loading state |
852
+ | `children` | `ReactNode` | - | Item render function: ({ item, index }) => ReactNode (required) |
853
+ | `className` | `string` | - | Root element style |
854
+
855
+ When list is null → **loading**, [] → **empty state**, array → **render**. ErrorBoundary built-in.
856
+
857
+ - **Default loading**: Without skeletonElement, a Spinner is shown automatically
858
+ - **Skeleton loading**: Pass a custom skeleton component to skeletonElement, rendered skeletonCount times
590
859
 
591
860
  ```tsx
592
- <Countdown endTimestamp={Date.now() + 60_000} showDays={false} onEnd={handleEnd} />
861
+ // Basic usage Spinner shown automatically when list is null
862
+ <DataList list={users} noDataMessage="No users found">
863
+ {({ item, index }) => <UserCard key={item.id} user={item} />}
864
+ </DataList>
865
+
866
+ // Skeleton loading — implement and pass a custom skeleton component
867
+ function UserSkeleton() {
868
+ return (
869
+ <div className="flex items-center gap-3 px-4 py-3">
870
+ <Skeleton circle width={32} height={32} />
871
+ <div className="flex-1 space-y-1.5">
872
+ <Skeleton className="h-3 w-24" />
873
+ <Skeleton className="h-2.5 w-16" />
874
+ </div>
875
+ </div>
876
+ );
877
+ }
878
+
879
+ <DataList
880
+ list={products}
881
+ skeletonElement={<UserSkeleton />}
882
+ skeletonCount={5}
883
+ >
884
+ {({ item }) => <ProductCard key={item.id} {...item} />}
885
+ </DataList>
593
886
  ```
594
887
 
595
888
  ---
596
889
 
597
- ## Marquee
890
+ ## InfiniteScroll
598
891
 
599
- 마퀴(흐르는 텍스트/요소).
892
+ Infinite scroll. Based on IntersectionObserver.
600
893
 
601
894
  | Prop | Type | Default | Description |
602
895
  |---|---|---|---|
603
- | `direction` | `'left' \| 'right' \| 'up' \| 'down'` | `'left'` | 방향 |
604
- | `speed` | `number` | `40` | 애니메이션 속도 () |
605
- | `pauseOnHover` | `boolean` | `false` | 호버 일시정지 |
606
- | `gap` | `number` | `16` | 아이템 간격 (px) |
896
+ | `list` | `ReactNode`[] | - | Current data array (required) |
897
+ | `totalCount` | `number` | - | Total count (mutually exclusive with hasMore) |
898
+ | `hasMore` | `boolean` | - | Has more items (mutually exclusive with totalCount) |
899
+ | `tag` | `string` | `"div"` | Children wrapper tag |
900
+ | `rootMargin` | `number` | `100` | Detection margin (px) |
901
+ | `loading` | `boolean` | - | Loading state |
902
+ | `loadingElement` | `ReactNode` | - | Custom loading element |
903
+ | `handleLoadMore` | `ReactNode` | - | Load more callback () => void, required |
904
+ | `scrollTarget` | `ReactNode` | - | Scroll target element (HTMLElement | Document | MutableRefObject) |
905
+ | `children` | `ReactNode` | - | List item rendering (ReactNode, required) |
906
+ | `className` | `string` | - | Style override |
607
907
 
608
908
  ```tsx
609
- <Marquee direction="left" speed={30} pauseOnHover>
610
- <span>흐르는 텍스트</span>
611
- </Marquee>
909
+ <InfiniteScroll
910
+ list={items}
911
+ totalCount={100}
912
+ loading={isLoading}
913
+ handleLoadMore={fetchMore}
914
+ >
915
+ {items.map(item => <Card key={item.id} {...item} />)}
916
+ </InfiniteScroll>
612
917
  ```
613
918
 
614
919
  ---
615
920
 
616
- ## Tab
921
+ ## Ellipsis
617
922
 
618
- 네비게이션. line/pill 변형.
923
+ Text ellipsis. Built-in show more/less toggle.
619
924
 
620
925
  | Prop | Type | Default | Description |
621
926
  |---|---|---|---|
622
- | `items` | `TabItem[]` (필수) | - | 항목 배열 |
623
- | `activeKey` | `string` | - | 제어 모드 활성 |
624
- | `defaultActiveKey` | `string` | - | 비제어 모드 초기 |
625
- | `variant` | `'line' \| 'pill'` | `'line'` | 스타일 |
626
- | `size` | `'sm' \| 'md'` | `'md'` | 크기 |
627
- | `destroyInactive` | `boolean` | `false` | 비활성 패널 언마운트 |
628
- | `onTabChange` | `(key: string) => void` | - | 변경 콜백 |
927
+ | `content` | `ReactNode` | `""` | Body text (ReactNode) |
928
+ | `lineClamp` | `number` | `2` | Line clamp limit |
929
+ | `triggerMore` | `ReactNode` | `"more"` | Show more text (ReactNode) |
930
+ | `triggerLess` | `ReactNode` | `"less"` | Show less text (ReactNode) |
931
+ | `defaultShortened` | `boolean` | `true` | Initial collapsed state |
932
+ | `observingEnvs` | `boolean`[] | - | Re-measure on external condition change |
933
+ | `onShowMoreLessClick` | `ReactNode` | - | Show more/less click callback () => void |
934
+ | `className` | `string` | - | Style override |
629
935
 
630
936
  ```tsx
631
- <Tab
632
- items={[
633
- { key: 'a', label: 'Tab A', children: <p>A</p> },
634
- { key: 'b', label: 'Tab B', children: <p>B</p> },
635
- ]}
636
- defaultActiveKey="a"
637
- variant="pill"
638
- />
937
+ <Ellipsis content={longText} lineClamp={3} triggerMore="more" triggerLess="less" />
639
938
  ```
640
939
 
641
940
  ---
642
941
 
643
- ## Carousel
942
+ ## NumberInput
644
943
 
645
- 캐러셀. Embla Carousel 기반. 컴포저블 패턴.
944
+ Number input. Accelerated increment on long press. Exposes increment/decrement methods via ref. numberInputBind(ref, direction) binds same acceleration to external buttons.
646
945
 
647
946
  | Prop | Type | Default | Description |
648
947
  |---|---|---|---|
649
- | `opts` | `EmblaOptionsType` | `{ loop: false }` | Embla 옵션 |
650
- | `plugins` | `EmblaPluginType[]` | - | Embla 플러그인 |
651
- | `onApiChange` | `(api: CarouselApi) => void` | - | API 변경 콜백 |
652
-
653
- 서브 컴포넌트: `CarouselSlide`, `CarouselPrev`, `CarouselNext`, `CarouselDots`
948
+ | `value` | `number` \| `string` | - | Current value |
949
+ | `size` | `'sm'` \| `'md'` \| `'lg'` \| `'xl'` | `"md"` | Size |
950
+ | `error` | `boolean` | - | Error state |
951
+ | `min` | `number` | - | Minimum value |
952
+ | `max` | `number` | - | Maximum value |
953
+ | `step` | `number` | `1` | Step increment |
954
+ | `digit` | `number` | `0` | Decimal places |
955
+ | `hideButtons` | `boolean` | `false` | Hide default spin buttons. Use with numberInputBind for external button event binding |
956
+ | `disabled` | `boolean` | - | Disabled |
957
+ | `readOnly` | `boolean` | - | Read-only (includes hiding spin buttons) |
958
+ | `placeholder` | `string` | - | Placeholder |
959
+ | `name` | `string` | - | Form field name |
960
+ | `id` | `string` | - | Element ID |
961
+ | `autoFocus` | `boolean` | - | Auto focus |
962
+ | `onValueChange` | `ReactNode` | - | Value change callback (value: number | undefined) => void |
963
+ | `onBlur` | `ReactNode` | - | Blur callback |
964
+ | `onFocus` | `ReactNode` | - | Focus callback |
965
+ | `className` | `string` | - | Style override |
966
+
967
+ Press-and-hold accelerates increment/decrement (100ms → 75ms → 50ms → 30ms). Default is right-side vertical spin buttons. Use `numberInputBind(ref, direction)` to bind the same acceleration events to external buttons for free placement.
654
968
 
655
969
  ```tsx
656
- <Carousel opts={{ loop: true }}>
657
- <CarouselSlide>슬라이드 1</CarouselSlide>
658
- <CarouselSlide>슬라이드 2</CarouselSlide>
659
- <CarouselPrev />
660
- <CarouselNext />
661
- <CarouselDots />
662
- </Carousel>
970
+ // Basic usage right-side vertical spin buttons
971
+ <NumberInput
972
+ value={count}
973
+ onValueChange={setCount}
974
+ min={0}
975
+ max={100}
976
+ step={5}
977
+ />
978
+
979
+ // External buttons — bind events with numberInputBind
980
+ const ref = useRef<NumberInputRef>(null);
981
+
982
+ <NumberInput ref={ref} value={count} onValueChange={setCount} hideButtons />
983
+
984
+ // Buttons can be placed anywhere, completely separate from NumberInput
985
+ <button {...numberInputBind(ref, 'decrement')}><MinusIcon /></button>
986
+ <button {...numberInputBind(ref, 'increment')}><PlusIcon /></button>
987
+
988
+ // Single invocation (onClick)
989
+ <button onClick={() => ref.current?.increment()}>+1</button>
663
990
  ```
664
991
 
665
992
  ---
666
993
 
667
- ## VirtualList / VirtualGrid
994
+ ## Hooks
668
995
 
669
- 가상 스크롤 리스트/그리드. @tanstack/react-virtual 기반.
996
+ ### useModal
670
997
 
671
- ### VirtualList
998
+ ```tsx
999
+ const { open, close } = useModal();
1000
+ open(MyComponent, { title: 'Title' });
1001
+ ```
672
1002
 
673
- | Prop | Type | Default | Description |
674
- |---|---|---|---|
675
- | `items` | `T[]` (필수) | - | 데이터 배열 |
676
- | `estimateSize` | `number \| (index: number) => number` (필수) | - | 예상 아이템 높이 |
677
- | `renderItem` | `(item: T, index: number) => ReactNode` (필수) | - | 렌더러 |
678
- | `overscan` | `number` | `5` | 오버스캔 |
679
- | `gap` | `number` | `0` | 아이템 간격 |
680
- | `onEndReached` | `() => void` | - | 끝 도달 콜백 |
681
- | `endReachedThreshold` | `number` | `200` | 끝 감지 임계값 (px) |
1003
+ ### useInView
682
1004
 
683
1005
  ```tsx
684
- <VirtualList
685
- items={data}
686
- estimateSize={48}
687
- renderItem={(item) => <div>{item.name}</div>}
688
- onEndReached={loadMore}
689
- />
1006
+ const { ref, inView } = useInView({ threshold: 0.5 });
1007
+ <div ref={ref}>{inView && <LazyContent />}</div>
690
1008
  ```
691
1009
 
692
- ### VirtualGrid
1010
+ ### useCheckDevice
693
1011
 
694
- VirtualList와 동일 + `columns: number` (필수).
1012
+ ```tsx
1013
+ const { isMobile, isTablet, isDesktop } = useCheckDevice();
1014
+ ```
1015
+
1016
+ ### useClickOutside
695
1017
 
696
1018
  ```tsx
697
- <VirtualGrid items={data} estimateSize={120} columns={3} renderItem={(item) => <Card {...item} />} />
1019
+ const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
1020
+ <div ref={ref}>Dropdown content</div>
698
1021
  ```
1022
+
1023
+ ---
1024
+
1025
+ ## Toast (Imperative API)
1026
+
1027
+ Toast notifications. Sonner-based.
1028
+
1029
+ ```tsx
1030
+ import { toast, Toaster } from '@nexus-cross/design-system';
1031
+
1032
+ // Place Toaster at app root
1033
+ <Toaster position="top-right" />
1034
+
1035
+ // Usage
1036
+ toast('Saved successfully');
1037
+ toast.success('Success!');
1038
+ toast.error('An error occurred');
1039
+ toast.loading('Processing...');
1040
+ ```
1041
+