[READ-ONLY] a fast, modern browser for the npm registry
at main 114 lines 3.4 kB view raw
1<script setup lang="ts"> 2import { onKeyDown } from '@vueuse/core' 3import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component' 4 5defineProps<{ 6 /** Dependency path from root to vulnerable package (readonly from VulnerabilityTreeResult) */ 7 path: readonly string[] 8}>() 9 10const isOpen = shallowRef(false) 11const popupEl = useTemplateRef('popupEl') 12const popupPosition = shallowRef<{ top: number; left: number } | null>(null) 13 14function closePopup() { 15 isOpen.value = false 16} 17 18// Close popup on click outside 19onClickOutside(popupEl, () => { 20 if (isOpen.value) closePopup() 21}) 22 23onKeyDown( 24 'Escape', 25 e => { 26 e.preventDefault() 27 closePopup() 28 }, 29 { dedupe: true, target: popupEl }, 30) 31 32useEventListener('scroll', closePopup, { passive: true }) 33 34function togglePopup(event: MouseEvent) { 35 if (isOpen.value) { 36 closePopup() 37 } else { 38 const button = event.currentTarget as HTMLElement 39 const rect = button.getBoundingClientRect() 40 popupPosition.value = { 41 top: rect.bottom + 4, 42 left: rect.left, 43 } 44 isOpen.value = true 45 } 46} 47 48function getPopupStyle(): Record<string, string> { 49 if (!popupPosition.value) return {} 50 return { 51 top: `${popupPosition.value.top}px`, 52 left: `${popupPosition.value.left}px`, 53 } 54} 55 56// Parse package string "name@version" into { name, version } 57function parsePackageString(pkg: string): { name: string; version: string } { 58 const atIndex = pkg.lastIndexOf('@') 59 if (atIndex > 0) { 60 return { name: pkg.slice(0, atIndex), version: pkg.slice(atIndex + 1) } 61 } 62 return { name: pkg, version: '' } 63} 64</script> 65 66<template> 67 <div class="relative"> 68 <!-- Path badge button --> 69 <button 70 type="button" 71 class="path-badge font-mono text-3xs px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/30 text-amber-800 dark:text-amber-400 transition-all duration-200 ease-out whitespace-nowrap flex items-center gap-1 hover:bg-amber-500/20 hover:border-amber-500/50" 72 :aria-expanded="isOpen" 73 @click.stop="togglePopup" 74 > 75 <span class="i-lucide:network w-3 h-3" aria-hidden="true" /> 76 <span>{{ $t('package.vulnerabilities.path') }}</span> 77 </button> 78 79 <!-- Tree popup --> 80 <div 81 v-if="isOpen" 82 ref="popupEl" 83 class="fixed z-[100] bg-bg-elevated border border-border rounded-lg shadow-xl p-3 min-w-64 max-w-sm" 84 :style="getPopupStyle()" 85 > 86 <UseFocusTrap :options="{ immediate: true }"> 87 <ul class="list-none m-0 p-0 space-y-0.5"> 88 <li 89 v-for="(pathItem, idx) in path" 90 :key="idx" 91 class="font-mono text-xs" 92 :style="{ paddingLeft: `${idx * 12}px` }" 93 > 94 <span v-if="idx > 0" class="text-fg-subtle me-1"></span> 95 <NuxtLink 96 :to=" 97 packageRoute( 98 parsePackageString(pathItem).name, 99 parsePackageString(pathItem).version, 100 ) 101 " 102 class="hover:underline" 103 :class="idx === path.length - 1 ? 'text-fg font-medium' : 'text-fg-muted'" 104 @click="closePopup" 105 > 106 {{ pathItem }} 107 </NuxtLink> 108 <span v-if="idx === path.length - 1" class="ms-1 text-amber-500"></span> 109 </li> 110 </ul> 111 </UseFocusTrap> 112 </div> 113 </div> 114</template>