1- import { memo , useCallback , useEffect , useMemo , useState } from 'react' ;
1+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
22
33import {
44 Accessibility ,
@@ -72,6 +72,7 @@ const SettingsPage = memo(function SettingsPage() {
7272 requestedTab && validTabIds . has ( requestedTab ) ? requestedTab : defaultTab ;
7373
7474 const [ activeTab , setActiveTab ] = useState ( initialTab ) ;
75+ const contentRef = useRef < HTMLDivElement > ( null ) ;
7576
7677 // Redirect a retired tab key (e.g. ?tab=developer, ?tab=export) to the default
7778 // tab so old bookmarks land on a valid section instead of an empty area.
@@ -95,6 +96,8 @@ const SettingsPage = memo(function SettingsPage() {
9596 ( tabId : string ) => {
9697 setActiveTab ( tabId ) ;
9798 setSearchParams ( { tab : tabId } ) ;
99+ // Each section starts at its top, not wherever the previous one was left.
100+ contentRef . current ?. scrollTo ( { top : 0 } ) ;
98101 } ,
99102 [ setSearchParams ]
100103 ) ;
@@ -104,18 +107,17 @@ const SettingsPage = memo(function SettingsPage() {
104107 . find ( ( item ) => item . id === activeTab ) ?. label ;
105108
106109 return (
107- < div
108- data-page-scroll
109- className = "min-h-full bg-bg md:h-full md:max-h-full md:overflow-y-auto"
110- >
110+ < div className = "min-h-full bg-bg md:h-full md:max-h-full" >
111111 < Seo name = { activeTabLabel } noindex />
112- { /* Two-column shell, constrained to the navbar's width so the page reads
113- as one column under the bar: a sticky left section rail (always
114- visible, never collapses) and a scrolling content column on the right
115- (GitHub-settings pattern). */ }
116- < div className = "page-container flex flex-col gap-6 py-6 sm:py-8 md:flex-row md:gap-8 lg:gap-10" >
117- < div className = "shrink-0 mb-6 md:mb-0 md:border-r md:border-border md:pr-8 md:w-52 lg:w-56" >
118- < div className = "md:sticky md:top-8" >
112+ { /* Two-column split pane, constrained to the navbar's width so the page
113+ reads as one column under the bar (GitHub-settings pattern). At lg+
114+ the shell locks the outer page, the row fills the visible height and
115+ ONLY the right content column scrolls — the rail and the separator
116+ stay put. Below lg the window scrolls and the rail falls back to
117+ `sticky`, offset past the fixed navbar. */ }
118+ < div className = "page-container flex flex-col gap-6 py-6 sm:py-8 md:h-full md:min-h-0 md:flex-row md:gap-8 lg:gap-10" >
119+ < div className = "shrink-0 mb-6 md:mb-0 md:w-52 lg:w-56" >
120+ < div className = "md:sticky md:top-[calc(var(--navbar-height)+1.5rem)]" >
119121 < PageTabs
120122 groups = { groups }
121123 activeId = { activeTab }
@@ -125,10 +127,24 @@ const SettingsPage = memo(function SettingsPage() {
125127 </ div >
126128 </ div >
127129
130+ { /* Separator as its own flex child (not a border on the rail): at lg+
131+ it spans the row's padded content box, so it starts below the navbar
132+ and stops above the bottom instead of running edge to edge. */ }
133+ < div
134+ aria-hidden = "true"
135+ className = "hidden md:block w-px shrink-0 self-stretch bg-border"
136+ />
137+
128138 { /* Scroll region, NOT a landmark: the app shell already owns the single
129139 `<main id="main-content">`. A second `<main>` is an ARIA landmark
130140 violation (1.3.1). `role="region"` + label keeps it navigable. */ }
131- < div role = "region" aria-label = "Settings" className = "min-w-0 flex-1" >
141+ < div
142+ ref = { contentRef }
143+ data-page-scroll
144+ role = "region"
145+ aria-label = "Settings"
146+ className = "min-w-0 flex-1 md:min-h-0 lg:overflow-y-auto"
147+ >
132148 { activeTab === 'profile' && < AccountSection /> }
133149 { activeTab === 'appearance' && < AppearanceSection /> }
134150 { activeTab === 'board' && < BoardSection /> }
0 commit comments