├── demo ├── env.d.ts ├── public │ └── favicon.ico ├── src │ ├── main.ts │ ├── components │ │ ├── ResizeObserver.vue │ │ ├── Screens.vue │ │ └── MediaQuery.vue │ └── App.vue ├── tsconfig.vite-config.json ├── README.md ├── index.html ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── package.json └── package-lock.json ├── .gitignore ├── src ├── utils │ ├── defaultScreens.ts │ ├── window.ts │ ├── buildMediaQuery.ts │ ├── normalizeScreens.ts │ └── initScreens.ts ├── use │ ├── screens.ts │ ├── mediaQuery.ts │ ├── resizeObserver.ts │ └── darkMode.ts ├── plugins │ └── screens.ts └── index.ts ├── tsconfig.json ├── CHANGELOG.md ├── rollup.config.ts ├── package.json ├── LICENSE └── README.md /demo/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out-tsc 4 | .DS_Store 5 | .vscode -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanreyes/vue-screen-utils/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /src/utils/defaultScreens.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | xs: '0px', // (min-width: 0px) 3 | sm: '640px', 4 | md: '768px', 5 | lg: '1024px', 6 | xl: '1280px', 7 | }; 8 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { screens } from '@vsu'; 3 | import App from './App.vue'; 4 | 5 | createApp(App).use(screens).mount('#app'); 6 | -------------------------------------------------------------------------------- /demo/tsconfig.vite-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/window.ts: -------------------------------------------------------------------------------- 1 | export function windowExists() { 2 | return typeof window !== 'undefined'; 3 | } 4 | 5 | export function windowHasFeature(feature: string) { 6 | return windowExists() && feature in window; 7 | } 8 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Project Setup 6 | 7 | ```sh 8 | npm install 9 | ``` 10 | 11 | ### Compile and Hot-Reload for Development 12 | 13 | ```sh 14 | npm run dev 15 | ``` 16 | 17 | ### Type-Check, Compile and Minify for Production 18 | 19 | ```sh 20 | npm run build 21 | ``` 22 | -------------------------------------------------------------------------------- /src/use/screens.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted, provide } from 'vue'; 2 | import { Screens, ScreensOptions, initScreens, defaultInjectKey } from '../utils/initScreens'; 3 | 4 | export function useScreens(screens?: Screens, opts?: ScreensOptions) { 5 | const s = initScreens(screens); 6 | provide(opts?.injectKey || defaultInjectKey, s); 7 | onUnmounted(() => s!.cleanup()); 8 | return s; 9 | } 10 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)), 12 | '@vsu': fileURLToPath(new URL('../src', import.meta.url)), 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "module": "esnext", 3 | "moduleResolution": "node", 4 | "extends": "@vue/tsconfig/tsconfig.web.json", 5 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 6 | "compilerOptions": { 7 | "allowJs": true, 8 | "baseUrl": ".", 9 | "jsx": "preserve", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | }, 14 | 15 | "references": [ 16 | { 17 | "path": "./tsconfig.vite-config.json" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["types"], 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "outDir": "out-tsc", 8 | "sourceMap": true, 9 | "strict": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "declarationDir": "out-tsc/dts", 13 | "skipLibCheck": true, 14 | "esModuleInterop": false 15 | }, 16 | "include": ["./src"], 17 | "exclude": ["**/node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/components/ResizeObserver.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | 21 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit && vite build", 7 | "preview": "vite preview --port 5050", 8 | "typecheck": "vue-tsc --noEmit" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.2.33" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^16.11.27", 15 | "@vitejs/plugin-vue": "^2.3.1", 16 | "@vue/tsconfig": "^0.1.3", 17 | "typescript": "~4.6.3", 18 | "vite": "^2.9.5", 19 | "vue-tsc": "^0.34.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/screens.ts: -------------------------------------------------------------------------------- 1 | import { App, Plugin } from 'vue'; 2 | import { Screens, ScreensOptions, initScreens, defaultInjectKey } from '../utils/initScreens'; 3 | 4 | const plugin: Plugin = { 5 | install: (app: App, screens?: Screens, opts?: ScreensOptions) => { 6 | const s = initScreens(screens); 7 | const key = opts?.injectKey || defaultInjectKey; 8 | // Inject a globally available screens object method 9 | app.config.globalProperties[key] = s; 10 | // Provide screens object 11 | app.provide(key, s); 12 | }, 13 | }; 14 | 15 | export default plugin; 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.0-beta.6 2 | 3 | - Add new `useDarkMode` function for detecting dark mode using manual, user preference and class strategies. 4 | 5 | ## v1.0.0-beta.7 6 | 7 | - Fix argument type in `useDarkMode`. 8 | 9 | ## v1.0.0-beta.8 10 | 11 | - Refactor type exports. 12 | 13 | ## v1.0.0-beta.9 14 | 15 | - Update `DarkModeConfig` type. 16 | 17 | ## v1.0.0-beta.10 18 | 19 | - Fix SSR bugs 20 | 21 | ## v1.0.0-beta.11 22 | 23 | - Fix `initScreens` setup 24 | 25 | ## v1.0.0-beta.12 26 | 27 | - Fix `document` reference 28 | 29 | ## v1.0.0-beta.13 30 | 31 | - Fix build error 32 | -------------------------------------------------------------------------------- /src/utils/buildMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import type { NormalizedScreenValue } from './normalizeScreens'; 2 | 3 | // This function gratuitously borrowed from TailwindCSS 4 | // https://github.com/tailwindcss/tailwindcss/blob/master/src/util/buildMediaQuery.js 5 | export default function buildMediaQuery(screenValues: NormalizedScreenValue[]) { 6 | return screenValues 7 | .map((sv) => { 8 | if (sv.raw !== undefined) return sv.raw; 9 | return [sv.min && `(min-width: ${sv.min})`, sv.max && `(max-width: ${sv.max})`].filter(Boolean).join(' and '); 10 | }) 11 | .join(', '); 12 | } 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { MediaQueryCallback, useMediaQuery } from './use/mediaQuery'; 2 | export { ResizeObserverCallback, ResizeObserverOptions, useResizeObserver } from './use/resizeObserver'; 3 | export { DarkModeClassConfig, DarkModeConfig, useDarkMode } from './use/darkMode'; 4 | export { useScreens } from './use/screens'; 5 | export { default as screens } from './plugins/screens'; 6 | export { Screens, ScreensOptions, ScreensConfig } from './utils/initScreens'; 7 | export { NormalizedScreenValue, NormalizedScreen, normalizeScreens } from './utils/normalizeScreens'; 8 | export { default as buildMediaQuery } from './utils/buildMediaQuery'; 9 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import dts from 'rollup-plugin-dts'; 3 | 4 | export default [ 5 | { 6 | input: 'out-tsc/index.js', 7 | output: [ 8 | { 9 | format: 'esm', 10 | file: 'dist/index.mjs', 11 | sourcemap: true, 12 | }, 13 | { 14 | format: 'cjs', 15 | file: 'dist/index.js', 16 | sourcemap: true, 17 | }, 18 | ], 19 | external: ['vue'], 20 | plugins: [typescript()], 21 | }, 22 | { 23 | input: 'out-tsc/dts/index.d.ts', 24 | output: { 25 | format: 'es', 26 | file: 'dist/index.d.ts', 27 | }, 28 | plugins: [dts()], 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-screen-utils", 3 | "version": "1.0.0-beta.13", 4 | "description": "A dependency-free collection of screen utility functions in Vue 3.", 5 | "author": "Nathan Reyes ", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist/*" 11 | ], 12 | "scripts": { 13 | "compile": "tsc", 14 | "bundle": "rollup -c", 15 | "build": "rimraf dist out-tsc && tsc && rollup -c && rimraf out-tsc", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "keywords": [], 19 | "license": "MIT", 20 | "peerDependencies": { 21 | "vue": "^3.2.0" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-typescript": "^8.3.2", 25 | "rimraf": "^3.0.2", 26 | "rollup": "^2.74.1", 27 | "rollup-plugin-dts": "^4.2.2", 28 | "tslib": "^2.4.0", 29 | "typescript": "^4.6.4", 30 | "vue": "^3.2.36" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/src/components/Screens.vue: -------------------------------------------------------------------------------- 1 | 20 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Nathan Reyes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/use/mediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from 'vue'; 2 | import { windowHasFeature } from '../utils/window'; 3 | 4 | export type MediaQueryCallback = (ev?: MediaQueryListEvent) => void; 5 | 6 | export function useMediaQuery(query: string, callback: MediaQueryCallback) { 7 | let mediaQuery: MediaQueryList | undefined; 8 | const matches = ref(false); 9 | 10 | function listener(ev: MediaQueryListEvent) { 11 | if (callback) callback(ev); 12 | matches.value = ev.matches; 13 | } 14 | 15 | function cleanup() { 16 | if (mediaQuery) { 17 | mediaQuery.removeEventListener('change', listener); 18 | mediaQuery = undefined; 19 | } 20 | } 21 | 22 | function setup(newQuery = query) { 23 | cleanup(); 24 | if (windowHasFeature('matchMedia') && newQuery) { 25 | mediaQuery = window.matchMedia(newQuery); 26 | mediaQuery.addEventListener('change', listener); 27 | matches.value = mediaQuery.matches; 28 | } 29 | } 30 | 31 | onMounted(() => setup()); 32 | 33 | onUnmounted(() => cleanup()); 34 | 35 | return { matches, setup, cleanup }; 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/components/MediaQuery.vue: -------------------------------------------------------------------------------- 1 | 13 | 21 | 41 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 46 | -------------------------------------------------------------------------------- /src/use/resizeObserver.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, onUnmounted } from 'vue'; 2 | import type { Ref, ComponentPublicInstance } from 'vue'; 3 | import { windowHasFeature } from '../utils/window'; 4 | 5 | export type ResizeObserverCallback = (entries: ReadonlyArray, observer: ResizeObserver) => void; 6 | export interface ResizeObserverOptions { 7 | box?: 'content-box' | 'border-box'; 8 | } 9 | 10 | export function useResizeObserver( 11 | target: Ref, 12 | callback?: ResizeObserverCallback, 13 | options: ResizeObserverOptions = {} 14 | ) { 15 | let observer: ResizeObserver | undefined; 16 | const rect = ref(); 17 | 18 | const listener: ResizeObserverCallback = (...args) => { 19 | if (callback) callback(...args); 20 | const entry = args[0][0]; 21 | rect.value = entry.contentRect; 22 | }; 23 | 24 | const stopObserver = () => { 25 | if (observer) { 26 | observer.disconnect(); 27 | observer = undefined; 28 | } 29 | }; 30 | 31 | const stopWatch = watch( 32 | () => target.value, 33 | (elOrComp) => { 34 | stopObserver(); 35 | if (windowHasFeature('ResizeObserver') && elOrComp) { 36 | observer = new ResizeObserver(listener); 37 | observer.observe((elOrComp as ComponentPublicInstance).$el ?? elOrComp, options); 38 | } 39 | }, 40 | { immediate: true, flush: 'post' } 41 | ); 42 | 43 | const cleanup = () => { 44 | stopObserver(); 45 | stopWatch(); 46 | }; 47 | 48 | onUnmounted(() => cleanup()); 49 | 50 | return { rect, cleanup }; 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/normalizeScreens.ts: -------------------------------------------------------------------------------- 1 | export interface NormalizedScreenValue { 2 | min: string; 3 | max: string | undefined; 4 | raw?: string | undefined; 5 | } 6 | 7 | export interface NormalizedScreen { 8 | name: string; 9 | values: NormalizedScreenValue[]; 10 | } 11 | 12 | function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw }: any = {}): NormalizedScreenValue { 13 | return { min, max, raw }; 14 | } 15 | 16 | /** 17 | * A function that normalizes the various forms that the screens object can be 18 | * provided in. 19 | * 20 | * Input(s): 21 | * - ['100px', '200px'] // Raw strings 22 | * - { sm: '100px', md: '200px' } // Object with string values 23 | * - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values 24 | * - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values) 25 | * 26 | * Output(s): 27 | * - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values 28 | */ 29 | export function normalizeScreens(screens: any, root = true): NormalizedScreen[] { 30 | if (Array.isArray(screens)) { 31 | return screens.map((screen) => { 32 | if (root && Array.isArray(screen)) { 33 | throw new Error('The tuple syntax is not supported for `screens`.'); 34 | } 35 | 36 | if (typeof screen === 'string') { 37 | return { name: screen.toString(), values: [{ min: screen, max: undefined }] }; 38 | } 39 | 40 | let [name, options] = screen; 41 | name = name.toString(); 42 | 43 | if (typeof options === 'string') { 44 | return { name, values: [{ min: options, max: undefined }] }; 45 | } 46 | 47 | if (Array.isArray(options)) { 48 | return { name, values: options.map((option) => resolveValue(option)) }; 49 | } 50 | 51 | return { name, values: [resolveValue(options)] }; 52 | }); 53 | } 54 | return normalizeScreens(Object.entries(screens ?? {}), false); 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/initScreens.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from 'vue'; 2 | import { computed, reactive } from 'vue'; 3 | import { NormalizedScreen, normalizeScreens } from './normalizeScreens'; 4 | import buildMediaQuery from './buildMediaQuery'; 5 | import defaultScreens from './defaultScreens'; 6 | import { windowHasFeature } from './window'; 7 | 8 | export type Screens = Record; 9 | export type ScreensConfig = Record; 10 | 11 | export interface ScreensOptions { 12 | injectKey?: string; 13 | } 14 | 15 | interface ScreensState { 16 | screens: NormalizedScreen[]; 17 | queries: Record; 18 | matches: any; 19 | hasSetup: boolean; 20 | } 21 | 22 | export const defaultInjectKey = '$screens'; 23 | 24 | export function initScreens(screens?: Screens) { 25 | const state = reactive({ 26 | screens: normalizeScreens(screens || defaultScreens), 27 | queries: {}, 28 | matches: {}, 29 | hasSetup: false, 30 | }); 31 | 32 | function refreshMatches() { 33 | Object.entries(state.queries).forEach(([key, query]) => { 34 | state.matches[key] = query.matches; 35 | }); 36 | } 37 | 38 | function mapList(config: ScreensConfig): ComputedRef { 39 | return computed(() => 40 | Object.keys(state.matches) 41 | .filter((key) => state.matches[key] === true && config.hasOwnProperty(key)) 42 | .map((key) => config[key]) 43 | ); 44 | } 45 | 46 | const list = computed(() => Object.keys(state.matches).filter((k) => state.matches[k])); 47 | 48 | function mapCurrent(config: ScreensConfig, def?: any) { 49 | return computed(() => { 50 | const curr = current.value; 51 | if (curr && config.hasOwnProperty(curr)) return config[curr]; 52 | return def; 53 | }); 54 | } 55 | 56 | const current = computed(() => { 57 | const arr = list.value; 58 | if (arr.length) return arr[arr.length - 1]; 59 | return ''; 60 | }); 61 | 62 | function cleanup() { 63 | Object.values(state.queries).forEach((query) => query.removeEventListener('change', refreshMatches)); 64 | state.queries = {}; 65 | state.matches = {}; 66 | } 67 | 68 | if (!state.hasSetup && windowHasFeature('matchMedia')) { 69 | cleanup(); 70 | state.queries = state.screens.reduce((result, { name, values }) => { 71 | const mediaQuery = window.matchMedia(buildMediaQuery(values)); 72 | mediaQuery.addEventListener('change', refreshMatches); 73 | result[name] = mediaQuery; 74 | return result; 75 | }, {} as Record); 76 | refreshMatches(); 77 | state.hasSetup = true; 78 | } 79 | 80 | return { matches: state.matches, list, mapList, current, mapCurrent, cleanup }; 81 | } 82 | -------------------------------------------------------------------------------- /src/use/darkMode.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, computed, onUnmounted, watch } from 'vue'; 2 | import { windowExists, windowHasFeature } from '../utils/window'; 3 | 4 | export interface DarkModeClassConfig { 5 | selector: string; 6 | darkClass: string; 7 | } 8 | 9 | export type DarkModeConfig = boolean | 'system' | Partial; 10 | 11 | export function useDarkMode(config: Ref) { 12 | const isDark = ref(false); 13 | const displayMode = computed(() => (isDark.value ? 'dark' : 'light')); 14 | 15 | let mediaQuery: MediaQueryList | undefined; 16 | let mutationObserver: MutationObserver | undefined; 17 | 18 | function mqListener(ev: MediaQueryListEvent) { 19 | isDark.value = ev.matches; 20 | } 21 | 22 | function setupSystem() { 23 | if (windowHasFeature('matchMedia')) { 24 | mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 25 | mediaQuery.addEventListener('change', mqListener); 26 | isDark.value = mediaQuery.matches; 27 | } 28 | } 29 | 30 | function moListener() { 31 | const { selector = ':root', darkClass = 'dark' } = config.value as DarkModeClassConfig; 32 | const el = document.querySelector(selector); 33 | isDark.value = (el as HTMLElement).classList.contains(darkClass); 34 | } 35 | 36 | function setupClass(config: DarkModeClassConfig) { 37 | const { selector = ':root', darkClass = 'dark' } = config; 38 | if (windowExists() && selector && darkClass) { 39 | const el = document.querySelector(selector); 40 | if (el) { 41 | mutationObserver = new MutationObserver(moListener); 42 | mutationObserver.observe(el, { 43 | attributes: true, 44 | attributeFilter: ['class'], 45 | }); 46 | isDark.value = (el as HTMLElement).classList.contains(darkClass); 47 | } 48 | } 49 | } 50 | 51 | function setup() { 52 | stopObservers(); 53 | const type = typeof config.value; 54 | if (type === 'string' && (config.value as string).toLowerCase() === 'system') { 55 | setupSystem(); 56 | } else if (type === 'object') { 57 | setupClass(config.value as DarkModeClassConfig); 58 | } else { 59 | isDark.value = !!config.value; 60 | } 61 | } 62 | 63 | const stopWatch = watch( 64 | () => config.value, 65 | () => setup(), 66 | { 67 | immediate: true, 68 | } 69 | ); 70 | 71 | function stopObservers() { 72 | if (mediaQuery) { 73 | mediaQuery.removeEventListener('change', mqListener); 74 | mediaQuery = undefined; 75 | } 76 | if (mutationObserver) { 77 | mutationObserver.disconnect(); 78 | mutationObserver = undefined; 79 | } 80 | } 81 | 82 | function cleanup() { 83 | stopObservers(); 84 | stopWatch(); 85 | } 86 | 87 | onUnmounted(() => cleanup()); 88 | 89 | return { 90 | isDark, 91 | displayMode, 92 | cleanup, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-screen-utils 2 | 3 | A dependency-free collection of screen utility functions in Vue 3, written completely in TypeScript. 4 | 5 | - [**Use Screens**](#use-screens): Use function for mapping screen sizes to media query strings, arrays and custom values 6 | - [**Screens Plugin**](#screens-plugin): Same `useScreens` goodness but applied application-wide in a Vue plugin. 7 | - [**Use Media Query**](#use-media-query): Use function for evaluating simple media query strings. 8 | - [**Use Resize Observer**](#use-resize-observer): Use function for evaluating changes made to an ref element's content rect. 9 | - [**Use Dark Mode**](#use-dark-mode): Use function for observing dark mode using manual, class or system preference strategies. 10 | 11 | ## Install package 12 | 13 | ```console 14 | npm install vue-screen-utils 15 | ``` 16 | 17 | ## Use Screens 18 | 19 | ### Step 1. Import and call `useScreens` 20 | 21 | Import and call the `useScreens` function within a parent component, passing a config object that maps custom screen size keys to media query values. 22 | 23 | ```html 24 | 25 | 42 | ``` 43 | 44 | The `useScreens` function accepts a config object with screen size keys mapped to query values. A simple pixel value of '640px' will get mapped to 'min-width: 640px'. It is recommended to map a mobile-first key with a '0px' value followed by larger sizes. 45 | 46 | The query value may be in a variety of formats. 47 | 48 | ```js 49 | useScreens(['0px', '100px', '200px']); // Raw strings 50 | useScreens({ xs: '0px', sm: '100px', md: '200px' }); // Object with string values 51 | useScreens({ xs: { min: '0px' }, sm: { min: '100px' }, md: { min: '100px' } }); // Object with object values 52 | useScreens({ xs: [{ min: '0px' }, { max: '100px' }] }); // Object with object array (multiple values) 53 | ``` 54 | 55 | The `useScreens` function will return an [object](#screens-object) with a collection of utility properties and functions. This object will also get injected into the parent's child components as `$screens` (or custom `injectKey`). 56 | 57 | See notes about [cleanup](#cleanup). 58 | 59 | ### Step 2. Inject the `$screens` object into nested components. 60 | 61 | ```html 62 | 63 | 68 | ``` 69 | 70 | #### Matches Object 71 | 72 | The value of `matches` in the example above is a reactive object of size keys mapped to the match status of their respective media query. 73 | 74 | ```js 75 | // Viewport is 800px wide 76 | console.log(matches.value); // { xs: true, sm: true, md: true, lg: false, xl: false } 77 | ``` 78 | 79 | #### Current Screen 80 | 81 | The `current` computed property returns the current max screen size key. 82 | 83 | ```js 84 | console.log(current.value); // 'md' 85 | ``` 86 | 87 | The `mapCurrent()` function returns a computed value mapped to the `current` key. 88 | 89 | ```js 90 | const current = mapCurrent({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }); 91 | console.log(current.value); // 2 92 | ``` 93 | 94 | Pass an optional default value that gets returned when no screen sizes are matched. 95 | 96 | ```js 97 | const current = mapCurrent({ lg: 3 }, 0); 98 | console.log(current.value); // 0 99 | ``` 100 | 101 | #### List Matching Screens 102 | 103 | The `list` computed property returns a list of media-matched screen size keys. 104 | 105 | ```js 106 | console.log(list.value); // ['xs', 'sm', 'md'] 107 | ``` 108 | 109 | The `mapList()` function returns a computed property list of custom values mapped to the current matched size keys. 110 | 111 | ```js 112 | const value = mapList({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }); 113 | console.log(value.value); // [0, 1, 2] 114 | ``` 115 | 116 | #### Cleanup 117 | 118 | Event cleanup happens automatically when the parent component is unmounted, but can be manually called if needed. 119 | 120 | ```js 121 | // 122 | const { cleanup } = useScreens({...}); 123 | cleanup(); 124 | ``` 125 | 126 | ## Screens Plugin 127 | 128 | The `screens` plugin is exactly like the `useScreens` method above, but allows for a screen configuration to be used application-wide. Also, a global property will be created for easy access to `$screens` within templates. 129 | 130 | ### Step 1. Import the plugin. 131 | 132 | ```js 133 | // main.js 134 | import { screens } from 'vue-screen-utils'; 135 | 136 | // Use plugin with optional config 137 | app.use(screens, { 138 | mobile: '0px', 139 | tablet: '640px', 140 | laptop: '1024px', 141 | desktop: '1280px', 142 | }); 143 | ``` 144 | 145 | ### Step 2. Repeat step 2 from the [_Use Screens_](#use-screens) method above. 146 | 147 | ### Step 3. Quick reference from component templates 148 | 149 | ```html 150 | 153 | ``` 154 | 155 | ## Use Media Query 156 | 157 | Import and use the `useMediaQuery` function to evaluate simple media query strings. The function returns a `matches` computed property with the media query match status and an optional `cleanup()` function. 158 | 159 | If you wish to receive a callback of the raw media query event, provide the callback function as the second argument. 160 | 161 | Event cleanup happens automatically when the component is unmounted, but can be manually called via the `cleanup()` function. 162 | 163 | ```html 164 | 170 | 178 | ``` 179 | 180 | ## Use Resize Observer 181 | 182 | Import and use the `useResizeObserver` function to evaluate changes made to an ref element's content rect. The function returns a reactive content `rect` object. It also returns an optional `cleanup()` function. 183 | 184 | If you wish to receive a callback of the raw resize observer event, provide the callback function as the second argument. 185 | 186 | The backing event is cleaned up when the component is unmounted, but `cleanup()` can be called manually. 187 | 188 | ```html 189 | 198 | 205 | ``` 206 | 207 | ## Use Dark Mode 208 | 209 | Import and use the `useDarkMode` function to evaluate dark mode using a variety of strategies, based on the argument provided. 210 | 211 | ```ts 212 | declare function useDarkMode(config: Ref>): { 213 | isDark: Ref; 214 | displayMode: ComputedRef<'dark' | 'light'>; 215 | cleanup: () => void; 216 | }; 217 | ``` 218 | 219 | ### Manual Strategy 220 | 221 | Pass a boolean value for `isDark` to set the dark mode manually. 222 | 223 | ```html 224 | 227 | 228 | 234 | ``` 235 | 236 | ### System Preference Strategy 237 | 238 | Pass the `system` string to use the `Window.matchMedia()` API to read the user's system preference. This setting is continually watched to detect future changes made by the user. 239 | 240 | For example, to view the effect on the Mac, you can navigate to **System Preferences › General** and switch the **Appearance** setting between `Light`, `Dark` and `Auto`. 241 | 242 | ```html 243 | 246 | 247 | 253 | ``` 254 | 255 | ### Class Strategy 256 | 257 | To use the class strategy, pass an object with the element `selector` and `darkClass` to check against. 258 | 259 | ```ts 260 | interface DarkModeClassConfig { 261 | selector: string; 262 | darkClass: string; 263 | } 264 | ``` 265 | 266 | Any class updates made on the element are watched with a `MutationObserver` to detect future changes made by the user. 267 | 268 | ```html 269 | 272 | 273 | 279 | ``` 280 | 281 | Because `:root` and `dark` are the default `selector` and `darkClass`, respectively, a simple object could be passed to achieve the same effect. 282 | 283 | ```html 284 | 290 | ``` 291 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/parser": { 8 | "version": "7.18.4", 9 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", 10 | "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==" 11 | }, 12 | "@types/node": { 13 | "version": "16.11.36", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.36.tgz", 15 | "integrity": "sha512-FR5QJe+TaoZ2GsMHkjuwoNabr+UrJNRr2HNOo+r/7vhcuntM6Ee/pRPOnRhhL2XE9OOvX9VLEq+BcXl3VjNoWA==", 16 | "dev": true 17 | }, 18 | "@vitejs/plugin-vue": { 19 | "version": "2.3.3", 20 | "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz", 21 | "integrity": "sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==", 22 | "dev": true 23 | }, 24 | "@volar/code-gen": { 25 | "version": "0.34.17", 26 | "resolved": "https://registry.npmjs.org/@volar/code-gen/-/code-gen-0.34.17.tgz", 27 | "integrity": "sha512-rHR7BA71BJ/4S7xUOPMPiB7uk6iU9oTWpEMZxFi5VGC9iJmDncE82WzU5iYpcbOBCVHsOjMh0+5CGMgdO6SaPA==", 28 | "dev": true, 29 | "requires": { 30 | "@volar/source-map": "0.34.17" 31 | } 32 | }, 33 | "@volar/source-map": { 34 | "version": "0.34.17", 35 | "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-0.34.17.tgz", 36 | "integrity": "sha512-3yn1IMXJGGWB/G817/VFlFMi8oh5pmE7VzUqvgMZMrppaZpKj6/juvJIEiXNxRsgWc0RxIO8OSp4htdPUg1Raw==", 37 | "dev": true 38 | }, 39 | "@volar/vue-code-gen": { 40 | "version": "0.34.17", 41 | "resolved": "https://registry.npmjs.org/@volar/vue-code-gen/-/vue-code-gen-0.34.17.tgz", 42 | "integrity": "sha512-17pzcK29fyFWUc+C82J3JYSnA+jy3QNrIldb9kPaP9Itbik05ZjEIyEue9FjhgIAuHeYSn4LDM5s6nGjxyfhsQ==", 43 | "dev": true, 44 | "requires": { 45 | "@volar/code-gen": "0.34.17", 46 | "@volar/source-map": "0.34.17", 47 | "@vue/compiler-core": "^3.2.36", 48 | "@vue/compiler-dom": "^3.2.36", 49 | "@vue/shared": "^3.2.36" 50 | } 51 | }, 52 | "@volar/vue-typescript": { 53 | "version": "0.34.17", 54 | "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-0.34.17.tgz", 55 | "integrity": "sha512-U0YSVIBPRWVPmgJHNa4nrfq88+oS+tmyZNxmnfajIw9A/GOGZQiKXHC0k09SVvbYXlsjgJ6NIjhm9NuAhGRQjg==", 56 | "dev": true, 57 | "requires": { 58 | "@volar/code-gen": "0.34.17", 59 | "@volar/source-map": "0.34.17", 60 | "@volar/vue-code-gen": "0.34.17", 61 | "@vue/compiler-sfc": "^3.2.36", 62 | "@vue/reactivity": "^3.2.36" 63 | } 64 | }, 65 | "@vue/compiler-core": { 66 | "version": "3.2.36", 67 | "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.36.tgz", 68 | "integrity": "sha512-bbyZM5hvBicv0PW3KUfVi+x3ylHnfKG7DOn5wM+f2OztTzTjLEyBb/5yrarIYpmnGitVGbjZqDbODyW4iK8hqw==", 69 | "requires": { 70 | "@babel/parser": "^7.16.4", 71 | "@vue/shared": "3.2.36", 72 | "estree-walker": "^2.0.2", 73 | "source-map": "^0.6.1" 74 | } 75 | }, 76 | "@vue/compiler-dom": { 77 | "version": "3.2.36", 78 | "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.36.tgz", 79 | "integrity": "sha512-tcOTAOiW4s24QLnq+ON6J+GRONXJ+A/mqKCORi0LSlIh8XQlNnlm24y8xIL8la+ZDgkdbjarQ9ZqYSvEja6gVA==", 80 | "requires": { 81 | "@vue/compiler-core": "3.2.36", 82 | "@vue/shared": "3.2.36" 83 | } 84 | }, 85 | "@vue/compiler-sfc": { 86 | "version": "3.2.36", 87 | "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.36.tgz", 88 | "integrity": "sha512-AvGb4bTj4W8uQ4BqaSxo7UwTEqX5utdRSMyHy58OragWlt8nEACQ9mIeQh3K4di4/SX+41+pJrLIY01lHAOFOA==", 89 | "requires": { 90 | "@babel/parser": "^7.16.4", 91 | "@vue/compiler-core": "3.2.36", 92 | "@vue/compiler-dom": "3.2.36", 93 | "@vue/compiler-ssr": "3.2.36", 94 | "@vue/reactivity-transform": "3.2.36", 95 | "@vue/shared": "3.2.36", 96 | "estree-walker": "^2.0.2", 97 | "magic-string": "^0.25.7", 98 | "postcss": "^8.1.10", 99 | "source-map": "^0.6.1" 100 | } 101 | }, 102 | "@vue/compiler-ssr": { 103 | "version": "3.2.36", 104 | "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.36.tgz", 105 | "integrity": "sha512-+KugInUFRvOxEdLkZwE+W43BqHyhBh0jpYXhmqw1xGq2dmE6J9eZ8UUSOKNhdHtQ/iNLWWeK/wPZkVLUf3YGaw==", 106 | "requires": { 107 | "@vue/compiler-dom": "3.2.36", 108 | "@vue/shared": "3.2.36" 109 | } 110 | }, 111 | "@vue/reactivity": { 112 | "version": "3.2.36", 113 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.36.tgz", 114 | "integrity": "sha512-c2qvopo0crh9A4GXi2/2kfGYMxsJW4tVILrqRPydVGZHhq0fnzy6qmclWOhBFckEhmyxmpHpdJtIRYGeKcuhnA==", 115 | "requires": { 116 | "@vue/shared": "3.2.36" 117 | } 118 | }, 119 | "@vue/reactivity-transform": { 120 | "version": "3.2.36", 121 | "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.36.tgz", 122 | "integrity": "sha512-Jk5o2BhpODC9XTA7o4EL8hSJ4JyrFWErLtClG3NH8wDS7ri9jBDWxI7/549T7JY9uilKsaNM+4pJASLj5dtRwA==", 123 | "requires": { 124 | "@babel/parser": "^7.16.4", 125 | "@vue/compiler-core": "3.2.36", 126 | "@vue/shared": "3.2.36", 127 | "estree-walker": "^2.0.2", 128 | "magic-string": "^0.25.7" 129 | } 130 | }, 131 | "@vue/runtime-core": { 132 | "version": "3.2.36", 133 | "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.36.tgz", 134 | "integrity": "sha512-PTWBD+Lub+1U3/KhbCExrfxyS14hstLX+cBboxVHaz+kXoiDLNDEYAovPtxeTutbqtClIXtft+wcGdC+FUQ9qQ==", 135 | "requires": { 136 | "@vue/reactivity": "3.2.36", 137 | "@vue/shared": "3.2.36" 138 | } 139 | }, 140 | "@vue/runtime-dom": { 141 | "version": "3.2.36", 142 | "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.36.tgz", 143 | "integrity": "sha512-gYPYblm7QXHVuBohqNRRT7Wez0f2Mx2D40rb4fleehrJU9CnkjG0phhcGEZFfGwCmHZRqBCRgbFWE98bPULqkg==", 144 | "requires": { 145 | "@vue/runtime-core": "3.2.36", 146 | "@vue/shared": "3.2.36", 147 | "csstype": "^2.6.8" 148 | } 149 | }, 150 | "@vue/server-renderer": { 151 | "version": "3.2.36", 152 | "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.36.tgz", 153 | "integrity": "sha512-uZE0+jfye6yYXWvAQYeHZv+f50sRryvy16uiqzk3jn8hEY8zTjI+rzlmZSGoE915k+W/Ol9XSw6vxOUD8dGkUg==", 154 | "requires": { 155 | "@vue/compiler-ssr": "3.2.36", 156 | "@vue/shared": "3.2.36" 157 | } 158 | }, 159 | "@vue/shared": { 160 | "version": "3.2.36", 161 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.36.tgz", 162 | "integrity": "sha512-JtB41wXl7Au3+Nl3gD16Cfpj7k/6aCroZ6BbOiCMFCMvrOpkg/qQUXTso2XowaNqBbnkuGHurLAqkLBxNGc1hQ==" 163 | }, 164 | "@vue/tsconfig": { 165 | "version": "0.1.3", 166 | "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.1.3.tgz", 167 | "integrity": "sha512-kQVsh8yyWPvHpb8gIc9l/HIDiiVUy1amynLNpCy8p+FoCiZXCo6fQos5/097MmnNZc9AtseDsCrfkhqCrJ8Olg==", 168 | "dev": true 169 | }, 170 | "csstype": { 171 | "version": "2.6.20", 172 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz", 173 | "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==" 174 | }, 175 | "esbuild": { 176 | "version": "0.14.42", 177 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz", 178 | "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==", 179 | "dev": true, 180 | "requires": { 181 | "esbuild-android-64": "0.14.42", 182 | "esbuild-android-arm64": "0.14.42", 183 | "esbuild-darwin-64": "0.14.42", 184 | "esbuild-darwin-arm64": "0.14.42", 185 | "esbuild-freebsd-64": "0.14.42", 186 | "esbuild-freebsd-arm64": "0.14.42", 187 | "esbuild-linux-32": "0.14.42", 188 | "esbuild-linux-64": "0.14.42", 189 | "esbuild-linux-arm": "0.14.42", 190 | "esbuild-linux-arm64": "0.14.42", 191 | "esbuild-linux-mips64le": "0.14.42", 192 | "esbuild-linux-ppc64le": "0.14.42", 193 | "esbuild-linux-riscv64": "0.14.42", 194 | "esbuild-linux-s390x": "0.14.42", 195 | "esbuild-netbsd-64": "0.14.42", 196 | "esbuild-openbsd-64": "0.14.42", 197 | "esbuild-sunos-64": "0.14.42", 198 | "esbuild-windows-32": "0.14.42", 199 | "esbuild-windows-64": "0.14.42", 200 | "esbuild-windows-arm64": "0.14.42" 201 | } 202 | }, 203 | "esbuild-android-64": { 204 | "version": "0.14.42", 205 | "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz", 206 | "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==", 207 | "dev": true, 208 | "optional": true 209 | }, 210 | "esbuild-android-arm64": { 211 | "version": "0.14.42", 212 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz", 213 | "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==", 214 | "dev": true, 215 | "optional": true 216 | }, 217 | "esbuild-darwin-64": { 218 | "version": "0.14.42", 219 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz", 220 | "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==", 221 | "dev": true, 222 | "optional": true 223 | }, 224 | "esbuild-darwin-arm64": { 225 | "version": "0.14.42", 226 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz", 227 | "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==", 228 | "dev": true, 229 | "optional": true 230 | }, 231 | "esbuild-freebsd-64": { 232 | "version": "0.14.42", 233 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz", 234 | "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==", 235 | "dev": true, 236 | "optional": true 237 | }, 238 | "esbuild-freebsd-arm64": { 239 | "version": "0.14.42", 240 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz", 241 | "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==", 242 | "dev": true, 243 | "optional": true 244 | }, 245 | "esbuild-linux-32": { 246 | "version": "0.14.42", 247 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz", 248 | "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==", 249 | "dev": true, 250 | "optional": true 251 | }, 252 | "esbuild-linux-64": { 253 | "version": "0.14.42", 254 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz", 255 | "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==", 256 | "dev": true, 257 | "optional": true 258 | }, 259 | "esbuild-linux-arm": { 260 | "version": "0.14.42", 261 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz", 262 | "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==", 263 | "dev": true, 264 | "optional": true 265 | }, 266 | "esbuild-linux-arm64": { 267 | "version": "0.14.42", 268 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz", 269 | "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==", 270 | "dev": true, 271 | "optional": true 272 | }, 273 | "esbuild-linux-mips64le": { 274 | "version": "0.14.42", 275 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz", 276 | "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==", 277 | "dev": true, 278 | "optional": true 279 | }, 280 | "esbuild-linux-ppc64le": { 281 | "version": "0.14.42", 282 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz", 283 | "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==", 284 | "dev": true, 285 | "optional": true 286 | }, 287 | "esbuild-linux-riscv64": { 288 | "version": "0.14.42", 289 | "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz", 290 | "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==", 291 | "dev": true, 292 | "optional": true 293 | }, 294 | "esbuild-linux-s390x": { 295 | "version": "0.14.42", 296 | "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz", 297 | "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==", 298 | "dev": true, 299 | "optional": true 300 | }, 301 | "esbuild-netbsd-64": { 302 | "version": "0.14.42", 303 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz", 304 | "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==", 305 | "dev": true, 306 | "optional": true 307 | }, 308 | "esbuild-openbsd-64": { 309 | "version": "0.14.42", 310 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz", 311 | "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==", 312 | "dev": true, 313 | "optional": true 314 | }, 315 | "esbuild-sunos-64": { 316 | "version": "0.14.42", 317 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz", 318 | "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==", 319 | "dev": true, 320 | "optional": true 321 | }, 322 | "esbuild-windows-32": { 323 | "version": "0.14.42", 324 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz", 325 | "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==", 326 | "dev": true, 327 | "optional": true 328 | }, 329 | "esbuild-windows-64": { 330 | "version": "0.14.42", 331 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz", 332 | "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==", 333 | "dev": true, 334 | "optional": true 335 | }, 336 | "esbuild-windows-arm64": { 337 | "version": "0.14.42", 338 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz", 339 | "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==", 340 | "dev": true, 341 | "optional": true 342 | }, 343 | "estree-walker": { 344 | "version": "2.0.2", 345 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 346 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" 347 | }, 348 | "fsevents": { 349 | "version": "2.3.2", 350 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 351 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 352 | "dev": true, 353 | "optional": true 354 | }, 355 | "function-bind": { 356 | "version": "1.1.1", 357 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 358 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 359 | "dev": true 360 | }, 361 | "has": { 362 | "version": "1.0.3", 363 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 364 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 365 | "dev": true, 366 | "requires": { 367 | "function-bind": "^1.1.1" 368 | } 369 | }, 370 | "is-core-module": { 371 | "version": "2.9.0", 372 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", 373 | "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", 374 | "dev": true, 375 | "requires": { 376 | "has": "^1.0.3" 377 | } 378 | }, 379 | "magic-string": { 380 | "version": "0.25.9", 381 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", 382 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", 383 | "requires": { 384 | "sourcemap-codec": "^1.4.8" 385 | } 386 | }, 387 | "nanoid": { 388 | "version": "3.3.4", 389 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 390 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" 391 | }, 392 | "path-parse": { 393 | "version": "1.0.7", 394 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 395 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 396 | "dev": true 397 | }, 398 | "picocolors": { 399 | "version": "1.0.0", 400 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 401 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 402 | }, 403 | "postcss": { 404 | "version": "8.4.14", 405 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 406 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 407 | "requires": { 408 | "nanoid": "^3.3.4", 409 | "picocolors": "^1.0.0", 410 | "source-map-js": "^1.0.2" 411 | } 412 | }, 413 | "resolve": { 414 | "version": "1.22.0", 415 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", 416 | "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", 417 | "dev": true, 418 | "requires": { 419 | "is-core-module": "^2.8.1", 420 | "path-parse": "^1.0.7", 421 | "supports-preserve-symlinks-flag": "^1.0.0" 422 | } 423 | }, 424 | "rollup": { 425 | "version": "2.75.3", 426 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.3.tgz", 427 | "integrity": "sha512-YA29fLU6MAYSaDxIQYrGGOcbXlDmG96h0krGGYObroezcQ0KgEPM3+7MtKD/qeuUbFuAJXvKZee5dA1dpwq1PQ==", 428 | "dev": true, 429 | "requires": { 430 | "fsevents": "~2.3.2" 431 | } 432 | }, 433 | "source-map": { 434 | "version": "0.6.1", 435 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 436 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 437 | }, 438 | "source-map-js": { 439 | "version": "1.0.2", 440 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 441 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 442 | }, 443 | "sourcemap-codec": { 444 | "version": "1.4.8", 445 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 446 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" 447 | }, 448 | "supports-preserve-symlinks-flag": { 449 | "version": "1.0.0", 450 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 451 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 452 | "dev": true 453 | }, 454 | "typescript": { 455 | "version": "4.6.4", 456 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", 457 | "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", 458 | "dev": true 459 | }, 460 | "vite": { 461 | "version": "2.9.9", 462 | "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", 463 | "integrity": "sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==", 464 | "dev": true, 465 | "requires": { 466 | "esbuild": "^0.14.27", 467 | "fsevents": "~2.3.2", 468 | "postcss": "^8.4.13", 469 | "resolve": "^1.22.0", 470 | "rollup": "^2.59.0" 471 | } 472 | }, 473 | "vue": { 474 | "version": "3.2.36", 475 | "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.36.tgz", 476 | "integrity": "sha512-5yTXmrE6gW8IQgttzHW5bfBiFA6mx35ZXHjGLDmKYzW6MMmYvCwuKybANRepwkMYeXw2v1buGg3/lPICY5YlZw==", 477 | "requires": { 478 | "@vue/compiler-dom": "3.2.36", 479 | "@vue/compiler-sfc": "3.2.36", 480 | "@vue/runtime-dom": "3.2.36", 481 | "@vue/server-renderer": "3.2.36", 482 | "@vue/shared": "3.2.36" 483 | } 484 | }, 485 | "vue-tsc": { 486 | "version": "0.34.17", 487 | "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-0.34.17.tgz", 488 | "integrity": "sha512-jzUXky44ZLHC4daaJag7FQr3idlPYN719/K1eObGljz5KaS2UnVGTU/XSYCd7d6ampYYg4OsyalbHyJIxV0aEQ==", 489 | "dev": true, 490 | "requires": { 491 | "@volar/vue-typescript": "0.34.17" 492 | } 493 | } 494 | } 495 | } 496 | --------------------------------------------------------------------------------