@weing-dev/ui-kit-primitive 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -49
- package/dist/calendar.js +1383 -3790
- package/dist/chart.d.ts +2 -1
- package/dist/chart.js +614 -442
- package/dist/components/Button/Button.d.ts +2 -0
- package/dist/components/Chart/Chart.gradient.d.ts +10 -0
- package/dist/components/Chart/Chart.options.d.ts +7 -2
- package/dist/components/Chart/Chart.type.d.ts +51 -4
- package/dist/components/Chart/charts/RadarChart.d.ts +3 -0
- package/dist/components/FloatingAnchor/FloatingAnchor.context.d.ts +5 -0
- package/dist/components/FloatingAnchor/FloatingAnchor.d.ts +14 -0
- package/dist/components/FloatingAnchor/FloatingAnchor.type.d.ts +41 -0
- package/dist/components/FloatingAnchor/FloatingAnchor.util.d.ts +43 -0
- package/dist/components/Form/Dropdown/Dropdown.type.d.ts +2 -0
- package/dist/components/KanbanBoard/KanbanBoard.d.ts +4 -0
- package/dist/components/KanbanBoard/KanbanBoard.type.d.ts +36 -0
- package/dist/components/Pagination/Pagination.d.ts +4 -1
- package/dist/components/Timeline/Timeline.colors.d.ts +6 -0
- package/dist/components/Timeline/Timeline.d.ts +4 -0
- package/dist/components/Timeline/Timeline.type.d.ts +16 -0
- package/dist/core.esm-CuBlrIcC.js +2859 -0
- package/dist/display.css +1 -1
- package/dist/display.d.ts +4 -0
- package/dist/display.js +2932 -2123
- package/dist/entry/chart.d.ts +2 -1
- package/dist/entry/display.d.ts +4 -0
- package/dist/entry/feedback.d.ts +2 -0
- package/dist/feedback.css +1 -1
- package/dist/feedback.d.ts +2 -0
- package/dist/feedback.js +595 -312
- package/dist/form.css +1 -1
- package/dist/form.js +1740 -1742
- package/dist/index.css +1 -1
- package/dist/index.js +3521 -3522
- package/dist/index.umd.cjs +21 -21
- package/dist/navigation.css +1 -1
- package/dist/navigation.js +297 -296
- package/docs/ui-kit-agent-guide.md +616 -0
- package/package.json +3 -2
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# UI-Kit Primitive 에이전트 가이드
|
|
2
|
+
|
|
3
|
+
> **이 문서의 목적**: weing 모노레포에서 **화면/마크업 작업을 수행하는 에이전트**가, 이미 존재하는 `@weing-dev/ui-kit-primitive` 컴포넌트를 정확히 찾아 합성하도록 돕는다. 목표는 단 하나 — **불필요한 신규 컴포넌트(자작 Button/Modal/Input 등)를 만들지 않고, 검증된 프리미티브를 재사용**하는 것.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 0. 대원칙 (에이전트가 화면 작업 전에 반드시 따를 것)
|
|
8
|
+
|
|
9
|
+
1. **새 컴포넌트를 만들기 전에 § 2 "목적 → 컴포넌트" 표와 § 4 카탈로그를 먼저 본다.** 40개 프리미티브가 폼·네비·피드백·표시·데이터까지 대부분의 화면 요구를 이미 커버한다.
|
|
10
|
+
2. **버튼·입력·모달·탭·테이블·라벨 같은 기본 요소를 직접 `<button>`/`<input>`/`createPortal`로 새로 만들지 않는다.** 반드시 대응 프리미티브를 쓴다.
|
|
11
|
+
3. **폼 입력은 거의 항상 앱의 `InputCore`(react-hook-form 어댑터)를 경유**한다(§ 5). 프리미티브를 생짜로 RHF에 직접 묶지 않는다.
|
|
12
|
+
4. **import는 서브패스 우선**(`/form`, `/display` …). 루트 배럴(`@weing-dev/ui-kit-primitive`)은 무거운 의존성까지 끌어오므로 신규 코드에서 지양(§ 3).
|
|
13
|
+
5. **확실히 없을 때만** 신규 컴포넌트를 만든다. 그 판단 기준은 § 7에 있다.
|
|
14
|
+
6. 프리미티브로 안 되는 한 끗(앱 전용 색상·라벨·검증 표준화)은 **새로 만들지 말고 `ui-kit-extension`/`InputCore` 래퍼 패턴으로 감싼다**(§ 5).
|
|
15
|
+
|
|
16
|
+
> 헷갈리면: "이건 이미 있을 것이다"를 기본 가정으로 삼고 표에서 먼저 찾는다.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. 컴포넌트 패턴 빠른 이해
|
|
21
|
+
|
|
22
|
+
- **Compound 패턴**: 대부분 컴포넌트는 객체에 하위 컴포넌트를 담는다. 예) `Button.Root`, `Button.Text`, `Button.Icon`. `Root`가 Context Provider 역할을 하고, 하위 슬롯이 상태를 `React.use`로 읽는다.
|
|
23
|
+
- **`as` 렌더 prop**: 많은 하위 컴포넌트(Trigger, Item, Column 등)가 `as`로 마크업을 완전 커스터마이즈할 수 있다. 기본 마크업으로 부족할 때만 사용.
|
|
24
|
+
- **CSS 변수 토큰 테마**: 색상은 `var(--token, fallback)` 형태로 들어간다. 하드코딩 hex 대신 토큰을 쓴다.
|
|
25
|
+
- **상태 prop 표준**: 입력류는 `status: "error" | "warning" | "success"`, 크기는 `size`, 형태는 `variant`/`shapeType`/`colorType`으로 통일돼 있다.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. 목적 → 컴포넌트 (먼저 여기서 찾기)
|
|
30
|
+
|
|
31
|
+
| 만들려는 것 | 쓸 프리미티브 | 서브패스 |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| 클릭 액션 버튼 | `Button` | `/form` |
|
|
34
|
+
| 한 줄 입력 | `TextInput` | `/form` |
|
|
35
|
+
| 여러 줄 입력(글자수) | `TextArea` | `/form` |
|
|
36
|
+
| 셀렉트/드롭다운(검색·다중) | `Dropdown` | `/form` |
|
|
37
|
+
| 체크박스 | `CheckBox` | `/form` |
|
|
38
|
+
| 라디오 그룹 | `Radio` | `/form` |
|
|
39
|
+
| on/off 토글 | `Switch` | `/form` |
|
|
40
|
+
| 계층형(시/도→구) 선택 | `Cascader` | `/form` |
|
|
41
|
+
| 인증번호(OTP) 입력 | `OTPInput` | `/form` |
|
|
42
|
+
| 값/범위 슬라이더 | `Slider` | `/form` |
|
|
43
|
+
| 단계 진행 표시 | `Stepper` | `/form` |
|
|
44
|
+
| 시간 범위(HH:MM~HH:MM) | `TimeInput` | `/form` |
|
|
45
|
+
| 휠(드럼) 선택 | `MobilePicker` | `/form` |
|
|
46
|
+
| 입력 하단 안내·에러 문구 | `HelperText` / `Comment` | `/form` · `/display` |
|
|
47
|
+
| 탭 전환 | `Tab` | `/navigation` |
|
|
48
|
+
| 펼침/접힘 | `Accordion` | `/navigation` |
|
|
49
|
+
| 경로(빵부스러기) | `Breadcrumb` | `/navigation` |
|
|
50
|
+
| 페이지네이션 | `Pagination` | `/navigation` |
|
|
51
|
+
| 좌측 메뉴(LNB) | `LNB` + `makeNavigation` | `/navigation` |
|
|
52
|
+
| 설정 행/리스트 한 줄 | `List` | `/navigation` |
|
|
53
|
+
| 스크롤 위치로 현재 섹션 추적 | `ScrollSpy.useScrollSpy` | `/navigation` |
|
|
54
|
+
| 모달(딤드) | `Modal` (앱에선 `useModal` 경유) | `/feedback` |
|
|
55
|
+
| 팝업(트리거 옆 레이어) | `Popup` + `usePopup` | `/feedback` |
|
|
56
|
+
| 위치계산 팝오버(flip/shift·portal) | `FloatingAnchor` | `/feedback` |
|
|
57
|
+
| 바텀/사이드 시트 | `Sheet` | `/feedback` |
|
|
58
|
+
| 상태/카테고리 라벨 | `Label` | `/display` |
|
|
59
|
+
| 필터/태그 칩 | `Chips` | `/display` |
|
|
60
|
+
| 카운트/알림 점 배지 | `Badge` | `/display` |
|
|
61
|
+
| 프로필 이미지 | `Avatar` | `/display` |
|
|
62
|
+
| 업로드/수정 썸네일 | `Thumbnail` | `/display` |
|
|
63
|
+
| 구분선 | `Divider` | `/display` |
|
|
64
|
+
| 지연 로딩 이미지 | `LazyImage` | `/display` |
|
|
65
|
+
| QR / 바코드 | `QRCode` / `BarCode` | `/display` |
|
|
66
|
+
| 칸반 / 드래그 보드 | `KanbanBoard` | `/display` |
|
|
67
|
+
| 하루 시간대 타임라인 | `Timeline` | `/display` |
|
|
68
|
+
| 데이터 표 | `Table` (+ `@tanstack/react-table`) | `/table` |
|
|
69
|
+
| 막대/선/원/도넛 차트 | `BarChart` `LineChart` `PieChart` `DoughnutChart` | `/chart` |
|
|
70
|
+
| 날짜 선택(월/주) | `Calendar` | `/calendar` |
|
|
71
|
+
| 가로 스크롤 날짜 스트립 | `ScrollCalendar` | `/calendar` |
|
|
72
|
+
| 시간표(주간 타임라인·이벤트) | `WeeklyCalendar` | `/calendar` |
|
|
73
|
+
| 위지윅 에디터 | `Editor` (+ `editor/style.css`) | `/editor` |
|
|
74
|
+
| 비디오 플레이어 | `VideoPlayer` (+ `video-player/style.css`) | `/video-player` |
|
|
75
|
+
| 아이콘 | `Icon` (앱 루트 `IconProvider`) | `/icon` |
|
|
76
|
+
|
|
77
|
+
여기에 없는 것을 만들기 전에 § 7을 본다.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. import 규칙
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// 권장: 서브패스 (번들 분리 · 무거운 peer dep 미포함)
|
|
85
|
+
import { Button, TextInput } from "@weing-dev/ui-kit-primitive/form";
|
|
86
|
+
import { Modal } from "@weing-dev/ui-kit-primitive/feedback";
|
|
87
|
+
import { Icon } from "@weing-dev/ui-kit-primitive/icon";
|
|
88
|
+
|
|
89
|
+
// 무거운 컴포넌트는 CSS도 함께
|
|
90
|
+
import { Editor } from "@weing-dev/ui-kit-primitive/editor";
|
|
91
|
+
import "@weing-dev/ui-kit-primitive/editor/style.css";
|
|
92
|
+
|
|
93
|
+
import { VideoPlayer } from "@weing-dev/ui-kit-primitive/video-player";
|
|
94
|
+
import "@weing-dev/ui-kit-primitive/video-player/style.css";
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
서브패스 매핑: `/form`, `/navigation`, `/feedback`, `/display`, `/table`, `/chart`, `/calendar`, `/editor`, `/video-player`, `/icon`. 전체 표는 [`subpath-imports.md`](./subpath-imports.md).
|
|
98
|
+
|
|
99
|
+
**금지**: `@weing-dev/ui-kit-primitive/dist/...` 처럼 `dist`를 직접 가리키는 import (exports 맵 우회, 빌드 시 깨짐).
|
|
100
|
+
|
|
101
|
+
> 참고: 기존 `apps/cms-basic`은 루트 배럴(`@weing-dev/ui-kit-primitive`)을 일괄 사용하는 구(舊)관행이 남아 있다. **기존 파일을 수정할 때는 그 파일의 import 스타일을 따르고**, **새 파일/새 화면은 서브패스**로 작성한다.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 4. 컴포넌트 카탈로그 (합성 예시)
|
|
106
|
+
|
|
107
|
+
> 모든 열거값(variant/size/colorType…)은 코드 확인 기준. 기본값은 굵게 표시하지 않았으나 첫 값이 통상 기본인 경우가 많다. 정확한 기본값이 중요하면 해당 컴포넌트 소스 확인.
|
|
108
|
+
|
|
109
|
+
### 4.1 Form (`/form`)
|
|
110
|
+
|
|
111
|
+
#### Button — `Root / Text / Icon`
|
|
112
|
+
- `variant: "contained" | "outlined" | "soft" | "text"`, `colorType: "primary" | "inherit" | "red"`, `shapeType: "square" | "round"`, `size: "small" | "medium" | "large"`
|
|
113
|
+
- `isLoading`, `loadingText`, `disabled`, `isFitted`, `radius`(기본 8), `rounded`, `circle`, `textColor`, 표준 `button` 속성
|
|
114
|
+
- **앱 전용 색/형태는 `!important` 없이 override** — `Root`가 CSS변수를 노출하고 `style`은 후순위 머지된다. bg=`--default-color`, 보더=`--border-color`, 텍스트·아이콘=`--btn-text-color`(또는 `textColor` prop), radius=`radius` prop 또는 `--radius`. `Button.Text`/`Button.Icon`은 `--btn-text-color`를 상속하므로 자손 `& *{color}` 강제 불필요.
|
|
115
|
+
```tsx
|
|
116
|
+
// soft 임의 톤 / 다크 솔리드 — 토큰 밖 색도 한 줄로
|
|
117
|
+
<Button.Root variant="soft" radius={30}
|
|
118
|
+
style={{ "--default-color": "#D6E8E9", "--btn-text-color": "#13949C" }}>
|
|
119
|
+
<Button.Text>조회</Button.Text>
|
|
120
|
+
</Button.Root>
|
|
121
|
+
```
|
|
122
|
+
```tsx
|
|
123
|
+
<Button.Root variant="contained" colorType="primary" size="medium" onClick={onClick}>
|
|
124
|
+
<Button.Text>확인</Button.Text>
|
|
125
|
+
</Button.Root>
|
|
126
|
+
// 아이콘 포함
|
|
127
|
+
<Button.Root variant="outlined">
|
|
128
|
+
<Button.Icon name="Add" />
|
|
129
|
+
<Button.Text>추가</Button.Text>
|
|
130
|
+
</Button.Root>
|
|
131
|
+
```
|
|
132
|
+
> 주의: `Button.type.ts`에 남은 `ButtonProps`(cyan, xLarge 등)는 **미사용 정의**다. 런타임 값은 위 목록(context 기준)을 따른다.
|
|
133
|
+
|
|
134
|
+
#### TextInput — `Root / Label / Input / Erase / HelperText`
|
|
135
|
+
- `value`, `onChange(value)`, `variant: "outlined" | "contained"`, `size: "medium" | "large"`, `type: "text" | "password" | "email"`, `status`, `direction: "column" | "row"`, `accentType: "in" | "out" | "all"`
|
|
136
|
+
```tsx
|
|
137
|
+
<TextInput.Root value={value} onChange={setValue} name="email" placeholder="이메일">
|
|
138
|
+
<TextInput.Label>이메일</TextInput.Label>
|
|
139
|
+
<TextInput.Input><TextInput.Erase /></TextInput.Input>
|
|
140
|
+
<TextInput.HelperText>형식을 확인하세요</TextInput.HelperText>
|
|
141
|
+
</TextInput.Root>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### TextArea — `Root / Label / Input / Erase / HelperText / TextCount`
|
|
145
|
+
- `value`, `onChange(e)`, `rows`(기본 3), `maxRows`, `maxLength`, `variant`, `status`
|
|
146
|
+
```tsx
|
|
147
|
+
<TextArea.Root value={value} onChange={onChange} maxLength={200} rows={3}>
|
|
148
|
+
<TextArea.Label>소개</TextArea.Label>
|
|
149
|
+
<TextArea.Input><TextArea.TextCount /></TextArea.Input>
|
|
150
|
+
</TextArea.Root>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Dropdown — `Root / Trigger / Menu / Item`
|
|
154
|
+
- Root: `value: string | string[]`(배열=다중), `onChange`, `searchable`, `error`, `accentType: "in" | "out" | "all"`
|
|
155
|
+
- Trigger: `variant: "outlined" | "contained" | "text"`, `size`, `leftIcon`, `rightIcon`, `accentColor`(도메인 강조 보더 — inactive/selected/focused 보더를 이 색으로 통일, error/disabled는 우선 유지)
|
|
156
|
+
- Menu: `variant: "shadow" | "line"`
|
|
157
|
+
```tsx
|
|
158
|
+
<Dropdown.Root value={value} onChange={setValue} placeholder="선택">
|
|
159
|
+
<Dropdown.Trigger />
|
|
160
|
+
<Dropdown.Menu>
|
|
161
|
+
{options.map((o) => <Dropdown.Item key={o.value} value={o.value} label={o.label} />)}
|
|
162
|
+
</Dropdown.Menu>
|
|
163
|
+
</Dropdown.Root>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### CheckBox — `Root / Trigger / Label / HelperText`
|
|
167
|
+
- `id`(필수), `value: string | string[]`(배열=다중), `onChange`, `status`
|
|
168
|
+
```tsx
|
|
169
|
+
<CheckBox.Root id="agree" value={value} onChange={setValue}>
|
|
170
|
+
<CheckBox.Trigger />
|
|
171
|
+
<CheckBox.Label>약관에 동의합니다</CheckBox.Label>
|
|
172
|
+
</CheckBox.Root>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Radio — `Root / Trigger / Label / HelperText`
|
|
176
|
+
- `id`(이 라디오 값, 필수), `value`(그룹 현재 선택값; `id===value`면 checked), `name`, `onChange(id)`
|
|
177
|
+
```tsx
|
|
178
|
+
{items.map((it) => (
|
|
179
|
+
<Radio.Root key={it.id} id={it.id} value={selected} name="plan" onChange={setSelected}>
|
|
180
|
+
<Radio.Trigger />
|
|
181
|
+
<Radio.Label>{it.label}</Radio.Label>
|
|
182
|
+
</Radio.Root>
|
|
183
|
+
))}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Switch — `Root / Trigger / Label`
|
|
187
|
+
- `value: boolean`, `onChange(checked)`, `size`, `direction: "row" | "column"`, `status`
|
|
188
|
+
```tsx
|
|
189
|
+
<Switch.Root value={on} onChange={setOn}>
|
|
190
|
+
<Switch.Trigger />
|
|
191
|
+
<Switch.Label>알림 받기</Switch.Label>
|
|
192
|
+
</Switch.Root>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Cascader — `Root / Menu / Column / Item` (+ `useCascader` 훅)
|
|
196
|
+
- Root: `options: CascaderOption[]`(필수), `onSelect({option, level})`, `maxColumnView`(기본 6)
|
|
197
|
+
- `CascaderOption = { id, label, disabled?, role?: "title" | "divider", children? }`
|
|
198
|
+
```tsx
|
|
199
|
+
const { columns } = Cascader.useCascader({ options });
|
|
200
|
+
<Cascader.Root options={options} onSelect={onSelect}>
|
|
201
|
+
<Cascader.Menu>
|
|
202
|
+
{columns.map((col, level) => (
|
|
203
|
+
<Cascader.Column key={level} index={level}>
|
|
204
|
+
{col.map((opt) => <Cascader.Item key={opt.id} option={opt} level={level} />)}
|
|
205
|
+
</Cascader.Column>
|
|
206
|
+
))}
|
|
207
|
+
</Cascader.Menu>
|
|
208
|
+
</Cascader.Root>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### OTPInput — `Root / Input`
|
|
212
|
+
- `length`(필수), `onChange(otp: string)`(필수), `value`
|
|
213
|
+
```tsx
|
|
214
|
+
<OTPInput.Root length={6} value={code} onChange={setCode} />
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Slider — `Root / Progress / Thumb / Marks`
|
|
218
|
+
- `min`, `max`, `step`(양수), `defaultValue: number[]`(thumb 수=길이, 다중이면 범위), `marks: {value,label}[]`
|
|
219
|
+
```tsx
|
|
220
|
+
<Slider.Root min={0} max={100} step={10} defaultValue={[30]}>
|
|
221
|
+
<Slider.Progress><Slider.Thumb /></Slider.Progress>
|
|
222
|
+
<Slider.Marks />
|
|
223
|
+
</Slider.Root>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Stepper — `Root / Step`
|
|
227
|
+
- `activeStep`, `direction: "row" | "column"`, `contentsPosition: "top" | "bottom" | "left" | "right"`, `linear`, `onClick(index)`
|
|
228
|
+
- Step의 `index`는 Root가 자동 주입(직접 지정 불필요)
|
|
229
|
+
```tsx
|
|
230
|
+
<Stepper.Root activeStep={1} direction="row" onClick={setStep}>
|
|
231
|
+
<Stepper.Step title="정보 입력" />
|
|
232
|
+
<Stepper.Step title="확인" />
|
|
233
|
+
<Stepper.Step title="완료" />
|
|
234
|
+
</Stepper.Root>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### TimeInput — `Root / Label / InputWrapper / Input / Colon / Separator / TimeRange / HelperText`
|
|
238
|
+
- Root: `value`, `onChange({start, end})`, `size`, `variant`. Input의 `index`: 0,1=시작 HH/MM, 2,3=종료 HH/MM (max 자동)
|
|
239
|
+
```tsx
|
|
240
|
+
<TimeInput.Root value={value} onChange={({ start, end }) => save(start, end)}>
|
|
241
|
+
<TimeInput.InputWrapper>
|
|
242
|
+
<TimeInput.TimeRange>
|
|
243
|
+
<TimeInput.Input index={0} placeholder="HH" />
|
|
244
|
+
<TimeInput.Colon />
|
|
245
|
+
<TimeInput.Input index={1} placeholder="MM" />
|
|
246
|
+
</TimeInput.TimeRange>
|
|
247
|
+
<TimeInput.Separator />
|
|
248
|
+
<TimeInput.TimeRange>
|
|
249
|
+
<TimeInput.Input index={2} placeholder="HH" />
|
|
250
|
+
<TimeInput.Colon />
|
|
251
|
+
<TimeInput.Input index={3} placeholder="MM" />
|
|
252
|
+
</TimeInput.TimeRange>
|
|
253
|
+
</TimeInput.InputWrapper>
|
|
254
|
+
<TimeInput.HelperText />
|
|
255
|
+
</TimeInput.Root>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### MobilePicker — `Root / Column / Item`
|
|
259
|
+
- Root: `selectedValue: { [name]: value }`(필수), `onChange`(필수), `itemHeight`(기본 36), `itemViewCount`(기본 5)
|
|
260
|
+
```tsx
|
|
261
|
+
<MobilePicker.Root selectedValue={picked} onChange={setPicked}>
|
|
262
|
+
<MobilePicker.Column>
|
|
263
|
+
{hours.map((h) => <MobilePicker.Item key={h} name="hour" value={h} />)}
|
|
264
|
+
</MobilePicker.Column>
|
|
265
|
+
</MobilePicker.Root>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### HelperText — 단일
|
|
269
|
+
- `status: "error" | "warning" | "success" | "info" | "hint"`, `iconVisible`(기본 true), `iconName`
|
|
270
|
+
```tsx
|
|
271
|
+
<HelperText status="error">필수 입력 항목입니다.</HelperText>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 4.2 Navigation (`/navigation`)
|
|
275
|
+
|
|
276
|
+
#### Tab — `Root / List / Item / Panels / Panel`
|
|
277
|
+
- Root: `active`, `onChange(id)`, `tabType: "textLine" | "line" | "text" | "squareLine" | "circle" | "square" | "rounded"`, `direction: "horizontal" | "vertical"`, `align: "start" | "center" | "end"`, `size`, `isFitted`
|
|
278
|
+
- Item/Panel: `index`(필수), `id`(미지정 시 index 문자열)
|
|
279
|
+
```tsx
|
|
280
|
+
<Tab.Root active={active} onChange={setActive} tabType="line">
|
|
281
|
+
<Tab.List>
|
|
282
|
+
<Tab.Item index={0}>탭1</Tab.Item>
|
|
283
|
+
<Tab.Item index={1}>탭2</Tab.Item>
|
|
284
|
+
</Tab.List>
|
|
285
|
+
<Tab.Panels>
|
|
286
|
+
<Tab.Panel index={0}>패널1</Tab.Panel>
|
|
287
|
+
<Tab.Panel index={1}>패널2</Tab.Panel>
|
|
288
|
+
</Tab.Panels>
|
|
289
|
+
</Tab.Root>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Accordion — `Root / Trigger / Item`
|
|
293
|
+
- `variant: "contained" | "outlined" | "adaptived" | "sideLined" | "bottomLined" | "shadow" | "unset"`, `open`(제어형), `autoClosed`(기본 true), `transition`
|
|
294
|
+
```tsx
|
|
295
|
+
<Accordion.Root variant="contained" transition>
|
|
296
|
+
<Accordion.Trigger>제목</Accordion.Trigger>
|
|
297
|
+
<Accordion.Item>펼쳐지는 내용</Accordion.Item>
|
|
298
|
+
</Accordion.Root>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Breadcrumb — `List / Link / Separator / Ellipsis`
|
|
302
|
+
- Link: `href`, `focused`(현재 위치=링크 비활성), `as`(Next Link 등 커스텀 앵커)
|
|
303
|
+
```tsx
|
|
304
|
+
<Breadcrumb.List separator={<Breadcrumb.Separator />}>
|
|
305
|
+
<Breadcrumb.Link href="/">홈</Breadcrumb.Link>
|
|
306
|
+
<Breadcrumb.Link focused>현재 페이지</Breadcrumb.Link>
|
|
307
|
+
</Breadcrumb.List>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Pagination — `Root / PageList / PageItem / Prev / Next`
|
|
311
|
+
- Root: `total`(필수), `pageCount`(필수, 노출 버튼 수), `onClick({offset, limit})`(필수), `colorType: "primary" | "standard"`, `variant: "text" | "soft"`, `size: "medium" | "small"`, `limit`(기본 15), `offset`, `style`
|
|
312
|
+
- **형태 override** — `Root` `style`로 CSS변수를 주면 하위 버튼에 상속된다: `--pagination-gap`(번호 간격), `--pagination-item-size`(버튼 치수), `--pagination-radius`(번호 radius). `Prev`/`Next`는 `data-nav="prev|next"`로 식별(소비처에서 `[data-nav]`로 nav만 타깃). 예: `<Pagination.Root style={{ "--pagination-gap":"2px", "--pagination-item-size":"36px" }}>`
|
|
313
|
+
```tsx
|
|
314
|
+
<Pagination.Root total={120} pageCount={5} limit={15} offset={offset}
|
|
315
|
+
colorType="primary" variant="soft" size="medium"
|
|
316
|
+
onClick={({ offset }) => setOffset(offset)}>
|
|
317
|
+
<Pagination.Prev />
|
|
318
|
+
<Pagination.PageList />
|
|
319
|
+
<Pagination.Next />
|
|
320
|
+
</Pagination.Root>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### LNB — `Root / Logo / List / Item` (+ `makeNavigation`)
|
|
324
|
+
- `makeNavigation({ list, pathName, depth?, isCommerce?, maxDepth? })` → 라우트 트리에 `isOpen/isActive/depth` 주입
|
|
325
|
+
- Item: `label`(필수), `type: "full" | "secondary" | "list" | "title"`(필수), `hasChildren`(필수), `icon`, `subtitle`
|
|
326
|
+
```tsx
|
|
327
|
+
const nav = makeNavigation({ list: navigationList, pathName });
|
|
328
|
+
<LNB.Root>
|
|
329
|
+
<LNB.Logo src="/logo.svg" />
|
|
330
|
+
<LNB.List>
|
|
331
|
+
{nav.map((item) => (
|
|
332
|
+
<LNB.Item key={item.key} label={item.label} type={item.type}
|
|
333
|
+
icon={item.icon} hasChildren={!!item.children?.length}
|
|
334
|
+
isOpen={item.isOpen} isActive={item.isActive} />
|
|
335
|
+
))}
|
|
336
|
+
</LNB.List>
|
|
337
|
+
</LNB.Root>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### List — 단일
|
|
341
|
+
- `label: { value }`(필수), `type: "default" | "text" | "contentsHeader"`, `direction: "row" | "column"`, `size: "s" | "m" | "l"`
|
|
342
|
+
- 제약: `default`만 `icon` 허용, `text`/`contentsHeader`만 `title` 허용(위반 시 throw)
|
|
343
|
+
```tsx
|
|
344
|
+
<List label={{ value: "항목 라벨" }} size="m" />
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### ScrollSpy — 훅 `useScrollSpy(containerEl, spyRefEls, returnType?)`
|
|
348
|
+
- `returnType: "id"(기본) | "index"`
|
|
349
|
+
```tsx
|
|
350
|
+
const activeId = ScrollSpy.useScrollSpy(containerRef.current, [sec1Ref.current, sec2Ref.current], "id");
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 4.3 Feedback (`/feedback`)
|
|
354
|
+
|
|
355
|
+
#### Modal — 단일 (compound 아님)
|
|
356
|
+
- `visible`, `onClose`, `dimmedColor: "transparent" | "light" | "dark"`, `selector`(포털 대상, 기본 `"body"`)
|
|
357
|
+
```tsx
|
|
358
|
+
<Modal visible={open} dimmedColor="dark" onClose={() => setOpen(false)}>
|
|
359
|
+
<div>모달 내용</div>
|
|
360
|
+
</Modal>
|
|
361
|
+
```
|
|
362
|
+
> 앱(cms-basic 등)에서는 보통 `<Modal>`을 직접 쓰지 않고 **`useModal().showModal({ type })`** 체계를 경유한다(§ 5).
|
|
363
|
+
|
|
364
|
+
#### Popup — `Root / Layer / Dimmed / Contents` (+ `usePopup`)
|
|
365
|
+
- `usePopup()` → `{ id, controlAttr: { show, hide, toggle }, ref, show(), hide(), toggle() }`
|
|
366
|
+
- Root: `id`(필수, usePopup의 id), `bodyScroll`, `dimmedDisabled`
|
|
367
|
+
```tsx
|
|
368
|
+
const popup = Popup.usePopup();
|
|
369
|
+
<>
|
|
370
|
+
<button {...popup.controlAttr.show}>열기</button>
|
|
371
|
+
<Popup.Root id={popup.id}>
|
|
372
|
+
<Popup.Layer ref={popup.ref}>
|
|
373
|
+
<Popup.Dimmed />
|
|
374
|
+
<Popup.Contents>팝업 내용</Popup.Contents>
|
|
375
|
+
</Popup.Layer>
|
|
376
|
+
</Popup.Root>
|
|
377
|
+
</>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Sheet — `Root / Content / Dimmed`
|
|
381
|
+
- Root: `open`, `handleClose`, `direction: "top" | "bottom" | "left" | "right"`, `drag`(기본 true)
|
|
382
|
+
```tsx
|
|
383
|
+
<Sheet.Root open={open} direction="bottom" handleClose={() => setOpen(false)}>
|
|
384
|
+
<Sheet.Dimmed />
|
|
385
|
+
<Sheet.Content>시트 내용</Sheet.Content>
|
|
386
|
+
</Sheet.Root>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### FloatingAnchor — `Root / Trigger / Content`
|
|
390
|
+
- 트리거 기준으로 Content를 **자동 위치계산**해 portal로 띄움. Popup과 달리 `flip`(반대편 전환)·`shift`(뷰포트 안으로 보정)·`placement` 12종 내장. 스크롤/리사이즈 추적.
|
|
391
|
+
- Root: `placement`, `offset`, `flip`, `shift`, `open`/`defaultOpen`/`onOpenChange`(제어/비제어), `closeOnOutsideClick`, `closeOnEscape`. Trigger children은 **render-prop**.
|
|
392
|
+
```tsx
|
|
393
|
+
<FloatingAnchor.Root placement="bottom-start" flip shift>
|
|
394
|
+
<FloatingAnchor.Trigger>{(p) => <button {...p}>열기</button>}</FloatingAnchor.Trigger>
|
|
395
|
+
<FloatingAnchor.Content>레이어 내용</FloatingAnchor.Content>
|
|
396
|
+
</FloatingAnchor.Root>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### 4.4 Display (`/display`)
|
|
400
|
+
|
|
401
|
+
#### Label — 단일
|
|
402
|
+
- `color`(필수): `"inherit" | "red" | "cyanBlue" | "blue" | "purple" | "gold" | "lightGreen" | "deepGreen"`, `variant: "filled" | "outlined" | "outlineBack" | "soft" | "inverted"`, `size: "medium" | "small" | "xSmall"`
|
|
403
|
+
```tsx
|
|
404
|
+
<Label color="blue">신규</Label>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### Chips — 단일
|
|
408
|
+
- `variant: "contained" | "outlined" | "soft" | "text"`, `colorType: "primary" | "inherit" | "dual"`, `shapeType: "square" | "round"`, `size`, `checked`, `value`, `onClick`
|
|
409
|
+
```tsx
|
|
410
|
+
<Chips value="필터" checked onClick={onClick} />
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### Badge — 단일
|
|
414
|
+
- `value: number | string`, `dot`, `max`(기본 100), `badgeColor`, `overlap: "square" | "circle" | null`. children 없으면 단독 배지
|
|
415
|
+
```tsx
|
|
416
|
+
<Badge value={5} overlap="circle"><Avatar.Item src="/p.png" alt="홍길동" /></Badge>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### Avatar — `Item / Group`
|
|
420
|
+
- Item: `variant: "square" | "rounded" | "circle"`, `size`(기본 40), `nameAbbreviation`, `<img>` 속성
|
|
421
|
+
- Group: `max`(기본 4), `total`, `itemSize`
|
|
422
|
+
```tsx
|
|
423
|
+
<Avatar.Item src="/profile.png" alt="홍길동" size={40} variant="circle" />
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### Thumbnail — `Root / Image / Upload / EmptyImage / RemoveButton / EditButton`
|
|
427
|
+
- Root: `size: "xxs"|"xs"|"sm"|"s"|"m"|"l"|"xl"|"xxl" | number`, `variant: "square" | "circle"`, `objectFit`, `showLoadingSkeleton`, `imageComponent`(예: next/image), `onUpload` / `onEdit(e, index)` / `onRemove(index)`
|
|
428
|
+
```tsx
|
|
429
|
+
<Thumbnail.Root size="m" onRemove={(i) => remove(i)}>
|
|
430
|
+
<Thumbnail.Image src="/photo.jpg" alt="썸네일" index={0} />
|
|
431
|
+
</Thumbnail.Root>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### Comment — 단일
|
|
435
|
+
- `status: "error" | "warning" | "success" | "info" | "default"`, `size`(아이콘 크기)
|
|
436
|
+
```tsx
|
|
437
|
+
<Comment status="error">필수 입력 항목입니다.</Comment>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### Divider / LazyImage / QRCode / BarCode — 단일
|
|
441
|
+
```tsx
|
|
442
|
+
<Divider size="small" /> // size: "small" | "large" | number, verticalGap
|
|
443
|
+
<LazyImage src="/photo.jpg" ratio="16:9" width={320} /> // ratio: "1:1"|"4:3"|"16:9"|"3:4"|"9:16", width/height 중 1개 필수
|
|
444
|
+
<QRCode link="https://example.com" /> // ref.copy() 제공, 256px 고정
|
|
445
|
+
<BarCode value="1234567" type="EAN8" /> // type: "EAN13"(12자리) | "EAN8"(7자리), ref.copy()/download()
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### KanbanBoard — 단일(제네릭 `<T>`)
|
|
449
|
+
- 컬럼 기반 드래그&드롭 보드(`@dnd-kit`). `setItemColumn`+`onItemsChange`를 **둘 다** 주면 DnD 활성, 없으면 정적 렌더. 카드/헤더는 render-prop으로 호출부가 결정.
|
|
450
|
+
- `columns`, `items`, `getItemId`, `getColumnId`, `renderCard(item, isSelected)`, `renderColumnHeader?`, `selectedId`/`onSelect`, `emptyText`
|
|
451
|
+
```tsx
|
|
452
|
+
<KanbanBoard
|
|
453
|
+
columns={cols} items={deals}
|
|
454
|
+
getItemId={(d) => d.id} getColumnId={(d) => d.stage}
|
|
455
|
+
renderCard={(d) => <DealCard deal={d} />}
|
|
456
|
+
setItemColumn={(d, stage) => ({ ...d, stage })}
|
|
457
|
+
onItemsChange={setDeals}
|
|
458
|
+
/>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Timeline — 단일
|
|
462
|
+
- 하루(시간대) 세로 타임라인. 겹치는 이벤트는 자동 가로 컬럼 분할, 자정 넘는 이벤트는 두 조각으로 분리.
|
|
463
|
+
- `events: { id?, startHour:"HH:mm", endHour?, label, color? }[]`, `date`, `startHour`(기본 0)/`endHour`(기본 24). `color`는 토큰(`"blue"|"pink"|"green"|"orange"`) 또는 `{background,border}`
|
|
464
|
+
```tsx
|
|
465
|
+
<Timeline date="2026-06-26" events={[{ startHour: "09:00", endHour: "10:30", label: "회의", color: "blue" }]} />
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 4.5 Data / Heavy
|
|
469
|
+
|
|
470
|
+
#### Table — `Root / Header / HeaderCell / Body / Row / Cell` (+ `useRowResize`) · `/table`
|
|
471
|
+
- 렌더 전용. 데이터/정렬/페이징은 호출부가 `@tanstack/react-table`로 관리.
|
|
472
|
+
- Root: `variant: "default" | "bordered" | "striped" | "compact"`. Cell/HeaderCell: `size`(px width)
|
|
473
|
+
```tsx
|
|
474
|
+
<Table.Root variant="bordered">
|
|
475
|
+
<Table.Header>
|
|
476
|
+
<Table.Row><Table.HeaderCell size={120}>이름</Table.HeaderCell></Table.Row>
|
|
477
|
+
</Table.Header>
|
|
478
|
+
<Table.Body>
|
|
479
|
+
<Table.Row><Table.Cell>홍길동</Table.Cell></Table.Row>
|
|
480
|
+
</Table.Body>
|
|
481
|
+
</Table.Root>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### Chart (`BarChart` / `LineChart` / `PieChart` / `DoughnutChart`) · `/chart`
|
|
485
|
+
- 각각 단일 컴포넌트. 데이터 형태:
|
|
486
|
+
- `SimpleDatum = { label, value, color? }` → 간편형
|
|
487
|
+
- `SeriesData = { labels: string[], series: ChartSeries[] }` → 다중 시리즈
|
|
488
|
+
- bar/line: `CategoricalData`(둘 다 가능), pie/doughnut: `ProportionData = SimpleDatum[]`
|
|
489
|
+
- 공통: `title`, `legend`, `height`, `loading`, `empty`, `valueLabel`, `colors: PaletteInput`, `options`(deep-merge)
|
|
490
|
+
- `ChartThemeProvider`로 화면/앱 단위 팔레트·defaults 상속
|
|
491
|
+
```tsx
|
|
492
|
+
<BarChart data={[{ label: "1월", value: 120 }, { label: "2월", value: 200 }]} />
|
|
493
|
+
<DoughnutChart data={[{ label: "A", value: 30 }, { label: "B", value: 70 }]} showTotal />
|
|
494
|
+
```
|
|
495
|
+
> peer dep: `chart.js`, `react-chartjs-2` 등. `radar`는 미export(사용 불가).
|
|
496
|
+
|
|
497
|
+
#### Calendar / ScrollCalendar / WeeklyCalendar · `/calendar`
|
|
498
|
+
- **Calendar** — `Root / HeaderDate / DayTextList / Prev / Next / Body`. dayjs 기반. `selectedDate: string | string[]`(배열=범위), `handleDatePress`, `calendarType: "week" | "month"`, `excludeDates`, `minDate`/`maxDate`
|
|
499
|
+
```tsx
|
|
500
|
+
<Calendar.Root selectedDate={value} handleDatePress={setValue}>
|
|
501
|
+
<Calendar.Prev /><Calendar.HeaderDate /><Calendar.Next />
|
|
502
|
+
<Calendar.DayTextList />
|
|
503
|
+
<Calendar.Body />
|
|
504
|
+
</Calendar.Root>
|
|
505
|
+
```
|
|
506
|
+
- **ScrollCalendar** — `Root / Body / Column`. 가로 스크롤 날짜 스트립. 보통 `Calendar.Root` 하위에서 `Calendar.Body`의 `as`/`columnAs`로 합성.
|
|
507
|
+
- **WeeklyCalendar** — `Root / StandardTime / DateCellRow / Timelines / DateColumnRow`. 시간 그리드 + 이벤트 블록. `events`, `columnCount`, `minHour`/`maxHour`/`step`, `enableCreateByDrag`, `enableMoveByDrag`. peer dep: `@dnd-kit/core`, `react-popper`.
|
|
508
|
+
|
|
509
|
+
#### Editor — `Root / Input / Skeleton` (+ `Editor.TOOLBAR`) · `/editor` (+ CSS)
|
|
510
|
+
- Quill 2. `initialValue`(HTML), `onChange({ value })`(value=innerHTML), `toolbarOptions`, `onImageUpload(file, quill, range)`, `customFonts`
|
|
511
|
+
```tsx
|
|
512
|
+
import "@weing-dev/ui-kit-primitive/editor/style.css";
|
|
513
|
+
<Editor.Root initialValue={html} onChange={({ value }) => setHtml(value)}>
|
|
514
|
+
<Editor.Skeleton />
|
|
515
|
+
<Editor.Input />
|
|
516
|
+
</Editor.Root>
|
|
517
|
+
```
|
|
518
|
+
> 신규 위지윅은 항상 이 `Editor`를 쓴다. `WisywygEditor`(레거시 react-quill-new)는 미export — 쓰지 않는다.
|
|
519
|
+
|
|
520
|
+
#### VideoPlayer — `Root / Frame / Player / Thumbnail / Overlay / Loading / Error / Widgets…` · `/video-player` (+ CSS)
|
|
521
|
+
- Video.js + Zustand. `src`/`sources`, `tracks`(자막), `controllerRef`(외부 제어), 자막 유틸 `convertSrtToVtt` 등
|
|
522
|
+
```tsx
|
|
523
|
+
import "@weing-dev/ui-kit-primitive/video-player/style.css";
|
|
524
|
+
<VideoPlayer.Root src="/movie.mp4">
|
|
525
|
+
<VideoPlayer.Frame>
|
|
526
|
+
<VideoPlayer.Player />
|
|
527
|
+
<VideoPlayer.Thumbnail />
|
|
528
|
+
<VideoPlayer.Loading />
|
|
529
|
+
<VideoPlayer.Error />
|
|
530
|
+
</VideoPlayer.Frame>
|
|
531
|
+
</VideoPlayer.Root>
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### Icon · `/icon`
|
|
535
|
+
- `Icon`: `name: IconName`(필수, 미등록 이름이면 throw), `size`(기본 24), `color`
|
|
536
|
+
- 앱 루트를 **`IconProvider icons={...}`** 로 한 번 감싼다. 등록 가능한 이름은 `ICON_NAMES`(약 115종).
|
|
537
|
+
```tsx
|
|
538
|
+
<IconProvider icons={icons}>
|
|
539
|
+
<Icon name="Search" size={20} />
|
|
540
|
+
</IconProvider>
|
|
541
|
+
```
|
|
542
|
+
> 필요한 아이콘이 `ICON_NAMES`에 없으면 → `scaffold-icons` 스킬 / `pnpm icons:create` 파이프라인으로 추가. 임의 SVG를 화면에 인라인하지 않는다.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## 5. 소비 컨벤션 (앱에서 프리미티브를 쓰는 표준 방식)
|
|
547
|
+
|
|
548
|
+
프리미티브를 **생짜로 쓰지 않고 2계층으로 감싸는** 관행이 정착돼 있다. 새 화면도 이 관행을 따른다.
|
|
549
|
+
|
|
550
|
+
### 5.1 폼 입력 → `InputCore` (react-hook-form 어댑터)
|
|
551
|
+
`apps/cms-basic/src/components/Core/InputCore/InputCore.tsx`. 내부에 RHF `Controller`를 품고 label/required/info(Tooltip)/HelperText/검증(`rules`)을 표준화한다. children은 render-prop으로 field를 넘긴다.
|
|
552
|
+
```tsx
|
|
553
|
+
<InputCore control={control} name="email" label="이메일" rules={{ required: true }}>
|
|
554
|
+
{({ field }) => (
|
|
555
|
+
<TextInput.Root {...field} value={String(field.value ?? "")}>
|
|
556
|
+
<TextInput.Input />
|
|
557
|
+
</TextInput.Root>
|
|
558
|
+
)}
|
|
559
|
+
</InputCore>
|
|
560
|
+
```
|
|
561
|
+
→ 폼 입력류(TextInput/Dropdown/Radio/CheckBox/TimeInput…)는 거의 항상 이 패턴으로 소비한다. **검증 메시지(HelperText) 렌더 책임은 InputCore가 가진다.**
|
|
562
|
+
|
|
563
|
+
### 5.2 모달 → `useModal().showModal({ type })`
|
|
564
|
+
`ui-kit-extension`의 `useModal`(앱 `src/lib/ui-kit-extension/hooks/modal/useModal.tsx`)이 단일 진입점이다. 앱 코드는 `<Modal>`을 직접 렌더하지 않고 `showModal({ type })`로 띄운다(case: `default`/`locale`/`datePicker`/`passwordConfirm`/`search`/`timePicker`…).
|
|
565
|
+
- 단순 확인/알림은 기존 `type: "default"`로 충분 → 새 모달 컴포넌트를 만들지 않는다.
|
|
566
|
+
- 새로운 모달 유형이 진짜 필요하면 **`add-modal` 스킬**로 `useModal` 체계에 등록(독립 컴포넌트로 임의 렌더 금지).
|
|
567
|
+
|
|
568
|
+
### 5.3 앱 전용 변형 → `ui-kit-extension` 래퍼
|
|
569
|
+
프리미티브에 앱 전용 색상/prop을 더하고 싶으면 새로 만들지 말고 `ui-kit-extension/components/Form/{Button,TextInput,…}`처럼 프리미티브를 import해 prop을 합집합 확장한다.
|
|
570
|
+
```ts
|
|
571
|
+
import { Button as ButtonPrimitive } from "@weing-dev/ui-kit-primitive/form";
|
|
572
|
+
type ColorType = PrimitiveButtonColorType | ExtensionButtonColorType; // 확장
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## 6. 하지 말 것 / 함정
|
|
578
|
+
|
|
579
|
+
| 안티패턴 | 대신 |
|
|
580
|
+
| --- | --- |
|
|
581
|
+
| 자작 `<button>` 액션 버튼 | `Button.Root / Text` |
|
|
582
|
+
| `createPortal`로 자작 모달 | `Modal` + `useModal` |
|
|
583
|
+
| 자작 `<input>` + 라벨 + 에러문구 | `TextInput` + `InputCore` |
|
|
584
|
+
| 임의 SVG 인라인 | `Icon` (+ 없으면 `scaffold-icons`) |
|
|
585
|
+
| `@weing-dev/ui-kit-primitive/dist/...` import | 서브패스 import |
|
|
586
|
+
| `Dialog` 사용 | 빈 스텁 — `Modal`/`Popup`/`Sheet` 중 선택 |
|
|
587
|
+
| `WisywygEditor` 사용 | `Editor`(`/editor`) |
|
|
588
|
+
| `radar` 차트 | 미export — 4종(Bar/Line/Pie/Doughnut) 내에서 해결 |
|
|
589
|
+
| 하드코딩 hex 색상 | `var(--token, fallback)` 토큰 |
|
|
590
|
+
| 단순 확인용 새 모달 컴포넌트 | `showModal({ type: "default" })` |
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## 7. 신규 컴포넌트가 정당한 경우 (escape hatch)
|
|
595
|
+
|
|
596
|
+
다음을 **모두** 만족할 때만 신규 컴포넌트/래퍼를 만든다:
|
|
597
|
+
1. § 2 표와 § 4 카탈로그에서 목적에 맞는 프리미티브를 찾지 못했다.
|
|
598
|
+
2. 기존 프리미티브 + `as` 렌더 prop + `ui-kit-extension` 래퍼로도 합성이 불가능하다.
|
|
599
|
+
3. 한 화면 전용이 아니라 **재사용**될 요소다.
|
|
600
|
+
|
|
601
|
+
그리고 만들 때의 위치 규칙:
|
|
602
|
+
- **범용 프리미티브(여러 앱에서 재사용, 디자인 시스템 급)** → `ui-kit-primitive`에 추가. **`scaffold-primitive` 스킬**로 골든 레시피대로 스캐폴드(Compound + context + SCSS @layer + CSS 변수 테마 + entry 등록).
|
|
603
|
+
- **앱 전용 합성/확장** → 해당 앱의 `components/` 또는 `ui-kit-extension`에 래퍼로 추가.
|
|
604
|
+
- **모달 유형** → `add-modal` 스킬.
|
|
605
|
+
- **아이콘** → `scaffold-icons` 스킬 / `pnpm icons:create`.
|
|
606
|
+
|
|
607
|
+
> 신규 프리미티브를 만들었거나 기존 프리미티브를 변경했다면 **이 문서(`docs/ui-kit-agent-guide.md`)와 [README 인덱스](../README.md)를 함께 갱신**한다. 카탈로그가 사실의 원천이어야 에이전트가 중복 구현을 피할 수 있다.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## 부록: 정확한 props가 필요할 때
|
|
612
|
+
|
|
613
|
+
- **모노레포(소스 보유)**: 컴포넌트 소스 `packages/ui-kit-primitive/src/components/<이름>/`의 메인 `.tsx`, `.context.tsx`, `.type.ts`, `.colors.tsx`를 확인. 서브패스 export 정의는 `src/entry/*.ts`, exports 맵은 `package.json`의 `exports` 필드.
|
|
614
|
+
- **패키지를 의존성으로 쓰는 레포(소스 없음)**: `node_modules/@weing-dev/ui-kit-primitive/dist/`의 타입 선언(`*.d.ts`)을 확인한다. 서브패스별 타입은 `dist/entry/*.d.ts`.
|
|
615
|
+
|
|
616
|
+
> 이 가이드 문서는 패키지 배포물(`package.json`의 `files`)에 포함되므로, 패키지를 설치한 레포면 `node_modules/@weing-dev/ui-kit-primitive/docs/ui-kit-agent-guide.md`로도 동일 문서를 읽을 수 있다.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weing-dev/ui-kit-primitive",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"**/*.css"
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"main": "./dist/index.umd.cjs",
|
|
61
61
|
"files": [
|
|
62
62
|
"dist",
|
|
63
|
-
"docs/subpath-imports.md"
|
|
63
|
+
"docs/subpath-imports.md",
|
|
64
|
+
"docs/ui-kit-agent-guide.md"
|
|
64
65
|
],
|
|
65
66
|
"peerDependencies": {
|
|
66
67
|
"@aws-sdk/client-s3": "^3.717.0",
|