Skip to content

Commit 5df8a7c

Browse files
committed
feat: add main navigation
1 parent e79d983 commit 5df8a7c

File tree

6 files changed

+294
-0
lines changed

6 files changed

+294
-0
lines changed

playground/nuxt.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,17 @@ export default defineNuxtConfig({
66
brand: {
77
name: 'nuxtify-pages-playground',
88
},
9+
navigation: {
10+
primary: [
11+
{ text: 'Home', to: '/' },
12+
{ text: 'About', to: '/about' },
13+
{ text: 'External', href: 'https://nuxtify.dev', openInNew: true },
14+
],
15+
secondary: [
16+
{ text: 'Sign Up', to: '/signup' },
17+
{ text: 'Sign In', to: '/signin' },
18+
{ text: 'Help', to: '/help' },
19+
],
20+
},
921
},
1022
})

src/module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export default defineNuxtModule<ModuleOptions>({
4040
buttonText: '',
4141
buttonUrl: '',
4242
},
43+
navigation: {
44+
primary: [],
45+
secondary: [],
46+
},
4347
},
4448
async setup(_options, _nuxt) {
4549
const resolver = createResolver(import.meta.url)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<script setup lang="ts">
2+
import { useDisplay, useDrawer, useNuxtifyConfig, mdiArrowTopRight, mdiClose, mdiMenu } from '#imports'
3+
4+
// App state
5+
const { smAndDown } = useDisplay()
6+
const nuxtifyConfig = useNuxtifyConfig()
7+
const drawer = useDrawer()
8+
9+
// Navigation
10+
const primaryNavLinks = nuxtifyConfig.navigation?.primary
11+
const secondaryNavLinks = nuxtifyConfig.navigation?.secondary
12+
const featuredSecondaryLink = secondaryNavLinks?.slice(0, 1)[0] // first link gets featured
13+
</script>
14+
15+
<template>
16+
<v-app-bar
17+
:density="smAndDown ? 'compact' : 'default'"
18+
flat
19+
class="px-sm-2 bottom-border"
20+
>
21+
<template #prepend>
22+
<!-- Logo -->
23+
<NuxtLink
24+
to="/"
25+
class="ml-2"
26+
>
27+
<AppLogo />
28+
</NuxtLink>
29+
</template>
30+
31+
<!-- Desktop navigation -->
32+
<div v-if="!smAndDown">
33+
<!-- Primary links -->
34+
<v-btn
35+
v-for="link in primaryNavLinks"
36+
:key="link.text"
37+
:to="link.to"
38+
:href="link.href"
39+
:active="false"
40+
:prepend-icon="link.icon"
41+
slim
42+
exact
43+
:ripple="false"
44+
size="large"
45+
color="unset"
46+
:target="link.openInNew ? '_blank' : undefined"
47+
:rel="link.openInNew ? 'noopener nofollow' : undefined"
48+
class="nav-items mx-2"
49+
>
50+
{{ link.text }}
51+
<v-icon
52+
v-if="link.openInNew"
53+
:icon="mdiArrowTopRight"
54+
size="x-small"
55+
color="grey"
56+
class="ml-1"
57+
/>
58+
</v-btn>
59+
</div>
60+
61+
<template #append>
62+
<!-- Mobile navigation -->
63+
<v-app-bar-nav-icon
64+
v-if="smAndDown"
65+
:icon="drawer ? mdiClose : mdiMenu"
66+
color="primary"
67+
aria-label="Navigation Menu"
68+
@click="drawer = !drawer"
69+
/>
70+
71+
<!-- Desktop navigation -->
72+
<nav
73+
v-else
74+
class="d-flex align-center"
75+
>
76+
<!-- Secondary links -->
77+
<v-btn
78+
v-for="link in secondaryNavLinks?.slice(1).reverse()"
79+
:key="link.text"
80+
:to="link.to"
81+
:href="link.href"
82+
:prepend-icon="link.icon"
83+
:active="false"
84+
size="large"
85+
color="unset"
86+
:target="link.openInNew ? '_blank' : undefined"
87+
:rel="link.openInNew ? 'noopener nofollow' : undefined"
88+
class="nav-items mx-2"
89+
>
90+
{{ link.text }}
91+
<v-icon
92+
v-if="link.openInNew"
93+
:icon="mdiArrowTopRight"
94+
size="x-small"
95+
color="grey"
96+
class="ml-1"
97+
/>
98+
</v-btn>
99+
100+
<!-- Featured secondary link -->
101+
<v-btn
102+
v-if="featuredSecondaryLink?.text"
103+
:to="featuredSecondaryLink.to"
104+
:href="featuredSecondaryLink.href"
105+
:prepend-icon="featuredSecondaryLink.icon"
106+
:active="false"
107+
variant="flat"
108+
size="large"
109+
:target="featuredSecondaryLink.openInNew ? '_blank' : undefined"
110+
:rel="featuredSecondaryLink.openInNew ? 'noopener nofollow' : undefined"
111+
class="mx-2"
112+
>
113+
{{ featuredSecondaryLink.text }}
114+
<v-icon
115+
v-if="featuredSecondaryLink.openInNew"
116+
:icon="mdiArrowTopRight"
117+
size="small"
118+
class="ml-1"
119+
/>
120+
</v-btn>
121+
</nav>
122+
</template>
123+
</v-app-bar>
124+
</template>
125+
126+
<style scoped>
127+
/* Links */
128+
a {
129+
text-decoration: none;
130+
}
131+
132+
/* Nav links hover */
133+
.nav-items:hover {
134+
color: rgb(var(--v-theme-secondary)) !important;
135+
}
136+
:deep(.v-btn--variant-text .v-btn__overlay) {
137+
background-color: transparent;
138+
}
139+
140+
/* Bottom border */
141+
.bottom-border {
142+
border-bottom: 1px solid rgb(231, 237, 246);
143+
}
144+
145+
/* Vuetify app bar overrides */
146+
:deep(.v-toolbar__content) {
147+
align-self: center;
148+
max-width: 1280px;
149+
}
150+
:deep(.v-toolbar__prepend) {
151+
margin-inline: 4px 12px;
152+
}
153+
154+
/* Fix for emoji misalignment in button text */
155+
:deep(.v-btn__content) {
156+
line-height: 1;
157+
}
158+
</style>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<script setup lang="ts">
2+
import { useDisplay, useDrawer, useNuxtifyConfig, mdiArrowTopRight } from '#imports'
3+
4+
// App state
5+
const { smAndDown } = useDisplay()
6+
const nuxtifyConfig = useNuxtifyConfig()
7+
const drawer = useDrawer()
8+
9+
// Navigation
10+
const primaryNavLinks = nuxtifyConfig.navigation?.primary
11+
const secondaryNavLinks = nuxtifyConfig.navigation?.secondary
12+
const featuredSecondaryLink = secondaryNavLinks?.slice(0, 1)[0] // first link gets featured
13+
</script>
14+
15+
<template>
16+
<v-navigation-drawer
17+
v-if="smAndDown"
18+
v-model="drawer"
19+
location="right"
20+
>
21+
<nav>
22+
<v-list nav>
23+
<!-- Primary links -->
24+
<v-list-item
25+
v-for="(link, i) in primaryNavLinks"
26+
:key="i"
27+
:to="link.to"
28+
:href="link.href"
29+
:prepend-icon="link.icon"
30+
color="primary"
31+
:target="link.openInNew ? '_blank' : undefined"
32+
:rel="link.openInNew ? 'noopener nofollow' : undefined"
33+
slim
34+
exact
35+
>
36+
<v-list-item-title class="text-subtitle-1 font-weight-bold py-1">
37+
{{ link.text }}
38+
<v-icon
39+
v-if="link.openInNew"
40+
:icon="mdiArrowTopRight"
41+
size="x-small"
42+
color="grey"
43+
class="ml-1"
44+
/>
45+
</v-list-item-title>
46+
</v-list-item>
47+
48+
<!-- Secondary links -->
49+
<v-list-item
50+
v-for="link in secondaryNavLinks?.slice(1).reverse()"
51+
:key="link.text"
52+
:to="link.to"
53+
:href="link.href"
54+
:prepend-icon="link.icon"
55+
:target="link.openInNew ? '_blank' : undefined"
56+
:rel="link.openInNew ? 'noopener nofollow' : undefined"
57+
slim
58+
exact
59+
>
60+
<v-list-item-title class="text-subtitle-1 font-weight-bold py-1">
61+
{{ link.text }}
62+
<v-icon
63+
v-if="link.openInNew"
64+
:icon="mdiArrowTopRight"
65+
size="x-small"
66+
color="grey"
67+
class="ml-1"
68+
/>
69+
</v-list-item-title>
70+
</v-list-item>
71+
</v-list>
72+
</nav>
73+
74+
<template #append>
75+
<!-- Featured secondary link -->
76+
<div
77+
v-if="featuredSecondaryLink?.text"
78+
class="ma-2"
79+
>
80+
<v-btn
81+
:to="featuredSecondaryLink.to"
82+
:href="featuredSecondaryLink.href"
83+
:prepend-icon="featuredSecondaryLink.icon"
84+
variant="flat"
85+
size="large"
86+
:target="featuredSecondaryLink.openInNew ? '_blank' : undefined"
87+
:rel="featuredSecondaryLink.openInNew ? 'noopener nofollow' : undefined"
88+
block
89+
>
90+
{{ featuredSecondaryLink.text }}
91+
<v-icon
92+
v-if="featuredSecondaryLink.openInNew"
93+
:icon="mdiArrowTopRight"
94+
size="small"
95+
class="ml-1"
96+
/>
97+
</v-btn>
98+
</div>
99+
</template>
100+
</v-navigation-drawer>
101+
</template>

src/runtime/composables/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState } from '#app'
22
import { ref } from '#imports'
33

44
// App
5+
export const useDrawer = () => useState<boolean | null>('drawer', () => null)
56
export const useToast = () =>
67
useState('toast', () =>
78
ref({

src/runtime/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
import type { RouteLocationRaw } from 'vue-router'
2+
3+
interface Link {
4+
text: string
5+
to?: RouteLocationRaw
6+
href?: string
7+
icon?: string
8+
openInNew?: boolean
9+
}
10+
111
interface BrandOptions {
212
/**
313
* The name of the brand.
@@ -63,4 +73,12 @@ export interface ModuleOptions {
6373
buttonText?: string
6474
buttonUrl?: string
6575
}
76+
77+
/**
78+
* Navigation options
79+
*/
80+
navigation?: {
81+
primary?: Link[]
82+
secondary?: Link[]
83+
}
6684
}

0 commit comments

Comments
 (0)