@@ -11,6 +11,39 @@ withDefaults(
1111const { isConnected, npmUser } = useConnector ()
1212
1313const showFullSearch = shallowRef (false )
14+ const showMobileMenu = shallowRef (false )
15+
16+ // On mobile, clicking logo+search button expands search
17+ const route = useRoute ()
18+ const isMobile = useIsMobile ()
19+ const isSearchExpandedManually = shallowRef (false )
20+ const searchBoxRef = shallowRef <{ focus: () => void } | null >(null )
21+
22+ // On search page, always show search expanded on mobile
23+ const isOnSearchPage = computed (() => route .name === ' search' )
24+ const isSearchExpanded = computed (() => isOnSearchPage .value || isSearchExpandedManually .value )
25+
26+ function expandMobileSearch() {
27+ isSearchExpandedManually .value = true
28+ nextTick (() => {
29+ searchBoxRef .value ?.focus ()
30+ })
31+ }
32+
33+ function handleSearchBlur() {
34+ showFullSearch .value = false
35+ // Collapse expanded search on mobile after blur (with delay for click handling)
36+ // But don't collapse if we're on the search page
37+ if (isMobile .value && ! isOnSearchPage .value ) {
38+ setTimeout (() => {
39+ isSearchExpandedManually .value = false
40+ }, 150 )
41+ }
42+ }
43+
44+ function handleSearchFocus() {
45+ showFullSearch .value = true
46+ }
1447
1548onKeyStroke (
1649 ' ,' ,
@@ -32,43 +65,66 @@ onKeyStroke(
3265 <header class =" sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border" >
3366 <nav
3467 :aria-label =" $t('nav.main_navigation')"
35- class =" container min-h-14 flex items-center justify-start "
68+ class =" container min-h-14 flex items-center justify-between gap-2 "
3669 >
37- <!-- Start: Logo -->
38- <div :class =" { 'hidden sm:block': showFullSearch }" class =" flex-shrink-0" >
39- <div v-if =" showLogo" class =" flex items-center" >
40- <NuxtLink
41- to =" /"
42- :aria-label =" $t('header.home')"
43- dir =" ltr"
44- class =" inline-flex items-center gap-2 header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
45- >
46- <img
47- aria-hidden =" true"
48- :alt =" $t('alt_logo')"
49- src =" /logo.svg"
50- width =" 96"
51- height =" 96"
52- class =" w-8 h-8 rounded-lg"
53- />
54- <span >npmx</span >
55- </NuxtLink >
56- </div >
57- <!-- Spacer when logo is hidden -->
58- <span v-else class =" w-1" />
70+ <!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
71+ <button
72+ v-if =" !isSearchExpanded"
73+ type =" button"
74+ class =" sm:hidden flex-shrink-0 inline-flex items-center gap-2 font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
75+ :aria-label =" $t('nav.tap_to_search')"
76+ @click =" expandMobileSearch"
77+ >
78+ <img
79+ aria-hidden =" true"
80+ :alt =" $t('alt_logo')"
81+ src =" /logo.svg"
82+ width =" 96"
83+ height =" 96"
84+ class =" w-8 h-8 rounded-lg"
85+ />
86+ <span class =" i-carbon:search w-4 h-4 text-fg-subtle" aria-hidden =" true" />
87+ </button >
88+
89+ <!-- Desktop: Logo (navigates home) -->
90+ <div v-if =" showLogo" class =" hidden sm:flex flex-shrink-0 items-center" >
91+ <NuxtLink
92+ to =" /"
93+ :aria-label =" $t('header.home')"
94+ dir =" ltr"
95+ class =" inline-flex items-center gap-2 header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
96+ >
97+ <img
98+ aria-hidden =" true"
99+ :alt =" $t('alt_logo')"
100+ src =" /logo.svg"
101+ width =" 96"
102+ height =" 96"
103+ class =" w-8 h-8 rounded-lg"
104+ />
105+ <span >npmx</span >
106+ </NuxtLink >
59107 </div >
108+ <!-- Spacer when logo is hidden on desktop -->
109+ <span v-else class =" hidden sm:block w-1" />
60110
61111 <!-- Center: Search bar + nav items -->
62- <div class =" flex-1 flex items-center justify-center md:gap-6 mx-2" >
63- <!-- Search bar (shown on all pages except home) -->
112+ <div
113+ class =" flex-1 flex items-center justify-center md:gap-6"
114+ :class =" { 'hidden sm:flex': !isSearchExpanded }"
115+ >
116+ <!-- Search bar (hidden on mobile unless expanded) -->
64117 <SearchBox
65- :inputClass =" showFullSearch ? '' : 'max-w[6rem]'"
66- @focus =" showFullSearch = true"
67- @blur =" showFullSearch = false"
118+ ref =" searchBoxRef"
119+ :inputClass =" isSearchExpanded ? 'w-full' : ''"
120+ :class =" { 'max-w-md': !isSearchExpanded }"
121+ @focus =" handleSearchFocus"
122+ @blur =" handleSearchBlur"
68123 />
69124 <ul
70- :class =" { 'hidden sm:flex': showFullSearch }"
71- class =" flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
125+ v-if =" !isSearchExpanded"
126+ :class =" { hidden: showFullSearch }"
127+ class =" hidden sm:flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
72128 >
73129 <!-- Packages dropdown (when connected) -->
74130 <li v-if =" isConnected && npmUser" class =" flex items-center" >
@@ -82,34 +138,46 @@ onKeyStroke(
82138 </ul >
83139 </div >
84140
85- <!-- End: User status + GitHub -->
86- <div
87- :class =" { 'hidden sm:flex': showFullSearch }"
88- class =" flex-1 flex flex-wrap items-center justify-end sm:gap-3 ms-auto sm:ms-0"
89- >
90- <NuxtLink
91- to =" /about"
92- class =" px-2 py-1.5 sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
93- >
94- {{ $t('footer.about') }}
95- </NuxtLink >
96-
141+ <!-- End: Desktop nav items + Mobile menu button -->
142+ <div class =" flex-shrink-0 flex items-center gap-4 sm:gap-6" >
143+ <!-- Desktop: Settings link -->
97144 <NuxtLink
98145 to =" /settings"
99- class =" link-subtle font-mono text-sm inline-flex items-center gap-2 px-2 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
146+ class =" hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
100147 aria-keyshortcuts =" ,"
101148 >
102149 {{ $t('nav.settings') }}
103150 <kbd
104- class =" hidden sm: inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
151+ class =" inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
105152 aria-hidden =" true"
106153 >
107154 ,
108155 </kbd >
109156 </NuxtLink >
110157
111- <HeaderAccountMenu />
158+ <!-- Desktop: Account menu -->
159+ <div class =" hidden sm:block" >
160+ <HeaderAccountMenu />
161+ </div >
162+
163+ <!-- Mobile: Menu button (always visible, toggles menu) -->
164+ <button
165+ type =" button"
166+ class =" sm:hidden p-2 -m-2 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
167+ :aria-label =" showMobileMenu ? $t('common.close') : $t('nav.open_menu')"
168+ :aria-expanded =" showMobileMenu"
169+ @click =" showMobileMenu = !showMobileMenu"
170+ >
171+ <span
172+ class =" w-6 h-6 inline-block"
173+ :class =" showMobileMenu ? 'i-carbon:close' : 'i-carbon:menu'"
174+ aria-hidden =" true"
175+ />
176+ </button >
112177 </div >
113178 </nav >
179+
180+ <!-- Mobile menu -->
181+ <MobileMenu v-model:open =" showMobileMenu" />
114182 </header >
115183</template >
0 commit comments