├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── .vscode ├── _sfc.code-snippets ├── extensions.json └── settings.json ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── e2e ├── tsconfig.json └── vue.spec.ts ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── playwright.config.ts ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ └── logo.svg ├── components │ ├── BaseButton.spec.ts │ ├── BaseButton.vue │ ├── BaseInputText.spec.ts │ └── BaseInputText.vue ├── composables │ └── useTheme.ts ├── design │ ├── _colors.scss │ ├── _durations.scss │ ├── _fonts.scss │ ├── _layers.scss │ ├── _sizes.scss │ ├── _typography.scss │ └── index.scss ├── layouts │ └── AppLayout.vue ├── main.ts ├── pages │ ├── about.vue │ └── index.vue ├── router │ ├── index.ts │ └── routes.ts ├── stores │ └── counter.ts └── types.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vitest.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.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 | # Editor directories and files 18 | .idea 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # Playwright directories 26 | test-results/ 27 | playwright-report/ 28 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /.vscode/_sfc.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "veb-sfc": { 3 | "scope": "vue", 4 | "prefix": "sfc", 5 | "body": [ 6 | "", 9 | "", 10 | "", 13 | "", 14 | "" 17 | ], 18 | "description": "Single File Component (Comp API + TS) from VEB" 19 | }, 20 | "veb-sfc-options": { 21 | "scope": "vue", 22 | "prefix": "sfc-options", 23 | "body": [ 24 | "", 31 | "", 32 | "", 35 | "", 36 | "" 39 | ], 40 | "description": "Single File Component (Options API + TS) from VEB" 41 | }, 42 | "veb-script": { 43 | "scope": "vue", 44 | "prefix": "script", 45 | "body": [""], 46 | "description": "Script block (Comp API + TS)" 47 | }, 48 | "veb-script-options": { 49 | "scope": "vue", 50 | "prefix": "script-options", 51 | "body": [ 52 | "" 59 | ], 60 | "description": "Script block (Options API + TS)" 61 | }, 62 | "veb-template": { 63 | "scope": "vue", 64 | "prefix": "template", 65 | "body": [""], 66 | "description": "Template block" 67 | }, 68 | "veb-style": { 69 | "scope": "vue", 70 | "prefix": "style", 71 | "body": [""], 72 | "description": "Scoped CSS + Sass styles block from VEB" 73 | }, 74 | "veb-style-module": { 75 | "scope": "vue", 76 | "prefix": "style-module", 77 | "body": [""], 78 | "description": "CSS Module + Sass styles block from VEB" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // Vue - Official Extension 4 | // https://github.com/vuejs/language-tools 5 | "vue.volar", 6 | 7 | // Format-on-save with Prettier 8 | // https://github.com/prettier/prettier-vscode 9 | "esbenp.prettier-vscode", 10 | 11 | // Playwright Test - Official Extension 12 | // https://github.com/microsoft/playwright-vscode 13 | "ms-playwright.playwright", 14 | 15 | // Better Comments 16 | // https://github.com/aaron-bond/better-comments 17 | "aaron-bond.better-comments", 18 | 19 | // Path Intellisense 20 | // https://github.com/ChristianKohler/PathIntellisense 21 | "christian-kohler.path-intellisense", 22 | 23 | // Peacock - Workspace Color Customizer 24 | // https://github.com/johnpapa/vscode-peacock 25 | "johnpapa.vscode-peacock" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // ====== 3 | // Spacing 4 | // ====== 5 | 6 | "editor.insertSpaces": true, 7 | "editor.tabSize": 2, 8 | "editor.trimAutoWhitespace": true, 9 | "files.trimTrailingWhitespace": true, 10 | "files.eol": "\n", 11 | "files.insertFinalNewline": true, 12 | "files.trimFinalNewlines": true, 13 | 14 | // ====== 15 | // Files 16 | // ====== 17 | 18 | "files.exclude": { 19 | "**/*.log": true, 20 | "**/*.log*": true, 21 | "**/dist": true, 22 | "**/coverage": true 23 | }, 24 | 25 | // ====== 26 | // Event Triggers 27 | // ====== 28 | 29 | "editor.formatOnSave": true, 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | "editor.codeActionsOnSave": { 32 | "source.fixAll.eslint": "explicit", 33 | "source.fixAll.stylelint": "explicit", 34 | "source.fixAll.markdownlint": "explicit" 35 | }, 36 | "eslint.validate": ["javascript", "javascriptreact", "vue", "vue-html", "html"], 37 | 38 | // ====== 39 | // HTML 40 | // ====== 41 | 42 | "emmet.triggerExpansionOnTab": true, 43 | 44 | // ====== 45 | // CSS 46 | // ====== 47 | 48 | "stylelint.enable": true, 49 | "css.validate": false, 50 | "scss.validate": false, 51 | 52 | // ====== 53 | // MARKDOWN 54 | // ====== 55 | 56 | "[markdown]": { 57 | "editor.wordWrap": "wordWrapColumn", 58 | "editor.wordWrapColumn": 80 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Enterprise Boilerplate v3 (alpha) 2 | 3 | This repo is currently in active development and considered in alpha release. 4 | 5 | > This is an ever-evolving, opinionated architecture and dev environment for new Vue 3 + Vite SPA projects using [create-vue](https://github.com/vuejs/create-vue). 6 | 7 | 🎩 A huge thanks to [Chris Fritz](https://twitter.com/chrisvfritz) for the incredible work that this work builds upon. For those looking for his version, see [this branch for the original Vue 2 enterprise boilerplate](https://github.com/bencodezen/vue-enterprise-boilerplate/tree/vue-2-version). 8 | 9 | ## Recommended IDE Setup 10 | 11 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 12 | 13 | ## Type Support for `.vue` Imports in TS 14 | 15 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 16 | 17 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 18 | 19 | 1. Disable the built-in TypeScript Extension 20 | 1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette 21 | 2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 22 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 43 | 44 | ```sh 45 | npm run test:unit 46 | ``` 47 | 48 | ### Run End-to-End Tests with [Playwright](https://playwright.dev) 49 | 50 | ```sh 51 | # Install browsers for the first run 52 | npx playwright install 53 | 54 | # When testing on CI, must build the project first 55 | npm run build 56 | 57 | # Runs the end-to-end tests 58 | npm run test:e2e 59 | # Runs the tests only on Chromium 60 | npm run test:e2e -- --project=chromium 61 | # Runs the tests of a specific file 62 | npm run test:e2e -- tests/example.spec.ts 63 | # Runs the tests in debug mode 64 | npm run test:e2e -- --debug 65 | ``` 66 | 67 | ### Lint with [ESLint](https://eslint.org/) 68 | 69 | ```sh 70 | npm run lint 71 | ``` 72 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 10 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 11 | const computed: typeof import('vue')['computed'] 12 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 13 | const computedEager: typeof import('@vueuse/core')['computedEager'] 14 | const computedInject: typeof import('@vueuse/core')['computedInject'] 15 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 16 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 17 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 18 | const createApp: typeof import('vue')['createApp'] 19 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 20 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 21 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 22 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 23 | const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] 24 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 25 | const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] 26 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 27 | const customRef: typeof import('vue')['customRef'] 28 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 29 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 30 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 31 | const defineComponent: typeof import('vue')['defineComponent'] 32 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 33 | const effectScope: typeof import('vue')['effectScope'] 34 | const extendRef: typeof import('@vueuse/core')['extendRef'] 35 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 36 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 37 | const h: typeof import('vue')['h'] 38 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 39 | const inject: typeof import('vue')['inject'] 40 | const injectLocal: typeof import('@vueuse/core')['injectLocal'] 41 | const isDefined: typeof import('@vueuse/core')['isDefined'] 42 | const isProxy: typeof import('vue')['isProxy'] 43 | const isReactive: typeof import('vue')['isReactive'] 44 | const isReadonly: typeof import('vue')['isReadonly'] 45 | const isRef: typeof import('vue')['isRef'] 46 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 47 | const markRaw: typeof import('vue')['markRaw'] 48 | const nextTick: typeof import('vue')['nextTick'] 49 | const onActivated: typeof import('vue')['onActivated'] 50 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 51 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 52 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 53 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 54 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 55 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 56 | const onDeactivated: typeof import('vue')['onDeactivated'] 57 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 58 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 59 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 60 | const onMounted: typeof import('vue')['onMounted'] 61 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 62 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 63 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 64 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 65 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 66 | const onUnmounted: typeof import('vue')['onUnmounted'] 67 | const onUpdated: typeof import('vue')['onUpdated'] 68 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 69 | const provide: typeof import('vue')['provide'] 70 | const provideLocal: typeof import('@vueuse/core')['provideLocal'] 71 | const reactify: typeof import('@vueuse/core')['reactify'] 72 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 73 | const reactive: typeof import('vue')['reactive'] 74 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 75 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 76 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 77 | const readonly: typeof import('vue')['readonly'] 78 | const ref: typeof import('vue')['ref'] 79 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 80 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 81 | const refDefault: typeof import('@vueuse/core')['refDefault'] 82 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 83 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 84 | const resolveComponent: typeof import('vue')['resolveComponent'] 85 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 86 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 87 | const shallowReactive: typeof import('vue')['shallowReactive'] 88 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 89 | const shallowRef: typeof import('vue')['shallowRef'] 90 | const syncRef: typeof import('@vueuse/core')['syncRef'] 91 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 92 | const templateRef: typeof import('@vueuse/core')['templateRef'] 93 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 94 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 95 | const toRaw: typeof import('vue')['toRaw'] 96 | const toReactive: typeof import('@vueuse/core')['toReactive'] 97 | const toRef: typeof import('vue')['toRef'] 98 | const toRefs: typeof import('vue')['toRefs'] 99 | const toValue: typeof import('vue')['toValue'] 100 | const triggerRef: typeof import('vue')['triggerRef'] 101 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 102 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 103 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 104 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 105 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 106 | const unref: typeof import('vue')['unref'] 107 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 108 | const until: typeof import('@vueuse/core')['until'] 109 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 110 | const useAnimate: typeof import('@vueuse/core')['useAnimate'] 111 | const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] 112 | const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] 113 | const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] 114 | const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] 115 | const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] 116 | const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] 117 | const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] 118 | const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] 119 | const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] 120 | const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] 121 | const useArraySome: typeof import('@vueuse/core')['useArraySome'] 122 | const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] 123 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 124 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 125 | const useAttrs: typeof import('vue')['useAttrs'] 126 | const useBase64: typeof import('@vueuse/core')['useBase64'] 127 | const useBattery: typeof import('@vueuse/core')['useBattery'] 128 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 129 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 130 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 131 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 132 | const useCached: typeof import('@vueuse/core')['useCached'] 133 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 134 | const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] 135 | const useCloned: typeof import('@vueuse/core')['useCloned'] 136 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 137 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 138 | const useCounter: typeof import('@vueuse/core')['useCounter'] 139 | const useCssModule: typeof import('vue')['useCssModule'] 140 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 141 | const useCssVars: typeof import('vue')['useCssVars'] 142 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 143 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 144 | const useDark: typeof import('@vueuse/core')['useDark'] 145 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 146 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 147 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 148 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 149 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 150 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 151 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 152 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 153 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 154 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 155 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 156 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 157 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 158 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 159 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 160 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 161 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 162 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 163 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 164 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 165 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 166 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 167 | const useFetch: typeof import('@vueuse/core')['useFetch'] 168 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 169 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 170 | const useFocus: typeof import('@vueuse/core')['useFocus'] 171 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 172 | const useFps: typeof import('@vueuse/core')['useFps'] 173 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 174 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 175 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 176 | const useHead: typeof import('@unhead/vue')['useHead'] 177 | const useIdle: typeof import('@vueuse/core')['useIdle'] 178 | const useImage: typeof import('@vueuse/core')['useImage'] 179 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 180 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 181 | const useInterval: typeof import('@vueuse/core')['useInterval'] 182 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 183 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 184 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 185 | const useLink: typeof import('vue-router')['useLink'] 186 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 187 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 188 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 189 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 190 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 191 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 192 | const useMemory: typeof import('@vueuse/core')['useMemory'] 193 | const useMounted: typeof import('@vueuse/core')['useMounted'] 194 | const useMouse: typeof import('@vueuse/core')['useMouse'] 195 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 196 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 197 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 198 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 199 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 200 | const useNow: typeof import('@vueuse/core')['useNow'] 201 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 202 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 203 | const useOnline: typeof import('@vueuse/core')['useOnline'] 204 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 205 | const useParallax: typeof import('@vueuse/core')['useParallax'] 206 | const useParentElement: typeof import('@vueuse/core')['useParentElement'] 207 | const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] 208 | const usePermission: typeof import('@vueuse/core')['usePermission'] 209 | const usePointer: typeof import('@vueuse/core')['usePointer'] 210 | const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] 211 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 212 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 213 | const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] 214 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 215 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 216 | const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] 217 | const usePrevious: typeof import('@vueuse/core')['usePrevious'] 218 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 219 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 220 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 221 | const useRoute: typeof import('vue-router')['useRoute'] 222 | const useRouter: typeof import('vue-router')['useRouter'] 223 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 224 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 225 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 226 | const useScroll: typeof import('@vueuse/core')['useScroll'] 227 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 228 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 229 | const useShare: typeof import('@vueuse/core')['useShare'] 230 | const useSlots: typeof import('vue')['useSlots'] 231 | const useSorted: typeof import('@vueuse/core')['useSorted'] 232 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 233 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 234 | const useStepper: typeof import('@vueuse/core')['useStepper'] 235 | const useStorage: typeof import('@vueuse/core')['useStorage'] 236 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 237 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 238 | const useSupported: typeof import('@vueuse/core')['useSupported'] 239 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 240 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 241 | const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] 242 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 243 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 244 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 245 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 246 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 247 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 248 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 249 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 250 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 251 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 252 | const useTitle: typeof import('@vueuse/core')['useTitle'] 253 | const useToNumber: typeof import('@vueuse/core')['useToNumber'] 254 | const useToString: typeof import('@vueuse/core')['useToString'] 255 | const useToggle: typeof import('@vueuse/core')['useToggle'] 256 | const useTransition: typeof import('@vueuse/core')['useTransition'] 257 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 258 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 259 | const useVModel: typeof import('@vueuse/core')['useVModel'] 260 | const useVModels: typeof import('@vueuse/core')['useVModels'] 261 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 262 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 263 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 264 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 265 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 266 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 267 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 268 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 269 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 270 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 271 | const watch: typeof import('vue')['watch'] 272 | const watchArray: typeof import('@vueuse/core')['watchArray'] 273 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 274 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 275 | const watchDeep: typeof import('@vueuse/core')['watchDeep'] 276 | const watchEffect: typeof import('vue')['watchEffect'] 277 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 278 | const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] 279 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 280 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 281 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 282 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 283 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 284 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 285 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 286 | const whenever: typeof import('@vueuse/core')['whenever'] 287 | } 288 | // for type re-export 289 | declare global { 290 | // @ts-ignore 291 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 292 | import('vue') 293 | } 294 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AppLayout: typeof import('./src/layouts/AppLayout.vue')['default'] 11 | BaseButton: typeof import('./src/components/BaseButton.vue')['default'] 12 | BaseInputText: typeof import('./src/components/BaseInputText.vue')['default'] 13 | BaseLink: typeof import('./src/components/BaseLink.vue')['default'] 14 | RouterLink: typeof import('vue-router')['RouterLink'] 15 | RouterView: typeof import('vue-router')['RouterView'] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "include": ["./**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /e2e/vue.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | // See here how to get started: 4 | // https://playwright.dev/docs/intro 5 | test('visits the app root url', async ({ page }) => { 6 | await page.goto('/') 7 | await expect(page.locator('h1')).toHaveText('Home Page') 8 | }) 9 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue 3 App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-enterprise-boilerplate", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "test:unit": "vitest", 10 | "test:e2e": "playwright test", 11 | "test:e2e:ui": "playwright test --ui", 12 | "build-only": "vite build", 13 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 14 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 15 | "format": "prettier --write src/" 16 | }, 17 | "dependencies": { 18 | "@vueuse/core": "^10.9.0", 19 | "pinia": "^2.0.36", 20 | "vue": "^3.4.27", 21 | "vue-router": "^4.2.0" 22 | }, 23 | "devDependencies": { 24 | "@playwright/test": "^1.44.0", 25 | "@rushstack/eslint-patch": "^1.2.0", 26 | "@tsconfig/node18": "^2.0.1", 27 | "@types/jsdom": "^21.1.1", 28 | "@types/node": "^18.16.8", 29 | "@unhead/vue": "^1.9.10", 30 | "@vitejs/plugin-vue": "^4.2.3", 31 | "@vue/eslint-config-prettier": "^7.1.0", 32 | "@vue/eslint-config-typescript": "^11.0.3", 33 | "@vue/test-utils": "^2.4.6", 34 | "@vue/tsconfig": "^0.4.0", 35 | "eslint": "^8.39.0", 36 | "eslint-plugin-vue": "^9.11.0", 37 | "jsdom": "^22.0.0", 38 | "npm-run-all": "^4.1.5", 39 | "prettier": "^2.8.8", 40 | "sass": "^1.77.1", 41 | "typescript": "~5.0.4", 42 | "unplugin-auto-import": "^0.17.6", 43 | "unplugin-vue-components": "^0.27.0", 44 | "vite": "^4.3.5", 45 | "vitest": "^1.4.0", 46 | "vue-tsc": "^1.6.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig, devices } from '@playwright/test' 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: './e2e', 15 | /* Maximum time one test can run for. */ 16 | timeout: 30 * 1000, 17 | expect: { 18 | /** 19 | * Maximum time expect() should wait for the condition to be met. 20 | * For example in `await expect(locator).toHaveText();` 21 | */ 22 | timeout: 5000 23 | }, 24 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 25 | forbidOnly: !!process.env.CI, 26 | /* Retry on CI only */ 27 | retries: process.env.CI ? 2 : 0, 28 | /* Opt out of parallel tests on CI. */ 29 | workers: process.env.CI ? 1 : undefined, 30 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 31 | reporter: 'html', 32 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 33 | use: { 34 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 35 | actionTimeout: 0, 36 | /* Base URL to use in actions like `await page.goto('/')`. */ 37 | baseURL: 'http://localhost:8080', 38 | 39 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 40 | trace: 'on-first-retry', 41 | 42 | /* Only on CI systems run the tests headless */ 43 | headless: !!process.env.CI 44 | }, 45 | 46 | /* Configure projects for major browsers */ 47 | projects: [ 48 | { 49 | name: 'chromium', 50 | use: { 51 | ...devices['Desktop Chrome'] 52 | } 53 | }, 54 | { 55 | name: 'firefox', 56 | use: { 57 | ...devices['Desktop Firefox'] 58 | } 59 | }, 60 | { 61 | name: 'webkit', 62 | use: { 63 | ...devices['Desktop Safari'] 64 | } 65 | } 66 | 67 | /* Test against mobile viewports. */ 68 | // { 69 | // name: 'Mobile Chrome', 70 | // use: { 71 | // ...devices['Pixel 5'], 72 | // }, 73 | // }, 74 | // { 75 | // name: 'Mobile Safari', 76 | // use: { 77 | // ...devices['iPhone 12'], 78 | // }, 79 | // }, 80 | 81 | /* Test against branded browsers. */ 82 | // { 83 | // name: 'Microsoft Edge', 84 | // use: { 85 | // channel: 'msedge', 86 | // }, 87 | // }, 88 | // { 89 | // name: 'Google Chrome', 90 | // use: { 91 | // channel: 'chrome', 92 | // }, 93 | // }, 94 | ], 95 | 96 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 97 | // outputDir: 'test-results/', 98 | 99 | /* Run your local dev server before starting the tests */ 100 | webServer: { 101 | /** 102 | * Use the dev server by default for faster feedback loop. 103 | * Use the preview server on CI for more realistic testing. 104 | * Playwright will re-use the local server if there is already a dev-server running. 105 | */ 106 | command: process.env.CI ? 'vite preview --port 8080' : 'vite dev', 107 | port: 8080, 108 | reuseExistingServer: !process.env.CI 109 | } 110 | }) 111 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencodezen/vue-enterprise-boilerplate/d988ce0b296bf8f9e72c24bd468c44fdcb22716a/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/BaseButton.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { shallowMount } from '@vue/test-utils' 3 | import BaseButton from './BaseButton.vue' 4 | 5 | describe('BaseButton Componenet', () => { 6 | it('renders its content', () => { 7 | const slotContent = 'Click me!' 8 | const { element } = shallowMount(BaseButton, { 9 | slots: { 10 | default: slotContent 11 | } 12 | }) 13 | expect(element.innerHTML).toContain(slotContent) 14 | }) 15 | 16 | it('renders default content', () => { 17 | const slotContent = '' 18 | const { element } = shallowMount(BaseButton, { 19 | slots: { 20 | default: slotContent 21 | } 22 | }) 23 | expect(element.innerHTML).toContain('Submit') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/components/BaseButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /src/components/BaseInputText.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest' 2 | import { shallowMount, mount } from '@vue/test-utils' 3 | import BaseInputText from '@/components/BaseInputText.vue' 4 | 5 | describe('@components/BaseInputText', () => { 6 | it('works with v-model', () => { 7 | const wrapper = mount(BaseInputText, { 8 | props: { 9 | modelValue: 'aaa', 10 | 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }) 11 | } 12 | }) 13 | const inputWrapper = wrapper.find('input') 14 | const inputEl = inputWrapper.element 15 | 16 | // Has the correct starting value 17 | expect(inputEl.value).toEqual('aaa') 18 | 19 | // Sets the input to the correct value when props change 20 | inputWrapper.setValue('ccc') 21 | expect(inputEl.value).toEqual('ccc') 22 | }) 23 | 24 | it('allows a type of "password"', () => { 25 | const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) 26 | shallowMount(BaseInputText, { 27 | propsData: { value: 'aaa', type: 'password' } 28 | }) 29 | expect(consoleError).not.toBeCalled() 30 | consoleError.mockRestore() 31 | }) 32 | 33 | it('does NOT allow a type of "checkbox"', () => { 34 | const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) 35 | shallowMount(BaseInputText, { 36 | propsData: { value: 'aaa', type: 'checkbox' } 37 | }) 38 | 39 | expect(consoleWarn).toBeCalled() 40 | expect(consoleWarn.mock.calls[0][0]).toContain('custom validator check failed for prop "type"') 41 | consoleWarn.mockRestore() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/BaseInputText.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /src/composables/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useColorMode } from '@vueuse/core' 2 | 3 | type Theme = 'dark' | 'light' 4 | 5 | function useTheme() { 6 | const themePreference = useColorMode() 7 | 8 | function setTheme(theme: Theme) { 9 | themePreference.value = theme 10 | } 11 | 12 | return { setTheme, themePreference } 13 | } 14 | 15 | export default useTheme 16 | -------------------------------------------------------------------------------- /src/design/_colors.scss: -------------------------------------------------------------------------------- 1 | // CONTENT 2 | $color-body-bg: #f9f7f5; 3 | $color-text: #444; 4 | $color-heading-text: #35495e; 5 | 6 | // LINKS 7 | $color-link-text: #39a275; 8 | $color-link-text-active: $color-text; 9 | 10 | // INPUTS 11 | $color-input-border: lighten($color-heading-text, 50%); 12 | 13 | // BUTTONS 14 | $color-button-bg: $color-link-text; 15 | $color-button-disabled-bg: darken(desaturate($color-button-bg, 20%), 10%); 16 | $color-button-text: white; 17 | -------------------------------------------------------------------------------- /src/design/_durations.scss: -------------------------------------------------------------------------------- 1 | $duration-animation-base: 300ms; 2 | -------------------------------------------------------------------------------- /src/design/_fonts.scss: -------------------------------------------------------------------------------- 1 | $system-default-font-family: -apple-system, 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica', 'Arial', 2 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 3 | 4 | $heading-font-family: $system-default-font-family; 5 | $heading-font-weight: 600; 6 | 7 | $content-font-family: $system-default-font-family; 8 | $content-font-weight: 400; 9 | 10 | %font-heading { 11 | font-family: $heading-font-family; 12 | font-weight: $heading-font-weight; 13 | color: $color-heading-text; 14 | } 15 | 16 | %font-content { 17 | font-family: $content-font-family; 18 | font-weight: $content-font-weight; 19 | color: $color-text; 20 | } 21 | -------------------------------------------------------------------------------- /src/design/_layers.scss: -------------------------------------------------------------------------------- 1 | $layer-negative-z-index: -1; 2 | $layer-page-z-index: 1; 3 | $layer-dropdown-z-index: 2; 4 | $layer-modal-z-index: 3; 5 | $layer-popover-z-index: 4; 6 | $layer-tooltip-z-index: 5; 7 | -------------------------------------------------------------------------------- /src/design/_sizes.scss: -------------------------------------------------------------------------------- 1 | // GRID 2 | $size-grid-padding: 1.3rem; 3 | 4 | // CONTENT 5 | $size-content-width-max: 50rem; 6 | $size-content-width-min: 25rem; 7 | 8 | // INPUTS 9 | $size-input-padding-vertical: 0.75em; 10 | $size-input-padding-horizontal: 1em; 11 | $size-input-padding: $size-input-padding-vertical $size-input-padding-horizontal; 12 | $size-input-border: 1px; 13 | $size-input-border-radius: calc((1em + $size-input-padding-vertical * 2) / 10); 14 | 15 | // BUTTONS 16 | $size-button-padding-vertical: calc($size-grid-padding / 2); 17 | $size-button-padding-horizontal: calc($size-grid-padding / 1.5); 18 | $size-button-padding: $size-button-padding-vertical $size-button-padding-horizontal; 19 | -------------------------------------------------------------------------------- /src/design/_typography.scss: -------------------------------------------------------------------------------- 1 | // Interpolate v1.0 2 | 3 | // This mixin generates CSS for interpolation of length properties. 4 | // It has 5 required values, including the target property, initial 5 | // screen size, initial value, final screen size and final value. 6 | 7 | // It has two optional values which include an easing property, 8 | // which is a string, representing a CSS animation-timing-function 9 | // and finally a number of bending-points, that determines how many 10 | // interpolations steps are applied along the easing function. 11 | 12 | // Author: Mike Riethmuller - @MikeRiethmuller 13 | // More information: http://codepen.io/MadeByMike/pen/a2249946658b139b7625b2a58cf03a65?editors=0100 14 | 15 | /// 16 | /// @param {String} $property - The CSS property to interpolate 17 | /// @param {Unit} $min-screen - A CSS length unit 18 | /// @param {Unit} $min-value - A CSS length unit 19 | /// @param {Unit} $max-screen - Value to be parsed 20 | /// @param {Unit} $max-value - Value to be parsed 21 | /// @param {String} $easing - Value to be parsed 22 | /// @param {Integer} $bending-points - Value to be parsed 23 | /// 24 | 25 | // Examples on line 258 26 | 27 | // Issues: 28 | 29 | // - kubic-bezier requires whitespace 30 | // - kubic-bezier cannot parse negative values 31 | 32 | // stylelint-disable scss/dollar-variable-pattern 33 | @mixin typography-interpolate( 34 | $property, 35 | $min-screen, 36 | $min-value, 37 | $max-screen, 38 | $max-value, 39 | $easing: 'linear', 40 | $bending-points: 2 41 | ) { 42 | // Default Easing 'Linear' 43 | $p0: 0; 44 | $p1: 0; 45 | $p2: 1; 46 | $p3: 1; 47 | 48 | // Parse Cubic Bezier string 49 | @if (str-slice($easing, 1, 12) == 'kubic-bezier') { 50 | // Get the values between the brackets 51 | // TODO: Deal with different whitespace 52 | $i: str-index($easing, ')'); // Get index of closing bracket 53 | $values: str-slice($easing, 14, $i - 1); // Extract values between brackts 54 | $list: typography-explode($values, ', '); // Split the values into a list 55 | 56 | @debug ($list); 57 | 58 | // Cast values to numebrs 59 | $p0: typography-number(nth($list, 1)); 60 | $p1: typography-number(nth($list, 2)); 61 | $p2: typography-number(nth($list, 3)); 62 | $p3: typography-number(nth($list, 4)); 63 | } 64 | 65 | @if ($easing == 'ease') { 66 | $p0: 0.25; 67 | $p1: 1; 68 | $p2: 0.25; 69 | $p3: 1; 70 | } 71 | 72 | @if ($easing == 'ease-in-out') { 73 | $p0: 0.42; 74 | $p1: 0; 75 | $p2: 0.58; 76 | $p3: 1; 77 | } 78 | 79 | @if ($easing == 'ease-in') { 80 | $p0: 0.42; 81 | $p1: 0; 82 | $p2: 1; 83 | $p3: 1; 84 | } 85 | 86 | @if ($easing == 'ease-out') { 87 | $p0: 0; 88 | $p1: 0; 89 | $p2: 0.58; 90 | $p3: 1; 91 | } 92 | 93 | #{$property}: $min-value; 94 | 95 | @if ($easing == 'linear' or $bending-points < 1) { 96 | @media screen and (min-width: $min-screen) { 97 | #{$property}: typography-calc-interpolation($min-screen, $min-value, $max-screen, $max-value); 98 | } 99 | } @else { 100 | // Loop through bending points 101 | $t: 1 / ($bending-points + 1); 102 | $i: 1; 103 | $prev-screen: $min-screen; 104 | $prev-value: $min-value; 105 | 106 | @while $t * $i <= 1 { 107 | $bending-point: $t * $i; 108 | $value: typography-cubic-bezier($p0, $p1, $p2, $p3, $bending-point); 109 | $screen-int: typography-lerp($min-screen, $max-screen, $bending-point); 110 | $value-int: typography-lerp($min-value, $max-value, $value); 111 | 112 | @media screen and (min-width: $prev-screen) { 113 | #{$property}: typography-calc-interpolation( 114 | $prev-screen, 115 | $prev-value, 116 | $screen-int, 117 | $value-int 118 | ); 119 | } 120 | 121 | $prev-screen: $screen-int; 122 | $prev-value: $value-int; 123 | $i: $i + 1; 124 | } 125 | } 126 | 127 | @media screen and (min-width: $max-screen) { 128 | #{$property}: $max-value; 129 | } 130 | } 131 | 132 | // Requires several helper functions including: pow, calc-interpolation, kubic-bezier, number and explode 133 | 134 | // Math functions: 135 | 136 | // Linear interpolations in CSS as a Sass function 137 | // Author: Mike Riethmuller | https://madebymike.com.au/writing/precise-control-responsive-typography/ I 138 | 139 | @function typography-calc-interpolation($min-screen, $min-value, $max-screen, $max-value) { 140 | $a: calc(($max-value - $min-value) / ($max-screen - $min-screen)); 141 | $b: $min-value - $a * $min-screen; 142 | 143 | $sign: '+'; 144 | 145 | @if ($b < 0) { 146 | $sign: '-'; 147 | $b: abs($b); 148 | } 149 | 150 | @return calc(#{$a * 100}vw #{$sign} #{$b}); 151 | } 152 | 153 | // This is a crude Sass port webkits cubic-bezier function. Looking to simplify this if you can help. 154 | @function typography-solve-bexier-x($p1x, $p1y, $p2x, $p2y, $x) { 155 | $cx: 3 * $p1x; 156 | $bx: 3 * ($p2x - $p1x) - $cx; 157 | $ax: 1 - $cx - $bx; 158 | 159 | $t0: 0; 160 | $t1: 1; 161 | $t2: $x; 162 | $x2: 0; 163 | $res: 1000; 164 | 165 | @while ($t0 < $t1 or $break) { 166 | $x2: (($ax * $t2 + $bx) * $t2 + $cx) * $t2; 167 | 168 | @if (abs($x2 - $x) < $res) { 169 | @return $t2; 170 | } 171 | 172 | @if ($x > $x2) { 173 | $t0: $t2; 174 | } @else { 175 | $t1: $t2; 176 | } 177 | $t2: ($t1 - $t0) * 0.5 + $t0; 178 | } 179 | 180 | @return $t2; 181 | } 182 | 183 | @function typography-cubic-bezier($p1x, $p1y, $p2x, $p2y, $x) { 184 | $cy: 3 * $p1y; 185 | $by: 3 * ($p2y - $p1y) - $cy; 186 | $ay: 1 - $cy - $by; 187 | $t: typography-solve-bexier-x($p1x, $p1y, $p2x, $p2y, $x); 188 | 189 | @return (($ay * $t + $by) * $t + $cy) * $t; 190 | } 191 | 192 | // A stright up lerp 193 | // Credit: Ancient Greeks possibly Hipparchus of Rhodes 194 | @function typography-lerp($a, $b, $t) { 195 | @return $a + ($b - $a) * $t; 196 | } 197 | 198 | // String functions: 199 | 200 | // Cast string to number 201 | // Credit: Hugo Giraudel | https://www.sassmeister.com/gist/9fa19d254864f33d4a80 202 | @function typography-number($value) { 203 | @if type-of($value) == 'number' { 204 | @return $value; 205 | } @else if type-of($value) != 'string' { 206 | $_: log('Value for `to-number` should be a number or a string.'); 207 | } 208 | 209 | $result: 0; 210 | $digits: 0; 211 | $minus: str-slice($value, 1, 1) == '-'; 212 | $numbers: ( 213 | '0': 0, 214 | '1': 1, 215 | '2': 2, 216 | '3': 3, 217 | '4': 4, 218 | '5': 5, 219 | '6': 6, 220 | '7': 7, 221 | '8': 8, 222 | '9': 9 223 | ); 224 | 225 | @for $i from if($minus, 2, 1) through str-length($value) { 226 | $character: str-slice($value, $i, $i); 227 | 228 | @if not(index(map-keys($numbers), $character) or $character == '.') { 229 | @return to-length(if($minus, -$result, $result), str-slice($value, $i)); 230 | } 231 | 232 | @if $character == '.' { 233 | $digits: 1; 234 | } @else if $digits == 0 { 235 | $result: $result * 10 + map-get($numbers, $character); 236 | } @else { 237 | $digits: $digits * 10; 238 | $result: $result + map-get($numbers, $character) / $digits; 239 | } 240 | } 241 | 242 | @return if($minus, -$result, $result); 243 | } 244 | 245 | // Explode a string by a delimiter 246 | // Credit: https://gist.github.com/danielpchen/3677421ea15dcf2579ff 247 | @function typography-explode($string, $delimiter) { 248 | $result: (); 249 | 250 | @if $delimiter == '' { 251 | @for $i from 1 through str-length($string) { 252 | $result: append($result, str-slice($string, $i, $i)); 253 | } 254 | 255 | @return $result; 256 | } 257 | $exploding: true; 258 | 259 | @while $exploding { 260 | $d-index: str-index($string, $delimiter); 261 | 262 | @if $d-index { 263 | @if $d-index > 1 { 264 | $result: append($result, str-slice($string, 1, $d-index - 1)); 265 | $string: str-slice($string, $d-index + str-length($delimiter)); 266 | } @else if $d-index == 1 { 267 | $string: str-slice($string, 1, $d-index + str-length($delimiter)); 268 | } @else { 269 | $result: append($result, $string); 270 | $exploding: false; 271 | } 272 | } @else { 273 | $result: append($result, $string); 274 | $exploding: false; 275 | } 276 | } 277 | 278 | @return $result; 279 | } 280 | 281 | // Using vertical rhythm methods from https://scotch.io/tutorials/aesthetic-sass-3-typography-and-vertical-rhythm 282 | // Using perfect 8/9 for low contrast and perfect fifth 2/3 for high 283 | $typography-type-scale: (-1: 0.889rem, 0: 1rem, 1: 1.125rem, 2: 1.266rem, 3: 1.424rem); 284 | 285 | @function typography-type-scale($level) { 286 | @if map-has-key($typography-type-scale, $level) { 287 | @return map-get($typography-type-scale, $level); 288 | } 289 | 290 | @warn 'Unknown `#{$level}` in $typography-type-scale.'; 291 | 292 | @return null; 293 | } 294 | 295 | $typography-type-scale-contrast: (-1: 1rem, 0: 1.3333rem, 1: 1.777rem, 2: 2.369rem, 3: 3.157rem); 296 | 297 | @function typography-type-scale-contrast($level) { 298 | @if map-has-key($typography-type-scale-contrast, $level) { 299 | @return map-get($typography-type-scale-contrast, $level); 300 | } 301 | 302 | @warn 'Unknown `#{$level}` in $typography-type-scale-contrast.'; 303 | 304 | @return null; 305 | } 306 | 307 | $typography-base-font-size: 1rem; 308 | $typography-base-line-height: $typography-base-font-size * 1.25; 309 | 310 | $typography-line-heights: ( 311 | -1: $typography-base-line-height, 312 | 0: $typography-base-line-height, 313 | 1: $typography-base-line-height * 1.5, 314 | 2: $typography-base-line-height * 1.5, 315 | 3: $typography-base-line-height * 1.5 316 | ); 317 | 318 | @function typography-line-height($level) { 319 | @if map-has-key($typography-line-heights, $level) { 320 | @return map-get($typography-line-heights, $level); 321 | } 322 | 323 | @warn 'Unknown `#{$level}` in $line-height.'; 324 | 325 | @return null; 326 | } 327 | 328 | $typography-base-line-height-contrast: $typography-base-line-height; 329 | 330 | $typography-line-heights-contrast: ( 331 | -1: $typography-base-line-height-contrast, 332 | 0: $typography-base-line-height-contrast * 2, 333 | 1: $typography-base-line-height-contrast * 2, 334 | 2: $typography-base-line-height-contrast * 2, 335 | 3: $typography-base-line-height * 3 336 | ); 337 | 338 | @function typography-line-height-contrast($level) { 339 | @if map-has-key($typography-line-heights-contrast, $level) { 340 | @return map-get($typography-line-heights-contrast, $level); 341 | } 342 | 343 | @warn 'Unknown `#{$level}` in $typography-line-heights-contrast.'; 344 | 345 | @return null; 346 | } 347 | 348 | // Mixing these two sets of mixins ala Rachel: 349 | @mixin typography-got-rhythm($level: 0) { 350 | @include typography-interpolate( 351 | 'font-size', 352 | $size-content-width-min, 353 | typography-type-scale($level), 354 | $size-content-width-max, 355 | typography-type-scale-contrast($level) 356 | ); 357 | @include typography-interpolate( 358 | 'line-height', 359 | $size-content-width-min, 360 | typography-line-height($level), 361 | $size-content-width-max, 362 | typography-line-height-contrast($level) 363 | ); 364 | } 365 | 366 | %typography-xxlarge { 367 | @include typography-got-rhythm(3); 368 | 369 | @extend %font-heading; 370 | } 371 | 372 | %typography-xlarge { 373 | @include typography-got-rhythm(2); 374 | 375 | @extend %font-heading; 376 | } 377 | 378 | %typography-large { 379 | @include typography-got-rhythm(1); 380 | 381 | @extend %font-heading; 382 | } 383 | 384 | %typography-medium { 385 | @include typography-got-rhythm(0); 386 | 387 | @extend %font-content; 388 | } 389 | 390 | %typography-small { 391 | @include typography-got-rhythm(-1); 392 | 393 | @extend %font-content; 394 | } 395 | -------------------------------------------------------------------------------- /src/design/index.scss: -------------------------------------------------------------------------------- 1 | @import 'colors'; 2 | @import 'durations'; 3 | @import 'fonts'; 4 | @import 'layers'; 5 | @import 'sizes'; 6 | @import 'typography'; 7 | 8 | :export { 9 | // Any values that need to be accessible from JavaScript 10 | // outside of a Vue component can be defined here, prefixed 11 | // with `global-` to avoid conflicts with classes. For 12 | // example: 13 | // 14 | // global-grid-padding: $size-grid-padding; 15 | // 16 | // Then in a JavaScript file, you can import this object 17 | // as you would normally with: 18 | // 19 | // import design from '@design' 20 | // 21 | // console.log(design['global-grid-padding']) 22 | } 23 | -------------------------------------------------------------------------------- /src/layouts/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { createHead } from '@unhead/vue' 4 | 5 | import App from '@/App.vue' 6 | import router from '@/router/index' 7 | 8 | const app = createApp(App) 9 | 10 | /** Pinia **/ 11 | /** https://pinia.vuejs.org/ **/ 12 | const pinia = createPinia() 13 | app.use(pinia) 14 | 15 | /** Vue Router **/ 16 | /** https://router.vuejs.org/ **/ 17 | app.use(router) 18 | 19 | /** Unhead **/ 20 | /** https://unhead.unjs.io/ **/ 21 | const head = createHead() 22 | app.use(head) 23 | 24 | app.mount('#app') 25 | -------------------------------------------------------------------------------- /src/pages/about.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import routes from './routes' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes 7 | }) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | name: 'home', 5 | component: () => import('@/pages/index.vue') 6 | }, 7 | { 8 | path: '/about', 9 | name: 'about', 10 | // route level code-splitting 11 | // this generates a separate chunk (About.[hash].js) for this route 12 | // which is lazy-loaded when the route is visited. 13 | component: () => import('@/pages/about.vue') 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/stores/counter.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counter', () => { 5 | const count = ref(0) 6 | const doubleCount = computed(() => count.value * 2) 7 | function increment() { 8 | count.value++ 9 | } 10 | 11 | return { count, doubleCount, increment } 12 | }) 13 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | 3 | /** 4 | * It could be a ref, or a plain value 5 | */ 6 | export type MaybeRef = T | Ref 7 | 8 | /** 9 | * It could be a ref, plain value, or getter function 10 | */ 11 | export type MaybeRefOrGetter = MaybeRef | (() => T) 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["auto-imports.d.ts", "component.d.ts", "env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "module": "ESNext", 7 | "types": ["node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | 5 | import vue from '@vitejs/plugin-vue' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import Components from 'unplugin-vue-components/vite' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | AutoImport({ 14 | // global imports to register 15 | imports: ['vue', 'vue-router', '@vueuse/core', { '@unhead/vue': ['useHead'] }], 16 | dirs: ['@src/composables'] 17 | }), 18 | Components({ 19 | dirs: ['src/components', 'src/layouts'] 20 | }) 21 | ], 22 | resolve: { 23 | alias: { 24 | '@': fileURLToPath(new URL('./src', import.meta.url)) 25 | } 26 | }, 27 | server: { 28 | port: 8080 29 | }, 30 | css: { 31 | preprocessorOptions: { 32 | scss: { 33 | additionalData: '@use "@/design/index.scss" as *;' 34 | } 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig } from 'vite' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | import viteConfig from './vite.config' 5 | 6 | export default mergeConfig( 7 | viteConfig, 8 | defineConfig({ 9 | test: { 10 | environment: 'jsdom', 11 | exclude: [...configDefaults.exclude, 'e2e/*'], 12 | root: fileURLToPath(new URL('./', import.meta.url)), 13 | transformMode: { 14 | web: [/\.[jt]sx$/], 15 | }, 16 | } 17 | }) 18 | ) 19 | --------------------------------------------------------------------------------