sellmate-design-system-react 9.0.0-beta.53 → 9.0.0-beta.54

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 (33) hide show
  1. package/AGENTS.md +95 -7
  2. package/dist/components/SChipFilter/README.md +34 -4
  3. package/dist/components/SChipFilter/SChipFilter.d.ts +25 -6
  4. package/dist/components/SChipFilter/index.d.ts +1 -1
  5. package/dist/components/SGhostButton/SGhostButton.d.ts +4 -1
  6. package/dist/components/SGnb/README.md +37 -1
  7. package/dist/components/SGnb/SGnb.d.ts +33 -2
  8. package/dist/components/SGnb/gnb.config.d.ts +6 -0
  9. package/dist/components/SIcon/README.md +2 -0
  10. package/dist/components/SLauncherListBox/README.md +78 -0
  11. package/dist/components/SLauncherListBox/SLauncherListBox.d.ts +70 -0
  12. package/dist/components/SLauncherListBox/index.d.ts +2 -0
  13. package/dist/components/SLauncherListBox/launcherListBox.config.d.ts +79 -0
  14. package/dist/components/SLoginCard/README.md +2 -0
  15. package/dist/components/SLogo/README.md +54 -0
  16. package/dist/components/SLogo/SLogo.d.ts +47 -0
  17. package/dist/components/SLogo/index.d.ts +2 -0
  18. package/dist/components/SLogo/logo.config.d.ts +14 -0
  19. package/dist/components/SLogo/sellmate-wordmark.d.ts +20 -0
  20. package/dist/components/SPortal/README.md +2 -0
  21. package/dist/components/STag/README.md +2 -0
  22. package/dist/index.cjs +576 -81
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +568 -82
  26. package/dist/index.js.map +1 -1
  27. package/dist/lib/depth-filter.d.ts +27 -0
  28. package/dist/llms-full.txt +290 -12
  29. package/dist/llms.txt +97 -9
  30. package/dist/styles.css +124 -3
  31. package/dist/theme.css +18 -0
  32. package/package.json +1 -1
  33. package/dist/components/SLoginCard/sellmate-wordmark.d.ts +0 -14
package/AGENTS.md CHANGED
@@ -43,10 +43,10 @@
43
43
  | **입력 (폼)** | `SForm` `SField` `SInput` `SChatInput`(대화 입력창) `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
44
44
  | **날짜·시간** | `SCalendar` `SCalendarBoard`(한 달치 일정 판) `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
45
45
  | **표·목록** | `SChatMessage`(대화의 메시지 한 건) `SChatAttachedFile`(입력창 위, 아직 보내지 않은 첨부 한 칸) `SChatFile`(대화 흐름에 선, 보낸 파일 한 칸) `SChatSystemMessage`(대화 흐름 가운데 서는 시스템 안내) `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
46
- | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
46
+ | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SLauncherListBox`(런처 버튼을 눌러 뜨는 서비스 목록) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
47
47
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
48
48
  | **차트** | `SBarChart`(막대 그래프 — 항목끼리 크기를 견준다. `stacked` 로 항목 안의 구성까지) |
49
- | **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
49
+ | **표시·상태** | `STag` `SBadge` `SIcon` `SLogo`(브랜드 로고를 아이콘처럼 — size 는 높이다) `SImage` `SCallout` `SGuide` |
50
50
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
51
51
  | **오버레이** | `STooltip` `SPopover` `SPopup` `SDrawer` `SPortal` |
52
52
  | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
@@ -89,7 +89,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
89
89
  | 직접 만든 탭/페이지네이션/스텝퍼 | `STabs`, `SPagination`, `SStepper` |
90
90
  | `<ul>`/`<li>` 로 만든 목록 UI | `SList` + `SListItem` (드래그 정렬은 `SDraggableItem`) |
91
91
  | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` 의 `title` / `padding` props |
92
- | `<svg>` 직접 삽입, 이모지 아이콘 | `SIcon` |
92
+ | `<svg>` 직접 삽입, 이모지 아이콘 | `SIcon` (셀메이트 로고는 `SLogo`) |
93
93
  | `<hr>` | `SDivider` |
94
94
  | `<details>` / `<summary>` | `SExpansionItem` |
95
95
  | `<progress>` | `SLinearProgress`, `SCircleProgress` |
@@ -169,8 +169,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
169
169
  | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage` `SPageHeader`(`SLayout` 안에서 `SPage` 앞에 둔다) |
170
170
  | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SChatMessage` `SChatSystemMessage` `SChatInput` `SLoginCard`(유일하게 `SPage` 밖에 선다 — 로그인 화면 자체가 자기 자리다, §4-6) `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `SBarChart` `STabs` `SStepper` `SPagination` `SDivider` |
