[READ-ONLY] a fast, modern browser for the npm registry
at main 274 lines 9.5 kB view raw
1<script setup lang="ts"> 2import { useAtproto } from '~/composables/atproto/useAtproto' 3import { useModal } from '~/composables/useModal' 4 5const { 6 isConnected: isNpmConnected, 7 isConnecting: isNpmConnecting, 8 npmUser, 9 avatar: npmAvatar, 10 activeOperations, 11 hasPendingOperations, 12} = useConnector() 13 14const { user: atprotoUser } = useAtproto() 15 16const isOpen = shallowRef(false) 17 18/** Check if connected to at least one service */ 19const hasAnyConnection = computed(() => isNpmConnected.value || !!atprotoUser.value) 20 21/** Check if connected to both services */ 22const hasBothConnections = computed(() => isNpmConnected.value && !!atprotoUser.value) 23 24/** Only show count of active (pending/approved/running) operations */ 25const operationCount = computed(() => activeOperations.value.length) 26 27const accountMenuRef = useTemplateRef('accountMenuRef') 28 29onClickOutside(accountMenuRef, () => { 30 isOpen.value = false 31}) 32 33useEventListener('keydown', event => { 34 if (event.key === 'Escape' && isOpen.value) { 35 isOpen.value = false 36 } 37}) 38 39const connectorModal = useModal('connector-modal') 40 41function openConnectorModal() { 42 if (connectorModal) { 43 isOpen.value = false 44 connectorModal.open() 45 } 46} 47 48const authModal = useModal('auth-modal') 49 50function openAuthModal() { 51 if (authModal) { 52 isOpen.value = false 53 authModal.open() 54 } 55} 56</script> 57 58<template> 59 <div ref="accountMenuRef" class="relative flex min-w-28 justify-end"> 60 <ButtonBase 61 type="button" 62 :aria-expanded="isOpen" 63 aria-haspopup="true" 64 @click="isOpen = !isOpen" 65 class="border-none" 66 > 67 <!-- Stacked avatars when connected --> 68 <span 69 v-if="hasAnyConnection" 70 class="flex items-center" 71 :class="hasBothConnections ? '-space-x-2' : ''" 72 > 73 <!-- npm avatar (first/back) --> 74 <img 75 v-if="isNpmConnected && npmAvatar" 76 :src="npmAvatar" 77 :alt="npmUser || $t('account_menu.npm_cli')" 78 width="24" 79 height="24" 80 class="w-6 h-6 rounded-full ring-2 ring-bg object-cover" 81 /> 82 <span 83 v-else-if="isNpmConnected" 84 class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center" 85 > 86 <span class="i-lucide:terminal w-3 h-3 text-fg-muted" aria-hidden="true" /> 87 </span> 88 89 <!-- Atmosphere avatar (second/front, overlapping) --> 90 <img 91 v-if="atprotoUser?.avatar" 92 :src="atprotoUser.avatar" 93 :alt="atprotoUser.handle" 94 width="24" 95 height="24" 96 class="w-6 h-6 rounded-full ring-2 ring-bg object-cover" 97 :class="hasBothConnections ? 'relative z-10' : ''" 98 /> 99 <span 100 v-else-if="atprotoUser" 101 class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center" 102 :class="hasBothConnections ? 'relative z-10' : ''" 103 > 104 <span class="i-lucide:at-sign w-3 h-3 text-fg-muted" aria-hidden="true" /> 105 </span> 106 </span> 107 108 <!-- "connect" text when not connected --> 109 <span v-if="!hasAnyConnection" class="font-mono text-sm"> 110 {{ $t('account_menu.connect') }} 111 </span> 112 113 <!-- Chevron --> 114 <span 115 class="i-lucide:chevron-down w-3 h-3 transition-transform duration-200" 116 :class="{ 'rotate-180': isOpen }" 117 aria-hidden="true" 118 /> 119 120 <!-- Operation count badge (when npm connected with pending ops) --> 121 <span 122 v-if="isNpmConnected && operationCount > 0" 123 class="absolute -top-1 -inset-ie-1 min-w-[1rem] h-4 px-1 flex items-center justify-center font-mono text-3xs rounded-full" 124 :class="hasPendingOperations ? 'bg-yellow-500 text-black' : 'bg-blue-500 text-white'" 125 aria-hidden="true" 126 > 127 {{ operationCount }} 128 </span> 129 </ButtonBase> 130 131 <!-- Dropdown menu --> 132 <Transition 133 enter-active-class="transition-all duration-150" 134 leave-active-class="transition-all duration-100" 135 enter-from-class="opacity-0 translate-y-1" 136 leave-to-class="opacity-0 translate-y-1" 137 > 138 <div v-if="isOpen" class="absolute inset-ie-0 top-full pt-2 w-72 z-50" role="menu"> 139 <div 140 class="bg-bg-subtle/80 backdrop-blur-sm border border-border-subtle rounded-lg shadow-lg shadow-bg-elevated/50 overflow-hidden px-1" 141 > 142 <!-- Connected accounts section --> 143 <div v-if="hasAnyConnection" class="py-1"> 144 <!-- npm CLI connection --> 145 <ButtonBase 146 v-if="isNpmConnected && npmUser" 147 role="menuitem" 148 class="w-full text-start gap-x-3 border-none" 149 @click="openConnectorModal" 150 out 151 > 152 <img 153 v-if="npmAvatar" 154 :src="npmAvatar" 155 :alt="npmUser" 156 width="32" 157 height="32" 158 class="w-8 h-8 rounded-full object-cover" 159 /> 160 <span 161 v-else 162 class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center" 163 > 164 <span class="i-lucide:terminal w-4 h-4 text-fg-muted" aria-hidden="true" /> 165 </span> 166 <span class="flex-1 min-w-0"> 167 <span class="font-mono text-sm text-fg truncate block">~{{ npmUser }}</span> 168 <span class="text-xs text-fg-subtle">{{ $t('account_menu.npm_cli') }}</span> 169 </span> 170 <span 171 v-if="operationCount > 0" 172 class="px-1.5 py-0.5 font-mono text-xs rounded" 173 :class=" 174 hasPendingOperations 175 ? 'bg-yellow-500/20 text-yellow-600' 176 : 'bg-blue-500/20 text-blue-500' 177 " 178 > 179 {{ 180 $t('account_menu.ops', { 181 count: operationCount, 182 }) 183 }} 184 </span> 185 </ButtonBase> 186 187 <!-- Atmosphere connection --> 188 <ButtonBase 189 v-if="atprotoUser" 190 role="menuitem" 191 class="w-full text-start gap-x-3 border-none" 192 @click="openAuthModal" 193 > 194 <img 195 v-if="atprotoUser.avatar" 196 :src="atprotoUser.avatar" 197 :alt="atprotoUser.handle" 198 width="32" 199 height="32" 200 class="w-8 h-8 rounded-full object-cover" 201 /> 202 <span 203 v-else 204 class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center" 205 > 206 <span class="i-lucide:at-sign w-4 h-4 text-fg-muted" aria-hidden="true" /> 207 </span> 208 <span class="flex-1 min-w-0"> 209 <span class="font-mono text-sm text-fg truncate block" 210 >@{{ atprotoUser.handle }}</span 211 > 212 <span class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere') }}</span> 213 </span> 214 </ButtonBase> 215 </div> 216 217 <!-- Divider (only if we have connections AND options to connect) --> 218 <div 219 v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser)" 220 class="border-t border-border" 221 /> 222 223 <!-- Connect options --> 224 <div v-if="!isNpmConnected || !atprotoUser" class="py-1"> 225 <ButtonBase 226 v-if="!isNpmConnected" 227 role="menuitem" 228 class="w-full text-start gap-x-3 border-none" 229 @click="openConnectorModal" 230 > 231 <span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"> 232 <span 233 v-if="isNpmConnecting" 234 class="i-svg-spinners:ring-resize w-4 h-4 text-yellow-500 animate-spin" 235 aria-hidden="true" 236 /> 237 <span v-else class="i-lucide:terminal w-4 h-4 text-fg-muted" aria-hidden="true" /> 238 </span> 239 <span class="flex-1 min-w-0"> 240 <span class="font-mono text-sm text-fg block"> 241 {{ 242 isNpmConnecting 243 ? $t('account_menu.connecting') 244 : $t('account_menu.connect_npm_cli') 245 }} 246 </span> 247 <span class="text-xs text-fg-subtle">{{ $t('account_menu.npm_cli_desc') }}</span> 248 </span> 249 </ButtonBase> 250 251 <ButtonBase 252 v-if="!atprotoUser" 253 role="menuitem" 254 class="w-full text-start gap-x-3 border-none" 255 @click="openAuthModal" 256 > 257 <span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"> 258 <span class="i-lucide:at-sign w-4 h-4 text-fg-muted" aria-hidden="true" /> 259 </span> 260 <span class="flex-1 min-w-0"> 261 <span class="font-mono text-sm text-fg block"> 262 {{ $t('account_menu.connect_atmosphere') }} 263 </span> 264 <span class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere_desc') }}</span> 265 </span> 266 </ButtonBase> 267 </div> 268 </div> 269 </div> 270 </Transition> 271 </div> 272 <HeaderConnectorModal /> 273 <HeaderAuthModal /> 274</template>