Skip to content

Commit 7f8174d

Browse files
committed
feat: use navigation builder for desktop and mobile links
1 parent 80e8db3 commit 7f8174d

File tree

4 files changed

+189
-159
lines changed

4 files changed

+189
-159
lines changed

app/components/AppHeader.vue

Lines changed: 124 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { LinkBase } from '#components'
3+
import type { NavigationConfig, NavigationConfigWithGroups } from '~/types'
34
import { isEditableElement } from '~/utils/input'
45
56
withDefaults(
@@ -13,6 +14,112 @@ withDefaults(
1314
1415
const { isConnected, npmUser } = useConnector()
1516
17+
const desktopLinks = computed<NavigationConfig>(() => [
18+
{
19+
name: 'Compare',
20+
label: $t('nav.compare'),
21+
to: { name: 'compare' },
22+
keyshortcut: 'c',
23+
type: 'link',
24+
external: false,
25+
iconClass: 'i-carbon:compare',
26+
},
27+
{
28+
name: 'Settings',
29+
label: $t('nav.settings'),
30+
to: { name: 'settings' },
31+
keyshortcut: ',',
32+
type: 'link',
33+
external: false,
34+
iconClass: 'i-carbon:settings',
35+
},
36+
])
37+
38+
const mobileLinks = computed<NavigationConfigWithGroups>(() => [
39+
{
40+
name: 'Desktop Links',
41+
type: 'group',
42+
items: [...desktopLinks.value],
43+
},
44+
{
45+
type: 'separator',
46+
},
47+
{
48+
name: 'About & Policies',
49+
type: 'group',
50+
items: [
51+
{
52+
name: 'About',
53+
label: $t('footer.about'),
54+
to: { name: 'about' },
55+
keyshortcut: null,
56+
type: 'link',
57+
external: false,
58+
iconClass: 'i-carbon:information',
59+
},
60+
{
61+
name: 'Privacy Policy',
62+
label: $t('privacy_policy.title'),
63+
to: { name: 'privacy' },
64+
keyshortcut: null,
65+
type: 'link',
66+
external: false,
67+
iconClass: 'i-carbon:security',
68+
},
69+
],
70+
},
71+
{
72+
type: 'separator',
73+
},
74+
{
75+
name: 'External Links',
76+
type: 'group',
77+
label: $t('nav.links'),
78+
items: [
79+
{
80+
name: 'Docs',
81+
label: $t('footer.docs'),
82+
href: 'https://docs.npmx.dev',
83+
target: '_blank',
84+
keyshortcut: null,
85+
type: 'link',
86+
external: true,
87+
iconClass: 'i-carbon:document',
88+
},
89+
{
90+
name: 'Source',
91+
label: $t('footer.source'),
92+
href: 'https://repo.npmx.dev',
93+
target: '_blank',
94+
keyshortcut: null,
95+
type: 'link',
96+
external: true,
97+
iconClass: 'i-carbon:logo-github',
98+
},
99+
{
100+
name: 'Social',
101+
label: $t('footer.social'),
102+
href: 'https://social.npmx.dev',
103+
target: '_blank',
104+
keyshortcut: null,
105+
type: 'link',
106+
external: true,
107+
iconClass: 'i-simple-icons:bluesky',
108+
},
109+
{
110+
name: 'Chat',
111+
label: $t('footer.chat'),
112+
href: 'https://chat.npmx.dev',
113+
target: '_blank',
114+
keyshortcut: null,
115+
type: 'link',
116+
external: true,
117+
iconClass: 'i-carbon:chat',
118+
},
119+
],
120+
},
121+
])
122+
16123
const showFullSearch = shallowRef(false)
17124
const showMobileMenu = shallowRef(false)
18125
@@ -63,23 +170,18 @@ function handleSearchFocus() {
63170
}
64171
65172
onKeyStroke(
66-
e => isKeyWithoutModifiers(e, ',') && !isEditableElement(e.target),
67173
e => {
68-
e.preventDefault()
69-
navigateTo({ name: 'settings' })
70-
},
71-
{ dedupe: true },
72-
)
174+
if (isEditableElement(e.target)) {
175+
return
176+
}
73177
74-
onKeyStroke(
75-
e =>
76-
isKeyWithoutModifiers(e, 'c') &&
77-
!isEditableElement(e.target) &&
78-
// Allow more specific handlers to take precedence
79-
!e.defaultPrevented,
80-
e => {
81-
e.preventDefault()
82-
navigateTo({ name: 'compare' })
178+
for (const link of desktopLinks.value) {
179+
if (isKeyWithoutModifiers(e, link.keyshortcut)) {
180+
e.preventDefault()
181+
navigateTo(link.to.name)
182+
break
183+
}
184+
}
83185
},
84186
{ dedupe: true },
85187
)
@@ -152,24 +254,16 @@ onKeyStroke(
152254

153255
<!-- End: Desktop nav items + Mobile menu button -->
154256
<div class="hidden sm:flex flex-shrink-0">
155-
<!-- Desktop: Compare link -->
156-
<LinkBase
157-
class="border-none"
158-
variant="button-secondary"
159-
:to="{ name: 'compare' }"
160-
keyshortcut="c"
161-
>
162-
{{ $t('nav.compare') }}
163-
</LinkBase>
164-
165-
<!-- Desktop: Settings link -->
257+
<!-- Desktop: Explore link -->
166258
<LinkBase
259+
v-for="link in desktopLinks"
260+
:key="link.name"
167261
class="border-none"
168262
variant="button-secondary"
169-
:to="{ name: 'settings' }"
170-
keyshortcut=","
263+
:to="link.to"
264+
:keyshortcut="link.keyshortcut"
171265
>
172-
{{ $t('nav.settings') }}
266+
{{ link.label }}
173267
</LinkBase>
174268

175269
<HeaderAccountMenu />
@@ -187,6 +281,6 @@ onKeyStroke(
187281
</nav>
188282

189283
<!-- Mobile menu -->
190-
<HeaderMobileMenu v-model:open="showMobileMenu" />
284+
<HeaderMobileMenu :links="mobileLinks" v-model:open="showMobileMenu" />
191285
</header>
192286
</template>

app/components/Header/MobileMenu.client.vue

Lines changed: 36 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
33
import { useAtproto } from '~/composables/atproto/useAtproto'
44
55
const isOpen = defineModel<boolean>('open', { default: false })
6+
const { links } = defineProps<{
7+
links: NavigationConfigWithGroups
8+
}>()
69
710
const { isConnected, npmUser, avatar: npmAvatar } = useConnector()
811
const { user: atprotoUser } = useAtproto()
@@ -175,137 +178,41 @@ onUnmounted(deactivate)
175178

176179
<!-- Navigation links -->
177180
<div class="flex-1 overflow-y-auto overscroll-contain py-2">
178-
<!-- App navigation -->
179-
<div class="px-2 py-2">
180-
<NuxtLink
181-
:to="{ name: 'compare' }"
182-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
183-
@click="closeMenu"
184-
>
185-
<span class="i-carbon:compare w-5 h-5 text-fg-muted" aria-hidden="true" />
186-
{{ $t('nav.compare') }}
187-
</NuxtLink>
188-
189-
<NuxtLink
190-
:to="{ name: 'settings' }"
191-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
192-
@click="closeMenu"
193-
>
194-
<span class="i-carbon:settings w-5 h-5 text-fg-muted" aria-hidden="true" />
195-
{{ $t('nav.settings') }}
196-
</NuxtLink>
197-
198-
<!-- Connected user links -->
199-
<template v-if="isConnected && npmUser">
200-
<NuxtLink
201-
:to="{ name: '~username', params: { username: npmUser } }"
202-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
203-
@click="closeMenu"
204-
>
205-
<span class="i-carbon:package w-5 h-5 text-fg-muted" aria-hidden="true" />
206-
{{ $t('header.packages') }}
207-
</NuxtLink>
208-
209-
<NuxtLink
210-
:to="{ name: '~username-orgs', params: { username: npmUser } }"
211-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
212-
@click="closeMenu"
213-
>
214-
<span class="i-carbon:enterprise w-5 h-5 text-fg-muted" aria-hidden="true" />
215-
{{ $t('header.orgs') }}
216-
</NuxtLink>
217-
</template>
218-
</div>
219-
220-
<!-- Divider -->
221-
<div class="mx-4 my-2 border-t border-border" />
222-
223-
<!-- Informational links -->
224-
<div class="px-2 py-2">
225-
<NuxtLink
226-
:to="{ name: 'about' }"
227-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
228-
@click="closeMenu"
229-
>
230-
<span class="i-carbon:information w-5 h-5 text-fg-muted" aria-hidden="true" />
231-
{{ $t('footer.about') }}
232-
</NuxtLink>
233-
234-
<NuxtLink
235-
:to="{ name: 'privacy' }"
236-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
237-
@click="closeMenu"
238-
>
239-
<span class="i-carbon:security w-5 h-5 text-fg-muted" aria-hidden="true" />
240-
{{ $t('privacy_policy.title') }}
241-
</NuxtLink>
242-
</div>
243-
244-
<!-- Divider -->
245-
<div class="mx-4 my-2 border-t border-border" />
181+
<template v-for="group in links">
182+
<div v-if="group.type === 'separator'" class="mx-4 my-2 border-t border-border" />
246183

247-
<!-- External links -->
248-
<div class="px-2 py-2">
249-
<span class="px-3 py-2 font-mono text-xs text-fg-subtle uppercase tracking-wider">
250-
{{ $t('nav.links') }}
251-
</span>
252-
253-
<a
254-
href="https://docs.npmx.dev"
255-
target="_blank"
256-
rel="noopener noreferrer"
257-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
258-
>
259-
<span class="i-carbon:document w-5 h-5 text-fg-muted" aria-hidden="true" />
260-
{{ $t('footer.docs') }}
184+
<div v-if="group.type === 'group'" :key="group.name" class="p-2">
261185
<span
262-
class="i-carbon:launch rtl-flip w-3 h-3 ms-auto text-fg-subtle"
263-
aria-hidden="true"
264-
/>
265-
</a>
266-
267-
<a
268-
href="https://repo.npmx.dev"
269-
target="_blank"
270-
rel="noopener noreferrer"
271-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
272-
>
273-
<span class="i-carbon:logo-github w-5 h-5 text-fg-muted" aria-hidden="true" />
274-
{{ $t('footer.source') }}
275-
<span
276-
class="i-carbon:launch rtl-flip w-3 h-3 ms-auto text-fg-subtle"
277-
aria-hidden="true"
278-
/>
279-
</a>
280-
281-
<a
282-
href="https://social.npmx.dev"
283-
target="_blank"
284-
rel="noopener noreferrer"
285-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
286-
>
287-
<span class="i-simple-icons:bluesky w-5 h-5 text-fg-muted" aria-hidden="true" />
288-
{{ $t('footer.social') }}
289-
<span
290-
class="i-carbon:launch rtl-flip w-3 h-3 ms-auto text-fg-subtle"
291-
aria-hidden="true"
292-
/>
293-
</a>
294-
295-
<a
296-
href="https://chat.npmx.dev"
297-
target="_blank"
298-
rel="noopener noreferrer"
299-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
300-
>
301-
<span class="i-carbon:chat w-5 h-5 text-fg-muted" aria-hidden="true" />
302-
{{ $t('footer.chat') }}
303-
<span
304-
class="i-carbon:launch rtl-flip w-3 h-3 ms-auto text-fg-subtle"
305-
aria-hidden="true"
306-
/>
307-
</a>
308-
</div>
186+
v-if="group.label"
187+
class="px-3 py-2 font-mono text-xs text-fg-subtle uppercase tracking-wider"
188+
>
189+
{{ group.label }}
190+
</span>
191+
<div>
192+
<NuxtLink
193+
v-for="link in group.items"
194+
:key="link.name"
195+
:to="link.to"
196+
:href="link.href"
197+
:target="link.target"
198+
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
199+
@click="link?.to && closeMenu"
200+
>
201+
<span
202+
:class="link.iconClass"
203+
class="w-5 h-5 text-fg-muted"
204+
aria-hidden="true"
205+
/>
206+
{{ link.label }}
207+
<span
208+
v-if="link.external"
209+
class="i-carbon:launch rtl-flip w-3 h-3 ms-auto text-fg-subtle"
210+
aria-hidden="true"
211+
/>
212+
</NuxtLink>
213+
</div>
214+
</div>
215+
</template>
309216
</div>
310217
</nav>
311218
</Transition>

app/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './navigation'

0 commit comments

Comments
 (0)