@nexus-cross/design-system 1.1.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-rules/nexus/CLAUDE.md +34 -6
- package/cursor-rules/CLAUDE.md +38 -9
- package/cursor-rules/nexus-project-setup.mdc +30 -16
- package/cursor-rules/nexus-ui-api.mdc +127 -41
- package/cursor-rules/nexus-ui-decisions.mdc +6 -2
- package/dist/chunks/{chunk-G3RLK2HS.js → chunk-3SCSND6S.js} +1 -1
- package/dist/chunks/{chunk-56ZOOQFE.mjs → chunk-AG2UJPFX.mjs} +124 -17
- package/dist/chunks/{chunk-EILXBLEV.mjs → chunk-QWK4CLS2.mjs} +1 -1
- package/dist/chunks/{chunk-5ASTWFJW.js → chunk-RC2Y4UH7.js} +127 -17
- package/dist/combobox.js +15 -3
- package/dist/combobox.mjs +1 -1
- package/dist/components/Combobox.d.ts +53 -8
- package/dist/components/Combobox.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -24
- package/dist/index.mjs +4 -5
- package/dist/modal/index.js +11 -11
- package/dist/modal/index.mjs +2 -2
- package/dist/schemas/_all.json +62 -17
- package/dist/schemas/combobox.d.ts +33 -10
- package/dist/schemas/combobox.d.ts.map +1 -1
- package/dist/schemas/combobox.json +6 -6
- package/dist/schemas/comboboxOption.json +16 -11
- package/dist/schemas/comboboxOptionDescription.json +20 -0
- package/dist/schemas/comboboxOptionMeta.json +20 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas.js +74 -13
- package/dist/schemas.mjs +72 -13
- package/dist/styles/.generated/built.d.ts +1 -1
- package/dist/styles/.generated/built.d.ts.map +1 -1
- package/dist/styles/layer.js +2 -2
- package/dist/styles/layer.mjs +1 -1
- package/dist/styles.css +13 -0
- package/dist/styles.js +2 -2
- package/dist/styles.layered.css +13 -0
- package/dist/styles.mjs +1 -1
- package/dist/tailwind-v4.css +19 -0
- package/dist/tokens/TOKENS.md +426 -0
- package/dist/tokens/company.css +410 -0
- package/dist/tokens/css.css +405 -0
- package/dist/tokens/data/borderWidth.json +38 -0
- package/dist/tokens/data/breakpoint.json +23 -0
- package/dist/tokens/data/color.json +973 -0
- package/dist/tokens/data/index.ts +63 -0
- package/dist/tokens/data/motion.json +64 -0
- package/dist/tokens/data/opacity.json +65 -0
- package/dist/tokens/data/radius.json +25 -0
- package/dist/tokens/data/shadow.json +76 -0
- package/dist/tokens/data/size.json +46 -0
- package/dist/tokens/data/space.json +86 -0
- package/dist/tokens/data/typography.json +626 -0
- package/dist/tokens/data/zIndex.json +22 -0
- package/dist/tokens/index.d.ts +11 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +12 -0
- package/dist/tokens/index.mjs +1 -0
- package/dist/tokens/tailwind.js +260 -0
- package/dist/tokens-domains/data/index.ts +16 -0
- package/dist/tokens-domains/data/prediction/domain.json +324 -0
- package/dist/tokens-domains/index.d.ts +12 -0
- package/dist/tokens-domains/index.d.ts.map +1 -0
- package/dist/tokens-domains/index.js +12 -0
- package/dist/tokens-domains/index.mjs +1 -0
- package/dist/tokens-domains/prediction-vars.css +154 -0
- package/dist/tokens-domains/prediction.css +153 -0
- package/dist/tokens-domains/prediction.md +70 -0
- package/dist/tokens-domains/tailwind.js +59 -0
- package/package.json +27 -6
- package/dist/chunks/{chunk-5ZVPTIL3.mjs → chunk-3VFBPFZF.mjs} +1 -1
- package/dist/chunks/{chunk-7F4SOLAC.js → chunk-U53UA76K.js} +1 -1
|
@@ -34,7 +34,13 @@
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
-
## Quick
|
|
37
|
+
## Quick Install (단일 패키지)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @nexus-cross/design-system # 토큰/도메인 토큰까지 자동 transitive install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Component / Token Import
|
|
38
44
|
|
|
39
45
|
```tsx
|
|
40
46
|
import {
|
|
@@ -47,25 +53,47 @@ import {
|
|
|
47
53
|
} from '@nexus-cross/design-system';
|
|
48
54
|
|
|
49
55
|
import { modal, useModal, ModalTemplate, ModalContainer } from '@nexus-cross/design-system/modal';
|
|
56
|
+
|
|
57
|
+
// 토큰 JS API (sub-path)
|
|
58
|
+
import { getTheme } from '@nexus-cross/design-system/tokens';
|
|
59
|
+
import { getPredictionTheme } from '@nexus-cross/design-system/tokens-domains';
|
|
50
60
|
```
|
|
51
61
|
|
|
52
62
|
---
|
|
53
63
|
|
|
54
64
|
## CSS Setup (한 번만)
|
|
55
65
|
|
|
56
|
-
### Tailwind v4
|
|
66
|
+
### Tailwind v4 (Next + Turbopack / Vite 등 모든 환경) — **2줄 셋업**
|
|
67
|
+
|
|
68
|
+
**design-system + 토큰을 함께 쓸 때 (대부분의 경우):**
|
|
57
69
|
```css
|
|
70
|
+
/* globals.css */
|
|
58
71
|
@import 'tailwindcss';
|
|
59
|
-
@import '@nexus-cross/
|
|
60
|
-
/*
|
|
72
|
+
@import '@nexus-cross/design-system/tailwind-v4.css';
|
|
73
|
+
/* 도메인 토큰 사용 시 한 줄 추가 */
|
|
74
|
+
/* @import '@nexus-cross/design-system/tokens-domains/prediction.css'; */
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`tailwind-v4.css` 안에서 회사 공통 토큰이 sub-path를 통해 자동 로드되므로 **토큰 import를 따로 쓸 필요 없습니다.** 컴포넌트 CSS는 Tailwind의 `components` 레이어에 주입되어 `className="bg-red-500"` 오버라이드가 항상 utility에 우선합니다.
|
|
78
|
+
|
|
79
|
+
**디자인 토큰만 쓸 때 (design-system 없이):** `@nexus-cross/tokens`를 별도 설치하고 `@import '@nexus-cross/tokens/company.css';` 사용.
|
|
80
|
+
|
|
81
|
+
**`@layer base, nexus, components, utilities;` statement를 직접 쓰지 마세요.** Tailwind v4 프로세서가 자동 정리/제거하기 때문에 의도와 다르게 동작합니다.
|
|
82
|
+
|
|
83
|
+
### Tailwind v3 / 순수 CSS
|
|
84
|
+
```css
|
|
85
|
+
/* globals.css */
|
|
86
|
+
@import '@nexus-cross/design-system/tokens/css';
|
|
61
87
|
@import '@nexus-cross/design-system/styles.css';
|
|
88
|
+
@tailwind base; @tailwind components; @tailwind utilities;
|
|
62
89
|
```
|
|
90
|
+
또는 entry에서 `import '@nexus-cross/design-system/styles'`.
|
|
63
91
|
|
|
64
|
-
### Next.js
|
|
92
|
+
### Next.js (모든 버전)
|
|
65
93
|
```js
|
|
66
94
|
// next.config.mjs
|
|
67
95
|
const nextConfig = {
|
|
68
|
-
transpilePackages: ['@nexus-cross/design-system'
|
|
96
|
+
transpilePackages: ['@nexus-cross/design-system'],
|
|
69
97
|
}
|
|
70
98
|
```
|
|
71
99
|
|
package/cursor-rules/CLAUDE.md
CHANGED
|
@@ -25,33 +25,62 @@
|
|
|
25
25
|
|
|
26
26
|
4. **`className` 오버라이드 시 `!important` 금지.** `cn()` 유틸이 프리픽스 충돌 자동 해소.
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## 설치 (단일 패키지)
|
|
29
|
+
|
|
30
|
+
v2.0부터 `@nexus-cross/design-system` 1개 install로 토큰까지 모두 사용 가능합니다.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @nexus-cross/design-system
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Component / Token Import
|
|
29
37
|
|
|
30
38
|
```tsx
|
|
39
|
+
// UI 컴포넌트
|
|
31
40
|
import { Button, TextInput, TextArea, Select, Switch, Chip, Spinner, Divider } from '@nexus-cross/design-system';
|
|
32
41
|
import { Tooltip, Popover, Drawer, Accordion } from '@nexus-cross/design-system';
|
|
33
42
|
import { toast, Toaster } from '@nexus-cross/design-system';
|
|
34
43
|
import { modal, useModal, ModalTemplate, ModalContainer } from '@nexus-cross/design-system/modal';
|
|
35
|
-
import { NumberInput, numberInputBind } from '@nexus-cross/design-system';
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
import { NumberInput, numberInputBind, Avatar, Tab, ToggleGroup, cn } from '@nexus-cross/design-system';
|
|
45
|
+
|
|
46
|
+
// 토큰 JS API (sub-path)
|
|
47
|
+
import { getTheme } from '@nexus-cross/design-system/tokens';
|
|
48
|
+
import { getPredictionTheme } from '@nexus-cross/design-system/tokens-domains';
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
## CSS Setup
|
|
41
52
|
|
|
42
|
-
### Tailwind v4
|
|
53
|
+
### Tailwind v4 (Next + Turbopack / Vite 등 모든 환경) — **2줄 셋업**
|
|
54
|
+
|
|
55
|
+
**design-system + 토큰을 함께 쓸 때 (대부분의 경우):**
|
|
43
56
|
```css
|
|
57
|
+
/* globals.css */
|
|
44
58
|
@import 'tailwindcss';
|
|
45
|
-
@import '@nexus-cross/
|
|
46
|
-
/*
|
|
59
|
+
@import '@nexus-cross/design-system/tailwind-v4.css';
|
|
60
|
+
/* 도메인 토큰 사용 시 한 줄 추가 */
|
|
61
|
+
/* @import '@nexus-cross/design-system/tokens-domains/prediction.css'; */
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`tailwind-v4.css` 안에서 회사 공통 토큰이 자동 로드되므로 **토큰 import를 따로 쓸 필요 없습니다.** 컴포넌트 CSS는 Tailwind의 `components` 레이어에 주입되어 `className="bg-red-500"` 오버라이드가 항상 utility에 우선합니다.
|
|
65
|
+
|
|
66
|
+
**디자인 토큰만 쓸 때 (design-system 없이):** `@nexus-cross/tokens`를 별도 설치하고 `@import '@nexus-cross/tokens/company.css';` 사용.
|
|
67
|
+
|
|
68
|
+
**절대 직접 `@layer base, nexus, components, utilities;` 같은 statement를 globals.css에 쓰지 말 것.** Tailwind v4 프로세서가 자동 정리해버려 의도와 다르게 동작합니다.
|
|
69
|
+
|
|
70
|
+
### Tailwind v3 / 순수 CSS
|
|
71
|
+
```css
|
|
72
|
+
/* globals.css */
|
|
73
|
+
@import '@nexus-cross/design-system/tokens/css';
|
|
47
74
|
@import '@nexus-cross/design-system/styles.css';
|
|
75
|
+
@tailwind base; @tailwind components; @tailwind utilities;
|
|
48
76
|
```
|
|
77
|
+
또는 entry에서 `import '@nexus-cross/design-system/styles'`.
|
|
49
78
|
|
|
50
|
-
### Next.js
|
|
79
|
+
### Next.js (모든 버전)
|
|
51
80
|
```js
|
|
52
81
|
// next.config.mjs
|
|
53
82
|
const nextConfig = {
|
|
54
|
-
transpilePackages: ['@nexus-cross/design-system'
|
|
83
|
+
transpilePackages: ['@nexus-cross/design-system'],
|
|
55
84
|
}
|
|
56
85
|
```
|
|
57
86
|
|
|
@@ -13,28 +13,41 @@ This project uses NEXUS Design System. All generated code MUST follow the rules
|
|
|
13
13
|
- **UI Components**: `@nexus-cross/design-system` (React, CVA + Plain CSS)
|
|
14
14
|
- **Styling**: Tailwind CSS v4 + NEXUS semantic tokens
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Install (단일 패키지)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
v2.0부터 design-system 1개 install로 토큰까지 모두 가능합니다.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| Tailwind v4 | `@nexus-cross/design-system/styles/layer` | wrapped in `@layer nexus` |
|
|
20
|
+
```bash
|
|
21
|
+
npm install @nexus-cross/design-system # @nexus-cross/tokens, tokens-domains 자동 transitive install
|
|
22
|
+
```
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
// Tailwind v3, Plain CSS, CSS Modules
|
|
27
|
-
import '@nexus-cross/design-system/styles'
|
|
24
|
+
토큰만 단독으로 쓰는 경우엔 `npm install @nexus-cross/tokens` (또는 `tokens-domains`) 가능.
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
import '@nexus-cross/design-system/styles/layer'
|
|
31
|
-
```
|
|
26
|
+
## Design System CSS Setup
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
| Environment | Setup |
|
|
29
|
+
|---|---|
|
|
30
|
+
| **Tailwind v4** (Next + Turbopack / Vite 등) | `globals.css`에서 2줄: `@import 'tailwindcss';` + `@import '@nexus-cross/design-system/tailwind-v4.css';` |
|
|
31
|
+
| Tailwind v3 | `globals.css`에 `@import '@nexus-cross/design-system/tokens/css';` + `@import '@nexus-cross/design-system/styles.css';` |
|
|
32
|
+
| Plain CSS / CSS Modules | `@import '@nexus-cross/design-system/tokens/css';` + entry에서 `import '@nexus-cross/design-system/styles'` |
|
|
33
|
+
|
|
34
|
+
### Tailwind v4 — **2줄 셋업** (권장)
|
|
35
|
+
|
|
36
|
+
**design-system + 토큰을 함께 쓸 때 (대부분의 경우):**
|
|
34
37
|
```css
|
|
35
|
-
|
|
38
|
+
/* globals.css */
|
|
39
|
+
@import 'tailwindcss';
|
|
40
|
+
@import '@nexus-cross/design-system/tailwind-v4.css';
|
|
41
|
+
/* 도메인 토큰 사용 시 */
|
|
42
|
+
/* @import '@nexus-cross/design-system/tokens-domains/prediction.css'; */
|
|
36
43
|
```
|
|
37
44
|
|
|
45
|
+
`tailwind-v4.css` 안에서 회사 공통 토큰이 sub-path를 통해 자동 로드되므로 **토큰 import를 따로 쓸 필요 없습니다.** 컴포넌트 CSS는 Tailwind의 `components` 레이어에 주입되어 `className="bg-red-500"` 오버라이드가 항상 utility에 우선합니다.
|
|
46
|
+
|
|
47
|
+
**디자인 토큰만 쓸 때 (design-system 없이):** `@nexus-cross/tokens`를 별도 설치하고 `@import '@nexus-cross/tokens/company.css';` 사용.
|
|
48
|
+
|
|
49
|
+
**`@layer base, nexus, components, utilities;` 같은 statement를 직접 쓰지 마세요.** Tailwind v4 프로세서가 자동으로 정리/제거하기 때문에 의도와 다르게 동작합니다.
|
|
50
|
+
|
|
38
51
|
## Absolute Rules
|
|
39
52
|
|
|
40
53
|
1. **Always use NEXUS tokens for colors.** Hardcoding is prohibited.
|
|
@@ -225,8 +238,9 @@ import { toast, Toaster } from '@nexus-cross/design-system';
|
|
|
225
238
|
// Modal system (separate subpath)
|
|
226
239
|
import { modal, useModal, ModalTemplate, ModalContainer } from '@nexus-cross/design-system/modal';
|
|
227
240
|
|
|
228
|
-
// Tokens (
|
|
229
|
-
import { getTheme } from '@nexus-cross/tokens';
|
|
241
|
+
// Tokens (sub-path — design-system 1개 install로 사용 가능)
|
|
242
|
+
import { getTheme } from '@nexus-cross/design-system/tokens';
|
|
243
|
+
import { getPredictionTheme } from '@nexus-cross/design-system/tokens-domains';
|
|
230
244
|
```
|
|
231
245
|
|
|
232
246
|
## Modal Writing Rules
|
|
@@ -241,7 +241,7 @@ Individual option within Select.
|
|
|
241
241
|
|
|
242
242
|
## Combobox
|
|
243
243
|
|
|
244
|
-
Searchable select. Text input + popover listbox. Single/multi-select. Sync (auto-filter) or async (onSearch + loading) modes.
|
|
244
|
+
Searchable select with compound option API. Text input + popover listbox. Single/multi-select. Sync (auto-filter) or async (onSearch + loading) modes.
|
|
245
245
|
|
|
246
246
|
WHEN TO USE:
|
|
247
247
|
• Options ≥ 7, OR labels are long, OR search/filter is needed → Combobox (not Select)
|
|
@@ -249,8 +249,29 @@ WHEN TO USE:
|
|
|
249
249
|
• Async data from server → set onSearch + loading
|
|
250
250
|
For ≤7 simple options use Select. For free-text tags use TagInput.
|
|
251
251
|
|
|
252
|
+
COMPOUND API:
|
|
253
|
+
<Combobox value={v} onValueChange={setV} placeholder="…">
|
|
254
|
+
<Combobox.Option value="kr">한국</Combobox.Option>
|
|
255
|
+
<Combobox.Option value="jp" disabled>
|
|
256
|
+
일본
|
|
257
|
+
<Combobox.OptionDescription>품절</Combobox.OptionDescription>
|
|
258
|
+
<Combobox.OptionMeta>JP</Combobox.OptionMeta>
|
|
259
|
+
</Combobox.Option>
|
|
260
|
+
</Combobox>
|
|
261
|
+
|
|
262
|
+
• <Combobox.Option> requires unique `value` (dev mode warns on duplicates and drops the duplicate)
|
|
263
|
+
• <Combobox.OptionDescription> = secondary line under label
|
|
264
|
+
• <Combobox.OptionMeta> = right-aligned slot (price, shortcut, badge)
|
|
265
|
+
• Both slots are excluded from textValue-based search
|
|
266
|
+
|
|
252
267
|
ASYNC PATTERN:
|
|
253
|
-
<Combobox
|
|
268
|
+
<Combobox loading={isFetching} onSearch={(q) => mutate(q)}>
|
|
269
|
+
{results.map((u) => (
|
|
270
|
+
<Combobox.Option key={u.id} value={u.id}>{u.name}
|
|
271
|
+
<Combobox.OptionDescription>{u.email}</Combobox.OptionDescription>
|
|
272
|
+
</Combobox.Option>
|
|
273
|
+
))}
|
|
274
|
+
</Combobox>
|
|
254
275
|
— onSearch fires after searchDebounce (default 250ms). Do NOT clear input on result update; component preserves user's typing.
|
|
255
276
|
|
|
256
277
|
IME (Korean/Japanese/Chinese): Enter during composition is ignored automatically — do not add custom keydown handlers.
|
|
@@ -258,19 +279,22 @@ IME (Korean/Japanese/Chinese): Enter during composition is ignored automatically
|
|
|
258
279
|
ANTI-PATTERNS:
|
|
259
280
|
✗ <Select> with 20 options → <Combobox>
|
|
260
281
|
✗ Manual <input> + dropdown div + filter logic → <Combobox>
|
|
282
|
+
✗ Passing options through a prop array (legacy API removed) → use <Combobox.Option> children
|
|
283
|
+
✗ Wrapping options in extra elements (<div><Combobox.Option/></div>) → keep them as direct children
|
|
284
|
+
✗ Same `value` on two <Combobox.Option> — duplicates are warned + dropped in dev mode
|
|
261
285
|
✗ Setting value externally to clear input mid-typing → use onValueChange instead
|
|
262
286
|
|
|
263
287
|
| Prop | Type | Default | Description |
|
|
264
288
|
|---|---|---|---|
|
|
265
|
-
| `
|
|
289
|
+
| `children` | `ReactNode` | - | <Combobox.Option> elements (required). Other children are ignored with a dev-mode warning. Async-search consumers swap this list as `onSearch` results arrive. |
|
|
266
290
|
| `value` | `ReactNode` | - | Selected value. string for single, string[] for multiple |
|
|
267
291
|
| `defaultValue` | `ReactNode` | - | Initial value (uncontrolled) |
|
|
268
292
|
| `onValueChange` | `ReactNode` | - | Value change callback. (value: string | string[]) => void |
|
|
269
293
|
| `multiple` | `boolean` | `false` | Multi-select mode. Selected values shown as chips inside input |
|
|
270
|
-
| `onSearch` | `ReactNode` | - | Async search callback. (query: string) => void. Triggers external data fetching with debounce |
|
|
294
|
+
| `onSearch` | `ReactNode` | - | Async search callback. (query: string) => void. Triggers external data fetching with debounce. When set, the built-in client filter is disabled — render whatever <Combobox.Option> children match the latest results. |
|
|
271
295
|
| `searchDebounce` | `number` | `250` | Debounce delay (ms) before onSearch fires |
|
|
272
|
-
| `loading` | `boolean` | `false` | Externally-controlled loading state. Shows spinner in input suffix |
|
|
273
|
-
| `filter` | `ReactNode` | - | Custom client-side filter. (option, query) => boolean. Default: case-insensitive
|
|
296
|
+
| `loading` | `boolean` | `false` | Externally-controlled loading state. Shows spinner in input suffix and a status row inside the popover. |
|
|
297
|
+
| `filter` | `ReactNode` | - | Custom client-side filter. (option: { value, textValue, disabled }, query: string) => boolean. Default: case-insensitive textValue includes match. Ignored when onSearch is set. |
|
|
274
298
|
| `placeholder` | `string` | - | Input placeholder |
|
|
275
299
|
| `emptyMessage` | `ReactNode` | - | Message when no options match (string | ReactNode). Default: "검색 결과 없음" |
|
|
276
300
|
| `loadingMessage` | `ReactNode` | - | Message during loading state inside popover (string | ReactNode). Default: "검색 중…" |
|
|
@@ -284,83 +308,140 @@ ANTI-PATTERNS:
|
|
|
284
308
|
| `className` | `string` | - | Wrapper className |
|
|
285
309
|
| `popoverClassName` | `string` | - | Popover content className |
|
|
286
310
|
|
|
287
|
-
###
|
|
311
|
+
### Combobox.Option
|
|
312
|
+
|
|
313
|
+
Single Combobox option. Direct child of <Combobox> only.
|
|
314
|
+
|
|
315
|
+
WHEN TO USE:
|
|
316
|
+
• One per selectable item; `value` MUST be unique within the Combobox
|
|
317
|
+
• Wrap rich content (icons, badges) directly as children — no escape hatch needed
|
|
318
|
+
• Use textValue when label is non-text (e.g. <Combobox.Option value="apple" textValue="사과 apple">🍎</Combobox.Option>)
|
|
319
|
+
|
|
320
|
+
ANTI-PATTERNS:
|
|
321
|
+
✗ <Combobox><div><Combobox.Option/></div></Combobox> — Option must be a direct child
|
|
322
|
+
✗ Same value on two options → dev warning + silent drop; pick unique values
|
|
323
|
+
✗ Putting label text inside <Combobox.OptionDescription> — that slot is the secondary line below the label
|
|
324
|
+
|
|
325
|
+
| Prop | Type | Default | Description |
|
|
326
|
+
|---|---|---|---|
|
|
327
|
+
| `value` | `string` | - | Unique value (string, required). Duplicate values within one Combobox produce a dev-mode console.error and the duplicate option is dropped. |
|
|
328
|
+
| `disabled` | `boolean` | - | Disable selection. Skipped by keyboard navigation (Arrow Up/Down, Home/End). |
|
|
329
|
+
| `textValue` | `string` | - | Text used for client-side filtering and the input display when this option is selected. If omitted, derived from `children` (string nodes only; OptionDescription / OptionMeta are excluded). Set this when label contains icons or non-text nodes you still want searchable (e.g. textValue="apple 사과 fruit"). |
|
|
330
|
+
| `className` | `string` | - | Class merged onto the rendered <div role="option">. |
|
|
331
|
+
| `children` | `ReactNode` | - | Label content + optional <Combobox.OptionDescription> / <Combobox.OptionMeta> slots. |
|
|
332
|
+
|
|
333
|
+
### Combobox.OptionDescription
|
|
334
|
+
|
|
335
|
+
Secondary text shown below an option label. Use for hints like "Republic of Korea" beneath "한국". Excluded from textValue-based search.
|
|
336
|
+
|
|
337
|
+
| Prop | Type | Default | Description |
|
|
338
|
+
|---|---|---|---|
|
|
339
|
+
| `children` | `ReactNode` | - | Secondary text below the label (ReactNode). |
|
|
340
|
+
| `className` | `string` | - | Class for the description node. |
|
|
341
|
+
|
|
342
|
+
### Combobox.OptionMeta
|
|
288
343
|
|
|
289
|
-
|
|
344
|
+
Right-aligned slot inside an option. Use for prices, keyboard shortcuts, version tags, status badges. Excluded from textValue-based search.
|
|
290
345
|
|
|
291
346
|
| Prop | Type | Default | Description |
|
|
292
347
|
|---|---|---|---|
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `description` | `ReactNode` | - | Secondary text below label (ReactNode) |
|
|
296
|
-
| `disabled` | `boolean` | - | Disabled option |
|
|
348
|
+
| `children` | `ReactNode` | - | Right-aligned meta content (price, badge, shortcut, etc.). |
|
|
349
|
+
| `className` | `string` | - | Class for the meta slot. |
|
|
297
350
|
|
|
298
|
-
Searchable select with popover listbox. Supports single/multi-select and sync (auto-filter) / async (onSearch + loading) modes. **WAI-ARIA Combobox pattern** (Radix Popover under the hood — NOT Radix Select).
|
|
351
|
+
Searchable select with popover listbox. **Compound API** — options are declared as <Combobox.Option> children. Supports single/multi-select and sync (auto-filter) / async (onSearch + loading) modes. **WAI-ARIA Combobox pattern** (Radix Popover under the hood — NOT Radix Select).
|
|
299
352
|
|
|
300
|
-
- **
|
|
301
|
-
- **
|
|
302
|
-
- **
|
|
353
|
+
- **Subcomponents**: `Combobox.Option` (required `value`), `Combobox.OptionDescription` (secondary line), `Combobox.OptionMeta` (right-aligned slot).
|
|
354
|
+
- **Duplicate `value`**: dev mode logs `console.error` and silently drops the duplicate.
|
|
355
|
+
- **textValue**: derived from option children (text nodes only; OptionDescription/OptionMeta excluded). Override per-option when label is non-text.
|
|
356
|
+
- **Sync mode** (no `onSearch`): client-side filters by `textValue` (case-insensitive includes; override with `filter`).
|
|
357
|
+
- **Async mode** (provide `onSearch`): debounced (`searchDebounce`, default 250ms) callback fires for external data fetch. Set `loading` for spinner.
|
|
358
|
+
- **Multi-select** (`multiple`): chips inside input; Backspace on empty input removes the last chip.
|
|
303
359
|
- Keyboard: Arrow Up/Down, Home/End, Enter to select, Escape to close.
|
|
304
360
|
|
|
305
361
|
```tsx
|
|
306
|
-
// 1) Sync —
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
value={value}
|
|
316
|
-
onValueChange={setValue}
|
|
317
|
-
placeholder="국가 선택"
|
|
318
|
-
/>
|
|
362
|
+
// 1) Sync — declarative options
|
|
363
|
+
<Combobox value={value} onValueChange={setValue} placeholder="국가 선택">
|
|
364
|
+
<Combobox.Option value="kr">한국</Combobox.Option>
|
|
365
|
+
<Combobox.Option value="jp">일본</Combobox.Option>
|
|
366
|
+
<Combobox.Option value="us" disabled>
|
|
367
|
+
미국
|
|
368
|
+
<Combobox.OptionDescription>일시 품절</Combobox.OptionDescription>
|
|
369
|
+
</Combobox.Option>
|
|
370
|
+
</Combobox>
|
|
319
371
|
|
|
320
372
|
// 2) Async — external search w/ loading spinner
|
|
321
|
-
const [results, setResults] = useState<
|
|
373
|
+
const [results, setResults] = useState<{ id: string; name: string; email: string }[]>([]);
|
|
322
374
|
const [loading, setLoading] = useState(false);
|
|
323
375
|
|
|
324
376
|
const handleSearch = async (q: string) => {
|
|
325
|
-
if (!q) return setResults([]);
|
|
326
377
|
setLoading(true);
|
|
327
|
-
|
|
328
|
-
setResults(users.map((u) => ({ value: u.id, label: u.name, description: u.email })));
|
|
378
|
+
setResults(await fetchUsers(q));
|
|
329
379
|
setLoading(false);
|
|
330
380
|
};
|
|
331
381
|
|
|
332
382
|
<Combobox
|
|
333
|
-
options={results}
|
|
334
383
|
loading={loading}
|
|
335
384
|
onSearch={handleSearch}
|
|
336
385
|
value={selectedId}
|
|
337
386
|
onValueChange={setSelectedId}
|
|
338
387
|
placeholder="유저 검색…"
|
|
339
388
|
emptyMessage="검색 결과 없음"
|
|
340
|
-
|
|
389
|
+
>
|
|
390
|
+
{results.map((u) => (
|
|
391
|
+
<Combobox.Option key={u.id} value={u.id}>
|
|
392
|
+
{u.name}
|
|
393
|
+
<Combobox.OptionDescription>{u.email}</Combobox.OptionDescription>
|
|
394
|
+
</Combobox.Option>
|
|
395
|
+
))}
|
|
396
|
+
</Combobox>
|
|
341
397
|
|
|
342
398
|
// 3) Multi-select with chips
|
|
343
399
|
<Combobox
|
|
344
400
|
multiple
|
|
345
|
-
options={results}
|
|
346
401
|
value={selectedIds}
|
|
347
402
|
onValueChange={setSelectedIds}
|
|
348
403
|
loading={loading}
|
|
349
404
|
onSearch={handleSearch}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
405
|
+
>
|
|
406
|
+
{results.map((u) => (
|
|
407
|
+
<Combobox.Option key={u.id} value={u.id}>{u.name}</Combobox.Option>
|
|
408
|
+
))}
|
|
409
|
+
</Combobox>
|
|
410
|
+
|
|
411
|
+
// 4) Rich option — meta slot for prices / shortcuts / badges
|
|
412
|
+
<Combobox placeholder="플랜 선택">
|
|
413
|
+
<Combobox.Option value="free">
|
|
414
|
+
Free
|
|
415
|
+
<Combobox.OptionDescription>개인용</Combobox.OptionDescription>
|
|
416
|
+
<Combobox.OptionMeta>$0/mo</Combobox.OptionMeta>
|
|
417
|
+
</Combobox.Option>
|
|
418
|
+
<Combobox.Option value="pro">
|
|
419
|
+
Pro
|
|
420
|
+
<Combobox.OptionDescription>소규모 팀</Combobox.OptionDescription>
|
|
421
|
+
<Combobox.OptionMeta>$12/mo</Combobox.OptionMeta>
|
|
422
|
+
</Combobox.Option>
|
|
423
|
+
</Combobox>
|
|
424
|
+
|
|
425
|
+
// 5) Non-text label — explicit textValue for searching
|
|
426
|
+
<Combobox.Option value="apple" textValue="apple 사과 fruit">
|
|
427
|
+
🍎 사과
|
|
428
|
+
</Combobox.Option>
|
|
429
|
+
|
|
430
|
+
// 6) With label / description / error
|
|
353
431
|
<Combobox
|
|
354
432
|
label="담당자"
|
|
355
433
|
description="검색 후 선택하세요"
|
|
356
|
-
options={results}
|
|
357
434
|
loading={loading}
|
|
358
435
|
onSearch={handleSearch}
|
|
359
436
|
value={value}
|
|
360
437
|
onValueChange={setValue}
|
|
361
438
|
error={!value}
|
|
362
439
|
size="lg"
|
|
363
|
-
|
|
440
|
+
>
|
|
441
|
+
{results.map((u) => (
|
|
442
|
+
<Combobox.Option key={u.id} value={u.id}>{u.name}</Combobox.Option>
|
|
443
|
+
))}
|
|
444
|
+
</Combobox>
|
|
364
445
|
```
|
|
365
446
|
|
|
366
447
|
**When to use `Select` vs `Combobox`**:
|
|
@@ -368,6 +449,11 @@ const handleSearch = async (q: string) => {
|
|
|
368
449
|
- Long list / async search / typed input → `Combobox`
|
|
369
450
|
- Free-form tag input (any string) → `TagInput`
|
|
370
451
|
|
|
452
|
+
**ANTI-PATTERNS**:
|
|
453
|
+
- ✗ `<Combobox options={[...]}>` — legacy prop API removed; use `<Combobox.Option>` children
|
|
454
|
+
- ✗ Wrapping options: `<Combobox><div><Combobox.Option/></div></Combobox>` — Option must be a direct child
|
|
455
|
+
- ✗ Duplicate `value` across options — dev warning + duplicate dropped
|
|
456
|
+
|
|
371
457
|
---
|
|
372
458
|
|
|
373
459
|
## CheckBox
|
|
@@ -40,8 +40,12 @@ UI 작업 시 어떤 컴포넌트를 골라야 할지 결정하는 가이드. **
|
|
|
40
40
|
// ❌ 옵션 20개에 Select
|
|
41
41
|
<Select><SelectItem>...</SelectItem>...</Select>
|
|
42
42
|
|
|
43
|
-
// ✅ 옵션 20개에 Combobox
|
|
44
|
-
<Combobox
|
|
43
|
+
// ✅ 옵션 20개에 Combobox (compound API)
|
|
44
|
+
<Combobox placeholder="검색하여 선택">
|
|
45
|
+
{options.map((o) => (
|
|
46
|
+
<Combobox.Option key={o.value} value={o.value}>{o.label}</Combobox.Option>
|
|
47
|
+
))}
|
|
48
|
+
</Combobox>
|
|
45
49
|
|
|
46
50
|
// ❌ 옵션 3개에 Select
|
|
47
51
|
<Select><SelectItem>S</SelectItem><SelectItem>M</SelectItem><SelectItem>L</SelectItem></Select>
|