sellmate-design-system-react 8.1.0 → 9.0.0-beta.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.
Files changed (50) hide show
  1. package/AGENTS.md +176 -111
  2. package/README.md +38 -8
  3. package/dist/components/SChipFilter/README.md +69 -0
  4. package/dist/components/SChipFilter/SChipFilter.d.ts +160 -0
  5. package/dist/components/SChipFilter/index.d.ts +1 -0
  6. package/dist/components/SCircleProgress/README.md +3 -0
  7. package/dist/components/SCircleProgress/SCircleProgress.d.ts +7 -1
  8. package/dist/components/SDatePicker/README.md +2 -0
  9. package/dist/components/SDateRangePicker/README.md +21 -0
  10. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +16 -0
  11. package/dist/components/SDateRangePicker/index.d.ts +1 -1
  12. package/dist/components/SGhostButton/README.md +4 -0
  13. package/dist/components/SGuide/README.md +1 -1
  14. package/dist/components/SGuide/SGuide.d.ts +1 -1
  15. package/dist/components/SIcon/README.md +4 -0
  16. package/dist/components/SIcon/icons.gen.d.ts +1 -0
  17. package/dist/components/SImage/README.md +44 -0
  18. package/dist/components/SImage/SImage.d.ts +41 -0
  19. package/dist/components/SImage/index.d.ts +1 -0
  20. package/dist/components/SList/README.md +1 -0
  21. package/dist/components/SList/SList.d.ts +2 -0
  22. package/dist/components/SListItem/README.md +2 -2
  23. package/dist/components/SListItem/SListItem.d.ts +2 -2
  24. package/dist/components/SPage/README.md +21 -0
  25. package/dist/components/SPage/SPage.d.ts +10 -0
  26. package/dist/components/SPage/SPageHeader.d.ts +32 -0
  27. package/dist/components/SPage/index.d.ts +1 -0
  28. package/dist/components/SPortal/README.md +1 -1
  29. package/dist/components/SPortal/SPortal.d.ts +7 -1
  30. package/dist/components/SRadio/README.md +2 -0
  31. package/dist/components/SRadioButton/README.md +13 -0
  32. package/dist/components/SSelect/README.md +2 -2
  33. package/dist/components/SSelect/SSelect.d.ts +2 -2
  34. package/dist/components/STag/README.md +2 -0
  35. package/dist/components/STextLink/README.md +2 -0
  36. package/dist/components/SToggle/README.md +1 -0
  37. package/dist/components/SToggle/SToggle.d.ts +4 -1
  38. package/dist/components/STooltip/README.md +2 -0
  39. package/dist/index.cjs +1830 -319
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +1826 -319
  43. package/dist/index.js.map +1 -1
  44. package/dist/lib/floating-width.d.ts +22 -0
  45. package/dist/llms-full.txt +343 -118
  46. package/dist/llms.txt +180 -114
  47. package/dist/styles.css +218 -16
  48. package/dist/theme.css +12 -5
  49. package/eslint/scale.gen.mjs +1 -1
  50. package/package.json +7 -4
package/dist/llms.txt CHANGED
@@ -38,10 +38,10 @@
38
38
  | **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
39
39
  | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
40
40
  | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
41
- | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
42
- | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
41
+ | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
42
+ | **레이아웃** | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
43
43
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
44
- | **표시·상태** | `STag` `SBadge` `SIcon` `SCallout` `SGuide` |
44
+ | **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
45
45
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
46
46
  | **오버레이** | `STooltip` `SPopover` `SPopup` `SDrawer` `SPortal` |
47
47
  | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
@@ -145,9 +145,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
145
145
 
146
146
  | 층 | 무엇인가 | 컴포넌트 |
147
147
  | --- | --- | --- |
148
- | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage` |
149
- | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
150
- | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `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` |
148
+ | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) |
149
+ | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
150
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `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` `SImage` `SLinearProgress` `SCircleProgress` |
151
151
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
152
152
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
153
153
 
@@ -195,9 +195,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
195
195
  블록 순서는 화면 종류와 무관하게 같다. **필요한 것만 남기되 순서를 바꾸지 않는다.**
196
196
 
197
197
  ```text
198
- 1. 페이지 제목 (+ 가이드·매뉴얼 링크. 액션 버튼은 오지 않는다 §4-2)
198
+ 1. 페이지 제목 `SPage` 의 `header` prop (+ 가이드·매뉴얼 링크는 slot. 액션 버튼은 오지 않는다 §4-2)
199
199
  2. 상시 안내 SCallout
