-
-
Notifications
You must be signed in to change notification settings - Fork 425
Expand file tree
/
Copy pathPackageList.vue
More file actions
143 lines (130 loc) · 4.26 KB
/
PackageList.vue
File metadata and controls
143 lines (130 loc) · 4.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<script setup lang="ts">
import type { NpmSearchResult } from '#shared/types'
import type { WindowVirtualizerHandle } from '~/composables/useVirtualInfiniteScroll'
import { WindowVirtualizer } from 'virtua/vue'
const props = defineProps<{
/** List of search results to display */
results: NpmSearchResult[]
/** Heading level for package names */
headingLevel?: 'h2' | 'h3'
/** Whether to show publisher username on cards */
showPublisher?: boolean
/** Whether there are more items to load */
hasMore?: boolean
/** Whether currently loading more items */
isLoading?: boolean
/** Page size for tracking current page */
pageSize?: number
/** Initial page to scroll to (1-indexed) */
initialPage?: number
/** Selected result index (for keyboard navigation) */
selectedIndex?: number
/** Search query for highlighting exact matches */
searchQuery?: string
}>()
const emit = defineEmits<{
/** Emitted when scrolled near the bottom and more items should be loaded */
loadMore: []
/** Emitted when the visible page changes */
pageChange: [page: number]
/** Emitted when a result is hovered/focused */
select: [index: number]
}>()
// Reference to WindowVirtualizer for infinite scroll detection
const listRef = useTemplateRef<WindowVirtualizerHandle>('listRef')
// Set up infinite scroll if hasMore is provided
const hasMore = computed(() => props.hasMore ?? false)
const isLoading = computed(() => props.isLoading ?? false)
const itemCount = computed(() => props.results.length)
const pageSize = computed(() => props.pageSize ?? 20)
const { handleScroll, scrollToPage } = useVirtualInfiniteScroll({
listRef,
itemCount,
hasMore,
isLoading,
pageSize: pageSize.value,
threshold: 5,
onLoadMore: () => emit('loadMore'),
onPageChange: page => emit('pageChange', page),
})
// Scroll to initial page once list is ready and has items
const hasScrolledToInitial = shallowRef(false)
watch(
[() => props.results.length, () => props.initialPage, listRef],
([length, initialPage, list]) => {
if (!hasScrolledToInitial.value && list && length > 0 && initialPage && initialPage > 1) {
// Wait for next tick to ensure list is rendered
nextTick(() => {
scrollToPage(initialPage)
hasScrolledToInitial.value = true
})
}
},
{ immediate: true },
)
// Reset scroll state when results change significantly (new search)
watch(
() => props.results,
(newResults, oldResults) => {
// If this looks like a new search (different first item or much shorter), reset
if (
!oldResults ||
newResults.length === 0 ||
(oldResults.length > 0 && newResults[0]?.package.name !== oldResults[0]?.package.name)
) {
hasScrolledToInitial.value = false
}
},
)
function scrollToIndex(index: number, smooth = true) {
listRef.value?.scrollToIndex(index, { align: 'center', smooth })
}
defineExpose({
scrollToIndex,
})
</script>
<template>
<div>
<WindowVirtualizer
ref="listRef"
:data="results"
:item-size="140"
as="ol"
item="li"
class="list-none m-0 p-0"
@scroll="handleScroll"
>
<template #default="{ item, index }">
<div class="pb-4">
<PackageCard
:result="item as NpmSearchResult"
:heading-level="headingLevel"
:show-publisher="showPublisher"
:selected="index === (selectedIndex ?? -1)"
:index="index"
:search-query="searchQuery"
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
@focus="emit('select', $event)"
/>
</div>
</template>
</WindowVirtualizer>
<!-- Loading indicator -->
<div v-if="isLoading" class="py-4 flex items-center justify-center">
<div class="flex items-center gap-3 text-fg-muted font-mono text-sm">
<span
class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full motion-safe:animate-spin"
/>
{{ $t('common.loading_more') }}
</div>
</div>
<!-- End of results -->
<p
v-else-if="!hasMore && results.length > 0"
class="py-4 text-center text-fg-subtle font-mono text-sm"
>
{{ $t('common.end_of_results') }}
</p>
</div>
</template>