Skip to content

Commit bedb01b

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
1 parent 507bbd6 commit bedb01b

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
@@ -92,7 +113,6 @@ onKeyStroke(',', e => {
92113
:placeholder="$t('search.placeholder')"
93114
v-bind="noCorrect"
94115
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"
95-
@input="handleSearchInput"
96116
@focus="isSearchFocused = true"
97117
@blur="isSearchFocused = false"
98118
/>

app/pages/search.vue

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ import { isPlatformSpecificPackage } from '~/utils/platform-packages'
77
const route = useRoute()
88
const router = useRouter()
99
10-
// Local input value (updates immediately as user types)
11-
const inputValue = ref((route.query.q as string) ?? '')
12-
13-
// Debounced URL update for search query
14-
const updateUrlQuery = debounce((value: string) => {
15-
router.replace({ query: { q: value || undefined } })
16-
}, 250)
17-
1810
// Debounced URL update for page (less aggressive to avoid too many URL changes)
1911
const updateUrlPage = debounce((page: number) => {
2012
router.replace({
@@ -25,29 +17,9 @@ const updateUrlPage = debounce((page: number) => {
2517
})
2618
}, 500)
2719
28-
// Watch input and debounce URL updates
29-
watch(inputValue, value => {
30-
updateUrlQuery(value)
31-
})
32-
3320
// The actual search query (from URL, used for API calls)
3421
const query = computed(() => (route.query.q as string) ?? '')
3522
36-
// Sync input with URL when navigating (e.g., back button)
37-
watch(
38-
() => route.query.q,
39-
urlQuery => {
40-
const value = (urlQuery as string) ?? ''
41-
if (inputValue.value !== value) {
42-
inputValue.value = value
43-
}
44-
},
45-
)
46-
47-
// For glow effect
48-
const searchInputRef = useTemplateRef('searchInputRef')
49-
const { focused: isSearchFocused } = useFocus(searchInputRef)
50-
5123
const selectedIndex = ref(0)
5224
const packageListRef = useTemplateRef('packageListRef')
5325
@@ -56,8 +28,6 @@ const resultCount = computed(() => visibleResults.value?.objects.length ?? 0)
5628
// Track if page just loaded (for hiding "Searching..." during view transition)
5729
const hasInteracted = ref(false)
5830
onMounted(() => {
59-
// Focus search onMount
60-
isSearchFocused.value = true
6131
// Small delay to let view transition complete
6232
setTimeout(() => {
6333
hasInteracted.value = true
@@ -716,60 +686,8 @@ defineOgImageComponent('Default', {
716686

717687
<template>
718688
<main class="overflow-x-hidden">
719-
<!-- Sticky search header - positioned below AppHeader (h-14 = 56px) -->
720-
<header class="sticky top-14 z-40 bg-bg/95 backdrop-blur-sm border-b border-border">
721-
<div class="container-sm py-4">
722-
<h1 class="font-mono text-xl sm:text-2xl font-medium mb-4">{{ $t('nav.search') }}</h1>
723-
724-
<search>
725-
<form method="GET" action="/search" class="relative" @submit.prevent>
726-
<label for="search-input" class="sr-only">{{ $t('search.label') }}</label>
727-
728-
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
729-
<!-- Subtle glow effect -->
730-
<div
731-
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"
732-
/>
733-
734-
<div class="search-box relative flex items-center">
735-
<span
736-
class="absolute left-4 text-fg-subtle font-mono text-base pointer-events-none transition-colors duration-200 group-focus-within:text-accent"
737-
aria-hidden="true"
738-
>
739-
/
740-
</span>
741-
<input
742-
id="search-input"
743-
ref="searchInputRef"
744-
v-model="inputValue"
745-
type="search"
746-
name="q"
747-
:placeholder="$t('search.placeholder')"
748-
v-bind="noCorrect"
749-
autofocus
750-
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"
751-
@keydown="handleResultsKeydown"
752-
/>
753-
<button
754-
v-show="inputValue"
755-
type="button"
756-
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"
757-
:aria-label="$t('search.clear')"
758-
@click="inputValue = ''"
759-
>
760-
<span class="i-carbon-close-large block w-3.5 h-3.5" aria-hidden="true" />
761-
</button>
762-
<!-- Hidden submit button for accessibility (form must have submit button per WCAG) -->
763-
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
764-
</div>
765-
</div>
766-
</form>
767-
</search>
768-
</div>
769-
</header>
770-
771689
<!-- Results area with container padding -->
772-
<div class="container-sm pt-20 pb-6">
690+
<div class="container-sm py-6">
773691
<section v-if="query" :aria-label="$t('search.results')" @keydown="handleResultsKeydown">
774692
<!-- Initial loading (only after user interaction, not during view transition) -->
775693
<LoadingSpinner v-if="showSearching" :text="$t('search.searching')" />

0 commit comments

Comments
 (0)