forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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>