@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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/dist/AlertDialog/index.cjs +185 -0
  4. package/dist/AlertDialog/index.cjs.map +1 -0
  5. package/dist/AlertDialog/index.d.cts +49 -0
  6. package/dist/AlertDialog/index.d.ts +49 -0
  7. package/dist/AlertDialog/index.js +148 -0
  8. package/dist/AlertDialog/index.js.map +1 -0
  9. package/dist/AlertDialog/styles.css +187 -0
  10. package/dist/Button/index.cjs +136 -0
  11. package/dist/Button/index.cjs.map +1 -0
  12. package/dist/Button/index.d.cts +65 -0
  13. package/dist/Button/index.d.ts +65 -0
  14. package/dist/Button/index.js +109 -0
  15. package/dist/Button/index.js.map +1 -0
  16. package/dist/Button/styles.css +181 -0
  17. package/dist/Checkbox/index.cjs +83 -0
  18. package/dist/Checkbox/index.cjs.map +1 -0
  19. package/dist/Checkbox/index.d.cts +40 -0
  20. package/dist/Checkbox/index.d.ts +40 -0
  21. package/dist/Checkbox/index.js +46 -0
  22. package/dist/Checkbox/index.js.map +1 -0
  23. package/dist/Checkbox/styles.css +78 -0
  24. package/dist/Input/index.cjs +103 -0
  25. package/dist/Input/index.cjs.map +1 -0
  26. package/dist/Input/index.d.cts +53 -0
  27. package/dist/Input/index.d.ts +53 -0
  28. package/dist/Input/index.js +76 -0
  29. package/dist/Input/index.js.map +1 -0
  30. package/dist/Input/styles.css +135 -0
  31. package/dist/Modal/index.cjs +120 -0
  32. package/dist/Modal/index.cjs.map +1 -0
  33. package/dist/Modal/index.d.cts +62 -0
  34. package/dist/Modal/index.d.ts +62 -0
  35. package/dist/Modal/index.js +83 -0
  36. package/dist/Modal/index.js.map +1 -0
  37. package/dist/Modal/styles.css +229 -0
  38. package/dist/Radio/index.cjs +86 -0
  39. package/dist/Radio/index.cjs.map +1 -0
  40. package/dist/Radio/index.d.cts +46 -0
  41. package/dist/Radio/index.d.ts +46 -0
  42. package/dist/Radio/index.js +48 -0
  43. package/dist/Radio/index.js.map +1 -0
  44. package/dist/Radio/styles.css +76 -0
  45. package/dist/Select/index.cjs +116 -0
  46. package/dist/Select/index.cjs.map +1 -0
  47. package/dist/Select/index.d.cts +61 -0
  48. package/dist/Select/index.d.ts +61 -0
  49. package/dist/Select/index.js +79 -0
  50. package/dist/Select/index.js.map +1 -0
  51. package/dist/Select/styles.css +157 -0
  52. package/dist/Switch/index.cjs +71 -0
  53. package/dist/Switch/index.cjs.map +1 -0
  54. package/dist/Switch/index.d.cts +34 -0
  55. package/dist/Switch/index.d.ts +34 -0
  56. package/dist/Switch/index.js +34 -0
  57. package/dist/Switch/index.js.map +1 -0
  58. package/dist/Switch/styles.css +71 -0
  59. package/dist/Textarea/index.cjs +127 -0
  60. package/dist/Textarea/index.cjs.map +1 -0
  61. package/dist/Textarea/index.d.cts +48 -0
  62. package/dist/Textarea/index.d.ts +48 -0
  63. package/dist/Textarea/index.js +100 -0
  64. package/dist/Textarea/index.js.map +1 -0
  65. package/dist/Textarea/styles.css +95 -0
  66. package/dist/Toast/index.cjs +184 -0
  67. package/dist/Toast/index.cjs.map +1 -0
  68. package/dist/Toast/index.d.cts +67 -0
  69. package/dist/Toast/index.d.ts +67 -0
  70. package/dist/Toast/index.js +146 -0
  71. package/dist/Toast/index.js.map +1 -0
  72. package/dist/Toast/styles.css +122 -0
  73. package/dist/Tooltip/index.cjs +105 -0
  74. package/dist/Tooltip/index.cjs.map +1 -0
  75. package/dist/Tooltip/index.d.cts +65 -0
  76. package/dist/Tooltip/index.d.ts +65 -0
  77. package/dist/Tooltip/index.js +67 -0
  78. package/dist/Tooltip/index.js.map +1 -0
  79. package/dist/Tooltip/styles.css +42 -0
  80. package/dist/index.cjs +900 -0
  81. package/dist/index.cjs.map +1 -0
  82. package/dist/index.d.cts +16 -0
  83. package/dist/index.d.ts +16 -0
  84. package/dist/index.js +850 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/styles.css +11 -0
  87. package/dist/tokens.css +232 -0
  88. package/dist/typography.css +104 -0
  89. package/llms.txt +369 -0
  90. 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
+ }