sellmate-design-system-react 9.0.0-beta.53 → 9.0.0-beta.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +126 -8
- package/dist/components/SChipFilter/README.md +34 -4
- package/dist/components/SChipFilter/SChipFilter.d.ts +25 -6
- package/dist/components/SChipFilter/index.d.ts +1 -1
- package/dist/components/SConfirmModal/README.md +2 -1
- package/dist/components/SConfirmModal/SConfirmModal.d.ts +31 -2
- package/dist/components/SGhostButton/SGhostButton.d.ts +4 -1
- package/dist/components/SGnb/README.md +40 -1
- package/dist/components/SGnb/SGnb.d.ts +36 -2
- package/dist/components/SGnb/gnb.config.d.ts +6 -0
- package/dist/components/SIcon/README.md +2 -0
- package/dist/components/SLauncherListBox/README.md +81 -0
- package/dist/components/SLauncherListBox/SLauncherListBox.d.ts +67 -0
- package/dist/components/SLauncherListBox/index.d.ts +2 -0
- package/dist/components/SLauncherListBox/launcherListBox.config.d.ts +118 -0
- package/dist/components/SLoginCard/README.md +2 -0
- package/dist/components/SLogo/README.md +54 -0
- package/dist/components/SLogo/SLogo.d.ts +47 -0
- package/dist/components/SLogo/index.d.ts +2 -0
- package/dist/components/SLogo/logo.config.d.ts +14 -0
- package/dist/components/SLogo/sellmate-wordmark.d.ts +20 -0
- package/dist/components/SModal/README.md +15 -5
- package/dist/components/SModal/SModal.d.ts +1 -1
- package/dist/components/SPortal/README.md +2 -0
- package/dist/components/STag/README.md +2 -0
- package/dist/index.cjs +616 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +606 -83
- package/dist/index.js.map +1 -1
- package/dist/lib/depth-filter.d.ts +27 -0
- package/dist/llms-full.txt +344 -19
- package/dist/llms.txt +128 -10
- package/dist/styles.css +82 -3
- package/dist/theme.css +18 -0
- package/package.json +1 -1
- 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 |
|
|
@@ -831,6 +833,32 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
831
833
|
|
|
832
834
|
**`button` 은 `SButton` 의 prop 을 그대로 받는다** — `icon`·`rightIcon`·`outline`·`disabled` 등을 함께 넘길 수 있다. 단 **`size` 는 타입에 없다**: 하단 액션 영역은 언제나 `md` 이고(§3-5-2) 예외가 없어 푸터가 고정한다. **`color` 는 주지 않는 것이 기본**이다 — 주지 않으면 주 액션 규칙대로 `primary` 가 되고, 그 버튼이 파괴적 액션일 때만 §3-5-3 에 따라 `danger` 를 준다. 카드(§3-7-8)의 `button` 도 같다.
|
|
833
835
|
|
|
836
|
+
**확인 모달은 확인 버튼 하나로 세우는 것이 기본이다 — `subButtonLabel` 을 주지 않는다.** 비워 두면 서브 버튼이 아예 서지 않는다. 답하지 않고 나가는 길은 **X 버튼이 이미 맡고**(늘 서 있다), `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다(§3-3-5).
|
|
837
|
+
|
|
838
|
+
**서브 버튼은 `onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 둔다.** 눌러도 창만 닫힌다면 X 와 똑같은 일을 하는 버튼이 하나 더 서는 것이라, 어느 것을 눌러야 하는지가 오히려 흐려진다. **되묻는 모달이라고 예외가 아니다** — "삭제할까요?" 에 답하지 않고 나가는 것은 X 로 충분하다.
|
|
839
|
+
|
|
840
|
+
그래서 서브 버튼이 설 자리는 **확인과 대등한 두 번째 선택지**뿐이고, 그때 글자는 `취소`·`닫기` 가 아니라 그 버튼이 하는 일의 이름이 된다.
|
|
841
|
+
|
|
842
|
+
```tsx
|
|
843
|
+
✅ SModal.confirm({ type: 'negative', modalTitle: '삭제할까요?', mainButtonLabel: '삭제' })
|
|
844
|
+
.onOk(() => remove())
|
|
845
|
+
✅ SModal.confirm({ type: 'negative', modalTitle: '재고가 모자란 주문이 있습니다',
|
|
846
|
+
mainButtonLabel: '자동 취소', subButtonLabel: '수동 처리' })
|
|
847
|
+
.onOk(() => cancelShortOrders()).onCancel(() => openManualFlow())
|
|
848
|
+
// 서브가 창을 닫는 것이 아니라 다른 흐름을 시작한다
|
|
849
|
+
|
|
850
|
+
❌ SModal.confirm({ …, mainButtonLabel: '삭제', subButtonLabel: '취소' }).onCancel(() => {})
|
|
851
|
+
// 눌러도 닫히기만 한다 — X 가 이미 하는 일
|
|
852
|
+
❌ SModal.confirm({ …, mainButtonLabel: '확인', subButtonLabel: '닫기' })
|
|
853
|
+
// 닫으려고 붙인 버튼 — X 와 겹친다
|
|
854
|
+
❌ SModal.confirm({ type: 'positive', modalTitle: '저장되었습니다', mainButtonLabel: '확인', subButtonLabel: '취소' })
|
|
855
|
+
// 이미 끝난 일 옆의 취소 — 무엇이 취소되는지 알 수 없다
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
습관적으로 `subButtonLabel: '취소'` 를 옵션에 넣지 않는다. **`onCancel` 에 적을 것이 없으면 서브 버튼도 없다.**
|
|
859
|
+
|
|
860
|
+
**확인 모달의 확인 버튼은 `mainButtonDisabled` 로 잠근다.** 기본값이 `false` 이므로 **아직 확정할 수 없을 때만** 준다 — 동의를 안 받았거나, `contentSlot` 의 입력이 덜 찼거나, 확인 뒤에 나갈 요청이 이미 나가는 중일 때. **왜 못 누르는지를 화면에 함께 둔다**(`bottomMessage` 나 `contentSlot`): 흐려진 버튼만 남으면 사용자가 고장으로 읽는다. **취소는 따라 잠그지 않는다** — 확정하지 못하는 상태일수록 나가는 길은 열려 있어야 해서 서브 버튼을 막는 prop 자체가 없다. 조건이 채워지면 `SModal.confirm` 이 돌려준 핸들로 `update({ mainButtonDisabled: false })` 해서 푼다.
|
|
861
|
+
|
|
834
862
|
**모달 안에서도 앱의 훅을 그냥 쓴다 — 단, 앱 루트에 `SModalOutlet` 이 있어야 한다 (§4-1).** outlet 이 있으면 명령형 모달이 앱 렌더 트리의 자식으로 그려지므로 `useQuery`·`useNavigate`·`useTheme` 같은 Context 기반 훅이 페이지에서와 똑같이 동작한다. **모달 컴포넌트를 Provider 로 다시 감싸지 않는다.** outlet 없이 띄우면 모달이 별도 React 루트로 떠서 Provider 가 하나도 닿지 않고, `No QueryClient set` 처럼 모달을 여는 순간에만 터진다.
|
|
835
863
|
|
|
836
864
|
#### 3-3-5. 닫기 경로 — `persistent` 기본값은 컴포넌트마다 다르다
|
|
@@ -841,7 +869,7 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
841
869
|
| --- | --- | --- | --- |
|
|
842
870
|
| `SActionModal` · `SLoadingModal` | `true` | X 버튼, 모달 안의 버튼 | 막힘 (흔들림) |
|
|
843
871
|
| `SDrawer` | `true` | X 버튼, footer 버튼 | 막힘 (흔들림) |
|
|
844
|
-
| `SConfirmModal` | `false` | X 버튼,
|
|
872
|
+
| `SConfirmModal` | `false` | X 버튼, 확인 버튼 | **닫힌다** |
|
|
845
873
|
| `SPopover` · `STooltip` · `SSelect` 등 floating | — | 바깥 클릭·ESC | **막지 않는다** — 이 규칙의 대상이 아니다 |
|
|
846
874
|
|
|
847
875
|
따라서 다음을 지킨다.
|
|
@@ -850,6 +878,7 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
850
878
|
- **기본값과 같은 `persistent` 를 직접 주지 않는다.** 중복이다.
|
|
851
879
|
- **`SActionModal`·`SDrawer` 에 `persistent={false}` 는 잃을 입력이 없을 때만.** 단순 알림처럼 임의로 닫혀도 아무것도 사라지지 않는 경우로 한정한다.
|
|
852
880
|
- **`SConfirmModal` 에 `persistent` 를 켜는 것은 반드시 답을 받아야 할 때만.** 되돌릴 수 없는 파괴적 작업의 확인처럼, 임의로 닫히면 안 되는 경우로 한정한다.
|
|
881
|
+
- **`SConfirmModal` 에 `persistent` 와 `mainButtonDisabled` 를 함께 켜면 나가는 길이 X 하나만 남는다.** 갇히지는 않지만(X 는 늘 서 있다) 확인이 왜 잠겼는지가 화면에 없으면 막다른 길로 읽힌다 — 잠근 이유를 `bottomMessage` 나 `contentSlot` 에 함께 둔다(§3-3-4).
|
|
853
882
|
|
|
854
883
|
작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
|
|
855
884
|
|
|
@@ -1089,6 +1118,21 @@ const columns: STableColumn[] = [
|
|
|
1089
1118
|
- **아예 대상이 아닌 행이라면 `rows` 에서 거르는 편이 낫다.** 잠긴 행이 잔뜩 섞이면 무엇을 고를 수 있는지가 오히려 안 읽힌다. 고를 수 있는 행이 한 줄도 없으면 헤더까지 잠기는데, 그 상태라면 `selectable` 을 켤 자리가 아니다.
|
|
1090
1119
|
- 잠금은 그리는 시점의 판정이라, **이미 `selected` 에 든 행이 나중에 잠겨도 DS 가 빼지 않는다** — 제어 상태를 말없이 바꾸지 않기 때문이다. 헤더의 전체 해제로는 걷힌다.
|
|
1091
1120
|
|
|
1121
|
+
#### 헤더 전체 선택이 집는 범위
|
|
1122
|
+
|
|
1123
|
+
**모드가 정한다. 앱이 보정하지 않는다.**
|
|
1124
|
+
|
|
1125
|
+
| 표 | 전체 선택이 집는 것 |
|
|
1126
|
+
| --- | --- |
|
|
1127
|
+
| 기본 | `rows` 전부 |
|
|
1128
|
+
| `pagination` · `internalPagination` | **현재 페이지** — 사용자가 고른 범위다 |
|
|
1129
|
+
| `virtualScroll` | `rows` **전부** — 그려진 창이 아니다 |
|
|
1130
|
+
|
|
1131
|
+
가상 스크롤의 창은 사용자가 고른 범위가 아니라 렌더러의 사정이라, 창을 집으면 같은 버튼이 스크롤 위치에 따라 다른 결과를 낸다. 헤더의 체크 표시도 같은 집합을 본다 — 그래서 전체 선택 뒤 스크롤해도 "전부"가 "일부"로 흔들리지 않는다.
|
|
1132
|
+
|
|
1133
|
+
- **선택 열을 직접 그리지 않는다.** 전체 선택 범위 때문에 `selectable` 을 끄고 48px 열을 손으로 만들면 Shift 구간 선택 · `isRowSelectable` 연동 · sticky 선택 열 · 열 폭/정렬 기본값을 통째로 잃는다.
|
|
1134
|
+
- **`toggleSelectAll(checked, rows)`**(ref 명령형)은 대상 행을 인자로 받으므로 모드와 무관하다 — 앱이 원하는 집합을 그대로 넘긴다.
|
|
1135
|
+
|
|
1092
1136
|
#### 표 본문의 스크롤에 닿아야 할 때
|
|
1093
1137
|
|
|
1094
1138
|
표는 자기 안에서 스크롤한다(§2-2). 그 스크롤을 **읽거나 되돌려야** 하면 두 가지가 있다.
|
|
@@ -1343,6 +1387,55 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1343
1387
|
- **`header="fix"` 에서 GNB 를 접거나 펴면 열려 있던 패널은 닫힌다.** 계정 행이 메뉴 컬럼 바닥에서 접힘 레일 바닥으로(또는 반대로) 옮겨 서기 때문이다 — 닫히는 것을 앱이 막을 수 없고, 막을 이유도 없다(누른 그 행이 화면에서 사라진다). 전폭 상단바(`header="full"`)는 접어도 계정 행이 제자리라 열린 채 남는다. **그 닫힘을 알아야 하면 `account.onOpenChange` 를 준다** — 열림을 따라 그리는 화면이 앱에 있을 때만 필요하다.
|
|
1344
1388
|
- **로그아웃을 여기 말고 다른 곳에 또 두지 않는다.** 계정에서 나가는 길이 화면마다 다르면 사용자가 매번 찾는다.
|
|
1345
1389
|
|
|
1390
|
+
#### 3-5-9. `SLauncherListBox` — 런처 버튼을 눌러 뜨는 서비스 목록
|
|
1391
|
+
|
|
1392
|
+
**"이 계정으로 갈 수 있는 다른 서비스가 무엇인가"를 한 자리에 모은 목록**이다. 한 줄이 서비스 하나이고, 위에 브랜드 로고(+ 상태 태그) 아래에 서비스 이름이 선다.
|
|
1393
|
+
|
|
1394
|
+
**직접 띄우지 않는다.** 런처 버튼에 붙여 여는 것은 `SGnb` 의 `launcher` 가 맡는다 — 목록만 넘기면 버튼 아래로 · 버튼 왼쪽 끝에 맞춰 뜨고, 서비스를 고르거나 바깥을 누르면 닫힌다. 고른 서비스는 **새 탭에서 열린다** — 이동도 앱이 만들지 않는다.
|
|
1395
|
+
|
|
1396
|
+
**앱이 넘기는 것은 서비스 키와 갈 주소 둘뿐이다.** 로고·서비스 표기·이름·상태 태그는 디자인 시스템의 카탈로그가 서비스마다 정해 두므로 앱이 만들지 않는다.
|
|
1397
|
+
|
|
1398
|
+
```tsx
|
|
1399
|
+
<SGnb
|
|
1400
|
+
items={MENU} value={current} onValueChange={navigate}
|
|
1401
|
+
logo={<Logo />}
|
|
1402
|
+
launcher={{
|
|
1403
|
+
items: [
|
|
1404
|
+
{ service: 'sellmate' }, // 갈 수 없는 줄 — 주소를 주지 않는다
|
|
1405
|
+
{ service: 'wms', href: env.WMS_URL }, // 주소는 앱의 환경(QA·실서버)에서 온다
|
|
1406
|
+
{ service: 'account', href: env.ACCOUNT_URL },
|
|
1407
|
+
{ service: 'crm', href: env.CRM_URL },
|
|
1408
|
+
],
|
|
1409
|
+
}}
|
|
1410
|
+
/>
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
- **`launcher` 를 주지 않으면 런처 버튼 자체가 렌더되지 않는다.** 옮겨 갈 서비스가 없는 앱은 이 슬롯을 비워 둔다 — 눌러도 아무것도 없는 버튼을 상단바에 남기지 않는다.
|
|
1414
|
+
- **서비스는 키(`service`)로 고른다 — 보이는 것은 아무것도 넘기지 않는다.** 쓸 수 있는 키는 `'sellmate' | 'wms' | 'account' | 'crm'`(`SLauncherService`)이고, 로고 오른쪽 표기(`WMS`·`Account`·`CRM`)·아래 줄 한글 이름·상태 태그(`NEW`·`출시예정`)·출시 여부를 카탈로그(`LAUNCHER_SERVICE_CATALOG`)가 그 키로 정한다. 앱이 넘기게 하면 같은 서비스가 앱마다 다른 이름·색·문구로 선다. **서비스가 늘거나 이름·상태가 바뀌면 디자인 시스템 릴리스로 따라온다** — 앱은 고칠 것이 없다.
|
|
1415
|
+
|
|
1416
|
+
```tsx
|
|
1417
|
+
✅ { service: 'wms', href: env.WMS_URL }
|
|
1418
|
+
❌ { service: 'WMS', name: '셀메이트 WMS', href: … } // 표기·이름을 앱이 정하지 않는다
|
|
1419
|
+
❌ { service: 'wms', tag: 'NEW', href: … } // 태그도 앱이 정하지 않는다
|
|
1420
|
+
❌ { service: <><SLogo size={12} />WMS</>, … } // 로고를 다시 넘기지 않는다
|
|
1421
|
+
```
|
|
1422
|
+
- **주소는 앱이 준다 — 디자인 시스템은 주소를 들지 않는다.** 앱마다 QA·실서버 주소가 달라 한곳에서 관리할 수 없기 때문이다. `href` 를 주면 그 줄은 링크가 되어 **새 탭에서 열린다** — 가운데 클릭·⌘클릭·주소 복사가 그대로 듣는다.
|
|
1423
|
+
- **이동을 앱이 다시 하지 않는다.** `launcher.onSelect` 는 **고른 것을 알려 줄 뿐 화면을 옮기지 않는다** — 여기서 `window.open` 이나 라우터를 부르면 탭이 두 번 열린다. 기록을 남기는 것처럼 이동 말고 할 일이 있을 때만 준다. 고르면 목록은 스스로 닫히고, 갈 수 없는 줄은 눌리지 않아 오지 않는다. (런처 **버튼**을 누른 것은 `launcher.onClick` 으로 따로 온다.)
|
|
1424
|
+
- **`href` 를 주는 것이 곧 "갈 수 있다"는 뜻이다 — 줄을 막는 prop 은 없다.** 주소를 주지 않으면 그 줄은 목록에 남되 눌리지 않고 물러난다. 지금 보고 있는 서비스든 이 앱이 계약하지 않은 서비스든 갈 곳이 없기는 같아서 한 가지로 다룬다.
|
|
1425
|
+
|
|
1426
|
+
```tsx
|
|
1427
|
+
✅ { service: 'sellmate' } // 갈 수 없다 — 주소를 주지 않는다
|
|
1428
|
+
❌ { service: 'sellmate', disabled: true } // 줄을 막는 prop 은 없다
|
|
1429
|
+
```
|
|
1430
|
+
- **갈 수 없는 서비스도 목록에서 빼지 않는다.** 그 서비스가 있다는 사실과 지금은 못 간다는 사실을 함께 알리는 것이 런처의 일이다. 지금 보고 있는 서비스를 빼면 사용자가 지금 어디에 있는지가 런처에서 사라진다.
|
|
1431
|
+
- **출시 전인 서비스는 앱이 할 일이 없다.** 카탈로그가 `출시예정` 태그와 함께 막아 두므로 주소를 줘도 열리지 않고, 열리는 날 디자인 시스템 릴리스 하나로 모든 앱에서 함께 열린다. **앱이 앞질러 열 수단은 없다.**
|
|
1432
|
+
- **목록 순서를 앱이 정하지 않는다.** 넘긴 순서가 아니라 카탈로그 순서(`LAUNCHER_SERVICES`)로 선다 — 같은 런처가 앱마다 다른 순서로 뜨면 사용자가 매번 다시 찾는다. 정렬해서 넘기려 애쓰지 않는다.
|
|
1433
|
+
- **줄에 얹으면 "여기서 나간다"는 것이 드러난다** — 배경이 옅은 파랑으로 물들고 이름이 한 단계 진해지며, 오른쪽 끝에 페이지 이동 아이콘이 떠오른다. **앱이 켜고 끄는 prop 이 없다** — 다른 서비스로 넘어가는 자리라는 신호는 화면마다 달라지면 안 된다. 갈 수 없는 줄은 얹어도 반응하지 않는다.
|
|
1434
|
+
- **폭을 늘리지 않는다.** 로고 길이에 따라 패널이 출렁이지 않도록 고정 폭이다.
|
|
1435
|
+
- **뜨는 방향은 prop 이 아니다.** 런처 버튼이 상단바 왼쪽 끝에 있으므로 **아래로 · 버튼 왼쪽 끝에 맞춰** 펼친다. 방향을 바꾸는 prop 은 없으니 찾지 않는다.
|
|
1436
|
+
- **`header="fix"` 에서 GNB 를 접으면 열려 있던 목록은 닫힌다.** 접힘 레일에는 폴드 버튼만 남아 런처 버튼이 화면에서 사라지기 때문이다. **그 닫힘을 알아야 하면 `launcher.onOpenChange` 를 준다** — 열림을 따라 그리는 화면이 앱에 있을 때만 필요하다.
|
|
1437
|
+
- **서비스 전환을 `topContent` 나 GNB 메뉴에 또 만들지 않는다.** 다른 서비스로 가는 길은 런처 하나다.
|
|
1438
|
+
|
|
1346
1439
|
### 3-6. 영역 나누기 — SDivider vs SSplitter
|
|
1347
1440
|
|
|
1348
1441
|
| 상황 | 사용 |
|
|
@@ -1711,7 +1804,8 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
1711
1804
|
- 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다. **단 마운트 후 첫 호출은 값이 처음 그대로여도 나간다** — 마운트 시 자동 조회하지 않는 화면(다이얼로그 등)에서 조건을 하나도 넣지 않고 누른 첫 "검색" 이 막히면 안 되기 때문이다. 그래서 **마운트 시 조회할지 말지는 앱이 정한다** — `SChipFilter` 는 마운트만으로 `onSearch` 를 부르지 않는다.
|
|
1712
1805
|
- **keyword 칩은 입력창에 남은 텍스트까지 조회에 넣는다.** Enter 로 담지 않고 팝오버를 닫아도(="검색" 클릭·바깥 클릭·Esc·다른 칩으로 전환) 그 텍스트를 키워드로 확정한 뒤 조회한다. 앱이 따로 확정시킬 일은 없다.
|
|
1713
1806
|
- **노출할 칩을 앱이 계산하지 않는다.** 바에 놓이는 것은 `fixed`·`required` 필드와 **`value` 에 값이 들어 있는 필드**다. 노출 목록을 밖에서 넘기는 prop 은 없다. 쿼리스트링·서버 상태에서 조건을 복원하는 목록 화면도 **`value` 만 넘기면 칩이 함께 살아나고**, 그 조건을 빼면 칩도 함께 빠진다 — 칩과 조회 조건이 어긋날 자리가 없다. "필터 추가" 로 꺼낸 필터는 값을 넣기 전에도 자리를 지키지만, "검색 초기화" 를 누르거나 화면을 다시 그리면 사라진다. 걸린 조건이 없으니 문제되지 않는다.
|
|
1714
|
-
-
|
|
1807
|
+
- **후보에 상하 관계가 있으면 `select-depth`·`select-multi-depth` 다.** 분류>세부분류처럼 묶어서 보여야 할 후보를 평평한 `select`·`select-multi` 에 늘어놓지 않는다. 계층은 `options` 의 `children` 으로 만들고, `children` 을 가진 항목은 **그 자체로 고를 수 없는 그룹 헤더**라 **값에는 언제나 리프만 담긴다** — 그룹의 `value` 가 값 맵에 들어오는 경로는 없으니 조회 파라미터를 짤 때 리프만 온다고 보면 된다. `select-depth` 의 헤더는 라벨 전용이고, `select-multi-depth` 의 헤더는 체크박스로 그 아래 리프를 한 번에 토글한다(일부만 골랐으면 부분 선택으로 뜨고 `(고른 수/전체)` 가 붙는다). 그룹 아래가 전부 비활성이면 어미 헤더도 함께 잠기므로, 앱이 `disabled` 를 어미에 다시 달아 줄 필요가 없다.
|
|
1808
|
+
- **`fixed`·`required` 이면서 후보가 하나뿐인 `select` 계열은 자동으로 선택된다.** 고를 여지가 없는 목록이라 컴포넌트가 그 값을 채우고, 사용자가 고르지 않아도 `onValueChange` 가 그 값과 함께 호출된다 — 마운트 직후부터 그 조건이 값 맵에 들어 있다고 보고 조회를 짠다. 옵션이 API 응답이라 늦게 도착해도 도착한 시점에 채워지므로, 앱이 따로 채워 넣을 필요가 없다. 사용자가 그 값을 지우면 다시 채우지 않는다. **뺄 수 있는 필터는 후보가 하나여도 채우지 않는다** — 처음에 바에 없는 필터라, 채우면 보이지도 지울 수도 없는 조건이 검색에 걸린다(`defaultValue` 를 `fixed`·`required` 로 제한하는 것과 같은 이유). 후보가 하나뿐인 조건을 반드시 걸어야 하면 그 필드를 `fixed` 나 `required` 로 준다.
|
|
1715
1809
|
- **`fields` 는 항상 그룹 배열이다.** 묶을 것이 없어도 `[{ fields: [...] }]` 로 한 겹 감싼다. 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 그 필드들만 별도 그룹으로 떼어 `rule` 을 준다 — 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다. 그룹 앞 구분선은 `divider` 로 켠다. 검증 단위와 구분선은 별개라, 묶어서 검증만 하고 싶으면 `divider` 를 주지 않는다.
|
|
1716
1810
|
|
|
1717
1811
|
#### 3-7-12. 이미지 — SImage
|
|
@@ -1731,6 +1825,29 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
1731
1825
|
❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
|
|
1732
1826
|
```
|
|
1733
1827
|
|
|
1828
|
+
#### 3-7-14. 로고 — SLogo
|
|
1829
|
+
|
|
1830
|
+
셀메이트 워드마크는 `<svg>` 를 직접 붙이지 않고 `SLogo` 를 쓴다. **`SIcon` 과 같은 손맛(`name`·`size`·`color`)이지만 두 가지가 다르다.**
|
|
1831
|
+
|
|
1832
|
+
- **`size` 는 높이다.** 로고는 가로로 긴 워드마크라 정사각으로 그리면 찌그러진다 — 폭은 비율에서 저절로 나오므로 주지 않는다.
|
|
1833
|
+
- **색을 주지 않으면 브랜드색이다.** `SIcon` 과 달리 currentColor 를 상속하지 않는다 — 로고가 주변 글자색을 따라 물들면 브랜드 색이 화면마다 달라진다. **비활성처럼 물러나야 하는 자리에서만** `color` 를 준다. 주면 워드마크 전체가 그 한 색이 된다.
|
|
1834
|
+
- **담고 있는 로고는 셀메이트 워드마크 하나다.** 서비스별 표기(WMS · Account · CRM …)는 그 오른쪽에 **앱이 자기 텍스트나 자기 로고로 붙인다** — 서비스가 늘어날 때마다 디자인 시스템을 다시 배포하지 않으려는 것이다.
|
|
1835
|
+
- **`label` 은 로고가 그 자리의 유일한 이름일 때만 준다**(로그인 화면·상단바). 옆이나 아래에 서비스 이름이 이미 적혀 있으면(런처 목록의 한 줄) 주지 않는다 — 같은 이름이 두 번 읽힌다.
|
|
1836
|
+
|
|
1837
|
+
```tsx
|
|
1838
|
+
✅ <SLogo size={20} /> {/* 상단바 로고 — 브랜드색 */}
|
|
1839
|
+
✅ <SLogo size={32} label="Sellmate" /> {/* 로고가 그 자리의 유일한 이름일 때 */}
|
|
1840
|
+
✅ <span className="inline-flex items-center gap-sd-2"> {/* 서비스별 표기는 앱이 잇는다 */}
|
|
1841
|
+
<SLogo size={12} />
|
|
1842
|
+
<span className="typo-body-xs-default text-(--sys-color-fg-success)">WMS</span>
|
|
1843
|
+
</span>
|
|
1844
|
+
✅ <SLogo size={12} color="grey_45" /> {/* 아직 못 쓰는 서비스 */}
|
|
1845
|
+
|
|
1846
|
+
❌ <svg viewBox="0 0 276 40">…</svg> {/* 워드마크를 손으로 붙이지 않는다 */}
|
|
1847
|
+
❌ <SLogo size={24} style={{ width: 120 }} /> {/* 폭은 비율이 정한다 — 찌그러진다 */}
|
|
1848
|
+
❌ <SLogo color="grey_45" /> {/* 물러날 이유가 없으면 브랜드색 그대로 둔다 */}
|
|
1849
|
+
```
|
|
1850
|
+
|
|
1734
1851
|
#### 3-7-13. 숫자 — SNumberInput 의 min·max 는 검증 경계다
|
|
1735
1852
|
|
|
1736
1853
|
`min`·`max` 는 `<input type="number">` 의 그것과 같다 — **값을 고쳐 쓰지 않는 검증 경계**다. 범위를 벗어나면 blur·제출 시 에러 상태가 서고, 사용자가 친 값은 그대로 남아 앱의 범위 가드에 도달한다.
|
|
@@ -1898,7 +2015,8 @@ import { SModalOutlet } from 'sellmate-design-system-react';
|
|
|
1898
2015
|
- **계정·알림·설정·도메인·서비스 전환은 `topContent` 에 손으로 만들지 않는다.** `header="full"` 이면 `system` 슬롯의 조각들이 상단바 오른쪽 끝으로 올라오므로, 그것들은 전부 `SGnbSystem` 이 그린다 (바로 아래 절).
|
|
1899
2016
|
- 슬롯이 남는 폭을 통째로 받으므로 **정렬은 안에서 직접 잡는다** (좌측 정렬 + 우측은 `ml-auto`).
|
|
1900
2017
|
- `header="full"` 에서 로고 자리는 140px 로 고정된다 — 로고 내용이 바뀌어도 `topContent` 시작점이 흔들리지 않게 하기 위함이다. 로고가 그보다 넓으면 잘리므로 이 폭에 맞춰 준비한다.
|
|
1901
|
-
- `
|
|
2018
|
+
- `launcher` 를 주지 않으면 런처가 렌더되지 않고, 그 자리(버튼 + 간격)를 로고 슬롯이 이어받아 172px 가 된다. `topContent` 시작점은 런처 유무와 관계없이 같은 자리다.
|
|
2019
|
+
- **런처 버튼은 서비스 목록의 트리거다.** `launcher.items` 에 옮겨 갈 서비스를 **키와 주소로** 넘기면 버튼 아래로 목록이 뜨고, 고른 서비스가 새 탭에서 열린다 — 팝오버도 이동도 손으로 만들지 않는다 (§3-5-9).
|
|
1902
2020
|
|
|
1903
2021
|
```tsx
|
|
1904
2022
|
<SLayout type="box" header="full">
|
|
@@ -75,11 +75,13 @@ export interface SChipFilterChangeDetail {
|
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
77
|
/** 필터 하나의 정의. type에 따라 쓸 수 있는 속성이 달라진다 —
|
|
78
|
-
* options는 select
|
|
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
|
|
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·
|
|
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
|
|
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
|
|
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';
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
| `contentSlot?` | `ReactNode` | — | 콘텐츠 박스 바로 아래에 통으로 들어가는 커스텀 콘텐츠 (전체 폭) |
|
|
26
26
|
| `mainButtonLabel?` | `string` | `'확인'` | 메인(확인) 버튼에 찍히는 글자 |
|
|
27
27
|
| `mainButtonOutline?` | `boolean` | — | 메인 버튼을 아웃라인(테두리)으로 세울지. 지정하지 않으면 `type` 이 정한다 — positive·negative 는 채운 버튼, default 는 아웃라인. **색은 여기서 고르지 않는다.** `type` 이 정한다(positive=primary / negative=danger / default=neutral) — 아이콘·제목이 말하는 것과 버튼 색이 어긋나면 읽는 사람이 헷갈리므로 그 조합은 타입에서 막는다. 글자는 `mainButtonLabel` 이다. `type="default"` 에 `false` 를 주면 테두리 없는 흰 버튼이 된다(neutral 은 solid·outline 이 같은 흰 면이고 테두리 유무만 다르다). 확인 버튼이 눌러야 할 것으로 안 읽히므로 권하지 않는다. |
|
|
28
|
-
| `
|
|
28
|
+
| `mainButtonDisabled?` | `boolean` | `false` | 메인 버튼을 눌리지 않게 한다. **기본값 `false`.** 모달 안에서 **아직 확정할 수 없을 때** 쓴다 — 동의 체크를 아직 안 했거나, `contentSlot` 의 입력이 덜 찼거나, 확인 뒤에 나갈 요청이 이미 나가는 중일 때. **왜 못 누르는지는 화면이 말해야 한다.** 흐려진 버튼만 남으면 사용자는 고장으로 읽는다 — 못 채운 것을 `bottomMessage` 나 `contentSlot` 안에서 함께 보인다. 서브(취소) 버튼은 따라 잠기지 않는다. 확정하지 못하는 상태일수록 **나가는 길은 열려 있어야** 하기 때문이다. 그래서 서브 버튼을 막는 prop 은 두지 않는다. |
|
|
29
|
+
| `subButtonLabel?` | `string` | — | 서브 버튼에 찍히는 글자. **주지 않는 것이 기본이다** — 비어 있으면 버튼이 아예 서지 않고 확인 하나만 남는다. **`onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 준다.** 눌러도 창만 닫힌다면 X 버튼이 이미 그 일을 하므로(X 는 늘 서 있고, `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다) 같은 일을 하는 버튼을 하나 더 세우지 않는다. 되묻는 모달이라고 해서 예외가 아니다 — "삭제할까요?" 에 답하지 않고 나가는 것은 X 로 충분하다. 그래서 서브 버튼이 설 자리는 **확인과 대등한 두 번째 선택지**다. 그때 글자는 `취소`·`닫기` 가 아니라 그 버튼이 하는 일의 이름이 된다. ```tsx ✅ { mainButtonLabel: '자동 취소', subButtonLabel: '수동 처리' } // 두 갈래를 고르게 한다 ❌ { mainButtonLabel: '삭제', subButtonLabel: '취소' } // X 가 이미 하는 일 ``` |
|
|
29
30
|
|
|
30
31
|
#### Events
|
|
31
32
|
|
|
@@ -43,7 +43,36 @@ export interface SConfirmModalProps {
|
|
|
43
43
|
* 같은 흰 면이고 테두리 유무만 다르다). 확인 버튼이 눌러야 할 것으로 안 읽히므로 권하지 않는다.
|
|
44
44
|
*/
|
|
45
45
|
mainButtonOutline?: boolean;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* 메인 버튼을 눌리지 않게 한다. **기본값 `false`.**
|
|
48
|
+
*
|
|
49
|
+
* 모달 안에서 **아직 확정할 수 없을 때** 쓴다 — 동의 체크를 아직 안 했거나, `contentSlot` 의
|
|
50
|
+
* 입력이 덜 찼거나, 확인 뒤에 나갈 요청이 이미 나가는 중일 때.
|
|
51
|
+
*
|
|
52
|
+
* **왜 못 누르는지는 화면이 말해야 한다.** 흐려진 버튼만 남으면 사용자는 고장으로 읽는다 —
|
|
53
|
+
* 못 채운 것을 `bottomMessage` 나 `contentSlot` 안에서 함께 보인다.
|
|
54
|
+
*
|
|
55
|
+
* 서브(취소) 버튼은 따라 잠기지 않는다. 확정하지 못하는 상태일수록 **나가는 길은 열려 있어야**
|
|
56
|
+
* 하기 때문이다. 그래서 서브 버튼을 막는 prop 은 두지 않는다.
|
|
57
|
+
*/
|
|
58
|
+
mainButtonDisabled?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* 서브 버튼에 찍히는 글자. **주지 않는 것이 기본이다** — 비어 있으면 버튼이 아예 서지 않고
|
|
61
|
+
* 확인 하나만 남는다.
|
|
62
|
+
*
|
|
63
|
+
* **`onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 준다.** 눌러도 창만 닫힌다면 X 버튼이
|
|
64
|
+
* 이미 그 일을 하므로(X 는 늘 서 있고, `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다)
|
|
65
|
+
* 같은 일을 하는 버튼을 하나 더 세우지 않는다. 되묻는 모달이라고 해서 예외가 아니다 —
|
|
66
|
+
* "삭제할까요?" 에 답하지 않고 나가는 것은 X 로 충분하다.
|
|
67
|
+
*
|
|
68
|
+
* 그래서 서브 버튼이 설 자리는 **확인과 대등한 두 번째 선택지**다. 그때 글자는 `취소`·`닫기`
|
|
69
|
+
* 가 아니라 그 버튼이 하는 일의 이름이 된다.
|
|
70
|
+
*
|
|
71
|
+
* ```tsx
|
|
72
|
+
* ✅ { mainButtonLabel: '자동 취소', subButtonLabel: '수동 처리' } // 두 갈래를 고르게 한다
|
|
73
|
+
* ❌ { mainButtonLabel: '삭제', subButtonLabel: '취소' } // X 가 이미 하는 일
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
47
76
|
subButtonLabel?: string;
|
|
48
77
|
/** 확인 버튼 클릭 (sdOk) */
|
|
49
78
|
onOk?: () => void;
|
|
@@ -52,4 +81,4 @@ export interface SConfirmModalProps {
|
|
|
52
81
|
onClose?: () => void;
|
|
53
82
|
}
|
|
54
83
|
/** SConfirmModal — sd-confirm-modal 포팅. 아이콘+제목+메시지+태그+확인/취소 버튼. */
|
|
55
|
-
export declare function SConfirmModal({ open, onOpenChange, persistent, type, modalTitle, titleClassName, topMessage, bottomMessage, tagLabel, tagShape, tagSize, tagColor, slotLabel, tagSlot, optionSlot, contentSlot, mainButtonLabel, mainButtonOutline, subButtonLabel, onOk, onCancel, onClose, }: SConfirmModalProps): import("react").JSX.Element;
|
|
84
|
+
export declare function SConfirmModal({ open, onOpenChange, persistent, type, modalTitle, titleClassName, topMessage, bottomMessage, tagLabel, tagShape, tagSize, tagColor, slotLabel, tagSlot, optionSlot, contentSlot, mainButtonLabel, mainButtonOutline, mainButtonDisabled, subButtonLabel, onOk, onCancel, onClose, }: SConfirmModalProps): import("react").JSX.Element;
|
|
@@ -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
|
|
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,41 @@ 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
|
+
* 목록에서 **서비스를 고름**. 고른 서비스 키가 온다.
|
|
105
|
+
*
|
|
106
|
+
* **여기서 화면을 옮기지 않는다** — 줄은 링크라 브라우저가 `href` 를 새 탭으로 이미 연다.
|
|
107
|
+
* 고른 것을 앱이 알아야 할 때만(기록 등) 준다. 고르면 목록은 할 일을 끝낸 것이라 스스로
|
|
108
|
+
* 닫힌다(`onOpenChange(false)` 도 함께 온다).
|
|
109
|
+
*/
|
|
110
|
+
onSelect?: (service: SLauncherService) => void;
|
|
111
|
+
/**
|
|
112
|
+
* 목록이 열리고 닫힐 때. 여닫는 것은 런처 버튼이 스스로 하므로 **그 사실을 알아야 할 때만** 준다
|
|
113
|
+
* (연 김에 목록을 다시 불러오거나, 열려 있는 동안 다른 층을 접어 두는 화면).
|
|
114
|
+
*
|
|
115
|
+
* 앱이 누르지 않은 닫힘도 온다: `header="fix"` 에서 GNB 를 접으면 런처 버튼이 사라지므로
|
|
116
|
+
* 목록이 닫히고 `false` 가 온다.
|
|
117
|
+
*/
|
|
118
|
+
onOpenChange?: (open: boolean) => void;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
87
122
|
## Dependencies
|
|
88
123
|
|
|
89
124
|
### Used by
|
|
@@ -94,7 +129,9 @@ export interface SGnbMenuItem {
|
|
|
94
129
|
|
|
95
130
|
- [SGhostButton](../SGhostButton)
|
|
96
131
|
- [SIcon](../SIcon)
|
|
132
|
+
- [SLauncherListBox](../SLauncherListBox)
|
|
97
133
|
- [SLayout](../SLayout)
|
|
134
|
+
- [SPortal](../SPortal)
|
|
98
135
|
- [STag](../STag)
|
|
99
136
|
|
|
100
137
|
### Graph
|
|
@@ -103,7 +140,9 @@ export interface SGnbMenuItem {
|
|
|
103
140
|
graph TD;
|
|
104
141
|
SGnb --> SGhostButton
|
|
105
142
|
SGnb --> SIcon
|
|
143
|
+
SGnb --> SLauncherListBox
|
|
106
144
|
SGnb --> SLayout
|
|
145
|
+
SGnb --> SPortal
|
|
107
146
|
SGnb --> STag
|
|
108
147
|
SGnbSystem --> SGnb
|
|
109
148
|
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, type SLauncherService } 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,36 @@ 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
|
+
* 목록에서 **서비스를 고름**. 고른 서비스 키가 온다.
|
|
59
|
+
*
|
|
60
|
+
* **여기서 화면을 옮기지 않는다** — 줄은 링크라 브라우저가 `href` 를 새 탭으로 이미 연다.
|
|
61
|
+
* 고른 것을 앱이 알아야 할 때만(기록 등) 준다. 고르면 목록은 할 일을 끝낸 것이라 스스로
|
|
62
|
+
* 닫힌다(`onOpenChange(false)` 도 함께 온다).
|
|
63
|
+
*/
|
|
64
|
+
onSelect?: (service: SLauncherService) => void;
|
|
65
|
+
/**
|
|
66
|
+
* 목록이 열리고 닫힐 때. 여닫는 것은 런처 버튼이 스스로 하므로 **그 사실을 알아야 할 때만** 준다
|
|
67
|
+
* (연 김에 목록을 다시 불러오거나, 열려 있는 동안 다른 층을 접어 두는 화면).
|
|
68
|
+
*
|
|
69
|
+
* 앱이 누르지 않은 닫힘도 온다: `header="fix"` 에서 GNB 를 접으면 런처 버튼이 사라지므로
|
|
70
|
+
* 목록이 닫히고 `false` 가 온다.
|
|
71
|
+
*/
|
|
72
|
+
onOpenChange?: (open: boolean) => void;
|
|
73
|
+
}
|
|
43
74
|
export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'> {
|
|
44
75
|
/** 메뉴 스타일: box(라운드) / belt(풀폭 행). 미지정 시 SLayout 의 type 을 따른다. */
|
|
45
76
|
type?: SGnbType;
|
|
@@ -68,8 +99,11 @@ export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>
|
|
|
68
99
|
folded?: boolean;
|
|
69
100
|
/** 접힘 토글 (sdFoldChange) */
|
|
70
101
|
onFoldedChange?: (folded: boolean) => void;
|
|
71
|
-
/**
|
|
72
|
-
|
|
102
|
+
/**
|
|
103
|
+
* 앱런처(그리드) 버튼. 미지정 시 런처 버튼을 렌더하지 않는다.
|
|
104
|
+
* 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다.
|
|
105
|
+
*/
|
|
106
|
+
launcher?: SGnbLauncher;
|
|
73
107
|
/** 상단바 로고 영역 (slot). header="full" 이면 폭이 140px 로 고정된다. */
|
|
74
108
|
logo?: ReactNode;
|
|
75
109
|
/**
|
|
@@ -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)과의 간격은
|