sellmate-design-system-react 9.0.0-beta.100 → 9.0.0-beta.102
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 +9 -7
- package/dist/components/SPage/README.md +1 -0
- package/dist/components/SPage/SPageHeader.d.ts +6 -0
- package/dist/components/SSectionNav/README.md +32 -0
- package/dist/components/SSectionNav/SSectionNav.d.ts +40 -0
- package/dist/components/SSectionNav/index.d.ts +1 -0
- package/dist/components/SSectionNav/section-nav-scroll.d.ts +27 -0
- package/dist/index.cjs +229 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +229 -3
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +44 -7
- package/dist/llms.txt +11 -9
- package/dist/styles.css +73 -20
- package/dist/theme.css +28 -20
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
| **날짜·시간** | `SCalendar` `SCalendarBoard`(한 달치 일정 판) `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
|
|
45
45
|
| **표·목록** | `SChatMessage`(대화의 메시지 한 건) `SChatAttachedFile`(입력창 위, 아직 보내지 않은 첨부 한 칸) `SChatFile`(대화 흐름에 선, 보낸 파일 한 칸) `SChatSystemMessage`(대화 흐름 가운데 서는 시스템 안내) `STable` `STableBar` `STreeTable`(행이 부모-자식 계층을 이루는 표 — 펼치고 접는다) `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` `STimeline`(날짜로 묶인 시간순 기록 — 작업 로그·메모 이력) |
|
|
46
46
|
| **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SLauncherListBox`(런처 버튼을 눌러 뜨는 서비스 목록) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
|
|
47
|
-
| **내비게이션** | `STabs` `SPagination` `SStepper` |
|
|
47
|
+
| **내비게이션** | `STabs` `SPagination` `SStepper` `SSectionNav`(같은 페이지의 섹션으로 스크롤해 이동하는 메뉴 — 앱 셸 좌측 내비게이션은 `SGnb`) |
|
|
48
48
|
| **차트** | `SBarChart`(막대 그래프 — 항목끼리 크기를 견준다. `stacked` 로 항목 안의 구성까지) `SLineChart`(꺾은선 그래프 — 순서가 있는 항목의 추이를 본다. `area` 로 크기까지, `stacked` 로 구성까지) `SPieChart`(원그래프 — 한 시점의 전체를 몇 개의 몫으로 나눈다) `SDonutChart`(도넛 그래프 — 가운데가 빈 원그래프. 구멍에 글자를 두지 않는다) |
|
|
49
49
|
| **표시·상태** | `STag` `SBadge` `SIcon` `SLogo`(셀메이트·서비스 로고를 아이콘처럼 — name 으로 서비스, mode 로 배경, direction 으로 가로·세로, size 는 높이다 — `"auto"` 면 부모 폭을 채운다) `SImage` `SCallout` `SContextAlert`(한 줄짜리 맥락 알림 — 오른쪽에 후속 동작 버튼) `SGuide` |
|
|
50
50
|
| **진행·로딩** | `SLinearProgress` `SCircleProgress` `SSkeleton`(처음 불러오는 동안 콘텐츠 자리를 같은 모양으로 채운다) `SLoadingContainer` `SLoadingModal` |
|
|
@@ -87,7 +87,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
|
|
|
87
87
|
| `<form>` | `SForm` |
|
|
88
88
|
| `<dialog>`, 직접 만든 오버레이 | `SModal.confirm(...)`, `SModal.create(...)`, `SPopup` |
|
|
89
89
|
| `alert()`, `confirm()` | `SToast`, `SModal.confirm(...)` |
|
|
90
|
-
| 직접 만든
|
|
90
|
+
| 직접 만든 탭/페이지네이션/스텝퍼/섹션 이동 메뉴 | `STabs`, `SPagination`, `SStepper`, `SSectionNav` |
|
|
91
91
|
| `<ul>`/`<li>` 로 만든 목록 UI | `SList` + `SListItem` (드래그 정렬은 `SDraggableItem`) |
|
|
92
92
|
| 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` 의 `title` / `padding` props |
|
|
93
93
|
| `<svg>` 직접 삽입, 이모지 아이콘 | `SIcon` (셀메이트·서비스 로고는 `SLogo`) |
|
|
@@ -169,7 +169,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
169
169
|
| --- | --- | --- |
|
|
170
170
|
| **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage` `SPageHeader`(`SLayout` 안에서 `SPage` 앞에 둔다) |
|
|
171
171
|
| **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SChatMessage` `SChatSystemMessage` `SChatInput` `SLoginCard`(유일하게 `SPage` 밖에 선다 — 로그인 화면 자체가 자기 자리다, §4-6) `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `STreeTable` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `STimeline` `SCallout` `SContextAlert` `SBarChart` `SLineChart` `SPieChart` `SDonutChart` `STabs` `SStepper` `SPagination` `SDivider` |
|
|
172
|
-
| **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SMeatballButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `SSlider` `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` `SSkeleton` |
|
|
172
|
+
| **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SMeatballButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `SSlider` `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` `SSkeleton` `SSectionNav`(본문과 좌우로 나란히 놓는 섹션 메뉴 — 혼자 페이지에 서지 않는다) |
|
|
173
173
|
| **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `SLogo` `STextLink` `SChip` |
|
|
174
174
|
| **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SLauncherListBox`(런처 버튼에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
|
|
175
175
|
|
|
@@ -217,7 +217,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
217
217
|
블록 순서는 화면 종류와 무관하게 같다. **필요한 것만 남기되 순서를 바꾸지 않는다.**
|
|
218
218
|
|
|
219
219
|
```text
|
|
220
|
-
1. 페이지 제목 `SPageHeader` (SGnb 와 SPage 사이. 가이드·매뉴얼 링크는 slot, 액션 버튼은 오지 않는다 §4-2)
|
|
220
|
+
1. 페이지 제목 `SPageHeader` (SGnb 와 SPage 사이. 제목에 붙는 태그는 titleSlot, 가이드·매뉴얼 링크는 slot, 액션 버튼은 오지 않는다 §4-2)
|
|
221
221
|
2. 상시 안내 SCallout
|
|
222
222
|
3. 필터 SKeyValueTable · SChipFilter (§3-7-11)
|
|
223
223
|
4. 툴바 STableBar (제목 slot + 건수 요약 + 액션)
|
|
@@ -606,13 +606,13 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
606
606
|
| 하려는 일 | 컴포넌트 | 갈림 |
|
|
607
607
|
| --- | --- | --- |
|
|
608
608
|
| 앱 셸(상단바 + 내비 + 본문)을 세운다 | `SLayout` | §4-1 |
|
|
609
|
-
| 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
|
|
609
|
+
| 앱 셸의 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
|
|
610
610
|
| 좌측 내비게이션 맨 아래(또는 전폭 상단바 오른쪽 끝)에 서비스·도메인·알림·설정·계정 묶음을 붙인다 | `SGnbSystem` | §4-1 |
|
|
611
611
|
| 앱 상단바 오른쪽 끝에 알림 벨·계정 이름을 둔다 | `SGnbSystem` (`SGnb` 의 `system` 슬롯 — 직접 만들지 않는다) | §4-1 |
|
|
612
612
|
| 계정 이름을 눌러 이메일·권한·계정 설정·언어 변경·로그아웃을 띄운다 | `SAccountListBox` (`SGnbSystem` 의 `account.listBox` 로 넘긴다 — 직접 띄우지 않는다) | §4-1 |
|
|
613
613
|
| 앱런처 버튼을 눌러 옮겨 갈 수 있는 서비스 목록을 띄운다 | `SLauncherListBox` (`SGnb` 의 `launcher.items` 로 넘긴다 — 직접 띄우지 않는다) | §4-1 |
|
|
614
614
|
| 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
|
|
615
|
-
| 페이지 제목(+ 서브
|
|
615
|
+
| 페이지 제목(+ 서브 텍스트·제목 옆 태그·뒤로가기·우측 슬롯)을 만든다 | `SPageHeader` (`SLayout` 안에서 `SPage` 앞에 둔다) | §4-1 |
|
|
616
616
|
| 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
|
|
617
617
|
| 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
|
|
618
618
|
| 통합 계정 로그인 화면을 만든다 | `SLoginCard` | §4-6 |
|
|
@@ -629,6 +629,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
629
629
|
| 하려는 일 | 컴포넌트 | 갈림 |
|
|
630
630
|
| --- | --- | --- |
|
|
631
631
|
| 같은 화면에서 보는 관점을 바꾼다 | `STabs` | §3-7-2 |
|
|
632
|
+
| 긴 페이지의 섹션 목록을 옆에 두고, 누르면 그 섹션으로 스크롤하고 스크롤하면 보이는 섹션이 선택되게 한다 | `SSectionNav` — 앱 셸의 좌측 내비게이션이 아니다(그건 `SGnb`). 항목의 `value` 는 이동할 섹션 요소의 `id` 이고, `items` 순서는 문서의 섹션 순서와 같아야 한다. 스크롤은 섹션에서 가장 가까운 스크롤 조상에서 일어나므로 `window` 스크롤을 직접 다루지 않는다. 긴 페이지에서 처음부터 특정 섹션으로 열려면 `defaultValue` 를 준다(그 섹션으로 바로 이동하고 선택한 채 시작한다 — 어떤 섹션인지는 앱이 URL 등에서 읽어 넘긴다). 같은 `id` 의 섹션이 한 문서에 둘 이상이면 `scrollContainer` 로 영역을 한정한다. 다른 화면으로 이동하는 메뉴가 아니다 | |
|
|
632
633
|
| 긴 목록을 페이지로 끊는다 | `SPagination` | §4-2 |
|
|
633
634
|
| 여러 단계의 진행 위치를 보여준다 | `SStepper` | |
|
|
634
635
|
|
|
@@ -1520,7 +1521,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1520
1521
|
| --- | --- |
|
|
1521
1522
|
| 페이지·모달의 일반 액션 | `SButton` — 라벨 폭만큼만 차지한다 |
|
|
1522
1523
|
| 목록에서 항목 하나를 고르게 한다 | `SListItem` · `STree` (§3-7-6·§3-7-7) |
|
|
1523
|
-
| 좌측 내비게이션의 메뉴 | `SGnb` 의 `menus` (§4-1) |
|
|
1524
|
+
| 앱 셸 좌측 내비게이션의 메뉴 | `SGnb` 의 `menus` (§4-1) |
|
|
1524
1525
|
| **GNB system 패널에서 설정·연결 상태를 한 줄씩 쌓고, 그 줄을 눌러 들어가게 한다** | **`SSystemActionButton`** |
|
|
1525
1526
|
|
|
1526
1527
|
**무엇이 그려지는지는 `type` 이 정한다.** 타입마다 다른 값만 `option` 으로 받으므로, 타입에 맞지 않는 `option` 은 타입 에러가 된다.
|
|
@@ -2410,6 +2411,7 @@ createRoot(document.getElementById('root')!).render(
|
|
|
2410
2411
|
- **`variant` 는 필수이고, 앱 전체에서 하나로 고정한다.** 어떤 페이지는 `"bar"`(높이 고정 + 하단 구분선), 다른 페이지는 `"plain"`(최소 높이) 로 섞어 쓰지 않는다. 프로젝트에서 하나를 고르면(예: 전부 `"bar"`) 모든 페이지 헤더가 그 값을 쓴다. **기본값이 없는 것도 이 규칙 때문이다** — 기본값이 있으면 안 준 페이지만 조용히 다른 모양이 되어 메뉴를 옮길 때마다 제목 줄이 튄다. 셸을 한 번 감싸 쓰면 지정하는 자리는 `AppShell` 한 곳이다. `SLayout` 의 `header`(`"fix" | "full"`)와는 다른 축이다 — 그쪽은 상단바가 GNB 컬럼 안에 있는지 화면 전폭인지를 정한다.
|
|
2411
2412
|
- **`slot`·`onBack` 도 페이지 성격이 실제로 다른 경우가 아니면 있는 대로 통일한다.** "목록 페이지엔 없고 상세·등록 페이지엔 있다"처럼 화면 종류에 따라 갈리는 것은 허용되지만, 같은 종류의 화면끼리는 임의로 넣었다 뺐다 하지 않는다.
|
|
2412
2413
|
- **`slot` 은 `ReactNode` 를 그대로 받지만, 원칙은 `size="sm"` 버튼 위주로만 채운다.** `STextLink`·복잡한 커스텀 마크업을 슬롯에 넣지 않는다 — 그 이상이 필요하면 페이지 헤더가 아니라 §4-2 의 `STableBar` 처럼 본문 쪽 액션 자리를 쓴다.
|
|
2414
|
+
- **제목에 붙는 태그·배지는 `slot` 이 아니라 `titleSlot` 에 둔다.** `titleSlot` 은 타이틀 바로 오른쪽(`bar`·`plain` 모두 타이틀과 같은 줄)에 놓이고 간격은 컴포넌트가 준다. `slot` 은 오른쪽 끝이라 제목과 떨어지므로 "기본 그룹" 같은 제목 수식 요소를 두면 안 된다. `subText` 에 노드를 억지로 넣지도 않는다 — `subText` 는 문자열 부제이고 `plain` 에서는 타이틀 아래로 내려간다.
|
|
2413
2415
|
|
|
2414
2416
|
**셸 밖의 단독 화면(로그인·온보딩 등)에서도 같은 제목 영역을 쓸 수 있다.** 이때는 페이지 열이 없으므로 `scrolled` 가 앱 몫이 된다.
|
|
2415
2417
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|------|------|---------|-------------|
|
|
11
11
|
| `title` | `string` | — | 타이틀 (필수) |
|
|
12
12
|
| `subText?` | `string` | — | 서브 텍스트. `bar` 면 타이틀 오른쪽에, `plain` 이면 타이틀 아래에 놓인다. |
|
|
13
|
+
| `titleSlot?` | `ReactNode` | — | 타이틀 바로 오른쪽 자리. 태그·배지처럼 제목에 붙는 요소가 온다. `bar`·`plain` 모두 타이틀과 같은 줄(세로 가운데)에 놓이고, `plain` 의 서브 텍스트는 그 아래에 그대로 남는다. 오른쪽 끝 액션은 `slot` 이다 — 이 자리와 헷갈리지 않는다. |
|
|
13
14
|
| `variant` | `SPageHeaderVariant` | — | 제목 영역의 모양 — **필수다.** - `plain`: 최소 높이(상하 패딩 안에서 타이틀 한 줄 높이로 결정) + 구분선 없음, 서브 텍스트가 타이틀 아래로 내려간다. - `bar`: 높이 고정 + 하단 구분선, 서브 텍스트가 타이틀과 같은 줄. 기본값을 두지 않는 이유는 이 값이 **앱 전체에서 하나로 고정돼야 하는 값**이기 때문이다. 기본값이 있으면 안 준 페이지만 조용히 다른 모양이 되어, 메뉴를 옮길 때 제목 줄의 높이와 구분선이 페이지마다 튄다. 셸을 한 번 감싸 쓰면(§4-1 AppShell) 지정하는 자리도 한 곳이다. 배경은 두 베리에이션 모두 흰 면이다. `SLayout` 의 `header`(`'fix' | 'full'`)와는 다른 축이다 — 그쪽은 상단바가 GNB 컬럼 안에 있는지 화면 전폭인지를 정한다. |
|
|
14
15
|
| `slot?` | `ReactNode` | — | 오른쪽 끝 커스텀 슬롯. 가이드 링크·액션 버튼 등 페이지마다 다른 것이 온다. |
|
|
15
16
|
| `scrolled?` | `boolean` | — | 아래 콘텐츠가 스크롤을 시작했는지. `plain` 일 때만 의미가 있다: 스크롤이 시작되면 그림자(floating)가 들어가 스크롤되는 콘텐츠 위에 뜬 것처럼 보인다. 배경은 흰 면 그대로다 — scrolled 가 더하는 것은 그림자뿐이다. `SLayout` 안에 놓았으면 **주지 않는다** — 같은 열의 `SPage` 가 자기 스크롤 상태를 올려 자동으로 들어온다. 스크롤 컨테이너를 앱이 직접 쥔 단독 배치에서만 `onScroll` 에서 준다. |
|
|
@@ -5,6 +5,12 @@ export interface SPageHeaderProps extends Omit<HTMLAttributes<HTMLElement>, 'slo
|
|
|
5
5
|
title: string;
|
|
6
6
|
/** 서브 텍스트. `bar` 면 타이틀 오른쪽에, `plain` 이면 타이틀 아래에 놓인다. */
|
|
7
7
|
subText?: string;
|
|
8
|
+
/**
|
|
9
|
+
* 타이틀 바로 오른쪽 자리. 태그·배지처럼 제목에 붙는 요소가 온다.
|
|
10
|
+
* `bar`·`plain` 모두 타이틀과 같은 줄(세로 가운데)에 놓이고, `plain` 의 서브 텍스트는 그 아래에 그대로 남는다.
|
|
11
|
+
* 오른쪽 끝 액션은 `slot` 이다 — 이 자리와 헷갈리지 않는다.
|
|
12
|
+
*/
|
|
13
|
+
titleSlot?: ReactNode;
|
|
8
14
|
/**
|
|
9
15
|
* 제목 영역의 모양 — **필수다.**
|
|
10
16
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SSectionNav
|
|
2
|
+
|
|
3
|
+
> 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
|
|
4
|
+
|
|
5
|
+
### SSectionNav
|
|
6
|
+
|
|
7
|
+
#### Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Description |
|
|
10
|
+
|------|------|---------|-------------|
|
|
11
|
+
| `'items'` | `SSectionNavItem[]` | — | 항목 목록 — 문서에 놓인 섹션 순서와 같아야 한다 |
|
|
12
|
+
| `'value'?` | `string` | — | 선택된 항목의 value. 주면 제어형이고, 없으면 스크롤 위치에 따라 스스로 정한다 |
|
|
13
|
+
| `'defaultValue'?` | `string` | — | 비제어일 때 처음 진입할 value — 그 섹션으로 애니메이션 없이 바로 이동해 선택한 채 시작한다. 섹션이 나중에 그려지면 나타났을 때 이동한다. 그 전에 사용자가 스크롤했으면 이동하지 않는다. 이동한 뒤에는 스크롤 위치를 따라간다 |
|
|
14
|
+
| `'onValueChange'?` | `(value: string) => void` | — | 선택이 바뀔 때 — 항목을 눌렀을 때와 스크롤로 섹션이 바뀔 때 호출된다. 처음 보이는 섹션으로 시작할 때와 이미 선택된 항목을 눌렀을 때는 호출되지 않는다 |
|
|
15
|
+
| `'scrollContainer'?` | `RefObject<HTMLElement \| null>` | — | 섹션을 찾고 스크롤할 영역. 없으면 문서 전체에서 id 로 섹션을 찾고 가장 가까운 스크롤 조상을 쓴다. 같은 id 의 섹션이 한 문서에 둘 이상 있을 때(같은 화면을 여러 번 그리는 경우 등) 영역을 지정한다 |
|
|
16
|
+
| `'aria-label'?` | `string` | — | 내비게이션 랜드마크 이름 — 한 화면에 내비게이션이 여럿일 때 구분한다 |
|
|
17
|
+
| `'className'?` | `string` | — | |
|
|
18
|
+
| `'style'?` | `CSSProperties` | — | |
|
|
19
|
+
|
|
20
|
+
## Types
|
|
21
|
+
|
|
22
|
+
### SSectionNavItem
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
export interface SSectionNavItem {
|
|
26
|
+
/** 항목에 표시할 텍스트 */
|
|
27
|
+
label: string;
|
|
28
|
+
/** 이동할 섹션 요소의 `id` — 목록 안에서 고유해야 한다 */
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type CSSProperties, type RefObject } from 'react';
|
|
2
|
+
export interface SSectionNavItem {
|
|
3
|
+
/** 항목에 표시할 텍스트 */
|
|
4
|
+
label: string;
|
|
5
|
+
/** 이동할 섹션 요소의 `id` — 목록 안에서 고유해야 한다 */
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SSectionNavProps {
|
|
9
|
+
/** 항목 목록 — 문서에 놓인 섹션 순서와 같아야 한다 */
|
|
10
|
+
'items': SSectionNavItem[];
|
|
11
|
+
/** 선택된 항목의 value. 주면 제어형이고, 없으면 스크롤 위치에 따라 스스로 정한다 */
|
|
12
|
+
'value'?: string;
|
|
13
|
+
/**
|
|
14
|
+
* 비제어일 때 처음 진입할 value — 그 섹션으로 애니메이션 없이 바로 이동해 선택한 채 시작한다.
|
|
15
|
+
* 섹션이 나중에 그려지면 나타났을 때 이동한다. 그 전에 사용자가 스크롤했으면 이동하지 않는다.
|
|
16
|
+
* 이동한 뒤에는 스크롤 위치를 따라간다
|
|
17
|
+
*/
|
|
18
|
+
'defaultValue'?: string;
|
|
19
|
+
/**
|
|
20
|
+
* 선택이 바뀔 때 — 항목을 눌렀을 때와 스크롤로 섹션이 바뀔 때 호출된다.
|
|
21
|
+
* 처음 보이는 섹션으로 시작할 때와 이미 선택된 항목을 눌렀을 때는 호출되지 않는다
|
|
22
|
+
*/
|
|
23
|
+
'onValueChange'?: (value: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* 섹션을 찾고 스크롤할 영역. 없으면 문서 전체에서 id 로 섹션을 찾고 가장 가까운 스크롤 조상을 쓴다.
|
|
26
|
+
* 같은 id 의 섹션이 한 문서에 둘 이상 있을 때(같은 화면을 여러 번 그리는 경우 등) 영역을 지정한다
|
|
27
|
+
*/
|
|
28
|
+
'scrollContainer'?: RefObject<HTMLElement | null>;
|
|
29
|
+
/** 내비게이션 랜드마크 이름 — 한 화면에 내비게이션이 여럿일 때 구분한다 */
|
|
30
|
+
'aria-label'?: string;
|
|
31
|
+
'className'?: string;
|
|
32
|
+
'style'?: CSSProperties;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* SSectionNav — 같은 페이지 안의 섹션으로 이동하는 사이드 메뉴.
|
|
36
|
+
* 항목을 누르면 해당 id 의 섹션으로 스크롤하고, 스크롤하면 보이는 섹션에 맞춰 선택이 따라간다.
|
|
37
|
+
* 스크롤은 섹션에서 가장 가까운 스크롤 조상(SPage 의 본문 등)에서 일어난다.
|
|
38
|
+
* 컴포넌트 토큰(--cmp-sideNav-*) 기반. 앱 셸의 좌측 내비게이션은 SGnb 가 맡는다.
|
|
39
|
+
*/
|
|
40
|
+
export declare const SSectionNav: import("react").ForwardRefExoticComponent<SSectionNavProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SSectionNav, type SSectionNavProps, type SSectionNavItem } from './SSectionNav';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ScrollMetrics {
|
|
2
|
+
clientHeight: number;
|
|
3
|
+
scrollTop: number;
|
|
4
|
+
scrollHeight: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 활성 섹션의 인덱스를 고른다.
|
|
8
|
+
* @param tops 각 섹션 상단의 스크롤 영역 위쪽 기준 거리(px) — 항목 순서 = 문서 순서여야 한다
|
|
9
|
+
* 끝까지 스크롤했고 마지막 섹션이 화면에 보이면, 그 섹션이 짧아 선에 닿지 못해도 마지막 항목을 고른다.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pickActiveIndex(tops: number[], m: ScrollMetrics): number;
|
|
12
|
+
/** id 로 섹션을 찾는다. scope 를 주면 그 안에서만 찾아 같은 id 가 문서에 또 있어도 섞이지 않는다 */
|
|
13
|
+
export declare function findSection(id: string, scope: HTMLElement | null): HTMLElement | null;
|
|
14
|
+
/**
|
|
15
|
+
* 요소에서 가장 가까운 세로 스크롤 조상. 없으면 문서 스크롤 요소.
|
|
16
|
+
* body·html 의 overflow 는 뷰포트로 전파되어 스스로 스크롤하지 않으므로 건너뛴다
|
|
17
|
+
*/
|
|
18
|
+
export declare function findScrollParent(el: HTMLElement): HTMLElement;
|
|
19
|
+
export declare const isRootScroller: (el: HTMLElement) => boolean;
|
|
20
|
+
export declare const readMetrics: (container: HTMLElement) => ScrollMetrics;
|
|
21
|
+
/** 섹션 상단이 스크롤 영역 위쪽에서 떨어진 거리(px) */
|
|
22
|
+
export declare const sectionTop: (section: HTMLElement, container: HTMLElement) => number;
|
|
23
|
+
/**
|
|
24
|
+
* 섹션 상단이 스크롤 영역 위쪽에 맞도록 스크롤한다.
|
|
25
|
+
* 섹션의 `scroll-margin-top` 만큼 비워 고정 헤더 아래로 들어가지 않게 한다
|
|
26
|
+
*/
|
|
27
|
+
export declare function scrollToSection(section: HTMLElement, container: HTMLElement, behavior?: 'smooth' | 'instant'): void;
|
package/dist/index.cjs
CHANGED
|
@@ -14087,8 +14087,10 @@ var PAGE_PADDINGS = ["default", "none"];
|
|
|
14087
14087
|
var PAGE_CONTENT_WIDTHS = ["full", "fixed", "fixedCenter"];
|
|
14088
14088
|
var PAGE_HEADER_VARIANTS = ["plain", "bar"];
|
|
14089
14089
|
var SPageHeader = /* @__PURE__ */ withDisplayName(
|
|
14090
|
-
react.forwardRef(function SPageHeader2({ title, subText, variant, onBack, slot, scrolled, className, style, ...rest }, ref) {
|
|
14090
|
+
react.forwardRef(function SPageHeader2({ title, subText, titleSlot, variant, onBack, slot, scrolled, className, style, ...rest }, ref) {
|
|
14091
14091
|
const hasBack = onBack != null;
|
|
14092
|
+
const hasTitleSlot = titleSlot != null && titleSlot !== false;
|
|
14093
|
+
const titleEl = /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-heading-lg min-w-0 truncate text-(--cmp-pageHeader-title-text)", children: title });
|
|
14092
14094
|
const bar = variant === "bar";
|
|
14093
14095
|
const column = react.useContext(SPageColumnContext);
|
|
14094
14096
|
const isScrolled = scrolled ?? column?.scrolled ?? false;
|
|
@@ -14138,7 +14140,13 @@ var SPageHeader = /* @__PURE__ */ withDisplayName(
|
|
|
14138
14140
|
!hasBack && "pl-(--cmp-pageHeader-title-paddingLeft)"
|
|
14139
14141
|
),
|
|
14140
14142
|
children: [
|
|
14141
|
-
|
|
14143
|
+
hasTitleSlot ? (
|
|
14144
|
+
// max-w-full: plain 의 열은 items-start 라 래퍼가 내용 폭을 따라가 긴 제목이 잘리지 않고 태그를 밀어낸다
|
|
14145
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex max-w-full min-w-0 items-center gap-(--cmp-pageHeader-gap)", children: [
|
|
14146
|
+
titleEl,
|
|
14147
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center", children: titleSlot })
|
|
14148
|
+
] })
|
|
14149
|
+
) : titleEl,
|
|
14142
14150
|
subText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typo-body-md-default min-w-0 truncate text-(--cmp-pageHeader-subtitle-text)", children: subText })
|
|
14143
14151
|
]
|
|
14144
14152
|
}
|
|
@@ -14381,6 +14389,224 @@ function SPagination({
|
|
|
14381
14389
|
);
|
|
14382
14390
|
}
|
|
14383
14391
|
|
|
14392
|
+
// src/components/SSectionNav/section-nav-scroll.ts
|
|
14393
|
+
var ACTIVATE_RATIO = 0.25;
|
|
14394
|
+
function pickActiveIndex(tops, m) {
|
|
14395
|
+
if (tops.length === 0) return -1;
|
|
14396
|
+
const atBottom = m.scrollTop > 0 && m.scrollTop + m.clientHeight >= m.scrollHeight - 1;
|
|
14397
|
+
const last = tops.length - 1;
|
|
14398
|
+
if (atBottom && tops[last] < m.clientHeight) return last;
|
|
14399
|
+
const line = m.clientHeight * ACTIVATE_RATIO;
|
|
14400
|
+
let active = 0;
|
|
14401
|
+
tops.forEach((top, i) => {
|
|
14402
|
+
if (top <= line) active = i;
|
|
14403
|
+
});
|
|
14404
|
+
return active;
|
|
14405
|
+
}
|
|
14406
|
+
function findSection(id, scope) {
|
|
14407
|
+
if (!scope) return document.getElementById(id);
|
|
14408
|
+
return scope.querySelector(`[id="${id.replace(/["\\]/g, "\\$&")}"]`);
|
|
14409
|
+
}
|
|
14410
|
+
var isScrollable = (el) => {
|
|
14411
|
+
const { overflowY } = window.getComputedStyle(el);
|
|
14412
|
+
return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
|
|
14413
|
+
};
|
|
14414
|
+
function findScrollParent(el) {
|
|
14415
|
+
for (let p = el.parentElement; p; p = p.parentElement) {
|
|
14416
|
+
if (p === document.body || p === document.documentElement) break;
|
|
14417
|
+
if (isScrollable(p)) return p;
|
|
14418
|
+
}
|
|
14419
|
+
return document.scrollingElement ?? document.documentElement;
|
|
14420
|
+
}
|
|
14421
|
+
var isRootScroller = (el) => el === document.scrollingElement || el === document.documentElement;
|
|
14422
|
+
var viewportTop = (container) => isRootScroller(container) ? 0 : container.getBoundingClientRect().top + container.clientTop;
|
|
14423
|
+
var readMetrics = (container) => ({
|
|
14424
|
+
// 문서 전체는 가로 스크롤바를 뺀 높이(documentElement.clientHeight)여야 scrollHeight 와 같은 기준이다
|
|
14425
|
+
clientHeight: isRootScroller(container) ? document.documentElement.clientHeight : container.clientHeight,
|
|
14426
|
+
scrollTop: container.scrollTop,
|
|
14427
|
+
scrollHeight: container.scrollHeight
|
|
14428
|
+
});
|
|
14429
|
+
var sectionTop = (section, container) => section.getBoundingClientRect().top - viewportTop(container);
|
|
14430
|
+
function scrollToSection(section, container, behavior = "smooth") {
|
|
14431
|
+
const margin = parseFloat(window.getComputedStyle(section).scrollMarginTop) || 0;
|
|
14432
|
+
const top = container.scrollTop + sectionTop(section, container) - margin;
|
|
14433
|
+
const reduce = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
14434
|
+
if (typeof container.scrollTo === "function") {
|
|
14435
|
+
container.scrollTo({ top, behavior: behavior === "smooth" && !reduce ? "smooth" : "instant" });
|
|
14436
|
+
} else {
|
|
14437
|
+
container.scrollTop = top;
|
|
14438
|
+
}
|
|
14439
|
+
}
|
|
14440
|
+
var SETTLE_MS = 150;
|
|
14441
|
+
var SSectionNav = /* @__PURE__ */ react.forwardRef(function SSectionNav2({
|
|
14442
|
+
items,
|
|
14443
|
+
value,
|
|
14444
|
+
defaultValue,
|
|
14445
|
+
onValueChange,
|
|
14446
|
+
scrollContainer,
|
|
14447
|
+
"aria-label": ariaLabel,
|
|
14448
|
+
className,
|
|
14449
|
+
style
|
|
14450
|
+
}, ref) {
|
|
14451
|
+
const controlled = value !== void 0;
|
|
14452
|
+
const [inner, setInner] = react.useState(defaultValue);
|
|
14453
|
+
const active = controlled ? value : inner;
|
|
14454
|
+
const activeRef = react.useRef(active);
|
|
14455
|
+
const emittedRef = react.useRef(void 0);
|
|
14456
|
+
activeRef.current = active;
|
|
14457
|
+
react.useEffect(() => {
|
|
14458
|
+
emittedRef.current = void 0;
|
|
14459
|
+
}, [active]);
|
|
14460
|
+
const onChangeRef = react.useRef(onValueChange);
|
|
14461
|
+
onChangeRef.current = onValueChange;
|
|
14462
|
+
const select = react.useCallback(
|
|
14463
|
+
(next, notify = true) => {
|
|
14464
|
+
if (next === activeRef.current) return;
|
|
14465
|
+
if (!controlled) setInner(next);
|
|
14466
|
+
if (notify && next !== emittedRef.current) {
|
|
14467
|
+
emittedRef.current = next;
|
|
14468
|
+
onChangeRef.current?.(next);
|
|
14469
|
+
}
|
|
14470
|
+
},
|
|
14471
|
+
[controlled]
|
|
14472
|
+
);
|
|
14473
|
+
const lockedRef = react.useRef(false);
|
|
14474
|
+
const unlockTimer = react.useRef(void 0);
|
|
14475
|
+
const holdLock = react.useCallback(() => {
|
|
14476
|
+
lockedRef.current = true;
|
|
14477
|
+
clearTimeout(unlockTimer.current);
|
|
14478
|
+
unlockTimer.current = setTimeout(() => {
|
|
14479
|
+
lockedRef.current = false;
|
|
14480
|
+
}, SETTLE_MS);
|
|
14481
|
+
}, []);
|
|
14482
|
+
const initialTarget = react.useRef(controlled ? void 0 : defaultValue);
|
|
14483
|
+
const initialPending = react.useRef(initialTarget.current !== void 0);
|
|
14484
|
+
const valuesKey = items.map((i) => i.value).join("\0");
|
|
14485
|
+
react.useEffect(() => {
|
|
14486
|
+
const scope = scrollContainer?.current ?? null;
|
|
14487
|
+
const findPresent = () => items.map((item4) => ({ item: item4, el: findSection(item4.value, scope) })).filter((s) => s.el !== null);
|
|
14488
|
+
let detach;
|
|
14489
|
+
let waiter;
|
|
14490
|
+
const attach = () => {
|
|
14491
|
+
const [firstPresent] = findPresent();
|
|
14492
|
+
if (!firstPresent) return false;
|
|
14493
|
+
const container = scope ?? findScrollParent(firstPresent.el);
|
|
14494
|
+
const root = isRootScroller(container);
|
|
14495
|
+
const scroller = root ? window : container;
|
|
14496
|
+
const update = (notify) => {
|
|
14497
|
+
const present = findPresent();
|
|
14498
|
+
const index = pickActiveIndex(
|
|
14499
|
+
present.map((s) => sectionTop(s.el, container)),
|
|
14500
|
+
readMetrics(container)
|
|
14501
|
+
);
|
|
14502
|
+
if (index >= 0) select(present[index].item.value, notify);
|
|
14503
|
+
};
|
|
14504
|
+
const refresh = () => {
|
|
14505
|
+
if (!lockedRef.current) update(true);
|
|
14506
|
+
};
|
|
14507
|
+
const onScroll = () => {
|
|
14508
|
+
if (lockedRef.current) {
|
|
14509
|
+
holdLock();
|
|
14510
|
+
return;
|
|
14511
|
+
}
|
|
14512
|
+
initialPending.current = false;
|
|
14513
|
+
update(true);
|
|
14514
|
+
};
|
|
14515
|
+
const applyInitial = () => {
|
|
14516
|
+
const target = initialTarget.current;
|
|
14517
|
+
if (!initialPending.current || target === void 0) return;
|
|
14518
|
+
const section = findSection(target, scope);
|
|
14519
|
+
if (!section) return;
|
|
14520
|
+
initialPending.current = false;
|
|
14521
|
+
holdLock();
|
|
14522
|
+
scrollToSection(section, container, "instant");
|
|
14523
|
+
};
|
|
14524
|
+
scroller.addEventListener("scroll", onScroll, { passive: true });
|
|
14525
|
+
const sizes = /* @__PURE__ */ new WeakMap();
|
|
14526
|
+
const resize = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver((entries4) => {
|
|
14527
|
+
const changed = entries4.filter((e) => {
|
|
14528
|
+
const size = `${e.contentRect.width}x${e.contentRect.height}`;
|
|
14529
|
+
const before = sizes.get(e.target);
|
|
14530
|
+
sizes.set(e.target, size);
|
|
14531
|
+
return before !== void 0 && before !== size;
|
|
14532
|
+
});
|
|
14533
|
+
if (changed.length > 0) refresh();
|
|
14534
|
+
});
|
|
14535
|
+
const observeSizes = () => {
|
|
14536
|
+
resize?.observe(root ? document.documentElement : container);
|
|
14537
|
+
findPresent().forEach((s) => resize?.observe(s.el));
|
|
14538
|
+
};
|
|
14539
|
+
observeSizes();
|
|
14540
|
+
let presentKey = findPresent().map((s) => s.item.value).join("\0");
|
|
14541
|
+
const mutation = typeof MutationObserver === "undefined" ? void 0 : new MutationObserver(() => {
|
|
14542
|
+
const key = findPresent().map((s) => s.item.value).join("\0");
|
|
14543
|
+
if (key === presentKey) return;
|
|
14544
|
+
presentKey = key;
|
|
14545
|
+
observeSizes();
|
|
14546
|
+
applyInitial();
|
|
14547
|
+
refresh();
|
|
14548
|
+
});
|
|
14549
|
+
mutation?.observe(root ? document.body : container, { childList: true, subtree: true });
|
|
14550
|
+
const current = activeRef.current;
|
|
14551
|
+
if (current === void 0 || !items.some((i) => i.value === current)) update(false);
|
|
14552
|
+
applyInitial();
|
|
14553
|
+
detach = () => {
|
|
14554
|
+
scroller.removeEventListener("scroll", onScroll);
|
|
14555
|
+
resize?.disconnect();
|
|
14556
|
+
mutation?.disconnect();
|
|
14557
|
+
};
|
|
14558
|
+
return true;
|
|
14559
|
+
};
|
|
14560
|
+
if (!attach() && typeof MutationObserver !== "undefined") {
|
|
14561
|
+
waiter = new MutationObserver(() => {
|
|
14562
|
+
if (attach()) waiter?.disconnect();
|
|
14563
|
+
});
|
|
14564
|
+
waiter.observe(scope ?? document.body, { childList: true, subtree: true });
|
|
14565
|
+
}
|
|
14566
|
+
return () => {
|
|
14567
|
+
waiter?.disconnect();
|
|
14568
|
+
detach?.();
|
|
14569
|
+
};
|
|
14570
|
+
}, [valuesKey, scrollContainer, select, holdLock]);
|
|
14571
|
+
const go = (next) => {
|
|
14572
|
+
const scope = scrollContainer?.current ?? null;
|
|
14573
|
+
const section = findSection(next, scope);
|
|
14574
|
+
if (!section) {
|
|
14575
|
+
throw new Error(`SSectionNav: id="${next}" \uC778 \uC139\uC158 \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
14576
|
+
}
|
|
14577
|
+
holdLock();
|
|
14578
|
+
select(next);
|
|
14579
|
+
scrollToSection(section, scope ?? findScrollParent(section));
|
|
14580
|
+
};
|
|
14581
|
+
return /* @__PURE__ */ jsxRuntime.jsx("nav", { ref, "aria-label": ariaLabel, className, style, children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col gap-(--cmp-sideNav-gap) p-0", children: items.map((item4) => {
|
|
14582
|
+
const selected = item4.value === active;
|
|
14583
|
+
return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
14584
|
+
"button",
|
|
14585
|
+
{
|
|
14586
|
+
type: "button",
|
|
14587
|
+
"aria-current": selected ? "location" : void 0,
|
|
14588
|
+
onClick: () => go(item4.value),
|
|
14589
|
+
className: cn(
|
|
14590
|
+
"box-border flex h-(--cmp-sideNav-height) w-full cursor-pointer items-center rounded-(--cmp-sideNav-radius) border-0 px-(--cmp-sideNav-paddingX) text-left transition-colors duration-200",
|
|
14591
|
+
selected ? "typo-body-md-semibold bg-(--cmp-sideNav-bg-selected) text-(--cmp-sideNav-fg-selected)" : "typo-body-md-default bg-transparent text-(--cmp-sideNav-fg-default) hover:font-semibold hover:text-(--cmp-sideNav-fg-hover)"
|
|
14592
|
+
),
|
|
14593
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "grid min-w-0 flex-1", children: [
|
|
14594
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
14595
|
+
"span",
|
|
14596
|
+
{
|
|
14597
|
+
"aria-hidden": true,
|
|
14598
|
+
className: "invisible truncate font-semibold",
|
|
14599
|
+
style: { gridArea: "1 / 1" },
|
|
14600
|
+
children: item4.label
|
|
14601
|
+
}
|
|
14602
|
+
),
|
|
14603
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", style: { gridArea: "1 / 1" }, children: item4.label })
|
|
14604
|
+
] })
|
|
14605
|
+
}
|
|
14606
|
+
) }, item4.value);
|
|
14607
|
+
}) }) });
|
|
14608
|
+
});
|
|
14609
|
+
|
|
14384
14610
|
// src/components/SStepper/stepper.config.ts
|
|
14385
14611
|
var STEPPER_SIZES = ["sm", "lg"];
|
|
14386
14612
|
var V2 = (path) => `var(--cmp-stepper-${path})`;
|
|
@@ -30411,6 +30637,7 @@ exports.SSPLITTER_UNITS = SSPLITTER_UNITS;
|
|
|
30411
30637
|
exports.SScrollArea = SScrollArea;
|
|
30412
30638
|
exports.SSearchInput = SSearchInput;
|
|
30413
30639
|
exports.SSectionHeaderCard = SSectionHeaderCard2;
|
|
30640
|
+
exports.SSectionNav = SSectionNav;
|
|
30414
30641
|
exports.SSelect = SSelect2;
|
|
30415
30642
|
exports.SSkeleton = SSkeleton;
|
|
30416
30643
|
exports.SSlider = SSlider;
|