for-humanity 0.4.3 → 0.4.4
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 +7 -0
- package/README.md +3 -3
- package/dist/cli.css +72 -87
- package/dist/cli.js +68 -4
- package/dist/client.js +6 -5
- package/package.json +1 -1
- package/src/component/widget/shell/_function/bind-mobile-navigation.ts +9 -2
- package/src/component/widget/shell/_function/bind-navigation-overflow.ts +47 -0
- package/src/component/widget/shell/_function/bind-navigation-scroll.ts +43 -82
- package/src/component/widget/shell/_function/restore-navigation-scroll.ts +47 -0
- package/src/component/widget/shell/_wg-shell-toc.css +5 -3
- package/src/component/widget/shell/wg-shell.css +67 -107
- package/src/component/widget/shell/wg-shell.tsx +24 -2
- package/src/style/token.css +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.4 — 2026-10-04
|
|
6
|
+
|
|
7
|
+
- Keep the document list and page outline in separate native scroll areas, so a shorter outline cannot clamp the document list position.
|
|
8
|
+
- Restore desktop document navigation before its first paint and avoid reinserting the same navigation DOM during startup.
|
|
9
|
+
- Indicate remaining navigation content with conditional top and bottom fades, keeping native scrollbars in forced colors and without JavaScript.
|
|
10
|
+
- Connect the active outline branch through its parent while highlighting only the exact current heading.
|
|
11
|
+
|
|
5
12
|
## 0.4.3 — 2026-10-04
|
|
6
13
|
|
|
7
14
|
- Use standard native navigation scrollbars in forced-color mode so browser color adjustment cannot hide custom thumbs against the background.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Write Markdown. Keep the context. Share a static site.
|
|
|
33
33
|
Node.js 22 or later and pnpm. Install the CLI in your document project:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pnpm add -D --save-exact for-humanity@0.4.
|
|
36
|
+
pnpm add -D --save-exact for-humanity@0.4.4
|
|
37
37
|
mkdir docs
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -52,7 +52,7 @@ pnpm exec for-humanity preview docs
|
|
|
52
52
|
|
|
53
53
|
If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
|
|
54
54
|
|
|
55
|
-
Version `0.4.
|
|
55
|
+
Version `0.4.4` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
|
|
56
56
|
Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
|
|
57
57
|
|
|
58
58
|
### Develop the kit
|
|
@@ -74,7 +74,7 @@ pnpm preview # Preview the static site
|
|
|
74
74
|
pnpm build:pages # Add Cloudflare Pages headers and a 404 page
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.4.
|
|
77
|
+
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.4.4` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
|
|
78
78
|
The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
|
|
79
79
|
|
|
80
80
|
## Make it yours
|
package/dist/cli.css
CHANGED
|
@@ -448,9 +448,6 @@
|
|
|
448
448
|
|
|
449
449
|
/* src/component/widget/shell/_wg-shell-toc.css */
|
|
450
450
|
.wg_shellToc__root {
|
|
451
|
-
margin-top: var(--app-space-group);
|
|
452
|
-
padding-top: var(--app-space-group);
|
|
453
|
-
border-top: 1px solid var(--app-color-border-soft);
|
|
454
451
|
font-size: var(--app-font-size-dense);
|
|
455
452
|
line-height: var(--app-line-height-tight);
|
|
456
453
|
}
|
|
@@ -565,6 +562,9 @@
|
|
|
565
562
|
&::after {
|
|
566
563
|
border-left-color: var(--app-color-text-strong);
|
|
567
564
|
}
|
|
565
|
+
& > .wg_shellToc__link::before {
|
|
566
|
+
border-top-color: var(--app-color-text-strong);
|
|
567
|
+
}
|
|
568
568
|
}
|
|
569
569
|
.wg_shellToc__sub {
|
|
570
570
|
margin: var(--app-space-tree-row) 0 4px var(--app-space-tree-indent);
|
|
@@ -754,6 +754,7 @@
|
|
|
754
754
|
--app-size-mobile-header: 56px;
|
|
755
755
|
--app-size-menu-button: 44px;
|
|
756
756
|
--app-size-drawer: 320px;
|
|
757
|
+
--app-size-navigation-fade: 28px;
|
|
757
758
|
--app-space-anchor: 80px;
|
|
758
759
|
--app-radius-pill: 0;
|
|
759
760
|
--app-radius-card: 0;
|
|
@@ -884,34 +885,64 @@
|
|
|
884
885
|
top: 0;
|
|
885
886
|
align-self: start;
|
|
886
887
|
min-width: 0;
|
|
887
|
-
|
|
888
|
-
max-height: 100vh;
|
|
888
|
+
height: 100dvh;
|
|
889
889
|
margin-inline-end: var(--app-space-sidebar-gap);
|
|
890
|
-
overflow-y: auto;
|
|
891
|
-
overscroll-behavior-y: contain;
|
|
892
|
-
scrollbar-gutter: stable;
|
|
893
|
-
scrollbar-width: auto;
|
|
894
|
-
scrollbar-color: auto;
|
|
895
|
-
scroll-padding-block: var(--app-size-brand) var(--app-space-block);
|
|
896
|
-
&::-webkit-scrollbar {
|
|
897
|
-
width: 3px;
|
|
898
|
-
}
|
|
899
|
-
&::-webkit-scrollbar-track {
|
|
900
|
-
background: transparent;
|
|
901
|
-
}
|
|
902
|
-
&::-webkit-scrollbar-thumb {
|
|
903
|
-
border-radius: 3px;
|
|
904
|
-
background: transparent;
|
|
905
|
-
}
|
|
906
|
-
&:hover::-webkit-scrollbar-thumb {
|
|
907
|
-
background: var(--app-color-border-soft);
|
|
908
|
-
}
|
|
909
|
-
&:focus-within::-webkit-scrollbar-thumb {
|
|
910
|
-
background: var(--app-color-border-soft);
|
|
911
|
-
}
|
|
912
890
|
}
|
|
913
891
|
.wg_shell__navigation {
|
|
892
|
+
display: flex;
|
|
893
|
+
flex-direction: column;
|
|
894
|
+
height: calc(100dvh - var(--app-size-brand));
|
|
914
895
|
min-width: 0;
|
|
896
|
+
min-height: 0;
|
|
897
|
+
}
|
|
898
|
+
.wg_shell__documents {
|
|
899
|
+
flex: none;
|
|
900
|
+
max-height: 60%;
|
|
901
|
+
}
|
|
902
|
+
.wg_shell__outline {
|
|
903
|
+
display: flex;
|
|
904
|
+
flex-direction: column;
|
|
905
|
+
min-height: 0;
|
|
906
|
+
margin-top: var(--app-space-group);
|
|
907
|
+
padding-top: var(--app-space-group);
|
|
908
|
+
border-top: 1px solid var(--app-color-border-soft);
|
|
909
|
+
&:has(.wg_shell__headings:empty) {
|
|
910
|
+
display: none;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
.wg_shell__headings {
|
|
914
|
+
min-height: 0;
|
|
915
|
+
}
|
|
916
|
+
.wg_shell__scroll {
|
|
917
|
+
overflow-y: auto;
|
|
918
|
+
overscroll-behavior-y: contain;
|
|
919
|
+
scroll-padding-block: var(--app-size-navigation-fade);
|
|
920
|
+
}
|
|
921
|
+
.wg_shell__scroll--ready {
|
|
922
|
+
scrollbar-width: none;
|
|
923
|
+
}
|
|
924
|
+
.wg_shell__scroll--above {
|
|
925
|
+
mask-image:
|
|
926
|
+
linear-gradient(
|
|
927
|
+
to bottom,
|
|
928
|
+
transparent,
|
|
929
|
+
#000 var(--app-size-navigation-fade));
|
|
930
|
+
}
|
|
931
|
+
.wg_shell__scroll--below {
|
|
932
|
+
mask-image:
|
|
933
|
+
linear-gradient(
|
|
934
|
+
to bottom,
|
|
935
|
+
#000 calc(100% - var(--app-size-navigation-fade)),
|
|
936
|
+
transparent);
|
|
937
|
+
}
|
|
938
|
+
.wg_shell__scroll--above.wg_shell__scroll--below {
|
|
939
|
+
mask-image:
|
|
940
|
+
linear-gradient(
|
|
941
|
+
to bottom,
|
|
942
|
+
transparent,
|
|
943
|
+
#000 var(--app-size-navigation-fade),
|
|
944
|
+
#000 calc(100% - var(--app-size-navigation-fade)),
|
|
945
|
+
transparent);
|
|
915
946
|
}
|
|
916
947
|
.wg_shell__main {
|
|
917
948
|
grid-area: main;
|
|
@@ -934,35 +965,12 @@
|
|
|
934
965
|
border-right: 1px solid var(--app-color-border-layout);
|
|
935
966
|
background: var(--app-color-ground);
|
|
936
967
|
color: var(--app-color-text);
|
|
937
|
-
|
|
938
|
-
scrollbar-width: auto;
|
|
939
|
-
scrollbar-color: auto;
|
|
940
|
-
overscroll-behavior: contain;
|
|
941
|
-
scroll-padding-block: var(--app-size-mobile-header) var(--app-space-group);
|
|
942
|
-
&::-webkit-scrollbar {
|
|
943
|
-
width: 3px;
|
|
944
|
-
}
|
|
945
|
-
&::-webkit-scrollbar-track {
|
|
946
|
-
background: transparent;
|
|
947
|
-
}
|
|
948
|
-
&::-webkit-scrollbar-thumb {
|
|
949
|
-
border-radius: 3px;
|
|
950
|
-
background: transparent;
|
|
951
|
-
}
|
|
952
|
-
&:hover::-webkit-scrollbar-thumb {
|
|
953
|
-
background: var(--app-color-border-soft);
|
|
954
|
-
}
|
|
955
|
-
&:focus-within::-webkit-scrollbar-thumb {
|
|
956
|
-
background: var(--app-color-border-soft);
|
|
957
|
-
}
|
|
968
|
+
overflow: hidden;
|
|
958
969
|
&::backdrop {
|
|
959
970
|
background: var(--app-color-backdrop);
|
|
960
971
|
}
|
|
961
972
|
}
|
|
962
973
|
.wg_shell__drawerHeader {
|
|
963
|
-
position: sticky;
|
|
964
|
-
top: 0;
|
|
965
|
-
z-index: var(--app-z-index-sticky);
|
|
966
974
|
display: flex;
|
|
967
975
|
align-items: center;
|
|
968
976
|
justify-content: space-between;
|
|
@@ -999,6 +1007,10 @@
|
|
|
999
1007
|
}
|
|
1000
1008
|
.wg_shell__drawerContent {
|
|
1001
1009
|
min-width: 0;
|
|
1010
|
+
height: calc(100% - var(--app-size-mobile-header) - var(--app-space-block));
|
|
1011
|
+
& > .wg_shell__navigation {
|
|
1012
|
+
height: 100%;
|
|
1013
|
+
}
|
|
1002
1014
|
}
|
|
1003
1015
|
.wg_shell__cursorFace {
|
|
1004
1016
|
position: fixed;
|
|
@@ -1013,42 +1025,11 @@
|
|
|
1013
1025
|
.wg_shell__cursorFace--visible {
|
|
1014
1026
|
visibility: visible;
|
|
1015
1027
|
}
|
|
1016
|
-
@supports not selector(::-webkit-scrollbar) {
|
|
1017
|
-
.wg_shell__sidebar {
|
|
1018
|
-
scrollbar-width: thin;
|
|
1019
|
-
scrollbar-color: transparent transparent;
|
|
1020
|
-
&:hover {
|
|
1021
|
-
scrollbar-color: var(--app-color-border-soft) transparent;
|
|
1022
|
-
}
|
|
1023
|
-
&:focus-within {
|
|
1024
|
-
scrollbar-color: var(--app-color-border-soft) transparent;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
.wg_shell__drawer {
|
|
1028
|
-
scrollbar-width: thin;
|
|
1029
|
-
scrollbar-color: var(--app-color-border-soft) transparent;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
1028
|
@media (forced-colors: active) {
|
|
1033
|
-
.
|
|
1029
|
+
.wg_shell__scroll {
|
|
1030
|
+
mask-image: none;
|
|
1034
1031
|
scrollbar-width: thin;
|
|
1035
1032
|
scrollbar-color: auto;
|
|
1036
|
-
&:hover {
|
|
1037
|
-
scrollbar-color: auto;
|
|
1038
|
-
}
|
|
1039
|
-
&:focus-within {
|
|
1040
|
-
scrollbar-color: auto;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
.wg_shell__drawer {
|
|
1044
|
-
scrollbar-width: thin;
|
|
1045
|
-
scrollbar-color: auto;
|
|
1046
|
-
&:hover {
|
|
1047
|
-
scrollbar-color: auto;
|
|
1048
|
-
}
|
|
1049
|
-
&:focus-within {
|
|
1050
|
-
scrollbar-color: auto;
|
|
1051
|
-
}
|
|
1052
1033
|
}
|
|
1053
1034
|
}
|
|
1054
1035
|
@media (width < 1024px) {
|
|
@@ -1058,13 +1039,17 @@
|
|
|
1058
1039
|
}
|
|
1059
1040
|
.wg_shell__sidebar {
|
|
1060
1041
|
position: static;
|
|
1061
|
-
|
|
1062
|
-
max-height: none;
|
|
1063
|
-
overflow-y: visible;
|
|
1042
|
+
height: auto;
|
|
1064
1043
|
margin-block-end: var(--app-space-block);
|
|
1065
1044
|
margin-inline-end: 0;
|
|
1066
1045
|
border-bottom: 1px solid var(--app-color-border-layout);
|
|
1067
1046
|
}
|
|
1047
|
+
.wg_shell__sidebar > .wg_shell__navigation {
|
|
1048
|
+
height: auto;
|
|
1049
|
+
& > .wg_shell__documents {
|
|
1050
|
+
max-height: none;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1068
1053
|
.wg_shell__sidebar--navigationReady {
|
|
1069
1054
|
position: sticky;
|
|
1070
1055
|
top: 0;
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,68 @@ var WgProse = (props) => {
|
|
|
33
33
|
// src/component/widget/shell/wg-shell.tsx
|
|
34
34
|
import clsx5 from "clsx";
|
|
35
35
|
|
|
36
|
+
// src/component/widget/shell/_constant/navigation.ts
|
|
37
|
+
var navigation_mobile_query = "(width < 1024px)";
|
|
38
|
+
var navigation_scroll_storage_key = "for-humanity:navigation-scroll";
|
|
39
|
+
|
|
40
|
+
// src/component/widget/shell/_function/bind-navigation-overflow.ts
|
|
41
|
+
var bindNavigationOverflow = () => {
|
|
42
|
+
const ports = [...document.querySelectorAll("[data-navigation-scroll]")];
|
|
43
|
+
const frame = { id: 0 };
|
|
44
|
+
const edgeTolerance = 1;
|
|
45
|
+
const updateOverflow = () => {
|
|
46
|
+
frame.id = 0;
|
|
47
|
+
for (const port of ports) {
|
|
48
|
+
port.classList.add("wg_shell__scroll--ready");
|
|
49
|
+
port.classList.toggle("wg_shell__scroll--above", port.scrollTop > edgeTolerance);
|
|
50
|
+
port.classList.toggle("wg_shell__scroll--below", port.scrollHeight - port.clientHeight - port.scrollTop > edgeTolerance);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const scheduleOverflow = () => {
|
|
54
|
+
if (frame.id === 0) {
|
|
55
|
+
frame.id = requestAnimationFrame(updateOverflow);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const observer = new ResizeObserver(scheduleOverflow);
|
|
59
|
+
for (const port of ports) {
|
|
60
|
+
port.addEventListener("scroll", scheduleOverflow, { passive: true });
|
|
61
|
+
observer.observe(port);
|
|
62
|
+
if (port.firstElementChild) {
|
|
63
|
+
observer.observe(port.firstElementChild);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
addEventListener("pageshow", scheduleOverflow);
|
|
67
|
+
updateOverflow();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/component/widget/shell/_function/restore-navigation-scroll.ts
|
|
71
|
+
var restoreNavigationScroll = (storageKey) => {
|
|
72
|
+
const navigation = document.querySelector("[data-navigation]");
|
|
73
|
+
const documents = document.querySelector('[data-navigation-scroll="documents"]');
|
|
74
|
+
const outline = document.querySelector('[data-navigation-scroll="outline"]');
|
|
75
|
+
if (!navigation || !documents || !outline) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const value = sessionStorage.getItem(storageKey);
|
|
80
|
+
if (value === null) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const stored = JSON.parse(value);
|
|
84
|
+
const signature = [...navigation.querySelectorAll(".wg_shellNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
|
|
85
|
+
if (typeof stored !== "object" || stored === null || !("navigation" in stored) || stored.navigation !== signature) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if ("documents" in stored && typeof stored.documents === "number" && Number.isFinite(stored.documents) && stored.documents >= 0) {
|
|
89
|
+
documents.scrollTop = stored.documents;
|
|
90
|
+
}
|
|
91
|
+
if ("pathname" in stored && stored.pathname === location.pathname && "outline" in stored && typeof stored.outline === "number" && Number.isFinite(stored.outline) && stored.outline >= 0) {
|
|
92
|
+
outline.scrollTop = stored.outline;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
36
98
|
// src/component/widget/shell/_function/to-toc-groups.ts
|
|
37
99
|
import { takeWhile } from "es-toolkit/array";
|
|
38
100
|
|
|
@@ -263,6 +325,7 @@ var asset_media_extensions = /* @__PURE__ */ new Set([".svg", ".png", ".jpg", ".
|
|
|
263
325
|
// src/component/widget/shell/wg-shell.tsx
|
|
264
326
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
265
327
|
var reloadScript = `new EventSource(${JSON.stringify(asset_reload_path)}).onmessage=function(){location.reload()}`;
|
|
328
|
+
var navigationScript = `(${bindNavigationOverflow.toString()})();if(!matchMedia(${JSON.stringify(navigation_mobile_query)}).matches){(${restoreNavigationScroll.toString()})(${JSON.stringify(`${navigation_scroll_storage_key}:desktop`)})}`;
|
|
266
329
|
var WgShell = (props) => {
|
|
267
330
|
const tocGroups = props.outline === void 0 ? [] : toTocGroups(props.outline);
|
|
268
331
|
return /* @__PURE__ */ jsxs5("html", { lang: "ko", children: [
|
|
@@ -296,10 +359,11 @@ var WgShell = (props) => {
|
|
|
296
359
|
)
|
|
297
360
|
] }),
|
|
298
361
|
/* @__PURE__ */ jsxs5("div", { className: clsx5("wg_shell__navigation"), "data-navigation": "", children: [
|
|
299
|
-
/* @__PURE__ */ jsx5(WgShellNav, { site: props.site, docs: props.docs, current: props.current }),
|
|
300
|
-
tocGroups.length > 0 && /* @__PURE__ */ jsx5(WgShellToc, { groups: tocGroups })
|
|
362
|
+
/* @__PURE__ */ jsx5("div", { className: clsx5("wg_shell__documents", "wg_shell__scroll"), "data-navigation-scroll": "documents", children: /* @__PURE__ */ jsx5(WgShellNav, { site: props.site, docs: props.docs, current: props.current }) }),
|
|
363
|
+
/* @__PURE__ */ jsx5("div", { className: clsx5("wg_shell__outline"), children: /* @__PURE__ */ jsx5("div", { className: clsx5("wg_shell__headings", "wg_shell__scroll"), "data-navigation-scroll": "outline", children: tocGroups.length > 0 && /* @__PURE__ */ jsx5(WgShellToc, { groups: tocGroups }) }) })
|
|
301
364
|
] })
|
|
302
365
|
] }),
|
|
366
|
+
/* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: navigationScript } }),
|
|
303
367
|
/* @__PURE__ */ jsx5("main", { className: clsx5("wg_shell__main"), children: props.children }),
|
|
304
368
|
/* @__PURE__ */ jsxs5("dialog", { className: clsx5("wg_shell__drawer"), id: "fh-navigation-drawer", "aria-labelledby": "fh-navigation-title", "data-navigation-drawer": "", children: [
|
|
305
369
|
/* @__PURE__ */ jsxs5("div", { className: clsx5("wg_shell__drawerHeader"), children: [
|
|
@@ -1018,8 +1082,8 @@ var rehypeAssets = (options) => (tree, file) => {
|
|
|
1018
1082
|
return;
|
|
1019
1083
|
}
|
|
1020
1084
|
const target = resolve2(dirname2(file.path), path);
|
|
1021
|
-
const
|
|
1022
|
-
if (
|
|
1085
|
+
const location2 = relative2(options.root, target);
|
|
1086
|
+
if (location2 === ".." || location2.startsWith(`..${sep}`) || isAbsolute(location2)) {
|
|
1023
1087
|
file.fail(`${copy_error_asset}: \uBB38\uC11C \uD3F4\uB354 \uBC16 \xB7 ${url}`, node);
|
|
1024
1088
|
}
|
|
1025
1089
|
if (!existsSync2(target)) {
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)
|
|
3
|
-
|
|
4
|
-
${Math.max(0,Math.min(t.
|
|
1
|
+
var E="fh-cursor-face";var b="(width < 1024px)",S="for-humanity:navigation-scroll";var v=e=>{if(!e)return null;try{return document.getElementById(decodeURIComponent(e.slice(1)))}catch(t){if(t instanceof URIError)return null;throw t}};var p=e=>{let t=v(e);for(let o=t?.closest("details");o;o=o.parentElement?.closest("details"))o.open=!0;return t};var q=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector("[data-navigation-drawer]"),o=document.querySelector("[data-navigation-content]"),r=document.querySelector("[data-navigation-open]"),c=document.querySelector("[data-navigation-close]"),n=e?.parentElement;if(!e||!t||!o||!r||!c||!n)return;let l=matchMedia(b),s=()=>{t.open||(document.body.classList.remove("wg_shell__root--drawerOpen"),r.setAttribute("aria-expanded","false"))},u=()=>{t.close(),s()},g=()=>{!l.matches||t.open||(t.showModal(),document.body.classList.add("wg_shell__root--drawerOpen"),r.setAttribute("aria-expanded","true"))},a=i=>{let d=t.getBoundingClientRect();i.target===t&&(i.clientX<d.left||i.clientX>d.right||i.clientY<d.top||i.clientY>d.bottom)&&u()},y=i=>{if(!t.open||i.defaultPrevented||i.button!==0||i.metaKey||i.ctrlKey||i.shiftKey||i.altKey||!(i.target instanceof Element))return;let d=i.target.closest("a[href]");if(!(!(d instanceof HTMLAnchorElement)||d.target!==""&&d.target!=="_self"||d.hasAttribute("download"))&&(u(),d.origin===location.origin&&d.pathname===location.pathname&&d.search===location.search&&d.hash)){let f=p(d.hash);f&&(f.tabIndex=-1,f.focus({preventScroll:!0}))}},m=()=>{let i=document.activeElement;if(l.matches){r.classList.add("wg_shell__menuToggle--visible"),e.parentElement!==o&&o.append(e),i instanceof HTMLElement&&e.contains(i)&&r.focus({preventScroll:!0});return}u(),e.parentElement!==n&&n.append(e),i instanceof HTMLElement&&e.contains(i)?i.focus({preventScroll:!0}):(i===r||i instanceof HTMLElement&&t.contains(i))&&n.querySelector(".wg_shell__brand")?.focus({preventScroll:!0}),r.classList.remove("wg_shell__menuToggle--visible")};r.addEventListener("click",g),c.addEventListener("click",u),t.addEventListener("close",s),t.addEventListener("click",a),e.addEventListener("click",y),l.addEventListener("change",m),m(),n.classList.add("wg_shell__sidebar--navigationReady")};var C=e=>{let t=document.querySelector("[data-navigation]"),o=document.querySelector('[data-navigation-scroll="documents"]'),r=document.querySelector('[data-navigation-scroll="outline"]');if(!(!t||!o||!r))try{let c=sessionStorage.getItem(e);if(c===null)return;let n=JSON.parse(c),l=[...t.querySelectorAll(".wg_shellNav__root a[href]")].map(s=>`${s.pathname}:${s.textContent}`).join(`
|
|
2
|
+
`);if(typeof n!="object"||n===null||!("navigation"in n)||n.navigation!==l)return;"documents"in n&&typeof n.documents=="number"&&Number.isFinite(n.documents)&&n.documents>=0&&(o.scrollTop=n.documents),"pathname"in n&&n.pathname===location.pathname&&"outline"in n&&typeof n.outline=="number"&&Number.isFinite(n.outline)&&n.outline>=0&&(r.scrollTop=n.outline)}catch{}};var N=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector('[data-navigation-scroll="documents"]'),o=document.querySelector('[data-navigation-scroll="outline"]'),r=document.querySelector("[data-navigation-drawer]"),c=document.querySelector("[data-navigation-open]");if(!e||!t||!o||!r||!c)return;let n=matchMedia(b),l={mobile:n.matches},s=[t,o],u=new Map,g=[...e.querySelectorAll(".wg_shellNav__root a[href]")].map(m=>`${m.pathname}:${m.textContent}`).join(`
|
|
3
|
+
`),a=()=>{if(l.mobile!==n.matches||n.matches&&!r.open)return;let m=`${S}:${n.matches?"drawer":"desktop"}`;u.set(m,new Map(s.map(i=>[i,i.scrollTop])));try{sessionStorage.setItem(m,JSON.stringify({navigation:g,pathname:location.pathname,documents:t.scrollTop,outline:o.scrollTop}))}catch{}},y=()=>{if(l.mobile=n.matches,n.matches&&!r.open)return;let m=`${S}:${n.matches?"drawer":"desktop"}`,i=u.get(m);if(i===void 0)C(m);else for(let d of s){let f=i.get(d);f!==void 0&&(d.scrollTop=f)}a()};for(let m of s)m.addEventListener("scroll",a,{passive:!0});e.addEventListener("click",a,{capture:!0}),r.addEventListener("cancel",a),r.querySelector("[data-navigation-close]")?.addEventListener("click",a,{capture:!0}),r.addEventListener("click",a,{capture:!0}),c.addEventListener("click",y),n.addEventListener("change",y),addEventListener("pagehide",a),a()};q();N();var h=document.querySelector("[data-cursor-face]");if(h){let e=matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"),t={x:0,y:0,frame:0},o=()=>{cancelAnimationFrame(t.frame),t.frame=0,h.classList.remove("wg_shell__cursorFace--visible")},r=()=>{e.matches||o()},c=()=>{t.frame===0&&(t.frame=requestAnimationFrame(()=>{t.frame=0,h.style.transform=`translate3d(
|
|
4
|
+
${Math.max(0,Math.min(t.x+16,document.documentElement.clientWidth-h.width-16))}px,
|
|
5
|
+
${Math.max(0,Math.min(t.y+16,document.documentElement.clientHeight-h.height-16))}px,
|
|
5
6
|
0
|
|
6
|
-
)`,
|
|
7
|
+
)`,h.classList.add("wg_shell__cursorFace--visible")}))},n=g=>{if(!(e.matches&&g.pointerType==="mouse")){o();return}t.x=g.clientX,t.y=g.clientY,c()},l=()=>{h.classList.contains("wg_shell__cursorFace--visible")&&c()},s=()=>{if(h.classList.contains("wg_shell__cursorFace--visible"))try{sessionStorage.setItem(E,JSON.stringify({x:t.x,y:t.y}))}catch{}},u=()=>{try{let g=sessionStorage.getItem(E);if(sessionStorage.removeItem(E),g!==null&&e.matches){let a=JSON.parse(g);typeof a=="object"&&a!==null&&"x"in a&&"y"in a&&typeof a.x=="number"&&typeof a.y=="number"&&Number.isFinite(a.x)&&Number.isFinite(a.y)&&(t.x=a.x,t.y=a.y,c())}}catch{}};u(),document.addEventListener("pointermove",n,{passive:!0}),document.addEventListener("pointerdown",n,{passive:!0}),document.documentElement.addEventListener("pointerleave",o),addEventListener("blur",o),addEventListener("resize",l),addEventListener("pagehide",s),addEventListener("pageshow",u),e.addEventListener("change",r)}var H=p(location.hash),L=null,w=0,_=null,k=()=>{clearTimeout(w),_?.abort(),_=null,L?.classList.remove("wg_prose__headingText--highlighted"),L=null},A=e=>{k(),L=e===null?null:e.querySelector(".wg_prose__headingText"),L!==null&&(L.classList.add("wg_prose__headingText--highlighted"),w=window.setTimeout(k,3e3),_=new AbortController,document.addEventListener("scrollend",O,{signal:_.signal}))},O=e=>{e.target===document&&(clearTimeout(w),_?.abort(),_=null,w=window.setTimeout(k,3e3))},K=e=>{if(!(e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)&&e.currentTarget instanceof HTMLAnchorElement&&(e.currentTarget.target===""||e.currentTarget.target==="_self")&&!e.currentTarget.hasAttribute("download")&&e.currentTarget.origin===location.origin&&e.currentTarget.pathname===location.pathname&&e.currentTarget.search===location.search&&e.currentTarget.hash){let t=p(e.currentTarget.hash);e.currentTarget.hash===location.hash&&A(t)}},P=()=>{let e=p(location.hash);e?.scrollIntoView(),A(e)};for(let e of document.querySelectorAll("a[href]"))e.addEventListener("click",K);addEventListener("hashchange",P);var M=[...document.querySelectorAll("[data-toc-link]")].flatMap(e=>{let t=v(e.hash),o=e.parentElement;return!t||!o?[]:[{heading:t,link:e,item:o,list:o.parentElement,subs:[...o.querySelectorAll("[data-toc-sub-link]")].flatMap(r=>{let c=v(r.hash),n=r.parentElement;return c&&n?[{heading:c,link:r,item:n}]:[]})}]}),x=M.at(0);if(x){let e=()=>{let t=Number.parseFloat(getComputedStyle(x.heading).scrollMarginTop)+1,o=M.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t)??x,r=o.subs.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t),c=M.indexOf(o),n=r===void 0?-1:o.subs.indexOf(r);for(let[l,s]of M.entries()){let u=s===o&&r===void 0;s.link.classList.toggle("wg_shellToc__link--active",u),u?s.link.setAttribute("aria-current","location"):s.link.removeAttribute("aria-current"),s.item.classList.toggle("wg_shellToc__item--trail",s.list===o.list&&l<c),s.item.classList.toggle("wg_shellToc__item--current",s===o);for(let[g,a]of s.subs.entries())a.link.classList.toggle("wg_shellToc__subLink--active",a===r),a===r?a.link.setAttribute("aria-current","location"):a.link.removeAttribute("aria-current"),a.item.classList.toggle("wg_shellToc__subItem--trail",s===o&&g<n),a.item.classList.toggle("wg_shellToc__subItem--current",a===r)}};addEventListener("scroll",e,{passive:!0}),addEventListener("resize",e),addEventListener("toggle",e,!0),e()}var B=new AbortController;for(let e of["wheel","touchmove","keydown","mousedown"])addEventListener(e,()=>B.abort(),{once:!0,passive:!0});var I=async()=>{await document.fonts.ready,H&&!B.signal.aborted&&(H.scrollIntoView({behavior:"instant"}),A(H)),document.documentElement.style.removeProperty("scroll-behavior")};document.readyState==="complete"?I():addEventListener("load",I,{once:!0});
|
package/package.json
CHANGED
|
@@ -107,7 +107,10 @@ export const bindMobileNavigation = () => {
|
|
|
107
107
|
|
|
108
108
|
if (mobile.matches) {
|
|
109
109
|
openButton.classList.add("wg_shell__menuToggle--visible");
|
|
110
|
-
|
|
110
|
+
|
|
111
|
+
if (navigation.parentElement !== content) {
|
|
112
|
+
content.append(navigation);
|
|
113
|
+
}
|
|
111
114
|
|
|
112
115
|
if (focused instanceof HTMLElement && navigation.contains(focused)) {
|
|
113
116
|
openButton.focus({preventScroll: true});
|
|
@@ -117,7 +120,11 @@ export const bindMobileNavigation = () => {
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
closeNavigation();
|
|
120
|
-
|
|
123
|
+
|
|
124
|
+
// 첫 desktop 연결에서 같은 DOM을 재삽입하면 내부 스크롤이 0으로 초기화됨
|
|
125
|
+
if (navigation.parentElement !== sidebar) {
|
|
126
|
+
sidebar.append(navigation);
|
|
127
|
+
}
|
|
121
128
|
|
|
122
129
|
if (focused instanceof HTMLElement && navigation.contains(focused)) {
|
|
123
130
|
focused.focus({preventScroll: true});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 실제 남은 내용이 있는 위·아래 끝만 흐림 표시 · 스크롤·글꼴·화면 크기 추적
|
|
3
|
+
* HTML 직후 직렬화해 실행하므로 import나 바깥 변수 없이 브라우저 DOM만 사용
|
|
4
|
+
* 관찰자는 문서 수명 동안 유지 · BFCache 복귀에서도 같은 탐색 DOM 추적
|
|
5
|
+
*/
|
|
6
|
+
export const bindNavigationOverflow = () => {
|
|
7
|
+
const ports = [...document.querySelectorAll<HTMLElement>("[data-navigation-scroll]")];
|
|
8
|
+
const frame = {id: 0};
|
|
9
|
+
// 소수 픽셀 반올림으로 끝에서 흐림이 남는 것을 방지
|
|
10
|
+
const edgeTolerance = 1;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 콘텐츠가 잘리는 끝만 표시 · 네이티브 스크롤은 그대로 사용
|
|
14
|
+
*/
|
|
15
|
+
const updateOverflow = () => {
|
|
16
|
+
frame.id = 0;
|
|
17
|
+
|
|
18
|
+
for (const port of ports) {
|
|
19
|
+
port.classList.add("wg_shell__scroll--ready");
|
|
20
|
+
port.classList.toggle("wg_shell__scroll--above", port.scrollTop > edgeTolerance);
|
|
21
|
+
port.classList.toggle("wg_shell__scroll--below", port.scrollHeight - port.clientHeight - port.scrollTop > edgeTolerance);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 연속 스크롤과 크기 변경은 프레임당 한 번만 반영
|
|
27
|
+
*/
|
|
28
|
+
const scheduleOverflow = () => {
|
|
29
|
+
if (frame.id === 0) {
|
|
30
|
+
frame.id = requestAnimationFrame(updateOverflow);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const observer = new ResizeObserver(scheduleOverflow);
|
|
35
|
+
|
|
36
|
+
for (const port of ports) {
|
|
37
|
+
port.addEventListener("scroll", scheduleOverflow, {passive: true});
|
|
38
|
+
observer.observe(port);
|
|
39
|
+
|
|
40
|
+
if (port.firstElementChild) {
|
|
41
|
+
observer.observe(port.firstElementChild);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addEventListener("pageshow", scheduleOverflow);
|
|
46
|
+
updateOverflow();
|
|
47
|
+
};
|
|
@@ -1,123 +1,84 @@
|
|
|
1
1
|
import {navigation_mobile_query, navigation_scroll_storage_key} from "@/component/widget/shell/_constant/navigation";
|
|
2
|
+
import {restoreNavigationScroll} from "@/component/widget/shell/_function/restore-navigation-scroll";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* 문서
|
|
5
|
-
*
|
|
5
|
+
* 문서 목록과 페이지 목차의 위치를 따로 저장 · TOC 길이가 목록 위치를 바꾸지 않음
|
|
6
|
+
* 첫 데스크톱 복원은 HTML에서 완료 · 늦은 module 실행에서 다시 이동하지 않음
|
|
6
7
|
*/
|
|
7
8
|
export const bindNavigationScroll = () => {
|
|
8
9
|
const navigation = document.querySelector<HTMLElement>("[data-navigation]");
|
|
9
|
-
const
|
|
10
|
+
const documents = document.querySelector<HTMLElement>('[data-navigation-scroll="documents"]');
|
|
11
|
+
const outline = document.querySelector<HTMLElement>('[data-navigation-scroll="outline"]');
|
|
10
12
|
const dialog = document.querySelector<HTMLDialogElement>("[data-navigation-drawer]");
|
|
11
13
|
const openButton = document.querySelector<HTMLButtonElement>("[data-navigation-open]");
|
|
12
14
|
|
|
13
|
-
if (!navigation || !
|
|
15
|
+
if (!navigation || !documents || !outline || !dialog || !openButton) {
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const mobile = matchMedia(navigation_mobile_query);
|
|
18
20
|
const layout = {mobile: mobile.matches};
|
|
19
|
-
|
|
21
|
+
const ports = [documents, outline];
|
|
22
|
+
const positions = new Map<string, Map<HTMLElement, number>>();
|
|
20
23
|
const signature = [...navigation.querySelectorAll<HTMLAnchorElement>(".wg_shellNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
|
|
21
|
-
const positions = new Map<HTMLElement, number>();
|
|
22
|
-
const storageKeys = new Map<HTMLElement, string>([
|
|
23
|
-
[sidebar, `${navigation_scroll_storage_key}:desktop`],
|
|
24
|
-
[dialog, `${navigation_scroll_storage_key}:drawer`],
|
|
25
|
-
]);
|
|
26
|
-
const userInput = new AbortController();
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/**
|
|
26
|
+
* 숨김·닫기보다 먼저 마지막 위치 보존 · 저장소 차단 시에도 드로어 재열기 유지
|
|
27
|
+
*/
|
|
28
|
+
const saveScroll = () => {
|
|
29
|
+
if (layout.mobile !== mobile.matches || (mobile.matches && !dialog.open)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const key = `${navigation_scroll_storage_key}:${mobile.matches ? "drawer" : "desktop"}`;
|
|
34
|
+
positions.set(key, new Map(ports.map((port) => [port, port.scrollTop])));
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
stored !== null &&
|
|
38
|
-
"navigation" in stored &&
|
|
39
|
-
stored.navigation === signature &&
|
|
40
|
-
"top" in stored &&
|
|
41
|
-
typeof stored.top === "number" &&
|
|
42
|
-
Number.isFinite(stored.top) &&
|
|
43
|
-
stored.top >= 0
|
|
44
|
-
) {
|
|
45
|
-
positions.set(port, stored.top);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
36
|
+
try {
|
|
37
|
+
sessionStorage.setItem(key, JSON.stringify({navigation: signature, pathname: location.pathname, documents: documents.scrollTop, outline: outline.scrollTop}));
|
|
48
38
|
} catch {
|
|
49
|
-
//
|
|
39
|
+
// 현재 페이지 안의 위치는 메모리에 유지 · 저장 실패가 링크 이동을 막지 않음
|
|
50
40
|
}
|
|
51
|
-
|
|
52
|
-
for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
|
|
53
|
-
port.addEventListener(type, () => userInput.abort(), {once: true, passive: true});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const initialPositions = new Map(positions);
|
|
41
|
+
};
|
|
57
42
|
|
|
58
43
|
/**
|
|
59
|
-
*
|
|
44
|
+
* 열기·반응형 DOM 이동 완료 후 화면별 위치 복원
|
|
60
45
|
*/
|
|
61
46
|
const restoreScroll = () => {
|
|
62
47
|
layout.mobile = mobile.matches;
|
|
63
|
-
const port = mobile.matches ? dialog : sidebar;
|
|
64
|
-
const top = positions.get(port);
|
|
65
48
|
|
|
66
|
-
if (
|
|
49
|
+
if (mobile.matches && !dialog.open) {
|
|
67
50
|
return;
|
|
68
51
|
}
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
const key = `${navigation_scroll_storage_key}:${mobile.matches ? "drawer" : "desktop"}`;
|
|
54
|
+
const saved = positions.get(key);
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const key = storageKeys.get(port);
|
|
56
|
+
if (saved === undefined) {
|
|
57
|
+
restoreNavigationScroll(key);
|
|
58
|
+
} else {
|
|
59
|
+
for (const port of ports) {
|
|
60
|
+
const top = saved.get(port);
|
|
79
61
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
if (top !== undefined) {
|
|
63
|
+
port.scrollTop = top;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
sessionStorage.setItem(key, JSON.stringify({navigation: signature, top: port.scrollTop}));
|
|
89
|
-
} catch {
|
|
90
|
-
// 저장 실패도 같은 페이지의 드로어 재열기와 반응형 위치 복원은 유지
|
|
91
|
-
}
|
|
68
|
+
saveScroll();
|
|
92
69
|
};
|
|
93
70
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const settleScroll = async () => {
|
|
98
|
-
await document.fonts.ready;
|
|
99
|
-
|
|
100
|
-
if (!userInput.signal.aborted) {
|
|
101
|
-
const port = mobile.matches ? dialog : sidebar;
|
|
102
|
-
const top = initialPositions.get(port);
|
|
103
|
-
|
|
104
|
-
if (top !== undefined && (!mobile.matches || dialog.open)) {
|
|
105
|
-
port.scrollTop = top;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
71
|
+
for (const port of ports) {
|
|
72
|
+
port.addEventListener("scroll", saveScroll, {passive: true});
|
|
73
|
+
}
|
|
109
74
|
|
|
110
|
-
|
|
111
|
-
dialog.addEventListener("scroll", saveScroll, {passive: true});
|
|
75
|
+
// 캡처 단계에서 모바일의 기본 닫기보다 먼저 저장
|
|
112
76
|
navigation.addEventListener("click", saveScroll, {capture: true});
|
|
77
|
+
dialog.addEventListener("cancel", saveScroll);
|
|
78
|
+
dialog.querySelector("[data-navigation-close]")?.addEventListener("click", saveScroll, {capture: true});
|
|
79
|
+
dialog.addEventListener("click", saveScroll, {capture: true});
|
|
113
80
|
openButton.addEventListener("click", restoreScroll);
|
|
114
81
|
mobile.addEventListener("change", restoreScroll);
|
|
115
82
|
addEventListener("pagehide", saveScroll);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (document.readyState === "complete") {
|
|
119
|
-
settleScroll();
|
|
120
|
-
} else {
|
|
121
|
-
addEventListener("load", settleScroll, {once: true});
|
|
122
|
-
}
|
|
83
|
+
saveScroll();
|
|
123
84
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 탐색 HTML 직후와 드로어 열기에서 저장 위치 복원 · 첫 paint 전 실행
|
|
3
|
+
* HTML에 함수 본문을 직렬화하므로 import나 바깥 변수 없이 브라우저 DOM만 사용
|
|
4
|
+
* @param storageKey 탭·화면 모드별 저장 키
|
|
5
|
+
*/
|
|
6
|
+
export const restoreNavigationScroll = (storageKey: string) => {
|
|
7
|
+
const navigation = document.querySelector<HTMLElement>("[data-navigation]");
|
|
8
|
+
const documents = document.querySelector<HTMLElement>('[data-navigation-scroll="documents"]');
|
|
9
|
+
const outline = document.querySelector<HTMLElement>('[data-navigation-scroll="outline"]');
|
|
10
|
+
|
|
11
|
+
if (!navigation || !documents || !outline) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const value = sessionStorage.getItem(storageKey);
|
|
17
|
+
|
|
18
|
+
if (value === null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const stored: unknown = JSON.parse(value);
|
|
23
|
+
const signature = [...navigation.querySelectorAll<HTMLAnchorElement>(".wg_shellNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
|
|
24
|
+
|
|
25
|
+
if (typeof stored !== "object" || stored === null || !("navigation" in stored) || stored.navigation !== signature) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ("documents" in stored && typeof stored.documents === "number" && Number.isFinite(stored.documents) && stored.documents >= 0) {
|
|
30
|
+
documents.scrollTop = stored.documents;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 문서 목록은 공통 위치 · 목차는 같은 페이지에 다시 들어왔을 때만 복원
|
|
34
|
+
if (
|
|
35
|
+
"pathname" in stored &&
|
|
36
|
+
stored.pathname === location.pathname &&
|
|
37
|
+
"outline" in stored &&
|
|
38
|
+
typeof stored.outline === "number" &&
|
|
39
|
+
Number.isFinite(stored.outline) &&
|
|
40
|
+
stored.outline >= 0
|
|
41
|
+
) {
|
|
42
|
+
outline.scrollTop = stored.outline;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// 차단·손상된 저장소는 복원만 생략 · 기본 링크와 스크롤 유지
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
* 문서 탐색 아래 목차 · 회색 제목·묶음과 본문 글색의 이동 대상 구분
|
|
3
3
|
*/
|
|
4
4
|
.wg_shellToc__root {
|
|
5
|
-
margin-top: var(--app-space-group);
|
|
6
|
-
padding-top: var(--app-space-group);
|
|
7
|
-
border-top: 1px solid var(--app-color-border-soft);
|
|
8
5
|
font-size: var(--app-font-size-dense);
|
|
9
6
|
line-height: var(--app-line-height-tight);
|
|
10
7
|
}
|
|
@@ -150,6 +147,11 @@
|
|
|
150
147
|
&::after {
|
|
151
148
|
border-left-color: var(--app-color-text-strong);
|
|
152
149
|
}
|
|
150
|
+
|
|
151
|
+
/* 현재 소제목의 상위 절은 글 강조 없이 경로의 가로 가지 연결 */
|
|
152
|
+
& > .wg_shellToc__link::before {
|
|
153
|
+
border-top-color: var(--app-color-text-strong);
|
|
154
|
+
}
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
.wg_shellToc__sub {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
.wg_shell__brandRow {
|
|
28
28
|
display: flex;
|
|
29
29
|
|
|
30
|
-
/*
|
|
30
|
+
/* 두 스크롤 영역 바깥 · 목록과 목차 위에서 이름 고정 */
|
|
31
31
|
position: sticky;
|
|
32
32
|
top: 0;
|
|
33
33
|
z-index: var(--app-z-index-sticky);
|
|
@@ -103,50 +103,69 @@
|
|
|
103
103
|
display: flex;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/* sticky 틀 안에서 문서 목록과 목차의 스크롤 높이를 독립 유지 */
|
|
106
107
|
.wg_shell__sidebar {
|
|
107
108
|
grid-area: nav;
|
|
108
|
-
|
|
109
|
-
/* 뷰포트 sticky · 문서 탐색과 목차는 이 컨테이너에서 함께 스크롤 */
|
|
110
109
|
position: sticky;
|
|
111
110
|
top: 0;
|
|
112
111
|
align-self: start;
|
|
113
112
|
min-width: 0;
|
|
114
|
-
|
|
115
|
-
max-height: 100vh;
|
|
113
|
+
height: 100dvh;
|
|
116
114
|
margin-inline-end: var(--app-space-sidebar-gap);
|
|
117
|
-
|
|
118
|
-
overscroll-behavior-y: contain;
|
|
115
|
+
}
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
.wg_shell__navigation {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
height: calc(100dvh - var(--app-size-brand));
|
|
121
|
+
min-width: 0;
|
|
122
|
+
min-height: 0;
|
|
123
|
+
}
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
.wg_shell__documents {
|
|
126
|
+
/* TOC 길이가 바뀌어도 공통 목록의 스크롤 범위는 동일 */
|
|
127
|
+
flex: none;
|
|
128
|
+
max-height: 60%;
|
|
129
|
+
}
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
.wg_shell__outline {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
min-height: 0;
|
|
135
|
+
margin-top: var(--app-space-group);
|
|
136
|
+
padding-top: var(--app-space-group);
|
|
137
|
+
border-top: 1px solid var(--app-color-border-soft);
|
|
138
|
+
|
|
139
|
+
&:has(.wg_shell__headings:empty) {
|
|
140
|
+
display: none;
|
|
132
141
|
}
|
|
142
|
+
}
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
144
|
+
.wg_shell__headings {
|
|
145
|
+
min-height: 0;
|
|
146
|
+
}
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
.wg_shell__scroll {
|
|
149
|
+
overflow-y: auto;
|
|
150
|
+
overscroll-behavior-y: contain;
|
|
151
|
+
scroll-padding-block: var(--app-size-navigation-fade);
|
|
152
|
+
}
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
.wg_shell__scroll--ready {
|
|
155
|
+
/* 흐림으로 넘침을 알릴 때만 손잡이 숨김 · JS 없으면 네이티브 손잡이 유지 */
|
|
156
|
+
scrollbar-width: none;
|
|
146
157
|
}
|
|
147
158
|
|
|
148
|
-
.
|
|
149
|
-
|
|
159
|
+
.wg_shell__scroll--above {
|
|
160
|
+
mask-image: linear-gradient(to bottom, transparent, #000 var(--app-size-navigation-fade));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.wg_shell__scroll--below {
|
|
164
|
+
mask-image: linear-gradient(to bottom, #000 calc(100% - var(--app-size-navigation-fade)), transparent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.wg_shell__scroll--above.wg_shell__scroll--below {
|
|
168
|
+
mask-image: linear-gradient(to bottom, transparent, #000 var(--app-size-navigation-fade), #000 calc(100% - var(--app-size-navigation-fade)), transparent);
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
.wg_shell__main {
|
|
@@ -174,32 +193,7 @@
|
|
|
174
193
|
border-right: 1px solid var(--app-color-border-layout);
|
|
175
194
|
background: var(--app-color-ground);
|
|
176
195
|
color: var(--app-color-text);
|
|
177
|
-
|
|
178
|
-
scrollbar-width: auto;
|
|
179
|
-
scrollbar-color: auto;
|
|
180
|
-
overscroll-behavior: contain;
|
|
181
|
-
scroll-padding-block: var(--app-size-mobile-header) var(--app-space-group);
|
|
182
|
-
|
|
183
|
-
&::-webkit-scrollbar {
|
|
184
|
-
width: 3px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
&::-webkit-scrollbar-track {
|
|
188
|
-
background: transparent;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
&::-webkit-scrollbar-thumb {
|
|
192
|
-
border-radius: 3px;
|
|
193
|
-
background: transparent;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
&:hover::-webkit-scrollbar-thumb {
|
|
197
|
-
background: var(--app-color-border-soft);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
&:focus-within::-webkit-scrollbar-thumb {
|
|
201
|
-
background: var(--app-color-border-soft);
|
|
202
|
-
}
|
|
196
|
+
overflow: hidden;
|
|
203
197
|
|
|
204
198
|
&::backdrop {
|
|
205
199
|
background: var(--app-color-backdrop);
|
|
@@ -207,10 +201,7 @@
|
|
|
207
201
|
}
|
|
208
202
|
|
|
209
203
|
.wg_shell__drawerHeader {
|
|
210
|
-
/*
|
|
211
|
-
position: sticky;
|
|
212
|
-
top: 0;
|
|
213
|
-
z-index: var(--app-z-index-sticky);
|
|
204
|
+
/* 두 스크롤 영역 바깥에서 닫기 버튼 유지 */
|
|
214
205
|
display: flex;
|
|
215
206
|
align-items: center;
|
|
216
207
|
justify-content: space-between;
|
|
@@ -252,6 +243,11 @@
|
|
|
252
243
|
|
|
253
244
|
.wg_shell__drawerContent {
|
|
254
245
|
min-width: 0;
|
|
246
|
+
height: calc(100% - var(--app-size-mobile-header) - var(--app-space-block));
|
|
247
|
+
|
|
248
|
+
& > .wg_shell__navigation {
|
|
249
|
+
height: 100%;
|
|
250
|
+
}
|
|
255
251
|
}
|
|
256
252
|
|
|
257
253
|
.wg_shell__cursorFace {
|
|
@@ -270,54 +266,12 @@
|
|
|
270
266
|
visibility: visible;
|
|
271
267
|
}
|
|
272
268
|
|
|
273
|
-
/*
|
|
274
|
-
@supports not selector(::-webkit-scrollbar) {
|
|
275
|
-
.wg_shell__sidebar {
|
|
276
|
-
scrollbar-width: thin;
|
|
277
|
-
scrollbar-color: transparent transparent;
|
|
278
|
-
|
|
279
|
-
&:hover {
|
|
280
|
-
scrollbar-color: var(--app-color-border-soft) transparent;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
&:focus-within {
|
|
284
|
-
scrollbar-color: var(--app-color-border-soft) transparent;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
.wg_shell__drawer {
|
|
289
|
-
scrollbar-width: thin;
|
|
290
|
-
scrollbar-color: var(--app-color-border-soft) transparent;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/* 색상 강제 환경은 시스템 손잡이 유지 · 대비 설정을 덮어쓰지 않음 */
|
|
269
|
+
/* 강제 색상 환경은 흐림 대신 시스템 손잡이로 넘침 표시 */
|
|
295
270
|
@media (forced-colors: active) {
|
|
296
|
-
.
|
|
297
|
-
|
|
271
|
+
.wg_shell__scroll {
|
|
272
|
+
mask-image: none;
|
|
298
273
|
scrollbar-width: thin;
|
|
299
274
|
scrollbar-color: auto;
|
|
300
|
-
|
|
301
|
-
&:hover {
|
|
302
|
-
scrollbar-color: auto;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
&:focus-within {
|
|
306
|
-
scrollbar-color: auto;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.wg_shell__drawer {
|
|
311
|
-
scrollbar-width: thin;
|
|
312
|
-
scrollbar-color: auto;
|
|
313
|
-
|
|
314
|
-
&:hover {
|
|
315
|
-
scrollbar-color: auto;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
&:focus-within {
|
|
319
|
-
scrollbar-color: auto;
|
|
320
|
-
}
|
|
321
275
|
}
|
|
322
276
|
}
|
|
323
277
|
|
|
@@ -332,14 +286,20 @@
|
|
|
332
286
|
|
|
333
287
|
.wg_shell__sidebar {
|
|
334
288
|
position: static;
|
|
335
|
-
|
|
336
|
-
max-height: none;
|
|
337
|
-
overflow-y: visible;
|
|
289
|
+
height: auto;
|
|
338
290
|
margin-block-end: var(--app-space-block);
|
|
339
291
|
margin-inline-end: 0;
|
|
340
292
|
border-bottom: 1px solid var(--app-color-border-layout);
|
|
341
293
|
}
|
|
342
294
|
|
|
295
|
+
.wg_shell__sidebar > .wg_shell__navigation {
|
|
296
|
+
height: auto;
|
|
297
|
+
|
|
298
|
+
& > .wg_shell__documents {
|
|
299
|
+
max-height: none;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
343
303
|
.wg_shell__sidebar--navigationReady {
|
|
344
304
|
/* 본문을 스크롤해도 메뉴 진입 유지 · 스크립트 없는 긴 목록에는 적용하지 않음 */
|
|
345
305
|
position: sticky;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
import type {ReactNode} from "react";
|
|
3
|
+
import {navigation_mobile_query, navigation_scroll_storage_key} from "@/component/widget/shell/_constant/navigation";
|
|
4
|
+
import {bindNavigationOverflow} from "@/component/widget/shell/_function/bind-navigation-overflow";
|
|
5
|
+
import {restoreNavigationScroll} from "@/component/widget/shell/_function/restore-navigation-scroll";
|
|
3
6
|
import {toTocGroups} from "@/component/widget/shell/_function/to-toc-groups";
|
|
4
7
|
import type {DocOutline} from "@/component/widget/shell/_type/doc-outline";
|
|
5
8
|
import {WgShellNav} from "@/component/widget/shell/_wg-shell-nav";
|
|
@@ -18,6 +21,12 @@ import "./wg-shell.css";
|
|
|
18
21
|
*/
|
|
19
22
|
const reloadScript = `new EventSource(${JSON.stringify(asset_reload_path)}).onmessage=function(){location.reload()}`;
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 탐색 DOM 직후 동기 실행 · 큰 본문과 module 다운로드를 기다리지 않고 첫 위치·흐림 적용
|
|
26
|
+
* 손잡이 숨김으로 달라지는 목록 폭을 먼저 확정한 뒤 좌표 복원
|
|
27
|
+
*/
|
|
28
|
+
const navigationScript = `(${bindNavigationOverflow.toString()})();if(!matchMedia(${JSON.stringify(navigation_mobile_query)}).matches){(${restoreNavigationScroll.toString()})(${JSON.stringify(`${navigation_scroll_storage_key}:desktop`)})}`;
|
|
29
|
+
|
|
21
30
|
/**
|
|
22
31
|
* 공통 HTML 틀의 입력 · 문서 탐색·현재 목차·본문
|
|
23
32
|
*/
|
|
@@ -112,10 +121,23 @@ export const WgShell = (props: WgShellProps) => {
|
|
|
112
121
|
* 같은 탐색 DOM을 모바일 대화상자로 이동 · 스크립트 없이도 기본 목록 제공
|
|
113
122
|
*/}
|
|
114
123
|
<div className={clsx("wg_shell__navigation")} data-navigation="">
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
{/**
|
|
125
|
+
* 모든 페이지에서 같은 높이의 문서 탐색 · 목차 길이와 스크롤 분리
|
|
126
|
+
*/}
|
|
127
|
+
<div className={clsx("wg_shell__documents", "wg_shell__scroll")} data-navigation-scroll="documents">
|
|
128
|
+
<WgShellNav site={props.site} docs={props.docs} current={props.current} />
|
|
129
|
+
</div>
|
|
130
|
+
{/**
|
|
131
|
+
* 문서 목록 아래 현재 페이지 목차 · 남은 뷰포트 안에서 독립 스크롤
|
|
132
|
+
*/}
|
|
133
|
+
<div className={clsx("wg_shell__outline")}>
|
|
134
|
+
<div className={clsx("wg_shell__headings", "wg_shell__scroll")} data-navigation-scroll="outline">
|
|
135
|
+
{tocGroups.length > 0 && <WgShellToc groups={tocGroups} />}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
117
138
|
</div>
|
|
118
139
|
</div>
|
|
140
|
+
<script dangerouslySetInnerHTML={{__html: navigationScript}} />
|
|
119
141
|
<main className={clsx("wg_shell__main")}>{props.children}</main>
|
|
120
142
|
{/**
|
|
121
143
|
* 브라우저의 모달 포커스·Escape 처리 · 목록은 한 번만 렌더링
|
package/src/style/token.css
CHANGED