171
171
  | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SChatAttachedFile` `SChatFile` `SImage` `SLinearProgress` `SCircleProgress` |
172
- | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
173
- | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
172
+ | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `SLogo` `STextLink` `SChip` |
173
+ | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SLauncherListBox`(런처 버튼에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
174
174
 
175
175
  여기에 화면을 차지하지 않는 **부트스트랩** 이 따로 있다 — `SModalOutlet` `SToastContainer` 는 앱 진입점에 한 번만 렌더한다 (§4-1).
176
176
 
@@ -571,6 +571,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
571
571
  | 상태·분류를 라벨로 찍는다 | `STag` | §3-1 |
572
572
  | 색 점만으로 상태를 찍는다 | `SBadge` | §3-1 |
573
573
  | 아이콘을 넣는다 | `SIcon` | |
574
+ | 셀메이트 로고를 넣는다 | `SLogo` (`SIcon` 과 달리 `size` 가 **높이**이고, 색을 주지 않으면 브랜드색이다) | §3-7-14 |
574
575
  | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
575
576
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
576
577
 
@@ -592,6 +593,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
592
593
  | 좌측 내비게이션 맨 아래(또는 전폭 상단바 오른쪽 끝)에 서비스·도메인·알림·설정·계정 묶음을 붙인다 | `SGnbSystem` | §4-1 |
593
594
  | 앱 상단바 오른쪽 끝에 알림 벨·계정 이름을 둔다 | `SGnbSystem` (`SGnb` 의 `system` 슬롯 — 직접 만들지 않는다) | §4-1 |
594
595
  | 계정 이름을 눌러 이메일·권한·계정 설정·언어 변경·로그아웃을 띄운다 | `SAccountListBox` (`SGnbSystem` 의 `account.listBox` 로 넘긴다 — 직접 띄우지 않는다) | §4-1 |
596
+ | 앱런처 버튼을 눌러 옮겨 갈 수 있는 서비스 목록을 띄운다 | `SLauncherListBox` (`SGnb` 의 `launcher.items` 로 넘긴다 — 직접 띄우지 않는다) | §4-1 |
595
597
  | 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
596
598
  | 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPageHeader` (`SLayout` 안에서 `SPage` 앞에 둔다) | §4-1 |
597
599
  | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
@@ -1089,6 +1091,21 @@ const columns: STableColumn[] = [
1089
1091
  - **아예 대상이 아닌 행이라면 `rows` 에서 거르는 편이 낫다.** 잠긴 행이 잔뜩 섞이면 무엇을 고를 수 있는지가 오히려 안 읽힌다. 고를 수 있는 행이 한 줄도 없으면 헤더까지 잠기는데, 그 상태라면 `selectable` 을 켤 자리가 아니다.
1090
1092
  - 잠금은 그리는 시점의 판정이라, **이미 `selected` 에 든 행이 나중에 잠겨도 DS 가 빼지 않는다** — 제어 상태를 말없이 바꾸지 않기 때문이다. 헤더의 전체 해제로는 걷힌다.
1091
1093
 
1094
+ #### 헤더 전체 선택이 집는 범위
1095
+
1096
+ **모드가 정한다. 앱이 보정하지 않는다.**
1097
+
1098
+ | 표 | 전체 선택이 집는 것 |
1099
+ | --- | --- |
1100
+ | 기본 | `rows` 전부 |
1101
+ | `pagination` · `internalPagination` | **현재 페이지** — 사용자가 고른 범위다 |
1102
+ | `virtualScroll` | `rows` **전부** — 그려진 창이 아니다 |
1103
+
1104
+ 가상 스크롤의 창은 사용자가 고른 범위가 아니라 렌더러의 사정이라, 창을 집으면 같은 버튼이 스크롤 위치에 따라 다른 결과를 낸다. 헤더의 체크 표시도 같은 집합을 본다 — 그래서 전체 선택 뒤 스크롤해도 "전부"가 "일부"로 흔들리지 않는다.
1105
+
1106
+ - **선택 열을 직접 그리지 않는다.** 전체 선택 범위 때문에 `selectable` 을 끄고 48px 열을 손으로 만들면 Shift 구간 선택 · `isRowSelectable` 연동 · sticky 선택 열 · 열 폭/정렬 기본값을 통째로 잃는다.
1107
+ - **`toggleSelectAll(checked, rows)`**(ref 명령형)은 대상 행을 인자로 받으므로 모드와 무관하다 — 앱이 원하는 집합을 그대로 넘긴다.
1108
+
1092
1109
  #### 표 본문의 스크롤에 닿아야 할 때
1093
1110
 
1094
1111
  표는 자기 안에서 스크롤한다(§2-2). 그 스크롤을 **읽거나 되돌려야** 하면 두 가지가 있다.
@@ -1343,6 +1360,52 @@ tableRef.current.scrollToRow(row); // 복원
1343
1360
  - **`header="fix"` 에서 GNB 를 접거나 펴면 열려 있던 패널은 닫힌다.** 계정 행이 메뉴 컬럼 바닥에서 접힘 레일 바닥으로(또는 반대로) 옮겨 서기 때문이다 — 닫히는 것을 앱이 막을 수 없고, 막을 이유도 없다(누른 그 행이 화면에서 사라진다). 전폭 상단바(`header="full"`)는 접어도 계정 행이 제자리라 열린 채 남는다. **그 닫힘을 알아야 하면 `account.onOpenChange` 를 준다** — 열림을 따라 그리는 화면이 앱에 있을 때만 필요하다.
1344
1361
  - **로그아웃을 여기 말고 다른 곳에 또 두지 않는다.** 계정에서 나가는 길이 화면마다 다르면 사용자가 매번 찾는다.
1345
1362
 
1363
+ #### 3-5-9. `SLauncherListBox` — 런처 버튼을 눌러 뜨는 서비스 목록
1364
+
1365
+ **"이 계정으로 갈 수 있는 다른 서비스가 무엇인가"를 한 자리에 모은 목록**이다. 한 줄이 서비스 하나이고, 위에 브랜드 로고(+ 상태 태그) 아래에 서비스 이름이 선다.
1366
+
1367
+ **직접 띄우지 않는다.** 런처 버튼에 붙여 여는 것은 `SGnb` 의 `launcher` 가 맡는다 — 목록만 넘기면 버튼 아래로 · 버튼 왼쪽 끝에 맞춰 뜨고, 서비스를 고르거나 바깥을 누르면 닫힌다.
1368
+
1369
+ ```tsx
1370
+ <SGnb
1371
+ items={MENU} value={current} onValueChange={navigate}
1372
+ logo={<Logo />}
1373
+ launcher={{
1374
+ items: [
1375
+ { value: 'sellmate', name: '셀메이트' },
1376
+ { value: 'wms', service: 'WMS', name: '셀메이트 WMS' },
1377
+ { value: 'account', service: 'Account', name: '셀메이트 어카운트',
1378
+ tag: 'NEW', tagColor: 'blue', tagIcon: 'star' },
1379
+ { value: 'crm', service: 'CRM', name: '셀메이트 CRM', tag: '출시예정', disabled: true },
1380
+ ],
1381
+ onSelect: value => go(SERVICE_URL[value]),
1382
+ }}
1383
+ />
1384
+ ```
1385
+
1386
+ - **`launcher` 를 주지 않으면 런처 버튼 자체가 렌더되지 않는다.** 옮겨 갈 서비스가 없는 앱은 이 슬롯을 비워 둔다 — 눌러도 아무것도 없는 버튼을 상단바에 남기지 않는다.
1387
+ - **어디로 갈지는 `launcher.onSelect` 하나로 받는다.** 줄마다 콜백을 두지 않는다 — 이 목록에서 일어나는 일은 "어느 서비스로 옮겨 가는가" 하나뿐이라, 고른 줄의 `value` 하나면 갈 곳이 정해진다. 고르면 목록은 스스로 닫히고, `disabled` 줄은 눌리지 않아 오지 않는다. (런처 **버튼**을 누른 것은 `launcher.onClick` 으로 따로 온다 — 여는 것 말고 할 일이 있을 때만 준다.)
1388
+ - **셀메이트 워드마크는 리스트박스가 늘 그린다.** 앱이 넘기는 것은 그 오른쪽에 잇는 **서비스 표기(`service`)** 뿐이다 — `WMS` · `Account` · `CRM` 같은 것. 워드마크를 앱이 넘기게 하면 같은 로고가 화면마다 다른 크기·색으로 선다. 셀메이트 본체처럼 표기가 없는 줄은 `service` 를 주지 않으면 워드마크만 선다.
1389
+ - **표기를 글자로 넘기면 크기·굵기·색을 주지 않는다.** 워드마크 옆에 서는 글자는 로고의 일부처럼 읽혀야 해서 리스트박스가 한 모습으로 정한다(14px bold · `fg.accentLight`). 서비스마다 색을 달리 주면 로고가 서비스마다 다른 물건처럼 보인다. 서비스가 자기 그래픽 마크를 가지면 svg·img 로 넘기고, 그때도 정해진 높이에 비율대로 들어가므로 **크기를 직접 주지 않는다.**
1390
+
1391
+ ```tsx
1392
+ ✅ { value: 'sellmate', name: '셀메이트' } // 표기 없음 — 워드마크만
1393
+ ✅ { value: 'wms', service: 'WMS', name: '셀메이트 WMS' }
1394
+ ✅ { value: 'x', service: <XMark />, name: '자기 마크를 가진 서비스' }
1395
+
1396
+ ❌ { value: 'wms', service: <><SLogo size={12} />WMS</>, … } // 워드마크를 다시 넘기지 않는다
1397
+ ❌ { value: 'wms', service: <span className="typo-body-sm-bold text-fg-success">WMS</span>, … }
1398
+ // 글자의 색·크기를 앱이 정하지 않는다
1399
+ ```
1400
+ - **아직 갈 수 없는 서비스는 목록에서 빼지 않고 `disabled` 로 둔다.** 그 서비스가 있다는 사실과 아직 못 쓴다는 사실을 함께 알리는 것이 런처의 일이다. **왜 못 쓰는지를 `tag` 로 함께 적는다**(`출시예정`·`미계약` 등) — 이유 없이 흐려진 줄은 고장으로 읽힌다.
1401
+ - **태그는 알릴 것이 있는 줄에만 붙인다.** 모든 줄에 붙으면 무엇이 새것인지가 읽히지 않는다.
1402
+ - **줄에 얹으면 "여기서 나간다"는 것이 드러난다** — 배경이 옅은 파랑으로 물들고 이름이 한 단계 진해지며, 오른쪽 끝에 페이지 이동 아이콘이 떠오른다. **앱이 켜고 끄는 prop 이 없다** — 다른 서비스로 넘어가는 자리라는 신호는 화면마다 달라지면 안 된다. `disabled` 줄은 얹어도 반응하지 않는다.
1403
+ - **지금 보고 있는 서비스를 표시하는 prop 은 없다.** 런처는 "여기 말고 어디로 갈 수 있는가"를 보여주는 자리라 현재 위치는 GNB 가 이미 말하고 있다.
1404
+ - **폭을 늘리지 않는다.** 로고 길이에 따라 패널이 출렁이지 않도록 고정 폭이다.
1405
+ - **뜨는 방향은 prop 이 아니다.** 런처 버튼이 상단바 왼쪽 끝에 있으므로 **아래로 · 버튼 왼쪽 끝에 맞춰** 펼친다. 방향을 바꾸는 prop 은 없으니 찾지 않는다.
1406
+ - **`header="fix"` 에서 GNB 를 접으면 열려 있던 목록은 닫힌다.** 접힘 레일에는 폴드 버튼만 남아 런처 버튼이 화면에서 사라지기 때문이다. **그 닫힘을 알아야 하면 `launcher.onOpenChange` 를 준다** — 열림을 따라 그리는 화면이 앱에 있을 때만 필요하다.
1407
+ - **서비스 전환을 `topContent` 나 GNB 메뉴에 또 만들지 않는다.** 다른 서비스로 가는 길은 런처 하나다.
1408
+
1346
1409
  ### 3-6. 영역 나누기 — SDivider vs SSplitter
1347
1410
 
1348
1411
  | 상황 | 사용 |
@@ -1711,7 +1774,8 @@ const [selectedId, setSelectedId] = useState<string>();
1711
1774
  - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다. **단 마운트 후 첫 호출은 값이 처음 그대로여도 나간다** — 마운트 시 자동 조회하지 않는 화면(다이얼로그 등)에서 조건을 하나도 넣지 않고 누른 첫 "검색" 이 막히면 안 되기 때문이다. 그래서 **마운트 시 조회할지 말지는 앱이 정한다** — `SChipFilter` 는 마운트만으로 `onSearch` 를 부르지 않는다.
1712
1775
  - **keyword 칩은 입력창에 남은 텍스트까지 조회에 넣는다.** Enter 로 담지 않고 팝오버를 닫아도(="검색" 클릭·바깥 클릭·Esc·다른 칩으로 전환) 그 텍스트를 키워드로 확정한 뒤 조회한다. 앱이 따로 확정시킬 일은 없다.
1713
1776
  - **노출할 칩을 앱이 계산하지 않는다.** 바에 놓이는 것은 `fixed`·`required` 필드와 **`value` 에 값이 들어 있는 필드**다. 노출 목록을 밖에서 넘기는 prop 은 없다. 쿼리스트링·서버 상태에서 조건을 복원하는 목록 화면도 **`value` 만 넘기면 칩이 함께 살아나고**, 그 조건을 빼면 칩도 함께 빠진다 — 칩과 조회 조건이 어긋날 자리가 없다. "필터 추가" 로 꺼낸 필터는 값을 넣기 전에도 자리를 지키지만, "검색 초기화" 를 누르거나 화면을 다시 그리면 사라진다. 걸린 조건이 없으니 문제되지 않는다.
1714
- - **`fixed`·`required` 이면서 후보가 하나뿐인 `select`·`select-multi` 자동으로 선택된다.** 고를 여지가 없는 목록이라 컴포넌트가 값을 채우고, 사용자가 고르지 않아도 `onValueChange` 값과 함께 호출된다 마운트 직후부터 조건이 맵에 들어 있다고 보고 조회를 짠다. 옵션이 API 응답이라 늦게 도착해도 도착한 시점에 채워지므로, 앱이 따로 채워 넣을 필요가 없다. 사용자가 값을 지우면 다시 채우지 않는다. **뺄 있는 필터는 후보가 하나여도 채우지 않는다** 처음에 바에 없는 필터라, 채우면 보이지도 지울 수도 없는 조건이 검색에 걸린다(`defaultValue` `fixed`·`required` 제한하는 것과 같은 이유). 후보가 하나뿐인 조건을 반드시 걸어야 하면 필드를 `fixed` `required` 준다.
1777
+ - **후보에 상하 관계가 있으면 `select-depth`·`select-multi-depth` 다.** 분류>세부분류처럼 묶어서 보여야 후보를 평평한 `select`·`select-multi` 늘어놓지 않는다. 계층은 `options` `children` 으로 만들고, `children` 가진 항목은 **그 자체로 고를 없는 그룹 헤더**라 **값에는 언제나 리프만 담긴다** 그룹의 `value` 맵에 들어오는 경로는 없으니 조회 파라미터를 리프만 온다고 보면 된다. `select-depth` 헤더는 라벨 전용이고, `select-multi-depth` 헤더는 체크박스로 아래 리프를 번에 토글한다(일부만 골랐으면 부분 선택으로 뜨고 `(고른 수/전체)` 붙는다). 그룹 아래가 전부 비활성이면 어미 헤더도 함께 잠기므로, 앱이 `disabled` 어미에 다시 달아 줄 필요가 없다.
1778
+ - **`fixed`·`required` 이면서 후보가 하나뿐인 `select` 계열은 자동으로 선택된다.** 고를 여지가 없는 목록이라 컴포넌트가 그 값을 채우고, 사용자가 고르지 않아도 `onValueChange` 가 그 값과 함께 호출된다 — 마운트 직후부터 그 조건이 값 맵에 들어 있다고 보고 조회를 짠다. 옵션이 API 응답이라 늦게 도착해도 도착한 시점에 채워지므로, 앱이 따로 채워 넣을 필요가 없다. 사용자가 그 값을 지우면 다시 채우지 않는다. **뺄 수 있는 필터는 후보가 하나여도 채우지 않는다** — 처음에 바에 없는 필터라, 채우면 보이지도 지울 수도 없는 조건이 검색에 걸린다(`defaultValue` 를 `fixed`·`required` 로 제한하는 것과 같은 이유). 후보가 하나뿐인 조건을 반드시 걸어야 하면 그 필드를 `fixed` 나 `required` 로 준다.
1715
1779
  - **`fields` 는 항상 그룹 배열이다.** 묶을 것이 없어도 `[{ fields: [...] }]` 로 한 겹 감싼다. 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 그 필드들만 별도 그룹으로 떼어 `rule` 을 준다 — 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다. 그룹 앞 구분선은 `divider` 로 켠다. 검증 단위와 구분선은 별개라, 묶어서 검증만 하고 싶으면 `divider` 를 주지 않는다.
1716
1780
 
1717
1781
  #### 3-7-12. 이미지 — SImage
@@ -1731,6 +1795,29 @@ const [selectedId, setSelectedId] = useState<string>();
1731
1795
  ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
1732
1796
  ```
1733
1797
 
1798
+ #### 3-7-14. 로고 — SLogo
1799
+
1800
+ 셀메이트 워드마크는 `<svg>` 를 직접 붙이지 않고 `SLogo` 를 쓴다. **`SIcon` 과 같은 손맛(`name`·`size`·`color`)이지만 두 가지가 다르다.**
1801
+
1802
+ - **`size` 는 높이다.** 로고는 가로로 긴 워드마크라 정사각으로 그리면 찌그러진다 — 폭은 비율에서 저절로 나오므로 주지 않는다.
1803
+ - **색을 주지 않으면 브랜드색이다.** `SIcon` 과 달리 currentColor 를 상속하지 않는다 — 로고가 주변 글자색을 따라 물들면 브랜드 색이 화면마다 달라진다. **비활성처럼 물러나야 하는 자리에서만** `color` 를 준다. 주면 워드마크 전체가 그 한 색이 된다.
1804
+ - **담고 있는 로고는 셀메이트 워드마크 하나다.** 서비스별 표기(WMS · Account · CRM …)는 그 오른쪽에 **앱이 자기 텍스트나 자기 로고로 붙인다** — 서비스가 늘어날 때마다 디자인 시스템을 다시 배포하지 않으려는 것이다.
1805
+ - **`label` 은 로고가 그 자리의 유일한 이름일 때만 준다**(로그인 화면·상단바). 옆이나 아래에 서비스 이름이 이미 적혀 있으면(런처 목록의 한 줄) 주지 않는다 — 같은 이름이 두 번 읽힌다.
1806
+
1807
+ ```tsx
1808
+ ✅ <SLogo size={20} /> {/* 상단바 로고 — 브랜드색 */}
1809
+ ✅ <SLogo size={32} label="Sellmate" /> {/* 로고가 그 자리의 유일한 이름일 때 */}
1810
+ ✅ <span className="inline-flex items-center gap-sd-2"> {/* 서비스별 표기는 앱이 잇는다 */}
1811
+ <SLogo size={12} />
1812
+ <span className="typo-body-xs-default text-(--sys-color-fg-success)">WMS</span>
1813
+ </span>
1814
+ ✅ <SLogo size={12} color="grey_45" /> {/* 아직 못 쓰는 서비스 */}
1815
+
1816
+ ❌ <svg viewBox="0 0 276 40">…</svg> {/* 워드마크를 손으로 붙이지 않는다 */}
1817
+ ❌ <SLogo size={24} style={{ width: 120 }} /> {/* 폭은 비율이 정한다 — 찌그러진다 */}
1818
+ ❌ <SLogo color="grey_45" /> {/* 물러날 이유가 없으면 브랜드색 그대로 둔다 */}
1819
+ ```
1820
+
1734
1821
  #### 3-7-13. 숫자 — SNumberInput 의 min·max 는 검증 경계다
1735
1822
 
1736
1823
  `min`·`max` 는 `<input type="number">` 의 그것과 같다 — **값을 고쳐 쓰지 않는 검증 경계**다. 범위를 벗어나면 blur·제출 시 에러 상태가 서고, 사용자가 친 값은 그대로 남아 앱의 범위 가드에 도달한다.
@@ -1898,7 +1985,8 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1898
1985
  - **계정·알림·설정·도메인·서비스 전환은 `topContent` 에 손으로 만들지 않는다.** `header="full"` 이면 `system` 슬롯의 조각들이 상단바 오른쪽 끝으로 올라오므로, 그것들은 전부 `SGnbSystem` 이 그린다 (바로 아래 절).
1899
1986
  - 슬롯이 남는 폭을 통째로 받으므로 **정렬은 안에서 직접 잡는다** (좌측 정렬 + 우측은 `ml-auto`).
1900
1987
  - `header="full"` 에서 로고 자리는 140px 로 고정된다 — 로고 내용이 바뀌어도 `topContent` 시작점이 흔들리지 않게 하기 위함이다. 로고가 그보다 넓으면 잘리므로 이 폭에 맞춰 준비한다.
1901
- - `onLauncherClick` 주지 않으면 런처가 렌더되지 않고, 그 자리(버튼 + 간격)를 로고 슬롯이 이어받아 172px 가 된다. `topContent` 시작점은 런처 유무와 관계없이 같은 자리다.
1988
+ - `launcher` 주지 않으면 런처가 렌더되지 않고, 그 자리(버튼 + 간격)를 로고 슬롯이 이어받아 172px 가 된다. `topContent` 시작점은 런처 유무와 관계없이 같은 자리다.
1989
+ - **런처 버튼은 서비스 목록의 트리거다.** `launcher.items` 에 옮겨 갈 서비스를 넘기면 버튼 아래로 목록이 뜬다 — 팝오버를 손으로 조립하지 않는다 (§3-5-9).
1902
1990
 
1903
1991
  ```tsx
1904
1992
  <SLayout type="box" header="full">
@@ -75,11 +75,13 @@ export interface SChipFilterChangeDetail {
75
75
 
76
76
  ```ts
77
77
  /** 필터 하나의 정의. type에 따라 쓸 수 있는 속성이 달라진다 —
78
- * options는 select·select-multi·keyword, presets·maxRange·radioButton은
78
+ * options는 select 계열·keyword, presets·maxRange·radioButton은
79
79
  * datepicker-range·datepicker-statistics, render는 custom 에만 있다 */
80
80
  export type SChipFilterField =
81
81
  | SChipFilterSelectField
82
82
  | SChipFilterSelectMultiField
83
+ | SChipFilterSelectDepthField
84
+ | SChipFilterSelectMultiDepthField
83
85
  | SChipFilterDatePickerDayField
84
86
  | SChipFilterDatePickerRangeField
85
87
  | SChipFilterDatePickerStatisticsField
@@ -129,6 +131,26 @@ export interface SChipFilterSelectMultiField extends SChipFilterOptionsField {
129
131
  }
130
132
  ```
131
133
 
134
+ ### SChipFilterSelectDepthField
135
+
136
+ ```ts
137
+ /** 계층 목록에서 후보 하나를 고른다. `children` 을 가진 항목은 라벨 전용 헤더라 고를 수 없다 —
138
+ * 값으로 올라오는 것은 리프뿐이다. 계층이 없다면 select 를 쓴다 */
139
+ export interface SChipFilterSelectDepthField extends SChipFilterOptionsField {
140
+ type: 'select-depth';
141
+ }
142
+ ```
143
+
144
+ ### SChipFilterSelectMultiDepthField
145
+
146
+ ```ts
147
+ /** 계층 목록에서 후보 여럿을 고른다. `children` 을 가진 항목은 체크박스를 달고 그 아래 리프를
148
+ * 한 번에 토글한다(일부만 골랐으면 부분 선택으로 표시) — 값에는 리프만 담긴다 */
149
+ export interface SChipFilterSelectMultiDepthField extends SChipFilterOptionsField {
150
+ type: 'select-multi-depth';
151
+ }
152
+ ```
153
+
132
154
  ### SChipFilterDatePickerDayField
133
155
 
134
156
  ```ts
@@ -240,11 +262,11 @@ export type SChipFilterCustomValue = Record<string, unknown>;
240
262
  ### SChipFilterOptionsField
241
263
 
242
264
  ```ts
243
- /** 후보 목록에서 고르는 필터 — select·select-multi·keyword */
265
+ /** 후보 목록에서 고르는 필터 — select·select-multi·select-depth·select-multi-depth·keyword */
244
266
  export interface SChipFilterOptionsField extends SChipFilterFieldBase {
245
- /** 고를 수 있는 후보 목록.
267
+ /** 고를 수 있는 후보 목록. select-depth·select-multi-depth 는 `children` 으로 계층을 만든다.
246
268
  *
247
- * **`fixed`·`required` 인 select·select-multi 에서 고를 수 있는 후보가 하나뿐이면 그 값이
269
+ * **`fixed`·`required` 인 select 계열에서 고를 수 있는 후보가 하나뿐이면 그 값이
248
270
  * 자동으로 선택된다** — 사용자가 고르지 않아도 `onValueChange` 로 값이 올라온다
249
271
  * (select 는 그 값, select-multi 는 그 값 하나짜리 배열). 옵션이 API 응답이라 늦게 도착해도
250
272
  * 도착한 시점에 채워진다. 사용자가 그 값을 지우면 다시 채우지 않는다 —
@@ -344,6 +366,14 @@ export interface SChipFilterOption {
344
366
  value: SChipFilterOptionValue;
345
367
  label: string;
346
368
  disabled?: boolean;
369
+ /** 하위 옵션. children 을 가진 항목은 **고를 수 없는 그룹 헤더**이고, 값으로 올라오는 것은
370
+ * 언제나 리프(children 없는 항목)의 value 뿐이다. 단일 타입에서 헤더는 라벨만, 다중 타입에서는
371
+ * 체크박스를 달고 그 아래 전체를 한 번에 토글한다.
372
+ *
373
+ * **계층을 담은 필드는 `select-depth`·`select-multi-depth` 로 선언한다.** 목록은 타입 이름이
374
+ * 아니라 children 유무를 보고 그리므로 `select`·`select-multi` 에 줘도 계층으로 그려지지만,
375
+ * 그러면 그 필드가 계층이라는 사실이 코드 어디에도 남지 않는다 */
376
+ children?: SChipFilterOption[];
347
377
  }
348
378
  ```
349
379
 
@@ -1,7 +1,8 @@
1
1
  import { type CSSProperties, type ReactNode } from 'react';
2
2
  import { type SDateRangeValue } from '../SDateRangePicker';
3
3
  /** 필터가 값을 받는 방식. 각 값에 대응하는 필드 인터페이스가 따로 있다 —
4
- * SChipFilterSelectField·SChipFilterSelectMultiField·SChipFilterKeywordField·
4
+ * SChipFilterSelectField·SChipFilterSelectMultiField·SChipFilterSelectDepthField·
5
+ * SChipFilterSelectMultiDepthField·SChipFilterKeywordField·
5
6
  * SChipFilterDatePickerDayField·SChipFilterDatePickerRangeField·
6
7
  * SChipFilterDatePickerStatisticsField·SChipFilterCustomField */
7
8
  export type SChipFilterType = SChipFilterField['type'];
@@ -12,6 +13,14 @@ export interface SChipFilterOption {
12
13
  value: SChipFilterOptionValue;
13
14
  label: string;
14
15
  disabled?: boolean;
16
+ /** 하위 옵션. children 을 가진 항목은 **고를 수 없는 그룹 헤더**이고, 값으로 올라오는 것은
17
+ * 언제나 리프(children 없는 항목)의 value 뿐이다. 단일 타입에서 헤더는 라벨만, 다중 타입에서는
18
+ * 체크박스를 달고 그 아래 전체를 한 번에 토글한다.
19
+ *
20
+ * **계층을 담은 필드는 `select-depth`·`select-multi-depth` 로 선언한다.** 목록은 타입 이름이
21
+ * 아니라 children 유무를 보고 그리므로 `select`·`select-multi` 에 줘도 계층으로 그려지지만,
22
+ * 그러면 그 필드가 계층이라는 사실이 코드 어디에도 남지 않는다 */
23
+ children?: SChipFilterOption[];
15
24
  }
16
25
  /** datepicker-range·datepicker-statistics 필드의 프리셋 라디오 항목 (오늘/지난 7일/일별/월별/사용자 지정 등) */
17
26
  export interface SChipFilterDatePreset {
@@ -74,11 +83,11 @@ export interface SChipFilterFieldBase {
74
83
  * 바 전체를 잠그려면 SChipFilterProps.disabled를 쓴다 — 둘은 OR로 합쳐진다 */
75
84
  disabled?: boolean;
76
85
  }
77
- /** 후보 목록에서 고르는 필터 — select·select-multi·keyword */
86
+ /** 후보 목록에서 고르는 필터 — select·select-multi·select-depth·select-multi-depth·keyword */
78
87
  export interface SChipFilterOptionsField extends SChipFilterFieldBase {
79
- /** 고를 수 있는 후보 목록.
88
+ /** 고를 수 있는 후보 목록. select-depth·select-multi-depth 는 `children` 으로 계층을 만든다.
80
89
  *
81
- * **`fixed`·`required` 인 select·select-multi 에서 고를 수 있는 후보가 하나뿐이면 그 값이
90
+ * **`fixed`·`required` 인 select 계열에서 고를 수 있는 후보가 하나뿐이면 그 값이
82
91
  * 자동으로 선택된다** — 사용자가 고르지 않아도 `onValueChange` 로 값이 올라온다
83
92
  * (select 는 그 값, select-multi 는 그 값 하나짜리 배열). 옵션이 API 응답이라 늦게 도착해도
84
93
  * 도착한 시점에 채워진다. 사용자가 그 값을 지우면 다시 채우지 않는다 —
@@ -113,6 +122,16 @@ export interface SChipFilterSelectField extends SChipFilterOptionsField {
113
122
  export interface SChipFilterSelectMultiField extends SChipFilterOptionsField {
114
123
  type: 'select-multi';
115
124
  }
125
+ /** 계층 목록에서 후보 하나를 고른다. `children` 을 가진 항목은 라벨 전용 헤더라 고를 수 없다 —
126
+ * 값으로 올라오는 것은 리프뿐이다. 계층이 없다면 select 를 쓴다 */
127
+ export interface SChipFilterSelectDepthField extends SChipFilterOptionsField {
128
+ type: 'select-depth';
129
+ }
130
+ /** 계층 목록에서 후보 여럿을 고른다. `children` 을 가진 항목은 체크박스를 달고 그 아래 리프를
131
+ * 한 번에 토글한다(일부만 골랐으면 부분 선택으로 표시) — 값에는 리프만 담긴다 */
132
+ export interface SChipFilterSelectMultiDepthField extends SChipFilterOptionsField {
133
+ type: 'select-multi-depth';
134
+ }
116
135
  /** keyword 필드의 입력 방식 — 무엇을 키워드 하나의 끝으로 볼지 */
117
136
  export type SChipFilterKeywordInput = 'tag' | 'csv';
118
137
  /** 키워드를 입력해 누적한다. options는 입력 중 후보로만 뜬다 */
@@ -166,9 +185,9 @@ export interface SChipFilterCustomField extends SChipFilterFieldBase {
166
185
  }) => ReactNode;
167
186
  }
168
187
  /** 필터 하나의 정의. type에 따라 쓸 수 있는 속성이 달라진다 —
169
- * options는 select·select-multi·keyword, presets·maxRange·radioButton은
188
+ * options는 select 계열·keyword, presets·maxRange·radioButton은
170
189
  * datepicker-range·datepicker-statistics, render는 custom 에만 있다 */
171
- export type SChipFilterField = SChipFilterSelectField | SChipFilterSelectMultiField | SChipFilterDatePickerDayField | SChipFilterDatePickerRangeField | SChipFilterDatePickerStatisticsField | SChipFilterKeywordField | SChipFilterCustomField;
190
+ export type SChipFilterField = SChipFilterSelectField | SChipFilterSelectMultiField | SChipFilterSelectDepthField | SChipFilterSelectMultiDepthField | SChipFilterDatePickerDayField | SChipFilterDatePickerRangeField | SChipFilterDatePickerStatisticsField | SChipFilterKeywordField | SChipFilterCustomField;
172
191
  export interface SChipFilterChangeDetail {
173
192
  key: string;
174
193
  value: SChipFilterValue;
@@ -1 +1 @@
1
- export { SChipFilter, type SChipFilterChangeDetail, type SChipFilterCustomField, type SChipFilterCustomValue, type SChipFilterDatePickerDayField, type SChipFilterDatePickerRangeField, type SChipFilterDatePickerStatisticsField, type SChipFilterDatePreset, type SChipFilterField, type SChipFilterFieldBase, type SChipFilterGroup, type SChipFilterGroupRule, type SChipFilterHandle, type SChipFilterKeywordField, type SChipFilterKeywordInput, type SChipFilterKeywordValue, type SChipFilterMatchMode, type SChipFilterOption, type SChipFilterOptionValue, type SChipFilterOptionsField, type SChipFilterPeriodUnit, type SChipFilterPeriodValue, type SChipFilterPresetsField, type SChipFilterProps, type SChipFilterSelectField, type SChipFilterSelectMultiField, type SChipFilterType, type SChipFilterValue, type SChipFilterValueMap, } from './SChipFilter';
1
+ export { SChipFilter, type SChipFilterChangeDetail, type SChipFilterCustomField, type SChipFilterCustomValue, type SChipFilterDatePickerDayField, type SChipFilterDatePickerRangeField, type SChipFilterDatePickerStatisticsField, type SChipFilterDatePreset, type SChipFilterField, type SChipFilterFieldBase, type SChipFilterGroup, type SChipFilterGroupRule, type SChipFilterHandle, type SChipFilterKeywordField, type SChipFilterKeywordInput, type SChipFilterKeywordValue, type SChipFilterMatchMode, type SChipFilterOption, type SChipFilterOptionValue, type SChipFilterOptionsField, type SChipFilterPeriodUnit, type SChipFilterPeriodValue, type SChipFilterPresetsField, type SChipFilterProps, type SChipFilterSelectDepthField, type SChipFilterSelectField, type SChipFilterSelectMultiDepthField, type SChipFilterSelectMultiField, type SChipFilterType, type SChipFilterValue, type SChipFilterValueMap, } from './SChipFilter';
@@ -31,5 +31,8 @@ export interface SGhostButtonProps {
31
31
  /**
32
32
  * SGhostButton — sd-ghost-button 포팅. 아이콘 전용 투명 버튼.
33
33
  * hover 시 intent 색 오버레이(5%), focus-visible 링, 옵션 툴팁(회색 STag).
34
+ *
35
+ * ref 는 버튼 요소로 그대로 간다 — 이 버튼에 붙여 띄우는 층(SGnb 런처의 서비스 목록 등)이
36
+ * 버튼을 앵커로 삼아야 한다. 툴팁을 두른 형태에서도 앵커는 래퍼가 아니라 버튼이다.
34
37
  */
35
- export declare function SGhostButton({ icon, size, intent, ariaLabel, ariaPressed, ariaExpanded, tooltipText, disabled, onClick, anchorClassName, anchorStyle, className, style, }: SGhostButtonProps): import("react").JSX.Element;
38
+ export declare const SGhostButton: import("react").ForwardRefExoticComponent<SGhostButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -15,6 +15,7 @@
15
15
  | `showRail?` | `boolean` | — | 좌측 레일 사용 여부. 켜면 items 의 첫 depth 가 아이콘+라벨 버튼의 세로 레일로 서고, 선택된 레일 아이템의 children 이 오른쪽 메뉴 패널에 깔린다. 미지정 시 SLayout 의 showRail 을 따른다. children 없는 레일 아이템이 활성이면 메뉴 패널은 렌더되지 않고 레일만 남는다. |
16
16
  | `value?` | `string` | `''` | 현재 선택된 아이템 value |
17
17
  | `folded?` | `boolean` | — | 접힘(레일) 상태. 미지정 시 SLayout 의 folded 를 따른다. |
18
+ | `launcher?` | `SGnbLauncher` | — | 앱런처(그리드) 버튼. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. |
18
19
  | `logo?` | `ReactNode` | — | 상단바 로고 영역 (slot). header="full" 이면 폭이 140px 로 고정된다. |
19
20
  | `topContent?` | `ReactNode` | — | 상단바 로고 오른쪽 슬롯 (검색·액션 등). 로고와 16px 띄고 남는 폭을 모두 차지하므로 안에서 자유롭게 정렬한다. 상단바가 전폭인 header="full" 에서만 렌더된다 (fix 는 상단바가 좁은 GNB 컬럼 안이라 놓을 자리가 없다). |
20
21
  | `railTop?` | `ReactNode` | — | 레일 상단 고정 슬롯. 레일 아이템이 많아 넘치면 아이템 목록(ul)만 스크롤되고 이 슬롯은 레일 상단(상단바 바로 아래)에 붙어 고정된다. showRail 일 때만 렌더된다. |
@@ -36,7 +37,6 @@
36
37
  | `onValueChange` | `(value: string) => void` | 선택 변경 (sdUpdate) |
37
38
  | `onRailChange` | `(value: string) => void` | 레일 선택 변경. 레일 아이템을 눌러 패널이 바뀔 때 알린다(선택 상태는 SGnb 가 자체 관리). |
38
39
  | `onFoldedChange` | `(folded: boolean) => void` | 접힘 토글 (sdFoldChange) |
39
- | `onLauncherClick` | `() => void` | 앱런처(그리드) 버튼 클릭. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. |
40
40
  | `onMenuWidthChange` | `(width: number) => void` | 메뉴 폭이 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
41
41
 
42
42
  ## Types
@@ -84,6 +84,38 @@ export interface SGnbMenuItem {
84
84
  }
85
85
  ```
86
86
 
87
+ ### SGnbLauncher
88
+
89
+ ```ts
90
+ /** 앱런처(그리드) 버튼 */
91
+ export interface SGnbLauncher {
92
+ /**
93
+ * 누르면 뜨는 서비스 목록(`SLauncherListBox`). 옮겨 갈 수 있는 서비스가 무엇인지는
94
+ * 서비스마다 다르지만 **뜨는 자리는 늘 같아야 하므로**, 방향·앵커·여닫힘(바깥 클릭·ESC 포함)과
95
+ * 배경·라운드·그림자는 컴포넌트가 쥐고 앱은 목록만 준다 — 런처 버튼 아래로, 버튼 왼쪽 끝에 맞춰 뜬다.
96
+ *
97
+ * 비면 목록이 뜨지 않는다 — 아직 목록을 못 받은 화면에서 빈 흰 상자가 뜨는 대신
98
+ * 버튼만 남고 `onClick` 은 그대로 온다.
99
+ */
100
+ items?: SLauncherListBoxItem[];
101
+ /** 런처 **버튼**을 누름. 목록은 어차피 열리므로, 여는 것 말고 따로 할 일이 있을 때만 준다 */
102
+ onClick?: () => void;
103
+ /**
104
+ * 목록에서 **서비스를 고름**. 고른 줄의 `value` 가 온다 — 그 값 하나로 앱이 갈 곳을 정한다.
105
+ * 고르면 목록은 할 일을 끝낸 것이라 스스로 닫힌다(`onOpenChange(false)` 도 함께 온다).
106
+ */
107
+ onSelect?: (value: string) => void;
108
+ /**
109
+ * 목록이 열리고 닫힐 때. 여닫는 것은 런처 버튼이 스스로 하므로 **그 사실을 알아야 할 때만** 준다
110
+ * (연 김에 목록을 다시 불러오거나, 열려 있는 동안 다른 층을 접어 두는 화면).
111
+ *
112
+ * 앱이 누르지 않은 닫힘도 온다: `header="fix"` 에서 GNB 를 접으면 런처 버튼이 사라지므로
113
+ * 목록이 닫히고 `false` 가 온다.
114
+ */
115
+ onOpenChange?: (open: boolean) => void;
116
+ }
117
+ ```
118
+
87
119
  ## Dependencies
88
120
 
89
121
  ### Used by
@@ -94,7 +126,9 @@ export interface SGnbMenuItem {
94
126
 
95
127
  - [SGhostButton](../SGhostButton)
96
128
  - [SIcon](../SIcon)
129
+ - [SLauncherListBox](../SLauncherListBox)
97
130
  - [SLayout](../SLayout)
131
+ - [SPortal](../SPortal)
98
132
  - [STag](../STag)
99
133
 
100
134
  ### Graph
@@ -103,7 +137,9 @@ export interface SGnbMenuItem {
103
137
  graph TD;
104
138
  SGnb --> SGhostButton
105
139
  SGnb --> SIcon
140
+ SGnb --> SLauncherListBox
106
141
  SGnb --> SLayout
142
+ SGnb --> SPortal
107
143
  SGnb --> STag
108
144
  SGnbSystem --> SGnb
109
145
  style SGnb fill:#f9f,stroke:#333,stroke-width:4px
@@ -1,4 +1,5 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type SLauncherListBoxItem } from '../SLauncherListBox';
2
3
  import type { SGnbSystemPlacement } from '../SGnbSystem/gnbSystem.config';
3
4
  import { type SGnbType, type SGnbHeader, type SGnbColor, type SGnbMenuItem } from './gnb.config';
4
5
  /**
@@ -40,6 +41,33 @@ export interface SGnbSystemContextValue {
40
41
  narrow?: boolean;
41
42
  }
42
43
  export declare const SGnbSystemContext: import("react").Context<SGnbSystemContextValue | null>;
44
+ /** 앱런처(그리드) 버튼 */
45
+ export interface SGnbLauncher {
46
+ /**
47
+ * 누르면 뜨는 서비스 목록(`SLauncherListBox`). 옮겨 갈 수 있는 서비스가 무엇인지는
48
+ * 서비스마다 다르지만 **뜨는 자리는 늘 같아야 하므로**, 방향·앵커·여닫힘(바깥 클릭·ESC 포함)과
49
+ * 배경·라운드·그림자는 컴포넌트가 쥐고 앱은 목록만 준다 — 런처 버튼 아래로, 버튼 왼쪽 끝에 맞춰 뜬다.
50
+ *
51
+ * 비면 목록이 뜨지 않는다 — 아직 목록을 못 받은 화면에서 빈 흰 상자가 뜨는 대신
52
+ * 버튼만 남고 `onClick` 은 그대로 온다.
53
+ */
54
+ items?: SLauncherListBoxItem[];
55
+ /** 런처 **버튼**을 누름. 목록은 어차피 열리므로, 여는 것 말고 따로 할 일이 있을 때만 준다 */
56
+ onClick?: () => void;
57
+ /**
58
+ * 목록에서 **서비스를 고름**. 고른 줄의 `value` 가 온다 — 그 값 하나로 앱이 갈 곳을 정한다.
59
+ * 고르면 목록은 할 일을 끝낸 것이라 스스로 닫힌다(`onOpenChange(false)` 도 함께 온다).
60
+ */
61
+ onSelect?: (value: string) => void;
62
+ /**
63
+ * 목록이 열리고 닫힐 때. 여닫는 것은 런처 버튼이 스스로 하므로 **그 사실을 알아야 할 때만** 준다
64
+ * (연 김에 목록을 다시 불러오거나, 열려 있는 동안 다른 층을 접어 두는 화면).
65
+ *
66
+ * 앱이 누르지 않은 닫힘도 온다: `header="fix"` 에서 GNB 를 접으면 런처 버튼이 사라지므로
67
+ * 목록이 닫히고 `false` 가 온다.
68
+ */
69
+ onOpenChange?: (open: boolean) => void;
70
+ }
43
71
  export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'> {
44
72
  /** 메뉴 스타일: box(라운드) / belt(풀폭 행). 미지정 시 SLayout 의 type 을 따른다. */
45
73
  type?: SGnbType;
@@ -68,8 +96,11 @@ export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>
68
96
  folded?: boolean;
69
97
  /** 접힘 토글 (sdFoldChange) */
70
98
  onFoldedChange?: (folded: boolean) => void;
71
- /** 앱런처(그리드) 버튼 클릭. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. */
72
- onLauncherClick?: () => void;
99
+ /**
100
+ * 앱런처(그리드) 버튼. 미지정 시 런처 버튼을 렌더하지 않는다.
101
+ * 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다.
102
+ */
103
+ launcher?: SGnbLauncher;
73
104
  /** 상단바 로고 영역 (slot). header="full" 이면 폭이 140px 로 고정된다. */
74
105
  logo?: ReactNode;
75
106
  /**
@@ -137,6 +137,12 @@ export declare const clampGnbMenuWidth: (width: number) => number;
137
137
  * 런처를 쓰지 않으면 그 자리를 로고가 이어받는다 → GNB_FULL_LOGO_WIDTH_NO_LAUNCHER.
138
138
  */
139
139
  export declare const GNB_FULL_LOGO_WIDTH = 140;
140
+ /**
141
+ * 앱런처 버튼·목록의 접근성 이름.
142
+ * 버튼에는 보이는 글자가 없고(아이콘만) 목록에도 제목이 없어, 둘이 같은 이름을 나눠 갖는다 —
143
+ * 스크린리더에서 "무엇을 눌러 무엇이 열렸는지" 가 한 이름으로 이어진다.
144
+ */
145
+ export declare const GNB_LAUNCHER_LABEL = "app launcher";
140
146
  /**
141
147
  * 상단바에서 아이콘 버튼끼리 붙는 간격(px, 디자인 스펙 — 전용 토큰 없음).
142
148
  * header="full" 의 런처↔폴드가 여기 해당한다. 슬롯(로고·topContent)과의 간격은
@@ -44,6 +44,7 @@
44
44
  - [SGuide](../SGuide)
45
45
  - [SImage](../SImage)
46
46
  - [SKeyValueTable](../SKeyValueTable)
47
+ - [SLauncherListBox](../SLauncherListBox)
47
48
  - [SListItem](../SListItem)
48
49
  - [SLoadingModal](../SLoadingModal)
49
50
  - [SNumberInput](../SNumberInput)
@@ -90,6 +91,7 @@ graph TD;
90
91
  SGuide --> SIcon
91
92
  SImage --> SIcon
92
93
  SKeyValueTable --> SIcon
94
+ SLauncherListBox --> SIcon
93
95
  SListItem --> SIcon
94
96
  SLoadingModal --> SIcon
95
97
  SNumberInput --> SIcon
@@ -0,0 +1,78 @@
1
+ # SLauncherListBox
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SLauncherListBox
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `items?` | `SLauncherListBoxItem[]` | — | 옮겨 갈 수 있는 서비스 목록. 비어 있으면 아무것도 렌더하지 않는다 |
12
+ | `ariaLabel?` | `string` | — | 목록(ul)의 접근성 레이블 |
13
+
14
+ #### Events
15
+
16
+ | Event | Type | Description |
17
+ |-------|------|-------------|
18
+ | `onSelect` | `(value: string) => void` | 서비스를 고름. **줄마다 콜백을 두지 않는다** — 이 목록에서 일어나는 일은 "어느 서비스로 옮겨 가는가" 하나뿐이라, 고른 줄의 `value` 하나면 앱이 갈 곳을 정할 수 있다. `disabled` 줄은 눌리지 않으므로 여기로 오지 않는다. |
19
+
20
+ ## Types
21
+
22
+ ### SLauncherListBoxItem
23
+
24
+ ```ts
25
+ /** 리스트박스의 한 줄 — 옮겨 갈 수 있는 서비스 하나 */
26
+ export interface SLauncherListBoxItem {
27
+ /** 목록 key */
28
+ value: string;
29
+ /**
30
+ * 워드마크 오른쪽에 잇는 **서비스 표기** (slot) — `WMS` · `Account` · `CRM` 같은 것.
31
+ * 셀메이트 워드마크는 이 컴포넌트가 늘 그리므로 넘기지 않는다. 없으면 워드마크만 선다.
32
+ *
33
+ * **글자로 넘기면 크기·굵기·색을 주지 않는다** — 워드마크 옆에 서는 글자는 로고의 일부처럼
34
+ * 읽혀야 해서 이 컴포넌트가 한 모습으로 정한다(14px bold · `fg.accentLight`).
35
+ *
36
+ * ```tsx
37
+ * { value: 'wms', service: 'WMS', name: '셀메이트 WMS' }
38
+ * ```
39
+ *
40
+ * 서비스가 자기 그래픽 마크를 가지면 svg·img 로 넘긴다 — 로고 높이에 맞춰 비율대로 들어가므로
41
+ * 크기를 따로 주지 않는다.
42
+ */
43
+ service?: ReactNode;
44
+ /** 로고 아래 줄에 적히는 서비스 이름 */
45
+ name?: string;
46
+ /** 로고 오른쪽에 붙는 태그 텍스트. 없으면 태그가 붙지 않는다 */
47
+ tag?: string;
48
+ /** 태그 색. 기본 `'grey'` */
49
+ tagColor?: STagColor;
50
+ /** 태그 라벨 왼쪽 아이콘. 없으면 글자만 남는다 */
51
+ tagIcon?: SIconName;
52
+ /** 아직 갈 수 없는 서비스(출시 전 · 미계약). 눌리지 않고 로고·이름이 물러난다 */
53
+ disabled?: boolean;
54
+ }
55
+ ```
56
+
57
+ ## Dependencies
58
+
59
+ ### Used by
60
+
61
+ - [SGnb](../SGnb)
62
+
63
+ ### Depends on
64
+
65
+ - [SIcon](../SIcon)
66
+ - [SLogo](../SLogo)
67
+ - [STag](../STag)
68
+
69
+ ### Graph
70
+
71
+ ```mermaid
72
+ graph TD;
73
+ SLauncherListBox --> SIcon
74
+ SLauncherListBox --> SLogo
75
+ SLauncherListBox --> STag
76
+ SGnb --> SLauncherListBox
77
+ style SLauncherListBox fill:#f9f,stroke:#333,stroke-width:4px
78
+ ```