sellmate-design-system-react 7.1.0 → 8.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 (38) hide show
  1. package/AGENTS.md +412 -18
  2. package/README.md +16 -1
  3. package/dist/components/SDraggableItem/README.md +1 -0
  4. package/dist/components/SDraggableItem/SDraggableItem.d.ts +3 -0
  5. package/dist/components/SDrawer/README.md +5 -4
  6. package/dist/components/SDrawer/SDrawer.d.ts +16 -5
  7. package/dist/components/SExpansionItem/README.md +1 -0
  8. package/dist/components/SExpansionItem/SExpansionItem.d.ts +3 -0
  9. package/dist/components/SGnb/README.md +7 -0
  10. package/dist/components/SGnb/SGnb.d.ts +27 -0
  11. package/dist/components/SGnb/gnb.config.d.ts +13 -0
  12. package/dist/components/SIcon/SIcon.d.ts +1 -1
  13. package/dist/components/SIcon/icons.gen.d.ts +8 -3
  14. package/dist/components/SLayout/SLayout.d.ts +6 -0
  15. package/dist/components/SListItem/README.md +1 -0
  16. package/dist/components/SListItem/SListItem.d.ts +3 -0
  17. package/dist/components/SModal/README.md +41 -6
  18. package/dist/components/SModal/SModalOutlet.d.ts +18 -0
  19. package/dist/components/SModal/index.d.ts +1 -0
  20. package/dist/components/SSplitter/README.md +27 -0
  21. package/dist/components/SSplitter/SSplitter.d.ts +32 -0
  22. package/dist/components/SSplitter/index.d.ts +2 -0
  23. package/dist/components/SSplitter/splitter.config.d.ts +15 -0
  24. package/dist/components/STooltip/README.md +2 -0
  25. package/dist/index.cjs +1034 -221
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1033 -223
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/autofill.d.ts +29 -0
  31. package/dist/lib/is-dev.d.ts +2 -0
  32. package/dist/lib/modal-outlet.d.ts +53 -0
  33. package/dist/llms-full.txt +502 -28
  34. package/dist/llms.txt +418 -20
  35. package/dist/styles.css +154 -17
  36. package/dist/theme.css +40 -10
  37. package/eslint/scale.gen.mjs +1 -1
  38. package/package.json +2 -1
@@ -30,22 +30,26 @@
30
30
 
31
31
  무엇을 만들지 정했으면 **이 표에서 먼저 찾는다.** 상세 Props 는 `dist/components/<이름>/README.md` 참조.
32
32
 
33
+ **이 표에서 어느 것을 골라야 할지 모르겠으면 §3-0 "의도 → 컴포넌트 라우팅" 으로 간다.** 하려는 일을 문장으로 찾으면 답이 하나 나온다 — 여기 인덱스는 "무엇이 있는지", §3-0 은 "언제 그걸 쓰는지" 를 담당한다.
34
+
33
35
  | 분류 | 컴포넌트 |
34
36
  | --- | --- |
35
37
  | **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
36
- | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
38
+ | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
37
39
  | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
38
- | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SDraggableItem` |
39
- | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SScrollArea` `SExpansionItem` |
40
+ | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
41
+ | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
40
42
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
41
43
  | **표시·상태** | `STag` `SBadge` `SIcon` `SCallout` `SGuide` |
42
44
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
43
- | **오버레이** | `STooltip` `SPopover` `SPopup` `SPortal` |
44
- | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` |
45
+ | **오버레이** | `STooltip` `SPopover` `SPopup` `SDrawer` `SPortal` |
46
+ | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
45
47
  | **알림** | `SToast` `SToastContainer` |
46
48
 
47
49
  표에 없는 UI 를 만들어야 할 때만 `div` 로 직접 조립하고, 그때도 §1-2 · §2 의 토큰 규칙을 지킨다.
48
50
 
51
+ **이 표는 패키지가 실제로 export 하는 컴포넌트와 일치해야 한다** — `npm run check:routing` 이 강제한다. 표에 없는 컴포넌트는 소비 앱 입장에서 존재하지 않는 것과 같다.
52
+
49
53
  ### 0-2. 프로젝트 설정
50
54
 
51
55
  설정(Tailwind v4 `theme.css` import, `@source` 지정, Next.js 주의사항)은 패키지 [README.md](./README.md)를 따른다. 이 문서는 설정이 끝난 상태에서의 **화면 작성 규칙**만 다룬다.
@@ -130,7 +134,76 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
130
134
 
131
135
  ---
132
136
 
133
- ## 2. 조합 어휘컴포넌트 "사이"를 채울 때 쓰는 것들
137
+ ## 2. 조합 규칙화면을 어떻게 쌓는가
138
+
139
+ §3 이 "무엇을 쓸지" 라면 여기는 **"고른 것들을 어떻게 붙일지"** 다. 층 구조(§2-0)가 먼저고, 타이포·간격·색(§2-1~2-3)은 그 층에 붙는 값이다.
140
+
141
+ ### 2-0. 화면의 층 구조 — 무엇을 어디에 놓는가
142
+
143
+ **모든 화면은 다섯 층으로 쌓인다. 컴포넌트는 저마다 놓이는 층이 정해져 있고, 층이 다르면 붙이는 방법도 다르다.** 조합이 어색해지는 원인은 대개 층을 건너뛴 것이다 — 요소를 페이지에 바로 놓거나, 인라인 요소를 블록처럼 세우거나, 카드 안에 카드를 겹치는 식이다.
144
+
145
+ | 층 | 무엇인가 | 컴포넌트 |
146
+ | --- | --- | --- |
147
+ | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage` |
148
+ | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
149
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SLinearProgress` `SCircleProgress` |
150
+ | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
151
+ | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
152
+
153
+ 여기에 화면을 차지하지 않는 **부트스트랩** 이 따로 있다 — `SModalOutlet` `SToastContainer` 는 앱 진입점에 한 번만 렌더한다 (§4-1).
154
+
155
+ > 이 표는 §0-1 인덱스 전체를 덮는다 (`npm run check:routing` 이 강제). **컴포넌트를 골랐으면 그것이 어느 층인지 먼저 확인하고, 아래 포함 규칙에 맞는 자리에 놓는다.**
156
+
157
+ #### 블록은 두 종류다
158
+
159
+ 같은 블록층이어도 **안에 다른 것을 담느냐** 로 갈린다. 이걸 구분해야 포함 규칙이 선다.
160
+
161
+ - **담는 블록** — `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea`. 안이 비어 있고 다른 블록·요소를 받는다.
162
+ - **그리는 블록** — 나머지 전부. 자기가 내용을 그리므로 안에 무엇을 넣을지 고민할 일이 없다 (`STable` 의 셀처럼 지정된 슬롯 제외).
163
+
164
+ #### 포함 규칙 — 무엇 안에 무엇이 올 수 있나
165
+
166
+ | 담는 것 | 올 수 있는 것 | 오면 안 되는 것 |
167
+ | --- | --- | --- |
168
+ | `SPage` | **블록만** | **요소를 직접** — 버튼 하나도 블록에 담아 놓는다 |
169
+ | `SSectionHeaderCard.Body` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
170
+ | `SCard` | 그리는 블록 · 요소 | `SCard` · `SSectionHeaderCard` |
171
+ | `SForm` | 블록 (보통 `SKeyValueTable` + 하단 액션) | — |
172
+ | `SSplitter.Before` / `.After` | 블록 | — |
173
+ | `SScrollArea` | 블록 | — |
174
+ | `STable` 셀 (`render`) | 인라인 · 요소 | 블록 — 표 안에 표·카드를 넣지 않는다 |
175
+ | `SKeyValueTable` 값 셀 | 인라인 · 요소 | 블록 |
176
+ | `SListItem` | 인라인 | 블록 · 요소 |
177
+
178
+ - **블록을 `div` 로 감싸지 않는다.** 감싸면 페이지 스택에서 빠져나가 `gap-sd-12` 리듬이 끊긴다. 여러 블록을 묶어야 하면 그건 섹션이므로 `SSectionHeaderCard` 다.
179
+ - **요소를 페이지에 직접 놓지 않는다.** 하단 액션 버튼들처럼 블록이 없는 자리는 `div` 로 한 줄을 만들어 그 `div` 가 블록이 된다 (§4-3·§4-4).
180
+ - **레이어는 어디서 띄워도 된다.** `body` 로 portal 되므로 셸 안에 넣을 필요가 없고, 넣어도 레이아웃이 바뀌지 않는다 (§4-1).
181
+
182
+ #### 블록을 쌓는 방법과 순서
183
+
184
+ **`SPage` 는 자식을 자동으로 쌓지 않는다.** 페이지가 직접 세로 스택을 만든다 — 이게 모든 §4 레시피가 `flex flex-col gap-sd-12` 로 시작하는 이유다.
185
+
186
+ ```tsx
187
+ <SPage background="frame">
188
+ <div className="flex flex-col gap-sd-12"> {/* 블록 스택 — 페이지가 만든다 */}
189
+ …블록들…
190
+ </div>
191
+ </SPage>
192
+ ```
193
+
194
+ 블록 순서는 화면 종류와 무관하게 같다. **필요한 것만 남기되 순서를 바꾸지 않는다.**
195
+
196
+ ```text
197
+ 1. 페이지 제목 (+ 가이드·매뉴얼 링크. 액션 버튼은 오지 않는다 §4-2)
198
+ 2. 상시 안내 SCallout
199
+ 3. 필터 SKeyValueTable
200
+ 4. 툴바 STableBar (건수 요약 + 액션)
201
+ 5. 본문 STable · 섹션 카드들 · SList …
202
+ 6. 페이지네이션 SPagination (STable 이 pagination prop 으로 직접 그린다)
203
+ 7. 하단 액션 되돌리기 왼쪽 · 실행 오른쪽 (§4-3)
204
+ ```
205
+
206
+ 간격은 층마다 다르다 — 블록 ↔ 블록은 `gap-sd-12`, 요소 ↔ 요소는 `gap-sd-8` 이 기본이고, 같은 컴포넌트를 나열할 때는 컴포넌트별 그룹 간격이 따로 있다. 전부 §2-2 에 있다.
134
207
 
135
208
  ### 2-1. 타이포그래피 프리셋
136
209
 
@@ -145,15 +218,22 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
145
218
 
146
219
  **기본 선택 — 이 조합을 쓴다.** 이 서비스는 정보 밀도가 높아 본문이 12px 이다. 14px 를 본문 기본으로 쓰지 않는다.
147
220
 
148
- | 역할 | 클래스 | 크기 |
149
- | --- | --- | --- |
150
- | 페이지 헤더 제목 (h1) | `typo-heading-lg` | 18px |
151
- | 섹션 제목 | `typo-heading-sm` | 14px |
152
- | 본문 | `typo-body-sm-default` | 12px |
153
- | 보조 설명 | `typo-body-sm-default` + `text-fg-tertiary` | 12px / `grey_65` |
221
+ **제목 위계는 §2-0 층을 따라간다** — 층이 한 단 내려가면 제목도 한 단 내려간다. 층을 건너뛰지 않듯 제목도 건너뛰지 않는다.
222
+
223
+ | (§2-0) | 역할 | 클래스 | 크기 |
224
+ | --- | --- | --- | --- |
225
+ | | 페이지 제목 (h1) | `typo-heading-lg` | 18px |
226
+ | 블록 | 섹션 제목 | `typo-heading-sm` | 14px |
227
+ | 블록 내부 | 하위 제목 (섹션 안을 더 나눌 때) | `typo-heading-xs` | 12px |
228
+ | — | 본문 | `typo-body-sm-default` | 12px |
229
+ | — | 보조 설명 | `typo-body-sm-default` + `text-fg-tertiary` | 12px / `grey_65` |
154
230
 
155
231
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
156
232
 
233
+ - **섹션 제목의 타이포를 직접 주지 않는다.** `SSectionHeaderCard.Header` 가 `title` 에 이미 넣는다 — 그 위에 `typo-heading-sm` 을 또 씌우지 않는다. 직접 쓰는 경우는 섹션 카드 없이 제목만 세울 때뿐이다.
234
+ - **하위 제목이 필요하면 먼저 섹션을 나눌 수 없는지 본다.** 한 섹션 안에서 제목이 두 단으로 갈린다는 것은 대개 섹션이 둘이라는 뜻이다 (§3-7-8).
235
+ - 본문 안에서 한 단어를 강조할 때는 `typo-body-sm-medium` 을 쓴다. `typo-body-sm-bold` 는 제목 성격의 짧은 라벨에만 쓴다. <!-- TODO(디자인): 강조 굵기 기준 확정 -->
236
+
157
237
  **보조 설명의 색** — 기본은 `text-fg-tertiary`(`grey_65`) 다. 보조 설명 안에서 위계가 한 단계 더 필요할 때만 `text-fg-secondary`(`grey_80`) → `text-fg-tertiary`(`grey_65`) 순으로 내려 쓴다 (§2-3).
158
238
 
159
239
  ### 2-2. 간격 (spacing)
@@ -165,14 +245,14 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
165
245
  - 형제 요소 간격은 margin 대신 부모의 `flex`/`grid` + `gap-sd-*`으로 잡는다.
166
246
  - 시맨틱 간격 토큰 (텍스트 덩어리·요소 사이 기본 리듬):
167
247
 
168
- 화면은 세 층이다. **여백(padding)과 간격(gap)은 서로 다른 축이고, 규칙도 다르다.**
248
+ **여백(padding)과 간격(gap)은 서로 다른 축이고, 규칙도 다르다.** 어느 쪽이든 값은 §2-0 의 층이 정한다.
169
249
 
170
250
  ```text
