Skip to content

Commit 69d1ee8

Browse files
committed
feat: keep search box in header
Avoid taking up space for searchbox on search page. Mainly moved the query string watching from search.vue to AppHeader.vuue # Conflicts: # app/components/AppHeader.vue # app/pages/search.vue # Conflicts: # app/components/AppHeader.vue # app/pages/search.vue
1 parent b13eeb8 commit 69d1ee8

2 files changed

Lines changed: 34 additions & 96 deletions

File tree

app/components/AppHeader.vue

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,48 @@ const { isConnected, npmUser } = useConnector()
1717
const router = useRouter()
1818
const route = useRoute()
1919
20-
const searchQuery = ref('')
2120
const isSearchFocused = ref(false)
2221
2322
const showSearchBar = computed(() => {
24-
return route.name !== 'search' && route.name !== 'index'
23+
return route.name !== 'index'
2524
})
2625
27-
const debouncedNavigate = debounce(async () => {
28-
const query = searchQuery.value.trim()
29-
await router.push({
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({
3040
name: 'search',
31-
query: query ? { q: query } : undefined,
41+
query: {
42+
q: value,
43+
},
3244
})
33-
// allow time for the navigation to occur before resetting searchQuery
34-
setTimeout(() => (searchQuery.value = ''), 1000)
35-
}, 100)
45+
}, 250)
3646
37-
async function handleSearchInput() {
38-
debouncedNavigate()
39-
}
47+
// Watch input and debounce URL updates
48+
watch(searchQuery, value => {
49+
updateUrlQuery(value)
50+
})
4051
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+
)
4162
onKeyStroke(',', e => {
4263
// Don't trigger if user is typing in an input
4364
const target = e.target as HTMLElement
@@ -95,7 +116,6 @@ onKeyStroke(',', e => {
95116
:placeholder="$t('search.placeholder')"
96117
v-bind="noCorrect"
97118
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"
98-
@input="handleSearchInput"
99119
@focus="isSearchFocused = true"
100120
@blur="isSearchFocused = false"
101121
/>

app/pages/search.vue

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@ const {
1818
resetColumns,
1919
} = usePackageListPreferences()
2020
21-
// Local input value (updates immediately as user types)
22-
const inputValue = ref((route.query.q as string) ?? '')
23-
24-
// Debounced URL update for search query
25-
const updateUrlQuery = debounce((value: string) => {
26-
router.replace({ query: { q: value || undefined } })
27-
}, 250)
28-
2921
// Debounced URL update for page (less aggressive to avoid too many URL changes)
3022
const updateUrlPage = debounce((page: number) => {
3123
router.replace({
@@ -36,37 +28,15 @@ const updateUrlPage = debounce((page: number) => {
3628
})
3729
}, 500)
3830
39-
// Watch input and debounce URL updates
40-
watch(inputValue, value => {
41-
updateUrlQuery(value)
42-
})
43-
4431
// The actual search query (from URL, used for API calls)
4532
const query = computed(() => (route.query.q as string) ?? '')
4633
47-
// Sync input with URL when navigating (e.g., back button)
48-
watch(
49-
() => route.query.q,
50-
urlQuery => {
51-
const value = (urlQuery as string) ?? ''
52-
if (inputValue.value !== value) {
53-
inputValue.value = value
54-
}
55-
},
56-
)
57-
58-
// For glow effect
59-
const searchInputRef = useTemplateRef('searchInputRef')
60-
const { focused: isSearchFocused } = useFocus(searchInputRef)
61-
6234
const selectedIndex = ref(0)
6335
const packageListRef = useTemplateRef('packageListRef')
6436
6537
// Track if page just loaded (for hiding "Searching..." during view transition)
6638
const hasInteracted = ref(false)
6739
onMounted(() => {
68-
// Focus search onMount
69-
isSearchFocused.value = true
7040
// Small delay to let view transition complete
7141
setTimeout(() => {
7242
hasInteracted.value = true
@@ -759,60 +729,8 @@ defineOgImageComponent('Default', {
759729

760730
<template>
761731
<main class="overflow-x-hidden">
762-
<!-- Sticky search header - positioned below AppHeader (h-14 = 56px) -->
763-
<header class="sticky top-14 z-40 bg-bg/95 backdrop-blur-sm border-b border-border">
764-
<div class="container-sm py-4">
765-
<h1 class="font-mono text-xl sm:text-2xl font-medium mb-4">{{ $t('nav.search') }}</h1>
766-
767-
<search>
768-
<form method="GET" action="/search" class="relative" @submit.prevent>
769-
<label for="search-input" class="sr-only">{{ $t('search.label') }}</label>
770-
771-
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
772-
<!-- Subtle glow effect -->
773-
<div
774-
class="absolute -inset-px rounded-lg bg-gradient-to-r from-fg/0 via-fg/5 to-fg/0 opacity-0 transition-opacity duration-500 blur-sm group-[.is-focused]:opacity-100 motion-reduce:transition-none"
775-
/>
776-
777-
<div class="search-box relative flex items-center">
778-
<span
779-
class="absolute left-4 text-fg-subtle font-mono text-base pointer-events-none transition-colors duration-200 group-focus-within:text-accent"
780-
aria-hidden="true"
781-
>
782-
/
783-
</span>
784-
<input
785-
id="search-input"
786-
ref="searchInputRef"
787-
v-model="inputValue"
788-
type="search"
789-
name="q"
790-
:placeholder="$t('search.placeholder')"
791-
v-bind="noCorrect"
792-
autofocus
793-
class="w-full max-w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-10 py-3 font-mono text-base text-fg placeholder:text-fg-subtle transition-colors duration-300 focus:border-accent focus-visible:outline-none appearance-none"
794-
@keydown="handleResultsKeydown"
795-
/>
796-
<button
797-
v-show="inputValue"
798-
type="button"
799-
class="absolute right-3 p-2 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
800-
:aria-label="$t('search.clear')"
801-
@click="inputValue = ''"
802-
>
803-
<span class="i-carbon-close-large block w-3.5 h-3.5" aria-hidden="true" />
804-
</button>
805-
<!-- Hidden submit button for accessibility (form must have submit button per WCAG) -->
806-
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
807-
</div>
808-
</div>
809-
</form>
810-
</search>
811-
</div>
812-
</header>
813-
814732
<!-- Results area with container padding -->
815-
<div class="container-sm pt-20 pb-6">
733+
<div class="container-sm py-6">
816734
<section v-if="query" :aria-label="$t('search.results')" @keydown="handleResultsKeydown">
817735
<!-- Initial loading (only after user interaction, not during view transition) -->
818736
<LoadingSpinner v-if="showSearching" :text="$t('search.searching')" />

0 commit comments

Comments
 (0)