11import { useState } from 'react' ;
2- import { Link , NavLink } from 'react-router' ;
2+ import { Link , NavLink , useLocation } from 'react-router' ;
33import { Button } from '@/components/ui/button' ;
44import {
55 DropdownMenu ,
@@ -8,11 +8,21 @@ import {
88 DropdownMenuSeparator ,
99 DropdownMenuTrigger ,
1010} from '@/components/ui/dropdown-menu' ;
11- import { Sheet , SheetContent , SheetTrigger } from '@/components/ui/sheet' ;
11+ import {
12+ Sheet ,
13+ SheetContent ,
14+ SheetDescription ,
15+ SheetHeader ,
16+ SheetTitle ,
17+ SheetTrigger ,
18+ } from '@/components/ui/sheet' ;
1219import { Separator } from '@/components/ui/separator' ;
1320import { SearchBox } from '@/components/SearchBox' ;
21+ import { GitHubIcon } from '@/components/icons/GitHubIcon' ;
1422import { useAuth } from '@/hooks/useAuth' ;
1523
24+ const GITHUB_URL = 'https://github.com/CodeForPhilly' ;
25+
1626function ChevronDownIcon ( ) {
1727 return (
1828 < svg
@@ -60,7 +70,7 @@ function AuthControls({ mobile = false }: { mobile?: boolean }) {
6070 return (
6171 < div
6272 className = { `h-8 ${ mobile ? 'w-full' : 'w-20' } bg-muted animate-pulse rounded` }
63- aria-label = "Loading auth state "
73+ aria-hidden = "true "
6474 />
6575 ) ;
6676 }
@@ -143,12 +153,8 @@ function AboutDropdown() {
143153 return (
144154 < DropdownMenu >
145155 < DropdownMenuTrigger asChild >
146- < Button
147- variant = "ghost"
148- size = "sm"
149- className = "flex items-center gap-1"
150- aria-label = "About menu"
151- >
156+ { /* No aria-label: the visible "About" text is the accessible name. */ }
157+ < Button variant = "ghost" size = "sm" className = "flex items-center gap-1" >
152158 About < ChevronDownIcon />
153159 </ Button >
154160 </ DropdownMenuTrigger >
@@ -181,8 +187,34 @@ const navLinkClass = ({ isActive }: { isActive: boolean }) =>
181187 isActive ? 'text-primary' : 'text-muted-foreground'
182188 } `;
183189
190+ function GitHubLink ( ) {
191+ return (
192+ // Desktop-only: between md and lg the header has no room for it (the
193+ // utility cluster would push "Help Wanted" onto two lines); the mobile
194+ // sheet carries its own GitHub row.
195+ < Button asChild variant = "ghost" size = "icon-sm" className = "hidden lg:inline-flex" >
196+ < a
197+ href = { GITHUB_URL }
198+ target = "_blank"
199+ rel = "noopener noreferrer"
200+ aria-label = "Code for Philly on GitHub"
201+ >
202+ < GitHubIcon />
203+ </ a >
204+ </ Button >
205+ ) ;
206+ }
207+
184208export function AppHeader ( ) {
185- const [ mobileOpen , setMobileOpen ] = useState ( false ) ;
209+ const location = useLocation ( ) ;
210+ // The sheet is open only for the location it was opened at, so any
211+ // client-side navigation — a NavLink or Enter in the inline search —
212+ // closes it without per-item onClick closers. Derived during render
213+ // rather than synced in an effect (react-hooks/set-state-in-effect).
214+ const [ openedAtKey , setOpenedAtKey ] = useState < string | null > ( null ) ;
215+ const mobileOpen = openedAtKey === location . key ;
216+ const setMobileOpen = ( open : boolean ) =>
217+ setOpenedAtKey ( open ? location . key : null ) ;
186218
187219 return (
188220 < header className = "sticky top-0 z-40 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 shadow-sm print:hidden" >
@@ -201,135 +233,125 @@ export function AppHeader() {
201233 />
202234 </ Link >
203235
204- { /* Desktop nav */ }
236+ { /* Desktop content cluster. The parent gap is the only source of
237+ spacing between children — no per-child margins. */ }
205238 < nav
206239 aria-label = "Primary navigation"
207- className = "hidden md:flex items-center gap-1 ml-4 flex-1"
240+ className = "hidden md:flex items-center gap-2 ml-4 flex-1"
208241 >
209242 < NavLink to = "/projects" className = { navLinkClass } >
210243 Projects
211244 </ NavLink >
212- < NavLink
213- to = "/help-wanted"
214- className = { ( { isActive } ) => navLinkClass ( { isActive } ) + ' ml-1' }
215- >
245+ < NavLink to = "/help-wanted" className = { navLinkClass } >
216246 Help Wanted
217247 </ NavLink >
218- < NavLink
219- to = "/members"
220- className = { ( { isActive } ) => navLinkClass ( { isActive } ) + ' ml-1' }
221- >
248+ < NavLink to = "/members" className = { navLinkClass } >
222249 Members
223250 </ NavLink >
224- < Button asChild size = "sm" className = "ml-1 bg-green-600 hover:bg-green-700 text-white" >
225- < NavLink to = "/volunteer" > Volunteer</ NavLink >
226- </ Button >
227- < div className = "ml-1" >
228- < AboutDropdown />
229- </ div >
251+ < AboutDropdown />
230252 </ nav >
231253
232- { /* Desktop: search + auth */ }
233- < div className = "hidden md:flex items-center gap-2 ml-auto" >
254+ { /* Desktop utility cluster: GitHub, search, auth, then the Volunteer
255+ CTA pinned rightmost (specs/behaviors/app-shell.md). */ }
256+ < nav
257+ aria-label = "Utility"
258+ className = "hidden md:flex items-center gap-2 ml-auto"
259+ >
260+ < GitHubLink />
234261 < SearchBox />
235262 < AuthControls />
236- </ div >
263+ < Button
264+ asChild
265+ size = "sm"
266+ className = "bg-green-600 hover:bg-green-700 text-white"
267+ >
268+ < NavLink to = "/volunteer" > Volunteer</ NavLink >
269+ </ Button >
270+ </ nav >
237271
238272 { /* Mobile: auth + hamburger */ }
239273 < div className = "flex md:hidden items-center gap-2 ml-auto" >
240274 < AuthControls />
241275 < Sheet open = { mobileOpen } onOpenChange = { setMobileOpen } >
242276 < SheetTrigger asChild >
277+ { /* No aria-expanded here — Radix's Dialog.Trigger supplies it. */ }
243278 < Button
244279 variant = "ghost"
245280 size = "sm"
246281 aria-label = "Open navigation menu"
247- aria-expanded = { mobileOpen }
248282 >
249283 < MenuIcon />
250284 </ Button >
251285 </ SheetTrigger >
252- < SheetContent side = "right" className = "w-72 flex flex-col gap-4 pt-8" >
286+ < SheetContent side = "right" >
287+ { /* SheetHeader/SheetTitle carry the panel's own padding and give
288+ the underlying Radix dialog its accessible name; the
289+ visually-hidden description satisfies aria-describedby. */ }
290+ < SheetHeader className = "pb-0" >
291+ < SheetTitle > Menu</ SheetTitle >
292+ < SheetDescription className = "sr-only" >
293+ Site navigation
294+ </ SheetDescription >
295+ </ SheetHeader >
296+ { /* min-h-0 + overflow-y-auto so the list stays reachable on
297+ short viewports instead of overflowing the panel. */ }
253298 < nav
254299 aria-label = "Mobile navigation"
255- className = "flex flex-col gap-2"
300+ className = "flex flex-col gap-2 px-4 min-h-0 overflow-y-auto "
256301 >
257- < NavLink
258- to = "/projects"
259- className = { navLinkClass }
260- onClick = { ( ) => setMobileOpen ( false ) }
261- >
302+ < NavLink to = "/projects" className = { navLinkClass } >
262303 Projects
263304 </ NavLink >
264- < NavLink
265- to = "/help-wanted"
266- className = { navLinkClass }
267- onClick = { ( ) => setMobileOpen ( false ) }
268- >
305+ < NavLink to = "/help-wanted" className = { navLinkClass } >
269306 Help Wanted
270307 </ NavLink >
271- < NavLink
272- to = "/members"
273- className = { navLinkClass }
274- onClick = { ( ) => setMobileOpen ( false ) }
275- >
308+ < NavLink to = "/members" className = { navLinkClass } >
276309 Members
277310 </ NavLink >
278- < NavLink
279- to = "/volunteer"
280- className = { navLinkClass }
281- onClick = { ( ) => setMobileOpen ( false ) }
282- >
283- Volunteer
284- </ NavLink >
285311 < Separator />
286312 < p className = "text-xs text-muted-foreground font-medium uppercase tracking-wide" >
287313 About
288314 </ p >
289- < NavLink
290- to = "/pages/mission"
291- className = { navLinkClass }
292- onClick = { ( ) => setMobileOpen ( false ) }
293- >
315+ < NavLink to = "/pages/mission" className = { navLinkClass } >
294316 Mission
295317 </ NavLink >
296- < NavLink
297- to = "/pages/leadership"
298- className = { navLinkClass }
299- onClick = { ( ) => setMobileOpen ( false ) }
300- >
318+ < NavLink to = "/pages/leadership" className = { navLinkClass } >
301319 Leadership
302320 </ NavLink >
303- < NavLink
304- to = "/pages/code-of-conduct"
305- className = { navLinkClass }
306- onClick = { ( ) => setMobileOpen ( false ) }
307- >
321+ < NavLink to = "/pages/code-of-conduct" className = { navLinkClass } >
308322 Code of Conduct
309323 </ NavLink >
310- < NavLink
311- to = "/pages/hackathons"
312- className = { navLinkClass }
313- onClick = { ( ) => setMobileOpen ( false ) }
314- >
324+ < NavLink to = "/pages/hackathons" className = { navLinkClass } >
315325 Hackathons
316326 </ NavLink >
317- < NavLink
318- to = "/sponsor"
319- className = { navLinkClass }
320- onClick = { ( ) => setMobileOpen ( false ) }
321- >
327+ < NavLink to = "/sponsor" className = { navLinkClass } >
322328 Sponsor
323329 </ NavLink >
324330 < a
325331 href = "mailto:hello@codeforphilly.org"
326332 className = "text-sm font-medium text-muted-foreground hover:text-primary"
333+ onClick = { ( ) => setMobileOpen ( false ) }
327334 >
328335 Contact
329336 </ a >
337+ < Separator />
338+ < a
339+ href = { GITHUB_URL }
340+ target = "_blank"
341+ rel = "noopener noreferrer"
342+ className = "text-sm font-medium text-muted-foreground hover:text-primary"
343+ onClick = { ( ) => setMobileOpen ( false ) }
344+ >
345+ GitHub
346+ </ a >
347+ < NavLink to = "/volunteer" className = { navLinkClass } >
348+ Volunteer
349+ </ NavLink >
330350 </ nav >
331351 < Separator />
332- < SearchBox inline />
352+ < div className = "px-4 pb-4" >
353+ < SearchBox inline />
354+ </ div >
333355 </ SheetContent >
334356 </ Sheet >
335357 </ div >
0 commit comments