Skip to content

Commit 1f40bc9

Browse files
committed
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
1 parent 99cc5a8 commit 1f40bc9

2 files changed

Lines changed: 35 additions & 103 deletions

File tree

app/components/AppHeader.vue

Lines changed: 34 additions & 20 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
@@ -74,13 +95,7 @@ onKeyStroke(',', e => {
7495
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
7596
<!-- Search bar (shown on all pages except home and search) -->
7697
<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-
>
98+
<form role="search" method="GET" action="/search" class="relative">
8499
<label for="header-search" class="sr-only">
85100
{{ $t('search.label') }}
86101
</label>
@@ -101,7 +116,6 @@ onKeyStroke(',', e => {
101116
:placeholder="$t('search.placeholder')"
102117
v-bind="noCorrect"
103118
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"
105119
@focus="isSearchFocused = true"
106120
@blur="isSearchFocused = false"
107121
/>

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 role="search" 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)