Skip to content

Commit c9badfc

Browse files
arg0NNYmvrlin
authored andcommitted
feat: per-page configuration (#85)
1 parent c92c842 commit c9badfc

12 files changed

+186
-111
lines changed

README.md

+20
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,26 @@ CSS media feature.
209209
}
210210
```
211211

212+
## Per-page configuration
213+
You can override the global configuration for specific pages using [`definePageMeta`](https://nuxt.com/docs/api/utils/define-page-meta).
214+
```vue
215+
<script setup>
216+
definePageMeta({
217+
viewport: {
218+
breakpoints: {
219+
desktop: 1024,
220+
mobile: 320,
221+
tablet: 768
222+
},
223+
cookie: {
224+
name: 'viewport-per-page'
225+
}
226+
// Other fields will be inherited from the global configuration
227+
}
228+
})
229+
</script>
230+
```
231+
212232
## API
213233

214234
### `viewport.breakpoint`
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<template>
2+
<section>
3+
<p>
4+
Your value of breakpoint {{ $viewport.breakpoint.value }} is:
5+
<b>{{ $viewport.breakpointValue($viewport.breakpoint.value) }}</b>
6+
</p>
7+
<p>
8+
Your current breakpoint is: <b>{{ $viewport.breakpoint }}</b>
9+
</p>
10+
11+
<div v-for="breakpoint in ['mobile', 'tablet', 'desktop']" :key="breakpoint">
12+
<hr />
13+
14+
<h4>{{ breakpoint }}</h4>
15+
16+
<p>
17+
<code>match</code> → <b>{{ $viewport.match(breakpoint) }}</b>
18+
</p>
19+
<p>
20+
<code>isLessThan</code> → <b>{{ $viewport.isLessThan(breakpoint) }}</b>
21+
</p>
22+
<p>
23+
<code>isLessOrEquals</code> → <b>{{ $viewport.isLessOrEquals(breakpoint) }}</b>
24+
</p>
25+
<p>
26+
<code>isGreaterThan</code> → <b>{{ $viewport.isGreaterThan(breakpoint) }}</b>
27+
</p>
28+
<p>
29+
<code>isGreaterOrEquals</code> → <b>{{ $viewport.isGreaterOrEquals(breakpoint) }}</b>
30+
</p>
31+
</div>
32+
</section>
33+
</template>
34+
35+
<style>
36+
* {
37+
box-sizing: border-box;
38+
padding: 0;
39+
margin: 0;
40+
}
41+
42+
html {
43+
font-family: sans-serif;
44+
font-size: 120%;
45+
}
46+
47+
body {
48+
display: flex;
49+
justify-content: center;
50+
align-items: center;
51+
min-height: 100vh;
52+
}
53+
54+
section {
55+
max-width: calc(100vw - 1rem);
56+
background: #eee;
57+
padding: 2rem;
58+
border-radius: 1rem;
59+
}
60+
61+
hr {
62+
display: block;
63+
border: none;
64+
border-top: 0.1rem dashed #ccc;
65+
margin: 1rem auto;
66+
}
67+
68+
p {
69+
margin: 0.5rem 0;
70+
}
71+
72+
h4 {
73+
text-decoration: underline;
74+
}
75+
76+
code {
77+
display: inline-block;
78+
padding: 0.25em 0.35rem;
79+
background: #ccc;
80+
border-radius: 0.25rem;
81+
}
82+
</style>

playground/pages/index.vue

+1-80
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,3 @@
11
<template>
2-
<section>
3-
<p>
4-
Your value of breakpoint {{ $viewport.breakpoint.value }} is:
5-
<b>{{ $viewport.breakpointValue($viewport.breakpoint.value) }}</b>
6-
</p>
7-
<p>
8-
Your current breakpoint is: <b>{{ $viewport.breakpoint }}</b>
9-
</p>
10-
11-
<div v-for="breakpoint in ['mobile', 'tablet', 'desktop']" :key="breakpoint">
12-
<hr />
13-
14-
<h4>{{ breakpoint }}</h4>
15-
16-
<p>
17-
<code>match</code> → <b>{{ $viewport.match(breakpoint) }}</b>
18-
</p>
19-
<p>
20-
<code>isLessThan</code> → <b>{{ $viewport.isLessThan(breakpoint) }}</b>
21-
</p>
22-
<p>
23-
<code>isLessOrEquals</code> → <b>{{ $viewport.isLessOrEquals(breakpoint) }}</b>
24-
</p>
25-
<p>
26-
<code>isGreaterThan</code> → <b>{{ $viewport.isGreaterThan(breakpoint) }}</b>
27-
</p>
28-
<p>
29-
<code>isGreaterOrEquals</code> → <b>{{ $viewport.isGreaterOrEquals(breakpoint) }}</b>
30-
</p>
31-
</div>
32-
</section>
2+
<ViewportState />
333
</template>
34-
35-
<style>
36-
* {
37-
box-sizing: border-box;
38-
padding: 0;
39-
margin: 0;
40-
}
41-
42-
html {
43-
font-family: sans-serif;
44-
font-size: 120%;
45-
}
46-
47-
body {
48-
display: flex;
49-
justify-content: center;
50-
align-items: center;
51-
min-height: 100vh;
52-
}
53-
54-
section {
55-
max-width: calc(100vw - 1rem);
56-
background: #eee;
57-
padding: 2rem;
58-
border-radius: 1rem;
59-
}
60-
61-
hr {
62-
display: block;
63-
border: none;
64-
border-top: 0.1rem dashed #ccc;
65-
margin: 1rem auto;
66-
}
67-
68-
p {
69-
margin: 0.5rem 0;
70-
}
71-
72-
h4 {
73-
text-decoration: underline;
74-
}
75-
76-
code {
77-
display: inline-block;
78-
padding: 0.25em 0.35rem;
79-
background: #ccc;
80-
border-radius: 0.25rem;
81-
}
82-
</style>

playground/pages/per-page-config.vue

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<ViewportState />
3+
</template>
4+
5+
<script setup lang="ts">
6+
import { definePageMeta } from '#imports'
7+
8+
definePageMeta({
9+
viewport: {
10+
breakpoints: {
11+
desktop: 1024,
12+
mobile: 320,
13+
tablet: 768,
14+
},
15+
cookie: {
16+
name: 'viewport-per-page-config',
17+
},
18+
},
19+
})
20+
</script>

src/module.ts

+4-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { fileURLToPath } from 'url'
2-
import { addImports, addPlugin, addTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
2+
import { addImports, addPlugin, addTemplate, addTypeTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
33

44
import { name, version } from '../package.json'
55

6-
import { DEFAULT_OPTIONS } from './constants'
7-
import type { ViewportOptions } from './types'
6+
import type { ViewportOptions } from './runtime/types'
7+
import { extendOptions } from './runtime/utils'
88

99
export type ModuleOptions = ViewportOptions
1010

@@ -21,15 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
2121
},
2222

2323
setup(options, nuxt) {
24-
options = {
25-
...DEFAULT_OPTIONS,
26-
...options,
27-
28-
cookie: {
29-
...DEFAULT_OPTIONS.cookie,
30-
...options.cookie,
31-
},
32-
}
24+
options = extendOptions(options)
3325

3426
const { resolve } = createResolver(import.meta.url)
3527
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))

src/runtime/composables.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { type ViewportManager } from '../types'
2-
import { useNuxtApp } from '#imports'
1+
import { computed, type ComputedRef } from 'vue-demi'
2+
import type { ViewportManager, ViewportOptions } from './types'
3+
import { extendOptions } from './utils'
4+
import { useNuxtApp, useRoute } from '#imports'
5+
import globalOptions from '#viewport-options'
36

47
export function useViewport(): ViewportManager {
58
return useNuxtApp().$viewport
69
}
10+
11+
export function useViewportOptions(): ComputedRef<ViewportOptions> {
12+
const route = useRoute()
13+
return computed(() => extendOptions(route.meta.viewport, globalOptions))
14+
}

src/constants.ts renamed to src/runtime/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ViewportOptions } from './types'
1+
import type { ViewportOptions } from './types'
22

33
export const COOKIE_EXPIRES_IN_DAYS = 365
44

src/runtime/manager.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import cookie from 'cookiejs'
2-
import { computed, type Ref } from 'vue-demi'
2+
import { computed, type MaybeRefOrGetter, type Ref, toRef, toValue } from 'vue-demi'
33

4-
import type { ViewportOptions, ViewportQuery } from '../types'
4+
import type { ViewportOptions, ViewportQuery } from './types'
55

66
export const STATE_KEY = 'viewportState'
77

8-
export function createViewportManager(options: ViewportOptions, state: Ref<string>) {
8+
export function createViewportManager(options: MaybeRefOrGetter<ViewportOptions>, state: Ref<string>) {
9+
options = toRef(options)
10+
911
const breakpoint = computed<string>({
1012
get() {
11-
return state.value || options.fallbackBreakpoint
13+
return state.value || options.value.fallbackBreakpoint
1214
},
1315

1416
set(newBreakpoint) {
1517
state.value = newBreakpoint
1618

17-
if (typeof window !== 'undefined' && options.cookie.name) {
18-
cookie.set(options.cookie.name, state.value, options.cookie)
19+
if (typeof window !== 'undefined' && options.value.cookie.name) {
20+
cookie.set(options.value.cookie.name, state.value, options.value.cookie)
1921
}
2022
},
2123
})
2224

2325
const queries = computed<Record<string, ViewportQuery>>(() => {
24-
const breakpoints = options.breakpoints || {}
26+
const breakpoints = options.value.breakpoints || {}
2527
const breakpointsKeys = Object.keys(breakpoints).sort((a, b) => breakpoints[a] - breakpoints[b])
2628

2729
const output: Record<string, ViewportQuery> = {}
@@ -36,7 +38,7 @@ export function createViewportManager(options: ViewportOptions, state: Ref<strin
3638

3739
let mediaQuery = ''
3840

39-
if (options.feature === 'minWidth') {
41+
if (options.value.feature === 'minWidth') {
4042
if (i > 0) {
4143
mediaQuery = `(min-width: ${size}px)`
4244
} else {
@@ -86,7 +88,7 @@ export function createViewportManager(options: ViewportOptions, state: Ref<strin
8688
* @param searchBreakpoint - Breakpoint to search.
8789
*/
8890
function breakpointValue(searchBreakpoint: string) {
89-
const breakpoints = options.breakpoints || {}
91+
const breakpoints = toValue(options).breakpoints || {}
9092

9193
return breakpoints[searchBreakpoint]
9294
}

src/runtime/plugin.client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { createViewportManager, STATE_KEY } from './manager'
22

3+
import { useViewportOptions } from './composables'
34
import { defineNuxtPlugin, useState } from '#imports'
4-
import viewportOptions from '#viewport-options'
55

6-
export default defineNuxtPlugin(async (nuxtApp) => {
6+
export default defineNuxtPlugin((nuxtApp) => {
7+
const viewportOptions = useViewportOptions()
78
const state = useState<string>(STATE_KEY)
89
const manager = createViewportManager(viewportOptions, state)
910

src/runtime/plugin.server.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { createViewportManager, STATE_KEY } from './manager'
22
import { detectBreakpoint, parseCookie } from './utils'
33

4+
import { useViewportOptions } from './composables'
45
import { defineNuxtPlugin, useState } from '#imports'
5-
import viewportOptions from '#viewport-options'
66

77
export default defineNuxtPlugin(async (nuxtApp) => {
8+
const viewportOptions = useViewportOptions()
89
const state = useState<string>(STATE_KEY)
910
const manager = createViewportManager(viewportOptions, state)
1011

@@ -22,8 +23,10 @@ export default defineNuxtPlugin(async (nuxtApp) => {
2223
cookie = ''
2324
}
2425

25-
cookie = parseCookie(cookie)[viewportOptions.cookie?.name]
26-
state.value = await detectBreakpoint(viewportOptions, { cookie, headers })
26+
state.value = await detectBreakpoint(viewportOptions.value, {
27+
cookie: viewportOptions.value.cookie?.name ? parseCookie(cookie)[viewportOptions.value.cookie.name] : undefined,
28+
headers,
29+
})
2730

2831
return nuxtApp.provide('viewport', manager)
2932
})

src/types.ts renamed to src/runtime/types.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CookieOptions } from 'cookiejs'
2-
import type { createViewportManager } from './runtime/manager'
2+
import type { createViewportManager } from './manager'
33

44
/**
55
* Viewport cookie options.
@@ -71,6 +71,16 @@ declare module 'vue/types/vue' {
7171
declare module '#app' {
7272
// eslint-disable-next-line no-use-before-define
7373
interface NuxtApp extends PluginInjection {}
74+
75+
interface PageMeta {
76+
viewport?: Partial<ViewportOptions>
77+
}
78+
}
79+
80+
declare module 'vue-router' {
81+
interface RouteMeta {
82+
viewport?: Partial<ViewportOptions>
83+
}
7484
}
7585

7686
interface PluginInjection {

0 commit comments

Comments
 (0)