@@ -8,7 +8,6 @@ import { isPlatformSpecificPackage } from '~/utils/platform-packages'
88import { normalizeSearchParam } from ' #shared/utils/url'
99
1010const route = useRoute ()
11- const router = useRouter ()
1211
1312// Preferences (persisted to localStorage)
1413const {
@@ -21,13 +20,16 @@ const {
2120} = usePackageListPreferences ()
2221
2322// Debounced URL update for page (less aggressive to avoid too many URL changes)
23+ // Use History API directly to update URL without triggering Router's scroll-to-top
2424const updateUrlPage = debounce ((page : number ) => {
25- router .replace ({
26- query: {
27- ... route .query ,
28- page: page > 1 ? page : undefined ,
29- },
30- })
25+ const url = new URL (window .location .href )
26+ if (page > 1 ) {
27+ url .searchParams .set (' page' , page .toString ())
28+ } else {
29+ url .searchParams .delete (' page' )
30+ }
31+ // This updates the address bar "silently"
32+ window .history .replaceState (window .history .state , ' ' , url )
3133}, 500 )
3234
3335const { model : searchQuery, provider : searchProvider } = useGlobalSearch ()
@@ -266,14 +268,18 @@ async function loadMore() {
266268 currentPage .value ++
267269 await fetchMore (requestedSize .value )
268270}
271+ onBeforeUnmount (() => {
272+ updateUrlPage .cancel ()
273+ })
269274
270275// Update URL when page changes from scrolling
271276function handlePageChange(page : number ) {
272277 updateUrlPage (page )
273278}
274279
275280// Reset page when query changes
276- watch (query , () => {
281+ watch (query , (newQuery , oldQuery ) => {
282+ if (newQuery .trim () === (oldQuery || ' ' ).trim ()) return
277283 currentPage .value = 1
278284 hasInteracted .value = true
279285})
@@ -387,20 +393,24 @@ const totalSelectableCount = computed(() => suggestionCount.value + resultCount.
387393 * Get all focusable result elements in DOM order (suggestions first, then packages)
388394 */
389395function getFocusableElements(): HTMLElement [] {
390- const suggestions = Array .from (
391- document .querySelectorAll <HTMLElement >(' [data-suggestion-index]' ),
392- ).sort ((a , b ) => {
393- const aIdx = Number .parseInt (a .dataset .suggestionIndex ?? ' 0' , 10 )
394- const bIdx = Number .parseInt (b .dataset .suggestionIndex ?? ' 0' , 10 )
395- return aIdx - bIdx
396- })
397- const packages = Array .from (document .querySelectorAll <HTMLElement >(' [data-result-index]' )).sort (
398- (a , b ) => {
396+ const isVisible = (el : HTMLElement ) => el .getClientRects ().length > 0
397+
398+ const suggestions = Array .from (document .querySelectorAll <HTMLElement >(' [data-suggestion-index]' ))
399+ .filter (isVisible )
400+ .sort ((a , b ) => {
401+ const aIdx = Number .parseInt (a .dataset .suggestionIndex ?? ' 0' , 10 )
402+ const bIdx = Number .parseInt (b .dataset .suggestionIndex ?? ' 0' , 10 )
403+ return aIdx - bIdx
404+ })
405+
406+ const packages = Array .from (document .querySelectorAll <HTMLElement >(' [data-result-index]' ))
407+ .filter (isVisible )
408+ .sort ((a , b ) => {
399409 const aIdx = Number .parseInt (a .dataset .resultIndex ?? ' 0' , 10 )
400410 const bIdx = Number .parseInt (b .dataset .resultIndex ?? ' 0' , 10 )
401411 return aIdx - bIdx
402- },
403- )
412+ })
413+
404414 return [... suggestions , ... packages ]
405415}
406416
@@ -533,7 +543,7 @@ defineOgImageComponent('Default', {
533543 </script >
534544
535545<template >
536- <main class =" flex-1 py-8" :class =" { 'overflow-x-hidden': viewMode !== 'table' }" >
546+ <main class =" flex-1 py-8 search-page " :class =" { 'overflow-x-hidden': viewMode !== 'table' }" >
537547 <div class =" container-sm" >
538548 <div class =" flex items-center justify-between gap-4 mb-4" >
539549 <h1 class =" font-mono text-2xl sm:text-3xl font-medium" >
@@ -542,13 +552,22 @@ defineOgImageComponent('Default', {
542552 <SearchProviderToggle />
543553 </div >
544554
545- <section v-if =" query" >
546- <!-- Initial loading (only after user interaction, not during view transition) -->
555+ <section v-if =" query" class =" results-layout" >
547556 <LoadingSpinner v-if =" showSearching" :text =" $t('search.searching')" />
548557
549- <div v-else-if =" visibleResults" >
550- <!-- User/Org search suggestions -->
551- <div v-if =" validatedSuggestions.length > 0" class =" mb-6 space-y-3" >
558+ <div
559+ v-show ="
560+ results ||
561+ displayResults.length > 0 ||
562+ isRateLimited ||
563+ status === 'error' ||
564+ status === 'success'
565+ "
566+ >
567+ <div
568+ v-if =" validatedSuggestions.length > 0 && displayResults.length > 0"
569+ class =" mb-6 space-y-3"
570+ >
552571 <SearchSuggestionCard
553572 v-for =" (suggestion, idx) in validatedSuggestions"
554573 :key =" `${suggestion.type}-${suggestion.name}`"
@@ -562,9 +581,8 @@ defineOgImageComponent('Default', {
562581 />
563582 </div >
564583
565- <!-- Claim prompt - shown at top when valid name but no exact match -->
566584 <div
567- v-if =" showClaimPrompt && visibleResults.total > 0"
585+ v-if =" showClaimPrompt && visibleResults && displayResults.length > 0"
568586 class =" mb-6 p-4 bg-bg-subtle border border-border rounded-lg sm:flex hidden flex-row sm:items-center gap-3 sm:gap-4"
569587 >
570588 <div class =" flex-1 min-w-0" >
@@ -582,15 +600,13 @@ defineOgImageComponent('Default', {
582600 </button >
583601 </div >
584602
585- <!-- Rate limited by npm - check FIRST before showing any results -->
586603 <div v-if =" isRateLimited" role =" status" class =" py-12" >
587604 <p class =" text-fg-muted font-mono mb-6 text-center" >
588605 {{ $t('search.rate_limited') }}
589606 </p >
590607 </div >
591608
592- <!-- Enhanced toolbar -->
593- <div v-else-if =" visibleResults.total > 0" class =" mb-6" >
609+ <div v-else-if =" visibleResults && displayResults.length > 0" class =" mb-6" >
594610 <PackageListToolbar
595611 :filters =" filters"
596612 v-model:sort-option =" sortOption"
@@ -615,7 +631,6 @@ defineOgImageComponent('Default', {
615631 @update:updated-within =" setUpdatedWithin"
616632 @toggle-keyword =" toggleKeyword"
617633 />
618- <!-- Show count status (infinite scroll mode only) -->
619634 <p
620635 v-if =" viewMode === 'cards' && paginationMode === 'infinite'"
621636 role =" status"
@@ -639,7 +654,6 @@ defineOgImageComponent('Default', {
639654 $t('search.updating')
640655 }}</span >
641656 </p >
642- <!-- Show "x of y" (paginated/table mode only) -->
643657 <p
644658 v-if =" viewMode === 'table' || paginationMode === 'paginated'"
645659 role =" status"
@@ -661,13 +675,11 @@ defineOgImageComponent('Default', {
661675 </p >
662676 </div >
663677
664- <!-- No results found -->
665678 <div v-else-if =" status === 'success' || status === 'error'" role =" status" class =" py-12" >
666679 <p class =" text-fg-muted font-mono mb-6 text-center" >
667680 {{ $t('search.no_results', { query }) }}
668681 </p >
669682
670- <!-- User/Org suggestions when no packages found -->
671683 <div v-if =" validatedSuggestions.length > 0" class =" max-w-md mx-auto mb-6 space-y-3" >
672684 <SearchSuggestionCard
673685 v-for =" (suggestion, idx) in validatedSuggestions"
@@ -682,7 +694,6 @@ defineOgImageComponent('Default', {
682694 />
683695 </div >
684696
685- <!-- Offer to claim the package name if it's valid -->
686697 <div v-if =" showClaimPrompt" class =" max-w-md mx-auto text-center hidden sm:block" >
687698 <div class =" p-4 bg-bg-subtle border border-border rounded-lg" >
688699 <p class =" text-sm text-fg-muted mb-3" >{{ $t('search.want_to_claim') }}</p >
@@ -698,7 +709,7 @@ defineOgImageComponent('Default', {
698709 </div >
699710
700711 <PackageList
701- v-if =" displayResults.length > 0 && !isRateLimited"
712+ v-show =" displayResults.length > 0 && !isRateLimited"
702713 :results =" displayResults"
703714 :search-query =" query"
704715 :filters =" filters"
@@ -719,7 +730,6 @@ defineOgImageComponent('Default', {
719730 @click-keyword =" toggleKeyword"
720731 />
721732
722- <!-- Pagination controls -->
723733 <PaginationControls
724734 v-if =" displayResults.length > 0 && !isRateLimited"
725735 v-model:mode =" paginationMode"
@@ -736,7 +746,6 @@ defineOgImageComponent('Default', {
736746 </section >
737747 </div >
738748
739- <!-- Claim package modal -->
740749 <PackageClaimPackageModal
741750 ref =" claimPackageModalRef"
742751 :package-name =" query"
@@ -745,3 +754,10 @@ defineOgImageComponent('Default', {
745754 />
746755 </main >
747756</template >
757+
758+ <style scoped>
759+ .results-layout {
760+ min-height : 50vh ;
761+ overflow-anchor : none ;
762+ }
763+ </style >
0 commit comments