[READ-ONLY] a fast, modern browser for the npm registry
at main 192 lines 7.2 kB view raw
1<script setup lang="ts"> 2import type { StructuredFilters } from '#shared/types/preferences' 3 4const props = defineProps<{ 5 /** The search result object containing package data */ 6 result: NpmSearchResult 7 /** Heading level for the package name (h2 for search, h3 for lists) */ 8 headingLevel?: 'h2' | 'h3' 9 /** Whether to show the publisher username */ 10 showPublisher?: boolean 11 prefetch?: boolean 12 index?: number 13 /** Filters to apply to the results */ 14 filters?: StructuredFilters 15 /** Search query for highlighting exact matches */ 16 searchQuery?: string 17}>() 18 19const emit = defineEmits<{ 20 clickKeyword: [keyword: string] 21}>() 22 23/** Check if this package is an exact match for the search query */ 24const isExactMatch = computed(() => { 25 if (!props.searchQuery) return false 26 const query = props.searchQuery.trim().toLowerCase() 27 const name = props.result.package.name.toLowerCase() 28 return query === name 29}) 30 31// Process package description 32const pkgDescription = useMarkdown(() => ({ 33 text: props.result.package.description ?? '', 34 plain: true, 35 packageName: props.result.package.name, 36})) 37 38const numberFormatter = useNumberFormatter() 39</script> 40 41<template> 42 <BaseCard :isExactMatch="isExactMatch"> 43 <div class="mb-2 flex items-baseline justify-start gap-2"> 44 <component 45 :is="headingLevel ?? 'h3'" 46 class="font-mono text-sm sm:text-base font-medium text-fg group-hover:text-fg transition-colors duration-200 min-w-0 break-all" 47 > 48 <NuxtLink 49 :to="packageRoute(result.package.name)" 50 :prefetch-on="prefetch ? 'visibility' : 'interaction'" 51 class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0" 52 :data-result-index="index" 53 dir="ltr" 54 >{{ result.package.name }}</NuxtLink 55 > 56 <span 57 v-if="isExactMatch" 58 class="text-xs px-1.5 py-0.5 ms-2 rounded bg-bg-elevated border border-border-hover text-fg" 59 >{{ $t('search.exact_match') }}</span 60 > 61 </component> 62 <span aria-hidden="true" class="flex-shrink-1 flex-grow-1" /> 63 <!-- Mobile: version next to package name --> 64 <div class="sm:hidden text-fg-subtle flex items-center gap-1.5 shrink-0"> 65 <span 66 v-if="result.package.version" 67 class="font-mono text-xs truncate max-w-20" 68 :title="result.package.version" 69 > 70 v{{ result.package.version }} 71 </span> 72 <ProvenanceBadge 73 v-if="result.package.publisher?.trustedPublisher" 74 :provider="result.package.publisher.trustedPublisher.id" 75 :package-name="result.package.name" 76 :version="result.package.version" 77 :linked="false" 78 compact 79 /> 80 </div> 81 </div> 82 <div class="flex justify-start items-start gap-4 sm:gap-8"> 83 <div class="min-w-0"> 84 <p v-if="pkgDescription" class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3"> 85 <span v-html="pkgDescription" /> 86 </p> 87 <div class="flex flex-wrap items-center gap-x-3 sm:gap-x-4 gap-y-2 text-xs text-fg-muted"> 88 <dl v-if="showPublisher || result.package.date" class="flex items-center gap-4 m-0"> 89 <div 90 v-if="showPublisher && result.package.publisher?.username" 91 class="flex items-center gap-1.5" 92 > 93 <dt class="sr-only">{{ $t('package.card.publisher') }}</dt> 94 <dd class="font-mono">{{ result.package.publisher.username }}</dd> 95 </div> 96 <div v-if="result.package.date" class="flex items-center gap-1.5"> 97 <dt class="sr-only">{{ $t('package.card.published') }}</dt> 98 <dd> 99 <DateTime 100 :datetime="result.package.date" 101 year="numeric" 102 month="short" 103 day="numeric" 104 /> 105 </dd> 106 </div> 107 <div v-if="result.package.license" class="flex items-center gap-1.5"> 108 <dt class="sr-only">{{ $t('package.card.license') }}</dt> 109 <dd>{{ result.package.license }}</dd> 110 </div> 111 </dl> 112 </div> 113 <!-- Mobile: downloads on separate row --> 114 <dl 115 v-if="result.downloads?.weekly" 116 class="sm:hidden flex items-center gap-4 mt-2 text-xs text-fg-muted m-0" 117 > 118 <div class="flex items-center gap-1.5"> 119 <dt class="sr-only">{{ $t('package.card.weekly_downloads') }}</dt> 120 <dd class="flex items-center gap-1.5"> 121 <span class="i-lucide:chart-line w-3.5 h-3.5" aria-hidden="true" /> 122 <span class="font-mono">{{ $n(result.downloads.weekly) }}/w</span> 123 </dd> 124 </div> 125 </dl> 126 </div> 127 <span aria-hidden="true" class="flex-shrink-1 flex-grow-1" /> 128 <!-- Desktop: version and downloads on right side --> 129 <div class="hidden sm:flex flex-col gap-2 shrink-0"> 130 <div class="text-fg-subtle flex items-start gap-2 justify-end"> 131 <span 132 v-if="result.package.version" 133 class="font-mono text-xs truncate max-w-32" 134 :title="result.package.version" 135 > 136 v{{ result.package.version }} 137 </span> 138 <div 139 v-if="result.package.publisher?.trustedPublisher" 140 class="flex items-center gap-1.5 shrink-0 max-w-32" 141 > 142 <ProvenanceBadge 143 :provider="result.package.publisher.trustedPublisher.id" 144 :package-name="result.package.name" 145 :version="result.package.version" 146 :linked="false" 147 compact 148 /> 149 </div> 150 </div> 151 <div 152 v-if="result.downloads?.weekly" 153 class="text-fg-subtle gap-2 flex items-center justify-end" 154 > 155 <span class="i-lucide:chart-line w-3.5 h-3.5" aria-hidden="true" /> 156 <span class="font-mono text-xs"> 157 {{ $n(result.downloads.weekly) }} {{ $t('common.per_week') }} 158 </span> 159 </div> 160 </div> 161 </div> 162 163 <ul 164 role="list" 165 v-if="result.package.keywords?.length" 166 :aria-label="$t('package.card.keywords')" 167 class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0 pointer-events-none items-center" 168 > 169 <li v-for="keyword in result.package.keywords.slice(0, 5)" :key="keyword"> 170 <ButtonBase 171 class="pointer-events-auto" 172 size="small" 173 :aria-pressed="props.filters?.keywords.includes(keyword)" 174 :title="`Filter by ${keyword}`" 175 :data-result-index="index" 176 @click.stop="emit('clickKeyword', keyword)" 177 > 178 {{ keyword }} 179 </ButtonBase> 180 </li> 181 <li> 182 <span 183 v-if="result.package.keywords.length > 5" 184 class="text-fg-subtle text-xs pointer-events-auto" 185 :title="result.package.keywords.slice(5).join(', ')" 186 > 187 +{{ numberFormatter.format(result.package.keywords.length - 5) }} 188 </span> 189 </li> 190 </ul> 191 </BaseCard> 192</template>