for-humanity 0.4.1 → 0.4.3

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.4.3 — 2026-10-04
6
+
7
+ - Use standard native navigation scrollbars in forced-color mode so browser color adjustment cannot hide custom thumbs against the background.
8
+
9
+ ## 0.4.2 — 2026-10-04
10
+
11
+ - Preserve sidebar and mobile drawer scroll positions across document navigation and reloads within a tab, independently of the main content.
12
+ - Show thin native navigation scrollbar thumbs on hover or keyboard focus, with a transparent track, Firefox thin fallback and system colors in forced-color environments.
13
+ - Explain when related sections belong in one page's headings and when a separate child document is appropriate.
14
+
5
15
  ## 0.4.1 — 2026-10-04
6
16
 
7
17
  - Coalesce consecutive document and asset saves and serialize dev refreshes so bulk AI edits cannot start overlapping full-site renders and exhaust memory.
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.1
36
+ pnpm add -D --save-exact for-humanity@0.4.3
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.1` 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).
55
+ Version `0.4.3` 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.1` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
77
+ Changes pushed to `main` update the documentation site. A stable version tag such as `v0.4.3` 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
@@ -888,7 +888,27 @@
888
888
  max-height: 100vh;
889
889
  margin-inline-end: var(--app-space-sidebar-gap);
890
890
  overflow-y: auto;
891
+ overscroll-behavior-y: contain;
892
+ scrollbar-gutter: stable;
893
+ scrollbar-width: auto;
894
+ scrollbar-color: auto;
891
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
+ }
892
912
  }
893
913
  .wg_shell__navigation {
894
914
  min-width: 0;
@@ -914,8 +934,27 @@
914
934
  border-right: 1px solid var(--app-color-border-layout);
915
935
  background: var(--app-color-ground);
916
936
  color: var(--app-color-text);
937
+ scrollbar-gutter: stable;
938
+ scrollbar-width: auto;
939
+ scrollbar-color: auto;
917
940
  overscroll-behavior: contain;
918
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
+ }
919
958
  &::backdrop {
920
959
  background: var(--app-color-backdrop);
921
960
  }
