sellmate-design-system-react 9.0.0-beta.30 → 9.0.0-beta.32

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 (30) hide show
  1. package/AGENTS.md +111 -21
  2. package/dist/components/SAccountListBox/AccountListBoxPortal.d.ts +31 -0
  3. package/dist/components/SAccountListBox/README.md +86 -0
  4. package/dist/components/SAccountListBox/SAccountListBox.d.ts +79 -0
  5. package/dist/components/SAccountListBox/SystemMenuPortal.d.ts +58 -0
  6. package/dist/components/SAccountListBox/accountListBox.config.d.ts +64 -0
  7. package/dist/components/SAccountListBox/index.d.ts +2 -0
  8. package/dist/components/SChipFilter/README.md +16 -3
  9. package/dist/components/SChipFilter/SChipFilter.d.ts +22 -4
  10. package/dist/components/SGnb/README.md +1 -1
  11. package/dist/components/SGnb/SGnb.d.ts +29 -7
  12. package/dist/components/SGnbSystem/README.md +29 -6
  13. package/dist/components/SGnbSystem/SGnbSystem.d.ts +57 -17
  14. package/dist/components/SGnbSystem/gnbSystem.config.d.ts +34 -1
  15. package/dist/components/SGnbSystem/index.d.ts +1 -1
  16. package/dist/components/SIcon/README.md +2 -0
  17. package/dist/components/SIcon/icons.gen.d.ts +2 -0
  18. package/dist/components/SPortal/README.md +2 -0
  19. package/dist/components/SSystemActionButton/README.md +96 -9
  20. package/dist/components/SSystemActionButton/SSystemActionButton.d.ts +97 -15
  21. package/dist/components/SSystemActionButton/index.d.ts +1 -1
  22. package/dist/index.cjs +1131 -398
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1125 -400
  26. package/dist/index.js.map +1 -1
  27. package/dist/llms-full.txt +333 -40
  28. package/dist/llms.txt +113 -23
  29. package/dist/styles.css +66 -26
  30. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -30,7 +30,7 @@
30
30
  | **입력 (폼)** | `SForm` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
31
31
  | **날짜·시간** | `SCalendar` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
32
32
  | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
33
- | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
33
+ | **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
34
34
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
35
35
  | **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
36
36
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
@@ -150,11 +150,11 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
150
150
 
151
151
  | 층 | 무엇인가 | 컴포넌트 |
152
152
  | --- | --- | --- |
153
- | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 패널) `SPage`(제목 영역은 `header` prop) |
153
+ | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage`(제목 영역은 `header` prop) |
154
154
  | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
155
155
  | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `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` `SImage` `SLinearProgress` `SCircleProgress` |
156
156
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
157
- | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
157
+ | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
158
158
 
159
159
  여기에 화면을 차지하지 않는 **부트스트랩** 이 따로 있다 — `SModalOutlet` `SToastContainer` 는 앱 진입점에 한 번만 렌더한다 (§4-1).
160
160
 
@@ -534,7 +534,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
534
534
  | --- | --- | --- |
535
535
  | 앱 셸(상단바 + 내비 + 본문)을 세운다 | `SLayout` | §4-1 |
536
536
  | 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
537
- | 좌측 내비게이션 맨 아래에 서비스·도메인·알림·설정·계정 묶음을 붙인다 | `SGnbSystem` | §4-1 |
537
+ | 좌측 내비게이션 맨 아래(또는 전폭 상단바 오른쪽 끝)에 서비스·도메인·알림·설정·계정 묶음을 붙인다 | `SGnbSystem` | §4-1 |
538
+ | 앱 상단바 오른쪽 끝에 알림 벨·계정 이름을 둔다 | `SGnbSystem` (`SGnb` 의 `system` 슬롯 — 직접 만들지 않는다) | §4-1 |
539
+ | 계정 이름을 눌러 이메일·권한·계정 설정·언어 변경·로그아웃을 띄운다 | `SAccountListBox` (`SGnbSystem` 의 `account.listBox` 로 넘긴다 — 직접 띄우지 않는다) | §4-1 |
538
540
  | 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
539
541
  | 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPage` 의 `header` prop | §4-1 |
540
542
  | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
