22import { debounce } from ' perfect-debounce'
33
44withDefaults (
5- defineProps <{
6- showLogo? : boolean
7- showConnector? : boolean
8- }>(),
9- {
10- showLogo: true ,
11- showConnector: true ,
12- },
5+ defineProps <{
6+ showLogo? : boolean
7+ showConnector? : boolean
8+ }>(),
9+ {
10+ showLogo: true ,
11+ showConnector: true ,
12+ },
1313)
1414
1515const { isConnected, npmUser } = useConnector ()
@@ -21,136 +21,130 @@ const searchQuery = ref('')
2121const isSearchFocused = ref (false )
2222
2323const showSearchBar = computed (() => {
24- return route .name !== ' search' && route .name !== ' index'
24+ return route .name !== ' search' && route .name !== ' index'
2525})
2626
2727const debouncedNavigate = debounce (async () => {
28- const query = searchQuery .value .trim ()
29- await router .push ({
30- name: ' search' ,
31- query: query ? { q: query } : undefined ,
32- })
33- // allow time for the navigation to occur before resetting searchQuery
34- setTimeout (() => (searchQuery .value = ' ' ), 1000 )
28+ const query = searchQuery .value .trim ()
29+ await router .push ({
30+ name: ' search' ,
31+ query: query ? { q: query } : undefined ,
32+ })
33+ // allow time for the navigation to occur before resetting searchQuery
34+ setTimeout (() => (searchQuery .value = ' ' ), 1000 )
3535}, 100 )
3636
3737async function handleSearchInput() {
38- debouncedNavigate ()
38+ debouncedNavigate ()
3939}
4040
4141onKeyStroke (' ,' , e => {
42- // Don't trigger if user is typing in an input
43- const target = e .target as HTMLElement
44- if (target .tagName === ' INPUT' || target .tagName === ' TEXTAREA' || target .isContentEditable ) {
45- return
46- }
47-
48- e .preventDefault ()
49- router .push (' /settings' )
42+ // Don't trigger if user is typing in an input
43+ const target = e .target as HTMLElement
44+ if (target .tagName === ' INPUT' || target .tagName === ' TEXTAREA' || target .isContentEditable ) {
45+ return
46+ }
47+
48+ e .preventDefault ()
49+ router .push (' /settings' )
5050})
5151 </script >
5252
5353<template >
54- <header
55- :aria-label =" $t('header.site_header')"
56- class =" sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
57- >
58- <nav :aria-label =" $t('nav.main_navigation')" class =" container h-14 flex items-center" >
59- <!-- Left: Logo -->
60- <div class =" flex-shrink-0" >
61- <NuxtLink
62- v-if =" showLogo"
63- to =" /"
64- :aria-label =" $t('header.home')"
65- class =" header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
66- >
67- <span class =" text-accent" ><span class =" -tracking-0.2em" >.</span >/</span >npmx
68- </NuxtLink >
69- <!-- Spacer when logo is hidden -->
70- <span v-else class =" w-1" />
71- </div >
72-
73- <!-- Center: Search bar + nav items -->
74- <div class =" flex-1 flex items-center justify-center gap-4 sm:gap-6" >
75- <!-- Search bar (shown on all pages except home and search) -->
76- <search v-if =" showSearchBar" class =" hidden sm:block flex-1 max-w-md" >
77- <form
78- role =" search"
79- method =" GET"
80- action =" /search"
81- class =" relative"
82- @submit.prevent =" handleSearchInput"
83- >
84- <label for =" header-search" class =" sr-only" >
85- {{ $t('search.label') }}
86- </label >
87-
88- <div class =" relative group" :class =" { 'is-focused': isSearchFocused }" >
89- <div class =" search-box relative flex items-center" >
90- <span
91- class =" absolute left-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none group-focus-within:text-accent z-1"
92- >
93- /
94- </span >
95-
96- <input
97- id =" header-search"
98- v-model =" searchQuery"
99- type =" search"
100- name =" q"
101- :placeholder =" $t('search.placeholder')"
102- v-bind =" noCorrect"
103- class =" w-full bg-bg-subtle border border-border rounded-md pl-7 pr-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-border-color duration-300 motion-reduce:transition-none focus:border-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
104- @input =" handleSearchInput"
105- @focus =" isSearchFocused = true"
106- @blur =" isSearchFocused = false"
107- />
108- <button type =" submit" class =" sr-only" >{{ $t('search.button') }}</button >
109- </div >
110- </div >
111- </form >
112- </search >
113-
114- <ul class =" flex items-center gap-4 sm:gap-6 list-none m-0 p-0" >
115- <!-- Packages dropdown (when connected) -->
116- <li v-if =" isConnected && npmUser" class =" flex items-center" >
117- <HeaderPackagesDropdown :username =" npmUser" />
118- </li >
119-
120- <!-- Orgs dropdown (when connected) -->
121- <li v-if =" isConnected && npmUser" class =" flex items-center" >
122- <HeaderOrgsDropdown :username =" npmUser" />
123- </li >
124- </ul >
125- </div >
126-
127- <!-- Right: User status + GitHub -->
128- <div class =" flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0" >
129- <NuxtLink
130- to =" /about"
131- class =" sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
132- >
133- {{ $t('footer.about') }}
134- </NuxtLink >
135-
136- <NuxtLink
137- to =" /settings"
138- class =" link-subtle font-mono text-sm inline-flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
139- aria-keyshortcuts =" ,"
140- >
141- {{ $t('nav.settings') }}
142- <kbd
143- class =" hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
144- aria-hidden =" true"
145- >
146- ,
147- </kbd >
148- </NuxtLink >
149-
150- <div v-if =" showConnector" class =" hidden sm:block" >
151- <ConnectorStatus />
152- </div >
153- </div >
154- </nav >
155- </header >
54+ <header
55+ :aria-label =" $t('header.site_header')"
56+ class =" sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
57+ >
58+ <nav :aria-label =" $t('nav.main_navigation')" class =" container h-14 flex items-center" >
59+ <!-- Left: Logo -->
60+ <div class =" flex-shrink-0" >
61+ <NuxtLink
62+ v-if =" showLogo"
63+ to =" /"
64+ :aria-label =" $t('header.home')"
65+ class =" header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
66+ >
67+ <span class =" text-accent" ><span class =" -tracking-0.2em" >.</span >/</span >npmx
68+ </NuxtLink >
69+ <!-- Spacer when logo is hidden -->
70+ <span v-else class =" w-1" />
71+ </div >
72+
73+ <!-- Center: Search bar + nav items -->
74+ <div class =" flex-1 flex items-center justify-center gap-4 sm:gap-6" >
75+ <!-- Search bar (shown on all pages except home and search) -->
76+ <search v-if =" showSearchBar" class =" hidden sm:block flex-1 max-w-md" >
77+ <form method =" GET" action =" /search" class =" relative" @submit.prevent =" handleSearchInput" >
78+ <label for =" header-search" class =" sr-only" >
79+ {{ $t('search.label') }}
80+ </label >
81+
82+ <div class =" relative group" :class =" { 'is-focused': isSearchFocused }" >
83+ <div class =" search-box relative flex items-center" >
84+ <span
85+ class =" absolute left-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none group-focus-within:text-accent z-1"
86+ >
87+ /
88+ </span >
89+
90+ <input
91+ id =" header-search"
92+ v-model =" searchQuery"
93+ type =" search"
94+ name =" q"
95+ :placeholder =" $t('search.placeholder')"
96+ v-bind =" noCorrect"
97+ class =" w-full bg-bg-subtle border border-border rounded-md pl-7 pr-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-border-color duration-300 motion-reduce:transition-none focus:border-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
98+ @input =" handleSearchInput"
99+ @focus =" isSearchFocused = true"
100+ @blur =" isSearchFocused = false"
101+ />
102+ <button type =" submit" class =" sr-only" >{{ $t('search.button') }}</button >
103+ </div >
104+ </div >
105+ </form >
106+ </search >
107+
108+ <ul class =" flex items-center gap-4 sm:gap-6 list-none m-0 p-0" >
109+ <!-- Packages dropdown (when connected) -->
110+ <li v-if =" isConnected && npmUser" class =" flex items-center" >
111+ <HeaderPackagesDropdown :username =" npmUser" />
112+ </li >
113+
114+ <!-- Orgs dropdown (when connected) -->
115+ <li v-if =" isConnected && npmUser" class =" flex items-center" >
116+ <HeaderOrgsDropdown :username =" npmUser" />
117+ </li >
118+ </ul >
119+ </div >
120+
121+ <!-- Right: User status + GitHub -->
122+ <div class =" flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0" >
123+ <NuxtLink
124+ to =" /about"
125+ class =" sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
126+ >
127+ {{ $t('footer.about') }}
128+ </NuxtLink >
129+
130+ <NuxtLink
131+ to =" /settings"
132+ class =" link-subtle font-mono text-sm inline-flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
133+ aria-keyshortcuts =" ,"
134+ >
135+ {{ $t('nav.settings') }}
136+ <kbd
137+ class =" hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
138+ aria-hidden =" true"
139+ >
140+ ,
141+ </kbd >
142+ </NuxtLink >
143+
144+ <div v-if =" showConnector" class =" hidden sm:block" >
145+ <ConnectorStatus />
146+ </div >
147+ </div >
148+ </nav >
149+ </header >
156150</template >
0 commit comments