171
- 페이지 프레임 SPage 가 여백을 넣는다. 직접 주지 않는다
172
- └ 블록 페이지 직계 자식 (헤더·필터·툴바·테이블·섹션 카드)
173
- 블록 블록 간격은 gap-sd-12
174
- └ 섹션·패널 블록 중 "안에 콘텐츠를 담는 컨테이너"인 것
251
+ (SPage) 여백을 SPage 가 넣는다. 직접 주지 않는다
252
+ └ 블록 블록 블록 간격은 gap-sd-12
253
+ └ 담는 블록 "안에 콘텐츠를 담는" 블록 (SSectionHeaderCard·SCard 등, §2-0)
175
254
  여기에만 안쪽 여백 선택지가 있다 (아래 "섹션·패널 안쪽 여백")
255
+ └ 요소 요소 ↔ 요소 간격은 gap-sd-8 이 기본
176
256
  ```
177
257
 
178
258
  | 상황 | 값 |
@@ -321,6 +401,124 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
321
401
 
322
402
  > ⚠️ 초안(개발 작성). <!-- TODO(디자인): 전체 검수·확정 --> 표시가 있는 행은 디자인 확정 전까지 초안 기준으로 사용.
323
403
 
404
+ ### 3-0. 의도 → 컴포넌트 라우팅 (여기서 시작한다)
405
+
406
+ **§3 의 입구는 이 표다.** 아래 §3-1 부터는 계열별로 정리돼 있어서 "내가 만들 게 어느 계열인지"를 이미 알아야 펼 수 있다. 그런데 실제 출발점은 계열이 아니라 **하려는 일**이다. 그 문장을 여기서 찾으면 답이 하나 나온다.
407
+
408
+ - **읽는 법**: 왼쪽에서 하려는 일을 찾고 → 가운데 컴포넌트를 쓴다. 오른쪽에 § 참조가 있으면 그 자리는 답이 갈리므로 **반드시 그 절을 읽고 고른다.** 참조가 없으면 더 볼 것 없이 그대로 쓴다.
409
+ - 이 표는 §0-1 인덱스 전체를 덮는다 (`npm run check:routing` 이 강제). **여기에 해당하는 일이 없으면 대응 컴포넌트가 없는 것이므로** §1-1 의 예외 규칙(순수 레이아웃 요소)으로 간다.
410
+
411
+ #### A. 값을 입력받는다
412
+
413
+ | 하려는 일 | 컴포넌트 | 갈림 |
414
+ | --- | --- | --- |
415
+ | 한 줄 텍스트를 받는다 | `SInput` | §3-7-1 |
416
+ | 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
417
+ | 숫자(수량·금액)를 받는다 | `SNumberInput` | |
418
+ | 바코드를 스캔해 받는다 | `SBarcodeInput` | |
419
+ | 목록에서 하나 고르게 한다 | `SSelect` | §3-7-2 |
420
+ | 선택지를 항상 펼쳐 두고 하나 고르게 한다 | `SRadioGroup` | §3-7-2 |
421
+ | 버튼 모양으로 모드를 하나 고르게 한다 | `SRadioButton` | §3-7-2 |
422
+ | 라디오 하나를 표 셀 등에 직접 배치한다 | `SRadio` | §3-7-2 |
423
+ | 여러 개를 고르게 한다 / 동의를 받는다 | `SCheckbox` | §3-7-3 |
424
+ | 켜는 즉시 반영되는 설정을 준다 | `SSwitch` | §3-7-3 |
425
+ | 목록을 좁히는 필터를 켜고 끄게 한다 | `SToggle` | §3-7-3 |
426
+ | 자유 입력값을 여러 개 쌓게 한다 | `SChipInput` | §3-1 |
427
+ | 입력된 값 하나를 지우거나 고치게 한다 | `SChip` | §3-1 |
428
+ | 파일을 받는다 | `SFilePicker` | |
429
+ | 날짜 하나를 받는다 | `SDatePicker` | §3-7-4 |
430
+ | 날짜 기간을 받는다 | `SDateRangePicker` | §3-7-4 |
431
+ | 시각 하나를 받는다 | `STimePicker` | |
432
+ | 시각 범위를 받는다 | `STimeRangePicker` | |
433
+ | 달력 자체를 화면에 펼쳐 보여준다 | `SCalendar` | §3-7-4 |
434
+ | 컨트롤에 라벨·필수·에러를 붙인다 | `SField` | §3-7-5 |
435
+ | 입력 여러 개를 묶어 한 번에 검증한다 | `SForm` | §4-3 |
436
+ | 폼·필터를 표 형태로 배치한다 | `SKeyValueTable` | §4 |
437
+
438
+ #### B. 정보를 읽게 보여준다
439
+
440
+ | 하려는 일 | 컴포넌트 | 갈림 |
441
+ | --- | --- | --- |
442
+ | 여러 건을 여러 열로 보여주고 열끼리 비교하게 한다 | `STable` | §3-7-6 |
443
+ | 항목 하나의 속성들을 `라벨: 값` 으로 보여준다 | `SKeyValueTable` | §4-4 |
444
+ | 한 줄로 읽히는 항목을 세로로 나열한다 | `SList` + `SListItem` | §3-7-6 |
445
+ | 나열한 항목을 펼쳐 하위 내용을 보여준다 | `SExpansionList` + `SExpansionItem` | §3-7-7 |
446
+ | 부모-자식 계층을 들여쓰기로 보여준다 | `STree` | §3-7-7 |
447
+ | 사용자가 순서를 드래그로 바꾸게 한다 | `SDraggableList` + `SDraggableItem` | §3-7-6 |
448
+ | 표 위에 건수 요약과 액션을 얹는다 | `STableBar` | §4-2 |
449
+ | 상태·분류를 라벨로 찍는다 | `STag` | §3-1 |
450
+ | 색 점만으로 상태를 찍는다 | `SBadge` | §3-1 |
451
+ | 아이콘을 넣는다 | `SIcon` | |
452
+ | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
453
+
454
+ #### C. 동작을 실행시킨다
455
+
456
+ | 하려는 일 | 컴포넌트 | 갈림 |
457
+ | --- | --- | --- |
458
+ | 라벨이 있는 일반 액션을 준다 | `SButton` | §3-5 |
459
+ | 아이콘 하나로 뜻이 통하는 부가 조작을 준다 | `SGhostButton` | §3-5-5 |
460
+ | 한 버튼에 여러 선택지를 매단다 | `SDropdownButton` | §3-5-4 |
461
+
462
+ #### D. 화면을 담고 나눈다
463
+
464
+ | 하려는 일 | 컴포넌트 | 갈림 |
465
+ | --- | --- | --- |
466
+ | 앱 셸(상단바 + 내비 + 본문)을 세운다 | `SLayout` | §4-1 |
467
+ | 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
468
+ | 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
469
+ | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
470
+ | 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
471
+ | 가로선으로 끊는다 | `SDivider` | §3-6 |
472
+ | 사용자가 영역 크기를 조절하게 한다 | `SSplitter` | §3-6 |
473
+ | 특정 영역 안에서만 스크롤시킨다 | `SScrollArea` | |
474
+
475
+ #### E. 다른 곳으로 이동시킨다
476
+
477
+ | 하려는 일 | 컴포넌트 | 갈림 |
478
+ | --- | --- | --- |
479
+ | 같은 화면에서 보는 관점을 바꾼다 | `STabs` | §3-7-2 |
480
+ | 긴 목록을 페이지로 끊는다 | `SPagination` | §4-2 |
481
+ | 여러 단계의 진행 위치를 보여준다 | `SStepper` | |
482
+
483
+ #### F. 흐름을 끊고 띄운다
484
+
485
+ > 이 그룹은 **전부 §3-3-1 판별 순서를 먼저 밟는다.** 아래는 그 결과를 되짚는 표다.
486
+
487
+ | 하려는 일 | 컴포넌트 | 갈림 |
488
+ | --- | --- | --- |
489
+ | 실행 여부만 확정받는다 | `SModal.confirm()` | §3-3-1 |
490
+ | 모달 안에서 작성·선택하게 한다 | `SActionModal` + `SModal.create()` | §3-3-1 |
491
+ | 띄우는 것 자체가 하나의 화면이다 | `SPopup` | §3-3-1 |
492
+ | 화면 옆에서 밀려 나오는 작업 패널을 연다 | `SDrawer` | §3-3-5 |
493
+ | 확인 다이얼로그를 화면에 직접 배치한다 | `SConfirmModal` | §3-3-4 |
494
+ | 클릭하면 상호작용 가능한 작은 콘텐츠를 띄운다 | `SPopover` | §3-3 |
495
+ | hover 하면 짧은 설명을 띄운다 | `STooltip` | §3-3 |
496
+ | 임의 요소에 붙는 저수준 레이어가 필요하다 | `SPortal` | §3-7-10 |
497
+
498
+ #### G. 알리고 안내한다
499
+
500
+ | 하려는 일 | 컴포넌트 | 갈림 |
501
+ | --- | --- | --- |
502
+ | 화면에 상시 노출되는 안내·경고를 둔다 | `SCallout` | §3-2 |
503
+ | 작업 결과를 일시적으로 알린다 | `SToast` | §3-2 |
504
+ | 기능 온보딩·도움말을 붙인다 | `SGuide` | §3-2 |
505
+
506
+ #### H. 기다리게 한다
507
+
508
+ | 하려는 일 | 컴포넌트 | 갈림 |
509
+ | --- | --- | --- |
510
+ | 화면 전체를 잠그고 기다리게 한다 | `SLoadingModal` (또는 `SModal.loading()`) | §3-2 |
511
+ | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-2 |
512
+ | 진행률을 가로 막대로 보여준다 | `SLinearProgress` | §3-7-9 |
513
+ | 진행률·대기를 원형으로 보여준다 | `SCircleProgress` | §3-7-9 |
514
+
515
+ #### I. 앱을 켤 때 한 번만 (§4-1)
516
+
517
+ | 하려는 일 | 컴포넌트 | 갈림 |
518
+ | --- | --- | --- |
519
+ | `SModal.*` 로 띄운 모달이 그려질 자리를 만든다 | `SModalOutlet` | §4-1 |
520
+ | `SToast` 가 그려질 자리를 만든다 | `SToastContainer` | §4-1 |
521
+
324
522
  ### 3-1. 라벨/표시류 — STag vs SBadge vs SChip
325
523
 
326
524
  | 상황 | 사용 |
@@ -460,7 +658,7 @@ function OrderModal({ orderId, open, onOpenChange, onClose, modalRef }: OrderMod
460
658
  width={720}
461
659
  // 주 액션은 button(단수), 보조 버튼은 footerLeft — 하단 버튼 양끝 분리 규칙과 같다
462
660
  button={{ label: '접수', onClick: () => modalRef.ok() }}
463
- footerLeft={<SButton color="neutral" outline label="취소" onClick={() => modalRef.cancel()} />}
661
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" onClick={() => modalRef.cancel()} />}
464
662
  >
465
663
  <SKeyValueTable fields={orderFields} values={order} />
466
664
  </SActionModal>
@@ -473,6 +671,10 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
473
671
  .onDismissed(() => {});
474
672
  ```
