@@ -10,44 +10,47 @@ import { edit as editNotifications } from '@/routes/user-notifications';
1010import { edit } from '@/routes/user-profile' ;
1111import { type NavItem } from '@/types' ;
1212import { Link } from '@inertiajs/react' ;
13- import { type PropsWithChildren } from 'react' ;
13+ import { Bell , Lock , Palette , Puzzle , ShieldCheck , User } from 'lucide-react' ;
14+ import { type PropsWithChildren , useEffect , useRef } from 'react' ;
1415import { useTranslation } from 'react-i18next' ;
1516
1617const getSidebarNavItems = ( t : ( key : string ) => string ) : NavItem [ ] => [
1718 {
1819 title : t ( 'settings_layout.nav.profile' ) ,
1920 href : edit ( ) ,
20- icon : null ,
21+ icon : User ,
2122 } ,
2223 {
2324 title : t ( 'settings_layout.nav.password' ) ,
2425 href : editPassword ( ) ,
25- icon : null ,
26+ icon : Lock ,
2627 } ,
2728 {
2829 title : t ( 'settings_layout.nav.two_factor' ) ,
2930 href : show ( ) ,
30- icon : null ,
31+ icon : ShieldCheck ,
3132 } ,
3233 {
3334 title : t ( 'settings_layout.nav.notifications' ) ,
3435 href : editNotifications ( ) ,
35- icon : null ,
36+ icon : Bell ,
3637 } ,
3738 {
3839 title : t ( 'settings_layout.nav.appearance' ) ,
3940 href : editAppearance ( ) ,
40- icon : null ,
41+ icon : Palette ,
4142 } ,
4243 {
4344 title : t ( 'settings_layout.nav.integrations' ) ,
4445 href : editIntegrations ( ) ,
45- icon : null ,
46+ icon : Puzzle ,
4647 } ,
4748] ;
4849
4950export default function SettingsLayout ( { children } : PropsWithChildren ) {
5051 const { t } = useTranslation ( 'common' ) ;
52+ const scrollRef = useRef < HTMLDivElement > ( null ) ;
53+
5154 // When server-side rendering, we only render the layout on the client...
5255 if ( typeof window === 'undefined' ) {
5356 return null ;
@@ -56,6 +59,26 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
5659 const currentPath = window . location . pathname ;
5760 const sidebarNavItems = getSidebarNavItems ( t ) ;
5861
62+ const isActive = ( item : NavItem ) =>
63+ currentPath ===
64+ ( typeof item . href === 'string' ? item . href : item . href . url ) ;
65+
66+ // Auto-scroll the active tab into view on mobile
67+ useEffect ( ( ) => {
68+ if ( scrollRef . current ) {
69+ const activeEl = scrollRef . current . querySelector (
70+ '[data-active="true"]' ,
71+ ) ;
72+ if ( activeEl ) {
73+ activeEl . scrollIntoView ( {
74+ behavior : 'smooth' ,
75+ inline : 'center' ,
76+ block : 'nearest' ,
77+ } ) ;
78+ }
79+ }
80+ } , [ currentPath ] ) ;
81+
5982 return (
6083 < div className = "px-4 py-6" >
6184 < Heading
@@ -64,20 +87,49 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
6487 />
6588
6689 < div className = "flex flex-col lg:flex-row lg:space-x-12" >
67- < aside className = "w-full max-w-xl lg:w-48" >
68- < nav className = "flex flex-col space-y-1 space-x-0" >
90+ { /* Mobile: Horizontal scrollable pill tab bar */ }
91+ < div className = "relative lg:hidden" >
92+ < div
93+ ref = { scrollRef }
94+ className = "no-scrollbar flex gap-2 overflow-x-auto pb-4"
95+ >
96+ { sidebarNavItems . map ( ( item , index ) => {
97+ const active = isActive ( item ) ;
98+ return (
99+ < Link
100+ key = { `${ typeof item . href === 'string' ? item . href : item . href . url } -${ index } ` }
101+ href = { item . href }
102+ data-active = { active }
103+ className = { cn (
104+ 'inline-flex shrink-0 items-center gap-1.5 rounded-full border px-3.5 py-2 text-sm font-medium transition-all duration-200' ,
105+ active
106+ ? 'border-primary bg-primary text-primary-foreground shadow-sm'
107+ : 'border-border bg-background text-muted-foreground hover:border-foreground/20 hover:bg-accent hover:text-foreground' ,
108+ ) }
109+ >
110+ { item . icon && (
111+ < item . icon className = "h-4 w-4" />
112+ ) }
113+ { item . title }
114+ </ Link >
115+ ) ;
116+ } ) }
117+ </ div >
118+ { /* Right fade gradient for scroll hint */ }
119+ < div className = "pointer-events-none absolute top-0 right-0 h-full w-8 bg-linear-to-l from-background to-transparent" />
120+ </ div >
121+
122+ { /* Desktop: Vertical sidebar nav */ }
123+ < aside className = "hidden w-48 lg:block" >
124+ < nav className = "flex flex-col gap-1" >
69125 { sidebarNavItems . map ( ( item , index ) => (
70126 < Button
71127 key = { `${ typeof item . href === 'string' ? item . href : item . href . url } -${ index } ` }
72128 size = "sm"
73129 variant = "ghost"
74130 asChild
75131 className = { cn ( 'w-full justify-start' , {
76- 'bg-muted' :
77- currentPath ===
78- ( typeof item . href === 'string'
79- ? item . href
80- : item . href . url ) ,
132+ 'bg-muted' : isActive ( item ) ,
81133 } ) }
82134 >
83135 < Link href = { item . href } >
@@ -91,7 +143,7 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
91143 </ nav >
92144 </ aside >
93145
94- < Separator className = "my-6 lg: hidden" />
146+ < Separator className = "my-6 hidden" />
95147
96148 < div className = "flex-1 md:max-w-2xl" >
97149 < section className = "max-w-xl space-y-12" >
0 commit comments