@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nurimedia Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# @nurimedia/react-ui
|
|
2
|
+
|
|
3
|
+
누리미디어의 헤드리스 React 컴포넌트 + 디자인 토큰 라이브러리. **브랜드 색은 앱이 주입**합니다.
|
|
4
|
+
|
|
5
|
+
- Radix Primitives 기반 — 키보드·포커스·ARIA 등 웹 접근성 내장
|
|
6
|
+
- 컴포넌트 11종: Button · Input · Textarea · Checkbox · Radio · Switch · Select · Modal · AlertDialog · Toast · Tooltip
|
|
7
|
+
- AI 도구용 사용 명세(`llms.txt`) 동봉 — [AI 도구와 함께 쓰기](#ai-도구와-함께-쓰기-claude-등)
|
|
8
|
+
|
|
9
|
+
## 설치
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @nurimedia/react-ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 사용
|
|
16
|
+
|
|
17
|
+
소비 앱의 진입점(루트 레이아웃)에서 **한 번** import:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import '@nurimedia/react-ui/tokens.css'; // base 토큰 — 중립색·여백·radius·컴포넌트 변수 (브랜드 색 제외)
|
|
21
|
+
import '@nurimedia/react-ui/styles.css'; // 전체 컴포넌트 CSS
|
|
22
|
+
import '@nurimedia/react-ui/typography.css'; // (선택) 타이포 유틸 클래스 .nuri-text-*
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
컴포넌트는 어디서나 import — 각 컴포넌트 CSS가 side-effect로 자동 따라옵니다 (`styles.css`를 받았다면 중복 없이 처리):
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { Button } from '@nurimedia/react-ui';
|
|
29
|
+
|
|
30
|
+
<Button variant="primary">저장</Button>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **브랜드 색은 라이브러리에 없습니다(헤드리스).** `tokens.css`의 컴포넌트 토큰은 `var(--nuri-color-brand-*)`만 참조하고, 그 값은 앱이 정의합니다 → 아래 [테마 적용](#테마-적용).
|
|
34
|
+
|
|
35
|
+
## 테마 적용
|
|
36
|
+
|
|
37
|
+
테마 = **브랜드 변수 4개**. @nurimedia/react-ui는 이 값을 갖지 않으므로 **앱이 정의**합니다.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
--nuri-color-brand-default /* 포인트 색 (버튼 primary·text.point·포커스링) */
|
|
41
|
+
--nuri-color-brand-pressed /* 눌림 */
|
|
42
|
+
--nuri-color-brand-subtle /* 옅은 배경 (secondary 버튼 등) */
|
|
43
|
+
--nuri-color-brand-subtle-pressed /* 옅은 배경 눌림 */
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`globals.css`에 직접 작성합니다.
|
|
47
|
+
|
|
48
|
+
앱마다 **단일 테마** — `:root`에 변수 4개만 정의하면 끝입니다. `ThemeProvider`도 `data-theme`도 필요 없어요.
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
/* 앱 globals.css */
|
|
52
|
+
:root {
|
|
53
|
+
--nuri-color-brand-default: #29b79c; /* 앱의 포인트 색 */
|
|
54
|
+
--nuri-color-brand-pressed: #1d9b83;
|
|
55
|
+
--nuri-color-brand-subtle: #e0faf4;
|
|
56
|
+
--nuri-color-brand-subtle-pressed: #c3eae2;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
앱이 여러 개라면 각 앱이 자기 색을 `:root`에 정의합니다 — 서로 다른 앱이므로 충돌하지 않습니다. 변수를 정의하지 않으면 라이브러리 기본색(teal)이 적용됩니다.
|
|
61
|
+
|
|
62
|
+
모달 등 포털되는 컴포넌트도 `:root` 변수를 상속하므로 색이 자동으로 따라옵니다 — 별도 설정 불필요.
|
|
63
|
+
|
|
64
|
+
> CSS import는 **앱당 1회**(루트 레이아웃). 컴포넌트는 아무 데서나 `import { Button }` — 사용처마다 import하지 않습니다.
|
|
65
|
+
>
|
|
66
|
+
> 한 앱 안에서 영역별로 색을 달리하거나 런타임에 색이 바뀌어야 하면 `[data-theme]` 스코프로 변수를 재정의하고 포털 컨테이너를 스코프 안에 두면 됩니다.
|
|
67
|
+
|
|
68
|
+
## subpath exports
|
|
69
|
+
|
|
70
|
+
| 경로 | 내용 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `@nurimedia/react-ui` | 모든 컴포넌트 (tree-shakable) |
|
|
73
|
+
| `@nurimedia/react-ui/Button` `…/Input` `…/Textarea` `…/Checkbox` `…/Radio` `…/Switch` `…/Select` `…/Modal` `…/AlertDialog` `…/Toast` `…/Tooltip` | 개별 컴포넌트 (명시 import) |
|
|
74
|
+
| `@nurimedia/react-ui/styles.css` | 전체 컴포넌트 CSS 번들 |
|
|
75
|
+
| `@nurimedia/react-ui/Button/styles.css` 등 | 개별 컴포넌트 CSS |
|
|
76
|
+
| `@nurimedia/react-ui/tokens.css` | base 토큰 — 중립색·구조 + 브랜드 var 슬롯 (브랜드 값 미포함) |
|
|
77
|
+
| `@nurimedia/react-ui/typography.css` | 타이포 유틸 클래스 `.nuri-text-*` |
|
|
78
|
+
|
|
79
|
+
브랜드 색은 라이브러리가 export하지 않습니다 — 앱이 `--nuri-color-brand-*`를 정의합니다([테마 적용](#테마-적용)).
|
|
80
|
+
|
|
81
|
+
## Button
|
|
82
|
+
|
|
83
|
+
### Props
|
|
84
|
+
|
|
85
|
+
| prop | type | 기본값 | 설명 |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| `variant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost'` | `'primary'` | 시각 유형 |
|
|
88
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 36 / 42 / 48 px 높이 |
|
|
89
|
+
| `iconLeft` | `ReactNode` | — | 라벨 좌측 아이콘 |
|
|
90
|
+
| `iconRight` | `ReactNode` | — | 라벨 우측 아이콘 |
|
|
91
|
+
| `iconOnly` | `boolean` | `false` | 정사각형 아이콘 전용. **`aria-label` 필수** |
|
|
92
|
+
| `block` | `boolean` | `false` | 컨테이너 전체너비 |
|
|
93
|
+
| `loading` | `boolean` | `false` | 스피너 + 클릭 차단 + `aria-busy` |
|
|
94
|
+
| `disabled` | `boolean` | `false` | 클릭 차단 |
|
|
95
|
+
| `asChild` | `boolean` | `false` | Radix Slot — 자식 엘리먼트가 Button props 받음 |
|
|
96
|
+
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | 폼 동작 |
|
|
97
|
+
| 그 외 | `ButtonHTMLAttributes` | — | `onClick`, `aria-*`, `data-*` 등 모두 통과 |
|
|
98
|
+
|
|
99
|
+
### 예시
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<Button variant="primary" size="md">저장</Button>
|
|
103
|
+
<Button variant="outline" iconLeft={<SearchIcon />}>검색</Button>
|
|
104
|
+
<Button iconOnly aria-label="닫기" variant="ghost"><CloseIcon /></Button>
|
|
105
|
+
<Button loading>저장</Button>
|
|
106
|
+
<Button block>로그인</Button>
|
|
107
|
+
<Button asChild><Link href="/about">소개</Link></Button>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Modal
|
|
111
|
+
|
|
112
|
+
Radix Dialog 기반 compound 컴포넌트. 슬롯을 조합해 구성하며, 포커스 트랩·Esc·스크롤 락·포커스 복귀·aria 연결은 자동.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { Modal, Button } from '@nurimedia/react-ui';
|
|
116
|
+
|
|
117
|
+
<Modal open={open} onOpenChange={setOpen}> {/* 또는 Modal.Trigger로 비제어 */}
|
|
118
|
+
<Modal.Content placement="center" size="md">
|
|
119
|
+
<Modal.Header>
|
|
120
|
+
<Modal.Title>제목</Modal.Title> {/* → aria-labelledby 자동 */}
|
|
121
|
+
<Modal.Description>설명</Modal.Description> {/* → aria-describedby 자동 */}
|
|
122
|
+
<Modal.CloseButton />
|
|
123
|
+
</Modal.Header>
|
|
124
|
+
<Modal.Body>본문 (스크롤 가능)</Modal.Body>
|
|
125
|
+
<Modal.Footer align="end">
|
|
126
|
+
<Modal.Close asChild><Button variant="outline">취소</Button></Modal.Close>
|
|
127
|
+
<Button variant="primary">확인</Button>
|
|
128
|
+
</Modal.Footer>
|
|
129
|
+
</Modal.Content>
|
|
130
|
+
</Modal>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 슬롯
|
|
134
|
+
|
|
135
|
+
| 슬롯 | 역할 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `Modal` | 루트 (`open`/`onOpenChange` 제어, 또는 비제어) |
|
|
138
|
+
| `Modal.Trigger` | 여는 트리거 (`asChild`) |
|
|
139
|
+
| `Modal.Content` | 오버레이+포털+패널. props: `placement`('center'\|'bottom'), `size`('sm'\|'md'\|'lg'), `overlay`, `container` |
|
|
140
|
+
| `Modal.Header` / `Modal.Body` / `Modal.Footer` | 레이아웃 슬롯 |
|
|
141
|
+
| `Modal.Title` / `Modal.Description` | a11y 연결되는 제목·설명 (필수 권장) |
|
|
142
|
+
| `Modal.Close` | 닫기 액션 (`asChild`) |
|
|
143
|
+
| `Modal.CloseButton` | 우상단 X (미리 스타일됨) |
|
|
144
|
+
|
|
145
|
+
`Modal.Footer`의 `align`: `end`(기본) / `start` / `center` / `between` / `full`(모바일 전체너비).
|
|
146
|
+
|
|
147
|
+
### 제목 — 항상 Modal.Title 포함 (필수)
|
|
148
|
+
|
|
149
|
+
Radix Dialog는 접근 가능한 이름을 위해 `Title`이 **필수**입니다(없으면 콘솔 에러 + 스크린리더가 모달을 식별 못 함). 시각적 제목이 없는 모달이라면 `visuallyHidden`으로 숨기되 이름은 유지하세요:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`Modal.Description`은 선택이며, 불필요하면 생략하면 됩니다.
|
|
156
|
+
|
|
157
|
+
### 초기 포커스 / 닫기 동작 커스터마이즈
|
|
158
|
+
|
|
159
|
+
`Modal.Content`는 Radix `Dialog.Content`의 props를 모두 통과시킵니다:
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<Modal.Content
|
|
163
|
+
onOpenAutoFocus={(e) => { e.preventDefault(); inputRef.current?.focus(); }} // 특정 요소에 초기 포커스
|
|
164
|
+
onInteractOutside={(e) => e.preventDefault()} // 백드롭 클릭으로 닫기 방지
|
|
165
|
+
onEscapeKeyDown={(e) => e.preventDefault()} // Esc로 닫기 방지
|
|
166
|
+
>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### CloseButton 라벨 (i18n)
|
|
170
|
+
|
|
171
|
+
`Modal.CloseButton`의 기본 `aria-label`은 `"닫기"`(한국어)입니다. 다른 로케일에선 직접 지정하세요:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<Modal.CloseButton aria-label="Close" />
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### overlay 주의
|
|
178
|
+
|
|
179
|
+
`overlay={false}`는 **시각적 백드롭만** 제거합니다. 포커스 트랩·스크롤 락 등 모달 동작은 유지됩니다(여전히 modal). 페이지와 상호작용 가능한 비모달 패널이 필요하면 Modal이 아닌 다른 패턴을 쓰세요.
|
|
180
|
+
|
|
181
|
+
### 반응형 — 바텀시트는 앱이 결정
|
|
182
|
+
|
|
183
|
+
`placement="bottom"`은 **항상** 바텀시트(풀 width)로 렌더됩니다. "모바일에서만 바텀시트"는 **소비 앱이 뷰포트를 판단**해 결정하세요 (라이브러리가 분기점을 강제하지 않음 — 프로젝트마다 브레이크포인트가 다르므로):
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
const isMobile = useMediaQuery('(max-width: 767px)'); // 앱의 훅 재사용
|
|
187
|
+
<Modal.Content placement={isMobile ? 'bottom' : 'center'}>...</Modal.Content>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### z-index 통합
|
|
191
|
+
|
|
192
|
+
모달 z-index는 토큰입니다. 앱의 헤더·토스트 등과 겹치면 조정하세요:
|
|
193
|
+
|
|
194
|
+
```css
|
|
195
|
+
:root { --nuri-modal-z-index: 5000; } /* overlay = 이 값, panel = +1 */
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 포털 위치
|
|
199
|
+
|
|
200
|
+
모달은 기본적으로 `document.body`로 포털됩니다. 단일 테마(`:root`)에선 body도 변수를 상속하므로 그대로 두면 됩니다. 특정 요소 안으로 포털하고 싶을 때만 `container`를 지정하세요.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
<Modal.Content container={someRef.current}>...</Modal.Content>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Escape hatches
|
|
207
|
+
|
|
208
|
+
ButtonHTMLAttributes를 extend하므로 HTML button이 받는 모든 속성·이벤트가 자동 통과:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<Button
|
|
212
|
+
onMouseEnter={...} onFocus={...} onKeyDown={...}
|
|
213
|
+
data-testid="save" aria-describedby="hint"
|
|
214
|
+
id="primary" name="action" form="my-form"
|
|
215
|
+
>저장</Button>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
인스턴스별 CSS 변수 override (일회성 예외):
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<Button style={{
|
|
222
|
+
['--nuri-button-primary-bg' as string]: '#ff0066',
|
|
223
|
+
}}>특별</Button>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## 디자인 시스템
|
|
227
|
+
|
|
228
|
+
컴포넌트는 CSS 변수만 참조합니다. 소유권이 둘로 나뉩니다:
|
|
229
|
+
|
|
230
|
+
- **라이브러리 (`tokens.css`)** — 중립색·여백·radius·타이포 등 **구조**. `src/tokens/{primitives,semantic}.json` 수정 → `pnpm build` → publish. *컴포넌트/구조가 바뀔 때만 버전이 오릅니다.*
|
|
231
|
+
- **앱** — 브랜드 색(`--nuri-color-brand-*`). 새 서비스·색 변경은 **앱에서** 처리하므로 라이브러리 버전과 무관합니다.
|
|
232
|
+
|
|
233
|
+
`semantic.json`이 브랜드를 참조하는 토큰은 빌드 시 `outputReferences`로 `var(--nuri-color-brand-*)`가 되어, 앱이 변수 4개만 덮으면 버튼·포인트색·포커스링이 전부 따라옵니다.
|
|
234
|
+
|
|
235
|
+
## 접근성
|
|
236
|
+
|
|
237
|
+
- `aria-disabled` / `aria-busy` 자동
|
|
238
|
+
- iconOnly aria-label 없으면 dev 경고
|
|
239
|
+
- focus-visible 시 브랜드 색 outline
|
|
240
|
+
- `asChild` 경로에서도 disabled 클릭 차단
|
|
241
|
+
- `prefers-reduced-motion` 시 transition 제거 + 스피너 감속
|
|
242
|
+
|
|
243
|
+
## AI 도구와 함께 쓰기 (Claude 등)
|
|
244
|
+
|
|
245
|
+
패키지에 **AI용 사용 명세**(`llms.txt`)가 동봉되어 있습니다 — 컴포넌트 목록·정확한 API·예시·주의사항을 AI가 읽기 좋은 형태로 담고 있어요. 설치하면 `node_modules/@nurimedia/react-ui/llms.txt`에 위치합니다.
|
|
246
|
+
|
|
247
|
+
소비 프로젝트의 AI 설정 문서(`CLAUDE.md` 등)에 아래 블록을 붙여넣으면, AI가 커스텀 UI를 새로 만들지 않고 이 라이브러리를 정확히 사용합니다:
|
|
248
|
+
|
|
249
|
+
```markdown
|
|
250
|
+
## UI 컴포넌트
|
|
251
|
+
|
|
252
|
+
- 이 프로젝트의 UI는 공통 라이브러리 **@nurimedia/react-ui**를 사용한다.
|
|
253
|
+
- 공통 UI(버튼·모달·폼 요소·알림 등)를 만들기 전에 반드시
|
|
254
|
+
`node_modules/@nurimedia/react-ui/llms.txt`에서 제공 컴포넌트를 확인하고,
|
|
255
|
+
있으면 직접 구현하지 말고 그것을 사용할 것.
|
|
256
|
+
- 사용법·API·주의사항은 llms.txt의 명세를 그대로 따를 것.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 개발
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
pnpm install
|
|
263
|
+
pnpm storybook # 컴포넌트 카탈로그 + 문서 (http://localhost:6006)
|
|
264
|
+
pnpm build # 토큰 + 컴포넌트 빌드 (dist/)
|
|
265
|
+
pnpm dev # watch 모드 (tsup)
|
|
266
|
+
pnpm test # 단위 테스트 (vitest)
|
|
267
|
+
pnpm build-storybook # 정적 Storybook 빌드
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
컴포넌트 사용법·prop·예시는 **Storybook**(`pnpm storybook`)에서 확인하세요. 상단 툴바에서 테마를 전환해 브랜드 변수 주입 결과를 비교할 수 있습니다. (Storybook의 브랜드 색은 데모용 `.storybook/demo-themes.css` — 배포물에는 포함되지 않습니다.)
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT © Nurimedia Co., Ltd.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/AlertDialog/index.ts
|
|
31
|
+
var AlertDialog_exports = {};
|
|
32
|
+
__export(AlertDialog_exports, {
|
|
33
|
+
AlertDialog: () => AlertDialog
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(AlertDialog_exports);
|
|
36
|
+
|
|
37
|
+
// src/AlertDialog/AlertDialog.tsx
|
|
38
|
+
var import_styles = require("./styles.css");
|
|
39
|
+
var Alert = __toESM(require("@radix-ui/react-alert-dialog"), 1);
|
|
40
|
+
var import_react = require("react");
|
|
41
|
+
|
|
42
|
+
// src/utils/cx.ts
|
|
43
|
+
function cx(...classes) {
|
|
44
|
+
return classes.filter(Boolean).join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/AlertDialog/icons.tsx
|
|
48
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
49
|
+
var SuccessIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
|
|
50
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "12", fill: "var(--nuri-alert-tone-success-bg)" }),
|
|
51
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
52
|
+
"path",
|
|
53
|
+
{
|
|
54
|
+
d: "M15.77 9.58L10.65 14.70L8.90 12.95",
|
|
55
|
+
stroke: "var(--nuri-alert-tone-success-fg)",
|
|
56
|
+
strokeWidth: "2.5",
|
|
57
|
+
strokeLinecap: "round",
|
|
58
|
+
strokeLinejoin: "round"
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
] });
|
|
62
|
+
var WarningIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
|
|
63
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "12", fill: "var(--nuri-alert-tone-warning-bg)" }),
|
|
64
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
65
|
+
"path",
|
|
66
|
+
{
|
|
67
|
+
d: "M12 6.75V12",
|
|
68
|
+
stroke: "var(--nuri-alert-tone-warning-fg)",
|
|
69
|
+
strokeWidth: "2.5",
|
|
70
|
+
strokeLinecap: "round"
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "16.31", r: "1.1", fill: "var(--nuri-alert-tone-warning-fg)" })
|
|
74
|
+
] });
|
|
75
|
+
var InfoIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
|
|
76
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "12", fill: "var(--nuri-alert-tone-info-bg)" }),
|
|
77
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
78
|
+
"path",
|
|
79
|
+
{
|
|
80
|
+
d: "M12.25 7h0.01",
|
|
81
|
+
stroke: "var(--nuri-alert-tone-info-fg)",
|
|
82
|
+
strokeWidth: "3",
|
|
83
|
+
strokeLinecap: "round",
|
|
84
|
+
strokeLinejoin: "round"
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
88
|
+
"path",
|
|
89
|
+
{
|
|
90
|
+
d: "M11 12h1.25v5h1.25",
|
|
91
|
+
stroke: "var(--nuri-alert-tone-info-fg)",
|
|
92
|
+
strokeWidth: "2.5",
|
|
93
|
+
strokeLinecap: "round",
|
|
94
|
+
strokeLinejoin: "round"
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] });
|
|
98
|
+
var defaultToneIcon = {
|
|
99
|
+
info: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InfoIcon, {}),
|
|
100
|
+
success: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SuccessIcon, {}),
|
|
101
|
+
warning: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WarningIcon, {})
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/AlertDialog/AlertDialog.tsx
|
|
105
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
106
|
+
var Root2 = (props) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Alert.Root, { ...props });
|
|
107
|
+
Root2.displayName = "AlertDialog";
|
|
108
|
+
var Trigger2 = Alert.Trigger;
|
|
109
|
+
var Content2 = (0, import_react.forwardRef)(
|
|
110
|
+
function AlertDialogContent({ tone = "info", icon, overlay = true, container, className, children, ...rest }, ref) {
|
|
111
|
+
const resolvedIcon = icon === void 0 ? defaultToneIcon[tone] : icon;
|
|
112
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Alert.Portal, { container, children: [
|
|
113
|
+
overlay && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Alert.Overlay, { className: "nuri-alert__overlay" }),
|
|
114
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
115
|
+
Alert.Content,
|
|
116
|
+
{
|
|
117
|
+
ref,
|
|
118
|
+
className: cx("nuri-alert", `nuri-alert--${tone}`, className),
|
|
119
|
+
...rest,
|
|
120
|
+
children: [
|
|
121
|
+
resolvedIcon != null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "nuri-alert__icon", "aria-hidden": "true", children: resolvedIcon }),
|
|
122
|
+
children
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
] });
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
var Header = (0, import_react.forwardRef)(
|
|
130
|
+
function AlertDialogHeader({ className, ...rest }, ref) {
|
|
131
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref, className: cx("nuri-alert__header", className), ...rest });
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
var Title2 = (0, import_react.forwardRef)(function AlertDialogTitle({ className, ...rest }, ref) {
|
|
135
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Alert.Title, { ref, className: cx("nuri-alert__title", className), ...rest });
|
|
136
|
+
});
|
|
137
|
+
var Description2 = (0, import_react.forwardRef)(function AlertDialogDescription({ className, ...rest }, ref) {
|
|
138
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Alert.Description, { ref, className: cx("nuri-alert__description", className), ...rest });
|
|
139
|
+
});
|
|
140
|
+
var Body = (0, import_react.forwardRef)(function AlertDialogBody({ className, ...rest }, ref) {
|
|
141
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref, className: cx("nuri-alert__body", className), ...rest });
|
|
142
|
+
});
|
|
143
|
+
var Footer = (0, import_react.forwardRef)(function AlertDialogFooter({ align = "full", className, ...rest }, ref) {
|
|
144
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
145
|
+
"div",
|
|
146
|
+
{
|
|
147
|
+
ref,
|
|
148
|
+
className: cx("nuri-alert__footer", `nuri-alert__footer--${align}`, className),
|
|
149
|
+
...rest
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
var Action2 = Alert.Action;
|
|
154
|
+
var Cancel2 = Alert.Cancel;
|
|
155
|
+
var CloseIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) });
|
|
156
|
+
var CloseButton = (0, import_react.forwardRef)(function AlertDialogCloseButton({ className, "aria-label": ariaLabel = "\uB2EB\uAE30", children, ...rest }, ref) {
|
|
157
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Alert.Cancel, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
158
|
+
"button",
|
|
159
|
+
{
|
|
160
|
+
ref,
|
|
161
|
+
type: "button",
|
|
162
|
+
"aria-label": ariaLabel,
|
|
163
|
+
className: cx("nuri-alert__close", className),
|
|
164
|
+
...rest,
|
|
165
|
+
children: children ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CloseIcon, {})
|
|
166
|
+
}
|
|
167
|
+
) });
|
|
168
|
+
});
|
|
169
|
+
var AlertDialog = Object.assign(Root2, {
|
|
170
|
+
Trigger: Trigger2,
|
|
171
|
+
Content: Content2,
|
|
172
|
+
Header,
|
|
173
|
+
Title: Title2,
|
|
174
|
+
Description: Description2,
|
|
175
|
+
Body,
|
|
176
|
+
Footer,
|
|
177
|
+
Action: Action2,
|
|
178
|
+
Cancel: Cancel2,
|
|
179
|
+
CloseButton
|
|
180
|
+
});
|
|
181
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
182
|
+
0 && (module.exports = {
|
|
183
|
+
AlertDialog
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/AlertDialog/index.ts","../../src/AlertDialog/AlertDialog.tsx","../../src/utils/cx.ts","../../src/AlertDialog/icons.tsx"],"sourcesContent":["export { AlertDialog } from './AlertDialog.js';\nexport type { AlertDialogContentProps, AlertDialogFooterProps } from './AlertDialog.js';\n","'use client';\n\nimport './styles.css';\nimport * as Alert from '@radix-ui/react-alert-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\nimport { defaultToneIcon } from './icons.js';\n\ntype Tone = 'info' | 'success' | 'warning';\n\n/**\n * 확인 강제형 다이얼로그 (alertdialog). Radix AlertDialog 기반.\n *\n * 일반 Modal과의 차이:\n * - `role=\"alertdialog\"` — 스크린리더가 더 강하게 안내\n * - **바깥(백드롭) 클릭으로 닫히지 않음** — 실수로 닫히는 것 방지\n * - 포커스가 기본적으로 Cancel(취소)에 위치 — 실수로 위험 액션 방지\n * - **중앙 정렬 + 상단 아이콘** (디자인 시스템 표준 레이아웃)\n *\n * Esc 키는 닫힘(키보드 탈출구 — 접근성). 막으려면 Content에 `onEscapeKeyDown` 전달.\n *\n * @example\n * <AlertDialog>\n * <AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>\n * <AlertDialog.Content tone=\"warning\">\n * <AlertDialog.Header>\n * <AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>\n * <AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>\n * </AlertDialog.Header>\n * <AlertDialog.Footer>\n * <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n * <AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>\n * </AlertDialog.Footer>\n * </AlertDialog.Content>\n * </AlertDialog>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Alert.Root>) => <Alert.Root {...props} />;\nRoot.displayName = 'AlertDialog';\n\n/** 다이얼로그를 여는 트리거. `asChild`로 임의 엘리먼트에 위임. */\nconst Trigger = Alert.Trigger;\n\nexport interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {\n /**\n * 상태 톤 — 기본 아이콘과 그 색을 결정.\n * @default 'info'\n */\n tone?: Tone;\n /**\n * 상단 아이콘.\n * - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더\n * - `ReactNode`: 커스텀 아이콘으로 교체\n * - `null`: 아이콘 제거\n */\n icon?: ReactNode | null;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/** 오버레이 + 포털 + 패널을 한 번에 렌더. 중앙 정렬 + 상단 아이콘 레이아웃. */\nconst Content = forwardRef<ElementRef<typeof Alert.Content>, AlertDialogContentProps>(\n function AlertDialogContent(\n { tone = 'info', icon, overlay = true, container, className, children, ...rest },\n ref,\n ) {\n // icon 미지정(undefined) → 기본 tone 아이콘 / null → 제거 / ReactNode → 교체\n const resolvedIcon = icon === undefined ? defaultToneIcon[tone] : icon;\n return (\n <Alert.Portal container={container}>\n {overlay && <Alert.Overlay className=\"nuri-alert__overlay\" />}\n <Alert.Content\n ref={ref}\n className={cx('nuri-alert', `nuri-alert--${tone}`, className)}\n {...rest}\n >\n {resolvedIcon != null && (\n <div className=\"nuri-alert__icon\" aria-hidden=\"true\">\n {resolvedIcon}\n </div>\n )}\n {children}\n </Alert.Content>\n </Alert.Portal>\n );\n },\n);\n\n/** 헤더 슬롯 (Title/Description 묶음, 중앙 정렬). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n function AlertDialogHeader({ className, ...rest }, ref) {\n return <div ref={ref} className={cx('nuri-alert__header', className)} {...rest} />;\n },\n);\n\n/** 제목. 자동으로 `aria-labelledby`로 연결됨. */\nconst Title = forwardRef<\n ElementRef<typeof Alert.Title>,\n ComponentPropsWithoutRef<typeof Alert.Title>\n>(function AlertDialogTitle({ className, ...rest }, ref) {\n return <Alert.Title ref={ref} className={cx('nuri-alert__title', className)} {...rest} />;\n});\n\n/** 설명. 자동으로 `aria-describedby`로 연결됨. */\nconst Description = forwardRef<\n ElementRef<typeof Alert.Description>,\n ComponentPropsWithoutRef<typeof Alert.Description>\n>(function AlertDialogDescription({ className, ...rest }, ref) {\n return (\n <Alert.Description ref={ref} className={cx('nuri-alert__description', className)} {...rest} />\n );\n});\n\n/** 본문 슬롯 (추가 콘텐츠). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function AlertDialogBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-alert__body', className)} {...rest} />;\n});\n\nexport interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.\n * @default 'full'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 슬롯 (Cancel/Action 배치). 기본 전체너비(`full`). */\nconst Footer = forwardRef<HTMLDivElement, AlertDialogFooterProps>(function AlertDialogFooter(\n { align = 'full', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-alert__footer', `nuri-alert__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 확인(주 액션) 버튼. 클릭 시 닫힘. `asChild`로 Button 위임.\n * @example <AlertDialog.Action asChild><Button onClick={onConfirm}>삭제</Button></AlertDialog.Action>\n */\nconst Action = Alert.Action;\n\n/**\n * 취소 버튼. 클릭 시 닫힘. 포커스가 기본적으로 여기 위치.\n * @example <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n */\nconst Cancel = Alert.Cancel;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 우상단 X 닫기 버튼 (옵션). 내부적으로 Cancel과 동일하게 닫힘. 기본 `aria-label=\"닫기\"`.\n * 확인을 강제하려면 사용하지 않는 편이 좋습니다 (Cancel/Action으로 명시적 선택 유도).\n */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function AlertDialogCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Alert.Cancel asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-alert__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Alert.Cancel>\n );\n});\n\nexport const AlertDialog = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Action,\n Cancel,\n CloseButton,\n});\n","/**\n * 조건부 className 합성 유틸. falsy 값(undefined/false/null/'')은 제거.\n * @example cx('btn', isActive && 'btn--active', className)\n */\nexport function cx(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(' ');\n}\n","/**\n * tone별 기본 아이콘 — 디자이너 시스템(Figma) 에셋을 그대로 inline SVG로 재현.\n * 구조: 틴트 원 배경 + tone 색 심볼. 색은 토큰(`--nuri-alert-tone-*`)으로 분리.\n * AlertDialog.Content의 `icon` 미지정 시 tone에 맞춰 자동 렌더.\n * `icon={<Custom/>}`로 교체, `icon={null}`로 제거 가능.\n */\nimport type { ReactNode } from 'react';\n\nconst SuccessIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-success-bg)\" />\n <path\n d=\"M15.77 9.58L10.65 14.70L8.90 12.95\"\n stroke=\"var(--nuri-alert-tone-success-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nconst WarningIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-warning-bg)\" />\n <path\n d=\"M12 6.75V12\"\n stroke=\"var(--nuri-alert-tone-warning-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"12\" cy=\"16.31\" r=\"1.1\" fill=\"var(--nuri-alert-tone-warning-fg)\" />\n </svg>\n);\n\nconst InfoIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-info-bg)\" />\n <path\n d=\"M12.25 7h0.01\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M11 12h1.25v5h1.25\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nexport const defaultToneIcon: Record<'info' | 'success' | 'warning', ReactNode> = {\n info: <InfoIcon />,\n success: <SuccessIcon />,\n warning: <WarningIcon />,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,oBAAO;AACP,YAAuB;AACvB,mBAA2B;;;ACApB,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ACGE;AADF,IAAM,cAAc,MAClB,6CAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE;AAAA,8CAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK,MAAK,qCAAoC;AAAA,EACxE;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EACjB;AAAA,GACF;AAGF,IAAM,cAAc,MAClB,6CAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE;AAAA,8CAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK,MAAK,qCAAoC;AAAA,EACxE;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA;AAAA,EAChB;AAAA,EACA,4CAAC,YAAO,IAAG,MAAK,IAAG,SAAQ,GAAE,OAAM,MAAK,qCAAoC;AAAA,GAC9E;AAGF,IAAM,WAAW,MACf,6CAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE;AAAA,8CAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK,MAAK,kCAAiC;AAAA,EACrE;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EACjB;AAAA,EACA;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EACjB;AAAA,GACF;AAGK,IAAM,kBAAqE;AAAA,EAChF,MAAM,4CAAC,YAAS;AAAA,EAChB,SAAS,4CAAC,eAAY;AAAA,EACtB,SAAS,4CAAC,eAAY;AACxB;;;AFrBqE,IAAAA,sBAAA;AAArE,IAAMC,QAAO,CAAC,UAAuD,6CAAO,YAAN,EAAY,GAAG,OAAO;AAC5FA,MAAK,cAAc;AAGnB,IAAMC,WAAgB;AAsBtB,IAAMC,eAAU;AAAA,EACd,SAAS,mBACP,EAAE,OAAO,QAAQ,MAAM,UAAU,MAAM,WAAW,WAAW,UAAU,GAAG,KAAK,GAC/E,KACA;AAEA,UAAM,eAAe,SAAS,SAAY,gBAAgB,IAAI,IAAI;AAClE,WACE,8CAAO,cAAN,EAAa,WACX;AAAA,iBAAW,6CAAO,eAAN,EAAc,WAAU,uBAAsB;AAAA,MAC3D;AAAA,QAAO;AAAA,QAAN;AAAA,UACC;AAAA,UACA,WAAW,GAAG,cAAc,eAAe,IAAI,IAAI,SAAS;AAAA,UAC3D,GAAG;AAAA,UAEH;AAAA,4BAAgB,QACf,6CAAC,SAAI,WAAU,oBAAmB,eAAY,QAC3C,wBACH;AAAA,YAED;AAAA;AAAA;AAAA,MACH;AAAA,OACF;AAAA,EAEJ;AACF;AAGA,IAAM,aAAS;AAAA,EACb,SAAS,kBAAkB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AACtD,WAAO,6CAAC,SAAI,KAAU,WAAW,GAAG,sBAAsB,SAAS,GAAI,GAAG,MAAM;AAAA,EAClF;AACF;AAGA,IAAMC,aAAQ,yBAGZ,SAAS,iBAAiB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AACvD,SAAO,6CAAO,aAAN,EAAY,KAAU,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,MAAM;AACzF,CAAC;AAGD,IAAMC,mBAAc,yBAGlB,SAAS,uBAAuB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AAC7D,SACE,6CAAO,mBAAN,EAAkB,KAAU,WAAW,GAAG,2BAA2B,SAAS,GAAI,GAAG,MAAM;AAEhG,CAAC;AAGD,IAAM,WAAO,yBAA4D,SAAS,gBAChF,EAAE,WAAW,GAAG,KAAK,GACrB,KACA;AACA,SAAO,6CAAC,SAAI,KAAU,WAAW,GAAG,oBAAoB,SAAS,GAAI,GAAG,MAAM;AAChF,CAAC;AAWD,IAAM,aAAS,yBAAmD,SAAS,kBACzE,EAAE,QAAQ,QAAQ,WAAW,GAAG,KAAK,GACrC,KACA;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,sBAAsB,uBAAuB,KAAK,IAAI,SAAS;AAAA,MAC5E,GAAG;AAAA;AAAA,EACN;AAEJ,CAAC;AAMD,IAAMC,UAAe;AAMrB,IAAMC,UAAe;AAErB,IAAM,YAAY,MAChB,6CAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE,uDAAC,UAAK,GAAE,wBAAuB,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GAC/F;AAOF,IAAM,kBAAc,yBAGlB,SAAS,uBACT,EAAE,WAAW,cAAc,YAAY,gBAAM,UAAU,GAAG,KAAK,GAC/D,KACA;AACA,SACE,6CAAO,cAAN,EAAa,SAAO,MACnB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,SAAS;AAAA,MAC3C,GAAG;AAAA,MAEH,sBAAY,6CAAC,aAAU;AAAA;AAAA,EAC1B,GACF;AAEJ,CAAC;AAEM,IAAM,cAAc,OAAO,OAAON,OAAM;AAAA,EAC7C,SAAAC;AAAA,EACA,SAAAC;AAAA,EACA;AAAA,EACA,OAAAC;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA;AACF,CAAC;","names":["import_jsx_runtime","Root","Trigger","Content","Title","Description","Action","Cancel"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import * as Alert from '@radix-ui/react-alert-dialog';
|
|
4
|
+
|
|
5
|
+
type Tone = 'info' | 'success' | 'warning';
|
|
6
|
+
interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {
|
|
7
|
+
/**
|
|
8
|
+
* 상태 톤 — 기본 아이콘과 그 색을 결정.
|
|
9
|
+
* @default 'info'
|
|
10
|
+
*/
|
|
11
|
+
tone?: Tone;
|
|
12
|
+
/**
|
|
13
|
+
* 상단 아이콘.
|
|
14
|
+
* - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더
|
|
15
|
+
* - `ReactNode`: 커스텀 아이콘으로 교체
|
|
16
|
+
* - `null`: 아이콘 제거
|
|
17
|
+
*/
|
|
18
|
+
icon?: ReactNode | null;
|
|
19
|
+
/** 백드롭(오버레이) 표시 여부. @default true */
|
|
20
|
+
overlay?: boolean;
|
|
21
|
+
/** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */
|
|
22
|
+
container?: HTMLElement | null;
|
|
23
|
+
}
|
|
24
|
+
interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {
|
|
25
|
+
/**
|
|
26
|
+
* 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.
|
|
27
|
+
* @default 'full'
|
|
28
|
+
*/
|
|
29
|
+
align?: 'start' | 'center' | 'end' | 'between' | 'full';
|
|
30
|
+
}
|
|
31
|
+
declare const AlertDialog: {
|
|
32
|
+
(props: ComponentPropsWithoutRef<typeof Alert.Root>): react.JSX.Element;
|
|
33
|
+
displayName: string;
|
|
34
|
+
} & {
|
|
35
|
+
Trigger: react.ForwardRefExoticComponent<Alert.AlertDialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
|
|
36
|
+
Content: react.ForwardRefExoticComponent<AlertDialogContentProps & react.RefAttributes<HTMLDivElement>>;
|
|
37
|
+
Header: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
38
|
+
Title: react.ForwardRefExoticComponent<Omit<Alert.AlertDialogTitleProps & react.RefAttributes<HTMLHeadingElement>, "ref"> & react.RefAttributes<HTMLHeadingElement>>;
|
|
39
|
+
Description: react.ForwardRefExoticComponent<Omit<Alert.AlertDialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>, "ref"> & react.RefAttributes<HTMLParagraphElement>>;
|
|
40
|
+
Body: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
Footer: react.ForwardRefExoticComponent<AlertDialogFooterProps & react.RefAttributes<HTMLDivElement>>;
|
|
42
|
+
Action: react.ForwardRefExoticComponent<Alert.AlertDialogActionProps & react.RefAttributes<HTMLButtonElement>>;
|
|
43
|
+
Cancel: react.ForwardRefExoticComponent<Alert.AlertDialogCancelProps & react.RefAttributes<HTMLButtonElement>>;
|
|
44
|
+
CloseButton: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref">, "children"> & {
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
} & react.RefAttributes<HTMLButtonElement>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { AlertDialog, type AlertDialogContentProps, type AlertDialogFooterProps };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import * as Alert from '@radix-ui/react-alert-dialog';
|
|
4
|
+
|
|
5
|
+
type Tone = 'info' | 'success' | 'warning';
|
|
6
|
+
interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {
|
|
7
|
+
/**
|
|
8
|
+
* 상태 톤 — 기본 아이콘과 그 색을 결정.
|
|
9
|
+
* @default 'info'
|
|
10
|
+
*/
|
|
11
|
+
tone?: Tone;
|
|
12
|
+
/**
|
|
13
|
+
* 상단 아이콘.
|
|
14
|
+
* - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더
|
|
15
|
+
* - `ReactNode`: 커스텀 아이콘으로 교체
|
|
16
|
+
* - `null`: 아이콘 제거
|
|
17
|
+
*/
|
|
18
|
+
icon?: ReactNode | null;
|
|
19
|
+
/** 백드롭(오버레이) 표시 여부. @default true */
|
|
20
|
+
overlay?: boolean;
|
|
21
|
+
/** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */
|
|
22
|
+
container?: HTMLElement | null;
|
|
23
|
+
}
|
|
24
|
+
interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {
|
|
25
|
+
/**
|
|
26
|
+
* 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.
|
|
27
|
+
* @default 'full'
|
|
28
|
+
*/
|
|
29
|
+
align?: 'start' | 'center' | 'end' | 'between' | 'full';
|
|
30
|
+
}
|
|
31
|
+
declare const AlertDialog: {
|
|
32
|
+
(props: ComponentPropsWithoutRef<typeof Alert.Root>): react.JSX.Element;
|
|
33
|
+
displayName: string;
|
|
34
|
+
} & {
|
|
35
|
+
Trigger: react.ForwardRefExoticComponent<Alert.AlertDialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
|
|
36
|
+
Content: react.ForwardRefExoticComponent<AlertDialogContentProps & react.RefAttributes<HTMLDivElement>>;
|
|
37
|
+
Header: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
38
|
+
Title: react.ForwardRefExoticComponent<Omit<Alert.AlertDialogTitleProps & react.RefAttributes<HTMLHeadingElement>, "ref"> & react.RefAttributes<HTMLHeadingElement>>;
|
|
39
|
+
Description: react.ForwardRefExoticComponent<Omit<Alert.AlertDialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>, "ref"> & react.RefAttributes<HTMLParagraphElement>>;
|
|
40
|
+
Body: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
Footer: react.ForwardRefExoticComponent<AlertDialogFooterProps & react.RefAttributes<HTMLDivElement>>;
|
|
42
|
+
Action: react.ForwardRefExoticComponent<Alert.AlertDialogActionProps & react.RefAttributes<HTMLButtonElement>>;
|
|
43
|
+
Cancel: react.ForwardRefExoticComponent<Alert.AlertDialogCancelProps & react.RefAttributes<HTMLButtonElement>>;
|
|
44
|
+
CloseButton: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref">, "children"> & {
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
} & react.RefAttributes<HTMLButtonElement>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { AlertDialog, type AlertDialogContentProps, type AlertDialogFooterProps };
|