@@ -1165,20 +1167,83 @@ const columns: STableColumn[] = [
1165
1167
 
1166
1168
  | `type` | 선행 아이콘 | 라벨 | 우측 | 언제 |
1167
1169
  | --- | --- | --- | --- | --- |
1168
- | `account` | `user` 고정 | `label` | 없음 | 로그인한 계정을 보여주고 계정 화면으로 보낼 |
1170
+ | `account` | `user` 고정 | `label` | 없음 | 로그인한 계정을 보여줄 때. 누르면 **언제나** 계정 리스트박스가 뜬다 `option` 필수 (§3-5-8) |
1169
1171
  | `select` | `option.icon` (필수) | `label` | 화살표 | 눌러서 하위 화면으로 들어갈 때 |
1170
1172
  | `switch` | `option.icon` (필수) | `label` | `option.text` + 새로고침 | 지금 붙어 있는 대상을 갈아 끼울 때 |
1171
- | `subSelect` | `option.icon` (필수) | `label` | `option.text` + 화살표 | 지금 선택돼 있는 값을 함께 보여줄 |
1172
- | `status` | 없음 | 상태 태그(`option.color`·`option.label`) | 화살표 | 라벨 대신 상태 자체가 제목일 |
1173
+ | `subSelect` | `option.icon` (필수) | `label` | 지금 + 화살표 | 값을 보여주고 **눌러서 바꿀** 때. 누르면 언제나 메뉴가 뜬다 — `option.options` 필수 |
1174
+ | `status` | 없음 | 상태 태그(고른 칸의 `color`·`label`) | 화살표 | 상태를 보여주고 **눌러서 바꿀** 때. 누르면 언제나 메뉴가 뜬다 — `option.options` 필수 |
1173
1175
 
1174
1176
  - **`SPage` 본문에 직접 놓지 않는다.** 폭을 가득 채우는 행이라 페이지에 바로 놓으면 표처럼 보인다. GNB system 패널·`SDrawer`·`SPopover` 처럼 **좁은 폭이 정해진 자리**에 쌓는다. 행 사이 간격은 `--cmp-gnb-system-panel-gap` 이다.
1175
- - **GNB 맨 아래에 쌓는 것이라면 이 버튼을 직접 배치하지 말고 `SGnbSystem` 의 `actions` 에 넘긴다** (§4-1). 패널의 배경·구분선·도메인 태그·알림·계정까지 한 벌로 그려주므로, 조각을 손으로 조립할 이유가 없다.
1177
+ - **GNB 의 system 자리(아래 · 전폭 상단바)에 놓는 것이라면 이 버튼을 직접 배치하지 말고 `SGnbSystem` 의 `actions` 에 넘긴다** (§4-1). 패널의 배경·구분선·도메인 태그·알림·계정까지 한 벌로 그려주므로, 조각을 손으로 조립할 이유가 없다. **단 `type="account"` 는 `actions` 가 받지 않는다** — 계정 행은 패널에 하나뿐이고 자리도 맨 아래로 정해져 있어 `account` prop 이 따로 있다 (§3-5-8).
1176
1178
  - **`color` 는 놓이는 표면을 따라간다** — 흰 면이면 `light`, 오션블루 면이면 `dark`, 네이비 면이면 `darker`. 배경과 다른 색을 고르면 버튼만 떠 보인다.
1177
1179
  - **상태는 default·hover 둘뿐이다.** 토큰에 selected 가 없으므로 "지금 열려 있는 항목"을 이 버튼으로 표시하지 않는다. 선택 상태가 필요한 자리면 `SGnb` 메뉴나 `SListItem`(§3-7-6) 이다.
1178
1180
  - **선행 아이콘이 비는 행은 없다.** `account` 는 `user` 고정, `status` 는 태그가 자리를 차지하고, 나머지는 `option.icon` 이 필수다.
1181
+ - **`account` 의 `option` 만 성격이 다르다** — 아이콘이 아니라 **누르면 뜨는 계정 리스트박스의 내용**이고, **필수다** (§3-5-8). 눌러도 아무것도 뜨지 않는 계정 행은 없다 — 계정 화면으로 넘기는 것은 이 행이 아니라 패널의 `accountSetting` 이 맡는다.
1179
1182
  - **우측 아이콘은 `type` 이 정하므로 밖에서 바꾸지 않는다.** 화살표가 필요한데 문구가 없으면 `select`, 문구가 함께 필요하면 `subSelect` 다.
1183
+ - **`subSelect` · `status` 는 언제나 값 고르기 메뉴의 트리거다** — 지금 값이 적혀 있는 행이라 누르면 그 값을 바꾸는 메뉴가 뜬다. 그래서 `option.options` 가 **필수**이고, 메뉴는 **행 오른쪽으로**, 행 위쪽 끝에 맞춰 펼쳐진다 — 이 행은 판 안에 세로로 쌓이는 자리라 아래로 펴면 바로 다음 행을 덮는다. 고르면 `onChange(value)` 가 돌며 닫힌다. **메뉴를 손으로 만들지 않는다** — 뜨는 자리·닫히는 시점이 어긋난다. 계정 패널의 언어 변경 행(§3-5-8)도 같은 메뉴이지만, 그쪽은 판 안에 갇혀 있어 우측 값 아래로 편다.
1184
+
1185
+ ```tsx
1186
+ <SSystemActionButton
1187
+ type="subSelect" label="작업 공간"
1188
+ option={{ icon: 'board', value: workspaceId, options: WORKSPACES, onChange: setWorkspace }}
1189
+ />
1190
+ <SSystemActionButton
1191
+ type="status"
1192
+ option={{ value: status, options: STATUSES, onChange: setStatus }} // STATUSES: { value, label, color? }[]
1193
+ />
1194
+ ```
1195
+
1196
+ - **`value` 를 함께 준다.** 우측 값(`subSelect`)·상태 태그(`status`)는 `options` 에서 같은 `value` 를 찾아 그 `label`(과 `status` 는 `color`)로 그린다. 못 찾으면 `value` 문자열이 그대로 노출되므로 반드시 맞춰 준다.
1197
+ - **눌러서 값 고르는 화면으로 보낼 것이면 `select` 다.** 그 타입은 행을 누르는 것 자체가 액션이라 `onClick` 만 돈다. 상태를 **보여주기만** 할 것이면 이 버튼이 아니라 `STag`(§3-7-2) 다 — 화살표가 붙는 행은 "눌러서 바꾼다"는 약속이다.
1198
+ - **태그의 색·이름을 밖에서 직접 주지 않는다** (`status`). `option.color`·`option.label` 은 없다 — 지금 상태가 가리키는 `options` 칸이 곧 태그다. 색이 상태의 뜻이라 둘이 어긋나면 안 되기 때문이다.
1199
+ - **상태 메뉴의 색은 칸마다 준다** — 점과 글자가 그 상태의 색으로 함께 물든다. 상태는 색 자체가 뜻이라 고르는 자리에서도 색을 보고 고른다.
1200
+ - `switch` 는 메뉴를 갖지 않는다 — 우측 문구가 "지금 값"이 아니라 보조 조작 문구라, 고를 목록이 있으면 그것은 `subSelect` 다.
1180
1201
  - 높이·라운드·아이콘 크기·좌우 여백은 전부 토큰이 넣는다. 직접 주지 않는다.
1181
1202
 
1203
+ #### 3-5-8. `SAccountListBox` — 계정 행을 눌러 뜨는 계정 패널
1204
+
1205
+ **"지금 누구로 로그인해 있는가"와 "여기서 나간다"를 한 자리에 모은 패널**이다. 위에서부터 [사용자정보(이메일 · 이름 + 권한)] · [계정 설정] · [언어 변경] · [계정 로그아웃] 이다.
1206
+
1207
+ **직접 띄우지 않는다.** 계정 행에 붙여 여는 것은 두 곳이 맡는다 — 어느 쪽이든 뜨는 방향·닫히는 시점을 컴포넌트가 정하므로 앱이 팝오버를 조립하지 않는다. **두 곳 모두 이 내용이 필수다** — 계정 행은 예외 없이 이 패널의 트리거라, 눌러도 아무것도 뜨지 않는 계정 행은 만들 수 없다.
1208
+
1209
+ | 어디에 | 어떻게 |
1210
+ | --- | --- |
1211
+ | GNB 의 system 자리 (기본) | `SGnbSystem` 의 `account.listBox` — 필수 (§4-1) |
1212
+ | system 밖의 **좁은 세로 판 아래쪽** (SDrawer·SPopover 하단 등) | `SSystemActionButton type="account"` 의 `option` — 필수 |
1213
+
1214
+ ```tsx
1215
+ <SGnbSystem
1216
+ account={{
1217
+ label: userName,
1218
+ listBox: {
1219
+ email: user.email,
1220
+ name: user.name,
1221
+ authority: user.authority, // 옵션
1222
+ accountSetting: { onClick: openAccountSetting }, // 옵션
1223
+ language: { // 옵션
1224
+ value: languageCode,
1225
+ options: LANGUAGES, // 주면 하위 메뉴가 펼쳐진다
1226
+ onChange: setLanguage,
1227
+ },
1228
+ logout: { onClick: signOut },
1229
+ },
1230
+ }}
1231
+ />
1232
+ ```
1233
+
1234
+ - **`authority` · `accountSetting` · `language` 는 옵션이고, 사용자정보와 로그아웃 행은 늘 있다.** 이 패널이 존재하는 이유가 그 둘이다. 옵션을 빼면 그 줄이 통째로 사라지고 나머지가 위로 붙으므로, 줄을 지우려고 빈 문자열이나 빈 객체를 넣지 않는다 (`accountSetting={{}}` 은 "이 행을 쓴다" 는 뜻이다).
1235
+ - **권한 prop 은 `authority` 다 (`role` 아님).** `role` 은 DOM 의 ARIA 속성이라, 그 이름을 쓰면 소비 앱의 a11y lint 가 "최고 관리자" 를 ARIA 롤로 읽고 실패한다.
1236
+ - **`language` 를 쓰면 `value` 를 함께 준다.** 우측에 붙는 그 값이 "지금 무슨 언어인가" 다 — 없으면 우측이 비어 눌러야 알 수 있는 행이 된다.
1237
+ - **언어를 앱 안에서 바꾸면 `language.options` 를 준다.** 그 행이 우측 값 아래로 하위 메뉴를 펴고, 고르면 `onChange(value)` 가 돌며 하위 메뉴와 계정 패널이 함께 닫힌다. `value` 는 `options` 의 `value` 와 맞추면 되고, 우측에는 그 항목의 `label` 이 적힌다. **하위 메뉴를 손으로 만들지 않는다** — 뜨는 자리·닫히는 시점이 어긋난다.
1238
+ - **언어를 고르는 화면이 따로 있으면 `options` 를 주지 않는다.** 그러면 행을 누르는 것 자체가 액션이라 `onClick` 이 돌고 계정 패널이 닫힌다. 둘을 같이 주면 화면으로 가지 않고 메뉴만 펴진다.
1239
+ - 하위 메뉴가 열려 있는 동안 계정 패널은 닫히지 않는다 — 언어를 고르는 일이 아직 끝나지 않았기 때문이다.
1240
+ - **행 아이콘은 밖에서 바꾸지 않는다.** 무엇을 하는 행인지가 곧 그 행의 정체다. 서비스 문구가 다르면 `label` 만 바꾼다.
1241
+ - **폭을 늘리지 않는다.** `--cmp-gnb-system-accountListBox-width` 가 정하는 고정 폭이다 — GNB 폭에 맞춰 뜨는 패널이라 늘리면 행 여백이 어긋난다.
1242
+ - **화면 위쪽에 계정 행을 직접 세우지 않는다.** system 밖에 세운 계정 행의 패널은 **늘 행 위로** 펼쳐진다 — 방향을 바꾸는 prop 이 없으므로 위가 화면 끝인 자리(앱 상단바·헤더)에 두면 잘린다. 앱 상단바의 계정은 손으로 만들지 말고 `SGnb` 의 `system` 슬롯에 맡긴다(`header="full"` 이면 조각들이 상단바 오른쪽 끝으로 올라가고, 그 자리에서는 패널이 아래로 펼쳐진다 — §4-1).
1243
+ - **뜨는 방향은 prop 이 아니다 — 놓인 자리가 정한다.** 판이면 계정 행이 맨 아래에 있으므로 **위로 · 행 왼쪽 끝에 맞춰**, 전폭 상단바면 **아래로 · 오른쪽 끝에 맞춰** 펼친다(접힌 레일은 판과 같다). 방향을 바꾸는 prop 은 없으니 찾지 않는다 — 같은 계정 행이 앱마다 다른 방향으로 뜨면 안 되기 때문이다.
1244
+ - **`header="fix"` 에서 GNB 를 접거나 펴면 열려 있던 패널은 닫힌다.** 계정 행이 메뉴 컬럼 바닥에서 접힘 레일 바닥으로(또는 반대로) 옮겨 서기 때문이다 — 닫히는 것을 앱이 막을 수 없고, 막을 이유도 없다(누른 그 행이 화면에서 사라진다). 전폭 상단바(`header="full"`)는 접어도 계정 행이 제자리라 열린 채 남는다. **그 닫힘을 알아야 하면 `account.onOpenChange` 를 준다** — 열림을 따라 그리는 화면이 앱에 있을 때만 필요하다.
1245
+ - **로그아웃을 여기 말고 다른 곳에 또 두지 않는다.** 계정에서 나가는 길이 화면마다 다르면 사용자가 매번 찾는다.
1246
+
1182
1247
  ### 3-6. 영역 나누기 — SDivider vs SSplitter
1183
1248
 
1184
1249
  | 상황 | 사용 |
@@ -1463,8 +1528,10 @@ const [selectedId, setSelectedId] = useState<string>();
1463
1528
 
1464
1529
  - **기본은 `SKeyValueTable` 이다** (§4-2). 조건이 대여섯 개 이하로 고정이면 표로 펼쳐 두는 편이 한눈에 읽힌다.
1465
1530
  - `SChipFilter` 는 조건을 **칩 한 줄**로 접고, "필터 추가" 로 필요한 것만 꺼내 쓰게 한다. 칩을 누르면 편집 팝오버가 열리고, 날짜는 프리셋(오늘·지난 7일·사용자 지정)으로 고른다. 조건 후보가 많은 목록 화면에서 필터가 화면을 세로로 잡아먹는 것을 막는 용도다.
1466
- - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다.
1531
+ - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다. **단 마운트 후 첫 호출은 값이 처음 그대로여도 나간다** — 마운트 시 자동 조회하지 않는 화면(다이얼로그 등)에서 조건을 하나도 넣지 않고 누른 첫 "검색" 이 막히면 안 되기 때문이다. 그래서 **마운트 시 조회할지 말지는 앱이 정한다** — `SChipFilter` 는 마운트만으로 `onSearch` 를 부르지 않는다.
1532
+ - **keyword 칩은 입력창에 남은 텍스트까지 조회에 넣는다.** Enter 로 담지 않고 팝오버를 닫아도(="검색" 클릭·바깥 클릭·Esc·다른 칩으로 전환) 그 텍스트를 키워드로 확정한 뒤 조회한다. 앱이 따로 확정시킬 일은 없다.
1467
1533
  - **노출할 칩을 앱이 계산하지 않는다.** 바에 놓이는 것은 `fixed`·`required` 필드와 **`value` 에 값이 들어 있는 필드**다. 노출 목록을 밖에서 넘기는 prop 은 없다. 쿼리스트링·서버 상태에서 조건을 복원하는 목록 화면도 **`value` 만 넘기면 칩이 함께 살아나고**, 그 조건을 빼면 칩도 함께 빠진다 — 칩과 조회 조건이 어긋날 자리가 없다. "필터 추가" 로 꺼낸 필터는 값을 넣기 전에도 자리를 지키지만, "검색 초기화" 를 누르거나 화면을 다시 그리면 사라진다. 걸린 조건이 없으니 문제되지 않는다.
1534
+ - **`fixed`·`required` 이면서 후보가 하나뿐인 `select`·`select-multi` 는 자동으로 선택된다.** 고를 여지가 없는 목록이라 컴포넌트가 그 값을 채우고, 사용자가 고르지 않아도 `onValueChange` 가 그 값과 함께 호출된다 — 마운트 직후부터 그 조건이 값 맵에 들어 있다고 보고 조회를 짠다. 옵션이 API 응답이라 늦게 도착해도 도착한 시점에 채워지므로, 앱이 따로 채워 넣을 필요가 없다. 사용자가 그 값을 지우면 다시 채우지 않는다. **뺄 수 있는 필터는 후보가 하나여도 채우지 않는다** — 처음에 바에 없는 필터라, 채우면 보이지도 지울 수도 없는 조건이 검색에 걸린다(`defaultValue` 를 `fixed`·`required` 로 제한하는 것과 같은 이유). 후보가 하나뿐인 조건을 반드시 걸어야 하면 그 필드를 `fixed` 나 `required` 로 준다.
1468
1535
  - **`fields` 는 항상 그룹 배열이다.** 묶을 것이 없어도 `[{ fields: [...] }]` 로 한 겹 감싼다. 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 그 필드들만 별도 그룹으로 떼어 `rule` 을 준다 — 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다. 그룹 앞 구분선은 `divider` 로 켠다. 검증 단위와 구분선은 별개라, 묶어서 검증만 하고 싶으면 `divider` 를 주지 않는다.
1469
1536
 
1470
1537
  #### 3-7-12. 이미지 — SImage
@@ -1581,9 +1648,10 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1581
1648
  | `header` | 상단바 배치 | 로고 폭 | `topContent` |
1582
1649
  | --- | --- | --- | --- |
1583
1650
  | `"fix"` (기본) | `[런처 · 로고 … 폴드]` — 상단바가 GNB 컬럼 안에 있고 폴드가 컬럼 오른쪽 끝 | 내용 폭 | **렌더되지 않는다** (놓을 자리가 없다) |
1584
- | `"full"` | `[런처 · 폴드 · 로고 · topContent]` — 상단바가 화면 전폭 | **140px 고정** (런처 없으면 172px) | 로고 오른쪽 남는 폭 전체 |
1651
+ | `"full"` | `[런처 · 폴드 · 로고 · topContent … system]` — 상단바가 화면 전폭이고 `system` 은 오른쪽 끝 | **140px 고정** (런처 없으면 172px) | 로고 오른쪽 남는 폭 전체 |
1585
1652
 
1586
- - `topContent` 는 상단바 로고 오른쪽 슬롯이다. 전역 검색·계정 메뉴·알림처럼 **모든 페이지에 공통인 것만** 넣는다. 페이지별 액션은 여기가 아니라 §4-2 의 `STableBar` 로 간다.
1653
+ - `topContent` 는 상단바 로고 오른쪽 슬롯이다. 전역 검색처럼 **모든 페이지에 공통인 것만** 넣는다. 페이지별 액션은 여기가 아니라 §4-2 의 `STableBar` 로 간다.
1654
+ - **계정·알림·설정·도메인·서비스 전환은 `topContent` 에 손으로 만들지 않는다.** `header="full"` 이면 `system` 슬롯의 조각들이 상단바 오른쪽 끝으로 올라오므로, 그것들은 전부 `SGnbSystem` 이 그린다 (바로 아래 절).
1587
1655
  - 슬롯이 남는 폭을 통째로 받으므로 **정렬은 안에서 직접 잡는다** (좌측 정렬 + 우측은 `ml-auto`).
1588
1656
  - `header="full"` 에서 로고 자리는 140px 로 고정된다 — 로고 내용이 바뀌어도 `topContent` 시작점이 흔들리지 않게 하기 위함이다. 로고가 그보다 넓으면 잘리므로 이 폭에 맞춰 준비한다.
1589
1657
  - `onLauncherClick` 을 주지 않으면 런처가 렌더되지 않고, 그 자리(버튼 + 간격)를 로고 슬롯이 이어받아 172px 가 된다. `topContent` 시작점은 런처 유무와 관계없이 같은 자리다.
@@ -1594,12 +1662,14 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1594
1662
  items={MENU} value={current} onValueChange={navigate}
1595
1663
  logo={<Logo />}
1596
1664
  topContent={
1597
- /* 남는 폭 전체를 받는다 — 왼쪽은 그대로, 오른쪽 끝은 ml-auto */
1665
+ /* 남는 폭 전체를 받는다 — 왼쪽은 그대로, 오른쪽 끝은 ml-auto.
1666
+ 계정·알림은 여기 만들지 않는다 — system 슬롯이 상단바 오른쪽 끝에 그린다 */
1598
1667
  <div className="flex w-full items-center gap-sd-8">
1599
1668
  <SSearchInput value={keyword} onValueChange={setKeyword} onSearch={runSearch} placeholder="통합 검색" />
1600
- <SButton size="sm" color="neutral" outline label="내 계정" className="ml-auto" onClick={openAccount} />
1669
+ <SButton size="sm" color="neutral" outline label="도움말" className="ml-auto" onClick={openHelp} />
1601
1670
  </div>
1602
1671
  }
1672
+ system={<SGnbSystem alert={{ count: unreadCount, onClick: openAlerts }} account={{ label: userName, listBox: accountPanel }} />}
1603
1673
  />
1604
1674
  <SPage background="frame">{children}</SPage>
1605
1675
  </SLayout>
@@ -1621,36 +1691,56 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1621
1691
 
1622
1692
  슬롯 안쪽 여백은 **슬롯 내용이 직접 갖는다** — 컴포넌트는 자리만 잡는다(폴드 슬롯만 좁은 폭에 맞춰 가운데 정렬한다). 메뉴 폭은 `resizable` 로 바뀔 수 있으므로 슬롯 내용은 고정 폭 대신 `w-full` 로 따라가게 둔다.
1623
1693
 
1624
- #### GNB 아래의 system 패널
1694
+ #### 서비스·도메인·알림·설정·계정 묶음 (system 슬롯)
1695
+
1696
+ **서비스 전환·도메인·알림·설정·계정처럼 "지금 어떤 자격으로 보고 있는가" 를 다루는 묶음은 `SGnbSystem` 을 `SGnb` 의 `system` 슬롯에 넣는다.** 이 조각들을 `menuFooter` 나 `topContent` 에 손으로 조립하지 않는다 — 배경·경계·구분선·행 간격이 전부 토큰으로 정해져 있어 직접 만들면 어긋난다.
1625
1697
 
1626
- **서비스 전환·도메인·알림·설정·계정처럼 "지금 어떤 자격으로 보고 있는가" 를 다루는 묶음은 `SGnbSystem` `SGnb` `system` 슬롯에 넣는다.** 조각들을 `menuFooter` 에 손으로 조립하지 않는다 — 패널의 배경·위쪽 경계·구분선·행 간격이 전부 토큰으로 정해져 있어 직접 만들면 어긋난다.
1698
+ **놓이는 자리는 `header` 정한다. 앱이 넘기는 것은 양쪽 모두 똑같다.**
1627
1699
 
1628
- `system` 은 다른 슬롯과 달리 **펼침·접힘 양쪽을 혼자 맡는다** — 펼치면 메뉴 컬럼 바닥에, 접히면(`header="fix"`) `foldedFooter` 아래 레일 바닥에 여백 없이 붙는다. 그래서 `menuFooter`/`foldedFooter` 로 나눠 줄 필요가 없다.
1700
+ | `header` | system 서는 자리 | 형태 |
1701
+ | --- | --- | --- |
1702
+ | `"fix"` (기본) | GNB 맨 아래 (접히면 `foldedFooter` 아래 레일 바닥) | 세로 판 — 위에서부터 `actions` · 도메인 · `[알림 \| 설정]` · 계정 |
1703
+ | `"full"` | 전폭 상단바 **오른쪽 끝** | 가로 한 줄 — `actions` \| 알림 \| 설정 \| 도메인·계정 |
1704
+
1705
+ `system` 은 다른 슬롯과 달리 **펼침·접힘 양쪽을 혼자 맡는다** — `header="fix"` 면 펼쳤을 때 메뉴 컬럼 바닥에, 접혔을 때 레일 바닥에 여백 없이 붙는다. 그래서 `menuFooter`/`foldedFooter` 로 나눠 줄 필요가 없다.
1629
1706
 
1630
1707
  ```tsx
1631
1708
  <SGnb
1632
1709
  items={MENU} value={current} onValueChange={navigate} logo={<Logo />}
1633
1710
  system={
1634
1711
  <SGnbSystem
1635
- /* 상단에 쌓는 행 — SSystemActionButton 의 props 배열이다 (§3-5). color 는 넘기지 않는다 */
1712
+ /* 상단에 쌓는 행 — SSystemActionButton 의 props 배열이다 (§3-5).
1713
+ color 는 넘기지 않고, 계정 행은 여기가 아니라 아래 account 로 준다 */
1636
1714
  actions={[{ type: 'select', label: '서비스 전환', option: { icon: 'robot' }, onClick: openServices }]}
1637
1715
  domain={domainName}
1638
1716
  alert={{ count: unreadCount, onClick: openAlerts }}
1639
1717
  setting={{ onClick: openSettings }}
1640
- account={{ label: userName, onClick: openAccount }}
1718
+ /* 계정 행. listBox 주면 눌렀을 때 계정 패널이 뜬다 (§3-5-8) */
1719
+ account={{
1720
+ label: userName,
1721
+ listBox: {
1722
+ email: user.email, name: user.name, authority: user.authority,
1723
+ accountSetting: { onClick: openAccountSetting },
1724
+ language: { value: currentLanguage, onClick: openLanguage },
1725
+ logout: { onClick: signOut },
1726
+ },
1727
+ }}
1641
1728
  />
1642
1729
  }
1643
1730
  />
1644
1731
  ```
1645
1732
 
1646
- - **`color` 와 `folded` 를 주지 않는다.** `system` 슬롯에 있으면 GNB 가 자기 색과 접힘 형태를 내려준다. prop GNB 밖에서 이 패널을 단독으로 쓸 때만 준다.
1733
+ - **`color`·`folded` 를 주지 않는다.** `system` 슬롯에 있으면 GNB 가 자기 색과 형태를 내려준다. 둘은 GNB 밖에서 단독으로 쓸 때만 준다.
1734
+ - **놓이는 자리는 prop 이 아니다.** 세로 판이냐 상단바 한 줄이냐는 GNB 의 `header` 가 정한다(fix → 판, full → 상단바). 고르는 prop 은 없으니 찾지 않는다 — 자리와 어긋난 형태를 세울 이유가 있는 화면이 없다.
1647
1735
  - **각 조각은 해당 prop 을 줄 때만 나타난다 — 어떤 조합이어도 그것만으로 판이 성립한다.** 계정만, 알림만, 설정만, 알림+계정 … 서비스가 쓰는 것만 넘긴다. 조각을 빼려고 빈 문자열이나 빈 객체를 넣지 않는다(`setting={{}}` 은 "설정 버튼을 쓴다" 는 뜻이다).
1648
1736
  - 구분선은 **위아래 양쪽에 내용이 있을 때만** 그어진다 — `actions` 만 넘겨도 바닥에 뜻 없는 선이 남지 않는다.
1649
1737
  - 알림·설정은 **혼자 서면 폭을 다 먹고**, 둘이 나란히 서면 알림이 남는 폭을·설정이 제 폭을 갖는다. 어느 쪽이 빠져도 왼쪽 끝은 위아래 행과 맞으므로 앱이 정렬을 맞출 일이 없다.
1650
1738
  - **켜진 조각이 하나도 없으면 아무것도 렌더되지 않는다.** 조건부로 조각이 다 빠지는 화면에서도 GNB 바닥에 빈 판이 남지 않는다.
1651
- - **접히면 알림·계정 아이콘만 남는다** — `actions`·`domain`·`setting` 은 48px 폭에 놓을 자리가 없어 렌더되지 않고, 알림 개수는 점 배지가 된다. **알림도 계정도 안 쓰는 조합이면 접힌 패널은 아예 렌더되지 않는다** — 접힌 상태에서도 반드시 눌러야 하는 것이 그 둘 밖에 있으면 `foldedTop`/`foldedFooter` 로 따로 준다.
1739
+ - **`header="fix"` 를 접으면 알림·계정 아이콘만 남는다** — `actions`·`domain`·`setting` 은 48px 폭에 놓을 자리가 없어 렌더되지 않고, 알림 개수는 점 배지가 된다. **알림도 계정도 안 쓰는 조합이면 접힌 패널은 아예 렌더되지 않는다** — 접힌 상태에서도 반드시 눌러야 하는 것이 그 둘 밖에 있으면 `foldedTop`/`foldedFooter` 로 따로 준다.
1740
+ - **`header="full"` 은 접어도 상단바가 남으므로 조각들도 그대로 남는다.** 대신 가로로 자리가 넉넉하지 않아 알림·설정이 라벨을 벗고 아이콘(알림은 개수까지)만 남는다 — 이건 컴포넌트가 알아서 하므로 앱이 라벨을 지우지 않는다.
1741
+ - **`account` 를 쓰면 `account.listBox` 가 필수다** (§3-5-8) — 계정 행을 누르면 이메일·이름·권한과 계정 설정·언어 변경·로그아웃이 예외 없이 뜬다. 뜨는 방향은 판이 놓인 자리가 정하므로 앱이 넘기지 않는다 — 판이면 계정 행 위로, 상단바면 아래로 펼치고, 접힌 레일도 판과 같다. `account.onClick` 은 패널을 여는 것 말고 따로 할 일(로깅 등)이 있을 때만 준다.
1652
1742
  - **알림 개수는 `alert.count` 하나로 표현한다.** 1 이상이면 벨이 울리는 아이콘과 강조색으로 바뀐다 — 색은 GNB 색이 정하므로 앱이 직접 칠하지 않는다.
1653
- - **`useRail` 에서 하위 메뉴가 없는 레일 아이템이 활성이면 메뉴 컬럼째 사라지므로 이 패널도 함께 빠진다.** 항상 보여야 하는 계정·알림이라면 그런 레일 리프를 두지 않는다.
1743
+ - **`header="fix"` + `useRail` 에서 하위 메뉴가 없는 레일 아이템이 활성이면 메뉴 컬럼째 사라지므로 이 패널도 함께 빠진다.** 항상 보여야 하는 계정·알림이라면 그런 레일 리프를 두지 않는다 — 또는 `header="full"` 로 두면 상단바에 남는다(그런 레일 리프가 있으면 GNB 가 `header` 를 `full` 로 강제한다).
1654
1744
 
1655
1745
  ### 4-2. 목록 페이지 (필터 + 테이블)
1656
1746
 
@@ -0,0 +1,31 @@
1
+ import { type RefObject } from 'react';
2
+ import { type SAccountListBoxContent } from './SAccountListBox';
3
+ /**
4
+ * 계정 행이 놓인 자리. 리스트박스가 어느 방향으로 펼쳐질지를 **이것만** 정한다.
5
+ * - `panel` — GNB 바닥의 세로 판(접힌 레일 포함). 계정 행이 맨 아래라 위로 편다.
6
+ * - `top` — 전폭 상단바 오른쪽 끝. 줄이 화면 맨 위라 아래로 편다.
7
+ */
8
+ type AccountListBoxAnchor = 'panel' | 'top';
9
+ export declare const AccountListBoxAnchorProvider: import("react").Provider<AccountListBoxAnchor>;
10
+ /**
11
+ * 계정 리스트박스를 앵커에 붙여 띄우는 내부 래퍼.
12
+ *
13
+ * 계정 행은 GNB 판(펼침)·레일(접힘)·상단바 세 자리에 서고, 세 자리 모두 "누르면 같은 패널이
14
+ * 뜬다". 그 여는 방식(위치 · SPortal 설정 · 행을 고르면 닫기)을 한 곳에 모아 두어야 자리마다
15
+ * 동작이 갈리지 않는다. 공개 API 가 아니므로 index.ts 로 내보내지 않는다 —
16
+ * 앱은 `SSystemActionButton` 의 `option` 이나 `SGnbSystem` 의 `account.listBox` 로 닿는다.
17
+ */
18
+ interface AccountListBoxPortalOptions {
19
+ /** 표시 여부 */
20
+ open: boolean;
21
+ /** 표시 상태 변경 */
22
+ onOpenChange: (open: boolean) => void;
23
+ /** 붙을 트리거 엘리먼트 */
24
+ anchorRef: RefObject<HTMLElement | null>;
25
+ /** 접근성 이름 */
26
+ ariaLabel?: string;
27
+ /** 패널에 담기는 내용 */
28
+ content: SAccountListBoxContent;
29
+ }
30
+ export declare function AccountListBoxPortal({ open, onOpenChange, anchorRef, ariaLabel, content, }: AccountListBoxPortalOptions): import("react").JSX.Element;
31
+ export {};
@@ -0,0 +1,86 @@
1
+ # SAccountListBox
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SAccountListBox
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `email?` | `string` | — | 로그인한 계정의 이메일 |
12
+ | `name?` | `string` | — | 로그인한 계정의 이름 |
13
+ | `authority?` | `string` | — | 권한. 이름 오른쪽 끝에 붙는다. 없으면 이름만 남는다. (`role` 이 아닌 이유 — 그 이름은 DOM 의 ARIA 속성이라 소비 앱의 a11y lint 가 값을 ARIA 롤로 읽는다) |
14
+ | `accountSetting?` | `SAccountListBoxItem` | — | 계정 설정 행. 주지 않으면 행이 빠진다 |
15
+ | `language?` | `SAccountListBoxLanguage` | — | 언어 변경 행. 주지 않으면 행이 빠진다 |
16
+ | `logout?` | `SAccountListBoxItem` | — | 로그아웃 행. 이 행은 늘 있고, 라벨과 클릭만 받는다 |
17
+
18
+ ## Types
19
+
20
+ ### SAccountListBoxItem
21
+
22
+ ```ts
23
+ /** 리스트박스의 한 행 — 라벨은 기본값이 있고 클릭만 앱이 준다 */
24
+ export interface SAccountListBoxItem {
25
+ /** 라벨 텍스트. 행마다 기본값이 있다 */
26
+ label?: string;
27
+ /** 클릭 */
28
+ onClick?: () => void;
29
+ }
30
+ ```
31
+
32
+ ### SAccountListBoxLanguage
33
+
34
+ ```ts
35
+ /** 언어 변경 행 — 지금 쓰는 언어가 우측에 붙고, 고를 것이 있으면 눌러서 편다 */
36
+ export interface SAccountListBoxLanguage extends SAccountListBoxItem {
37
+ /**
38
+ * 지금 선택돼 있는 언어. 우측에 브랜드색으로 붙는다.
39
+ * `options` 를 함께 주면 그 목록에서 같은 `value` 를 찾아 `label` 로 적는다.
40
+ */
41
+ value?: string;
42
+ /**
43
+ * 고를 수 있는 언어. 주면 행을 눌렀을 때 **하위 메뉴가 우측 아래로 펼쳐지고**,
44
+ * 그동안 계정 패널은 닫히지 않는다. 주지 않으면 행을 누르는 것 자체가 액션이라
45
+ * `onClick` 만 돌고 패널이 닫힌다 — 언어 화면을 따로 두는 앱이 그렇다.
46
+ */
47
+ options?: SAccountListBoxLanguageOption[];
48
+ /** 하위 메뉴에서 언어를 고름. 고르면 하위 메뉴와 계정 패널이 함께 닫힌다 */
49
+ onChange?: (value: string) => void;
50
+ }
51
+ ```
52
+
53
+ ### SAccountListBoxLanguageOption
54
+
55
+ ```ts
56
+ /** 언어 변경 하위 메뉴의 한 칸 */
57
+ export interface SAccountListBoxLanguageOption {
58
+ /** 값. `SAccountListBoxLanguage.value` 와 맞춰 지금 언어를 가린다 */
59
+ value: string;
60
+ /** 화면에 적히는 언어명 */
61
+ label: string;
62
+ }
63
+ ```
64
+
65
+ ## Dependencies
66
+
67
+ ### Used by
68
+
69
+ - [SGnbSystem](../SGnbSystem)
70
+ - [SSystemActionButton](../SSystemActionButton)
71
+
72
+ ### Depends on
73
+
74
+ - [SIcon](../SIcon)
75
+ - [SPortal](../SPortal)
76
+
77
+ ### Graph
78
+
79
+ ```mermaid
80
+ graph TD;
81
+ SAccountListBox --> SIcon
82
+ SAccountListBox --> SPortal
83
+ SGnbSystem --> SAccountListBox
84
+ SSystemActionButton --> SAccountListBox
85
+ style SAccountListBox fill:#f9f,stroke:#333,stroke-width:4px
86
+ ```
@@ -0,0 +1,79 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ /** 리스트박스의 한 행 — 라벨은 기본값이 있고 클릭만 앱이 준다 */
3
+ export interface SAccountListBoxItem {
4
+ /** 라벨 텍스트. 행마다 기본값이 있다 */
5
+ label?: string;
6
+ /** 클릭 */
7
+ onClick?: () => void;
8
+ }
9
+ /** 언어 변경 하위 메뉴의 한 칸 */
10
+ export interface SAccountListBoxLanguageOption {
11
+ /** 값. `SAccountListBoxLanguage.value` 와 맞춰 지금 언어를 가린다 */
12
+ value: string;
13
+ /** 화면에 적히는 언어명 */
14
+ label: string;
15
+ }
16
+ /** 언어 변경 행 — 지금 쓰는 언어가 우측에 붙고, 고를 것이 있으면 눌러서 편다 */
17
+ export interface SAccountListBoxLanguage extends SAccountListBoxItem {
18
+ /**
19
+ * 지금 선택돼 있는 언어. 우측에 브랜드색으로 붙는다.
20
+ * `options` 를 함께 주면 그 목록에서 같은 `value` 를 찾아 `label` 로 적는다.
21
+ */
22
+ value?: string;
23
+ /**
24
+ * 고를 수 있는 언어. 주면 행을 눌렀을 때 **하위 메뉴가 우측 아래로 펼쳐지고**,
25
+ * 그동안 계정 패널은 닫히지 않는다. 주지 않으면 행을 누르는 것 자체가 액션이라
26
+ * `onClick` 만 돌고 패널이 닫힌다 — 언어 화면을 따로 두는 앱이 그렇다.
27
+ */
28
+ options?: SAccountListBoxLanguageOption[];
29
+ /** 하위 메뉴에서 언어를 고름. 고르면 하위 메뉴와 계정 패널이 함께 닫힌다 */
30
+ onChange?: (value: string) => void;
31
+ }
32
+ export interface SAccountListBoxProps extends HTMLAttributes<HTMLDivElement> {
33
+ /** 로그인한 계정의 이메일 */
34
+ email?: string;
35
+ /** 로그인한 계정의 이름 */
36
+ name?: string;
37
+ /**
38
+ * 권한. 이름 오른쪽 끝에 붙는다. 없으면 이름만 남는다.
39
+ * (`role` 이 아닌 이유 — 그 이름은 DOM 의 ARIA 속성이라 소비 앱의 a11y lint 가 값을 ARIA 롤로 읽는다)
40
+ */
41
+ authority?: string;
42
+ /** 계정 설정 행. 주지 않으면 행이 빠진다 */
43
+ accountSetting?: SAccountListBoxItem;
44
+ /** 언어 변경 행. 주지 않으면 행이 빠진다 */
45
+ language?: SAccountListBoxLanguage;
46
+ /** 로그아웃 행. 이 행은 늘 있고, 라벨과 클릭만 받는다 */
47
+ logout?: SAccountListBoxItem;
48
+ }
49
+ /**
50
+ * 리스트박스에 **담기는 내용**만 추린 것.
51
+ * 팝오버로 띄우는 쪽(`SSystemActionButton` 의 `option` · `SGnbSystem` 의 `account.listBox`)이
52
+ * 이 덩어리째 넘겨받으므로, 패널의 DOM 속성(className·style …)과 분리해 둔다.
53
+ */
54
+ export type SAccountListBoxContent = Pick<SAccountListBoxProps, 'email' | 'name' | 'authority' | 'accountSetting' | 'language' | 'logout'>;
55
+ /**
56
+ * SAccountListBox — GNB system 의 계정 행을 눌렀을 때 뜨는 계정 리스트박스.
57
+ *
58
+ * 판에서는 계정 행이 맨 아래에 있어 **행 위로 · 행 왼쪽 끝에 맞춰**, 전폭 상단바에서는 **행 아래로 ·
59
+ * 오른쪽 끝에 맞춰** 펼쳐진다. 이 방향은 고정이라 앱이 고르지 않는다.
60
+ *
61
+ * 위에서부터 [사용자정보(이메일 · 이름 + 권한)] · [계정 설정] · [언어 변경] · [계정 로그아웃] 이다.
62
+ * **권한(`authority`) · 계정 설정 · 언어 변경은 옵션**이고, 사용자정보와 로그아웃은 늘 있다 — 이 패널이 존재하는
63
+ * 이유가 "지금 누구로 로그인해 있는가"와 "나가기"이기 때문이다. 옵션을 빼면 그 줄이 통째로 사라지고
64
+ * 나머지가 위로 붙는다.
65
+ *
66
+ * 폭은 토큰이 정하는 고정값(240px)이라 앱이 늘리지 않는다 — 이 패널은 GNB 폭에 맞춰 뜬다.
67
+ * 행 라벨·아이콘은 무엇을 하는 행인지가 곧 정체라 아이콘을 밖에서 바꾸지 않고, 라벨만 바꿀 수 있다.
68
+ *
69
+ * **언어 변경만 하위 메뉴를 가질 수 있다.** `language.options` 를 주면 그 행이 우측 값 아래로
70
+ * 목록을 펴고, 고를 때까지 계정 패널은 열려 있는다. 주지 않으면 행을 누르는 것 자체가 액션이라
71
+ * `onClick` 만 돌고 패널이 닫힌다 — 언어를 고르는 화면을 따로 두는 앱이 그렇다.
72
+ *
73
+ * **이 컴포넌트는 패널만 그린다.** 계정 행에 붙여 띄우는 것은 `SSystemActionButton` 의
74
+ * `type="account"` 이 맡으므로(`option` 에 이 내용을 넘긴다 — 필수다), 앱이 직접 팝오버를 조립할
75
+ * 일은 없다. 계정 행은 예외 없이 이 패널의 트리거다.
76
+ *
77
+ * 토큰은 `--cmp-gnb-system-accountListBox-*` · `--cmp-gnb-system-accountButton-*`.
78
+ */
79
+ export declare const SAccountListBox: import("react").ForwardRefExoticComponent<SAccountListBoxProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,58 @@
1
+ import { type RefObject } from 'react';
2
+ import { type SPortalAlign, type SPortalPlacement } from '../SPortal';
3
+ import { type STagColor } from '../STag';
4
+ /**
5
+ * 시스템 메뉴의 한 칸.
6
+ *
7
+ * `color` 를 주면 그 칸은 **상태 칸**이 되어 점과 글자가 같은 색으로 물든다(시안) — 상태는 색 자체가
8
+ * 뜻이라, 고르는 자리에서도 그 색을 보고 고른다. 주지 않으면 글자만 있는 칸이고, 지금 값만
9
+ * 브랜드색으로 떠오른다.
10
+ */
11
+ export interface SystemMenuOption {
12
+ /** 값. 지금 값과 맞춰 어느 칸이 선택돼 있는지를 가린다 */
13
+ value: string;
14
+ /** 화면에 적히는 이름 */
15
+ label: string;
16
+ /** 상태 칸의 색. 주면 점 + 같은 색 글자로 그린다 */
17
+ color?: STagColor;
18
+ }
19
+ interface SystemMenuPortalProps {
20
+ /** 표시 여부 */
21
+ open: boolean;
22
+ /** 표시 상태 변경 */
23
+ onOpenChange: (open: boolean) => void;
24
+ /** 붙을 자리 — 값 아래로 펼 때는 **바뀌는 값**(우측 값 묶음 · 상태 태그), 옆으로 펼 때는 행 자체 */
25
+ anchorRef: RefObject<HTMLElement | null>;
26
+ /** 뜨는 방향. 기본 `'bottom'`(앵커 아래) */
27
+ placement?: SPortalPlacement;
28
+ /** 교차축 정렬. 아래로 펼 때는 값이 놓인 쪽 끝, 옆으로 펼 때는 위쪽 끝(`start`) */
29
+ align: SPortalAlign;
30
+ /**
31
+ * 메뉴를 여는 행. 이 행 위의 클릭은 행이 스스로 토글하므로 여기서 닫지 않는다 —
32
+ * 앵커는 행이 아니라 그 안의 값이라, 값 밖(라벨 쪽)을 누르면 닫혔다가 곧바로 다시 열린다.
33
+ */
34
+ triggerRef?: RefObject<HTMLElement | null>;
35
+ /** 접근성 이름 — 무엇을 고르는 메뉴인지 */
36
+ ariaLabel?: string;
37
+ /** 지금 값 */
38
+ value?: string;
39
+ /** 고를 수 있는 칸 */
40
+ options: SystemMenuOption[];
41
+ /** 칸을 고름 */
42
+ onSelect: (value: string) => void;
43
+ }
44
+ /**
45
+ * GNB system 의 값 고르기 메뉴.
46
+ *
47
+ * 계정 패널의 언어 변경 행 · `SSystemActionButton` 의 `subSelect` · `status` 가 모두 이것을 연다 —
48
+ * 세 자리 모두 "지금 값이 적혀 있고, 눌러서 다른 값으로 바꾼다" 는 같은 일이라, 뜨는 자리와 닫히는
49
+ * 시점을 한 곳에 모아 두지 않으면 자리마다 동작이 갈린다.
50
+ *
51
+ * **뜨는 자리는 부르는 쪽이 정한다.** 계정 패널 안의 행처럼 판 안에서 열리는 자리는 바꾸는 대상인
52
+ * 값 바로 아래로, 값이 놓인 쪽 끝에 맞춰 편다(패널 폭 안에 갇혀 있어 옆이 없다). 판 없이 행 하나로
53
+ * 서는 자리는 행 오른쪽으로 편다 — 행 아래는 다음 행이 이어지는 자리라 메뉴가 그 위를 덮는다.
54
+ *
55
+ * 공개 API 가 아니므로 index.ts 로 내보내지 않는다 — 앱은 각 행의 `options` 로 닿는다.
56
+ */
57
+ export declare function SystemMenuPortal({ open, onOpenChange, anchorRef, placement, align, triggerRef, ariaLabel, value, options, onSelect, }: SystemMenuPortalProps): import("react").JSX.Element;
58
+ export {};
@@ -0,0 +1,64 @@
1
+ /** 행의 성격. default = 보통 행, negative = 되돌릴 수 없는 행(로그아웃) */
2
+ export declare const ACCOUNT_LIST_BOX_ITEM_TONES: readonly ["default", "negative"];
3
+ export type SAccountListBoxItemTone = (typeof ACCOUNT_LIST_BOX_ITEM_TONES)[number];
4
+ /** 행마다 정해져 있는 아이콘 — 무엇을 하는 행인지가 아이콘으로 읽히므로 밖에서 바꾸지 않는다 */
5
+ export declare const ACCOUNT_LIST_BOX_ICONS: {
6
+ readonly accountSetting: "account";
7
+ readonly language: "global";
8
+ readonly logout: "logout";
9
+ };
10
+ /** 행 라벨 기본값 — 시안 문구. 앱이 바꿀 수 있다 */
11
+ export declare const ACCOUNT_LIST_BOX_LABELS: {
12
+ readonly accountSetting: "계정 설정";
13
+ readonly language: "언어 변경";
14
+ readonly logout: "계정 로그아웃";
15
+ };
16
+ /** 행 성격별 색 */
17
+ export declare const ACCOUNT_LIST_BOX_TONE_CONFIG: Record<SAccountListBoxItemTone, {
18
+ bg: string;
19
+ bgHover: string;
20
+ text: string;
21
+ icon: string;
22
+ }>;
23
+ /** 치수·색 — 전부 토큰 참조 */
24
+ export declare const ACCOUNT_LIST_BOX_LAYOUT: {
25
+ /** 패널 고정 폭 */
26
+ readonly width: string;
27
+ /** 패널 배경 */
28
+ readonly bg: string;
29
+ /** 패널 라운드 */
30
+ readonly radius: string;
31
+ /** 사용자정보 영역 배경 */
32
+ readonly userdataBg: string;
33
+ /** 사용자정보 영역 좌우 여백 */
34
+ readonly userdataPaddingX: string;
35
+ /** 사용자정보 영역 상하 여백 */
36
+ readonly userdataPaddingY: string;
37
+ /** 이메일 글자색 */
38
+ readonly emailColor: string;
39
+ /** 이름 글자색 */
40
+ readonly nameColor: string;
41
+ /** 권한 글자색 */
42
+ readonly roleColor: string;
43
+ /** 행 묶음의 상하 여백 */
44
+ readonly listPaddingY: string;
45
+ /** 행 좌우 여백 */
46
+ readonly itemPaddingX: string;
47
+ /** 행 상하 여백 */
48
+ readonly itemPaddingY: string;
49
+ /** 행 아이콘 ↔ 라벨 간격 */
50
+ readonly itemGap: string;
51
+ /** 행 아이콘 크기 */
52
+ readonly itemIcon: string;
53
+ /** 우측 값 ↔ 화살표 간격 */
54
+ readonly selectDataGap: string;
55
+ /** 우측 화살표 크기 */
56
+ readonly selectDataIcon: string;
57
+ /** 우측 값 글자색 */
58
+ readonly selectDataText: string;
59
+ /** 시스템 메뉴에서 지금 값인 칸의 글자색 — 계정 행이 아니라 목록 항목이라
60
+ * listBoxItem 의 선택 색을 그대로 따른다(다른 목록의 선택 표시와 같은 색) */
61
+ readonly menuSelectedText: string;
62
+ /** 우측 화살표색 */
63
+ readonly selectDataIconColor: string;
64
+ };
@@ -0,0 +1,2 @@
1
+ export { SAccountListBox, type SAccountListBoxProps, type SAccountListBoxContent, type SAccountListBoxItem, type SAccountListBoxLanguage, type SAccountListBoxLanguageOption, } from './SAccountListBox';
2
+ export { ACCOUNT_LIST_BOX_ICONS, ACCOUNT_LIST_BOX_ITEM_TONES, ACCOUNT_LIST_BOX_LABELS, ACCOUNT_LIST_BOX_LAYOUT, ACCOUNT_LIST_BOX_TONE_CONFIG, type SAccountListBoxItemTone, } from './accountListBox.config';
@@ -22,7 +22,7 @@
22
22
  |-------|------|-------------|
23
23
  | `onValueChange` | `(value: SChipFilterValueMap) => void` | 전체 값 변경 — 편집 중인 값이 바뀔 때마다(선택할 때마다) 호출된다. 실제 검색 실행은 onSearch를 쓴다 |
24
24
  | `onFilterChange` | `(detail: SChipFilterChangeDetail) => void` | 개별 필터 값 변경 |
25
- | `onSearch` | `(value: SChipFilterValueMap) => void` | 실제 검색을 실행할 시점 — 편집 팝오버의 "검색" 버튼을 누르거나 팝오버가 닫힐 때(바깥 클릭·Esc·다른 칩으로 전환 포함) 그 시점의 전체 값 맵과 함께 호출된다. 팝오버가 없는 필드(인라인 date 프리셋·custom, clearable ×, 검색 초기화)는 값이 바뀌는 즉시 호출된다. keyword 필터에서 Enter 로 키워드를 추가할 때도 그 즉시 호출된다 — 팝오버는 열린 채라 키워드를 이어서 더 넣을 수 있고, 넣을 때마다 조회가 갱신된다. dirty 체크가 기본 적용되어 있어 — 마지막으로 실제 검색이 실행된 값 맵과 비교해 하나라도 달라진 게 없으면(예: 팝오버를 열었다 아무것도 안 바꾸고 닫는 경우) 호출되지 않는다. fields를 그룹으로 넘겼다면 rule을 만족하지 못한 그룹이 있는 동안엔 onSearch가 호출되지 않는다 — 해당 그룹의 경고 툴팁은 이 시점과 무관하게 값이 비어 있는 동안 항상 실시간으로 떠 있다(별도로 validate()를 호출할 필요 없음). required 필드가 비어 있는 동안에도 마찬가지로 호출되지 않는다 — 정상 설정(내장 기본값 또는 defaultValue)이라면 원래 비워지지 않지만, defaultValue 없는 select-multi·keyword·custom에 required만 준 오설정에서는 "검색 초기화"·clearable이 값을 null로 비울 수 있어 이때의 안전장치다 |
25
+ | `onSearch` | `(value: SChipFilterValueMap) => void` | 실제 검색을 실행할 시점 — 편집 팝오버의 "검색" 버튼을 누르거나 팝오버가 닫힐 때(바깥 클릭·Esc·다른 칩으로 전환 포함) 그 시점의 전체 값 맵과 함께 호출된다. 팝오버가 없는 필드(인라인 date 프리셋·custom, clearable ×, 검색 초기화)는 값이 바뀌는 즉시 호출된다. keyword 필터에서 Enter 로 키워드를 추가할 때도 그 즉시 호출된다 — 팝오버는 열린 채라 키워드를 이어서 더 넣을 수 있고, 넣을 때마다 조회가 갱신된다. keyword 입력창에 Enter 없이 텍스트를 남긴 채 팝오버를 닫으면 그 텍스트도 키워드로 확정한 뒤 호출된다(input: 'csv'면 콤마로 쪼개는 규칙도 그대로 적용) — 사용자가 검색어를 넣고 "검색"을 누른 조작이 입력 확정 여부 때문에 사라지지 않게 한다. dirty 체크가 기본 적용되어 있어 — 마지막으로 실제 검색이 실행된 값 맵과 비교해 하나라도 달라진 게 없으면(예: 팝오버를 열었다 아무것도 안 바꾸고 닫는 경우) 호출되지 않는다. 단 마운트 후 첫 호출은 비교할 기준이 없어 값이 처음 그대로여도 나간다 — 마운트 시 자동 조회하지 않는 화면(다이얼로그 등)에서 사용자의 첫 "검색"이 막히지 않게 하기 위함이다. fields를 그룹으로 넘겼다면 rule을 만족하지 못한 그룹이 있는 동안엔 onSearch가 호출되지 않는다 — 해당 그룹의 경고 툴팁은 이 시점과 무관하게 값이 비어 있는 동안 항상 실시간으로 떠 있다(별도로 validate()를 호출할 필요 없음). required 필드가 비어 있는 동안에도 마찬가지로 호출되지 않는다 — 정상 설정(내장 기본값 또는 defaultValue)이라면 원래 비워지지 않지만, defaultValue 없는 select-multi·keyword·custom에 required만 준 오설정에서는 "검색 초기화"·clearable이 값을 null로 비울 수 있어 이때의 안전장치다 |
26
26
  | `onReset` | `() => void` | "검색 초기화" 클릭 — 모든 필드가 기본값(또는 null)으로 리셋되고 "필터 추가"로 꺼낸 칩이 바에서 빠진 뒤 호출된다 |
27
27
  | `onAddFilter` | `(key: string) => void` | "필터 추가" 목록에서 항목을 골랐을 때 호출된다(알림). 칩 노출은 컴포넌트가 알아서 하므로 이 콜백에서 따로 해줄 일은 없다 |
28
28
 
@@ -242,7 +242,18 @@ export type SChipFilterCustomValue = Record<string, unknown>;
242
242
  ```ts
243
243
  /** 후보 목록에서 고르는 필터 — select·select-multi·keyword */
244
244
  export interface SChipFilterOptionsField extends SChipFilterFieldBase {
245
- /** 고를 수 있는 후보 목록 */
245
+ /** 고를 수 있는 후보 목록.
246
+ *
247
+ * **`fixed`·`required` 인 select·select-multi 에서 고를 수 있는 후보가 하나뿐이면 그 값이
248
+ * 자동으로 선택된다** — 사용자가 고르지 않아도 `onValueChange` 로 값이 올라온다
249
+ * (select 는 그 값, select-multi 는 그 값 하나짜리 배열). 옵션이 API 응답이라 늦게 도착해도
250
+ * 도착한 시점에 채워진다. 사용자가 그 값을 지우면 다시 채우지 않는다 —
251
+ * 다만 "검색 초기화"는 되돌려 놓는다(다른 기본값과 같은 규칙).
252
+ * 후보가 하나인데 비활성(`disabled`)이면 고를 수 없는 값이므로 채우지 않는다.
253
+ *
254
+ * 뺄 수 있는 필터(= `fixed`·`required` 가 아닌 필드)는 후보가 하나여도 채우지 않는다 —
255
+ * 처음에 바에 없는 필터라 보이지도 지울 수도 없는 조건이 검색에 걸리기 때문이다.
256
+ * 후보가 하나뿐인 필터를 반드시 걸어야 한다면 `fixed` 나 `required` 로 준다 */
246
257
  options?: SChipFilterOption[];
247
258
  }
248
259
  ```
@@ -275,7 +286,9 @@ export interface SChipFilterFieldBase {
275
286
  * 조용히 되살아난다. 처음부터 값이 정해져 있어야 하는 필터라면 그것이 `fixed`·`required` 다.
276
287
  *
277
288
  * required인데 지정하지 않으면 타입별 내장 기본값(select: 첫 번째 옵션,
278
- * datepicker-day·datepicker-range: 오늘 날짜)을 대신 쓴다. 인라인(radioButton) datepicker-range
289
+ * datepicker-day·datepicker-range: 오늘 날짜)을 대신 쓴다. `fixed`·`required`
290
+ * select·select-multi 는 후보가 하나뿐일 때 그 값이 기본값이 된다(options 참고) —
291
+ * defaultValue 를 함께 주면 그쪽이 이긴다. 인라인(radioButton) datepicker-range
279
292
  * 필드는 팝오버 없이 바에 바로 노출되어 빈 상태로 둘 수 없으므로, 이 규칙과 무관하게 언제나
280
293
  * 오늘이 채워진다 */
281
294
  defaultValue?: SChipFilterValue;