475
673
 
674
+ **하단 버튼을 본문(children)에 직접 두지 않는다.** 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다 — 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡히는 자리다. `button` 은 클릭해도 **모달을 닫지 않으므로**(`onClick` 만 발화) 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 되고, 그 때문에 본문에 버튼을 따로 둘 이유가 없다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
675
+
676
+ **모달 안에서도 앱의 훅을 그냥 쓴다 — 단, 앱 루트에 `SModalOutlet` 이 있어야 한다 (§4-1).** outlet 이 있으면 명령형 모달이 앱 렌더 트리의 자식으로 그려지므로 `useQuery`·`useNavigate`·`useTheme` 같은 Context 기반 훅이 페이지에서와 똑같이 동작한다. **모달 컴포넌트를 Provider 로 다시 감싸지 않는다.** outlet 없이 띄우면 모달이 별도 React 루트로 떠서 Provider 가 하나도 닿지 않고, `No QueryClient set` 처럼 모달을 여는 순간에만 터진다.
677
+
476
678
  #### 3-3-5. 닫기 경로 — `persistent` 기본값은 컴포넌트마다 다르다
477
679
 
478
680
  **입력을 담는 모달은 백드롭 클릭·ESC 로 닫히지 않는 것이 기본**이다. 닫기 시도는 흔들림(shake)으로만 반응한다. 작성 중인 내용을 실수로 잃지 않게 하기 위한 것이다. 반면 `SConfirmModal` 은 잃을 입력이 없으므로 **백드롭·ESC 로 닫히는 것이 기본**이다.
