Skip to content

Commit 98c68f5

Browse files
authored
feat: use navigation builder for desktop and mobile links (#1235)
1 parent ac7e638 commit 98c68f5

File tree

4 files changed

+188
-159
lines changed

4 files changed

+188
-159
lines changed

app/components/AppHeader.vue

Lines changed: 118 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,106 @@ 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+
type: 'link',
56+
external: false,
57+
iconClass: 'i-carbon:information',
58+
},
59+
{
60+
name: 'Privacy Policy',
61+
label: $t('privacy_policy.title'),
62+
to: { name: 'privacy' },
63+
type: 'link',
64+
external: false,
65+
iconClass: 'i-carbon:security',
66+
},
67+
],
68+
},
69+
{
70+
type: 'separator',
71+
},
72+
{
73+
name: 'External Links',
74+
type: 'group',
75+
label: $t('nav.links'),
76+
items: [
77+
{
78+
name: 'Docs',
79+
label: $t('footer.docs'),
80+
href: 'https://docs.npmx.dev',
81+
target: '_blank',
82+
type: 'link',
83+
external: true,
84+
iconClass: 'i-carbon:document',
85+
},
86+
{
87+
name: 'Source',
88+
label: $t('footer.source'),
89+
href: 'https://repo.npmx.dev',
90+
target: '_blank',
91+
type: 'link',
92+
external: true,
93+
iconClass: 'i-carbon:logo-github',
94+
},
95+
{
96+
name: 'Social',
97+
label: $t('footer.social'),
98+
href: 'https://social.npmx.dev',
99+
target: '_blank',
100+
type: 'link',
101+
external: true,
102+
iconClass: 'i-simple-icons:bluesky',
103+
},
104+
{
105+
name: 'Chat',
106+
label: $t('footer.chat'),
107+
href: 'https://chat.npmx.dev',
108+
target: '_blank',
109+
type: 'link',
110+
external: true,
111+
iconClass: 'i-carbon:chat',
112+
},
113+
],
114+
},
115+
])
116+
16117
const showFullSearch = shallowRef(false)
17118
const showMobileMenu = shallowRef(false)
18119
@@ -63,23 +164,18 @@ function handleSearchFocus() {
63164
}
64165
65166
onKeyStroke(
66-
e => isKeyWithoutModifiers(e, ',') && !isEditableElement(e.target),
67167
e => {
68-
e.preventDefault()
69-
navigateTo({ name: 'settings' })
70-
},
71-
{ dedupe: true },
72-
)
168+
if (isEditableElement(e.target)) {
169+
return
170+
}
73171
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' })
172+
for (const link of desktopLinks.value) {
173+
if (link.to && link.keyshortcut && isKeyWithoutModifiers(e, link.keyshortcut)) {
174+
e.preventDefault()
175+
navigateTo(link.to.name)
176+
break
177+
}
178+
}
83179
},
84180
{ dedupe: true },
85181
)
@@ -156,24 +252,16 @@ onKeyStroke(
156252

157253
<!-- End: Desktop nav items + Mobile menu button -->
158254
<div class="hidden sm:flex flex-shrink-0">
159-
<!-- Desktop: Compare link -->
160-
<LinkBase
161-
class="border-none"
162-
variant="button-secondary"
163-
:to="{ name: 'compare' }"
164-
keyshortcut="c"
165-
>
166-
{{ $t('nav.compare') }}
167-
</LinkBase>
168-
169-
<!-- Desktop: Settings link -->
255+
<!-- Desktop: Explore link -->
170256
<LinkBase
257+
v-for="link in desktopLinks"
258+
:key="link.name"
171259
class="border-none"
172260
variant="button-secondary"
173-
:to="{ name: 'settings' }"
174-
keyshortcut=","
261+
:to="link.to"
262+
:keyshortcut="link.keyshortcut"
175263
>
176-
{{ $t('nav.settings') }}
264+
{{ link.label }}
177265
</LinkBase>
178266

179267
<HeaderAccountMenu />
@@ -191,6 +279,6 @@ onKeyStroke(
191279
</nav>
192280

193281
<!-- Mobile menu -->
194-
<HeaderMobileMenu v-model:open="showMobileMenu" />
282+
<HeaderMobileMenu :links="mobileLinks" v-model:open="showMobileMenu" />
195283
</header>
196284
</template>

app/components/Header/MobileMenu.client.vue

Lines changed: 41 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
<script setup lang="ts">
22
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
33
import { useAtproto } from '~/composables/atproto/useAtproto'
4+
import type { NavigationConfigWithGroups } from '~/types'
45
56
const isOpen = defineModel<boolean>('open', { default: false })
7+
const { links } = defineProps<{
8+
links: NavigationConfigWithGroups
9+
}>()
610
711
const { isConnected, npmUser, avatar: npmAvatar } = useConnector()
812
const { user: atprotoUser } = useAtproto()
@@ -175,137 +179,45 @@ onUnmounted(deactivate)
175179

176180
<!-- Navigation links -->
177181
<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" />
246-
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') }}
261-
<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>
182+
<template v-for="(group, index) in links">
183+
<div
184+
v-if="group.type === 'separator'"
185+
:key="`seperator-${index}`"
186+
class="mx-4 my-2 border-t border-border"
187+
/>
294188

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') }}
189+
<div v-if="group.type === 'group'" :key="group.name" class="p-2">
303190
<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>
191+
v-if="group.label"
192+
class="px-3 py-2 font-mono text-xs text-fg-subtle uppercase tracking-wider"
193+
>
194+
{{ group.label }}
195+
</span>
196+
<div>
197+
<NuxtLink
198+
v-for="link in group.items"
199+
:key="link.name"
200+
:to="link.to"
201+
:href="link.href"
202+
:target="link.target"
203+
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"
204+
@click="closeMenu"
205+
>
206+
<span
207+
:class="link.iconClass"
208+
class="w-5 h-5 text-fg-muted"
209+
aria-hidden="true"
210+
/>
211+
{{ link.label }}
212+
<span
213+
v-if="link.external"
214+
class="i-carbon:launch rtl-flip w-3 h-3 ms-auto text-fg-subtle"
215+
aria-hidden="true"
216+
/>
217+
</NuxtLink>
218+
</div>
219+
</div>
220+
</template>
309221
</div>
310222
</nav>
311223
</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)