200
- 3. 필터 SKeyValueTable
200
+ 3. 필터 SKeyValueTable · SChipFilter (§3-7-11)
201
201
  4. 툴바 STableBar (건수 요약 + 액션)
202
202
  5. 본문 STable · 섹션 카드들 · SList …
203
203
  6. 페이지네이션 SPagination (STable 이 pagination prop 으로 직접 그린다)
@@ -223,7 +223,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
223
223
 
224
224
  | 층 (§2-0) | 역할 | 클래스 | 크기 |
225
225
  | --- | --- | --- | --- |
226
- | 셸 | 페이지 제목 (h1) | `typo-heading-lg` | 18px |
226
+ | 셸 | 페이지 제목 (`SPage` 의 `header.title`) | `typo-heading-lg` | 18px |
227
227
  | 블록 | 섹션 제목 | `typo-heading-sm` | 14px |
228
228
  | 블록 내부 | 하위 제목 (섹션 안을 더 나눌 때) | `typo-heading-xs` | 12px |
229
229
  | — | 본문 | `typo-body-sm-default` | 12px |
@@ -231,7 +231,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
231
231
 
232
232
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
233
233
 
234
- - **섹션 제목의 타이포를 직접 주지 않는다.** `SSectionHeaderCard.Header` 가 `title` 에 이미 넣는다 — 그 위에 `typo-heading-sm` 을 또 씌우지 않는다. 직접 쓰는 경우는 섹션 카드 없이 제목만 세울 때뿐이다.
234
+ - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard.Header` 의 `title` 이 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
235
235
  - **하위 제목이 필요하면 먼저 섹션을 나눌 수 없는지 본다.** 한 섹션 안에서 제목이 두 단으로 갈린다는 것은 대개 섹션이 둘이라는 뜻이다 (§3-7-8).
236
236
  - 본문 안에서 한 단어를 강조할 때는 `typo-body-sm-medium` 을 쓴다. `typo-body-sm-bold` 는 제목 성격의 짧은 라벨에만 쓴다. <!-- TODO(디자인): 강조 굵기 기준 확정 -->
237
237
 
@@ -435,6 +435,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
435
435
  | 컨트롤에 라벨·필수·에러를 붙인다 | `SField` | §3-7-5 |
436
436
  | 입력 여러 개를 묶어 한 번에 검증한다 | `SForm` | §4-3 |
437
437
  | 폼·필터를 표 형태로 배치한다 | `SKeyValueTable` | §4 |
438
+ | 필요한 검색 조건만 칩으로 골라 붙이게 한다 | `SChipFilter` | §3-7-11 |
438
439
 
439
440
  #### B. 정보를 읽게 보여준다
440
441
 
@@ -450,6 +451,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
450
451
  | 상태·분류를 라벨로 찍는다 | `STag` | §3-1 |
451
452
  | 색 점만으로 상태를 찍는다 | `SBadge` | §3-1 |
452
453
  | 아이콘을 넣는다 | `SIcon` | |
454
+ | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
453
455
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
454
456
 
455
457
  #### C. 동작을 실행시킨다
@@ -467,6 +469,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
467
469
  | 앱 셸(상단바 + 내비 + 본문)을 세운다 | `SLayout` | §4-1 |
468
470
  | 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
469
471
  | 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
472
+ | 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPage` 의 `header` prop | §4-1 |
470
473
  | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
471
474
  | 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
472
475
  | 가로선으로 끊는다 | `SDivider` | §3-6 |
