Skip to content

Commit 5f61c17

Browse files
committed
feat: use dropdown for atmosphere + npm
1 parent f9bdefb commit 5f61c17

5 files changed

Lines changed: 272 additions & 10 deletions

File tree

app/components/AppHeader.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
withDefaults(
33
defineProps<{
44
showLogo?: boolean
5-
showConnector?: boolean
65
}>(),
76
{
87
showLogo: true,
9-
showConnector: true,
108
},
119
)
1210
@@ -98,11 +96,7 @@ onKeyStroke(',', e => {
9896
</kbd>
9997
</NuxtLink>
10098

101-
<div v-if="showConnector" class="hidden sm:block">
102-
<ConnectorStatus />
103-
</div>
104-
105-
<AuthButton />
99+
<HeaderAccountMenu />
106100
</div>
107101
</nav>
108102
</header>
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<script setup lang="ts">
2+
const {
3+
isConnected: isNpmConnected,
4+
isConnecting: isNpmConnecting,
5+
npmUser,
6+
avatar: npmAvatar,
7+
activeOperations,
8+
hasPendingOperations,
9+
} = useConnector()
10+
11+
const { user: atprotoUser } = useAtproto()
12+
13+
const isOpen = ref(false)
14+
const showConnectorModal = ref(false)
15+
const showAuthModal = ref(false)
16+
17+
/** Check if connected to at least one service */
18+
const hasAnyConnection = computed(() => isNpmConnected.value || !!atprotoUser.value)
19+
20+
/** Check if connected to both services */
21+
const hasBothConnections = computed(() => isNpmConnected.value && !!atprotoUser.value)
22+
23+
/** Only show count of active (pending/approved/running) operations */
24+
const operationCount = computed(() => activeOperations.value.length)
25+
26+
function handleClickOutside(event: MouseEvent) {
27+
const target = event.target as HTMLElement
28+
if (!target.closest('.account-menu')) {
29+
isOpen.value = false
30+
}
31+
}
32+
33+
function handleKeydown(event: KeyboardEvent) {
34+
if (event.key === 'Escape' && isOpen.value) {
35+
isOpen.value = false
36+
}
37+
}
38+
39+
onMounted(() => {
40+
document.addEventListener('click', handleClickOutside)
41+
})
42+
43+
onUnmounted(() => {
44+
document.removeEventListener('click', handleClickOutside)
45+
})
46+
47+
function openConnectorModal() {
48+
isOpen.value = false
49+
showConnectorModal.value = true
50+
}
51+
52+
function openAuthModal() {
53+
isOpen.value = false
54+
showAuthModal.value = true
55+
}
56+
</script>
57+
58+
<template>
59+
<div class="account-menu relative" @keydown="handleKeydown">
60+
<button
61+
type="button"
62+
class="relative flex items-center gap-2 px-2 py-1.5 rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
63+
:aria-expanded="isOpen"
64+
aria-haspopup="true"
65+
@click="isOpen = !isOpen"
66+
>
67+
<!-- Stacked avatars when connected -->
68+
<div
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"
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-carbon-terminal w-3 h-3 text-fg-muted" aria-hidden="true" />
87+
</span>
88+
89+
<!-- Atmosphere avatar (second/front, overlapping) -->
90+
<span
91+
v-if="atprotoUser"
92+
class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center"
93+
:class="hasBothConnections ? 'relative z-10' : ''"
94+
>
95+
<span class="i-carbon-cloud w-3 h-3 text-fg-muted" aria-hidden="true" />
96+
</span>
97+
</div>
98+
99+
<!-- "connect" text when not connected -->
100+
<span v-if="!hasAnyConnection" class="font-mono text-sm">
101+
{{ $t('account_menu.connect') }}
102+
</span>
103+
104+
<!-- Chevron -->
105+
<span
106+
class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200"
107+
:class="{ 'rotate-180': isOpen }"
108+
aria-hidden="true"
109+
/>
110+
111+
<!-- Operation count badge (when npm connected with pending ops) -->
112+
<span
113+
v-if="isNpmConnected && operationCount > 0"
114+
class="absolute -top-1 -inset-ie-1 min-w-[1rem] h-4 px-1 flex items-center justify-center font-mono text-[10px] rounded-full"
115+
:class="hasPendingOperations ? 'bg-yellow-500 text-black' : 'bg-blue-500 text-white'"
116+
aria-hidden="true"
117+
>
118+
{{ operationCount }}
119+
</span>
120+
</button>
121+
122+
<!-- Dropdown menu -->
123+
<Transition
124+
enter-active-class="transition-all duration-150"
125+
leave-active-class="transition-all duration-100"
126+
enter-from-class="opacity-0 translate-y-1"
127+
leave-to-class="opacity-0 translate-y-1"
128+
>
129+
<div v-if="isOpen" class="absolute inset-ie-0 top-full pt-2 w-72 z-50" role="menu">
130+
<div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden">
131+
<!-- Connected accounts section -->
132+
<div v-if="hasAnyConnection" class="py-1">
133+
<!-- npm CLI connection -->
134+
<button
135+
v-if="isNpmConnected && npmUser"
136+
type="button"
137+
role="menuitem"
138+
class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start"
139+
@click="openConnectorModal"
140+
>
141+
<img
142+
v-if="npmAvatar"
143+
:src="npmAvatar"
144+
:alt="npmUser"
145+
width="32"
146+
height="32"
147+
class="w-8 h-8 rounded-full"
148+
/>
149+
<span
150+
v-else
151+
class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center"
152+
>
153+
<span class="i-carbon-terminal w-4 h-4 text-fg-muted" aria-hidden="true" />
154+
</span>
155+
<div class="flex-1 min-w-0">
156+
<div class="font-mono text-sm text-fg truncate">~{{ npmUser }}</div>
157+
<div class="text-xs text-fg-subtle">{{ $t('account_menu.npm_cli') }}</div>
158+
</div>
159+
<span
160+
v-if="operationCount > 0"
161+
class="px-1.5 py-0.5 font-mono text-xs rounded"
162+
:class="
163+
hasPendingOperations
164+
? 'bg-yellow-500/20 text-yellow-600'
165+
: 'bg-blue-500/20 text-blue-500'
166+
"
167+
>
168+
{{ operationCount }} {{ $t('account_menu.ops') }}
169+
</span>
170+
</button>
171+
172+
<!-- Atmosphere connection -->
173+
<button
174+
v-if="atprotoUser"
175+
type="button"
176+
role="menuitem"
177+
class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start"
178+
@click="openAuthModal"
179+
>
180+
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
181+
<span class="i-carbon-cloud w-4 h-4 text-fg-muted" aria-hidden="true" />
182+
</span>
183+
<div class="flex-1 min-w-0">
184+
<div class="font-mono text-sm text-fg truncate">@{{ atprotoUser.handle }}</div>
185+
<div class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere') }}</div>
186+
</div>
187+
</button>
188+
</div>
189+
190+
<!-- Divider (only if we have connections AND options to connect) -->
191+
<div
192+
v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser)"
193+
class="border-t border-border"
194+
/>
195+
196+
<!-- Connect options -->
197+
<div v-if="!isNpmConnected || !atprotoUser" class="py-1">
198+
<button
199+
v-if="!isNpmConnected"
200+
type="button"
201+
role="menuitem"
202+
class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start"
203+
@click="openConnectorModal"
204+
>
205+
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
206+
<span
207+
v-if="isNpmConnecting"
208+
class="i-carbon-circle-dash w-4 h-4 text-yellow-500 animate-spin"
209+
aria-hidden="true"
210+
/>
211+
<span v-else class="i-carbon-terminal w-4 h-4 text-fg-muted" aria-hidden="true" />
212+
</span>
213+
<div class="flex-1 min-w-0">
214+
<div class="font-mono text-sm text-fg">
215+
{{
216+
isNpmConnecting
217+
? $t('account_menu.connecting')
218+
: $t('account_menu.connect_npm_cli')
219+
}}
220+
</div>
221+
<div class="text-xs text-fg-subtle">{{ $t('account_menu.npm_cli_desc') }}</div>
222+
</div>
223+
</button>
224+
225+
<button
226+
v-if="!atprotoUser"
227+
type="button"
228+
role="menuitem"
229+
class="w-full px-3 py-2.5 flex items-center gap-3 hover:bg-bg-subtle transition-colors text-start"
230+
@click="openAuthModal"
231+
>
232+
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
233+
<span class="i-carbon-cloud w-4 h-4 text-fg-muted" aria-hidden="true" />
234+
</span>
235+
<div class="flex-1 min-w-0">
236+
<div class="font-mono text-sm text-fg">
237+
{{ $t('account_menu.connect_atmosphere') }}
238+
</div>
239+
<div class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere_desc') }}</div>
240+
</div>
241+
</button>
242+
</div>
243+
</div>
244+
</div>
245+
</Transition>
246+
247+
<!-- Modals -->
248+
<ConnectorModal v-model:open="showConnectorModal" />
249+
<AuthModal v-model:open="showAuthModal" />
250+
</div>
251+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<div class="flex items-center gap-2 px-2 py-1.5">
3+
<span class="font-mono text-sm text-fg-muted">{{ $t('account_menu.connect') }}</span>
4+
</div>
5+
</template>

app/error.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ useHead({
3131

3232
<template>
3333
<div class="min-h-screen flex flex-col bg-bg text-fg">
34-
<AppHeader :show-connector="false" />
34+
<AppHeader />
3535

3636
<main class="flex-1 container flex flex-col items-center justify-center py-20 text-center">
3737
<p class="font-mono text-8xl sm:text-9xl font-medium text-fg-subtle mb-4">

i18n/locales/en.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@
347347
"connector": {
348348
"status": {
349349
"connecting": "connecting...",
350-
"connected_as": "connected as {'@'}{user}",
350+
"connected_as": "connected as ~{user}",
351351
"connected": "connected",
352352
"connect_cli": "connect local CLI",
353353
"aria_connecting": "Connecting to local connector",
@@ -359,7 +359,7 @@
359359
"title": "Local Connector",
360360
"close_modal": "Close modal",
361361
"connected": "Connected",
362-
"logged_in_as": "Logged in as {'@'}{user}",
362+
"logged_in_as": "Logged in as ~{user}",
363363
"connected_hint": "You can now manage packages and organizations from the web UI.",
364364
"disconnect": "Disconnect",
365365
"run_hint": "Run the connector on your machine to enable admin features.",
@@ -729,6 +729,18 @@
729729
}
730730
}
731731
},
732+
"account_menu": {
733+
"connect": "connect",
734+
"account": "Account",
735+
"npm_cli": "npm CLI",
736+
"atmosphere": "Atmosphere",
737+
"npm_cli_desc": "Manage packages & orgs",
738+
"atmosphere_desc": "Social features & identity",
739+
"connect_npm_cli": "Connect to npm CLI",
740+
"connect_atmosphere": "Connect to Atmosphere",
741+
"connecting": "Connecting...",
742+
"ops": "ops"
743+
},
732744
"header": {
733745
"home": "npmx home",
734746
"github": "GitHub",

0 commit comments

Comments
 (0)