@@ -723,12 +925,156 @@ const columns: STableColumn[] = [
723
925
  </div>
724
926
  ```
725
927
 
928
+ ### 3-6. 영역 나누기 — SDivider vs SSplitter
929
+
930
+ | 상황 | 사용 |
931
+ | --- | --- |
932
+ | 두 영역 사이에 **선만** 그을 때 | `SDivider` — 위치·두께가 고정된 구분선이다 |
933
+ | 사용자가 **경계를 끌어 넓이를 바꿀 수 있어야** 할 때 | `SSplitter` — 두 패널을 감싸고 경계 위치를 소유한다 |
934
+
935
+ `SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
936
+
937
+ ```tsx
938
+ <SSplitter defaultValue={30} limits={[20, 60]}>
939
+ <SSplitter.Before>내비게이션</SSplitter.Before>
940
+ <SSplitter.After>본문</SSplitter.After>
941
+ </SSplitter>
942
+ ```
943
+
944
+ - 자식은 `SSplitter.Before` 와 `SSplitter.After` **둘뿐**이고, 루트의 직접 자식이어야 한다.
945
+ - 크기 단위는 `unit` 이 정한다. 기본 `'%'` 는 창이 바뀌어도 비율을 유지하고, `'px'` 는 폭을 유지한다. **사이드바처럼 폭이 고정돼야 하는 자리는 `'px'`**, 화면을 비율로 나누는 자리는 기본값 그대로 둔다.
946
+ - 본문이 읽을 수 없을 만큼 좁아지지 않도록 `limits={[최소, 최대]}` 를 준다. 생략하면 `'%'` 는 `[10, 90]`, `'px'` 는 `[50, Infinity]`.
947
+ - 각 패널은 넘치는 만큼 **스스로 스크롤한다.** 패널 안에 `SScrollArea` 를 겹쳐 넣지 않는다.
948
+ - 모델은 항상 **첫 패널**(`SSplitter.Before`) 크기다. 사이드가 기준인 화면이면 사이드를 `Before` 에 둔다.
949
+ - 앱 셸의 GNB 폭은 `SGnb` 가 소유한다. `SLayout`/`SGnb` 를 `SSplitter` 로 감싸지 않는다 — **GNB 폭을 끌 수 있게 하려면 `SGnb` 에 `resizable` 을 준다**(§4-1).
950
+
951
+ **Quasar `q-splitter` 에서 옮겨올 때** — `unit` · `limits` · `emitImmediately` 는 이름과 뜻이 같고, 나머지는 아래처럼 바뀐다. `emitImmediately` 를 주지 않으면 **드래그를 놓는 순간 한 번만** `onValueChange` 가 온다.
952
+
953
+ | q-splitter | SSplitter |
954
+ | --- | --- |
955
+ | `horizontal` (상/하 분할) | **`vertical`** — 이 저장소는 `STabs`·`SStepper` 와 같이 "세로 **배치**" 로 읽는다 |
956
+ | `v-model` | `value` + `onValueChange` (또는 `defaultValue`) |
957
+ | `disable` | `disabled` |
958
+ | `before` / `after` 슬롯 | `SSplitter.Before` / `SSplitter.After` |
959
+ | `before-class` / `after-class` | 각 슬롯에 `className` 을 직접 |
960
+ | `separator-class` / `separator-style` | `dividerClassName` / `dividerStyle` |
961
+ | `reverse` · `dark` | 없음 — 모델은 항상 첫 패널 크기다 |
962
+
963
+ ### 3-7. 나머지 판별 — 입력·목록·컨테이너
964
+
965
+ > §3-0 라우팅에서 이 절을 가리키는 자리들이다. <!-- TODO(디자인): 전체 검수·확정 -->
966
+
967
+ #### 3-7-1. SInput vs STextarea
968
+
969
+ **줄 수가 아니라 값의 성격으로 고른다.** 값의 길이를 미리 알 수 있으면 `SInput`, 없으면 `STextarea` 다.
970
+
971
+ | 값 | 사용 |
972
+ | --- | --- |
973
+ | 이름·코드·전화번호·URL 처럼 형식이 정해진 값 | `SInput` |
974
+ | 메모·사유·설명처럼 길이가 예측되지 않는 문장 | `STextarea` |
975
+
976
+ 값이 길어질 수 있는데 `SInput` 을 쓰면 사용자가 자기가 쓴 것을 다시 읽지 못한다 — 한 줄 안에서 좌우로 스크롤해야 하기 때문이다. 반대로 짧은 값에 `STextarea` 를 쓰면 빈 공간이 남아 입력량을 잘못 기대하게 한다.
977
+
978
+ #### 3-7-2. 하나를 고르게 하는 다섯 — SSelect vs SRadioGroup vs SRadioButton vs STabs vs SRadio
979
+
980
+ **먼저 "고르면 무엇이 바뀌는가"를 본다.**
981
+
982
+ 1. **화면의 내용이 통째로 바뀐다** → `STabs`. 값을 고르는 게 아니라 보는 관점을 바꾸는 것이다. 폼 값이 아니다.
983
+ 2. 그 밖에는 폼 값이므로 **선택지 수와 노출 여부**로 고른다.
984
+
985
+ | 선택지 | 사용 |
986
+ | --- | --- |
987
+ | 6개 이상, 또는 서버에서 오는 동적 목록 | `SSelect` |
988
+ | 2~5개 고정 + 선택지를 항상 보여야 함 | `SRadioGroup` |
989
+ | 2~4개 + 짧은 라벨의 배타적 모드 전환 (세그먼트) | `SRadioButton` |
990
+
991
+ - **`SRadio` 를 직접 나열하지 않는다.** 그룹 간격은 `SRadioGroup` 이 맞춘다 (§2-2). `SRadio` 단독은 `SRadioGroup` 이 만들 수 없는 배치 — 표 셀 안에 행마다 하나씩 놓는 경우 — 에만 쓴다.
992
+ - `SRadioButton` 은 `options` 를 통째로 받는 세그먼트 컨트롤이라 `SRadio` 를 여러 개 넣는 게 아니다.
993
+
994
+ #### 3-7-3. 켜고 끄는 셋 — SCheckbox vs SSwitch vs SToggle
995
+
996
+ 셋 다 on/off 지만 **값이 언제 반영되는지**가 다르다. 이걸 틀리면 사용자가 저장 버튼을 찾다가 못 찾거나, 눌렀는데 반영이 안 돼 다시 누른다.
997
+
998
+ | 판별 | 사용 |
999
+ | --- | --- |
1000
+ | **폼 값으로 제출된다** (저장 버튼을 눌러야 반영) | `SCheckbox` |
1001
+ | **누르는 즉시 반영된다** (저장 버튼 없음) | `SSwitch` |
1002
+ | **목록을 좁히는 필터** (여러 개를 나란히 켜고 끔) | `SToggle` |
1003
+
1004
+ - `SCheckbox` 만 다중 선택(배열)과 `indeterminate`(부분 선택)를 갖는다. 전체 선택 체크박스는 반드시 `SCheckbox` 다.
1005
+ - 약관 동의처럼 **제출 시점에 값이 필요한 것은 항상 `SCheckbox`** 다 — 모양이 스위치에 가까워 보여도 그렇다.
1006
+ - `SToggle` 은 알약형 버튼이라 여러 개를 가로로 늘어놓는 필터 자리에 맞는다. 설정 화면의 on/off 한 줄에는 쓰지 않는다.
1007
+
1008
+ #### 3-7-4. 날짜 셋 — SDatePicker vs SDateRangePicker vs SCalendar
1009
+
1010
+ | 판별 | 사용 |
1011
+ | --- | --- |
1012
+ | 날짜 **하나**를 값으로 받는다 | `SDatePicker` |
1013
+ | **시작~종료** 를 값으로 받는다 | `SDateRangePicker` |
1014
+ | 달력 격자 **자체가 화면 콘텐츠** 다 (일정·이벤트 보기) | `SCalendar` |
1015
+
1016
+ - **기간을 `SDatePicker` 두 개로 만들지 않는다.** 시작이 종료보다 뒤인 입력을 막는 검증과 한쪽만 고른 중간 상태 처리가 `SDateRangePicker` 안에 이미 있다. 두 개로 쪼개면 그게 전부 앱 몫이 된다.
1017
+ - `SDatePicker`·`SDateRangePicker` 는 내부적으로 `SCalendar` 를 팝오버로 띄운다. 값을 받는 자리에 `SCalendar` 를 직접 쓰지 않는다.
1018
+
1019
+ #### 3-7-5. SField 를 직접 쓰는 경우
1020
+
1021
+ **거의 없다.** `SInput`·`SNumberInput`·`STextarea`·`SSelect`·날짜/시간 피커는 이미 내부에서 `SField` 를 쓰고 있어서 `label`·`required`·에러 표시를 자기 prop 으로 받는다. 그 위에 `SField` 를 한 겹 더 감싸면 라벨이 두 번 나온다.
1022
+
1023
+ 직접 쓰는 경우는 하나뿐이다 — **디자인 시스템에 없는 컨트롤**에 다른 필드와 똑같은 라벨·필수·에러 모양을 붙일 때.
1024
+
1025
+ #### 3-7-6. 여러 건을 나열하는 셋 — STable vs SList vs SDraggableList
1026
+
1027
+ | 판별 | 사용 |
1028
+ | --- | --- |
1029
+ | 열이 둘 이상이고 **열끼리 값을 비교**한다 (정렬·합계·자릿수 맞춤) | `STable` |
1030
+ | 한 항목이 **한 줄로 읽힌다** (제목 + 보조 텍스트) | `SList` + `SListItem` |
1031
+ | **순서 자체가 데이터**라 사용자가 끌어서 바꾼다 | `SDraggableList` + `SDraggableItem` |
1032
+
1033
+ - **항목 하나의 속성을 나열하는 것은 목록이 아니다.** `라벨: 값` 이 세로로 쌓이는 것은 `SKeyValueTable` 이다 (§4-4).
1034
+ - `SList` 는 레이아웃만 담당한다. 펼침·단일 선택 동작이 필요하면 `SExpansionList` 다 (§3-7-7).
1035
+
1036
+ #### 3-7-7. 펼치는 셋 — SExpansionItem vs SExpansionList vs STree
1037
+
1038
+ | 판별 | 사용 |
1039
+ | --- | --- |
1040
+ | 항목들이 **서로 독립적으로** 여닫힌다 (여러 개 동시에 열려도 됨) | `SExpansionItem` 단독 |
1041
+ | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
1042
+ | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
1043
+
1044
+ `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다.
1045
+
1046
+ #### 3-7-8. SCard vs SSectionHeaderCard
1047
+
1048
+ | 판별 | 사용 |
1049
+ | --- | --- |
1050
+ | **제목이 붙는 섹션** 이다 | `SSectionHeaderCard` |
1051
+ | 제목 없이 **흰 면만** 필요하다 (요약 타일, 빈 상태 박스) | `SCard` |
1052
+
1053
+ - 페이지 골격에서 콘텐츠를 묶는 섹션은 **사실상 전부 `SSectionHeaderCard`** 다 (§4-4·§4-5). 제목·필수 표시·도움말·헤더 우측 액션이 전부 여기 붙는다.
1054
+ - **카드 안에 카드를 겹치지 않는다.** 섹션 안을 더 나눠야 하면 `SDivider` 로 끊거나(§3-6) 섹션을 둘로 분리한다.
1055
+ - 안쪽 여백은 `SSectionHeaderCard.Body` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1056
+
1057
+ #### 3-7-9. SLinearProgress vs SCircleProgress
1058
+
1059
+ | 판별 | 사용 |
1060
+ | --- | --- |
1061
+ | 진행률(%)이 있고 가로로 길게 놓을 자리가 있다 | `SLinearProgress` |
1062
+ | 자리가 좁다, 또는 **끝나는 시점을 모른다**(대기) | `SCircleProgress` |
1063
+
1064
+ `SCircleProgress` 는 `indeterminate` 로 두면 스피너가 된다. **다만 화면이나 영역을 막아야 하는 상황이면 progress 가 아니라 `SLoadingModal`·`SLoadingContainer` 다** (§3-2) — 진행 표시와 입력 차단은 다른 일이고, 막지 않으면 사용자가 로딩 중에 또 누른다.
1065
+
1066
+ #### 3-7-10. SPortal — 직접 쓸 일이 거의 없다
1067
+
1068
+ `STooltip`·`SPopover`·`SSelect`·날짜 피커가 내부에서 쓰는 저수준 레이어다. 앵커에 붙여 띄우는 동작이 필요하면 **먼저 §3-3 에서 대응 컴포넌트를 찾는다.** `SPortal` 을 직접 쓰는 것은 그 넷 중 어느 것도 아닌 새로운 부착형 레이어를 만들 때뿐이고, 그때도 모달 안에서 열릴 수 있다면 소속 컨테이너를 맞춰야 한다.
1069
+
726
1070
  ---
727
1071
 
728
1072
  ## 4. 페이지 레시피 — 표준 골격
729
1073
 
730
1074
  > 새 페이지는 반드시 아래 골격에서 시작한다. 임의 골격을 발명하지 않는다.
731
1075
  >
1076
+ > **아래 레시피는 §2-0 조합 문법으로 유도된 결과다.** 여기 없는 화면(대시보드·설정·마법사 등)을 만들 때는 임의로 짜지 말고 §2-0 의 층 구조·포함 규칙·블록 순서로 직접 유도한다.
1077
+ >
732
1078
  > **핵심 원칙 — 표 형태의 정보는 `SKeyValueTable` 로 만든다.** 필터·등록/수정 폼·상세 정보가 모두 여기 해당한다.
733
1079
  > `SField` 컨트롤을 `div` 로 직접 나열해 폼을 만들지 않는다.
734
1080
 
@@ -758,6 +1104,30 @@ export default function AppShell({
758
1104
  }
759
1105
  ```
760
1106
 
1107
+ **GNB 폭을 사용자가 조절하게 하려면 `SGnb` 에 `resizable` 을 준다.** 메뉴 오른쪽 경계가 조절선이 되고, 레일 폭은 고정된 채 메뉴 컬럼만 늘고 준다. 범위는 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
1108
+
1109
+ ```tsx
1110
+ {/* 폭을 기억해야 하면 menuWidth 를 앱이 쥐고 onMenuWidthChange 로 되받아 저장한다.
1111
+ 초기값만 정하면 되면 defaultMenuWidth 하나로 끝난다. */}
1112
+ <SGnb items={MENU} value={current} onValueChange={navigate} resizable defaultMenuWidth={240} />
1113
+ ```
1114
+
1115
+ `onMenuWidthChange` 는 **드래그를 놓는 순간**(또는 방향키 조작) 한 번만 온다 — 저장 로직을 그대로 붙여도 프레임마다 쓰이지 않는다. 접혀 있거나 레일 리프가 활성이라 깔 메뉴가 없으면 조절선은 나오지 않는다.
1116
+
1117
+ **앱 부트스트랩에는 `<SModalOutlet />` 을 한 번 렌더한다 — Provider 안쪽에 둔다.**
1118
+
1119
+ ```tsx
1120
+ import { SModalOutlet } from 'sellmate-design-system-react';
1121
+
1122
+ // 앱 진입점 (main.tsx / App.tsx) — 앱 전체에 하나. 위치는 Provider 안쪽이기만 하면 된다
1123
+ <QueryClientProvider client={queryClient}>
1124
+ <RouterProvider router={router} />
1125
+ <SModalOutlet />
1126
+ </QueryClientProvider>;
1127
+ ```
1128
+
1129
+ `SModal.confirm/loading/create` 로 띄운 모달이 그려지는 자리다. outlet 이 앱 트리 안에 있어야 모달이 앱의 Context(QueryClient·Router·Theme 등)를 상속한다 — outlet 이 없으면 모달은 뜨지만 별도 React 루트라 Provider 가 닿지 않는다(§3-3-4). 모달은 언제나 `body` 로 portal 되므로 outlet 을 어디에 두든 레이아웃에는 영향이 없고, **셸 안에 넣을 필요도 없다.** 토스트를 쓴다면 `SToastContainer` 도 같은 자리에 둔다.
1130
+
761
1131
  **최소 너비는 `SLayout` 이 보장한다 — 앱이 `min-w-*` 를 직접 주지 않는다.** 창이 최소 너비(`SLAYOUT_MIN_WIDTH`, GNB 포함한 전체 기준)보다 좁아지면 GNB·상단바는 제자리에 남고 **`SPage` 안에만 가로 스크롤이 생긴다.** 문서(브라우저 창)에는 가로 스크롤이 생기지 않는다. 그래서 셸을 감싸는 요소에 `min-width` 나 `overflow-x` 를 걸지 않는다 — 걸면 창 전체가 스크롤되어 GNB 가 화면 밖으로 밀린다.
762
1132
 
763
1133
  **셸의 `SPage` 는 모든 페이지가 공유하므로, 스크롤 끝 여백을 끄려면 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` 을 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `false` 를 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(켬)이 적용된다.
@@ -791,6 +1161,22 @@ export default function AppShell({
791
1161
  </SLayout>
792
1162
  ```
793
1163
 
1164
+ **메뉴 목록과 함께 스크롤되면 안 되는 것은 `SGnb` 의 위아래 고정 슬롯에 둔다.** 레일과 메뉴에 각각 위(`railTop`·`menuTop`)와 아래(`railFooter`·`menuFooter`) 슬롯이 있다. 아이템이 많아 넘치면 **목록만 스크롤되고 이 슬롯들은 제자리에 남는다** — 메뉴 검색, 워크스페이스 전환, 계정 행처럼 항상 보여야 하는 것이 여기 온다. 레일 슬롯은 `useRail` 일 때만, 메뉴 슬롯은 깔 메뉴가 있을 때만 렌더된다.
1165
+
1166
+ **접으면 레일·메뉴가 통째로 빠져나가면서 그 슬롯들도 함께 사라진다.** 접힌 상태에서도 남겨야 할 것은 `foldedTop`·`foldedFooter` 로 따로 준다 — `header="fix"` 로 접혔을 때만 나타나며, 폭이 좁은 폴드 레일이므로 아이콘 버튼 하나 정도로 줄인다.
1167
+
1168
+ ```tsx
1169
+ {/* 접히면 menuTop·menuFooter 가 함께 빠지므로, 폴드 레일에 남길 것만 foldedTop 으로 따로 준다 */}
1170
+ <SGnb
1171
+ items={MENU} value={current} onValueChange={navigate} useRail
1172
+ menuTop={<SInput value={keyword} onValueChange={setKeyword} placeholder="메뉴 검색" />}
1173
+ menuFooter={<AccountRow />}
1174
+ foldedTop={<SGhostButton icon="search" size="sm" ariaLabel="메뉴 검색" onClick={openSearch} />}
1175
+ />
1176
+ ```
1177
+
1178
+ 슬롯 안쪽 여백은 **슬롯 내용이 직접 갖는다** — 컴포넌트는 자리만 잡는다(폴드 슬롯만 좁은 폭에 맞춰 가운데 정렬한다). 메뉴 폭은 `resizable` 로 바뀔 수 있으므로 슬롯 내용은 고정 폭 대신 `w-full` 로 따라가게 둔다.
1179
+
794
1180
  ### 4-2. 목록 페이지 (필터 + 테이블)
795
1181
 
796
1182
  구조: **페이지 헤더(제목 + 가이드 링크) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
@@ -1077,6 +1463,14 @@ export default function ProductDetailPage() {
1077
1463
  - [ ] `SGhostButton` 의 `intent` 가 조작 성격과 맞는가 (되돌릴 수 없는 삭제만 `danger`, 진입·추가는 `action`, 나머지는 `default`)
1078
1464
  - [ ] 창을 띄울 때 §3-3-1 판별 순서를 따랐는가 (그 자체가 화면 → `SPopup` / 실행 여부만 확정 → `SModal.confirm` / 모달 안에서 작성 → `SActionModal`)
1079
1465
  - [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
1466
+ - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4)
1467
+ - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
1468
+ - [ ] 고른 컴포넌트를 §2-0 의 제 층에 놓았는가 (요소를 `SPage` 에 직접 놓지 않았는가, 블록을 `div` 로 감싸지 않았는가)
1469
+ - [ ] §2-0 포함 규칙을 지켰는가 (카드 안 카드 ❌, 표 셀 안 블록 ❌)
1470
+ - [ ] 블록 순서가 §2-0 순서와 맞는가 (제목 → 안내 → 필터 → 툴바 → 본문 → 페이지네이션 → 하단 액션)
1471
+ - [ ] 제목 위계가 층을 따라갔는가 (18 → 14 → 12, 건너뛰기 ❌), 섹션 제목 타이포를 `SSectionHeaderCard` 위에 덧씌우지 않았는가
1472
+ - [ ] 화면 요소마다 §3-0 라우팅에서 컴포넌트를 골랐는가 (직접 만들거나 비슷한 것으로 대체하지 않았는가)
1473
+ - [ ] §3-0 의 "갈림" 열이 가리킨 판별 절을 읽고 골랐는가 (§3-7-2 선택 컨트롤, §3-7-3 켜고 끄기 등)
1080
1474
  - [ ] 상태 표시·알림·확인 다이얼로그가 §3의 선택 규칙을 따르는가
1081
1475
 
1082
1476
  ---
@@ -1204,6 +1598,10 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1204
1598
  | typo-table-header | 12px | 500 | 20px |
1205
1599
  | typo-table-body | 12px | 400 | 20px |
1206
1600
  | typo-table-accent | 12px | 700 | 20px |
1601
+ | typo-item-sm-default | 12px | 500 | 20px |
1602
+ | typo-item-sm-selected | 12px | 700 | 20px |
1603
+ | typo-item-md-default | 14px | 500 | 24px |
1604
+ | typo-item-md-selected | 14px | 700 | 24px |
1207
1605
 
1208
1606
  ### 시맨틱 색 토큰 (var() 참조 전용 — 예: bg-[var(--sys-color-bg-frame)])
1209
1607
  --sys-color-bg-accent
@@ -1921,6 +2319,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1921
2319
  | `selected?` | `boolean` | `false` | 선택 상태 여부 |
1922
2320
  | `dragging?` | `boolean` | `false` | 드래그 중인 스타일을 고정해서 표시할지 여부 |
1923
2321
  | `dense?` | `boolean` | `false` | 조밀한 높이 사용 여부 |
2322
+ | `size?` | `SDraggableItemSize` | `'sm'` | 타이포그래피·세로 패딩 크기 |
1924
2323
  | `disabled?` | `boolean` | `false` | 비활성 상태 여부 |
1925
2324
  | `dragOverlay?` | `boolean` | `true` | 드래그 시 마우스를 따라가는 overlay 표시 여부 |
1926
2325
  | `dragOverlayOpacity?` | `number` | `0.75` | 드래그 overlay 투명도 |
@@ -2013,10 +2412,10 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2013
2412
  | `open?` | `boolean` | — | 표시 여부 |
2014
2413
  | `persistent?` | `boolean` | `true` | backdrop·ESC로 닫히지 않고 흔들림 효과를 준다. **기본값 true** — Drawer는 내용을 작성·구성하는 곳이라 임의 닫힘을 막는 것이 기본이다. 닫기 경로는 X 버튼과 footer 버튼뿐이다. false로 주면 backdrop·ESC 닫기가 열린다. |
2015
2414
  | `title?` | `string` | `''` | 접근성 제목 및 헤더 제목 |
2016
- | `width?` | `number \| string` | `572` | Drawer 너비. 기본값은 Figma drawer 기준 572px이다. |
2017
- | `resizable?` | `boolean` | `false` | true면 왼쪽 테두리를 드래그해 너비를 조절할 수 있다. |
2018
- | `minWidth?` | `number \| string` | — | 리사이즈 가능한 최소 너비(px, %, vw, vh) |
2019
- | `maxWidth?` | `number \| string` | — | 리사이즈 가능한 최대 너비(px, %, vw, vh) |
2415
+ | `width?` | `number \| string` | `572` | Drawer 너비. 기본값은 Figma drawer 기준 572px이다. 창이 이보다 좁으면 창 폭까지만 넓어진다 — 드로어가 화면 밖으로 나가지 않는다. `resizable` 로 조절한 폭은 이 값이 바뀔 때 되돌아간다 — 앱이 폭을 저장해 두었다가 다시 열 때 넘겨주면 그 폭으로 열린다. |
2416
+ | `resizable?` | `boolean` | `false` | 왼쪽 테두리를 끌어 너비를 조절할 수 있게 한다. 조절선은 패널 왼쪽 경계 전체이며 평소엔 보이지 않다가 hover·포커스·조절 중에만 드러난다(SSplitter·SGnb 와 같은 선). 포커스를 받아 방향키(Shift 는 크게)·Home·End 로도 조절된다. |
2417
+ | `minWidth?` | `number \| string` | — | 리사이즈 가능한 최소 너비(px, %, vw, vh). % 는 viewport 기준이며, 창 폭보다 클 수 없다 |
2418
+ | `maxWidth?` | `number \| string` | — | 리사이즈 가능한 최대 너비(px, %, vw, vh). % 는 viewport 기준이며, 주지 않으면 창 폭이 상한이다 |
2020
2419
  | `footerLeft?` | `ReactNode` | — | footer 좌측 슬롯 |
2021
2420
  | `button?` | `SDrawerButton` | — | 우측 기본 액션 버튼 |
2022
2421
  | `children?` | `ReactNode` | — | |
@@ -2029,6 +2428,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2029
2428
  |-------|------|-------------|
2030
2429
  | `onOpenChange` | `(open: boolean) => void` | 표시 상태 변경 |
2031
2430
  | `onClose` | `() => void` | 닫기(X) 버튼 클릭 |
2431
+ | `onWidthChange` | `(width: number) => void` | 너비가 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
2032
2432
 
2033
2433
  ## Dependencies
2034
2434
 
@@ -2111,6 +2511,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2111
2511
  | `interaction?` | `SExpansionItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
2112
2512
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
2113
2513
  | `dense?` | `boolean` | `false` | |
2514
+ | `size?` | `SExpansionItemSize` | `'sm'` | 타이포그래피 크기 |
2114
2515
  | `disabled?` | `boolean` | `false` | |
2115
2516
 
2116
2517
  #### Events
@@ -2432,9 +2833,15 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2432
2833
  | `folded?` | `boolean` | — | 접힘(레일) 상태. 미지정 시 SLayout 의 folded 를 따른다. |
2433
2834
  | `logo?` | `ReactNode` | — | 상단바 로고 영역 (slot). header="full" 이면 폭이 140px 로 고정된다. |
2434
2835
  | `topContent?` | `ReactNode` | — | 상단바 로고 오른쪽 슬롯 (검색·액션 등). 로고와 16px 띄고 남는 폭을 모두 차지하므로 안에서 자유롭게 정렬한다. 상단바가 전폭인 header="full" 에서만 렌더된다 (fix 는 상단바가 좁은 GNB 컬럼 안이라 놓을 자리가 없다). |
2836
+ | `railTop?` | `ReactNode` | — | 레일 상단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고 이 슬롯은 레일 상단(상단바 바로 아래)에 붙어 고정된다. useRail 일 때만 렌더된다. |
2435
2837
  | `railFooter?` | `ReactNode` | — | 레일 하단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고 이 슬롯은 레일 하단에 붙어 고정된다. useRail 일 때만 렌더된다. |
2838
+ | `menuTop?` | `ReactNode` | — | 메뉴 상단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고 이 슬롯은 메뉴 상단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다. |
2436
2839
  | `menuFooter?` | `ReactNode` | — | 메뉴 하단 고정 슬롯. 메뉴 아이템이 많아 넘치면 메뉴 목록(ul)만 스크롤되고 이 슬롯은 메뉴 하단에 붙어 고정된다. 메뉴가 렌더될 때만(showMenu) 나타난다. |
2840
+ | `foldedTop?` | `ReactNode` | — | 접힘(fix 레일) 상단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu top 도 사라지므로, 48px 폴드 레일 상단(상단바 바로 아래)에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다. |
2437
2841
  | `foldedFooter?` | `ReactNode` | — | 접힘(fix 레일) 하단 고정 슬롯. 접으면 본문이 빠져나가며 rail/menu footer 도 사라지므로, 48px 폴드 레일 바닥에 붙는 별도 슬롯이다. header="fix" 로 접혔을 때만 나타난다. |
2842
+ | `resizable?` | `boolean` | `false` | 메뉴 폭을 드래그로 조절할 수 있게 한다. 레일 폭은 고정이고 **메뉴 컬럼만** 늘고 준다. 조절선은 GNB 컬럼의 오른쪽 경계 전체다 — fix 는 상단바 높이까지, full 은 상단바가 전폭이라 본문 높이까지. 접혀 있거나 깔 메뉴가 없으면(레일 리프가 활성) 조절선이 나오지 않는다. |
2843
+ | `menuWidth?` | `number` | — | 메뉴 폭(px). 주면 controlled — onMenuWidthChange 로 직접 갱신해야 움직인다 |
2844
+ | `defaultMenuWidth?` | `number` | — | 메뉴 초기 폭(px). uncontrolled |
2438
2845
  | `ariaLabel?` | `string` | `'global navigation'` | 메뉴 landmark(nav) 의 접근성 레이블. 한 화면에 nav 가 여럿일 때 구분한다. |
2439
2846
 
2440
2847
  #### Events
@@ -2445,6 +2852,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2445
2852
  | `onRailChange` | `(value: string) => void` | 레일 선택 변경. 레일 아이템을 눌러 패널이 바뀔 때 알린다(선택 상태는 SGnb 가 자체 관리). |
2446
2853
  | `onFoldChange` | `(folded: boolean) => void` | 접힘 토글 (sdFoldChange) |
2447
2854
  | `onLauncherClick` | `() => void` | 앱런처(그리드) 버튼 클릭. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. |
2855
+ | `onMenuWidthChange` | `(width: number) => void` | 메뉴 폭이 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
2448
2856
 
2449
2857
  ## Dependencies
2450
2858
 
@@ -2752,6 +3160,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2752
3160
  | `interaction?` | `SListItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
2753
3161
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
2754
3162
  | `dense?` | `boolean` | `false` | 조밀한 높이 사용 여부 |
3163
+ | `size?` | `SListItemSize` | `'sm'` | 타이포그래피 크기 |
2755
3164
  | `disabled?` | `boolean` | `false` | 비활성 상태 여부 |
2756
3165
 
2757
3166
  ## Dependencies
@@ -2847,6 +3256,8 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2847
3256
 
2848
3257
  호출 시마다 `document.body` 에 컨테이너를 만들어 모달을 렌더하고, 닫힘 애니메이션이 끝나면 자동으로 언마운트한다. 모든 메서드는 체이닝 핸들 [`SModalRef`](#smodalref) 를 반환한다.
2849
3258
 
3259
+ > **앱 부트스트랩에 [`<SModalOutlet />`](#smodaloutlet) 을 한 번 렌더한다.** 그래야 명령형 모달이 앱 렌더 트리의 자식으로 그려져 QueryClient·Router·Theme 등 Context 를 상속한다. 없으면 예전처럼 별도 React 루트로 떠서 **앱의 Provider 가 하나도 닿지 않는다.**
3260
+
2850
3261
  | 메서드 | 띄우는 모달 | 용도 | 주요 콜백/제어 |
2851
3262
  |---|---|---|---|
2852
3263
  | [`SModal.confirm(options)`](#smodalconfirm) | `SConfirmModal` | 확인/취소 | `onOk` / `onCancel` / `onClose` |
@@ -2861,6 +3272,29 @@ import { SModal } from 'sellmate-design-system-react';
2861
3272
 
2862
3273
  ---
2863
3274
 
3275
+ ## SModalOutlet
3276
+
3277
+ 명령형 모달이 **그려지는 자리**. 앱 부트스트랩에서 Provider 안쪽에 **한 번만** 렌더한다. props 는 없다.
3278
+
3279
+ ```tsx
3280
+ import { SModalOutlet } from 'sellmate-design-system-react';
3281
+
3282
+ <QueryClientProvider client={queryClient}>
3283
+ <RouterProvider router={router} />
3284
+ <SModalOutlet /> {/* 앱 전체에 하나 */}
3285
+ </QueryClientProvider>;
3286
+ ```
3287
+
3288
+ `SModal.confirm/loading/create` 는 전역 스토어에 모달을 넣기만 하고, outlet 이 그것을 `createPortal` 로 `body` 에 그린다. **DOM 위치·쌓임 순서는 outlet 유무와 무관하게 같고**, 달라지는 것은 렌더 트리다 — outlet 이 있으면 모달이 앱 트리의 자식이 되어 Context 를 상속한다.
3289
+
3290
+ - outlet 을 어디에 두든(Provider 안쪽이기만 하면) 모달은 `body` 로 portal 되므로 레이아웃·`overflow`·`transform` 의 영향을 받지 않는다.
3291
+ - **호출부 API 는 그대로다.** outlet 도입 전 코드를 고칠 필요가 없다.
3292
+ - outlet 이 없으면 예전처럼 별도 React 루트(`createRoot`)로 마운트되어 모달은 뜨지만 앱의 Provider 가 닿지 않는다 (개발 모드에서 1회 `console.warn`).
3293
+ - 모달 본문이 렌더 중 예외를 던지면 모달만 닫히고 앱 트리는 유지된다 — 원인을 지목하는 `console.error` 가 함께 찍힌다.
3294
+ - 모달이 떠 있는 채로 outlet 이 언마운트되면(앱 언마운트·Provider 교체) 그 모달은 정리되고 `onDismissed` 가 발화한다 — 남아서 되살아나지 않는다.
3295
+
3296
+ ---
3297
+
2864
3298
  ## SModal.confirm
2865
3299
 
2866
3300
  아이콘 + 제목 + 메시지 + 확인/취소 버튼. `type` 에 따라 아이콘·메인 버튼 색이 결정된다.
@@ -2962,30 +3396,40 @@ SModal.create({ component: OrderModal, componentProps: { orderId: 'ORD-001' } })
2962
3396
 
2963
3397
  ### 비동기 제출 — 응답 보고 닫기
2964
3398
 
2965
- SActionModal `button` 푸터 버튼은 클릭 **즉시 닫힌다**. 저장 API 응답에 따라 닫힘 여부를 정해야 하면 푸터 버튼 대신 **본문에 버튼을 두고** `modalRef` 로 닫힘 시점을 직접 제어한다.
3399
+ **하단 버튼은 본문에 직접 두지 않는다.** 액션은 `button`(의도적으로 단수), 보조 버튼은 `footerLeft` 슬롯에 넣는다 그래야 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡힌다.
3400
+
3401
+ `button` 은 클릭해도 **모달을 닫지 않는다.** `onClick` 만 발화하므로 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 된다.
2966
3402
 
2967
3403
  ```tsx
2968
3404
  function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderModalProps) {
2969
3405
  const [error, setError] = useState('');
3406
+ const [saving, setSaving] = useState(false);
2970
3407
  const handleSubmit = async () => {
3408
+ setSaving(true);
2971
3409
  try {
2972
3410
  await save(orderId);
2973
3411
  modalRef.ok(); // 성공 → onOk + 닫기
2974
3412
  } catch {
2975
3413
  setError('저장 실패'); // 실패 → 모달 유지
3414
+ } finally {
3415
+ setSaving(false);
2976
3416
  }
2977
3417
  };
2978
3418
  return (
2979
- // button 주지 않으면 푸터가 렌더되지 않는다
2980
- <SActionModal open={open} onOpenChange={onOpenChange} onClose={onClose} persistent modalTitle="주문 처리">
3419
+ // button footerLeft 도 주지 않으면 푸터가 렌더되지 않는다
3420
+ <SActionModal
3421
+ open={open} onOpenChange={onOpenChange} onClose={onClose} persistent modalTitle="주문 처리"
3422
+ button={{ label: saving ? '저장 중...' : '저장', disabled: saving, onClick: handleSubmit }}
3423
+ footerLeft={<SButton color="neutral" outline size="md" label="취소" disabled={saving} onClick={() => modalRef.cancel()} />}
3424
+ >
2981
3425
  {error && <p>{error}</p>}
2982
- <SButton label="저장" onClick={handleSubmit} />
2983
- <SButton label="취소" onClick={() => modalRef.cancel()} />
2984
3426
  </SActionModal>
2985
3427
  );
2986
3428
  }
2987
3429
  ```
2988
3430
 
3431
+ `button` 은 `label` · `color`(기본 `primary`) · `outline` · `size`(기본 `md`) · `disabled` · `onClick` 을 받는다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며, 푸터 규칙상 `size="md"` 를 명시한다.
3432
+
2989
3433
  ---
2990
3434
 
2991
3435
  ## SModalRef
@@ -3021,7 +3465,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3021
3465
 
3022
3466
  - **선언형과 공존**: 서비스는 추가 API다. open 상태가 앱 상태/라우트에 묶인 경우엔 선언형 `<SConfirmModal open>` / `<SLoadingModal open>` 이 더 적합하다.
3023
3467
  - **백드롭·ESC = 중립적 닫힘**: 특정 콜백(onClose 등) 없이 `onDismissed` 만 발화한다. 명시적 버튼·메서드만 onOk/onCancel/onClose 를 발화한다.
3024
- - **Context 미상속**: `create` 의 커스텀 컴포넌트는 React 트리(createRoot)에서 렌더되어 부모의 Context Provider(Theme·Store 등)를 상속하지 않는다. 필요하면 컴포넌트 내부에서 직접 Provider 로 감싸라. (confirm/loading 은 토큰이 `:root` CSS 변수라 무관)
3468
+ - **Context outlet 이 있어야 상속된다**: [`<SModalOutlet />`](#smodaloutlet) 을 앱 부트스트랩에 렌더하면 `create` 의 커스텀 컴포넌트(와 `confirm/loading` `contentSlot`) 트리의 자식으로 렌더되어 QueryClient·Router·Theme 등을 그대로 쓴다. outlet 없으면 별도 React 트리에서 렌더되어 Provider 가 닿지 않는다 — `useQuery` 는 `No QueryClient set`, `useNavigate` 는 `may be used only in the context of a <Router>` 죽는다. (토큰은 `:root` CSS 변수라 어느 쪽이든 무관)
3025
3469
  - **`create` 의 `component` 는 SActionModal 을 루트로**: `create` 는 컨테이너를 덧씌우지 않으므로, 본문만 렌더하는 컴포넌트를 넘기면 딤·카드 없이 콘텐츠가 그대로 화면에 붙는다. TypeScript 는 이를 막지 못한다(`component` 타입이 아무 컴포넌트나 허용). 개발 모드에서는 마운트 직후 렌더 결과로 이를 감지해 `console.warn` 으로 경고한다 — 세 모달은 모두 Portal 로 `body` 에 렌더되므로 `create` 가 만든 host 는 비어 있어야 하는데, host 에 엘리먼트가 남아 있으면 모달이 아닌 것으로 판정한다.
3026
3470
 
3027
3471
  ## Dependencies
@@ -3593,6 +4037,35 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3593
4037
 
3594
4038
  ---
3595
4039
 
4040
+ # SSplitter
4041
+
4042
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4043
+
4044
+ ### SSplitter
4045
+
4046
+ #### Props
4047
+
4048
+ | Prop | Type | Default | Description |
4049
+ |------|------|---------|-------------|
4050
+ | `value?` | `number` | — | 첫 패널 크기. unit 단위. 주면 controlled |
4051
+ | `defaultValue?` | `number` | — | 첫 패널 초기 크기. uncontrolled. 기본은 '%' 면 50, 'px' 면 240 |
4052
+ | `unit?` | `SSplitterUnit` | `'%'` | 모델·limits 를 읽는 단위 |
4053
+ | `limits?` | `readonly [number, number]` | — | [최소, 최대]. 생략하면 '%' 는 [10, 90], 'px' 는 [50, Infinity] |
4054
+ | `emitImmediately?` | `boolean` | `false` | 드래그하는 동안에도 onValueChange 를 계속 보낸다 |
4055
+ | `vertical?` | `boolean` | `false` | true면 패널을 위아래로 쌓는다 (Quasar q-splitter 의 horizontal 에 해당) |
4056
+ | `disabled?` | `boolean` | `false` | true면 크기를 바꿀 수 없다. 커서도 구분선도 나오지 않는다 |
4057
+ | `dividerClassName?` | `string` | — | 구분선에 얹을 클래스 |
4058
+ | `dividerStyle?` | `CSSProperties` | — | 구분선에 얹을 인라인 스타일 |
4059
+ | `children?` | `ReactNode` | — | SSplitter.Before 와 SSplitter.After 둘 |
4060
+
4061
+ #### Events
4062
+
4063
+ | Event | Type | Description |
4064
+ |-------|------|-------------|
4065
+ | `onValueChange` | `(value: number) => void` | 크기가 확정될 때. emitImmediately 가 아니면 드래그를 놓는 순간 한 번만 온다 |
4066
+
4067
+ ---
4068
+
3596
4069
  # SStepper
3597
4070
 
3598
4071
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
@@ -4184,6 +4657,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4184
4657
  - [SField](../SField)
4185
4658
  - [SKeyValueTable](../SKeyValueTable)
4186
4659
  - [SSectionHeaderCard](../SSectionHeaderCard)
4660
+ - [SStepper](../SStepper)
4187
4661
 
4188
4662
  ### Depends on
4189
4663