@@ -11,6 +11,52 @@ 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+ watch (
34+ isOnSearchPage ,
35+ visible => {
36+ if (! visible ) return
37+
38+ searchBoxRef .value ?.focus ()
39+ nextTick (() => {
40+ searchBoxRef .value ?.focus ()
41+ })
42+ },
43+ { flush: ' sync' },
44+ )
45+
46+ function handleSearchBlur() {
47+ showFullSearch .value = false
48+ // Collapse expanded search on mobile after blur (with delay for click handling)
49+ // But don't collapse if we're on the search page
50+ if (isMobile .value && ! isOnSearchPage .value ) {
51+ setTimeout (() => {
52+ isSearchExpandedManually .value = false
53+ }, 150 )
54+ }
55+ }
56+
57+ function handleSearchFocus() {
58+ showFullSearch .value = true
59+ }
1460
1561onKeyStroke (
1662 ' ,' ,
@@ -32,43 +78,66 @@ onKeyStroke(
3278 <header class =" sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border" >
3379 <nav
3480 :aria-label =" $t('nav.main_navigation')"
35- class =" container min-h-14 flex items-center justify-start "
81+ class =" container min-h-14 flex items-center justify-between gap-2 "
3682 >
37- <!-- Start: Logo -->
38- <div :class =" { 'hidden sm:block': showFullSearch }" class =" flex-shrink-0" >
39- <div v-if =" showLogo" >
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" />
83+ <!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
84+ <button
85+ v-if =" !isSearchExpanded"
86+ type =" button"
87+ 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"
88+ :aria-label =" $t('nav.tap_to_search')"
89+ @click =" expandMobileSearch"
90+ >
91+ <img
92+ aria-hidden =" true"
93+ :alt =" $t('alt_logo')"
94+ src =" /logo.svg"
95+ width =" 96"
96+ height =" 96"
97+ class =" w-8 h-8 rounded-lg"
98+ />
99+ <span class =" i-carbon:search w-4 h-4 text-fg-subtle" aria-hidden =" true" />
100+ </button >
101+
102+ <!-- Desktop: Logo (navigates home) -->
103+ <div v-if =" showLogo" class =" hidden sm:flex flex-shrink-0 items-center" >
104+ <NuxtLink
105+ to =" /"
106+ :aria-label =" $t('header.home')"
107+ dir =" ltr"
108+ 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"
109+ >
110+ <img
111+ aria-hidden =" true"
112+ :alt =" $t('alt_logo')"
113+ src =" /logo.svg"
114+ width =" 96"
115+ height =" 96"
116+ class =" w-8 h-8 rounded-lg"
117+ />
118+ <span >npmx</span >
119+ </NuxtLink >
59120 </div >
121+ <!-- Spacer when logo is hidden on desktop -->
122+ <span v-else class =" hidden sm:block w-1" />
60123
61124 <!-- 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) -->
125+ <div
126+ class =" flex-1 flex items-center justify-center md:gap-6"
127+ :class =" { 'hidden sm:flex': !isSearchExpanded }"
128+ >
129+ <!-- Search bar (hidden on mobile unless expanded) -->
64130 <SearchBox
65- :inputClass =" showFullSearch ? '' : 'max-w[6rem]'"
66- @focus =" showFullSearch = true"
67- @blur =" showFullSearch = false"
131+ ref =" searchBoxRef"
132+ :inputClass =" isSearchExpanded ? 'w-full' : ''"
133+ :class =" { 'max-w-md': !isSearchExpanded }"
134+ @focus =" handleSearchFocus"
135+ @blur =" handleSearchBlur"
68136 />
69137 <ul
70- :class =" { 'hidden sm:flex': showFullSearch }"
71- class =" flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
138+ v-if =" !isSearchExpanded"
139+ :class =" { hidden: showFullSearch }"
140+ class =" hidden sm:flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
72141 >
73142 <!-- Packages dropdown (when connected) -->
74143 <li v-if =" isConnected && npmUser" class =" flex items-center" >
@@ -82,34 +151,46 @@ onKeyStroke(
82151 </ul >
83152 </div >
84153
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-
154+ <!-- End: Desktop nav items + Mobile menu button -->
155+ <div class =" flex-shrink-0 flex items-center gap-4 sm:gap-6" >
156+ <!-- Desktop: Settings link -->
97157 <NuxtLink
98158 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"
159+ 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"
100160 aria-keyshortcuts =" ,"
101161 >
102162 {{ $t('nav.settings') }}
103163 <kbd
104- class =" hidden sm: inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
164+ class =" inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
105165 aria-hidden =" true"
106166 >
107167 ,
108168 </kbd >
109169 </NuxtLink >
110170
111- <HeaderAccountMenu />
171+ <!-- Desktop: Account menu -->
172+ <div class =" hidden sm:block" >
173+ <HeaderAccountMenu />
174+ </div >
175+
176+ <!-- Mobile: Menu button (always visible, toggles menu) -->
177+ <button
178+ type =" button"
179+ 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"
180+ :aria-label =" showMobileMenu ? $t('common.close') : $t('nav.open_menu')"
181+ :aria-expanded =" showMobileMenu"
182+ @click =" showMobileMenu = !showMobileMenu"
183+ >
184+ <span
185+ class =" w-6 h-6 inline-block"
186+ :class =" showMobileMenu ? 'i-carbon:close' : 'i-carbon:menu'"
187+ aria-hidden =" true"
188+ />
189+ </button >
112190 </div >
113191 </nav >
192+
193+ <!-- Mobile menu -->
194+ <MobileMenu v-model:open =" showMobileMenu" />
114195 </header >
115196</template >
0 commit comments