@@ -974,6 +1013,44 @@
974
1013
  .wg_shell__cursorFace--visible {
975
1014
  visibility: visible;
976
1015
  }
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
+ @media (forced-colors: active) {
1033
+ .wg_shell__sidebar {
1034
+ scrollbar-width: thin;
1035
+ 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
+ }
1053
+ }
977
1054
  @media (width < 1024px) {
978
1055
  .wg_shell__root {
979
1056
  max-width: calc(var(--app-size-main) + var(--app-space-gutter) * 2);
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
- var L="fh-cursor-face";var k="(width < 1024px)";var p=e=>{if(!e)return null;try{return document.getElementById(decodeURIComponent(e.slice(1)))}catch(t){if(t instanceof URIError)return null;throw t}};var m=e=>{let t=p(e);for(let n=t?.closest("details");n;n=n.parentElement?.closest("details"))n.open=!0;return t};var S=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector("[data-navigation-drawer]"),n=document.querySelector("[data-navigation-content]"),i=document.querySelector("[data-navigation-open]"),d=document.querySelector("[data-navigation-close]"),c=e?.parentElement;if(!e||!t||!n||!i||!d||!c)return;let l=matchMedia(k),a=()=>{t.open||(document.body.classList.remove("wg_shell__root--drawerOpen"),i.setAttribute("aria-expanded","false"))},u=()=>{t.close(),a()},h=()=>{!l.matches||t.open||(t.showModal(),document.body.classList.add("wg_shell__root--drawerOpen"),i.setAttribute("aria-expanded","true"))},r=o=>{let s=t.getBoundingClientRect();o.target===t&&(o.clientX<s.left||o.clientX>s.right||o.clientY<s.top||o.clientY>s.bottom)&&u()},q=o=>{if(!t.open||o.defaultPrevented||o.button!==0||o.metaKey||o.ctrlKey||o.shiftKey||o.altKey||!(o.target instanceof Element))return;let s=o.target.closest("a[href]");if(!(!(s instanceof HTMLAnchorElement)||s.target!==""&&s.target!=="_self"||s.hasAttribute("download"))&&(u(),s.origin===location.origin&&s.pathname===location.pathname&&s.search===location.search&&s.hash)){let T=m(s.hash);T&&(T.tabIndex=-1,T.focus({preventScroll:!0}))}},H=()=>{let o=document.activeElement;if(l.matches){i.classList.add("wg_shell__menuToggle--visible"),n.append(e),o instanceof HTMLElement&&e.contains(o)&&i.focus({preventScroll:!0});return}u(),c.append(e),o instanceof HTMLElement&&e.contains(o)?o.focus({preventScroll:!0}):(o===i||o instanceof HTMLElement&&t.contains(o))&&c.querySelector(".wg_shell__brand")?.focus({preventScroll:!0}),i.classList.remove("wg_shell__menuToggle--visible")};i.addEventListener("click",h),d.addEventListener("click",u),t.addEventListener("close",a),t.addEventListener("click",r),e.addEventListener("click",q),l.addEventListener("change",H),H(),c.classList.add("wg_shell__sidebar--navigationReady")};S();var g=document.querySelector("[data-cursor-face]");if(g){let e=matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"),t={x:0,y:0,frame:0},n=()=>{cancelAnimationFrame(t.frame),t.frame=0,g.classList.remove("wg_shell__cursorFace--visible")},i=()=>{e.matches||n()},d=()=>{t.frame===0&&(t.frame=requestAnimationFrame(()=>{t.frame=0,g.style.transform=`translate3d(
2
- ${Math.max(0,Math.min(t.x+16,document.documentElement.clientWidth-g.width-16))}px,
3
- ${Math.max(0,Math.min(t.y+16,document.documentElement.clientHeight-g.height-16))}px,
1
+ var T="fh-cursor-face";var w="(width < 1024px)",x="for-humanity:navigation-scroll";var b=e=>{if(!e)return null;try{return document.getElementById(decodeURIComponent(e.slice(1)))}catch(t){if(t instanceof URIError)return null;throw t}};var y=e=>{let t=b(e);for(let n=t?.closest("details");n;n=n.parentElement?.closest("details"))n.open=!0;return t};var I=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector("[data-navigation-drawer]"),n=document.querySelector("[data-navigation-content]"),a=document.querySelector("[data-navigation-open]"),s=document.querySelector("[data-navigation-close]"),u=e?.parentElement;if(!e||!t||!n||!a||!s||!u)return;let d=matchMedia(w),l=()=>{t.open||(document.body.classList.remove("wg_shell__root--drawerOpen"),a.setAttribute("aria-expanded","false"))},m=()=>{t.close(),l()},g=()=>{!d.matches||t.open||(t.showModal(),document.body.classList.add("wg_shell__root--drawerOpen"),a.setAttribute("aria-expanded","true"))},i=r=>{let o=t.getBoundingClientRect();r.target===t&&(r.clientX<o.left||r.clientX>o.right||r.clientY<o.top||r.clientY>o.bottom)&&m()},L=r=>{if(!t.open||r.defaultPrevented||r.button!==0||r.metaKey||r.ctrlKey||r.shiftKey||r.altKey||!(r.target instanceof Element))return;let o=r.target.closest("a[href]");if(!(!(o instanceof HTMLAnchorElement)||o.target!==""&&o.target!=="_self"||o.hasAttribute("download"))&&(m(),o.origin===location.origin&&o.pathname===location.pathname&&o.search===location.search&&o.hash)){let c=y(o.hash);c&&(c.tabIndex=-1,c.focus({preventScroll:!0}))}},p=()=>{let r=document.activeElement;if(d.matches){a.classList.add("wg_shell__menuToggle--visible"),n.append(e),r instanceof HTMLElement&&e.contains(r)&&a.focus({preventScroll:!0});return}m(),u.append(e),r instanceof HTMLElement&&e.contains(r)?r.focus({preventScroll:!0}):(r===a||r instanceof HTMLElement&&t.contains(r))&&u.querySelector(".wg_shell__brand")?.focus({preventScroll:!0}),a.classList.remove("wg_shell__menuToggle--visible")};a.addEventListener("click",g),s.addEventListener("click",m),t.addEventListener("close",l),t.addEventListener("click",i),e.addEventListener("click",L),d.addEventListener("change",p),p(),u.classList.add("wg_shell__sidebar--navigationReady")};var N=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector(".wg_shell__sidebar"),n=document.querySelector("[data-navigation-drawer]"),a=document.querySelector("[data-navigation-open]");if(!e||!t||!n||!a)return;let s=matchMedia(w),u={mobile:s.matches},d=[...e.querySelectorAll(".wg_shellNav__root a[href]")].map(o=>`${o.pathname}:${o.textContent}`).join(`
2
+ `),l=new Map,m=new Map([[t,`${x}:desktop`],[n,`${x}:drawer`]]),g=new AbortController;for(let[o,c]of m){try{let _=sessionStorage.getItem(c);if(_!==null){let h=JSON.parse(_);typeof h=="object"&&h!==null&&"navigation"in h&&h.navigation===d&&"top"in h&&typeof h.top=="number"&&Number.isFinite(h.top)&&h.top>=0&&l.set(o,h.top)}}catch{}for(let _ of["wheel","touchstart","pointerdown","keydown"])o.addEventListener(_,()=>g.abort(),{once:!0,passive:!0})}let i=new Map(l),L=()=>{u.mobile=s.matches;let o=s.matches?n:t,c=l.get(o);c===void 0||s.matches&&!n.open||(o.scrollTop=c)},p=o=>{let c=s.matches?n:t,_=m.get(c);if(!(u.mobile!==s.matches||o.type==="scroll"&&o.currentTarget!==c||_===void 0||s.matches&&!n.open)){l.set(c,c.scrollTop);try{sessionStorage.setItem(_,JSON.stringify({navigation:d,top:c.scrollTop}))}catch{}}},r=async()=>{if(await document.fonts.ready,!g.signal.aborted){let o=s.matches?n:t,c=i.get(o);c!==void 0&&(!s.matches||n.open)&&(o.scrollTop=c)}};t.addEventListener("scroll",p,{passive:!0}),n.addEventListener("scroll",p,{passive:!0}),e.addEventListener("click",p,{capture:!0}),a.addEventListener("click",L),s.addEventListener("change",L),addEventListener("pagehide",p),L(),document.readyState==="complete"?r():addEventListener("load",r,{once:!0})};I();N();var f=document.querySelector("[data-cursor-face]");if(f){let e=matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"),t={x:0,y:0,frame:0},n=()=>{cancelAnimationFrame(t.frame),t.frame=0,f.classList.remove("wg_shell__cursorFace--visible")},a=()=>{e.matches||n()},s=()=>{t.frame===0&&(t.frame=requestAnimationFrame(()=>{t.frame=0,f.style.transform=`translate3d(
3
+ ${Math.max(0,Math.min(t.x+16,document.documentElement.clientWidth-f.width-16))}px,
4
+ ${Math.max(0,Math.min(t.y+16,document.documentElement.clientHeight-f.height-16))}px,
4
5
  0
5
- )`,g.classList.add("wg_shell__cursorFace--visible")}))},c=h=>{if(!(e.matches&&h.pointerType==="mouse")){n();return}t.x=h.clientX,t.y=h.clientY,d()},l=()=>{g.classList.contains("wg_shell__cursorFace--visible")&&d()},a=()=>{if(g.classList.contains("wg_shell__cursorFace--visible"))try{sessionStorage.setItem(L,JSON.stringify({x:t.x,y:t.y}))}catch{}},u=()=>{try{let h=sessionStorage.getItem(L);if(sessionStorage.removeItem(L),h!==null&&e.matches){let r=JSON.parse(h);typeof r=="object"&&r!==null&&"x"in r&&"y"in r&&typeof r.x=="number"&&typeof r.y=="number"&&Number.isFinite(r.x)&&Number.isFinite(r.y)&&(t.x=r.x,t.y=r.y,d())}}catch{}};u(),document.addEventListener("pointermove",c,{passive:!0}),document.addEventListener("pointerdown",c,{passive:!0}),document.documentElement.addEventListener("pointerleave",n),addEventListener("blur",n),addEventListener("resize",l),addEventListener("pagehide",a),addEventListener("pageshow",u),e.addEventListener("change",i)}var v=m(location.hash),_=null,y=0,f=null,x=()=>{clearTimeout(y),f?.abort(),f=null,_?.classList.remove("wg_prose__headingText--highlighted"),_=null},M=e=>{x(),_=e===null?null:e.querySelector(".wg_prose__headingText"),_!==null&&(_.classList.add("wg_prose__headingText--highlighted"),y=window.setTimeout(x,3e3),f=new AbortController,document.addEventListener("scrollend",B,{signal:f.signal}))},B=e=>{e.target===document&&(clearTimeout(y),f?.abort(),f=null,y=window.setTimeout(x,3e3))},N=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=m(e.currentTarget.hash);e.currentTarget.hash===location.hash&&M(t)}},K=()=>{let e=m(location.hash);e?.scrollIntoView(),M(e)};for(let e of document.querySelectorAll("a[href]"))e.addEventListener("click",N);addEventListener("hashchange",K);var E=[...document.querySelectorAll("[data-toc-link]")].flatMap(e=>{let t=p(e.hash),n=e.parentElement;return!t||!n?[]:[{heading:t,link:e,item:n,list:n.parentElement,subs:[...n.querySelectorAll("[data-toc-sub-link]")].flatMap(i=>{let d=p(i.hash),c=i.parentElement;return d&&c?[{heading:d,link:i,item:c}]:[]})}]}),w=E.at(0);if(w){let e=()=>{let t=Number.parseFloat(getComputedStyle(w.heading).scrollMarginTop)+1,n=E.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t)??w,i=n.subs.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t),d=E.indexOf(n),c=i===void 0?-1:n.subs.indexOf(i);for(let[l,a]of E.entries()){let u=a===n&&i===void 0;a.link.classList.toggle("wg_shellToc__link--active",u),u?a.link.setAttribute("aria-current","location"):a.link.removeAttribute("aria-current"),a.item.classList.toggle("wg_shellToc__item--trail",a.list===n.list&&l<d),a.item.classList.toggle("wg_shellToc__item--current",a===n);for(let[h,r]of a.subs.entries())r.link.classList.toggle("wg_shellToc__subLink--active",r===i),r===i?r.link.setAttribute("aria-current","location"):r.link.removeAttribute("aria-current"),r.item.classList.toggle("wg_shellToc__subItem--trail",a===n&&h<c),r.item.classList.toggle("wg_shellToc__subItem--current",r===i)}};addEventListener("scroll",e,{passive:!0}),addEventListener("resize",e),addEventListener("toggle",e,!0),e()}var I=new AbortController;for(let e of["wheel","touchmove","keydown","mousedown"])addEventListener(e,()=>I.abort(),{once:!0,passive:!0});var A=async()=>{await document.fonts.ready,v&&!I.signal.aborted&&(v.scrollIntoView({behavior:"instant"}),M(v)),document.documentElement.style.removeProperty("scroll-behavior")};document.readyState==="complete"?A():addEventListener("load",A,{once:!0});
6
+ )`,f.classList.add("wg_shell__cursorFace--visible")}))},u=g=>{if(!(e.matches&&g.pointerType==="mouse")){n();return}t.x=g.clientX,t.y=g.clientY,s()},d=()=>{f.classList.contains("wg_shell__cursorFace--visible")&&s()},l=()=>{if(f.classList.contains("wg_shell__cursorFace--visible"))try{sessionStorage.setItem(T,JSON.stringify({x:t.x,y:t.y}))}catch{}},m=()=>{try{let g=sessionStorage.getItem(T);if(sessionStorage.removeItem(T),g!==null&&e.matches){let i=JSON.parse(g);typeof i=="object"&&i!==null&&"x"in i&&"y"in i&&typeof i.x=="number"&&typeof i.y=="number"&&Number.isFinite(i.x)&&Number.isFinite(i.y)&&(t.x=i.x,t.y=i.y,s())}}catch{}};m(),document.addEventListener("pointermove",u,{passive:!0}),document.addEventListener("pointerdown",u,{passive:!0}),document.documentElement.addEventListener("pointerleave",n),addEventListener("blur",n),addEventListener("resize",d),addEventListener("pagehide",l),addEventListener("pageshow",m),e.addEventListener("change",a)}var k=y(location.hash),E=null,S=0,v=null,C=()=>{clearTimeout(S),v?.abort(),v=null,E?.classList.remove("wg_prose__headingText--highlighted"),E=null},q=e=>{C(),E=e===null?null:e.querySelector(".wg_prose__headingText"),E!==null&&(E.classList.add("wg_prose__headingText--highlighted"),S=window.setTimeout(C,3e3),v=new AbortController,document.addEventListener("scrollend",P,{signal:v.signal}))},P=e=>{e.target===document&&(clearTimeout(S),v?.abort(),v=null,S=window.setTimeout(C,3e3))},R=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=y(e.currentTarget.hash);e.currentTarget.hash===location.hash&&q(t)}},$=()=>{let e=y(location.hash);e?.scrollIntoView(),q(e)};for(let e of document.querySelectorAll("a[href]"))e.addEventListener("click",R);addEventListener("hashchange",$);var H=[...document.querySelectorAll("[data-toc-link]")].flatMap(e=>{let t=b(e.hash),n=e.parentElement;return!t||!n?[]:[{heading:t,link:e,item:n,list:n.parentElement,subs:[...n.querySelectorAll("[data-toc-sub-link]")].flatMap(a=>{let s=b(a.hash),u=a.parentElement;return s&&u?[{heading:s,link:a,item:u}]:[]})}]}),A=H.at(0);if(A){let e=()=>{let t=Number.parseFloat(getComputedStyle(A.heading).scrollMarginTop)+1,n=H.findLast(d=>d.heading.checkVisibility()&&d.heading.getBoundingClientRect().top<=t)??A,a=n.subs.findLast(d=>d.heading.checkVisibility()&&d.heading.getBoundingClientRect().top<=t),s=H.indexOf(n),u=a===void 0?-1:n.subs.indexOf(a);for(let[d,l]of H.entries()){let m=l===n&&a===void 0;l.link.classList.toggle("wg_shellToc__link--active",m),m?l.link.setAttribute("aria-current","location"):l.link.removeAttribute("aria-current"),l.item.classList.toggle("wg_shellToc__item--trail",l.list===n.list&&d<s),l.item.classList.toggle("wg_shellToc__item--current",l===n);for(let[g,i]of l.subs.entries())i.link.classList.toggle("wg_shellToc__subLink--active",i===a),i===a?i.link.setAttribute("aria-current","location"):i.link.removeAttribute("aria-current"),i.item.classList.toggle("wg_shellToc__subItem--trail",l===n&&g<u),i.item.classList.toggle("wg_shellToc__subItem--current",i===a)}};addEventListener("scroll",e,{passive:!0}),addEventListener("resize",e),addEventListener("toggle",e,!0),e()}var K=new AbortController;for(let e of["wheel","touchmove","keydown","mousedown"])addEventListener(e,()=>K.abort(),{once:!0,passive:!0});var F=async()=>{await document.fonts.ready,k&&!K.signal.aborted&&(k.scrollIntoView({behavior:"instant"}),q(k)),document.documentElement.style.removeProperty("scroll-behavior")};document.readyState==="complete"?F():addEventListener("load",F,{once:!0});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "for-humanity",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Documents written for humans to read: a docs kit with a sidebar, parts, sections and flowcharts, built on Hono and React",
5
5
  "keywords": [
6
6
  "docs",
package/src/client.ts CHANGED
@@ -7,10 +7,12 @@ import {heading_highlight_hold_ms} from "@/component/widget/prose/_constant/head
7
7
  import {cursor_face_offset_px, cursor_face_storage_key} from "@/component/widget/shell/_constant/cursor-face";
8
8
  import {reading_line_slack_px} from "@/component/widget/shell/_constant/reading-line";
9
9
  import {bindMobileNavigation} from "@/component/widget/shell/_function/bind-mobile-navigation";
10
+ import {bindNavigationScroll} from "@/component/widget/shell/_function/bind-navigation-scroll";
10
11
  import {findHashTarget} from "@/util/dom/find-hash-target";
11
12
  import {revealHashTarget} from "@/util/dom/reveal-hash-target";
12
13
 
13
14
  bindMobileNavigation();
15
+ bindNavigationScroll();
14
16
 
15
17
  const cursorFace = document.querySelector<HTMLImageElement>("[data-cursor-face]");
16
18
 
@@ -2,3 +2,8 @@
2
2
  * 모바일 탐색 전환 · wg-shell.css의 폭 조건과 일치
3
3
  */
4
4
  export const navigation_mobile_query = "(width < 1024px)";
5
+
6
+ /**
7
+ * 같은 탭에서 문서 이동 후 탐색 위치 복원 · 데스크톱과 드로어는 별도 저장
8
+ */
9
+ export const navigation_scroll_storage_key = "for-humanity:navigation-scroll";
@@ -0,0 +1,123 @@
1
+ import {navigation_mobile_query, navigation_scroll_storage_key} from "@/component/widget/shell/_constant/navigation";
2
+
3
+ /**
4
+ * 문서 이동·새로고침에서 공유 탐색의 위치 유지 · 본문 스크롤과 독립
5
+ * 저장소 실패는 기본 탐색 유지, 문서 목록 변경은 이전 위치를 사용하지 않음
6
+ */
7
+ export const bindNavigationScroll = () => {
8
+ const navigation = document.querySelector<HTMLElement>("[data-navigation]");
9
+ const sidebar = document.querySelector<HTMLElement>(".wg_shell__sidebar");
10
+ const dialog = document.querySelector<HTMLDialogElement>("[data-navigation-drawer]");
11
+ const openButton = document.querySelector<HTMLButtonElement>("[data-navigation-open]");
12
+
13
+ if (!navigation || !sidebar || !dialog || !openButton) {
14
+ return;
15
+ }
16
+
17
+ const mobile = matchMedia(navigation_mobile_query);
18
+ const layout = {mobile: mobile.matches};
19
+ // 목차와 현재 상태는 페이지마다 바뀜 · 공통 문서 링크의 순서로 저장 위치의 유효성 판단
20
+ 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
+
28
+ for (const [port, key] of storageKeys) {
29
+ try {
30
+ const value = sessionStorage.getItem(key);
31
+
32
+ if (value !== null) {
33
+ const stored: unknown = JSON.parse(value);
34
+
35
+ if (
36
+ typeof stored === "object" &&
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
+ }
48
+ } catch {
49
+ // 차단되거나 손상된 저장소는 복원만 생략 · 스크롤과 기본 링크 이동 유지
50
+ }
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);
57
+
58
+ /**
59
+ * 네이티브 스크롤 범위로 복원 · 닫힌 드로어의 0 좌표는 복원하지 않음
60
+ */
61
+ const restoreScroll = () => {
62
+ layout.mobile = mobile.matches;
63
+ const port = mobile.matches ? dialog : sidebar;
64
+ const top = positions.get(port);
65
+
66
+ if (top === undefined || (mobile.matches && !dialog.open)) {
67
+ return;
68
+ }
69
+
70
+ port.scrollTop = top;
71
+ };
72
+
73
+ /**
74
+ * 드로어 닫기 이전 클릭·현재 스크롤·페이지 이탈에서 마지막 탐색 위치 저장
75
+ */
76
+ const saveScroll = (event: Event) => {
77
+ const port = mobile.matches ? dialog : sidebar;
78
+ const key = storageKeys.get(port);
79
+
80
+ // 반응형 전환의 숨김·닫기 스크롤은 사용자 위치가 아님 · 활성 포트만 저장
81
+ if (layout.mobile !== mobile.matches || (event.type === "scroll" && event.currentTarget !== port) || key === undefined || (mobile.matches && !dialog.open)) {
82
+ return;
83
+ }
84
+
85
+ positions.set(port, port.scrollTop);
86
+
87
+ try {
88
+ sessionStorage.setItem(key, JSON.stringify({navigation: signature, top: port.scrollTop}));
89
+ } catch {
90
+ // 저장 실패도 같은 페이지의 드로어 재열기와 반응형 위치 복원은 유지
91
+ }
92
+ };
93
+
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
+ };
109
+
110
+ sidebar.addEventListener("scroll", saveScroll, {passive: true});
111
+ dialog.addEventListener("scroll", saveScroll, {passive: true});
112
+ navigation.addEventListener("click", saveScroll, {capture: true});
113
+ openButton.addEventListener("click", restoreScroll);
114
+ mobile.addEventListener("change", restoreScroll);
115
+ addEventListener("pagehide", saveScroll);
116
+ restoreScroll();
117
+
118
+ if (document.readyState === "complete") {
119
+ settleScroll();
120
+ } else {
121
+ addEventListener("load", settleScroll, {once: true});
122
+ }
123
+ };
@@ -115,7 +115,34 @@
115
115
  max-height: 100vh;
116
116
  margin-inline-end: var(--app-space-sidebar-gap);
117
117
  overflow-y: auto;
118
+ overscroll-behavior-y: contain;
119
+
120
+ /* 얇은 네이티브 손잡이 · 트랙 없이 기존 본문 간격 안쪽에 표시 */
121
+ scrollbar-gutter: stable;
122
+ scrollbar-width: auto;
123
+ scrollbar-color: auto;
118
124
  scroll-padding-block: var(--app-size-brand) var(--app-space-block);
125
+
126
+ &::-webkit-scrollbar {
127
+ width: 3px;
128
+ }
129
+
130
+ &::-webkit-scrollbar-track {
131
+ background: transparent;
132
+ }
133
+
134
+ &::-webkit-scrollbar-thumb {
135
+ border-radius: 3px;
136
+ background: transparent;
137
+ }
138
+
139
+ &:hover::-webkit-scrollbar-thumb {
140
+ background: var(--app-color-border-soft);
141
+ }
142
+
143
+ &:focus-within::-webkit-scrollbar-thumb {
144
+ background: var(--app-color-border-soft);
145
+ }
119
146
  }
