[READ-ONLY] a fast, modern browser for the npm registry
at main 122 lines 3.7 kB view raw
1<script setup lang="ts"> 2const props = defineProps<{ 3 username: string 4}>() 5 6const { listUserOrgs } = useConnector() 7 8const isOpen = shallowRef(false) 9const isLoading = shallowRef(false) 10const orgs = shallowRef<string[]>([]) 11const hasLoaded = shallowRef(false) 12const error = shallowRef<string | null>(null) 13 14async function loadOrgs() { 15 if (hasLoaded.value || isLoading.value) return 16 17 isLoading.value = true 18 error.value = null 19 try { 20 const orgList = await listUserOrgs() 21 if (orgList) { 22 // Already sorted alphabetically by server, take top 10 23 orgs.value = orgList.slice(0, 10) 24 } else { 25 error.value = $t('header.orgs_dropdown.error') 26 } 27 hasLoaded.value = true 28 } catch { 29 error.value = $t('header.orgs_dropdown.error') 30 } finally { 31 isLoading.value = false 32 } 33} 34 35function handleMouseEnter() { 36 isOpen.value = true 37 if (!hasLoaded.value) { 38 loadOrgs() 39 } 40} 41 42function handleMouseLeave() { 43 isOpen.value = false 44} 45 46function handleKeydown(event: KeyboardEvent) { 47 if (event.key === 'Escape' && isOpen.value) { 48 isOpen.value = false 49 } 50} 51</script> 52 53<template> 54 <div 55 class="relative" 56 @mouseenter="handleMouseEnter" 57 @mouseleave="handleMouseLeave" 58 @keydown="handleKeydown" 59 > 60 <NuxtLink 61 :to="{ name: '~username-orgs', params: { username } }" 62 class="link-subtle font-mono text-sm inline-flex items-center gap-1" 63 > 64 {{ $t('header.orgs') }} 65 <span 66 class="i-lucide:chevron-down w-3 h-3 transition-transform duration-200" 67 :class="{ 'rotate-180': isOpen }" 68 aria-hidden="true" 69 /> 70 </NuxtLink> 71 72 <Transition 73 enter-active-class="transition-all duration-150" 74 leave-active-class="transition-all duration-100" 75 enter-from-class="opacity-0 translate-y-1" 76 leave-to-class="opacity-0 translate-y-1" 77 > 78 <div v-if="isOpen" class="absolute inset-ie-0 top-full pt-2 w-56 z-50"> 79 <div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"> 80 <div class="px-3 py-2 border-b border-border"> 81 <span class="font-mono text-xs text-fg-subtle">{{ 82 $t('header.orgs_dropdown.title') 83 }}</span> 84 </div> 85 86 <div v-if="isLoading" class="px-3 py-4 text-center"> 87 <span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.loading') }}</span> 88 </div> 89 90 <div v-else-if="error" class="px-3 py-4 text-center"> 91 <span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.error') }}</span> 92 </div> 93 94 <ul v-else-if="orgs.length > 0" class="py-1 max-h-80 overflow-y-auto"> 95 <li v-for="org in orgs" :key="org"> 96 <NuxtLink 97 :to="{ name: 'org', params: { org } }" 98 class="block px-3 py-2 font-mono text-sm text-fg hover:bg-bg-subtle transition-colors" 99 > 100 @{{ org }} 101 </NuxtLink> 102 </li> 103 </ul> 104 105 <div v-else class="px-3 py-4 text-center"> 106 <span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.empty') }}</span> 107 </div> 108 109 <div class="px-3 py-2 border-t border-border"> 110 <NuxtLink 111 :to="{ name: '~username-orgs', params: { username } }" 112 class="link-subtle font-mono text-xs inline-flex items-center gap-1" 113 > 114 {{ $t('header.orgs_dropdown.view_all') }} 115 <span class="i-lucide:arrow-right rtl-flip w-3 h-3" aria-hidden="true" /> 116 </NuxtLink> 117 </div> 118 </div> 119 </div> 120 </Transition> 121 </div> 122</template>