Skip to content

Commit 94abefe

Browse files
committed
feat: add about, user, orgs, and packages to nav
1. Add an "about" nav item (and a new about page, content tbd) 2. Always show "github" item instead of hiding it when logged in 3. Add logged in username next to 🟢, clicking brings up `/~user` 4. when logged in, shows new 'packages' + 'orgs' nav items - clicking packages goes to user's packages - clicking orgs goes to user's orgs (new page, lists user's orgs, with org card showing # of pkgs + user's teams) - hovering on packages pops down user's 10 most recently updated packages (this is a placeholder, eventually user prefs screen will allow pinning packages and such) - hovering on orgs pops down user's 10 first orgs alphabetically (this is a placeholder, eventually user prefs screen will allow pinning orgs and such) Closes #95
1 parent 2df63c6 commit 94abefe

13 files changed

Lines changed: 926 additions & 33 deletions

File tree

app/components/AppHeader.vue

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,28 @@ withDefaults(
99
showConnector: true,
1010
},
1111
)
12+
13+
const { isConnected, npmUser } = useConnector()
1214
</script>
1315

1416
<template>
1517
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
16-
<nav aria-label="Main navigation" class="container h-14 flex items-center justify-between">
17-
<NuxtLink
18-
v-if="showLogo"
19-
to="/"
20-
aria-label="npmx home"
21-
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
22-
>
23-
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
24-
</NuxtLink>
25-
<!-- Spacer when logo is hidden -->
26-
<span v-else class="w-1" />
27-
28-
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
18+
<nav aria-label="Main navigation" class="container h-14 flex items-center">
19+
<!-- Left: Logo -->
20+
<div class="flex-shrink-0">
21+
<NuxtLink
22+
v-if="showLogo"
23+
to="/"
24+
aria-label="npmx home"
25+
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
26+
>
27+
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
28+
</NuxtLink>
29+
<span v-else class="w-1" />
30+
</div>
31+
32+
<!-- Center: Main nav items -->
33+
<ul class="flex-1 flex items-center justify-center gap-4 sm:gap-6 list-none m-0 p-0">
2934
<li class="flex">
3035
<NuxtLink
3136
to="/search"
@@ -38,20 +43,38 @@ withDefaults(
3843
>
3944
</NuxtLink>
4045
</li>
41-
<li v-if="showConnector" class="flex">
42-
<ConnectorStatus />
46+
47+
<!-- Packages dropdown (when connected) -->
48+
<li v-if="isConnected && npmUser" class="flex">
49+
<HeaderPackagesDropdown :username="npmUser" />
4350
</li>
44-
<li v-else class="flex">
45-
<a
46-
href="https://github.com/npmx-dev/npmx.dev"
47-
rel="noopener noreferrer"
48-
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
49-
>
50-
<span class="i-carbon-logo-github w-4 h-4" />
51-
<span class="hidden sm:inline">github</span>
52-
</a>
51+
52+
<!-- Orgs dropdown (when connected) -->
53+
<li v-if="isConnected && npmUser" class="flex">
54+
<HeaderOrgsDropdown :username="npmUser" />
55+
</li>
56+
57+
<li class="flex">
58+
<NuxtLink to="/about" class="link-subtle font-mono text-sm"> about </NuxtLink>
5359
</li>
5460
</ul>
61+
62+
<!-- Right: User status + GitHub -->
63+
<div class="flex-shrink-0 flex items-center gap-6">
64+
<div v-if="showConnector">
65+
<ConnectorStatus />
66+
</div>
67+
68+
<a
69+
href="https://github.com/npmx-dev/npmx.dev"
70+
target="_blank"
71+
rel="noopener noreferrer"
72+
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
73+
>
74+
<span class="i-carbon-logo-github w-4 h-4" />
75+
<span class="hidden sm:inline">github</span>
76+
</a>
77+
</div>
5578
</nav>
5679
</header>
5780
</template>

app/components/ConnectorStatus.client.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ const { isConnected, isConnecting, npmUser, error, activeOperations, hasPendingO
55
const showModal = ref(false)
66
const showTooltip = ref(false)
77
8-
const statusText = computed(() => {
8+
const tooltipText = computed(() => {
99
if (isConnecting.value) return 'connecting…'
10-
if (isConnected.value && npmUser.value) return `connected as @${npmUser.value}`
1110
if (isConnected.value) return 'connected'
1211
return 'connect local CLI'
1312
})
@@ -30,7 +29,16 @@ const ariaLabel = computed(() => {
3029
</script>
3130

3231
<template>
33-
<div class="relative">
32+
<div class="relative flex items-center gap-2">
33+
<!-- Username link (when connected) -->
34+
<NuxtLink
35+
v-if="isConnected && npmUser"
36+
:to="`/~${npmUser}`"
37+
class="link-subtle font-mono text-sm hidden sm:inline"
38+
>
39+
@{{ npmUser }}
40+
</NuxtLink>
41+
3442
<button
3543
type="button"
3644
class="relative flex items-center justify-center w-8 h-8 rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@@ -70,7 +78,7 @@ const ariaLabel = computed(() => {
7078
role="tooltip"
7179
class="absolute right-0 top-full mt-2 px-2 py-1 font-mono text-xs text-fg bg-bg-elevated border border-border rounded shadow-lg whitespace-nowrap z-50"
7280
>
73-
{{ statusText }}
81+
{{ tooltipText }}
7482
</div>
7583
</Transition>
7684

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
username: string
4+
}>()
5+
6+
const { listUserOrgs } = useConnector()
7+
8+
const isOpen = ref(false)
9+
const isLoading = ref(false)
10+
const orgs = ref<string[]>([])
11+
const hasLoaded = ref(false)
12+
13+
async function loadOrgs() {
14+
if (hasLoaded.value || isLoading.value) return
15+
16+
isLoading.value = true
17+
try {
18+
const orgList = await listUserOrgs()
19+
if (orgList) {
20+
// Already sorted alphabetically by server, take top 10
21+
orgs.value = orgList.slice(0, 10)
22+
}
23+
hasLoaded.value = true
24+
} finally {
25+
isLoading.value = false
26+
}
27+
}
28+
29+
function handleMouseEnter() {
30+
isOpen.value = true
31+
if (!hasLoaded.value) {
32+
loadOrgs()
33+
}
34+
}
35+
36+
function handleMouseLeave() {
37+
isOpen.value = false
38+
}
39+
40+
function handleKeydown(event: KeyboardEvent) {
41+
if (event.key === 'Escape' && isOpen.value) {
42+
isOpen.value = false
43+
}
44+
}
45+
</script>
46+
47+
<template>
48+
<div
49+
class="relative"
50+
@mouseenter="handleMouseEnter"
51+
@mouseleave="handleMouseLeave"
52+
@keydown="handleKeydown"
53+
>
54+
<NuxtLink
55+
:to="`/~${username}/orgs`"
56+
class="link-subtle font-mono text-sm inline-flex items-center gap-1"
57+
>
58+
orgs
59+
<span
60+
class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200"
61+
:class="{ 'rotate-180': isOpen }"
62+
/>
63+
</NuxtLink>
64+
65+
<Transition
66+
enter-active-class="transition-all duration-150"
67+
leave-active-class="transition-all duration-100"
68+
enter-from-class="opacity-0 translate-y-1"
69+
leave-to-class="opacity-0 translate-y-1"
70+
>
71+
<div
72+
v-if="isOpen"
73+
class="absolute right-0 top-full mt-2 w-56 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 overflow-hidden"
74+
>
75+
<div class="px-3 py-2 border-b border-border">
76+
<span class="font-mono text-xs text-fg-subtle">Your Organizations</span>
77+
</div>
78+
79+
<div v-if="isLoading" class="px-3 py-4 text-center">
80+
<span class="text-fg-muted text-sm">Loading...</span>
81+
</div>
82+
83+
<ul v-else-if="orgs.length > 0" class="py-1 max-h-80 overflow-y-auto">
84+
<li v-for="org in orgs" :key="org">
85+
<NuxtLink
86+
:to="`/@${org}`"
87+
class="block px-3 py-2 font-mono text-sm text-fg hover:bg-bg-subtle transition-colors"
88+
>
89+
@{{ org }}
90+
</NuxtLink>
91+
</li>
92+
</ul>
93+
94+
<div v-else class="px-3 py-4 text-center">
95+
<span class="text-fg-muted text-sm">No organizations found</span>
96+
</div>
97+
98+
<div class="px-3 py-2 border-t border-border">
99+
<NuxtLink
100+
:to="`/~${username}/orgs`"
101+
class="link-subtle font-mono text-xs inline-flex items-center gap-1"
102+
>
103+
View all
104+
<span class="i-carbon-arrow-right w-3 h-3" />
105+
</NuxtLink>
106+
</div>
107+
</div>
108+
</Transition>
109+
</div>
110+
</template>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
username: string
4+
}>()
5+
6+
const { listUserPackages } = useConnector()
7+
8+
const isOpen = ref(false)
9+
const isLoading = ref(false)
10+
const packages = ref<Array<{ name: string; updated: string }>>([])
11+
const hasLoaded = ref(false)
12+
13+
async function loadPackages() {
14+
if (hasLoaded.value || isLoading.value) return
15+
16+
isLoading.value = true
17+
try {
18+
const pkgMap = await listUserPackages()
19+
if (pkgMap) {
20+
const pkgNames = Object.keys(pkgMap)
21+
22+
// Fetch package info to get update times using our server API
23+
// Limit to 20 packages to avoid too many parallel requests
24+
const pkgInfoPromises = pkgNames.slice(0, 20).map(async name => {
25+
try {
26+
const response = await $fetch<{ time?: { modified?: string } }>(
27+
`/api/registry/${encodeURIComponent(name).replace('%40', '@')}`,
28+
{ timeout: 5000 },
29+
)
30+
return {
31+
name,
32+
updated: response.time?.modified || '',
33+
}
34+
} catch {
35+
return { name, updated: '' }
36+
}
37+
})
38+
39+
const results = await Promise.all(pkgInfoPromises)
40+
41+
// Sort by most recently updated
42+
packages.value = results
43+
.filter(p => p.updated)
44+
.sort((a, b) => b.updated.localeCompare(a.updated))
45+
.slice(0, 10)
46+
47+
// If we couldn't get update times, fall back to alphabetical
48+
if (packages.value.length === 0) {
49+
packages.value = pkgNames
50+
.sort()
51+
.slice(0, 10)
52+
.map(name => ({ name, updated: '' }))
53+
}
54+
}
55+
hasLoaded.value = true
56+
} finally {
57+
isLoading.value = false
58+
}
59+
}
60+
61+
function handleMouseEnter() {
62+
isOpen.value = true
63+
if (!hasLoaded.value) {
64+
loadPackages()
65+
}
66+
}
67+
68+
function handleMouseLeave() {
69+
isOpen.value = false
70+
}
71+
72+
function handleKeydown(event: KeyboardEvent) {
73+
if (event.key === 'Escape' && isOpen.value) {
74+
isOpen.value = false
75+
}
76+
}
77+
78+
function getPackageUrl(pkgName: string): string {
79+
return `/${pkgName}`
80+
}
81+
</script>
82+
83+
<template>
84+
<div
85+
class="relative"
86+
@mouseenter="handleMouseEnter"
87+
@mouseleave="handleMouseLeave"
88+
@keydown="handleKeydown"
89+
>
90+
<NuxtLink
91+
:to="`/~${username}`"
92+
class="link-subtle font-mono text-sm inline-flex items-center gap-1"
93+
>
94+
packages
95+
<span
96+
class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200"
97+
:class="{ 'rotate-180': isOpen }"
98+
/>
99+
</NuxtLink>
100+
101+
<Transition
102+
enter-active-class="transition-all duration-150"
103+
leave-active-class="transition-all duration-100"
104+
enter-from-class="opacity-0 translate-y-1"
105+
leave-to-class="opacity-0 translate-y-1"
106+
>
107+
<div
108+
v-if="isOpen"
109+
class="absolute right-0 top-full mt-2 w-64 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 overflow-hidden"
110+
>
111+
<div class="px-3 py-2 border-b border-border">
112+
<span class="font-mono text-xs text-fg-subtle">Your Packages</span>
113+
</div>
114+
115+
<div v-if="isLoading" class="px-3 py-4 text-center">
116+
<span class="text-fg-muted text-sm">Loading...</span>
117+
</div>
118+
119+
<ul v-else-if="packages.length > 0" class="py-1 max-h-80 overflow-y-auto">
120+
<li v-for="pkg in packages" :key="pkg.name">
121+
<NuxtLink
122+
:to="getPackageUrl(pkg.name)"
123+
class="block px-3 py-2 font-mono text-sm text-fg hover:bg-bg-subtle transition-colors truncate"
124+
>
125+
{{ pkg.name }}
126+
</NuxtLink>
127+
</li>
128+
</ul>
129+
130+
<div v-else class="px-3 py-4 text-center">
131+
<span class="text-fg-muted text-sm">No packages found</span>
132+
</div>
133+
134+
<div class="px-3 py-2 border-t border-border">
135+
<NuxtLink
136+
:to="`/~${username}`"
137+
class="link-subtle font-mono text-xs inline-flex items-center gap-1"
138+
>
139+
View all
140+
<span class="i-carbon-arrow-right w-3 h-3" />
141+
</NuxtLink>
142+
</div>
143+
</div>
144+
</Transition>
145+
</div>
146+
</template>

0 commit comments

Comments
 (0)