Skip to content

Commit d2befa5

Browse files
committed
feat: move SearchBox to its own component
Improve search on mobile. Smaller search box that expands full width on click.
1 parent 736ac2c commit d2befa5

2 files changed

Lines changed: 119 additions & 81 deletions

File tree

app/components/AppHeader.vue

Lines changed: 17 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
<script setup lang="ts">
2-
import { debounce } from 'perfect-debounce'
3-
42
withDefaults(
53
defineProps<{
64
showLogo?: boolean
@@ -15,50 +13,9 @@ withDefaults(
1513
const { isConnected, npmUser } = useConnector()
1614
1715
const router = useRouter()
18-
const route = useRoute()
19-
20-
const isSearchFocused = ref(false)
21-
22-
const showSearchBar = computed(() => {
23-
return route.name !== 'index'
24-
})
25-
26-
// Local input value (updates immediately as user types)
27-
const searchQuery = ref((route.query.q as string) ?? '')
28-
29-
// Debounced URL update for search query
30-
const updateUrlQuery = debounce((value: string) => {
31-
if (route.name === 'search') {
32-
router.replace({ query: { q: value || undefined } })
33-
return
34-
}
35-
if (!value) {
36-
return
37-
}
38-
39-
router.push({
40-
name: 'search',
41-
query: {
42-
q: value,
43-
},
44-
})
45-
}, 250)
4616
47-
// Watch input and debounce URL updates
48-
watch(searchQuery, value => {
49-
updateUrlQuery(value)
50-
})
17+
const showFullSearch = ref(false)
5118
52-
// Sync input with URL when navigating (e.g., back button)
53-
watch(
54-
() => route.query.q,
55-
urlQuery => {
56-
const value = (urlQuery as string) ?? ''
57-
if (searchQuery.value !== value) {
58-
searchQuery.value = value
59-
}
60-
},
61-
)
6219
onKeyStroke(',', e => {
6320
// Don't trigger if user is typing in an input
6421
const target = e.target as HTMLElement
@@ -78,7 +35,7 @@ onKeyStroke(',', e => {
7835
class="container h-14 flex items-center justify-start"
7936
>
8037
<!-- Start: Logo -->
81-
<div class="flex-shrink-0">
38+
<div :class="{ 'hidden sm:block': showFullSearch }" class="flex-shrink-0">
8239
<NuxtLink
8340
v-if="showLogo"
8441
to="/"
@@ -92,41 +49,17 @@ onKeyStroke(',', e => {
9249
</div>
9350

9451
<!-- Center: Search bar + nav items -->
95-
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
96-
<!-- Search bar (shown on all pages except home and search) -->
97-
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
98-
<form method="GET" action="/search" class="relative">
99-
<label for="header-search" class="sr-only">
100-
{{ $t('search.label') }}
101-
</label>
102-
103-
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
104-
<div class="search-box relative flex items-center">
105-
<span
106-
class="absolute inset-is-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"
107-
>
108-
/
109-
</span>
110-
111-
<input
112-
id="header-search"
113-
autofocus
114-
v-model="searchQuery"
115-
type="search"
116-
name="q"
117-
:placeholder="$t('search.placeholder')"
118-
v-bind="noCorrect"
119-
class="w-full bg-bg-subtle border border-border rounded-md ps-7 pe-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"
120-
@focus="isSearchFocused = true"
121-
@blur="isSearchFocused = false"
122-
/>
123-
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
124-
</div>
125-
</div>
126-
</form>
127-
</search>
128-
129-
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
52+
<div class="flex-1 flex items-center justify-center md:gap-6 mx-2">
53+
<!-- Search bar (shown on all pages except home) -->
54+
<SearchBox
55+
:inputClass="showFullSearch ? '' : 'max-w[6rem]'"
56+
@focus="showFullSearch = true"
57+
@blur="showFullSearch = false"
58+
/>
59+
<ul
60+
:class="{ 'hidden sm:flex': showFullSearch }"
61+
class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
62+
>
13063
<!-- Packages dropdown (when connected) -->
13164
<li v-if="isConnected && npmUser" class="flex items-center">
13265
<HeaderPackagesDropdown :username="npmUser" />
@@ -140,7 +73,10 @@ onKeyStroke(',', e => {
14073
</div>
14174

14275
<!-- End: User status + GitHub -->
143-
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0">
76+
<div
77+
:class="{ 'hidden sm:flex': showFullSearch }"
78+
class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0"
79+
>
14480
<NuxtLink
14581
to="/about"
14682
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"

app/components/SearchBox.vue

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script setup lang="ts">
2+
import { debounce } from 'perfect-debounce'
3+
4+
withDefaults(
5+
defineProps<{
6+
inputClass?: string
7+
}>(),
8+
{
9+
inputClass: 'inline sm:block',
10+
},
11+
)
12+
13+
const emit = defineEmits(['blur', 'focus'])
14+
15+
const router = useRouter()
16+
const route = useRoute()
17+
18+
const isSearchFocused = ref(false)
19+
20+
const showSearchBar = computed(() => {
21+
return route.name !== 'index'
22+
})
23+
24+
// Local input value (updates immediately as user types)
25+
const searchQuery = ref((route.query.q as string) ?? '')
26+
27+
// Debounced URL update for search query
28+
const updateUrlQuery = debounce((value: string) => {
29+
if (route.name === 'search') {
30+
router.replace({ query: { q: value || undefined } })
31+
return
32+
}
33+
if (!value) {
34+
return
35+
}
36+
37+
router.push({
38+
name: 'search',
39+
query: {
40+
q: value,
41+
},
42+
})
43+
}, 250)
44+
45+
// Watch input and debounce URL updates
46+
watch(searchQuery, value => {
47+
updateUrlQuery(value)
48+
})
49+
50+
// Sync input with URL when navigating (e.g., back button)
51+
watch(
52+
() => route.query.q,
53+
urlQuery => {
54+
const value = (urlQuery as string) ?? ''
55+
if (searchQuery.value !== value) {
56+
searchQuery.value = value
57+
}
58+
},
59+
)
60+
61+
function handleSearchBlur() {
62+
isSearchFocused.value = false
63+
emit('blur')
64+
}
65+
function handleSearchFocus() {
66+
isSearchFocused.value = true
67+
emit('focus')
68+
}
69+
</script>
70+
<template>
71+
<search v-if="showSearchBar" :class="'flex-1 sm:max-w-md ' + inputClass">
72+
<form method="GET" action="/search" class="relative">
73+
<label for="header-search" class="sr-only">
74+
{{ $t('search.label') }}
75+
</label>
76+
77+
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
78+
<div class="search-box relative flex items-center">
79+
<span
80+
class="absolute inset-is-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"
81+
>
82+
/
83+
</span>
84+
85+
<input
86+
id="header-search"
87+
autofocus
88+
v-model="searchQuery"
89+
type="search"
90+
name="q"
91+
:placeholder="$t('search.placeholder')"
92+
v-bind="noCorrect"
93+
class="w-full bg-bg-subtle border border-border rounded-md ps-7 pe-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"
94+
@focus="handleSearchFocus"
95+
@blur="handleSearchBlur"
96+
/>
97+
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
98+
</div>
99+
</div>
100+
</form>
101+
</search>
102+
</template>

0 commit comments

Comments
 (0)