120
147
 
121
148
  .wg_shell__navigation {
@@ -147,9 +174,33 @@
147
174
  border-right: 1px solid var(--app-color-border-layout);
148
175
  background: var(--app-color-ground);
149
176
  color: var(--app-color-text);
177
+ scrollbar-gutter: stable;
178
+ scrollbar-width: auto;
179
+ scrollbar-color: auto;
150
180
  overscroll-behavior: contain;
151
181
  scroll-padding-block: var(--app-size-mobile-header) var(--app-space-group);
152
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
+ }
203
+
153
204
  &::backdrop {
154
205
  background: var(--app-color-backdrop);
155
206
  }
@@ -219,6 +270,57 @@
219
270
  visibility: visible;
220
271
  }
221
272
 
273
+ /* Firefox는 표준 thin 사용 · 손잡이 폭은 브라우저가 결정 */
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
+ /* 색상 강제 환경은 시스템 손잡이 유지 · 대비 설정을 덮어쓰지 않음 */
295
+ @media (forced-colors: active) {
296
+ .wg_shell__sidebar {
297
+ /* non-auto 표준 폭은 WebKit 전용 스타일을 비활성화해 네이티브 색상 유지 */
298
+ scrollbar-width: thin;
299
+ 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
+ }
322
+ }
323
+
222
324
  /* 좁은 화면 · 상단 브랜드와 메뉴 버튼 · 탐색 목록은 네이티브 모달 */
223
325
  @media (width < 1024px) {
224
326
  .wg_shell__root {