@@ -1033,6 +1036,16 @@ const columns: STableColumn[] = [
1033
1036
 
1034
1037
  - **항목 하나의 속성을 나열하는 것은 목록이 아니다.** `라벨: 값` 이 세로로 쌓이는 것은 `SKeyValueTable` 이다 (§4-4).
1035
1038
  - `SList` 는 레이아웃만 담당한다. 펼침·단일 선택 동작이 필요하면 `SExpansionList` 다 (§3-7-7).
1039
+ - **항목 사이 구분선은 리스트가 그린다.** `SListItem`·`SExpansionItem`·`SDraggableItem` 셋 다 스스로 구분선을 그리지 않으므로, 목록을 감싸는 `SList`·`SExpansionList`·`SDraggableList` 에 `separator` 를 준다 — 아이템에 `border-b` 를 직접 붙이지 않는다. 테두리형(`bordered`)은 테두리가 구분 역할을 하므로 `separator` 대신 `useGap` 으로 띄운다.
1040
+ - **`SListItem` 은 기본이 표시 전용이다.** 눌러서 이동·선택하게 하려면 `clickable` 을 준다 — hover·`selected`·`interaction="chevron"` 표현이 전부 여기에 딸려 있어서, `clickable` 없이 `selected` 만 주면 선택 표시가 나오지 않는다. `SExpansionList` 는 선택을 자기가 관리하므로 자식 아이템을 알아서 클릭 가능하게 만든다.
1041
+
1042
+ ```tsx
1043
+ ✅ <SList separator><SListItem title="일반 문의" />…</SList> {/* 줄로 구분되는 목록 */}
1044
+ ✅ <SList useGap><SListItem title="일반 문의" bordered />…</SList> {/* 카드처럼 떨어진 목록 */}
1045
+ ✅ <SListItem title="일반 문의" clickable selected onClick={…} /> {/* 눌러서 고르는 목록 */}
1046
+ ❌ <SList><SListItem title="일반 문의" className="border-b" />…</SList>
1047
+ ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1048
+ ```
1036
1049
 
1037
1050
  #### 3-7-7. 펼치는 셋 — SExpansionItem vs SExpansionList vs STree
1038
1051
 
@@ -1042,7 +1055,7 @@ const columns: STableColumn[] = [
1042
1055
  | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
1043
1056
  | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
1044
1057
 
1045
- `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다.
1058
+ `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 그린다 — `separator` 를 준다 (§3-7-6).
1046
1059
 
1047
1060
  #### 3-7-8. SCard vs SSectionHeaderCard
1048
1061
 
@@ -1068,6 +1081,37 @@ const columns: STableColumn[] = [
1068
1081
 
1069
1082
  `STooltip`·`SPopover`·`SSelect`·날짜 피커가 내부에서 쓰는 저수준 레이어다. 앵커에 붙여 띄우는 동작이 필요하면 **먼저 §3-3 에서 대응 컴포넌트를 찾는다.** `SPortal` 을 직접 쓰는 것은 그 넷 중 어느 것도 아닌 새로운 부착형 레이어를 만들 때뿐이고, 그때도 모달 안에서 열릴 수 있다면 소속 컨테이너를 맞춰야 한다.
1070
1083
 
1084
+ #### 3-7-11. 필터 둘 — SKeyValueTable vs SChipFilter
1085
+
1086
+ 둘 다 §2-0 블록 순서의 **3번 자리(필터)** 에 놓이고, 한 화면에 둘을 같이 두지 않는다.
1087
+
1088
+ | 상황 | 컴포넌트 |
1089
+ | --- | --- |
1090
+ | 조건이 정해져 있고 **항상 다 보여야** 한다 (기본 형태) | `SKeyValueTable` |
1091
+ | 조건 후보가 많아 **쓸 것만 골라 붙이고** 나머지는 숨겨야 한다 | `SChipFilter` |
1092
+
1093
+ - **기본은 `SKeyValueTable` 이다** (§4-2). 조건이 대여섯 개 이하로 고정이면 표로 펼쳐 두는 편이 한눈에 읽힌다.
1094
+ - `SChipFilter` 는 조건을 **칩 한 줄**로 접고, "필터 추가" 로 필요한 것만 꺼내 쓰게 한다. 칩을 누르면 편집 팝오버가 열리고, 날짜는 프리셋(오늘·지난 7일·사용자 지정)으로 고른다. 조건 후보가 많은 목록 화면에서 필터가 화면을 세로로 잡아먹는 것을 막는 용도다.
1095
+ - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다.
1096
+ - 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 `fields` 를 그룹으로 넘긴다. 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다.
1097
+
1098
+ #### 3-7-12. 이미지 — SImage
1099
+
1100
+ 사진·썸네일은 `<img>` 를 직접 쓰지 않고 `SImage` 를 쓴다. **로딩·실패 상태를 컴포넌트가 이미 처리하기 때문이다.**
1101
+
1102
+ - **로딩 스피너와 실패 표시를 앱이 만들지 않는다.** `src` 가 없거나 로드에 실패하면 빈 이미지 아이콘이, 로딩 중에는 스피너가 자동으로 놓인다. 크기는 상자 높이에 비례하므로 썸네일이든 큰 미리보기든 따로 맞출 것이 없다.
1103
+ - **크기는 `ratio` 와 너비로 준다.** `ratio` 기본값이 `1`(정사각형)이라 너비만 주면 정사각형이 된다. `className`/`style` 로 높이를 직접 주면 그쪽이 이기고 `ratio` 는 무시된다.
1104
+ - 상자를 채우는 방식은 `fit`(기본 `cover`), 붙는 위치는 `position` 이다. 안쪽 `<img>` 에 네이티브 속성이 필요하면 `imgProps` 로 넘긴다 — 목록에서 고스트 드래그를 막는 `imgProps={{ draggable: false }}` 가 대표적이다.
1105
+ - 썸네일이 많은 목록에서는 `loadingShowDelay` 를 준다. 캐시된 이미지가 즉시 로드될 때 스피너가 한 프레임 번쩍이는 것을 막는다.
1106
+
1107
+ ```tsx
1108
+ ✅ <SImage src={item.thumbnailUrl} alt={item.name} style={{ width: 64 }} /> {/* 정사각 썸네일 */}
1109
+ ✅ <SImage src={banner} ratio={16 / 9} className="w-full" /> {/* 가로형 배너 */}
1110
+
1111
+ ❌ <img src={item.thumbnailUrl} /> {/* 로딩·실패 상태가 없다 */}
1112
+ ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
1113
+ ```
1114
+
1071
1115
  ---
1072
1116
 
1073
1117
  ## 4. 페이지 레시피 — 표준 골격
@@ -1078,11 +1122,12 @@ const columns: STableColumn[] = [
1078
1122
  >
1079
1123
  > **핵심 원칙 — 표 형태의 정보는 `SKeyValueTable` 로 만든다.** 필터·등록/수정 폼·상세 정보가 모두 여기 해당한다.
1080
1124
  > `SField` 컨트롤을 `div` 로 직접 나열해 폼을 만들지 않는다.
1125
+ > (필터만 예외가 하나 있다 — 조건 후보가 많아 골라 붙이게 해야 하면 `SChipFilter` 다. §3-7-11)
1081
1126
 
1082
1127
  ### 4-1. 앱 셸 (모든 페이지 공통)
1083
1128
 
1084
1129
  ```tsx
1085
- import { SLayout, SGnb, SPage, type SGnbMenuItem } from 'sellmate-design-system-react';
1130
+ import { SLayout, SGnb, SPage, type SGnbMenuItem, type SPageHeaderProps } from 'sellmate-design-system-react';
1086
1131
 
1087
1132
  const MENU: SGnbMenuItem[] = [
1088
1133
  { label: '주문', value: 'orders', icon: 'bill' },
@@ -1091,20 +1136,32 @@ const MENU: SGnbMenuItem[] = [
1091
1136
 
1092
1137
  export default function AppShell({
1093
1138
  children,
1139
+ header,
1094
1140
  scrollEndSpacing,
1095
- }: { children: React.ReactNode; scrollEndSpacing?: boolean }) {
1141
+ }: { children: React.ReactNode; header?: SPageHeaderProps; scrollEndSpacing?: boolean }) {
1096
1142
  return (
1097
1143
  <SLayout type="box" header="fix">
1098
1144
  {/* type/header/folded 는 SLayout 에만 준다 — SGnb 는 context 에서 읽는다 */}
1099
1145
  <SGnb items={MENU} value={current} onValueChange={navigate} logo={<Logo />} />
1100
1146
  {/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
1101
1147
  {/* 스크롤 끝 여백도 SPage 가 넣는다. 끄는 건 페이지네이션 있는 목록뿐이라 페이지가 정한다 */}
1102
- <SPage background="frame" scrollEndSpacing={scrollEndSpacing}>{children}</SPage>
1148
+ {/* header 는 페이지마다 달라 AppShell 이 그대로 받아 넘긴다 — 페이지 제목은 여기서 만들지 않는다 */}
1149
+ <SPage background="frame" scrollEndSpacing={scrollEndSpacing} header={header}>
1150
+ {children}
1151
+ </SPage>
1103
1152
  </SLayout>
1104
1153
  );
1105
1154
  }
1106
1155
  ```
1107
1156
 
1157
+ **페이지는 `AppShell` 을 직접 호출하며 자기 `header` 를 넘긴다** — SPage 는 셸 안에 하나뿐이므로, 페이지 제목이 페이지마다 다르다는 사실은 이렇게 프레임 컴포넌트를 통해 흘려보낸다(§4-2·§4-3·§4-4 참고).
1158
+
1159
+ **페이지 헤더(`header`) 사용 규칙 — 이 앱에서는 값이 아니라 값의 일관성이 규칙이다.**
1160
+
1161
+ - **`fix` 는 앱 전체에서 하나로 고정한다.** 어떤 페이지는 `fix: true`(바), 다른 페이지는 `fix: false`(투명) 로 섞어 쓰지 않는다. 프로젝트에서 하나를 고르면(예: 전부 `fix: true`) 모든 `header` 가 그 값을 쓴다.
1162
+ - **`slot`·`onBack` 도 페이지 성격이 실제로 다른 경우가 아니면 있는 대로 통일한다.** "목록 페이지엔 없고 상세·등록 페이지엔 있다"처럼 화면 종류에 따라 갈리는 것은 허용되지만, 같은 종류의 화면끼리는 임의로 넣었다 뺐다 하지 않는다.
1163
+ - **`slot` 은 `ReactNode` 를 그대로 받지만, 원칙은 `size="sm"` 버튼 위주로만 채운다.** `STextLink`·복잡한 커스텀 마크업을 슬롯에 넣지 않는다 — 그 이상이 필요하면 페이지 헤더가 아니라 §4-2 의 `STableBar` 처럼 본문 쪽 액션 자리를 쓴다.
1164
+
1108
1165
  **GNB 폭을 사용자가 조절하게 하려면 `SGnb` 에 `resizable` 을 준다.** 메뉴 오른쪽 경계가 조절선이 되고, 레일 폭은 고정된 채 메뉴 컬럼만 늘고 준다. 범위는 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
1109
1166
 
1110
1167
  ```tsx
@@ -1180,18 +1237,20 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1180
1237
 
1181
1238
  ### 4-2. 목록 페이지 (필터 + 테이블)
1182
1239
 
1183
- 구조: **페이지 헤더(제목 + 가이드 링크) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
1240
+ 구조: **페이지 헤더(`AppShell` 의 `header` prop, 가이드 등 부가 액션은 slot) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
1241
+
1242
+ 필터 자리는 `SChipFilter` 로 바꿔 놓을 수 있다 — 조건 후보가 많아 쓸 것만 골라 붙이게 하는 화면이면 그쪽이다 (§3-7-11). 나머지 골격은 같다.
1184
1243
 
1185
1244
  액션 버튼의 위치가 핵심이다:
1186
1245
 
1187
- - **페이지 제목 줄에는 액션 버튼을 두지 않는다.** 가이드·매뉴얼 링크 등 부가 정보만 온다.
1246
+ - **페이지 제목 줄에는 이 페이지의 주요 액션을 두지 않는다.** 부가적인 것만 `header.slot` 에 `SButton size="sm"` 으로 온다 (§4-1 "페이지 헤더 사용 규칙").
1188
1247
  - **주요 액션(등록 등)은 `STableBar` 의 `rightActions`** 에 둔다.
1189
1248
  - **선택 상태 액션(선택 삭제 등)은 `STableBar` 의 `actions`** 에 둔다. `actions` 슬롯은 건수 요약이 있으면 앞에 구분선(`SDivider`)을 **자동으로** 넣으므로 직접 구분선을 만들지 않는다.
1190
- - **페이지네이션이 있으면 스크롤 끝 여백을 끈다** — 셸의 `SPage` 에 `scrollEndSpacing={false}` 를 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝"을 알려준다.
1249
+ - **페이지네이션이 있으면 스크롤 끝 여백을 끈다** — `AppShell` 에 `scrollEndSpacing={false}` 를 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝"을 알려준다.
1191
1250
 
1192
1251
  ```tsx
1193
1252
  import {
1194
- SButton, STextLink, SKeyValueTable, STableBar, STable, STag,
1253
+ SButton, SKeyValueTable, STableBar, STable, STag,
1195
1254
  type STableColumn, type SRow, type SKeyValueField,
1196
1255
  } from 'sellmate-design-system-react';
1197
1256
 
@@ -1229,55 +1288,58 @@ export default function ProductListPage() {
1229
1288
  const [selected, setSelected] = useState<SRow[]>([]);
1230
1289
 
1231
1290
  return (
1232
- <div className="flex flex-col gap-sd-12">
1233
- {/* 페이지 헤더 — 액션 버튼 없음. 가이드/매뉴얼 링크 자리 */}
1234
- <div className="flex items-center justify-between">
1235
- <h1 className="typo-heading-lg m-0">상품 목록</h1>
1236
- <STextLink label="이용 가이드" rightArrow="chevron" onClick={openGuide} />
1237
- </div>
1291
+ <AppShell
1292
+ header={{
1293
+ title: '상품 목록',
1294
+ // 이 페이지의 주요 액션이 아니라 부가 액션 — slot 은 sm 버튼으로만 채운다
1295
+ slot: <SButton size="sm" color="neutral" outline label="이용 가이드" onClick={openGuide} />,
1296
+ }}
1297
+ scrollEndSpacing={false} // 페이지네이션이 있으므로 끈다
1298
+ >
1299
+ <div className="flex flex-col gap-sd-12">
1300
+ {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1301
+ <SKeyValueTable
1302
+ fields={filterFields}
1303
+ values={filters}
1304
+ search
1305
+ onChange={({ values }) => setFilters(values)}
1306
+ onSearch={fetchList}
1307
+ />
1238
1308
 
1239
- {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1240
- <SKeyValueTable
1241
- fields={filterFields}
1242
- values={filters}
1243
- search
1244
- onChange={({ values }) => setFilters(values)}
1245
- onSearch={fetchList}
1246
- />
1247
-
1248
- {/* 툴바 — 좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션 */}
1249
- <STableBar
1250
- total={total}
1251
- selected={selected.length}
1252
- actions={
1253
- /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
1254
- <SButton size="sm" color="danger" outline label="선택 삭제"
1255
- disabled={!selected.length} onClick={removeSelected} />
1256
- }
1257
- rightActions={
1258
- /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
1259
- <SButton size="sm" label="상품 등록" onClick={goCreate} />
1260
- }
1261
- />
1262
-
1263
- <STable
1264
- columns={columns}
1265
- rows={rows}
1266
- rowKey="id"
1267
- selectable
1268
- selected={selected}
1269
- onSelectedChange={setSelected}
1270
- pagination={{ currentPage, lastPage }}
1271
- isLoading={isLoading}
1272
- />
1273
- </div>
1309
+ {/* 툴바 — 좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션 */}
1310
+ <STableBar
1311
+ total={total}
1312
+ selected={selected.length}
1313
+ actions={
1314
+ /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
1315
+ <SButton size="sm" color="danger" outline label="선택 삭제"
1316
+ disabled={!selected.length} onClick={removeSelected} />
1317
+ }
1318
+ rightActions={
1319
+ /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
1320
+ <SButton size="sm" label="상품 등록" onClick={goCreate} />
1321
+ }
1322
+ />
1323
+
1324
+ <STable
1325
+ columns={columns}
1326
+ rows={rows}
1327
+ rowKey="id"
1328
+ selectable
1329
+ selected={selected}
1330
+ onSelectedChange={setSelected}
1331
+ pagination={{ currentPage, lastPage }}
1332
+ isLoading={isLoading}
1333
+ />
1334
+ </div>
1335
+ </AppShell>
1274
1336
  );
1275
1337
  }
1276
1338
  ```
1277
1339
 
1278
1340
  ### 4-3. 폼 페이지 (등록/수정)
1279
1341
 
1280
- 구조: **페이지 제목 → `SForm` + `SKeyValueTable` → 하단 버튼**
1342
+ 구조: **페이지 제목(`AppShell` 의 `header` prop) → `SForm` + `SKeyValueTable` → 하단 버튼**
1281
1343
 
1282
1344
  - 필드를 `div` 로 나열하지 않고 **`SKeyValueTable` 의 행으로 구성**한다.
1283
1345
  - 검증 규칙은 각 field 의 `options.rules` 로 넘긴다. `SForm` 이 하위 컨트롤을 자동 수집해 submit 시 일괄 검증하고, 실패 시 첫 실패 필드로 포커스를 옮긴다.
@@ -1315,33 +1377,33 @@ export default function ProductCreatePage() {
1315
1377
  const [values, setValues] = useState<Record<string, unknown>>({});
1316
1378
 
1317
1379
  return (
1318
- <div className="flex flex-col gap-sd-12">
1319
- <h1 className="typo-heading-lg m-0">상품 등록</h1>
1320
-
1321
- <SForm ref={formRef} formClass="flex flex-col gap-sd-12" onSubmit={save}>
1322
- <SKeyValueTable
1323
- fields={fields}
1324
- values={values}
1325
- onChange={({ values }) => setValues(values)}
1326
- />
1327
-
1328
- {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
1329
- <div className="flex items-center justify-between">
1330
- <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
1331
- <div className="flex items-center gap-sd-8">
1332
- <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
1333
- <SButton type="submit" label="저장" />
1380
+ <AppShell header={{ title: '상품 등록', onBack: goBack }}>
1381
+ <div className="flex flex-col gap-sd-12">
1382
+ <SForm ref={formRef} formClass="flex flex-col gap-sd-12" onSubmit={save}>
1383
+ <SKeyValueTable
1384
+ fields={fields}
1385
+ values={values}
1386
+ onChange={({ values }) => setValues(values)}
1387
+ />
1388
+
1389
+ {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
1390
+ <div className="flex items-center justify-between">
1391
+ <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
1392
+ <div className="flex items-center gap-sd-8">
1393
+ <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
1394
+ <SButton type="submit" label="저장" />
1395
+ </div>
1334
1396
  </div>
1335
- </div>
1336
- </SForm>
1337
- </div>
1397
+ </SForm>
1398
+ </div>
1399
+ </AppShell>
1338
1400
  );
1339
1401
  }
1340
1402
  ```
1341
1403
 
1342
1404
  ### 4-4. 상세(조회) 페이지
1343
1405
 
1344
- 구조: **페이지 헤더(제목) → 섹션별 `SSectionHeaderCard` + `SKeyValueTable` → 하단 버튼**
1406
+ 구조: **페이지 헤더(`AppShell` 의 `header` prop, 목록에서 들어오는 뒤로가기는 onBack) → 섹션별 `SSectionHeaderCard` + `SKeyValueTable` → 하단 버튼**
1345
1407
 
1346
1408
  - 조회 값은 `type: 'text'` 행으로 표시한다. **상태·분류 태그도 별도 영역이 아니라 표의 한 행**으로 넣는다 (`render` 에 `STag`).
1347
1409
  - 행이 많아지면 **유형별로 섹션을 나누고, 각 섹션을 `SSectionHeaderCard` 로 감싼다.**
@@ -1375,38 +1437,39 @@ const priceFields: SKeyValueField[][] = [
1375
1437
 
1376
1438
  export default function ProductDetailPage() {
1377
1439
  return (
1378
- <div className="flex flex-col gap-sd-12">
1379
- <h1 className="typo-heading-lg m-0">클래식 셔츠</h1>
1380
-
1381
- <SSectionHeaderCard>
1382
- <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1383
- <SSectionHeaderCard.Body>
1384
- <SKeyValueTable fields={basicFields} values={product} />
1385
- </SSectionHeaderCard.Body>
1386
- </SSectionHeaderCard>
1387
-
1388
- <SSectionHeaderCard>
1389
- {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1390
- <SSectionHeaderCard.Header
1391
- title="가격 정보"
1392
- marker
1393
- helpText={['부가세 포함 금액입니다.']}
1394
- slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1395
- />
1396
- <SSectionHeaderCard.Body>
1397
- <SKeyValueTable fields={priceFields} values={product} />
1398
- </SSectionHeaderCard.Body>
1399
- </SSectionHeaderCard>
1400
-
1401
- {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
1402
- <div className="flex items-center justify-between">
1403
- <SButton color="neutral" outline label="목록" onClick={goList} />
1404
- <div className="flex items-center gap-sd-8">
1405
- <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
1406
- <SButton label="수정" onClick={goEdit} />
1440
+ // 목록에서 들어온 상세 페이지 — onBack 으로 뒤로가기를 준다
1441
+ <AppShell header={{ fix: true, title: '클래식 셔츠', onBack: goList }}>
1442
+ <div className="flex flex-col gap-sd-12">
1443
+ <SSectionHeaderCard>
1444
+ <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1445
+ <SSectionHeaderCard.Body>
1446
+ <SKeyValueTable fields={basicFields} values={product} />
1447
+ </SSectionHeaderCard.Body>
1448
+ </SSectionHeaderCard>
1449
+
1450
+ <SSectionHeaderCard>
1451
+ {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1452
+ <SSectionHeaderCard.Header
1453
+ title="가격 정보"
1454
+ marker
1455
+ helpText={['부가세 포함 금액입니다.']}
1456
+ slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1457
+ />
1458
+ <SSectionHeaderCard.Body>
1459
+ <SKeyValueTable fields={priceFields} values={product} />
1460
+ </SSectionHeaderCard.Body>
1461
+ </SSectionHeaderCard>
1462
+
1463
+ {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
1464
+ <div className="flex items-center justify-between">
1465
+ <SButton color="neutral" outline label="목록" onClick={goList} />
1466
+ <div className="flex items-center gap-sd-8">
1467
+ <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
1468
+ <SButton label="수정" onClick={goEdit} />
1469
+ </div>
1407
1470
  </div>
1408
1471
  </div>
1409
- </div>
1472
+ </AppShell>
1410
1473
  );
1411
1474
  }
1412
1475
  ```
@@ -1447,8 +1510,10 @@ export default function ProductDetailPage() {
1447
1510
  - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1448
1511
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1449
1512
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1513
+ - [ ] `header.fix` 가 프로젝트 전체와 같은 값인가 (다른 페이지와 다르게 섞어 쓰지 않았는가, §4-1)
1514
+ - [ ] `header.slot` 을 채웠다면 `SButton size="sm"` 위주인가 (§4-1 "페이지 헤더 사용 규칙")
1450
1515
  - [ ] 앱 셸이나 그 바깥에 `min-width`·`overflow-x` 를 직접 걸지 않았는가 (최소 너비는 `SLayout` 이 보장한다, §4-1)
1451
- - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가)
1516
+ - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1452
1517
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1453
1518
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1454
1519
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
@@ -1529,7 +1594,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1529
1594
  - oceanblue: 15 25 50 60 65 70 75 80 85 90 95 99
1530
1595
  - brilliantblue: 10 20 25 40 50 60 70 75 80 85 90 95 99 05
1531
1596
  - bg: screen frame brand subtle accent accentLight accentBright accentFaint deep danger dangerLight warning caution progress success successLight neutralLight neutralBright tableHeader
1532
- - fg: primary secondary tertiary quaternary brand subtleWarm subtleCool deep success dangerLight accent accentLight danger warning inverse
1597
+ - fg: primary secondary tertiary quaternary brand subtleWarm subtleCool deep success successLight dangerLight accent accentLight danger warning inverse
1533
1598
  - border: default strong bold accent accentLight danger warning success
1534
1599
  - disabled: bg text icon border
1535
1600
  - link: accent
@@ -1655,6 +1720,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1655
1720
  --sys-color-fg-subtleCool
1656
1721
  --sys-color-fg-subtleWarm
1657
1722
  --sys-color-fg-success
1723
+ --sys-color-fg-successLight
1658
1724
  --sys-color-fg-tertiary
1659
1725
  --sys-color-fg-warning
1660
1726
  --sys-color-field-bg-default
@@ -1676,7 +1742,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1676
1742
  --sys-color-link-accent
1677
1743
  --sys-color-navigation-gnb-bg-dark
1678
1744
 
1679
- ## 3. 컴포넌트 인덱스 (63종)
1745
+ ## 3. 컴포넌트 인덱스 (65종)
1680
1746
 
1681
1747
  **Props 는 이 문서에 없다.** 쓸 컴포넌트를 정했으면 그 컴포넌트의 README 를 읽는다.
1682
1748
 
@@ -1685,4 +1751,4 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1685
1751
  예) SButton 을 쓴다면 `dist/components/SButton/README.md`.
1686
1752
  prop 을 추측해서 쓰지 말고 반드시 README 로 확인한다 (분류별 용도는 §1 의 "전체 컴포넌트 인덱스" 참조).
1687
1753
 
1688
- SActionModal · SBadge · SBarcodeInput · SButton · SCalendar · SCallout · SCard · SCheckbox · SChip · SChipInput · SCircleProgress · SConfirmModal · SDatePicker · SDateRangePicker · SDivider · SDraggableItem · SDraggableList · SDrawer · SDropdownButton · SExpansionItem · SExpansionList · SField · SFilePicker · SForm · SGhostButton · SGnb · SGuide · SIcon · SInput · SKeyValueTable · SLayout · SLinearProgress · SList · SListItem · SLoadingContainer · SLoadingModal · SModal · SNumberInput · SPage · SPagination · SPopover · SPopup · SPortal · SRadio · SRadioButton · SScrollArea · SSectionHeaderCard · SSelect · SSplitter · SStepper · SSwitch · STable · STableBar · STabs · STag · STextLink · STextarea · STimePicker · STimeRangePicker · SToast · SToggle · STooltip · STree
1754
+ SActionModal · SBadge · SBarcodeInput · SButton · SCalendar · SCallout · SCard · SCheckbox · SChip · SChipFilter · SChipInput · SCircleProgress · SConfirmModal · SDatePicker · SDateRangePicker · SDivider · SDraggableItem · SDraggableList · SDrawer · SDropdownButton · SExpansionItem · SExpansionList · SField · SFilePicker · SForm · SGhostButton · SGnb · SGuide · SIcon · SImage · SInput · SKeyValueTable · SLayout · SLinearProgress · SList · SListItem · SLoadingContainer · SLoadingModal · SModal · SNumberInput · SPage · SPagination · SPopover · SPopup · SPortal · SRadio · SRadioButton · SScrollArea · SSectionHeaderCard · SSelect · SSplitter · SStepper · SSwitch · STable · STableBar · STabs · STag · STextLink · STextarea · STimePicker · STimeRangePicker · SToast · SToggle · STooltip · STree