@nurimedia/react-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/AlertDialog/index.cjs +185 -0
- package/dist/AlertDialog/index.cjs.map +1 -0
- package/dist/AlertDialog/index.d.cts +49 -0
- package/dist/AlertDialog/index.d.ts +49 -0
- package/dist/AlertDialog/index.js +148 -0
- package/dist/AlertDialog/index.js.map +1 -0
- package/dist/AlertDialog/styles.css +187 -0
- package/dist/Button/index.cjs +136 -0
- package/dist/Button/index.cjs.map +1 -0
- package/dist/Button/index.d.cts +65 -0
- package/dist/Button/index.d.ts +65 -0
- package/dist/Button/index.js +109 -0
- package/dist/Button/index.js.map +1 -0
- package/dist/Button/styles.css +181 -0
- package/dist/Checkbox/index.cjs +83 -0
- package/dist/Checkbox/index.cjs.map +1 -0
- package/dist/Checkbox/index.d.cts +40 -0
- package/dist/Checkbox/index.d.ts +40 -0
- package/dist/Checkbox/index.js +46 -0
- package/dist/Checkbox/index.js.map +1 -0
- package/dist/Checkbox/styles.css +78 -0
- package/dist/Input/index.cjs +103 -0
- package/dist/Input/index.cjs.map +1 -0
- package/dist/Input/index.d.cts +53 -0
- package/dist/Input/index.d.ts +53 -0
- package/dist/Input/index.js +76 -0
- package/dist/Input/index.js.map +1 -0
- package/dist/Input/styles.css +135 -0
- package/dist/Modal/index.cjs +120 -0
- package/dist/Modal/index.cjs.map +1 -0
- package/dist/Modal/index.d.cts +62 -0
- package/dist/Modal/index.d.ts +62 -0
- package/dist/Modal/index.js +83 -0
- package/dist/Modal/index.js.map +1 -0
- package/dist/Modal/styles.css +229 -0
- package/dist/Radio/index.cjs +86 -0
- package/dist/Radio/index.cjs.map +1 -0
- package/dist/Radio/index.d.cts +46 -0
- package/dist/Radio/index.d.ts +46 -0
- package/dist/Radio/index.js +48 -0
- package/dist/Radio/index.js.map +1 -0
- package/dist/Radio/styles.css +76 -0
- package/dist/Select/index.cjs +116 -0
- package/dist/Select/index.cjs.map +1 -0
- package/dist/Select/index.d.cts +61 -0
- package/dist/Select/index.d.ts +61 -0
- package/dist/Select/index.js +79 -0
- package/dist/Select/index.js.map +1 -0
- package/dist/Select/styles.css +157 -0
- package/dist/Switch/index.cjs +71 -0
- package/dist/Switch/index.cjs.map +1 -0
- package/dist/Switch/index.d.cts +34 -0
- package/dist/Switch/index.d.ts +34 -0
- package/dist/Switch/index.js +34 -0
- package/dist/Switch/index.js.map +1 -0
- package/dist/Switch/styles.css +71 -0
- package/dist/Textarea/index.cjs +127 -0
- package/dist/Textarea/index.cjs.map +1 -0
- package/dist/Textarea/index.d.cts +48 -0
- package/dist/Textarea/index.d.ts +48 -0
- package/dist/Textarea/index.js +100 -0
- package/dist/Textarea/index.js.map +1 -0
- package/dist/Textarea/styles.css +95 -0
- package/dist/Toast/index.cjs +184 -0
- package/dist/Toast/index.cjs.map +1 -0
- package/dist/Toast/index.d.cts +67 -0
- package/dist/Toast/index.d.ts +67 -0
- package/dist/Toast/index.js +146 -0
- package/dist/Toast/index.js.map +1 -0
- package/dist/Toast/styles.css +122 -0
- package/dist/Tooltip/index.cjs +105 -0
- package/dist/Tooltip/index.cjs.map +1 -0
- package/dist/Tooltip/index.d.cts +65 -0
- package/dist/Tooltip/index.d.ts +65 -0
- package/dist/Tooltip/index.js +67 -0
- package/dist/Tooltip/index.js.map +1 -0
- package/dist/Tooltip/styles.css +42 -0
- package/dist/index.cjs +900 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +850 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +11 -0
- package/dist/tokens.css +232 -0
- package/dist/typography.css +104 -0
- package/llms.txt +369 -0
- package/package.json +183 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# @nurimedia/react-ui — AI 사용 명세
|
|
2
|
+
|
|
3
|
+
> 이 파일은 AI 코딩 도구(Claude 등)가 @nurimedia/react-ui를 정확히 사용하기 위한 명세다.
|
|
4
|
+
> Nurimedia의 공통 React UI 라이브러리 — 아래 컴포넌트가 필요한 UI는
|
|
5
|
+
> **직접 만들지 말고 반드시 이 라이브러리를 사용할 것.**
|
|
6
|
+
|
|
7
|
+
## 필수 설정 (앱당 1회, 루트 진입점에서)
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
// 예: app/layout.tsx 또는 src/main.tsx
|
|
11
|
+
import '@nurimedia/react-ui/tokens.css'; // 디자인 토큰 (필수)
|
|
12
|
+
import '@nurimedia/react-ui/styles.css'; // 컴포넌트 스타일 (필수)
|
|
13
|
+
import '@nurimedia/react-ui/typography.css'; // 타이포 유틸 .nuri-text-* (선택)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- 위 import는 **루트에서 한 번만**. 컴포넌트 사용처마다 CSS를 import하지 않는다.
|
|
17
|
+
- 브랜드 색은 기본값(teal `#29b79c`)이 내장되어 있어 **설정 없이 바로 동작**한다.
|
|
18
|
+
다른 브랜드 색이 필요할 때만 앱 CSS의 `:root`에서 4개 변수를 덮어쓴다:
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
:root {
|
|
22
|
+
--nuri-color-brand-default: #29b79c; /* 포인트 색 */
|
|
23
|
+
--nuri-color-brand-pressed: #1d9b83; /* 눌림 */
|
|
24
|
+
--nuri-color-brand-subtle: #e0faf4; /* 옅은 배경 */
|
|
25
|
+
--nuri-color-brand-subtle-pressed: #c3eae2; /* 옅은 배경 눌림 */
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- 폰트는 강제하지 않는다 — 앱에 설정된 font-family를 그대로 상속한다.
|
|
30
|
+
- Toast를 쓰면 루트에 `<Toaster />` 1회, Tooltip을 많이 쓰면 루트에 `<TooltipProvider>` 권장(없어도 동작).
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Toaster, TooltipProvider } from '@nurimedia/react-ui';
|
|
34
|
+
|
|
35
|
+
<TooltipProvider>
|
|
36
|
+
{children}
|
|
37
|
+
<Toaster />
|
|
38
|
+
</TooltipProvider>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 컴포넌트 목록
|
|
42
|
+
|
|
43
|
+
Button · Input · Textarea · Checkbox · RadioGroup/Radio · Switch · Select · Modal · AlertDialog · Toast · Tooltip
|
|
44
|
+
모두 `import { X } from '@nurimedia/react-ui'` 로 가져온다. 접근성(포커스 관리·ARIA·키보드)은 내장되어 있으므로 별도 처리가 필요 없다.
|
|
45
|
+
(이 목록이 이 버전이 제공하는 전부다 — 여기 없는 UI는 직접 구현해도 된다.)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Button
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { Button } from '@nurimedia/react-ui';
|
|
53
|
+
|
|
54
|
+
<Button variant="primary" size="md" onClick={onSave}>저장</Button>
|
|
55
|
+
<Button variant="outline" iconLeft={<SearchIcon />}>검색</Button>
|
|
56
|
+
<Button iconOnly aria-label="닫기" variant="ghost"><CloseIcon /></Button>
|
|
57
|
+
<Button loading>저장</Button>
|
|
58
|
+
<Button block>로그인</Button>
|
|
59
|
+
<Button asChild><a href="/about">소개</a></Button> {/* 링크를 버튼처럼 */}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Props:
|
|
63
|
+
- `variant`: `'primary' | 'secondary' | 'outline' | 'ghost'` (기본 `primary`)
|
|
64
|
+
- `size`: `'sm' | 'md' | 'lg'` — 36/42/48px (기본 `md`)
|
|
65
|
+
- `iconLeft` / `iconRight`: ReactNode — 라벨 옆 아이콘
|
|
66
|
+
- `iconOnly`: boolean — 정사각 아이콘 버튼. **`aria-label` 필수**
|
|
67
|
+
- `block`: boolean — 전체 너비
|
|
68
|
+
- `loading`: boolean — 스피너 표시 + 클릭 차단 (포커스는 유지됨)
|
|
69
|
+
- `disabled`, `type`(`'button'` 기본), `onClick` 등 HTML button 속성 전부 통과
|
|
70
|
+
- `asChild`: boolean — 자식 엘리먼트(a, Link 등)에 버튼 스타일·동작 위임
|
|
71
|
+
|
|
72
|
+
규칙:
|
|
73
|
+
- 아이콘 전용 버튼에는 반드시 `aria-label`을 지정한다.
|
|
74
|
+
- 제출 버튼은 `type="submit"`을 명시한다 (기본이 `"button"`이므로).
|
|
75
|
+
|
|
76
|
+
## Input
|
|
77
|
+
|
|
78
|
+
자체 완결형 — 라벨·힌트·에러 메시지를 내장한다. 별도 `<label>`을 만들지 않는다.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { Input } from '@nurimedia/react-ui';
|
|
82
|
+
|
|
83
|
+
<Input label="이메일" placeholder="you@example.com" block />
|
|
84
|
+
<Input label="비밀번호" type="password" description="8자 이상 입력하세요" />
|
|
85
|
+
<Input label="비밀번호" type="password" invalid errorMessage="비밀번호가 일치하지 않습니다" />
|
|
86
|
+
<Input label="검색" hideLabel placeholder="검색어 입력" iconLeft={<SearchIcon />} />
|
|
87
|
+
<Input aria-label="수량" size="sm" /> {/* 라벨 없이 쓸 땐 aria-label 필수 */}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Props:
|
|
91
|
+
- `size`: `'sm' | 'md' | 'lg'` — 36/42/48px (기본 `md`)
|
|
92
|
+
- `label`: ReactNode — 필드 위 라벨 (자동으로 input과 연결됨)
|
|
93
|
+
- `hideLabel`: boolean — 라벨을 시각적으로만 숨김 (스크린리더엔 유지)
|
|
94
|
+
- `description`: ReactNode — 평상시 힌트 (필드 아래, 회색)
|
|
95
|
+
- `invalid`: boolean — 에러 상태 (보더·배경·텍스트가 에러색)
|
|
96
|
+
- `errorMessage`: ReactNode — invalid일 때 description을 대체하는 빨간 메시지
|
|
97
|
+
- `iconLeft` / `iconRight`: ReactNode — 필드 안 어도먼트
|
|
98
|
+
- `block`: boolean — 전체 너비
|
|
99
|
+
- `value`, `onChange`, `placeholder`, `type`, `disabled` 등 HTML input 속성 전부 통과
|
|
100
|
+
|
|
101
|
+
규칙:
|
|
102
|
+
- `label`이 없으면 반드시 `aria-label`을 지정한다 (placeholder는 라벨 대체가 아니다).
|
|
103
|
+
- 에러는 `invalid` + `errorMessage`를 함께 쓴다.
|
|
104
|
+
|
|
105
|
+
## Textarea
|
|
106
|
+
|
|
107
|
+
여러 줄 텍스트 입력. Input과 동일한 라벨/힌트/에러 패턴.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { Textarea } from '@nurimedia/react-ui';
|
|
111
|
+
|
|
112
|
+
<Textarea label="문의 내용" rows={5} block />
|
|
113
|
+
<Textarea label="자기소개" description="500자 이내로 작성하세요" />
|
|
114
|
+
<Textarea label="자기소개" invalid errorMessage="글자 수를 초과했습니다" />
|
|
115
|
+
<Textarea aria-label="메모" resize="none" />
|
|
116
|
+
<Textarea label="댓글" autosize rows={3} maxRows={8} /> {/* 내용 따라 3→8줄 확장 후 스크롤 */}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Props:
|
|
120
|
+
- `label` / `hideLabel` / `description` / `invalid` / `errorMessage` / `block` — Input과 동일
|
|
121
|
+
- `rows`: number — 표시 줄 수 (기본 3). size prop은 없다 — 높이는 rows로 제어
|
|
122
|
+
- `resize`: `'vertical'(기본) | 'none'` — 세로 크기 조절 핸들
|
|
123
|
+
- `autosize`: boolean — 내용에 따라 높이 자동 확장 (`rows`가 최소). 켜면 resize 핸들은 무시됨
|
|
124
|
+
- `maxRows`: number — autosize의 최대 줄 수, 넘으면 스크롤 (미지정 시 무제한)
|
|
125
|
+
- `value`, `onChange`, `placeholder`, `disabled` 등 HTML textarea 속성 전부 통과
|
|
126
|
+
|
|
127
|
+
규칙: `label`이 없으면 반드시 `aria-label`을 지정한다.
|
|
128
|
+
|
|
129
|
+
## Checkbox
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { Checkbox } from '@nurimedia/react-ui';
|
|
133
|
+
|
|
134
|
+
<Checkbox label="이용약관에 동의합니다" checked={agreed} onCheckedChange={setAgreed} />
|
|
135
|
+
<Checkbox label="전체 선택" checked={some ? 'indeterminate' : all} onCheckedChange={…} />
|
|
136
|
+
<Checkbox aria-label="행 선택" /> {/* 라벨 없이 — aria-label 필수 */}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Props:
|
|
140
|
+
- `label`: ReactNode — 우측 라벨 (클릭으로도 토글). 없으면 `aria-label` 필수
|
|
141
|
+
- `size`: `'md'(16px, 기본) | 'sm'(12px)`
|
|
142
|
+
- `checked`: `boolean | 'indeterminate'` / `defaultChecked` / `onCheckedChange`
|
|
143
|
+
- `disabled`, `name`(폼 참여), `value`, `required`
|
|
144
|
+
|
|
145
|
+
여러 개를 배열 하나로 관리 (다중 선택):
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
149
|
+
|
|
150
|
+
{TOPICS.map((t) => (
|
|
151
|
+
<Checkbox
|
|
152
|
+
key={t.value}
|
|
153
|
+
label={t.label}
|
|
154
|
+
checked={selected.includes(t.value)}
|
|
155
|
+
onCheckedChange={(checked) =>
|
|
156
|
+
setSelected((prev) =>
|
|
157
|
+
checked === true ? [...prev, t.value] : prev.filter((v) => v !== t.value),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
전체 선택 헤더는 `selected.length`로 파생: 모두 켜짐 `true` / 모두 꺼짐 `false` / 일부만 `'indeterminate'`.
|
|
165
|
+
|
|
166
|
+
## RadioGroup / Radio
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { RadioGroup, Radio } from '@nurimedia/react-ui';
|
|
170
|
+
|
|
171
|
+
<RadioGroup aria-label="정렬" value={sort} onValueChange={setSort}>
|
|
172
|
+
<Radio value="latest" label="최신순" />
|
|
173
|
+
<Radio value="edited" label="수정순" />
|
|
174
|
+
<Radio value="author" label="저자이름순" disabled />
|
|
175
|
+
</RadioGroup>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `RadioGroup`: `value`/`defaultValue`/`onValueChange`, `orientation`(`'vertical'(기본)|'horizontal'`),
|
|
179
|
+
`disabled`, `name`, `required`. **그룹에 `aria-label` 필수** (시각 제목이 없을 때)
|
|
180
|
+
- `Radio`: `value`(필수), `label`, `disabled` — 반드시 RadioGroup 안에서 사용
|
|
181
|
+
- 화살표 키 그룹 탐색·단일 선택은 자동
|
|
182
|
+
|
|
183
|
+
## Switch
|
|
184
|
+
|
|
185
|
+
즉시 적용되는 켬/끔 설정용 토글 (제출이 필요한 선택은 Checkbox 사용).
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { Switch } from '@nurimedia/react-ui';
|
|
189
|
+
|
|
190
|
+
<Switch label="알림 받기" checked={on} onCheckedChange={setOn} />
|
|
191
|
+
<Switch aria-label="다크 모드" /> {/* 라벨 없이 — aria-label 필수 */}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Props: `label` / `checked` / `defaultChecked` / `onCheckedChange` / `disabled` / `name` / `value` / `required`
|
|
195
|
+
|
|
196
|
+
## Select
|
|
197
|
+
|
|
198
|
+
단일 선택 드롭다운. **다중 선택은 지원하지 않는다.**
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { Select } from '@nurimedia/react-ui';
|
|
202
|
+
|
|
203
|
+
<Select
|
|
204
|
+
aria-label="정렬"
|
|
205
|
+
placeholder="정렬 선택"
|
|
206
|
+
options={[
|
|
207
|
+
{ value: 'latest', label: '최신순' },
|
|
208
|
+
{ value: 'edited', label: '수정순' },
|
|
209
|
+
{ value: 'author', label: '저자이름순', disabled: true },
|
|
210
|
+
]}
|
|
211
|
+
onValueChange={(v) => setSort(v)}
|
|
212
|
+
/>
|
|
213
|
+
<Select aria-label="연도" options={YEARS} defaultValue="2026" size="sm" />
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Props:
|
|
217
|
+
- `options`: `{ value: string; label: ReactNode; disabled?: boolean }[]` (필수)
|
|
218
|
+
- `size`: `'sm' | 'md'` — 36/42px (기본 `md`) ※ lg 없음
|
|
219
|
+
- `placeholder`: ReactNode — 값 없을 때 표시
|
|
220
|
+
- `value` / `defaultValue` / `onValueChange(value: string)` — 제어/비제어
|
|
221
|
+
- `disabled`, `block`, `name`(폼 전송용), `required`
|
|
222
|
+
- `aria-label`: 시각 라벨이 없을 때 필수
|
|
223
|
+
|
|
224
|
+
키보드(화살표·Enter·Esc·글자 입력으로 옵션 점프)는 자동 지원된다.
|
|
225
|
+
|
|
226
|
+
## Modal
|
|
227
|
+
|
|
228
|
+
compound 컴포넌트 — 슬롯을 조합해 구성한다. 포커스 트랩·Esc·스크롤 락·포커스 복귀 자동.
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { Modal, Button } from '@nurimedia/react-ui';
|
|
232
|
+
|
|
233
|
+
// 비제어 (트리거 방식)
|
|
234
|
+
<Modal>
|
|
235
|
+
<Modal.Trigger asChild><Button>열기</Button></Modal.Trigger>
|
|
236
|
+
<Modal.Content size="md">
|
|
237
|
+
<Modal.Header>
|
|
238
|
+
<Modal.Title>제목</Modal.Title>
|
|
239
|
+
<Modal.Description>설명 (선택)</Modal.Description>
|
|
240
|
+
<Modal.CloseButton />
|
|
241
|
+
</Modal.Header>
|
|
242
|
+
<Modal.Body>본문 내용</Modal.Body>
|
|
243
|
+
<Modal.Footer align="end">
|
|
244
|
+
<Modal.Close asChild><Button variant="outline">취소</Button></Modal.Close>
|
|
245
|
+
<Button onClick={onConfirm}>확인</Button>
|
|
246
|
+
</Modal.Footer>
|
|
247
|
+
</Modal.Content>
|
|
248
|
+
</Modal>
|
|
249
|
+
|
|
250
|
+
// 제어형
|
|
251
|
+
<Modal open={open} onOpenChange={setOpen}>
|
|
252
|
+
<Modal.Content>…</Modal.Content>
|
|
253
|
+
</Modal>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
슬롯:
|
|
257
|
+
- `Modal` 루트: `open`/`onOpenChange`(제어형) 또는 `Modal.Trigger`(비제어)
|
|
258
|
+
- `Modal.Content`: `placement`(`'center'`(기본)`|'bottom'` 바텀시트), `size`(`'sm'|'md'|'lg'` — 360/480/640px), `overlay`(기본 true)
|
|
259
|
+
- `Modal.Header` / `Modal.Body`(스크롤 가능) / `Modal.Footer`(`align`: `'start'|'center'|'end'(기본)|'between'|'full'`)
|
|
260
|
+
- `Modal.Title` / `Modal.Description` — 스크린리더 자동 연결
|
|
261
|
+
- `Modal.Close`(asChild) / `Modal.CloseButton`(우상단 X)
|
|
262
|
+
|
|
263
|
+
규칙 (중요):
|
|
264
|
+
- **`Modal.Title`은 반드시 포함한다.** 시각적 제목이 없으면 `<Modal.Title visuallyHidden>이름</Modal.Title>`.
|
|
265
|
+
- 모바일 바텀시트가 필요하면 앱에서 뷰포트를 판단해 `placement`를 바꾼다.
|
|
266
|
+
|
|
267
|
+
## AlertDialog
|
|
268
|
+
|
|
269
|
+
확인을 강제하는 다이얼로그 (바깥 클릭으로 닫히지 않음, 기본 포커스가 '취소').
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import { AlertDialog, Button } from '@nurimedia/react-ui';
|
|
273
|
+
|
|
274
|
+
<AlertDialog>
|
|
275
|
+
<AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>
|
|
276
|
+
<AlertDialog.Content tone="warning">
|
|
277
|
+
<AlertDialog.Header>
|
|
278
|
+
<AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>
|
|
279
|
+
<AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>
|
|
280
|
+
</AlertDialog.Header>
|
|
281
|
+
<AlertDialog.Footer>
|
|
282
|
+
<AlertDialog.Cancel asChild><Button variant="secondary">취소</Button></AlertDialog.Cancel>
|
|
283
|
+
<AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>
|
|
284
|
+
</AlertDialog.Footer>
|
|
285
|
+
</AlertDialog.Content>
|
|
286
|
+
</AlertDialog>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
- `AlertDialog.Content`: `tone`: `'info'(기본) | 'success' | 'warning'` — 상단 아이콘·색 결정.
|
|
290
|
+
**`danger` 톤은 존재하지 않는다** (디자인 시스템에 없음 — 파괴적 확인도 `warning` 사용).
|
|
291
|
+
- `icon`: 미지정=톤 기본 아이콘 / ReactNode=교체 / `null`=제거
|
|
292
|
+
- 슬롯: Header/Title/Description/Body/Footer(기본 `align="full"` 전체너비 버튼)/Action/Cancel/CloseButton
|
|
293
|
+
- 알림형(버튼 1개)은 Footer에 `Action`만 넣는다.
|
|
294
|
+
|
|
295
|
+
## Toast
|
|
296
|
+
|
|
297
|
+
전역 알림. **`<Toaster />`를 앱 루트에 1회만** 마운트하고, 어디서든 `toast()`를 호출한다.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// 루트 (1회)
|
|
301
|
+
import { Toaster } from '@nurimedia/react-ui';
|
|
302
|
+
<body>{children}<Toaster /></body>
|
|
303
|
+
|
|
304
|
+
// 아무 곳에서나 (컴포넌트, 이벤트 핸들러, API 레이어)
|
|
305
|
+
import { toast } from '@nurimedia/react-ui';
|
|
306
|
+
|
|
307
|
+
toast('기본 메시지');
|
|
308
|
+
toast.success('저장됐습니다');
|
|
309
|
+
toast.error('오류가 발생했습니다'); // 스크린리더가 즉시 낭독
|
|
310
|
+
toast.info('참고해 주세요');
|
|
311
|
+
const id = toast('진행 중…', { duration: 10000 });
|
|
312
|
+
toast.dismiss(id); // 특정 토스트 닫기 (id 없으면 전체)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- 옵션: `{ tone?, duration?(기본 3000ms), id? }`
|
|
316
|
+
- 사용처마다 컴포넌트를 렌더하지 않는다 — 함수 호출만 한다.
|
|
317
|
+
- `<Toaster />`를 여러 번 마운트하지 않는다.
|
|
318
|
+
- 아래로 스와이프해 닫기·hover 시 타이머 정지·F8 키보드 접근이 내장되어 있다.
|
|
319
|
+
|
|
320
|
+
## Tooltip
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { Tooltip } from '@nurimedia/react-ui';
|
|
324
|
+
|
|
325
|
+
<Tooltip content="즐겨찾기에 추가">
|
|
326
|
+
<Button iconOnly aria-label="즐겨찾기">★</Button>
|
|
327
|
+
</Tooltip>
|
|
328
|
+
|
|
329
|
+
<Tooltip content="설명" side="bottom" align="start">…</Tooltip>
|
|
330
|
+
<Tooltip content="항상 표시" open>…</Tooltip> {/* 제어형 */}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Props:
|
|
334
|
+
- `content`: ReactNode (필수) — 툴팁 내용
|
|
335
|
+
- `children`: 단일 엘리먼트 (필수) — 트리거
|
|
336
|
+
- `side`: `'top'(기본)|'bottom'|'left'|'right'` / `align`: `'start'|'center'(기본)|'end'`
|
|
337
|
+
- `arrow`: boolean (기본 true) — 꼬리 화살표
|
|
338
|
+
- `delayDuration`: number — 열림 지연 ms (기본 100)
|
|
339
|
+
- `open`/`defaultOpen`/`onOpenChange` — 제어형
|
|
340
|
+
- `disabled`: boolean — true면 툴팁 없이 트리거만 렌더
|
|
341
|
+
|
|
342
|
+
규칙:
|
|
343
|
+
- hover와 키보드 포커스 모두에서 열린다. 터치 기기에서는 표시되지 않는다 —
|
|
344
|
+
**터치에서도 필요한 정보는 툴팁에 넣지 않는다.**
|
|
345
|
+
- 툴팁 안에 버튼·링크를 넣지 않는다 (닫기 버튼 있는 안내는 툴팁 용도가 아님).
|
|
346
|
+
- 공간이 부족하면 자동으로 반대쪽으로 반전된다 — 위치 보정 코드를 쓰지 않는다.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 다국어(i18n) 앱에서
|
|
351
|
+
|
|
352
|
+
사용자에게 보이는 모든 텍스트는 props로 전달되므로 앱의 i18n(next-intl 등)이 그대로 적용된다.
|
|
353
|
+
단, **스크린리더 전용 기본 문자열 3곳은 한국어 기본값**이므로 다국어 앱은 로케일에 맞게 덮어쓴다:
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
<Modal.CloseButton aria-label={t('close')} /> {/* 기본 "닫기" */}
|
|
357
|
+
<AlertDialog.CloseButton aria-label={t('close')} /> {/* 기본 "닫기" */}
|
|
358
|
+
<Toaster label={t('notifications')} /> {/* 기본 "알림" */}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## 하지 말 것 (요약)
|
|
362
|
+
|
|
363
|
+
1. 이 문서에 나열된 컴포넌트에 해당하는 UI를 직접 구현하지 않는다 (커스텀 모달/토스트/셀렉트 금지).
|
|
364
|
+
2. `Modal.Title` 생략 금지 — 시각적으로 숨길 땐 `visuallyHidden`.
|
|
365
|
+
3. `AlertDialog`에 `tone="danger"` 없음 — `warning` 사용.
|
|
366
|
+
4. 아이콘 전용 `Button`·라벨 없는 `Input`/`Select`에 `aria-label` 누락 금지.
|
|
367
|
+
5. CSS(`tokens.css`/`styles.css`)를 사용처마다 중복 import하지 않는다 — 루트 1회.
|
|
368
|
+
6. `<Toaster />` 중복 마운트 금지.
|
|
369
|
+
7. 컴포넌트 색을 인라인 스타일로 바꾸지 않는다 — 브랜드 변경은 `:root` 변수 4개로.
|
package/package.json
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nurimedia/react-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "누리미디어 공통 헤드리스 React 컴포넌트 + 디자인 토큰 라이브러리. 브랜드 색은 앱이 CSS 변수로 주입합니다.",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/donghoonlee94/nuri-ui.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react",
|
|
16
|
+
"ui",
|
|
17
|
+
"components",
|
|
18
|
+
"headless",
|
|
19
|
+
"design-tokens",
|
|
20
|
+
"radix-ui",
|
|
21
|
+
"accessibility"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs"
|
|
35
|
+
},
|
|
36
|
+
"./Button": {
|
|
37
|
+
"types": "./dist/Button/index.d.ts",
|
|
38
|
+
"import": "./dist/Button/index.js",
|
|
39
|
+
"require": "./dist/Button/index.cjs"
|
|
40
|
+
},
|
|
41
|
+
"./Button/styles.css": "./dist/Button/styles.css",
|
|
42
|
+
"./Input": {
|
|
43
|
+
"types": "./dist/Input/index.d.ts",
|
|
44
|
+
"import": "./dist/Input/index.js",
|
|
45
|
+
"require": "./dist/Input/index.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./Input/styles.css": "./dist/Input/styles.css",
|
|
48
|
+
"./Textarea": {
|
|
49
|
+
"types": "./dist/Textarea/index.d.ts",
|
|
50
|
+
"import": "./dist/Textarea/index.js",
|
|
51
|
+
"require": "./dist/Textarea/index.cjs"
|
|
52
|
+
},
|
|
53
|
+
"./Textarea/styles.css": "./dist/Textarea/styles.css",
|
|
54
|
+
"./Checkbox": {
|
|
55
|
+
"types": "./dist/Checkbox/index.d.ts",
|
|
56
|
+
"import": "./dist/Checkbox/index.js",
|
|
57
|
+
"require": "./dist/Checkbox/index.cjs"
|
|
58
|
+
},
|
|
59
|
+
"./Checkbox/styles.css": "./dist/Checkbox/styles.css",
|
|
60
|
+
"./Radio": {
|
|
61
|
+
"types": "./dist/Radio/index.d.ts",
|
|
62
|
+
"import": "./dist/Radio/index.js",
|
|
63
|
+
"require": "./dist/Radio/index.cjs"
|
|
64
|
+
},
|
|
65
|
+
"./Radio/styles.css": "./dist/Radio/styles.css",
|
|
66
|
+
"./Switch": {
|
|
67
|
+
"types": "./dist/Switch/index.d.ts",
|
|
68
|
+
"import": "./dist/Switch/index.js",
|
|
69
|
+
"require": "./dist/Switch/index.cjs"
|
|
70
|
+
},
|
|
71
|
+
"./Switch/styles.css": "./dist/Switch/styles.css",
|
|
72
|
+
"./Modal": {
|
|
73
|
+
"types": "./dist/Modal/index.d.ts",
|
|
74
|
+
"import": "./dist/Modal/index.js",
|
|
75
|
+
"require": "./dist/Modal/index.cjs"
|
|
76
|
+
},
|
|
77
|
+
"./Modal/styles.css": "./dist/Modal/styles.css",
|
|
78
|
+
"./AlertDialog": {
|
|
79
|
+
"types": "./dist/AlertDialog/index.d.ts",
|
|
80
|
+
"import": "./dist/AlertDialog/index.js",
|
|
81
|
+
"require": "./dist/AlertDialog/index.cjs"
|
|
82
|
+
},
|
|
83
|
+
"./AlertDialog/styles.css": "./dist/AlertDialog/styles.css",
|
|
84
|
+
"./Select": {
|
|
85
|
+
"types": "./dist/Select/index.d.ts",
|
|
86
|
+
"import": "./dist/Select/index.js",
|
|
87
|
+
"require": "./dist/Select/index.cjs"
|
|
88
|
+
},
|
|
89
|
+
"./Select/styles.css": "./dist/Select/styles.css",
|
|
90
|
+
"./Toast": {
|
|
91
|
+
"types": "./dist/Toast/index.d.ts",
|
|
92
|
+
"import": "./dist/Toast/index.js",
|
|
93
|
+
"require": "./dist/Toast/index.cjs"
|
|
94
|
+
},
|
|
95
|
+
"./Toast/styles.css": "./dist/Toast/styles.css",
|
|
96
|
+
"./Tooltip": {
|
|
97
|
+
"types": "./dist/Tooltip/index.d.ts",
|
|
98
|
+
"import": "./dist/Tooltip/index.js",
|
|
99
|
+
"require": "./dist/Tooltip/index.cjs"
|
|
100
|
+
},
|
|
101
|
+
"./Tooltip/styles.css": "./dist/Tooltip/styles.css",
|
|
102
|
+
"./styles.css": "./dist/styles.css",
|
|
103
|
+
"./tokens.css": "./dist/tokens.css",
|
|
104
|
+
"./typography.css": "./dist/typography.css"
|
|
105
|
+
},
|
|
106
|
+
"files": [
|
|
107
|
+
"dist",
|
|
108
|
+
"llms.txt"
|
|
109
|
+
],
|
|
110
|
+
"engines": {
|
|
111
|
+
"node": ">=20"
|
|
112
|
+
},
|
|
113
|
+
"packageManager": "pnpm@10.8.0",
|
|
114
|
+
"scripts": {
|
|
115
|
+
"build": "node scripts/build-tokens.mjs && tsup",
|
|
116
|
+
"prepublishOnly": "pnpm build",
|
|
117
|
+
"dev": "node scripts/build-tokens.mjs && tsup --watch",
|
|
118
|
+
"test": "vitest run",
|
|
119
|
+
"test:watch": "vitest",
|
|
120
|
+
"typecheck": "tsc --noEmit",
|
|
121
|
+
"lint": "eslint .",
|
|
122
|
+
"lint:fix": "eslint . --fix",
|
|
123
|
+
"format": "prettier --write \"**/*.{ts,tsx,css}\"",
|
|
124
|
+
"format:check": "prettier --check \"**/*.{ts,tsx,css}\"",
|
|
125
|
+
"storybook": "node scripts/build-tokens.mjs && storybook dev -p 6006 --ci",
|
|
126
|
+
"build-storybook": "node scripts/build-tokens.mjs && storybook build",
|
|
127
|
+
"chromatic": "chromatic --build-script-name build-storybook --exit-zero-on-changes",
|
|
128
|
+
"clean": "rm -rf dist .turbo storybook-static"
|
|
129
|
+
},
|
|
130
|
+
"peerDependencies": {
|
|
131
|
+
"react": ">=18",
|
|
132
|
+
"react-dom": ">=18"
|
|
133
|
+
},
|
|
134
|
+
"dependencies": {
|
|
135
|
+
"@radix-ui/react-alert-dialog": "^1.1.6",
|
|
136
|
+
"@radix-ui/react-checkbox": "^1.3.7",
|
|
137
|
+
"@radix-ui/react-dialog": "^1.1.6",
|
|
138
|
+
"@radix-ui/react-radio-group": "^1.4.3",
|
|
139
|
+
"@radix-ui/react-select": "^2.3.2",
|
|
140
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
141
|
+
"@radix-ui/react-switch": "^1.3.3",
|
|
142
|
+
"@radix-ui/react-toast": "^1.2.18",
|
|
143
|
+
"@radix-ui/react-tooltip": "^1.2.11"
|
|
144
|
+
},
|
|
145
|
+
"devDependencies": {
|
|
146
|
+
"@eslint/js": "^9.39.4",
|
|
147
|
+
"@storybook/addon-a11y": "^10",
|
|
148
|
+
"@storybook/addon-docs": "^10",
|
|
149
|
+
"@storybook/react-vite": "^10",
|
|
150
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
151
|
+
"@testing-library/react": "^16.3.2",
|
|
152
|
+
"@testing-library/user-event": "^14.6.1",
|
|
153
|
+
"@types/node": "^25.9.3",
|
|
154
|
+
"@types/react": "^18.3.17",
|
|
155
|
+
"@types/react-dom": "^18.3.5",
|
|
156
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
157
|
+
"chromatic": "^18.0.0",
|
|
158
|
+
"eslint": "^9.39.4",
|
|
159
|
+
"eslint-config-prettier": "^10.1.8",
|
|
160
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
161
|
+
"eslint-plugin-react": "^7.37.5",
|
|
162
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
163
|
+
"globals": "^17.7.0",
|
|
164
|
+
"jsdom": "^29.1.1",
|
|
165
|
+
"prettier": "^3.9.4",
|
|
166
|
+
"react": "^18.3.1",
|
|
167
|
+
"react-dom": "^18.3.1",
|
|
168
|
+
"storybook": "^10",
|
|
169
|
+
"style-dictionary": "^4.3.0",
|
|
170
|
+
"tsup": "^8.3.5",
|
|
171
|
+
"typescript": "^5.7.2",
|
|
172
|
+
"typescript-eslint": "^8.62.1",
|
|
173
|
+
"vite": "^6.0.5",
|
|
174
|
+
"vitest": "^4.1.9"
|
|
175
|
+
},
|
|
176
|
+
"pnpm": {
|
|
177
|
+
"onlyBuiltDependencies": [
|
|
178
|
+
"@bundled-es-modules/glob",
|
|
179
|
+
"esbuild",
|
|
180
|
+
"style-dictionary"
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
}
|