for-humanity 0.4.3 → 0.5.0
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/CHANGELOG.md +23 -0
- package/README.md +5 -5
- package/dist/browser.js +39 -0
- package/dist/cli.css +372 -299
- package/dist/cli.js +1125 -301
- package/package.json +8 -11
- package/src/component/widget/{shell → cursor-face}/_constant/cursor-face.ts +5 -0
- package/src/component/widget/cursor-face/wg-cursor-face.css +15 -0
- package/src/component/widget/cursor-face/wg-cursor-face.tsx +153 -0
- package/src/component/widget/navigation/_constant/navigation-data-schema.ts +35 -0
- package/src/component/widget/navigation/_constant/navigation.ts +9 -0
- package/src/component/widget/navigation/_function/restore-navigation-scroll.ts +59 -0
- package/src/{content → component/widget/navigation/_function}/to-doc-groups/_to-doc-branches.ts +2 -2
- package/src/{content → component/widget/navigation/_function}/to-doc-groups/_to-doc-group-branches.ts +3 -3
- package/src/{content → component/widget/navigation/_function}/to-doc-groups/to-doc-groups.ts +1 -1
- package/src/component/widget/navigation/_function/to-navigation-branch-id.ts +6 -0
- package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts +14 -0
- package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts +14 -0
- package/src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts +25 -0
- package/src/component/widget/navigation/_function/to-navigation-json.ts +8 -0
- package/src/component/widget/{shell → navigation}/_function/to-toc-groups.ts +2 -2
- package/src/{type → component/widget/navigation/_type}/doc-group.ts +1 -1
- package/src/component/widget/navigation/_type/navigation-data.ts +83 -0
- package/src/component/widget/navigation/_type/navigation-focus-snapshot.ts +21 -0
- package/src/component/widget/navigation/_wg-navigation-branch-toggle.css +28 -0
- package/src/component/widget/navigation/_wg-navigation-branch-toggle.tsx +48 -0
- package/src/component/widget/{shell/_wg-shell-nav-list.css → navigation/_wg-navigation-nav-list.css} +24 -16
- package/src/component/widget/navigation/_wg-navigation-nav-list.tsx +106 -0
- package/src/component/widget/{shell/_wg-shell-nav.css → navigation/_wg-navigation-nav.css} +9 -3
- package/src/component/widget/navigation/_wg-navigation-nav.tsx +43 -0
- package/src/component/widget/navigation/_wg-navigation-scroll.tsx +117 -0
- package/src/component/widget/{shell/_wg-shell-toc.css → navigation/_wg-navigation-toc.css} +43 -26
- package/src/component/widget/navigation/_wg-navigation-toc.tsx +155 -0
- package/src/component/widget/navigation/wg-navigation.css +282 -0
- package/src/component/widget/navigation/wg-navigation.tsx +479 -0
- package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +1 -1
- package/src/component/widget/shell/wg-shell.css +13 -309
- package/src/component/widget/shell/wg-shell.tsx +16 -75
- package/src/component/widget/shell-controls/wg-shell-controls.css +4 -0
- package/src/component/widget/shell-controls/wg-shell-controls.tsx +170 -0
- package/src/constant/asset.ts +0 -5
- package/src/entry/browser/browser.tsx +22 -0
- package/src/{app.tsx → entry/cli/_function/create-app.tsx} +6 -6
- package/src/{content → entry/cli/_function/create-processor}/create-processor.ts +3 -3
- package/src/{content → entry/cli/_function}/read-docs.ts +1 -1
- package/src/{content → entry/cli/_function}/read-home.ts +1 -1
- package/src/{content/read-docs.test.ts → entry/cli/cli.test.ts} +105 -30
- package/src/{cli.ts → entry/cli/cli.ts} +10 -11
- package/src/store/navigation/_constant/tree.ts +6 -0
- package/src/store/navigation/create-navigation-stores.test.ts +92 -0
- package/src/store/navigation/create-navigation-stores.ts +73 -0
- package/src/store/navigation/navigation-state.ts +46 -0
- package/src/store/navigation/navigation-storage.ts +14 -0
- package/src/store/navigation/to-navigation-scroll-state.ts +42 -0
- package/src/store/navigation/to-navigation-tree-state.ts +19 -0
- package/src/store/navigation/use-navigation-tree-store.ts +12 -0
- package/src/style/token.css +4 -2
- package/src/type/doc-content.ts +1 -1
- package/src/type/doc-file-data.ts +1 -1
- package/src/{component/widget/shell/_type → type}/doc-outline.ts +1 -1
- package/src/type/site-assets.ts +2 -2
- package/dist/client.js +0 -6
- package/src/client.ts +0 -335
- package/src/component/widget/shell/_constant/navigation.ts +0 -9
- package/src/component/widget/shell/_function/bind-mobile-navigation.ts +0 -140
- package/src/component/widget/shell/_function/bind-navigation-scroll.ts +0 -123
- package/src/component/widget/shell/_wg-shell-nav-list.tsx +0 -87
- package/src/component/widget/shell/_wg-shell-nav.tsx +0 -49
- package/src/component/widget/shell/_wg-shell-toc.tsx +0 -57
- /package/src/{constant → component/widget/navigation/_constant}/locale.ts +0 -0
- /package/src/component/widget/{shell → navigation}/_constant/reading-line.ts +0 -0
- /package/src/{type → component/widget/navigation/_type}/doc-branch.ts +0 -0
- /package/src/component/widget/{shell → navigation}/_type/toc-group.ts +0 -0
- /package/src/{constant → entry/cli/_constant}/cli.ts +0 -0
- /package/src/{dev.ts → entry/cli/_function/create-dev-app.ts} +0 -0
- /package/src/{content/rehype-assets.ts → entry/cli/_function/create-processor/_rehype-assets.ts} +0 -0
- /package/src/{content/rehype-heading-ids.ts → entry/cli/_function/create-processor/_rehype-heading-ids.ts} +0 -0
- /package/src/{content/remark-report.ts → entry/cli/_function/create-processor/_remark-report.ts} +0 -0
- /package/src/{component/widget/prose/_type → type}/doc-section.ts +0 -0
package/src/component/widget/{shell/_wg-shell-nav-list.css → navigation/_wg-navigation-nav-list.css}
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 재귀 문서 목록 · 단계별 가지와 조상 경로의 1px 트리
|
|
3
3
|
*/
|
|
4
|
-
.
|
|
4
|
+
.wg_navigationNavList__root {
|
|
5
5
|
margin: 0 0 0 var(--app-space-tree-inset);
|
|
6
6
|
padding: 0;
|
|
7
7
|
list-style: none;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.
|
|
10
|
+
.wg_navigationNavList__branch {
|
|
11
11
|
/* 하위 묶음도 문서와 같은 첫 줄 기준으로 가지 연결 */
|
|
12
12
|
position: relative;
|
|
13
13
|
display: block;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
.
|
|
31
|
+
.wg_navigationNavList__link {
|
|
32
32
|
/* 가로 가지는 링크 첫 줄 기준 · 여러 줄 제목에서도 연결 위치 유지 */
|
|
33
33
|
position: relative;
|
|
34
34
|
display: block;
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
.
|
|
71
|
+
.wg_navigationNavList__link--active {
|
|
72
72
|
color: var(--app-color-link);
|
|
73
73
|
font-weight: var(--app-font-weight-active);
|
|
74
74
|
|
|
@@ -77,13 +77,21 @@
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
.
|
|
80
|
+
.wg_navigationNavList__item {
|
|
81
81
|
/* 하위 목록을 포함해 다음 형제까지 줄기 연결 */
|
|
82
82
|
display: flow-root;
|
|
83
83
|
|
|
84
84
|
/* 세로 줄기는 각 항목 기준 · 마지막 항목의 첫 줄 가운데에서 종료 */
|
|
85
85
|
position: relative;
|
|
86
86
|
|
|
87
|
+
&:has(> .wg_navigationBranchToggle__root) > .wg_navigationNavList__link {
|
|
88
|
+
padding-inline-end: calc(var(--app-size-tree-toggle) + var(--app-space-inline));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&:has(> .wg_navigationBranchToggle__root) > .wg_navigationNavList__branch {
|
|
92
|
+
padding-inline-end: calc(var(--app-size-tree-toggle) + var(--app-space-inline));
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
&::before {
|
|
88
96
|
position: absolute;
|
|
89
97
|
top: 0;
|
|
@@ -119,53 +127,53 @@
|
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
/* 뒤 항목까지의 세로 경로 · 같은 목록의 형제 안에서만 연결 */
|
|
122
|
-
&:where(:has(~ .
|
|
130
|
+
&:where(:has(~ .wg_navigationNavList__item:hover))::before {
|
|
123
131
|
border-left-color: var(--app-color-link);
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
&:where(:has(~ .
|
|
134
|
+
&:where(:has(~ .wg_navigationNavList__item :focus-visible))::before {
|
|
127
135
|
border-left-color: var(--app-color-link);
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
& > .
|
|
138
|
+
& > .wg_navigationNavList__root {
|
|
131
139
|
margin-left: var(--app-space-tree-indent);
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
/* 하위 문서를 탐색할 때 조상 묶음의 가지도 함께 연결 */
|
|
135
|
-
&:where(:hover) > .
|
|
143
|
+
&:where(:hover) > .wg_navigationNavList__branch::before {
|
|
136
144
|
border-top-color: var(--app-color-link);
|
|
137
145
|
}
|
|
138
146
|
|
|
139
|
-
&:where(:has(:focus-visible)) > .
|
|
147
|
+
&:where(:has(:focus-visible)) > .wg_navigationNavList__branch::before {
|
|
140
148
|
border-top-color: var(--app-color-link);
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/* 자식 문서를 탐색할 때 클릭 가능한 부모 문서의 가지도 연결 */
|
|
144
|
-
&:where(:hover) > .
|
|
152
|
+
&:where(:hover) > .wg_navigationNavList__link::before {
|
|
145
153
|
border-top-color: var(--app-color-link);
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
&:where(:has(:focus-visible)) > .
|
|
156
|
+
&:where(:has(:focus-visible)) > .wg_navigationNavList__link::before {
|
|
149
157
|
border-top-color: var(--app-color-link);
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
.
|
|
161
|
+
.wg_navigationNavList__item--trail {
|
|
154
162
|
&::before {
|
|
155
163
|
border-left-color: var(--app-color-link);
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
.
|
|
167
|
+
.wg_navigationNavList__item--current {
|
|
160
168
|
&::after {
|
|
161
169
|
border-left-color: var(--app-color-link);
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
& > .
|
|
172
|
+
& > .wg_navigationNavList__branch::before {
|
|
165
173
|
border-top-color: var(--app-color-link);
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
& > .
|
|
176
|
+
& > .wg_navigationNavList__link::before {
|
|
169
177
|
border-top-color: var(--app-color-link);
|
|
170
178
|
}
|
|
171
179
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import {Fragment} from "hono/jsx";
|
|
3
|
+
import {toNavigationBranchId} from "@/component/widget/navigation/_function/to-navigation-branch-id";
|
|
4
|
+
import type {NavigationGroup} from "@/component/widget/navigation/_type/navigation-data";
|
|
5
|
+
import {WgNavigationBranchToggle} from "@/component/widget/navigation/_wg-navigation-branch-toggle";
|
|
6
|
+
import "./_wg-navigation-nav-list.css";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 한 단계의 문서 목록과 하위 묶음 · 재귀에서도 같은 탐색 표현 유지
|
|
10
|
+
*/
|
|
11
|
+
export interface WgNavigationNavListProps {
|
|
12
|
+
/**
|
|
13
|
+
* 현재 단계의 문서와 하위 묶음
|
|
14
|
+
*/
|
|
15
|
+
group: NavigationGroup;
|
|
16
|
+
/**
|
|
17
|
+
* 사용자가 접은 가지
|
|
18
|
+
*/
|
|
19
|
+
collapsed: Record<string, boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* 브라우저에서 가지 선택 변경
|
|
22
|
+
*/
|
|
23
|
+
onToggle?: (key: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* 부모 버튼과 연결할 가지 키 · 루트 묶음·문서 모두 같은 규칙 사용
|
|
26
|
+
*/
|
|
27
|
+
branchKey: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const WgNavigationNavList = (props: WgNavigationNavListProps) => {
|
|
31
|
+
const entries = [
|
|
32
|
+
...props.group.docs.map((doc) => {
|
|
33
|
+
const active = doc.active;
|
|
34
|
+
return {kind: "document" as const, key: `doc:${doc.id}`, doc, active, current: doc.current};
|
|
35
|
+
}),
|
|
36
|
+
...props.group.groups.map((group) => ({
|
|
37
|
+
kind: "group" as const,
|
|
38
|
+
key: `group:${JSON.stringify(group.path)}`,
|
|
39
|
+
group,
|
|
40
|
+
current: group.current,
|
|
41
|
+
})),
|
|
42
|
+
];
|
|
43
|
+
const currentIndex = entries.findIndex((entry) => entry.current);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ul
|
|
47
|
+
className={clsx("wg_navigationNavList__root")}
|
|
48
|
+
aria-label={props.group.name}
|
|
49
|
+
id={toNavigationBranchId(props.branchKey)}
|
|
50
|
+
hidden={props.collapsed[props.branchKey] === true}
|
|
51
|
+
>
|
|
52
|
+
{/**
|
|
53
|
+
* 현재 경로 앞의 줄기와 도착 가지 · 문서 링크는 원래 파일 URL 사용
|
|
54
|
+
*/}
|
|
55
|
+
{entries.map((entry, index) => (
|
|
56
|
+
<li
|
|
57
|
+
className={clsx("wg_navigationNavList__item", {
|
|
58
|
+
"wg_navigationNavList__item--trail": index < currentIndex,
|
|
59
|
+
"wg_navigationNavList__item--current": entry.current,
|
|
60
|
+
})}
|
|
61
|
+
key={entry.key}
|
|
62
|
+
>
|
|
63
|
+
{/**
|
|
64
|
+
* 문서 링크 · 부모도 독립 페이지, 현재 한 문서에만 페이지 상태 부여
|
|
65
|
+
*/}
|
|
66
|
+
{entry.kind === "document" && (
|
|
67
|
+
<Fragment>
|
|
68
|
+
<a
|
|
69
|
+
className={clsx("wg_navigationNavList__link", {"wg_navigationNavList__link--active": entry.active})}
|
|
70
|
+
href={`/${entry.doc.id}/`}
|
|
71
|
+
title={entry.doc.label}
|
|
72
|
+
aria-current={entry.active ? "page" : undefined}
|
|
73
|
+
>
|
|
74
|
+
{entry.doc.name}
|
|
75
|
+
</a>
|
|
76
|
+
{/**
|
|
77
|
+
* 하위 문서 · 부모 아래에서도 기존 목록의 들여쓰기와 키보드 이동 유지
|
|
78
|
+
*/}
|
|
79
|
+
{entry.doc.children.length > 0 && (
|
|
80
|
+
<WgNavigationBranchToggle branchKey={entry.key} label={entry.doc.name} collapsed={props.collapsed} onToggle={props.onToggle} />
|
|
81
|
+
)}
|
|
82
|
+
{entry.doc.children.length > 0 && (
|
|
83
|
+
<WgNavigationNavList
|
|
84
|
+
group={{name: entry.doc.name, path: props.group.path, current: entry.current, docs: entry.doc.children, groups: []}}
|
|
85
|
+
collapsed={props.collapsed}
|
|
86
|
+
onToggle={props.onToggle}
|
|
87
|
+
branchKey={entry.key}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</Fragment>
|
|
91
|
+
)}
|
|
92
|
+
{/**
|
|
93
|
+
* 하위 묶음 · 같은 목록을 재귀로 이어 깊이에 따라 들여쓰기
|
|
94
|
+
*/}
|
|
95
|
+
{entry.kind === "group" && (
|
|
96
|
+
<Fragment>
|
|
97
|
+
<span className={clsx("wg_navigationNavList__branch")}>{entry.group.name}</span>
|
|
98
|
+
<WgNavigationBranchToggle branchKey={entry.key} label={entry.group.name} collapsed={props.collapsed} onToggle={props.onToggle} />
|
|
99
|
+
<WgNavigationNavList group={entry.group} collapsed={props.collapsed} onToggle={props.onToggle} branchKey={entry.key} />
|
|
100
|
+
</Fragment>
|
|
101
|
+
)}
|
|
102
|
+
</li>
|
|
103
|
+
))}
|
|
104
|
+
</ul>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 읽는 순서의 문서 목록 · 작은 회색 묶음과 링크색 문서
|
|
3
3
|
*/
|
|
4
|
-
.
|
|
4
|
+
.wg_navigationNav__root {
|
|
5
5
|
font-size: var(--app-font-size-dense);
|
|
6
6
|
line-height: var(--app-line-height-tight);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
.
|
|
9
|
+
.wg_navigationNav__docs {
|
|
10
10
|
display: flex;
|
|
11
11
|
flex-direction: column;
|
|
12
12
|
gap: var(--app-space-stack);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
.
|
|
15
|
+
.wg_navigationNav__group {
|
|
16
|
+
padding-inline-end: calc(var(--app-size-tree-toggle) + var(--app-space-inline));
|
|
16
17
|
margin: 0 0 4px;
|
|
17
18
|
color: var(--app-color-text-muted);
|
|
18
19
|
font-size: var(--app-font-size-group);
|
|
19
20
|
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
20
21
|
text-box: trim-start cap alphabetic;
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
.wg_navigationNav__docGroup {
|
|
25
|
+
/* 묶음 접기 버튼의 첫 행 기준 */
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type {NavigationGroup} from "@/component/widget/navigation/_type/navigation-data";
|
|
3
|
+
import {WgNavigationBranchToggle} from "@/component/widget/navigation/_wg-navigation-branch-toggle";
|
|
4
|
+
import {WgNavigationNavList} from "@/component/widget/navigation/_wg-navigation-nav-list";
|
|
5
|
+
import {copy_nav_aria_label} from "@/constant/copy";
|
|
6
|
+
import "./_wg-navigation-nav.css";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 서버·브라우저가 공유하는 문서 트리
|
|
10
|
+
*/
|
|
11
|
+
export interface WgNavigationNavProps {
|
|
12
|
+
/**
|
|
13
|
+
* 본문을 제외한 읽는 순서의 문서 묶음
|
|
14
|
+
*/
|
|
15
|
+
groups: NavigationGroup[];
|
|
16
|
+
/**
|
|
17
|
+
* 사용자가 접은 가지
|
|
18
|
+
*/
|
|
19
|
+
collapsed: Record<string, boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* 브라우저에서 가지 선택 변경 · 서버 출력에서는 생략
|
|
22
|
+
*/
|
|
23
|
+
onToggle?: (key: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const WgNavigationNav = (props: WgNavigationNavProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<nav className={clsx("wg_navigationNav__root")} aria-label={copy_nav_aria_label}>
|
|
29
|
+
{/**
|
|
30
|
+
* 문서 이동 · 클릭하지 않는 묶음과 독립 문서
|
|
31
|
+
*/}
|
|
32
|
+
<div className={clsx("wg_navigationNav__docs")}>
|
|
33
|
+
{props.groups.map((group) => (
|
|
34
|
+
<section className={clsx("wg_navigationNav__docGroup")} key={JSON.stringify(group.path)} aria-label={group.name}>
|
|
35
|
+
<div className={clsx("wg_navigationNav__group")}>{group.name}</div>
|
|
36
|
+
<WgNavigationBranchToggle branchKey={`group:${JSON.stringify(group.path)}`} label={group.name} collapsed={props.collapsed} onToggle={props.onToggle} />
|
|
37
|
+
<WgNavigationNavList group={group} branchKey={`group:${JSON.stringify(group.path)}`} collapsed={props.collapsed} onToggle={props.onToggle} />
|
|
38
|
+
</section>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</nav>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import {type Child, type DOMAttributes, type RefObject, useLayoutEffect, useRef, useState} from "hono/jsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 탐색 좌표를 제어하는 부모와 넘침 표시를 소유하는 스크롤 영역의 경계
|
|
6
|
+
*/
|
|
7
|
+
export interface WgNavigationScrollProps {
|
|
8
|
+
/**
|
|
9
|
+
* 배치별 좌표 복원과 저장에 필요한 실제 스크롤 요소
|
|
10
|
+
*/
|
|
11
|
+
portRef: RefObject<HTMLDivElement | null>;
|
|
12
|
+
/**
|
|
13
|
+
* 문서 목록과 현재 페이지 목차의 독립된 스크롤 수명
|
|
14
|
+
*/
|
|
15
|
+
kind: "documents" | "outline";
|
|
16
|
+
/**
|
|
17
|
+
* JavaScript가 연결된 뒤에만 손잡이를 넘침 표시로 대체
|
|
18
|
+
*/
|
|
19
|
+
ready: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 탐색 링크를 포함한 영역 내용
|
|
22
|
+
*/
|
|
23
|
+
children: Child;
|
|
24
|
+
/**
|
|
25
|
+
* 이동 직전 좌표 보존과 모바일 모달 닫기
|
|
26
|
+
*/
|
|
27
|
+
onClick?: DOMAttributes["onClick"];
|
|
28
|
+
/**
|
|
29
|
+
* 실제 스크롤 좌표가 바뀔 때 부모 저장소에 반영
|
|
30
|
+
*/
|
|
31
|
+
onScroll?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const WgNavigationScroll = (props: WgNavigationScrollProps) => {
|
|
35
|
+
const [overflow, setOverflow] = useState({above: false, below: false});
|
|
36
|
+
const frameRef = useRef(0);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 소수 픽셀 반올림을 제외한 실제 남은 내용만 위·아래 흐림으로 알림
|
|
40
|
+
*/
|
|
41
|
+
const updateOverflow = () => {
|
|
42
|
+
frameRef.current = 0;
|
|
43
|
+
if (props.portRef.current === null) return;
|
|
44
|
+
const above = props.portRef.current.scrollTop > 1;
|
|
45
|
+
const below = props.portRef.current.scrollHeight - props.portRef.current.clientHeight - props.portRef.current.scrollTop > 1;
|
|
46
|
+
setOverflow((state) => (state.above === above && state.below === below ? state : {above, below}));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 연속 스크롤과 관찰자 알림은 프레임당 한 번 측정
|
|
51
|
+
*/
|
|
52
|
+
const scheduleOverflow = () => {
|
|
53
|
+
if (frameRef.current === 0) frameRef.current = requestAnimationFrame(updateOverflow);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 네이티브 좌표는 바로 저장하고 시각적 넘침만 프레임에 맞춰 갱신
|
|
58
|
+
*/
|
|
59
|
+
const handlePortScroll: DOMAttributes["onScroll"] = (_event) => {
|
|
60
|
+
props.onScroll?.();
|
|
61
|
+
scheduleOverflow();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 영역과 내용의 크기를 함께 추적 · Hono 최초 mount의 분리된 fragment 측정은 연결 직후 실행
|
|
66
|
+
*/
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (!props.ready || props.portRef.current === null) return;
|
|
69
|
+
const observer = new ResizeObserver(scheduleOverflow);
|
|
70
|
+
observer.observe(props.portRef.current);
|
|
71
|
+
if (props.portRef.current.firstElementChild !== null) observer.observe(props.portRef.current.firstElementChild);
|
|
72
|
+
let active = true;
|
|
73
|
+
addEventListener("pageshow", scheduleOverflow);
|
|
74
|
+
if (props.portRef.current.isConnected) {
|
|
75
|
+
updateOverflow();
|
|
76
|
+
} else {
|
|
77
|
+
// Hono는 최초 layout effect를 fragment에서 실행하므로 같은 script의 paint 전 microtask에서 측정한다
|
|
78
|
+
queueMicrotask(() => {
|
|
79
|
+
if (active) updateOverflow();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return () => {
|
|
83
|
+
active = false;
|
|
84
|
+
observer.disconnect();
|
|
85
|
+
removeEventListener("pageshow", scheduleOverflow);
|
|
86
|
+
cancelAnimationFrame(frameRef.current);
|
|
87
|
+
frameRef.current = 0;
|
|
88
|
+
};
|
|
89
|
+
}, [props.portRef, props.ready]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 접힘 선택의 렌더가 바뀐 직후 측정 · 좌표 복원은 부모의 연결 후 처리보다 앞서 저장하지 않음
|
|
93
|
+
*/
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
if (props.ready && props.portRef.current?.isConnected) updateOverflow();
|
|
96
|
+
}, [props.children, props.ready]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: 부모의 링크 이벤트 위임이며 자식 a·button의 네이티브 키보드 동작을 그대로 소비한다
|
|
100
|
+
<div
|
|
101
|
+
ref={props.ready ? props.portRef : undefined}
|
|
102
|
+
className={clsx(
|
|
103
|
+
"wg_navigation__scroll",
|
|
104
|
+
props.kind === "documents" && "wg_navigation__documents",
|
|
105
|
+
props.kind === "outline" && "wg_navigation__headings",
|
|
106
|
+
props.ready && "wg_navigation__scroll--ready",
|
|
107
|
+
props.ready && overflow.above && "wg_navigation__scroll--above",
|
|
108
|
+
props.ready && overflow.below && "wg_navigation__scroll--below",
|
|
109
|
+
)}
|
|
110
|
+
data-navigation-scroll={props.kind}
|
|
111
|
+
onClick={props.onClick}
|
|
112
|
+
onScroll={handlePortScroll}
|
|
113
|
+
>
|
|
114
|
+
{props.children}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* 와이드 화면의 오른쪽 목차 · 회색 제목·묶음과 본문 글색의 이동 대상 구분
|
|
3
3
|
*/
|
|
4
|
-
.
|
|
5
|
-
margin-top: var(--app-space-group);
|
|
6
|
-
padding-top: var(--app-space-group);
|
|
7
|
-
border-top: 1px solid var(--app-color-border-soft);
|
|
4
|
+
.wg_navigationToc__root {
|
|
8
5
|
font-size: var(--app-font-size-dense);
|
|
9
6
|
line-height: var(--app-line-height-tight);
|
|
10
7
|
}
|
|
11
8
|
|
|
12
|
-
.
|
|
9
|
+
.wg_navigationToc__label {
|
|
13
10
|
margin-bottom: var(--app-space-inline);
|
|
14
11
|
color: var(--app-color-text-muted);
|
|
15
12
|
font-size: var(--app-font-size-label);
|
|
@@ -17,20 +14,31 @@
|
|
|
17
14
|
letter-spacing: var(--app-letter-spacing-label);
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
.
|
|
21
|
-
margin:
|
|
17
|
+
.wg_navigationToc__group {
|
|
18
|
+
margin: 0 0 4px;
|
|
19
|
+
padding-inline-end: calc(var(--app-size-tree-toggle) + var(--app-space-inline));
|
|
22
20
|
color: var(--app-color-text-muted);
|
|
23
21
|
font-size: var(--app-font-size-group);
|
|
24
22
|
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
.
|
|
25
|
+
.wg_navigationToc__groupSection {
|
|
26
|
+
/* 가름 제목과 그 하위 목록을 하나의 접힘 대상으로 연결 */
|
|
27
|
+
position: relative;
|
|
28
|
+
display: flow-root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wg_navigationToc__groupSection--named {
|
|
32
|
+
margin-top: var(--app-space-stack);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.wg_navigationToc__list {
|
|
28
36
|
margin: 0 0 0 var(--app-space-tree-inset);
|
|
29
37
|
padding: 0;
|
|
30
38
|
list-style: none;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
.
|
|
41
|
+
.wg_navigationToc__link {
|
|
34
42
|
/* 가로 가지는 링크 첫 줄 기준 · 여러 줄 제목에서도 연결 위치 유지 */
|
|
35
43
|
position: relative;
|
|
36
44
|
display: block;
|
|
@@ -71,7 +79,7 @@
|
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
.
|
|
82
|
+
.wg_navigationToc__link--active {
|
|
75
83
|
color: var(--app-color-text-strong);
|
|
76
84
|
font-weight: var(--app-font-weight-active);
|
|
77
85
|
|
|
@@ -80,13 +88,17 @@
|
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
.
|
|
91
|
+
.wg_navigationToc__item {
|
|
84
92
|
/* 하위 목록의 margin을 포함해 다음 항목까지 줄기 연결 */
|
|
85
93
|
display: flow-root;
|
|
86
94
|
|
|
87
95
|
/* 세로 줄기는 항목 전체 기준 · 중간 항목의 소제목 옆에서도 연결 */
|
|
88
96
|
position: relative;
|
|
89
97
|
|
|
98
|
+
&:has(> .wg_navigationBranchToggle__root) > .wg_navigationToc__link {
|
|
99
|
+
padding-inline-end: calc(var(--app-size-tree-toggle) + var(--app-space-inline));
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
&::before {
|
|
91
103
|
position: absolute;
|
|
92
104
|
top: 0;
|
|
@@ -122,43 +134,48 @@
|
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
/* 뒤 항목까지의 세로 경로 · 같은 목록의 형제 안에서만 연결 */
|
|
125
|
-
&:where(:has(~ .
|
|
137
|
+
&:where(:has(~ .wg_navigationToc__item:hover))::before {
|
|
126
138
|
border-left-color: var(--app-color-text-strong);
|
|
127
139
|
}
|
|
128
140
|
|
|
129
|
-
&:where(:has(~ .
|
|
141
|
+
&:where(:has(~ .wg_navigationToc__item :focus-visible))::before {
|
|
130
142
|
border-left-color: var(--app-color-text-strong);
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
/* 하위 제목을 탐색할 때 상위 절의 가지도 연결 */
|
|
134
|
-
&:where(:hover) > .
|
|
146
|
+
&:where(:hover) > .wg_navigationToc__link::before {
|
|
135
147
|
border-top-color: var(--app-color-text-strong);
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
&:where(:has(:focus-visible)) > .
|
|
150
|
+
&:where(:has(:focus-visible)) > .wg_navigationToc__link::before {
|
|
139
151
|
border-top-color: var(--app-color-text-strong);
|
|
140
152
|
}
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
.
|
|
155
|
+
.wg_navigationToc__item--trail {
|
|
144
156
|
&::before {
|
|
145
157
|
border-left-color: var(--app-color-text-strong);
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
.
|
|
161
|
+
.wg_navigationToc__item--current {
|
|
150
162
|
&::after {
|
|
151
163
|
border-left-color: var(--app-color-text-strong);
|
|
152
164
|
}
|
|
165
|
+
|
|
166
|
+
/* 현재 소제목의 상위 절은 글 강조 없이 경로의 가로 가지 연결 */
|
|
167
|
+
& > .wg_navigationToc__link::before {
|
|
168
|
+
border-top-color: var(--app-color-text-strong);
|
|
169
|
+
}
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
.
|
|
172
|
+
.wg_navigationToc__sub {
|
|
156
173
|
margin: var(--app-space-tree-row) 0 4px var(--app-space-tree-indent);
|
|
157
174
|
padding: 0;
|
|
158
175
|
list-style: none;
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
.
|
|
178
|
+
.wg_navigationToc__subItem {
|
|
162
179
|
/* 하위 줄기는 소제목 항목 기준 · 마지막 소제목 첫 줄에서 종료 */
|
|
163
180
|
position: relative;
|
|
164
181
|
|
|
@@ -197,28 +214,28 @@
|
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
/* 뒤 항목까지의 세로 경로 · 같은 목록의 형제 안에서만 연결 */
|
|
200
|
-
&:where(:has(~ .
|
|
217
|
+
&:where(:has(~ .wg_navigationToc__subItem:hover))::before {
|
|
201
218
|
border-left-color: var(--app-color-text-strong);
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
&:where(:has(~ .
|
|
221
|
+
&:where(:has(~ .wg_navigationToc__subItem :focus-visible))::before {
|
|
205
222
|
border-left-color: var(--app-color-text-strong);
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
|
|
209
|
-
.
|
|
226
|
+
.wg_navigationToc__subItem--trail {
|
|
210
227
|
&::before {
|
|
211
228
|
border-left-color: var(--app-color-text-strong);
|
|
212
229
|
}
|
|
213
230
|
}
|
|
214
231
|
|
|
215
|
-
.
|
|
232
|
+
.wg_navigationToc__subItem--current {
|
|
216
233
|
&::after {
|
|
217
234
|
border-left-color: var(--app-color-text-strong);
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
237
|
|
|
221
|
-
.
|
|
238
|
+
.wg_navigationToc__subLink {
|
|
222
239
|
/* 하위 가지도 링크 첫 줄 기준 · 줄바꿈된 제목 옆으로 선이 늘어나지 않음 */
|
|
223
240
|
position: relative;
|
|
224
241
|
display: block;
|
|
@@ -259,7 +276,7 @@
|
|
|
259
276
|
}
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
.
|
|
279
|
+
.wg_navigationToc__subLink--active {
|
|
263
280
|
color: var(--app-color-text-strong);
|
|
264
281
|
font-weight: var(--app-font-weight-active);
|
|
265
282
|
|