├── src
├── assets
│ ├── scss
│ │ ├── objects
│ │ │ ├── _index.scss
│ │ │ └── _transition.scss
│ │ ├── utilities
│ │ │ ├── _index.scss
│ │ │ └── _hide-scrollbar.scss
│ │ ├── style.scss
│ │ ├── abstracts
│ │ │ ├── _z-index.scss
│ │ │ ├── _functions.scss
│ │ │ ├── _breakpoints.scss
│ │ │ ├── _index.scss
│ │ │ ├── _shape.scss
│ │ │ ├── _elevation.scss
│ │ │ ├── _colors.scss
│ │ │ ├── _typography.scss
│ │ │ ├── _variables.scss
│ │ │ └── _mixin.scss
│ │ ├── index.scss
│ │ ├── base
│ │ │ ├── _index.scss
│ │ │ ├── _base.scss
│ │ │ ├── _components.scss
│ │ │ ├── _theme.scss
│ │ │ ├── _fonts.scss
│ │ │ └── _reset.scss
│ │ ├── themes
│ │ │ ├── _index.scss
│ │ │ ├── _dark-theme.scss
│ │ │ └── _light-theme.scss
│ │ └── components
│ │ │ ├── _skeleton.scss
│ │ │ ├── _toast-manager.scss
│ │ │ ├── _index.scss
│ │ │ ├── _dialog.scss
│ │ │ ├── _side-sheet.scss
│ │ │ └── _date-picker.scss
│ ├── fonts
│ │ └── roboto
│ │ │ ├── roboto-bold.ttf
│ │ │ ├── roboto-medium.ttf
│ │ │ └── roboto-regular.ttf
│ └── icons
│ │ ├── filled
│ │ ├── remove.icon.vue
│ │ ├── arrow-drop-up.icon.vue
│ │ ├── arrow-drop-down.icon.vue
│ │ ├── check.icon.vue
│ │ ├── chevron-left.icon.vue
│ │ ├── chevron-right.icon.vue
│ │ ├── favorite.icon.vue
│ │ ├── close.icon.vue
│ │ ├── add.icon.vue
│ │ ├── arrow-back.icon.vue
│ │ ├── arrow-forward.icon.vue
│ │ └── add-circle.icon.vue
│ │ └── outlined
│ │ └── favorite.icon.vue
├── plug-in
│ ├── router
│ │ └── install.js
│ ├── vee-validate
│ │ ├── index.js
│ │ ├── rules.js
│ │ ├── install.js
│ │ └── composable
│ │ │ └── use-input.composable.js
│ ├── i18n
│ │ ├── index.js
│ │ ├── dictionary
│ │ │ ├── fa.json
│ │ │ ├── en.json
│ │ │ └── index.js
│ │ ├── utils
│ │ │ └── double-bracket-replace.util.js
│ │ ├── install.js
│ │ ├── composable
│ │ │ ├── use-locale.js
│ │ │ └── use-translate.js
│ │ └── translate.js
│ ├── toast
│ │ ├── index.js
│ │ ├── composable
│ │ │ └── use-toast.composable.js
│ │ ├── install.js
│ │ ├── constants
│ │ │ └── toast.constant.js
│ │ ├── components
│ │ │ ├── toast-notification.component.vue
│ │ │ └── toast-manager.component.vue
│ │ └── toast-manager.js
│ ├── axios
│ │ ├── middlewares
│ │ │ ├── index.js
│ │ │ ├── on-response
│ │ │ │ ├── responsibilities.js
│ │ │ │ └── index.js
│ │ │ └── on-request
│ │ │ │ ├── responsibilities.js
│ │ │ │ └── index.js
│ │ ├── index.js
│ │ ├── install.js
│ │ └── default-config-builder
│ │ │ └── index.js
│ └── index.js
├── utils
│ ├── index.js
│ ├── math.util.js
│ ├── uuid.util.js
│ ├── object.util.js
│ ├── dom.util.js
│ ├── deep-clone.util.js
│ └── timeout.util.js
├── services
│ ├── event-bus
│ │ ├── index.js
│ │ └── event-bus.service.js
│ ├── calendar
│ │ └── gregorian
│ │ │ ├── index.js
│ │ │ └── gregorian-calendar.service.js
│ └── mask
│ │ └── index.js
├── layouts
│ └── default.layout.vue
├── constants
│ ├── mask
│ │ └── mask.constant.js
│ ├── calendar
│ │ └── gregorian.constant.js
│ └── router
│ │ └── routes.constant.js
├── router
│ ├── middlewares
│ │ ├── index.js
│ │ ├── before-each.js
│ │ └── after-each.js
│ └── index.js
├── App.vue
├── components
│ ├── common
│ │ ├── router
│ │ │ ├── transition.component.vue
│ │ │ └── indicator.component.vue
│ │ ├── layout-view
│ │ │ └── layout-view.component.vue
│ │ ├── picker
│ │ │ └── date
│ │ │ │ ├── header.vue
│ │ │ │ ├── control-menu.vue
│ │ │ │ ├── year-picker.vue
│ │ │ │ └── base-calendar.vue
│ │ ├── scrim
│ │ │ ├── container.component.vue
│ │ │ └── item.component.vue
│ │ ├── sheet
│ │ │ └── side
│ │ │ │ ├── modal.component.vue
│ │ │ │ └── standard.component.vue
│ │ ├── skeleton
│ │ │ └── base.component.vue
│ │ ├── snack-bar
│ │ │ └── snack-bar.component.vue
│ │ ├── icon
│ │ │ └── base.component.vue
│ │ ├── input
│ │ │ ├── radio.component.vue
│ │ │ ├── stepper.component.vue
│ │ │ └── switch.component.vue
│ │ ├── progress-indicator
│ │ │ ├── linear.component.vue
│ │ │ └── circular.component.vue
│ │ ├── dialog
│ │ │ └── basic.component.vue
│ │ ├── button
│ │ │ ├── icon.component.vue
│ │ │ ├── segmented.component.vue
│ │ │ └── base.component.vue
│ │ ├── chip
│ │ │ └── filter.component.vue
│ │ └── field
│ │ │ └── filled-text.component.vue
│ ├── view
│ │ └── presentation
│ │ │ ├── date-picker.component.vue
│ │ │ ├── snackbar.component.vue
│ │ │ ├── switch.component.vue
│ │ │ ├── radio-input.component.vue
│ │ │ ├── progress-bar.component.vue
│ │ │ ├── segmented-button.component.vue
│ │ │ ├── skeleton.component.vue
│ │ │ ├── stepper-input.component.vue
│ │ │ ├── icon-button.component.vue
│ │ │ ├── filter-chip.component.vue
│ │ │ └── button.component.vue
│ └── index.js
├── main.js
├── composable
│ ├── use-event-bus.composable.js
│ ├── use-promise.composable.js
│ └── use-draggable.composable.js
├── views
│ ├── playground
│ │ └── playground-view.vue
│ └── presentation
│ │ └── presentation-view.vue
└── interfaces
│ └── calendar
│ └── calendar.interface.js
├── public
└── favicon.ico
├── .vscode
└── extensions.json
├── .prettierrc.json
├── .eslintrc.cjs
├── index.html
├── .gitignore
├── vite.config.js
├── package.json
└── README.md
/src/assets/scss/objects/_index.scss:
--------------------------------------------------------------------------------
1 | @forward './transition';
2 |
--------------------------------------------------------------------------------
/src/assets/scss/utilities/_index.scss:
--------------------------------------------------------------------------------
1 | @forward './hide-scrollbar';
2 |
--------------------------------------------------------------------------------
/src/plug-in/router/install.js:
--------------------------------------------------------------------------------
1 | export { default } from '@/router';
2 |
--------------------------------------------------------------------------------
/src/assets/scss/style.scss:
--------------------------------------------------------------------------------
1 | @use './base' as *;
2 | @use './objects' as *;
3 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_z-index.scss:
--------------------------------------------------------------------------------
1 | $route-indicator: 99;
2 | $toast: 98;
3 | $scrim: 97;
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0Amirreza0/vue3-boilerplate/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/scss/index.scss:
--------------------------------------------------------------------------------
1 | @forward './abstracts';
2 | @forward './components';
3 | @forward './themes';
4 | @forward './utilities';
5 |
--------------------------------------------------------------------------------
/src/plug-in/vee-validate/index.js:
--------------------------------------------------------------------------------
1 | export { default as useInput } from '@/plug-in/vee-validate/composable/use-input.composable.js';
2 |
--------------------------------------------------------------------------------
/src/assets/fonts/roboto/roboto-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0Amirreza0/vue3-boilerplate/HEAD/src/assets/fonts/roboto/roboto-bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/roboto/roboto-medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0Amirreza0/vue3-boilerplate/HEAD/src/assets/fonts/roboto/roboto-medium.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/roboto/roboto-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0Amirreza0/vue3-boilerplate/HEAD/src/assets/fonts/roboto/roboto-regular.ttf
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_functions.scss:
--------------------------------------------------------------------------------
1 | @use './variables' as *;
2 |
3 | @function space($count) {
4 | @return $base-space * $count;
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_breakpoints.scss:
--------------------------------------------------------------------------------
1 | $breakpoints: (
2 | large: 1599px,
3 | expanded: 1199px,
4 | medium: 839px,
5 | compact: 599px,
6 | );
7 |
--------------------------------------------------------------------------------
/src/assets/scss/base/_index.scss:
--------------------------------------------------------------------------------
1 | @forward './reset';
2 | @forward './fonts';
3 | @forward './theme';
4 | @forward './base';
5 | @forward './components';
6 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { default as deepClone } from '@/utils/deep-clone.util.js';
2 | export { default as Timeout } from '@/utils/timeout.util.js';
3 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/index.js:
--------------------------------------------------------------------------------
1 | export { default as useLocale } from './composable/use-locale';
2 | export { default as useTranslate } from './composable/use-translate';
3 |
--------------------------------------------------------------------------------
/src/assets/scss/themes/_index.scss:
--------------------------------------------------------------------------------
1 | @use './light-theme' as *;
2 | @use './dark-theme' as *;
3 |
4 | $themes: (
5 | light: $light-theme,
6 | dark: $dark-theme,
7 | );
8 |
--------------------------------------------------------------------------------
/src/services/event-bus/index.js:
--------------------------------------------------------------------------------
1 | import EventBus from '@/services/event-bus/event-bus.service';
2 |
3 | const eventBus = new EventBus();
4 |
5 | export default eventBus;
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "printWidth": 80,
6 | "trailingComma": "es5",
7 | "vueIndentScriptAndStyle": true
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/scss/base/_base.scss:
--------------------------------------------------------------------------------
1 | body {
2 | min-height: 100vh;
3 | overflow: hidden auto;
4 | font-family: 'Roboto';
5 | background-color: var(--palette-surface-container-lowest);
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/scss/components/_skeleton.scss:
--------------------------------------------------------------------------------
1 | $skeleton: (
2 | skeleton-from-color: var(--palette-surface-container-high),
3 | skeleton-to-color: var(--palette-surface-container-low),
4 | );
5 |
--------------------------------------------------------------------------------
/src/assets/scss/utilities/_hide-scrollbar.scss:
--------------------------------------------------------------------------------
1 | %hide-scrollbar {
2 | -ms-overflow-style: none;
3 | scrollbar-width: none;
4 | &::-webkit-scrollbar {
5 | display: none;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/plug-in/toast/index.js:
--------------------------------------------------------------------------------
1 | export { default as useToast } from './composable/use-toast.composable.js';
2 | export { default as ToastManager } from './components/toast-manager.component.vue';
3 |
--------------------------------------------------------------------------------
/src/assets/scss/components/_toast-manager.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/functions' as *;
2 |
3 | $toast-manager: (
4 | toast-stack-margin: space(6),
5 | toast-stack-item-margin: space(2) 0 0,
6 | );
7 |
--------------------------------------------------------------------------------
/src/layouts/default.layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/plug-in/toast/composable/use-toast.composable.js:
--------------------------------------------------------------------------------
1 | import { inject } from 'vue';
2 |
3 | const useToast = () => {
4 | const toast = inject('toast');
5 | return toast;
6 | };
7 |
8 | export default useToast;
9 |
--------------------------------------------------------------------------------
/src/plug-in/axios/middlewares/index.js:
--------------------------------------------------------------------------------
1 | export { default as requestMiddlewares } from '@/plug-in/axios/middlewares/on-request';
2 | export { default as responseMiddlewares } from '@/plug-in/axios/middlewares/on-response';
3 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/dictionary/fa.json:
--------------------------------------------------------------------------------
1 | {
2 | "validationErrors": {
3 | "required": "این فیلد اجباریست",
4 | "betweenLength": "مقدار باید بین {{0}} و {{1}} باشد"
5 | },
6 | "playground": {
7 | "title": "زمین بازی"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/dictionary/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "validationErrors": {
3 | "required": "This Field is required",
4 | "betweenLength": "Must be between {{0}} and {{1}}"
5 | },
6 | "playground": {
7 | "title": "Playground"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/scss/base/_components.scss:
--------------------------------------------------------------------------------
1 | @use '@/assets/scss/components' as *;
2 |
3 | :root {
4 | @each $component, $variables in $components {
5 | @each $variable, $value in $variables {
6 | --#{$variable}: #{$value};
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_index.scss:
--------------------------------------------------------------------------------
1 | @forward './colors';
2 | @forward './variables';
3 | @forward './mixin';
4 | @forward './typography';
5 | @forward './breakpoints';
6 | @forward './shape';
7 | @forward './functions';
8 | @forward './elevation';
9 | @forward './z-index';
10 |
--------------------------------------------------------------------------------
/src/assets/scss/base/_theme.scss:
--------------------------------------------------------------------------------
1 | @use '../themes' as *;
2 |
3 | :root {
4 | @each $theme-name, $theme in $themes {
5 | &[data-theme='#{$theme-name}'] {
6 | @each $name, $value in $theme {
7 | --palette-#{$name}: #{$value};
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/constants/mask/mask.constant.js:
--------------------------------------------------------------------------------
1 | export const REGEX_STARTING_CHAR = '^';
2 | export const REGEX_ENDING_CHAR = '$';
3 | export const REGEX_TOKEN = '[^.]';
4 | export const DEFAULT_TOKEN = '#';
5 |
6 | export default {
7 | REGEX_STARTING_CHAR,
8 | REGEX_ENDING_CHAR,
9 | REGEX_TOKEN,
10 | DEFAULT_TOKEN,
11 | };
12 |
--------------------------------------------------------------------------------
/src/assets/scss/components/_index.scss:
--------------------------------------------------------------------------------
1 | @use './toast-manager.scss' as *;
2 | @use './date-picker.scss' as *;
3 | @use './side-sheet.scss' as *;
4 | @use './dialog.scss' as *;
5 | @use './skeleton.scss' as *;
6 |
7 | $components: (
8 | toast-manager: $toast-manager,
9 | side-sheet: $side-sheet,
10 | skeleton: $skeleton,
11 | );
12 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/remove.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/utils/double-bracket-replace.util.js:
--------------------------------------------------------------------------------
1 | export const doubleBracketReplace = (value, replacementMap = {}) => {
2 | const doubleBracketRegex = /{{(.*?)}}/g;
3 |
4 | return value.replace(doubleBracketRegex, (match, captured) => {
5 | const sanitizedCaptured = captured.trim();
6 | return replacementMap[sanitizedCaptured] || match;
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/.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-prettier/skip-formatting'
10 | ],
11 | parserOptions: {
12 | ecmaVersion: 'latest'
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/arrow-drop-up.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/arrow-drop-down.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/services/calendar/gregorian/index.js:
--------------------------------------------------------------------------------
1 | import GregorianCalendar from './gregorian-calendar.service';
2 | import {
3 | WEEK_DAYS_LIST,
4 | MONTH_LIST,
5 | } from '@/constants/calendar/gregorian.constant.js';
6 |
7 | const gregorianCalendar = new GregorianCalendar({
8 | monthList: MONTH_LIST,
9 | weekDayList: WEEK_DAYS_LIST,
10 | });
11 |
12 | export default gregorianCalendar;
13 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/check.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/plug-in/toast/install.js:
--------------------------------------------------------------------------------
1 | import ToastManager from './toast-manager.js';
2 | import ToastManagerComponent from './components/toast-manager.component.vue';
3 |
4 | export default (vueInstance, config) => {
5 | const toastManager = new ToastManager(config);
6 |
7 | vueInstance.provide('toast', toastManager);
8 | vueInstance.component('toast-manager', ToastManagerComponent);
9 | };
10 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/chevron-left.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/chevron-right.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/utils/math.util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Clamp value between min and max values
3 | * @param {number} value
4 | * @param {Object} options
5 | * @param {number} [options.min=0]
6 | * @param {number} options.max
7 | * @returns {number} - clamped value
8 | */
9 | export const clamp = (value, { max, min = 0 }) => {
10 | return Math.min(Math.max(min, value), max);
11 | };
12 |
13 | export default { clamp };
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/constants/calendar/gregorian.constant.js:
--------------------------------------------------------------------------------
1 | export const MONTH_LIST = [
2 | 'January',
3 | 'February',
4 | 'March',
5 | 'April',
6 | 'May',
7 | 'June',
8 | 'July',
9 | 'August',
10 | 'September',
11 | 'October',
12 | 'November',
13 | 'December',
14 | ];
15 |
16 | export const WEEK_DAYS_LIST = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
17 |
18 | export default { WEEK_DAYS_LIST, MONTH_LIST };
19 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_shape.scss:
--------------------------------------------------------------------------------
1 | $border-radius-base: 4px;
2 |
3 | $radius-1x: $border-radius-base;
4 | $radius-2x: $border-radius-base * 2;
5 | $radius-3x: $border-radius-base * 3;
6 | $radius-4x: $border-radius-base * 4;
7 | $radius-5x: $border-radius-base * 5;
8 | $radius-6x: $border-radius-base * 6;
9 | $radius-7x: $border-radius-base * 7;
10 | $radius-8x: $border-radius-base * 8;
11 |
12 | $pill: 9999px;
13 | $circle: 50%;
14 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/favorite.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/scss/base/_fonts.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Roboto;
3 | font-weight: 400;
4 | src: url('@/assets/fonts/roboto/roboto-regular.ttf');
5 | }
6 |
7 | @font-face {
8 | font-family: Roboto;
9 | font-weight: 500;
10 | src: url('@/assets/fonts/roboto/roboto-medium.ttf');
11 | }
12 |
13 | @font-face {
14 | font-family: Roboto;
15 | font-weight: 700;
16 | src: url('@/assets/fonts/roboto/roboto-bold.ttf');
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/scss/base/_reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | font-family: inherit;
4 | }
5 |
6 | body,
7 | p,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6 {
14 | margin: 0;
15 | }
16 |
17 | ol,
18 | ul,
19 | menu {
20 | list-style: none;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | input,
26 | button {
27 | background-color: transparent;
28 | outline: none;
29 | border: none;
30 | padding: 0;
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/uuid.util.js:
--------------------------------------------------------------------------------
1 | export const generateUUID = () => {
2 | const pattern = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
3 |
4 | return pattern.replace(/[xy]/g, (placeholder) => {
5 | const randomValue = (Math.random() * 16) | 0;
6 | const value = placeholder === 'x' ? randomValue : (randomValue & 0x3) | 0x8;
7 |
8 | return value.toString(16);
9 | });
10 | };
11 |
12 | export default {
13 | generate: generateUUID,
14 | };
15 |
--------------------------------------------------------------------------------
/src/plug-in/axios/index.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 |
3 | class AxiosSingleton {
4 | static #instance;
5 |
6 | constructor(options) {
7 | return Axios.create(options);
8 | }
9 |
10 | static getInstance(options) {
11 | if (!AxiosSingleton.#instance) {
12 | AxiosSingleton.#instance = new AxiosSingleton(options);
13 | }
14 |
15 | return AxiosSingleton.#instance;
16 | }
17 | }
18 |
19 | export default AxiosSingleton;
20 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/install.js:
--------------------------------------------------------------------------------
1 | import { translate } from './translate.js';
2 | import { useLocale } from './index.js';
3 |
4 | export default (vueInstance, config) => {
5 | const { setLocale, locale: defaultLocale } = useLocale();
6 | setLocale(config?.defaultLocale ?? 'en');
7 |
8 | vueInstance.config.globalProperties.$t = (
9 | query,
10 | locale = defaultLocale.value
11 | ) => {
12 | return translate(query, locale);
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/router/middlewares/index.js:
--------------------------------------------------------------------------------
1 | import afterEach from '@/router/middlewares/after-each.js';
2 | import beforeEach from '@/router/middlewares/before-each.js';
3 |
4 | /**
5 | * Apply beforeEach and afterEach middlewares
6 | * @param {Object} router - router instance
7 | */
8 | const applyMiddlewares = (router) => {
9 | router.afterEach(afterEach);
10 | router.beforeEach(beforeEach);
11 | };
12 |
13 | export default { apply: applyMiddlewares };
14 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/close.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/plug-in/vee-validate/rules.js:
--------------------------------------------------------------------------------
1 | const required = (value) => {
2 | if (!value || !value.length) {
3 | return false;
4 | }
5 |
6 | return true;
7 | };
8 |
9 | const betweenLength = (value, [min, max]) => {
10 | if (!value || !value.length) {
11 | return true;
12 | }
13 |
14 | if (value.length < min || value.length > max) {
15 | return false;
16 | }
17 |
18 | return true;
19 | };
20 |
21 | export default { required, betweenLength };
22 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/add.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/arrow-back.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/arrow-forward.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 | import ROUTES_MAP from '@/constants/router/routes.constant.js';
3 | import Middlewares from '@/router/middlewares';
4 |
5 | const routes = Object.values(ROUTES_MAP).map((routeConfig) => routeConfig);
6 |
7 | const router = createRouter({
8 | history: createWebHashHistory(import.meta.env.BASE_URL),
9 | routes,
10 | });
11 |
12 | Middlewares.apply(router);
13 |
14 | export default router;
15 |
--------------------------------------------------------------------------------
/src/utils/object.util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if the given entry is object and it's empty
3 | * @param {Object} entry
4 | * @returns {boolean} - entry emptiness
5 | */
6 | export const isObjectEmpty = (entry) => {
7 | if (!entry) return false;
8 |
9 | const isObject = entry?.constructor === Object;
10 | if (!isObject) return false;
11 |
12 | const isEmpty = Object.keys(entry).length === 0;
13 | return isEmpty;
14 | };
15 |
16 | export default {
17 | isEmpty: isObjectEmpty,
18 | };
19 |
--------------------------------------------------------------------------------
/src/router/middlewares/before-each.js:
--------------------------------------------------------------------------------
1 | import eventBus from '@/services/event-bus';
2 |
3 | /**
4 | * Publish beforeEach happened event
5 | */
6 | const publishEvent = () => {
7 | eventBus.publish('router:beforeEach');
8 | };
9 |
10 | /**
11 | * BeforeEach middlewares call
12 | * @param {Object} to - VueRouter `to` route object
13 | * @param {Object} from - VueRouter `from` route object
14 | */
15 | const beforeEach = (to, from) => {
16 | publishEvent(to, from);
17 | };
18 |
19 | export default beforeEach;
20 |
--------------------------------------------------------------------------------
/src/utils/dom.util.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Move cursor to the end if focused
3 | * @param {HTMLElement} inputElement
4 | */
5 | export const moveCursorToEnd = (inputElement) => {
6 | const selection = window.getSelection();
7 |
8 | if (selection.rangeCount > 0) {
9 | selection.removeAllRanges();
10 | }
11 |
12 | const range = document.createRange();
13 |
14 | range.selectNodeContents(inputElement);
15 | range.collapse(false);
16 |
17 | selection.addRange(range);
18 | };
19 |
20 | export default { moveCursorToEnd };
21 |
--------------------------------------------------------------------------------
/src/assets/scss/components/_dialog.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/mixin' as *;
2 |
3 | .dialog-enter-active {
4 | @include transition(emphasized-decelerate) {
5 | transform-origin: top;
6 | transition-property: transform, opacity;
7 | }
8 | }
9 |
10 | .dialog-leave-active {
11 | @include transition(emphasized-accelerate) {
12 | transform-origin: top;
13 | transition-property: transform, opacity;
14 | }
15 | }
16 |
17 | .dialog-leave-to,
18 | .dialog-enter-from {
19 | transform: scaleY(0);
20 | opacity: 0;
21 | }
22 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node: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 | },
13 | },
14 | css: {
15 | preprocessorOptions: {
16 | scss: {
17 | additionalData: `@use "@/assets/scss/index.scss" as *;`,
18 | },
19 | },
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/src/plug-in/index.js:
--------------------------------------------------------------------------------
1 | const pluginSourceMap = import.meta.globEager(['@/plug-in/*/install.js'], {
2 | import: 'default',
3 | });
4 |
5 | const pluginSourceList = Object.keys(pluginSourceMap);
6 |
7 | export const registerPlugins = (vueInstance, pluginConfigMap = {}) => {
8 | pluginSourceList.forEach((source) => {
9 | const plugin = pluginSourceMap[source];
10 |
11 | const [pluginName] = source.split('/').slice(-2);
12 | const config = pluginConfigMap[pluginName];
13 |
14 | vueInstance.use(plugin, config);
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/deep-clone.util.js:
--------------------------------------------------------------------------------
1 | export const deepClone = (itemToClone) => {
2 | if (!itemToClone) return itemToClone;
3 |
4 | const isArray = Array.isArray(itemToClone);
5 | if (isArray) return itemToClone.map((item) => deepClone(item));
6 |
7 | const isObject = typeof itemToClone === 'object';
8 |
9 | if (!isObject) return itemToClone;
10 |
11 | const objectClone = {};
12 |
13 | Object.keys(itemToClone).forEach((key) => {
14 | const item = itemToClone[key];
15 | objectClone[key] = deepClone(item);
16 | });
17 |
18 | return objectClone;
19 | };
20 |
21 | export default deepClone;
22 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/src/components/common/router/transition.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
25 |
--------------------------------------------------------------------------------
/src/plug-in/vee-validate/install.js:
--------------------------------------------------------------------------------
1 | import { configure, defineRule } from 'vee-validate';
2 | import allRules from '@/plug-in/vee-validate/rules.js';
3 |
4 | const messageGenerator = ({ rule }) => {
5 | return rule;
6 | };
7 |
8 | const setupRules = (rules) => {
9 | Object.entries(rules).forEach(([ruleName, validator]) => {
10 | defineRule(ruleName, validator);
11 | });
12 | };
13 |
14 | const DEFAULT_CONFIG = Object.freeze({
15 | generateMessage: messageGenerator,
16 | });
17 |
18 | export default (_, userConfig) => {
19 | setupRules(allRules);
20 |
21 | const config = { ...DEFAULT_CONFIG, ...userConfig };
22 | configure(config);
23 | };
24 |
--------------------------------------------------------------------------------
/src/assets/icons/outlined/favorite.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp as createVueApp } from 'vue';
2 |
3 | import App from '@/App.vue';
4 | import { registerPlugins } from '@/plug-in';
5 | import { registerCommonComponents } from '@/components';
6 |
7 | import '@/assets/scss/style.scss';
8 |
9 | //TODO: move to constants or configs
10 | const pluginConfigs = {
11 | i18n: {
12 | defaultLocale: 'en',
13 | },
14 | };
15 |
16 | const createApplication = () => {
17 | const vueAppInstance = createVueApp(App);
18 |
19 | registerPlugins(vueAppInstance, pluginConfigs);
20 | registerCommonComponents(vueAppInstance, ['scrim']);
21 |
22 | vueAppInstance.mount('#app');
23 | };
24 |
25 | createApplication();
26 |
--------------------------------------------------------------------------------
/src/plug-in/toast/constants/toast.constant.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_CONFIG = Object.freeze({
2 | duration: 3000,
3 | position: 'bottom',
4 | type: 'info',
5 | autoDismiss: true,
6 | stackMaxToast: 3,
7 | });
8 |
9 | export const TOAST_POSITIONS = [
10 | 'top',
11 | 'bottom',
12 | 'top-center',
13 | 'top-left',
14 | 'top-right',
15 | 'bottom-center',
16 | 'bottom-right',
17 | 'bottom-left',
18 | ];
19 |
20 | export const STACK_MAP = {
21 | top: [],
22 | bottom: [],
23 | 'top-center': [],
24 | 'top-left': [],
25 | 'top-right': [],
26 | 'bottom-center': [],
27 | 'bottom-right': [],
28 | 'bottom-left': [],
29 | };
30 |
31 | export default { POSITIONS: TOAST_POSITIONS, DEFAULT_CONFIG, STACK_MAP };
32 |
--------------------------------------------------------------------------------
/src/plug-in/axios/middlewares/on-response/responsibilities.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build response payload
3 | * @param {Object} response - AxiosResponse object
4 | * @param {Object} context
5 | * @param {Object} context.vueInstance - Current vue instance
6 | * @returns {any} - Modified response
7 | */
8 | const getResponsePayload = (response) => response?.data;
9 |
10 | /**
11 | * Build error payload
12 | * @param {Object} error - AxiosError object
13 | * @param {Object} context
14 | * @param {Object} context.vueInstance - Current vue instance
15 | * @returns {any} - Modified error
16 | */
17 | const getErrorPayload = (error) => Promise.reject(error.response);
18 |
19 | export default {
20 | getErrorPayload,
21 | getResponsePayload,
22 | };
23 |
--------------------------------------------------------------------------------
/src/plug-in/axios/middlewares/on-request/responsibilities.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build request payload
3 | * @param {Object} request - AxiosRequest object
4 | * @param {Object} context
5 | * @param {Object} context.vueInstance - Current vue instance
6 | * @returns {any} - Modified response
7 | */
8 | const getRequestPayload = (request) => {
9 | return request;
10 | };
11 |
12 | /**
13 | * Build error payload
14 | * @param {Object} error - AxiosError object
15 | * @param {Object} context
16 | * @param {Object} context.vueInstance - Current vue instance
17 | * @returns {any} - Modified error
18 | */
19 | const getErrorPayload = (error) => {
20 | return Promise.reject(error);
21 | };
22 |
23 | export default {
24 | getRequestPayload,
25 | getErrorPayload,
26 | };
27 |
--------------------------------------------------------------------------------
/src/constants/router/routes.constant.js:
--------------------------------------------------------------------------------
1 | export const PRESENTATION_ROUTE = Object.freeze({
2 | name: 'presentation',
3 | path: '/presentation',
4 | component: () => import('@/views/presentation/presentation-view.vue'),
5 | meta: {
6 | title: 'Presentation | Boilerplate',
7 | },
8 | });
9 |
10 | export const PLAYGROUND_ROUTE = Object.freeze({
11 | name: 'playground',
12 | path: '/playground',
13 | component: () => import('@/views/playground/playground-view.vue'),
14 | meta: {
15 | title: 'Playground | Boilerplate',
16 | },
17 | });
18 |
19 | export const ROOT_ROUTE = Object.freeze({
20 | name: 'root',
21 | path: '/',
22 | redirect: { name: 'presentation' },
23 | });
24 |
25 | export default { PRESENTATION_ROUTE, PLAYGROUND_ROUTE, ROOT_ROUTE };
26 |
--------------------------------------------------------------------------------
/src/components/view/presentation/date-picker.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
DatePicker Component Presentation
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
29 |
--------------------------------------------------------------------------------
/src/assets/icons/filled/add-circle.icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/src/assets/scss/objects/_transition.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/_mixin' as *;
2 |
3 | .scale-fade-enter-active {
4 | @include transition(standard-decelerate) {
5 | transition-property: opacity, transform;
6 | }
7 | }
8 |
9 | .scale-fade-leave-active {
10 | @include transition(standard-accelerate) {
11 | transition-property: opacity, transform;
12 | }
13 | }
14 |
15 | .scale-fade-enter-from,
16 | .scale-fade-leave-to {
17 | opacity: 0;
18 | transform: scale(0.9);
19 | }
20 |
21 | .fade-enter-active {
22 | @include transition(standard-decelerate) {
23 | transition-property: opacity;
24 | }
25 | }
26 |
27 | .fade-leave-active {
28 | @include transition(standard-accelerate) {
29 | transition-property: opacity;
30 | }
31 | }
32 |
33 | .fade-leave-to,
34 | .fade-enter-from {
35 | opacity: 0;
36 | }
37 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/dictionary/index.js:
--------------------------------------------------------------------------------
1 | const dictionaryMap = import.meta.globEager(
2 | ['@/plug-in/i18n/dictionary/*.json'],
3 | { import: 'default' }
4 | );
5 |
6 | export const supportedLocaleList = [];
7 | export const rtlLocaleList = ['fa'];
8 |
9 | const dictionaryPathList = Object.keys(dictionaryMap);
10 | export const dictionaries = dictionaryPathList.reduce(
11 | (dictionaries, dictionaryPath) => {
12 | const dictionary = dictionaryMap[dictionaryPath];
13 |
14 | const [dictionaryFileName] = dictionaryPath.split('/').slice(-1);
15 | const [dictionaryName] = dictionaryFileName.split('.json');
16 |
17 | supportedLocaleList.push(dictionaryName);
18 |
19 | return { ...dictionaries, [dictionaryName]: dictionary };
20 | },
21 | {}
22 | );
23 |
24 | export default { dictionaries, supportedLocaleList };
25 |
--------------------------------------------------------------------------------
/src/router/middlewares/after-each.js:
--------------------------------------------------------------------------------
1 | import eventBus from '@/services/event-bus';
2 |
3 | /**
4 | * Update document title based on `route.meta.title`
5 | * @param {Object} to - VueRouter `to` route object
6 | * @returns {undefined}
7 | */
8 | const updateDocumentTitle = (to) => {
9 | const title = to.meta.title;
10 | if (!title) return;
11 |
12 | document.title = title;
13 | };
14 |
15 | /**
16 | * Publish afterEach happened event
17 | */
18 | const publishEvent = () => {
19 | eventBus.publish('router:afterEach');
20 | };
21 |
22 | /**
23 | * afterEach middlewares call
24 | * @param {Object} to - VueRouter `to` route object
25 | * @param {Object} from - VueRouter `from` route object
26 | */
27 | const afterEach = (to, from) => {
28 | updateDocumentTitle(to, from);
29 | publishEvent(to, from);
30 | };
31 |
32 | export default afterEach;
33 |
--------------------------------------------------------------------------------
/src/plug-in/axios/install.js:
--------------------------------------------------------------------------------
1 | import AxiosSingleton from './index';
2 |
3 | import DefaultConfigBuilder from '@/plug-in/axios/default-config-builder';
4 | import {
5 | requestMiddlewares,
6 | responseMiddlewares,
7 | } from '@/plug-in/axios/middlewares';
8 |
9 | export default (vueInstance, config) => {
10 | const defaultConfig = new DefaultConfigBuilder(config).build();
11 | const axios = AxiosSingleton.getInstance(defaultConfig);
12 |
13 | axios.interceptors.request.use(
14 | (request) => requestMiddlewares.onRequest(request, { vueInstance }),
15 | (error) => requestMiddlewares.onRequestError(error, { vueInstance })
16 | );
17 |
18 | axios.interceptors.response.use(
19 | (response) => responseMiddlewares.onResponse(response, { vueInstance }),
20 | (error) => responseMiddlewares.onResponseError(error, { vueInstance })
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3-boilerplate",
3 | "version": "1.0.0-alpha.55",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "devn": "vite --host",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
11 | "format": "prettier --write src/"
12 | },
13 | "dependencies": {
14 | "axios": "^1.6.8",
15 | "pinia": "^2.1.3",
16 | "vee-validate": "^4.12.6",
17 | "vue": "^3.4.21",
18 | "vue-router": "^4.2.2"
19 | },
20 | "devDependencies": {
21 | "@rushstack/eslint-patch": "^1.2.0",
22 | "@vitejs/plugin-vue": "^4.2.3",
23 | "@vue/eslint-config-prettier": "^7.1.0",
24 | "eslint": "^8.39.0",
25 | "eslint-plugin-vue": "^9.11.0",
26 | "prettier": "^2.8.8",
27 | "sass": "^1.64.1",
28 | "vite": "^4.3.9"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/composable/use-event-bus.composable.js:
--------------------------------------------------------------------------------
1 | import { onUnmounted } from 'vue';
2 | import eventBus from '@/services/event-bus';
3 |
4 | export const useEventBus = () => {
5 | const subscriptionMap = {};
6 |
7 | const subscribeOn = (eventName, callback) => {
8 | const subscription = eventBus.subscribeOn(eventName, callback);
9 | subscriptionMap[subscription.id] = subscription.unsubscribe;
10 |
11 | return subscription;
12 | };
13 |
14 | const publish = (eventName, ...args) => eventBus.publish(eventName, ...args);
15 |
16 | const unsubscribe = (subscriptionId) => {
17 | subscriptionMap[subscriptionId]?.();
18 | subscriptionMap[subscriptionId] = null;
19 | };
20 |
21 | onUnmounted(() => {
22 | Object.values(subscriptionMap).forEach((unSubscriber) => unSubscriber?.());
23 | });
24 |
25 | return { subscribeOn, publish, unsubscribe };
26 | };
27 |
28 | export default useEventBus;
29 |
--------------------------------------------------------------------------------
/src/components/view/presentation/snackbar.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
SnackBar Component Presentation
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
37 |
--------------------------------------------------------------------------------
/src/views/playground/playground-view.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
22 |
23 |
37 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/composable/use-locale.js:
--------------------------------------------------------------------------------
1 | import { computed, ref } from 'vue';
2 | import { supportedLocaleList, rtlLocaleList } from '../dictionary';
3 |
4 | const locale = ref('');
5 |
6 | const useLocale = () => {
7 | const getLocale = computed(() => locale.value);
8 |
9 | const direction = computed(() =>
10 | rtlLocaleList.includes(locale.value) ? 'rtl' : 'ltr'
11 | );
12 |
13 | const setLocale = (newLocale) => {
14 | document.documentElement.setAttribute('lang', newLocale);
15 |
16 | const newDirection = rtlLocaleList.includes(newLocale) ? 'rtl' : 'ltr';
17 | document.documentElement.setAttribute('dir', newDirection);
18 |
19 | if (locale.value === newLocale) return;
20 |
21 | if (!supportedLocaleList.includes(newLocale))
22 | throw new Error(`Unsupported locale "${newLocale}"`);
23 |
24 | locale.value = newLocale;
25 | };
26 |
27 | return { locale: getLocale, setLocale, supportedLocaleList, direction };
28 | };
29 |
30 | export default useLocale;
31 |
--------------------------------------------------------------------------------
/src/components/view/presentation/switch.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Switch Component Presentation
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
37 |
--------------------------------------------------------------------------------
/src/components/common/layout-view/layout-view.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
32 |
--------------------------------------------------------------------------------
/src/assets/scss/components/_side-sheet.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/mixin' as *;
2 |
3 | $side-sheet: (
4 | modal-side-sheet-color: var(--palette-surface-container-low),
5 | );
6 |
7 | .side-sheet-enter-active,
8 | .side-sheet-leave-active {
9 | @at-root [dir='ltr'] & {
10 | transform-origin: right;
11 | }
12 |
13 | @at-root [dir='rtl'] & {
14 | transform-origin: left;
15 | }
16 | }
17 |
18 | .side-sheet-enter-active {
19 | @include transition(standard-decelerate) {
20 | transition-property: transform, opacity;
21 | }
22 | }
23 |
24 | .side-sheet-leave-active {
25 | @include transition(standard-accelerate) {
26 | transition-property: transform, opacity;
27 | }
28 | }
29 |
30 | .side-sheet-leave-to,
31 | .side-sheet-enter-from {
32 | opacity: 0;
33 |
34 | @at-root [dir='ltr'] & {
35 | transform: translateX(10%) scaleX(0);
36 | transform-origin: right;
37 | }
38 |
39 | @at-root [dir='rtl'] & {
40 | transform: translateX(-10%) scaleX(0);
41 | transform-origin: left;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/plug-in/axios/middlewares/on-request/index.js:
--------------------------------------------------------------------------------
1 | import responsibilities from './responsibilities';
2 |
3 | /**
4 | * Response chain of responsibilities
5 | * @param {Object} response - AxiosResponse object
6 | * @param {Object} context
7 | * @param {Object} context.vueInstance - Current vue instance
8 | * @returns {any} - Modified response
9 | */
10 | export const onResponse = (response, context) => {
11 | //TODO: Pass response to each response responsibility to modify (or not)
12 | return responsibilities.getRequestPayload(response, context);
13 | };
14 |
15 | /**
16 | * Error chain of responsibilities
17 | * @param {Object} error - AxiosError object
18 | * @param {Object} context
19 | * @param {Object} context.vueInstance - Current vue instance
20 | * @returns {any} - Modified error
21 | */
22 | export const onResponseError = (error, context) => {
23 | //TODO: Pass error to each error responsibility to modify (or not)
24 | return responsibilities.getErrorPayload(error, context);
25 | };
26 |
27 | export default {
28 | onResponse,
29 | onResponseError,
30 | };
31 |
--------------------------------------------------------------------------------
/src/plug-in/axios/middlewares/on-response/index.js:
--------------------------------------------------------------------------------
1 | import responsibilities from './responsibilities';
2 |
3 | /**
4 | * Response chain of responsibilities
5 | * @param {Object} response - AxiosResponse object
6 | * @param {Object} context
7 | * @param {Object} context.vueInstance - Current vue instance
8 | * @returns {any} - Modified response
9 | */
10 | export const onResponse = (response, context) => {
11 | //TODO: Pass response to each response responsibility to modify (or not)
12 | return responsibilities.getResponsePayload(response, context);
13 | };
14 |
15 | /**
16 | * Error chain of responsibilities
17 | * @param {Object} error - AxiosError object
18 | * @param {Object} context
19 | * @param {Object} context.vueInstance - Current vue instance
20 | * @returns {any} - Modified error
21 | */
22 | export const onResponseError = (error, context) => {
23 | //TODO: Pass error to each error responsibility to modify (or not)
24 | return responsibilities.getErrorPayload(error, context);
25 | };
26 |
27 | export default {
28 | onResponse,
29 | onResponseError,
30 | };
31 |
--------------------------------------------------------------------------------
/src/utils/timeout.util.js:
--------------------------------------------------------------------------------
1 | class Timeout {
2 | #timerId;
3 | #callback;
4 | #startTime;
5 | #remainingTime;
6 | #params;
7 | #delay;
8 |
9 | constructor(callback, delay, ...params) {
10 | this.#callback = callback;
11 | this.#params = params;
12 | this.#delay = delay;
13 | this._startTimer();
14 | }
15 |
16 | _startTimer() {
17 | this.#startTime = Date.now();
18 | this.#remainingTime = this.#delay;
19 | this.#timerId = setTimeout(this.#callback, this.#delay, ...this.#params);
20 | }
21 |
22 | pause() {
23 | if (!this.#timerId) return;
24 |
25 | this.cancel();
26 |
27 | this.#remainingTime -= Date.now() - this.#startTime;
28 | }
29 |
30 | cancel() {
31 | clearTimeout(this.#timerId);
32 | this.#timerId = null;
33 | }
34 |
35 | resume() {
36 | if (this.#timerId) return;
37 |
38 | this.#startTime = Date.now();
39 | this.#timerId = setTimeout(
40 | this.#callback,
41 | this.#remainingTime,
42 | ...this.#params
43 | );
44 | }
45 |
46 | get remainingTime() {
47 | return this.#remainingTime;
48 | }
49 | }
50 |
51 | export default Timeout;
52 |
--------------------------------------------------------------------------------
/src/components/common/picker/date/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
27 |
28 |
48 |
--------------------------------------------------------------------------------
/src/components/view/presentation/radio-input.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
RadioInput Component Presentation
4 |
5 |
6 |
11 |
12 |
18 |
23 |
24 |
25 |
26 |
27 |
41 |
--------------------------------------------------------------------------------
/src/plug-in/toast/components/toast-notification.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ message }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
32 |
33 |
47 |
--------------------------------------------------------------------------------
/src/plug-in/axios/default-config-builder/index.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 |
3 | class DefaultConfigBuilder {
4 | #defaultConfig = {};
5 | /**
6 | *
7 | * @param {Object} config - axios config
8 | */
9 | constructor(config) {
10 | this.#defaultConfig = { ...Axios.defaults, ...config };
11 | }
12 |
13 | /**
14 | * Set common headers like `Cache-Control`
15 | */
16 | #setCommonHeaders() {
17 | this.#defaultConfig.headers.common['Cache-Control'] = 'no-cache';
18 | }
19 |
20 | /**
21 | * Set authorize headers like `token` or something.
22 | */
23 | #setSystemAuthorizeHeaders() {
24 | //TODO: Authorize headers base on project
25 | }
26 |
27 | /**
28 | * Set request timeout duration
29 | */
30 | #setTimeOutDuration() {
31 | const TWO_MINUTES = 2 * 60 * 1000;
32 | this.#defaultConfig.timeout = TWO_MINUTES;
33 | }
34 |
35 | /**
36 | *
37 | * @returns {Object} - default config
38 | */
39 | build() {
40 | this.#setTimeOutDuration();
41 | this.#setCommonHeaders();
42 | this.#setSystemAuthorizeHeaders();
43 |
44 | return this.#defaultConfig;
45 | }
46 | }
47 |
48 | export default DefaultConfigBuilder;
49 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/translate.js:
--------------------------------------------------------------------------------
1 | import { dictionaries } from './dictionary/index.js';
2 | import { doubleBracketReplace } from './utils/double-bracket-replace.util.js';
3 |
4 | /**
5 | *
6 | * @param {string} query - query to find in the dictionary
7 | * @param {Object} options
8 | * @param {string} options.locale - The locale
9 | * @param {Object} options.params - parameters to replace within the translation
10 | * @returns {string} - translation
11 | */
12 | export const translate = (query, { params, locale }) => {
13 | const dictionary = dictionaries[locale];
14 | if (!dictionary) throw new Error(`Unsupported locale "${locale}"`);
15 |
16 | try {
17 | const queryAsList = query.split('.');
18 | const queryTranslation = queryAsList.reduce(
19 | (dictionaryChunk, queryChunk) => {
20 | return dictionaryChunk[queryChunk];
21 | },
22 | dictionary
23 | );
24 |
25 | if (!queryTranslation) {
26 | throw new Error(`Undefined query "${query}"`);
27 | }
28 |
29 | const translation = doubleBracketReplace(queryTranslation, params);
30 | return translation;
31 | } catch (error) {
32 | console.error(error);
33 | }
34 | };
35 |
36 | export default { translate };
37 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_elevation.scss:
--------------------------------------------------------------------------------
1 | $levels: 0, 1, 2, 3, 4, 6, 8, 12, 16, 24;
2 |
3 | $light-elevation-umbra-map: (
4 | 0: 0px 0px 0px 0px,
5 | 1: 0px 2px 1px -1px,
6 | 2: 0px 3px 1px -2px,
7 | 3: 0px 3px 3px -2px,
8 | 4: 0px 2px 4px -1px,
9 | 6: 0px 3px 5px -1px,
10 | 8: 0px 5px 5px -3px,
11 | 12: 0px 7px 8px -4px,
12 | 16: 0px 8px 10px -5px,
13 | 24: 0px 11px 15px -7px,
14 | );
15 |
16 | $light-elevation-penumbra-map: (
17 | 0: 0px 0px 0px 0px,
18 | 1: 0px 1px 1px 0px,
19 | 2: 0px 2px 2px 0px,
20 | 3: 0px 3px 4px 0px,
21 | 4: 0px 4px 5px 0px,
22 | 6: 0px 6px 10px 0px,
23 | 8: 0px 8px 10px 1px,
24 | 12: 0px 12px 17px 2px,
25 | 16: 0px 16px 24px 2px,
26 | 24: 0px 24px 38px 3px,
27 | );
28 |
29 | $light-elevation-ambient-map: (
30 | 0: 0px 0px 0px 0px,
31 | 1: 0px 1px 3px 0px,
32 | 2: 0px 1px 5px 0px,
33 | 3: 0px 1px 8px 0px,
34 | 4: 0px 1px 10px 0px,
35 | 6: 0px 1px 18px 0px,
36 | 8: 0px 3px 14px 2px,
37 | 12: 0px 5px 22px 4px,
38 | 16: 0px 6px 30px 5px,
39 | 24: 0px 9px 46px 8px,
40 | );
41 |
42 | $dark-elevation-opacity-map: (
43 | 0: 0,
44 | 1: 0.05,
45 | 2: 0.07,
46 | 3: 0.08,
47 | 4: 0.09,
48 | 6: 0.11,
49 | 8: 0.12,
50 | 12: 0.14,
51 | 16: 0.15,
52 | 24: 0.16,
53 | );
54 |
--------------------------------------------------------------------------------
/src/components/view/presentation/progress-bar.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Button Component Presentation
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
46 |
--------------------------------------------------------------------------------
/src/composable/use-promise.composable.js:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 |
3 | /**
4 | * @typedef {Object} UsePromiseConfig
5 | * @property {boolean} [throwOnError=false] - Should throw error if happened
6 | */
7 | const defaultConfig = {
8 | throwOnError: false,
9 | };
10 |
11 | /**
12 | * usePromise composable
13 | * @param {Promise} promise - any function that returns a promise
14 | * @param {UsePromiseConfig} config - usePromise options
15 | * @returns {Object} - {data, error, loading, execute}
16 | */
17 | export const usePromise = (promise, { throwOnError } = defaultConfig) => {
18 | const loading = ref(false);
19 | const error = ref(null);
20 | const data = ref(null);
21 |
22 | /**
23 | * Execute promise
24 | * @param {...any} params - params to call promise with them
25 | * @returns
26 | */
27 | const execute = async (...params) => {
28 | if (loading.value) return;
29 |
30 | loading.value = true;
31 | error.value = null;
32 |
33 | try {
34 | data.value = await promise(...params);
35 | return data.value;
36 | } catch (errorDetails) {
37 | error.value = errorDetails;
38 |
39 | if (throwOnError) throw errorDetails;
40 | } finally {
41 | loading.value = false;
42 | }
43 | };
44 |
45 | return { data, error, loading, execute };
46 | };
47 |
48 | export default usePromise;
49 |
--------------------------------------------------------------------------------
/src/components/view/presentation/segmented-button.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Segmented Button Component Presentation
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
42 |
43 |
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 Boilerplate
2 |
3 | This template should help get you started developing with Vue 3 in Vite + Some general requirements.
4 |
5 | ## Extra Features
6 |
7 | - [x] configured file-base structure
8 | - [x] i18n plugin
9 | - [x] event bus
10 | - [x] some useful composable
11 | - [x] layouts and configured router with loading indicator
12 | - [x] general components
13 |
14 | ### Note
15 |
16 | > `utils` folder contains utility functions or helper classes that provide generic functionality not directly tied to specific services or features.
17 |
18 | > `services` folder contains classes or modules that encapsulate functionality related to specific services or features within the application.
19 |
20 | > `plug-in` folder contains self-contained and encapsulated functionalities in a way that a package does or extend Vue core functionalities by adding global methods, components, directives and so on. if a functionality already exists project wide like a util or something just use it to avoid duplication otherwise put it inside the plugin.
21 |
22 | ## [Github Repository](https://github.com/0Amirreza0/vue3-boilerplate)
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 | ### Compile and Minify for Production
37 |
38 | ```sh
39 | npm run build
40 | ```
41 |
42 | ### Lint with [ESLint](https://eslint.org/)
43 |
44 | ```sh
45 | npm run lint
46 | ```
47 |
--------------------------------------------------------------------------------
/src/assets/scss/themes/_dark-theme.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/_colors' as *;
2 |
3 | $dark-theme: (
4 | primary: map-get($primary, 80),
5 | on-primary: map-get($primary, 20),
6 | inverse-primary: map-get($primary, 40),
7 | primary-container: map-get($primary, 30),
8 | on-primary-container: map-get($primary, 90),
9 | secondary: map-get($secondary, 80),
10 | on-secondary: map-get($secondary, 20),
11 | secondary-container: map-get($secondary, 30),
12 | on-secondary-container: map-get($secondary, 90),
13 | tertiary: map-get($tertiary, 80),
14 | on-tertiary: map-get($tertiary, 20),
15 | tertiary-container: map-get($tertiary, 30),
16 | on-tertiary-container: map-get($tertiary, 90),
17 | error: map-get($error, 80),
18 | on-error: map-get($error, 20),
19 | error-container: map-get($error, 30),
20 | on-error-container: map-get($error, 90),
21 | surface-container-lowest: map-get($neutral, 5),
22 | surface-container-low: map-get($neutral, 10),
23 | surface-container: map-get($neutral, 15),
24 | surface-container-high: map-get($neutral, 20),
25 | // deprecated
26 | surface-container-highest: map-get($neutral, 25),
27 | inverse-surface: map-get($neutral, 90),
28 | inverse-on-surface: map-get($neutral, 20),
29 | surface-variant: map-get($neutral-variant, 30),
30 | on-surface-variant: map-get($neutral-variant, 80),
31 | on-surface: map-get($neutral, 90),
32 | outline: map-get($neutral-variant, 60),
33 | outline-variant: map-get($neutral-variant, 30),
34 | );
35 |
--------------------------------------------------------------------------------
/src/components/view/presentation/skeleton.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Skeleton Component Presentation
4 |
5 |
6 |
7 |
14 |
15 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
62 |
--------------------------------------------------------------------------------
/src/assets/scss/components/_date-picker.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/mixin' as *;
2 | @use '../abstracts/variables' as *;
3 |
4 | .calendar-backward-leave-active,
5 | .calendar-backward-enter-active,
6 | .calendar-forward-leave-active,
7 | .calendar-forward-enter-active {
8 | transition: {
9 | property: transform, opacity;
10 | duration: map-get($duration, medium-2);
11 | timing-function: map-get($easing, linear);
12 | }
13 | }
14 |
15 | .calendar-backward-enter-from,
16 | .calendar-forward-leave-to {
17 | position: absolute;
18 | transform: translate(-100%);
19 | opacity: 0;
20 | }
21 |
22 | .calendar-backward-leave-to,
23 | .calendar-forward-enter-from {
24 | position: absolute;
25 | transform: translate(100%);
26 | opacity: 0;
27 | }
28 |
29 | .selected-date-leave-active,
30 | .selected-date-enter-active {
31 | transition: {
32 | property: transform, opacity;
33 | duration: map-get($duration, medium-2);
34 | timing-function: map-get($easing, easeOutExpo);
35 | }
36 | }
37 |
38 | .selected-date-leave-to {
39 | position: absolute;
40 | transform: translateY(100%);
41 | opacity: 0;
42 | }
43 | .selected-date-enter-from {
44 | position: absolute;
45 | transform: translateY(-100%);
46 | opacity: 0;
47 | }
48 |
49 | .year-picker-enter-active {
50 | @include transition(standard-decelerate) {
51 | transition-property: opacity, transform;
52 | }
53 | }
54 |
55 | .year-picker-leave-active {
56 | @include transition(standard-accelerate) {
57 | transition-property: opacity, transform;
58 | }
59 | }
60 |
61 | .year-picker-leave-to,
62 | .year-picker-enter-from {
63 | position: absolute;
64 | transform: translateY(-100%);
65 | opacity: 0;
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/view/presentation/stepper-input.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
StepperInput Component Presentation
4 |
5 |
6 |
Value ~ {{ value }}
7 |
8 | Value must be between -50 and 50 (both included) and step is 10 (between
9 | numbers are valid)
10 |
11 |
12 |
13 | Also nowidthis set to the
14 | stepper
15 |
16 |
17 |
18 |
19 |
26 |
33 |
34 |
35 |
36 |
37 |
42 |
43 |
67 |
--------------------------------------------------------------------------------
/src/components/common/router/indicator.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
38 |
39 |
66 |
--------------------------------------------------------------------------------
/src/components/common/scrim/container.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
65 |
--------------------------------------------------------------------------------
/src/components/common/sheet/side/modal.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
15 |
44 |
45 |
73 |
--------------------------------------------------------------------------------
/src/interfaces/calendar/calendar.interface.js:
--------------------------------------------------------------------------------
1 | class CalendarInterface {
2 | /**
3 | * Determine if a given year is a leap year.
4 | * @param {number} year - The year.
5 | * @returns {boolean} True if the year is a leap year, false otherwise.
6 | */
7 | isLeapYear(year) {
8 | throw new Error('Must implement isLeapYear method');
9 | }
10 |
11 | /**
12 | * Calculate the number of days in a given month and year.
13 | * @param {number} year - The year.
14 | * @param {number} month - The month (1-12).
15 | * @returns {number} The number of days in the month.
16 | * @throws {Error} If the month is not between 1 and 12.
17 | */
18 | getDaysInMonth(year, month) {
19 | throw new Error('Must implement getDaysInMonthBy method');
20 | }
21 |
22 | /**
23 | * Calculate the day of the given date using Zeller's Congruence.
24 | * @param {number} year - The year.
25 | * @param {number} month - The month (1-12).
26 | * @param {number} [day=1] - The day (1-31).
27 | * @returns {number} The day of the week (0-6) where 0 is Sunday, 1 is Monday, etc.
28 | * @throws {Error} If the month is not between 1 and 12.
29 | */
30 | getDayOfWeek(year, month, day) {
31 | throw new Error('Must implement getFirstDayOfMonth method');
32 | }
33 |
34 | /**
35 | * Get today's date in the Gregorian calendar.
36 | * @returns {Object} Today's date in the format '{year, month, date}'.
37 | */
38 | get todayAsObject() {
39 | throw new Error('Must implement getToday method');
40 | }
41 |
42 | /**
43 | * Get month list for gregorian calendar.
44 | * @returns {Array.} Month list ['January', ... , 'December'].
45 | */
46 | get monthList() {
47 | throw new Error('Must monthList getter');
48 | }
49 |
50 | /**
51 | * Get week days list.
52 | * @returns {Array.} Week days list ['Sunday', ... , 'Saturday'].
53 | */
54 | get weekDayList() {
55 | throw new Error('Must weekDayList getter');
56 | }
57 | }
58 |
59 | export default CalendarInterface;
60 |
--------------------------------------------------------------------------------
/src/plug-in/i18n/composable/use-translate.js:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue';
2 |
3 | import { translate } from '../translate.js';
4 | import useLocale from './use-locale.js';
5 |
6 | const DEFAULT_CONFIG = Object.freeze({
7 | asComputed: true,
8 | });
9 |
10 | const useTranslate = (defaultConfig = DEFAULT_CONFIG) => {
11 | const { locale } = useLocale();
12 |
13 | /**
14 | * Decide if it's a single query or an array of queries and call translate function
15 | * @param {(string|string[])} queries
16 | * @param {Object} options
17 | * @param {string} [options.locale=locale.value]
18 | * @param {Object} options.params - parameters to replace within the translation
19 | * @returns {(string|Object)} - object contains query string and translations as values
20 | */
21 | const translateDirector = (queries, { locale, params }) => {
22 | if (typeof queries === 'string') {
23 | return translate(queries, { locale, params });
24 | }
25 |
26 | if (Array.isArray(queries)) {
27 | return queryList.reduce((dictionary, query) => {
28 | dictionary[query] = translate(query, { locale });
29 | return dictionary;
30 | }, {});
31 | }
32 | };
33 |
34 | /**
35 | * Apply configuration and call the translate director for translation
36 | * @param {(string|string[])} queries
37 | * @param {Object} params - parameters to replace within the translation
38 | * @param {Object} [config=defaultConfig]
39 | * @returns {(string|Object|import('vue').ComputedRef)} - object contains query string and translations as values
40 | */
41 | const translator = (queries, params, config = defaultConfig) => {
42 | const { asComputed } = config;
43 |
44 | if (asComputed) {
45 | return computed(() =>
46 | translateDirector(queries, { params, locale: locale.value })
47 | );
48 | }
49 |
50 | return translateDirector(queries, { params, locale: locale.value });
51 | };
52 |
53 | return { t: translator };
54 | };
55 |
56 | export default useTranslate;
57 |
--------------------------------------------------------------------------------
/src/plug-in/vee-validate/composable/use-input.composable.js:
--------------------------------------------------------------------------------
1 | import { computed, toValue, watch, triggerRef } from 'vue';
2 | import { useField } from 'vee-validate';
3 |
4 | import Mask from '@/services/mask';
5 | import { useTranslate } from '@/plug-in/i18n';
6 |
7 | const { t: translator } = useTranslate({ asComputed: false });
8 |
9 | const DEFAULT_OPTIONS = Object.freeze({
10 | validationConfig: {
11 | validateOnMount: false,
12 | },
13 | errorAsCode: false,
14 | maskPattern: '',
15 | });
16 |
17 | const useInput = (name, rules, { strictRules = [], options = {} } = {}) => {
18 | const { errorAsCode, validationConfig, maskPattern, syncValue } = {
19 | ...DEFAULT_OPTIONS,
20 | ...options,
21 | };
22 |
23 | const { value: fieldValue, errors: fieldErrors } = useField(name, rules, {
24 | ...DEFAULT_OPTIONS.validationConfig,
25 | ...(validationConfig ?? {}),
26 | });
27 |
28 | const mask = new Mask(maskPattern);
29 | const value = computed({
30 | get: () => mask.toMasked(fieldValue.value),
31 | set: (newValue) => {
32 | const unMaskedValue = mask.toUnMasked(newValue);
33 | const strictValue = strictRules.reduce(
34 | (strictValue, rule) => rule(strictValue),
35 | unMaskedValue
36 | );
37 |
38 | fieldValue.value = strictValue;
39 |
40 | const hasValueChanged = unMaskedValue !== strictValue;
41 | if (hasValueChanged) triggerRef(value);
42 | },
43 | });
44 |
45 | if (syncValue) {
46 | watch(
47 | syncValue,
48 | () => {
49 | value.value = toValue(syncValue);
50 | },
51 | {
52 | deep: true,
53 | immediate: true,
54 | }
55 | );
56 | }
57 |
58 | const errors = computed(() => {
59 | if (errorAsCode) {
60 | return fieldErrors.value;
61 | }
62 |
63 | return fieldErrors.value.map(({ name, params }) =>
64 | translator(`validationErrors.${name}`, params)
65 | );
66 | });
67 |
68 | const errorMessage = computed(() => errors.value[0]);
69 |
70 | return { value, fieldValue, errors, errorMessage };
71 | };
72 |
73 | export default useInput;
74 |
--------------------------------------------------------------------------------
/src/assets/scss/themes/_light-theme.scss:
--------------------------------------------------------------------------------
1 | @use '../abstracts/_colors' as *;
2 |
3 | // NOTE: Deprecation flag
4 | // https://material.io/blog/tone-based-surface-color-m3
5 | // ----------------------------------------------------
6 | // Surface Container Lowest is a new role
7 | // Surface at elevation +1 becomes Surface Container Low
8 | // Surface at elevation +2 becomes Surface Container
9 | // Surface at elevation +3 becomes Surface Container High
10 | // Surface at elevation +4 and +5 are being deprecated, it is recommended to use Surface Container Highest by default as a replacement. As an alternative Surface Container High, or Surface Dim can be used depending on the specific use case.
11 | // Surface Variant becomes Surface Container Highest
12 |
13 | $light-theme: (
14 | primary: map-get($primary, 40),
15 | on-primary: map-get($primary, 100),
16 | inverse-primary: map-get($primary, 80),
17 | primary-container: map-get($primary, 90),
18 | on-primary-container: map-get($primary, 10),
19 | secondary: map-get($secondary, 40),
20 | on-secondary: map-get($secondary, 100),
21 | secondary-container: map-get($secondary, 90),
22 | on-secondary-container: map-get($secondary, 10),
23 | tertiary: map-get($tertiary, 40),
24 | on-tertiary: map-get($tertiary, 100),
25 | tertiary-container: map-get($tertiary, 90),
26 | on-tertiary-container: map-get($tertiary, 10),
27 | error: map-get($error, 40),
28 | on-error: map-get($error, 100),
29 | error-container: map-get($error, 90),
30 | on-error-container: map-get($error, 10),
31 | surface-container-lowest: map-get($neutral, 100),
32 | surface-container-low: map-get($neutral, 98),
33 | surface-container: map-get($neutral-variant, 95),
34 | surface-container-high: map-get($neutral-variant, 90),
35 | surface-container-highest: map-get($neutral-variant, 90),
36 | inverse-surface: map-get($neutral, 20),
37 | inverse-on-surface: map-get($neutral, 95),
38 | surface-variant: map-get($neutral-variant, 90),
39 | on-surface-variant: map-get($neutral-variant, 30),
40 | on-surface: map-get($neutral, 10),
41 | outline: map-get($neutral-variant, 50),
42 | outline-variant: map-get($neutral-variant, 80),
43 | );
44 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | const componentSourceMap = import.meta.globEager(
2 | ['@/components/common/**/*.component.vue'],
3 | { import: 'default' }
4 | );
5 |
6 | const componentSourceList = Object.keys(componentSourceMap);
7 |
8 | /**
9 | * Determine the component name by it's source path
10 | * @param {string} source
11 | * @param {Object} config
12 | * @param {Object} [config.direction="backward"] - (forward) root-(nestedDirectory)*-fileName | (backward) fileName-(nestedDirectory)*-root
13 | * @returns {string} - component name from source using direction
14 | */
15 | const getComponentNameByPath = (source, { direction = 'backward' }) => {
16 | const sourceSegments = source.split('/');
17 | const fileName = sourceSegments.pop().replace(/\.(.*)/, '');
18 |
19 | const isBackward = direction === 'backward';
20 |
21 | if (isBackward) {
22 | sourceSegments.reverse();
23 | }
24 |
25 | const kebabCaseDirectories = sourceSegments.join('-');
26 |
27 | if (kebabCaseDirectories === fileName) {
28 | return fileName;
29 | }
30 |
31 | const prefix = isBackward ? `${fileName}-` : '';
32 | const postfix = !isBackward ? `-${fileName}` : '';
33 |
34 | const componentName = `${prefix}${kebabCaseDirectories}${postfix}`;
35 | return componentName;
36 | };
37 |
38 | /**
39 | * Register all the components inside the src/components/common/* no mater how deep if they follow `*.component.vue`
40 | * @param {Object} vueInstance - the app created by createVueApp
41 | * @param {Array.} [reversedNamePaths] - a path nested in /common/ to reverse travel the path
42 | */
43 | export const registerCommonComponents = (
44 | vueInstance,
45 | reversedNamePaths = []
46 | ) => {
47 | componentSourceList.forEach((sourceFromRoot) => {
48 | const source = sourceFromRoot.split('/common/')[1];
49 | const component = componentSourceMap[sourceFromRoot];
50 |
51 | const shouldReverseName = reversedNamePaths.some((path) =>
52 | source.startsWith(path)
53 | );
54 |
55 | const direction = shouldReverseName ? 'forward' : 'backward';
56 | const componentName = getComponentNameByPath(source, { direction });
57 |
58 | vueInstance.component(componentName, component);
59 | });
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/common/picker/date/control-menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
58 |
59 |
82 |
--------------------------------------------------------------------------------
/src/components/common/skeleton/base.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
57 |
58 |
99 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_colors.scss:
--------------------------------------------------------------------------------
1 | $primary: (
2 | 0: #000000,
3 | 10: #022100,
4 | 20: #053900,
5 | 25: #0a4601,
6 | 30: #19520d,
7 | 35: #265e19,
8 | 40: #326b24,
9 | 50: #4a843a,
10 | 60: #649f51,
11 | 70: #7dbb69,
12 | 80: #98d782,
13 | 90: #b3f39c,
14 | 95: #cbffb6,
15 | 98: #edffe1,
16 | 99: #f7ffee,
17 | 100: #ffffff,
18 | );
19 |
20 | $secondary: (
21 | 0: #000000,
22 | 10: #121f0e,
23 | 20: #273421,
24 | 25: #323f2c,
25 | 30: #3d4b37,
26 | 35: #485642,
27 | 40: #54624d,
28 | 50: #6c7b65,
29 | 60: #86957d,
30 | 70: #a0b097,
31 | 80: #bbcbb1,
32 | 90: #d7e8cc,
33 | 95: #e6f6da,
34 | 98: #eeffe2,
35 | 99: #f7ffee,
36 | 100: #ffffff,
37 | );
38 |
39 | $tertiary: (
40 | 0: #000000,
41 | 10: #002021,
42 | 20: #003739,
43 | 25: #0f4244,
44 | 30: #1e4e50,
45 | 35: #2b595c,
46 | 40: #386668,
47 | 50: #517f81,
48 | 60: #6b999b,
49 | 70: #85b3b6,
50 | 80: #a0cfd1,
51 | 90: #bcebee,
52 | 95: #cafafc,
53 | 98: #e6feff,
54 | 99: #f2ffff,
55 | 100: #ffffff,
56 | );
57 |
58 | $error: (
59 | 0: #000000,
60 | 10: #410002,
61 | 20: #690005,
62 | 25: #7e0007,
63 | 30: #93000a,
64 | 35: #a80710,
65 | 40: #ba1a1a,
66 | 50: #de3730,
67 | 60: #ff5449,
68 | 70: #ff897d,
69 | 80: #ffb4ab,
70 | 90: #ffdad6,
71 | 95: #ffedea,
72 | 98: #fff8f7,
73 | 99: #fffbff,
74 | 100: #ffffff,
75 | );
76 |
77 | $neutral: (
78 | 0: #000000,
79 | 5: #10110e,
80 | 10: #1a1c18,
81 | 15: #252623,
82 | 20: #2f312d,
83 | 25: #3a3c38,
84 | 30: #454743,
85 | 35: #51534e,
86 | 40: #5d5f5a,
87 | 50: #767872,
88 | 60: #90918c,
89 | 70: #abaca6,
90 | 80: #c6c7c1,
91 | 90: #e2e3dc,
92 | 95: #f1f1ea,
93 | 98: #fafaf3,
94 | 99: #fcfdf6,
95 | 100: #ffffff,
96 | );
97 |
98 | $neutral-variant: (
99 | 0: #000000,
100 | 5: #0e120c,
101 | 10: #181d15,
102 | 15: #222720,
103 | 20: #2c3229,
104 | 25: #373d34,
105 | 30: #43483f,
106 | 35: #4e544a,
107 | 40: #5a6056,
108 | 50: #73796e,
109 | 60: #8d9387,
110 | 70: #a7ada1,
111 | 80: #c3c8bc,
112 | 90: #dfe4d7,
113 | 95: #edf3e5,
114 | 98: #f6fbee,
115 | 99: #f9fef1,
116 | 100: #ffffff,
117 | );
118 |
--------------------------------------------------------------------------------
/src/components/common/snack-bar/snack-bar.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ supportingText }}
4 |
10 | {{ actionLabel }}
11 |
12 |
19 |
20 |
21 |
22 |
44 |
45 |
91 |
--------------------------------------------------------------------------------
/src/components/common/icon/base.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
68 |
69 |
90 |
--------------------------------------------------------------------------------
/src/plug-in/toast/components/toast-manager.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
71 |
72 |
104 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_typography.scss:
--------------------------------------------------------------------------------
1 | $typography: (
2 | default: (
3 | display-large: (
4 | font-size: 3.56rem,
5 | font-weight: 400,
6 | line-height: 4rem,
7 | letter-spacing: -0.015rem,
8 | ),
9 | display-medium: (
10 | font-size: 2.8rem,
11 | font-weight: 400,
12 | line-height: 3.25rem,
13 | letter-spacing: 0,
14 | ),
15 | display-small: (
16 | font-size: 2.25rem,
17 | font-weight: 400,
18 | line-height: 2.75rem,
19 | letter-spacing: 0,
20 | ),
21 | headline-large: (
22 | font-size: 2rem,
23 | font-weight: 400,
24 | line-height: 2.5rem,
25 | letter-spacing: 0,
26 | ),
27 | headline-medium: (
28 | font-size: 1.75rem,
29 | font-weight: 400,
30 | line-height: 2.25rem,
31 | letter-spacing: 0,
32 | ),
33 | headline-small: (
34 | font-size: 1.5rem,
35 | font-weight: 400,
36 | line-height: 2rem,
37 | letter-spacing: 0,
38 | ),
39 | title-large: (
40 | font-size: 1.375rem,
41 | font-weight: 400,
42 | line-height: 1.75rem,
43 | letter-spacing: 0,
44 | ),
45 | title-medium: (
46 | font-size: 1rem,
47 | font-weight: 400,
48 | line-height: 1.5rem,
49 | letter-spacing: 0.009rem,
50 | ),
51 | title-small: (
52 | font-size: 0.875rem,
53 | font-weight: 500,
54 | line-height: 1.25rem,
55 | letter-spacing: 0,
56 | ),
57 | label-large: (
58 | font-size: 0.875rem,
59 | font-weight: 500,
60 | line-height: 1.25rem,
61 | letter-spacing: 0.00625rem,
62 | ),
63 | label-medium: (
64 | font-size: 0.75rem,
65 | font-weight: 400,
66 | line-height: 1rem,
67 | letter-spacing: 0.03125rem,
68 | ),
69 | label-small: (
70 | font-size: 0.6875rem,
71 | font-weight: 500,
72 | line-height: 1rem,
73 | letter-spacing: 0.03125rem,
74 | ),
75 | label-xsmall: (
76 | font-size: 0.5rem,
77 | font-weight: 500,
78 | line-height: 0.625rem,
79 | letter-spacing: 0.03125rem,
80 | ),
81 | body-large: (
82 | font-size: 1rem,
83 | font-weight: 400,
84 | line-height: 1.5rem,
85 | letter-spacing: 0.03125rem,
86 | ),
87 | body-medium: (
88 | font-size: 0.875rem,
89 | font-weight: 400,
90 | line-height: 1.25rem,
91 | letter-spacing: 0.015625rem,
92 | ),
93 | body-small: (
94 | font-size: 0.75rem,
95 | font-weight: 400,
96 | line-height: 1rem,
97 | letter-spacing: 0.025rem,
98 | ),
99 | ),
100 | );
101 |
--------------------------------------------------------------------------------
/src/views/presentation/presentation-view.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Presentation
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
33 |
34 |
53 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_variables.scss:
--------------------------------------------------------------------------------
1 | $base-space: 4px;
2 |
3 | $elevation-base-color: (
4 | light: #000000,
5 | dark: #000000,
6 | );
7 |
8 | $easing: (
9 | emphasized: cubic-bezier(0.2, 0, 0, 1),
10 | emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1),
11 | emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15),
12 | standard: cubic-bezier(0.2, 0, 0, 1),
13 | standard-decelerate: cubic-bezier(0, 0, 0, 1),
14 | standard-accelerate: cubic-bezier(0.3, 0, 1, 1),
15 | ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1),
16 | ease-out: ease-out,
17 | linear: linear,
18 | easeInSine: cubic-bezier(0.12, 0, 0.39, 0),
19 | easeOutSine: cubic-bezier(0.61, 1, 0.88, 1),
20 | easeInOutSine: cubic-bezier(0.37, 0, 0.63, 1),
21 | easeInQuad: cubic-bezier(0.11, 0, 0.5, 0),
22 | easeOutQuad: cubic-bezier(0.5, 1, 0.89, 1),
23 | easeInOutQuad: cubic-bezier(0.45, 0, 0.55, 1),
24 | easeInCubic: cubic-bezier(0.32, 0, 0.67, 0),
25 | easeOutCubic: cubic-bezier(0.33, 1, 0.68, 1),
26 | easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1),
27 | easeInQuart: cubic-bezier(0.5, 0, 0.75, 0),
28 | easeOutQuart: cubic-bezier(0.25, 1, 0.5, 1),
29 | easeInOutQuart: cubic-bezier(0.76, 0, 0.24, 1),
30 | easeInQuint: cubic-bezier(0.64, 0, 0.78, 0),
31 | easeOutQuint: cubic-bezier(0.22, 1, 0.36, 1),
32 | easeInOutQuint: cubic-bezier(0.83, 0, 0.17, 1),
33 | easeInExpo: cubic-bezier(0.7, 0, 0.84, 0),
34 | easeOutExpo: cubic-bezier(0.16, 1, 0.3, 1),
35 | easeInOutExpo: cubic-bezier(0.87, 0, 0.13, 1),
36 | easeInCirc: cubic-bezier(0.55, 0, 1, 0.45),
37 | easeOutCirc: cubic-bezier(0, 0.55, 0.45, 1),
38 | easeInOutCirc: cubic-bezier(0.85, 0, 0.15, 1),
39 | easeInBack: cubic-bezier(0.36, 0, 0.66, -0.56),
40 | easeOutBack: cubic-bezier(0.34, 1.56, 0.64, 1),
41 | easeInOutBack: cubic-bezier(0.68, -0.6, 0.32, 1.6),
42 | );
43 |
44 | $duration: (
45 | short-1: 50ms,
46 | short-2: 100ms,
47 | short-3: 150ms,
48 | short-4: 200ms,
49 | medium-1: 250ms,
50 | medium-2: 300ms,
51 | medium-3: 350ms,
52 | medium-4: 400ms,
53 | long-1: 450ms,
54 | long-2: 500ms,
55 | long-3: 550ms,
56 | long-4: 600ms,
57 | extra-long-1: 700ms,
58 | extra-long-2: 800ms,
59 | extra-long-3: 900ms,
60 | extra-long-4: 1000ms,
61 | );
62 |
63 | $easing-duration-pair: (
64 | // Exit the screen temporarily
65 | emphasized: map-get($duration, 'long-2'),
66 | // Enter the screen
67 | emphasized-decelerate: map-get($duration, 'medium-4'),
68 | // Exit the screen permanently
69 | emphasized-accelerate: map-get($duration, 'short-4'),
70 | // Exit the screen temporarily
71 | standard: map-get($duration, 'medium-2'),
72 | // Enter the screen
73 | standard-decelerate: map-get($duration, 'medium-1'),
74 | // Exit the screen permanently
75 | standard-accelerate: map-get($duration, 'short-4')
76 | );
77 |
--------------------------------------------------------------------------------
/src/components/view/presentation/icon-button.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Icon Button Component Presentation
4 |
5 |
6 |
7 |
8 |
14 |
20 |
21 |
22 |
23 |
28 |
33 |
39 |
45 |
46 |
47 |
48 |
53 |
58 |
64 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
95 |
--------------------------------------------------------------------------------
/src/components/view/presentation/filter-chip.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Filter Chip Component Presentation
4 |
5 |
6 |
7 |
14 |
21 |
28 |
34 |
35 |
36 |
41 |
47 |
55 |
63 |
70 |
71 |
72 |
73 |
74 |
79 |
80 |
98 |
--------------------------------------------------------------------------------
/src/plug-in/toast/toast-manager.js:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue';
2 | import Timeout from '@/utils/timeout.util.js';
3 | import {
4 | TOAST_POSITIONS,
5 | DEFAULT_CONFIG,
6 | STACK_MAP,
7 | } from './constants/toast.constant.js';
8 |
9 | export const stacks = reactive(STACK_MAP);
10 |
11 | class ToastManager {
12 | #config;
13 | #toastsTimer = {};
14 | #reserveStacks = TOAST_POSITIONS.reduce((stackMap, position) => {
15 | stackMap[position] = [];
16 | return stackMap;
17 | }, {});
18 |
19 | constructor(config = {}) {
20 | this.#config = { ...DEFAULT_CONFIG, ...config };
21 | }
22 |
23 | #getToastConfig(toastConfig) {
24 | const config = { ...this.#config, ...toastConfig };
25 |
26 | return {
27 | duration: config.duration,
28 | position: config.position,
29 | type: config.type,
30 | autoDismiss: config.autoDismiss,
31 | };
32 | }
33 |
34 | #checkReserveStack({ position }) {
35 | const reserveStack = this.#reserveStacks[position];
36 | const hasFreeSpace = stacks[position].length < this.#config.stackMaxToast;
37 |
38 | if (!hasFreeSpace || !reserveStack.length) return;
39 |
40 | const reservedToast = reserveStack.pop();
41 | this.show(reservedToast.props, reservedToast.toastConfig);
42 | }
43 |
44 | dismiss(toast) {
45 | const stack = stacks[toast.position];
46 |
47 | const toastIndex = stack.findIndex(({ id }) => toast.id === id);
48 | stack.splice(toastIndex, 1);
49 |
50 | this.#toastsTimer[toast.id]?.cancel();
51 | this.#checkReserveStack(toast);
52 | }
53 |
54 | pauseTimer(toastId) {
55 | this.#toastsTimer[toastId]?.pause();
56 | }
57 |
58 | resumeTimer(toastId) {
59 | this.#toastsTimer[toastId]?.resume();
60 | }
61 |
62 | show(props, toastConfig = {}) {
63 | const config = this.#getToastConfig(toastConfig);
64 | const { duration, position, autoDismiss } = config;
65 |
66 | const stackItemCount = stacks[position].length;
67 | if (stackItemCount >= this.#config.stackMaxToast) {
68 | this.#reserveStacks[position].unshift({ toastConfig, props });
69 | return;
70 | }
71 |
72 | if (!TOAST_POSITIONS.includes(position))
73 | throw new Error(`Unsupported toast position: ${position}`);
74 |
75 | const id = Symbol();
76 |
77 | if (autoDismiss) {
78 | const timeout = new Timeout(() => this.dismiss(toast), duration);
79 | this.#toastsTimer[id] = timeout;
80 | }
81 |
82 | const timer = this.#toastsTimer[id];
83 |
84 | const toast = {
85 | id,
86 | props,
87 | ...config,
88 | get remainingTime() {
89 | return timer?.remainingTime;
90 | },
91 | };
92 |
93 | const addStrategy = position.includes('top') ? 'push' : 'unshift';
94 | stacks[position][addStrategy](toast);
95 |
96 | return toast;
97 | }
98 | }
99 |
100 | export default ToastManager;
101 |
--------------------------------------------------------------------------------
/src/components/common/picker/date/year-picker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
8 |
12 | {{ yearRange[0] + index }}
13 |
14 |
15 |
16 |
17 |
18 |
78 |
79 |
123 |
--------------------------------------------------------------------------------
/src/components/common/sheet/side/standard.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
22 |
23 |
24 |
25 |
61 |
62 |
130 |
--------------------------------------------------------------------------------
/src/components/view/presentation/button.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Button Component Presentation
4 |
5 |
6 | No Icon
7 | Leading
8 | Trailing
9 |
10 | Both Icons
11 |
12 |
13 |
14 |
15 |
20 | No Icon
21 |
22 |
23 | Leading
24 |
25 |
26 | Trailing
27 |
28 |
33 | Both Icons
34 |
35 |
36 |
37 |
38 | No Icon
39 |
40 | Leading
41 |
42 |
43 | Trailing
44 |
45 |
50 | Both Icons
51 |
52 |
53 |
54 |
55 | No Icon
56 |
57 | Leading
58 |
59 |
60 | Trailing
61 |
62 |
67 | Both Icons
68 |
69 |
70 |
71 |
72 | No Icon
73 |
74 | Leading
75 |
76 |
77 | Trailing
78 |
79 |
84 | Both Icons
85 |
86 |
87 |
88 |
89 |
90 |
95 |
96 |
110 |
--------------------------------------------------------------------------------
/src/components/common/input/radio.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | {{ labelText }}
9 |
10 |
11 |
12 |
13 |
14 |
35 |
36 |
141 |
--------------------------------------------------------------------------------
/src/services/calendar/gregorian/gregorian-calendar.service.js:
--------------------------------------------------------------------------------
1 | import CalendarInterface from '@/interfaces/calendar/calendar.interface.js';
2 |
3 | class GregorianCalendar extends CalendarInterface {
4 | #todayDate;
5 | #monthList;
6 | #weekDayList;
7 |
8 | constructor({ monthList, weekDayList }) {
9 | super();
10 |
11 | const today = new Date();
12 | const year = today.getFullYear();
13 | const month = today.getMonth() + 1;
14 | const day = today.getDate();
15 |
16 | this.#todayDate = { year, month, day };
17 | this.#monthList = monthList;
18 | this.#weekDayList = weekDayList;
19 | }
20 |
21 | /**
22 | * Determine if a given year is a leap year.
23 | * @param {number} year - The year.
24 | * @returns {boolean} True if the year is a leap year, false otherwise.
25 | */
26 | isLeapYear(year) {
27 | if (year % 4 === 0) {
28 | if (year % 100 === 0) {
29 | return year % 400 === 0;
30 | }
31 |
32 | return true;
33 | }
34 |
35 | return false;
36 | }
37 |
38 | /**
39 | * Calculate the number of days in a given month and year.
40 | * @param {number} year - The year.
41 | * @param {number} month - The month (1-12).
42 | * @returns {number} The number of days in the month.
43 | * @throws {Error} If the month is not between 1 and 12.
44 | */
45 | getDaysInMonth(year, month) {
46 | if (month < 1 || month > 12) {
47 | throw new Error('Month must be between 1 and 12.');
48 | }
49 |
50 | const daysInMonthMap = [31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
51 |
52 | if (month === 2) {
53 | return this.isLeapYear(year) ? 29 : 28;
54 | }
55 |
56 | return daysInMonthMap[month - 1];
57 | }
58 |
59 | /**
60 | * Calculate the day of the given date using Zeller's Congruence.
61 | * @param {number} year - The year.
62 | * @param {number} month - The month (1-12).
63 | * @param {number} [day=1] - The day (1-31).
64 | * @returns {number} The day of the week (0-6) where 0 is Sunday, 1 is Monday, etc.
65 | * @throws {Error} If the month is not between 1 and 12.
66 | */
67 | getDayOfWeek(year, month, day = 1) {
68 | if (month < 1 || month > 12) {
69 | throw new Error('Month must be between 1 and 12.');
70 | }
71 |
72 | if (month < 3) {
73 | month += 12;
74 | year -= 1;
75 | }
76 |
77 | const adjustedMonth = month;
78 | const yearOfCentury = year % 100;
79 | const zeroBasedCentury = Math.floor(year / 100);
80 |
81 | const dayOfWeekIndex =
82 | (day +
83 | Math.floor((13 * (adjustedMonth + 1)) / 5) +
84 | yearOfCentury +
85 | Math.floor(yearOfCentury / 4) +
86 | Math.floor(zeroBasedCentury / 4) +
87 | 5 * zeroBasedCentury) %
88 | 7;
89 |
90 | const dayOfWeek = (dayOfWeekIndex + 6) % 7;
91 |
92 | return dayOfWeek;
93 | }
94 |
95 | /**
96 | * Get today's date in the Gregorian calendar.
97 | * @returns {Object} Today's date in the format '{year, month, date}'.
98 | */
99 | get todayAsObject() {
100 | return this.#todayDate;
101 | }
102 |
103 | /**
104 | * Get month list for gregorian calendar.
105 | * @returns {Array.} Month list ['January', ... , 'December'].
106 | */
107 | get monthList() {
108 | return this.#monthList;
109 | }
110 |
111 | /**
112 | * Get week days list.
113 | * @returns {Array.} Week days list ['Sunday', ... , 'Saturday'].
114 | */
115 | get weekDayList() {
116 | return this.#weekDayList;
117 | }
118 | }
119 |
120 | export default GregorianCalendar;
121 |
--------------------------------------------------------------------------------
/src/assets/scss/abstracts/_mixin.scss:
--------------------------------------------------------------------------------
1 | @use './typography' as *;
2 | @use './breakpoints' as *;
3 | @use './elevation' as *;
4 | @use './variables' as *;
5 |
6 | @mixin media-query($screen-size) {
7 | $size: map-get($breakpoints, $screen-size);
8 |
9 | @if $size {
10 | // Desktop first approach
11 | // TODO: Implement configurable approach
12 | @media only screen and (max-width: $size) {
13 | @content;
14 | }
15 | }
16 | }
17 |
18 | @mixin typography($name) {
19 | $target: map-get($typography, default);
20 | $level: map-get($target, $name);
21 |
22 | font-size: map-get($level, font-size);
23 | font-weight: map-get($level, font-weight);
24 | line-height: map-get($level, line-height);
25 | letter-spacing: map-get($level, letter-spacing);
26 |
27 | @each $point, $value in $breakpoints {
28 | $target: map-get($typography, $point);
29 |
30 | @if $target {
31 | @include media-query($point) {
32 | $level: map-get($target, $name);
33 |
34 | @if $level {
35 | font-size: map-get($level, font-size);
36 | font-weight: map-get($level, font-weight);
37 | line-height: map-get($level, line-height);
38 | letter-spacing: map-get($level, letter-spacing);
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
45 | @mixin flex(
46 | $dir: row,
47 | $wrap: nowrap,
48 | $align: flex-start,
49 | $justify: flex-start
50 | ) {
51 | display: flex;
52 | flex-direction: $dir;
53 | flex-wrap: $wrap;
54 | align-items: $align;
55 | justify-content: $justify;
56 | }
57 |
58 | @mixin shadow-elevation(
59 | $level,
60 | $base-color: map-get($elevation-base-color, light)
61 | ) {
62 | @if ($level == 0) {
63 | box-shadow: none;
64 | } @else {
65 | $umbra: rgba($base-color, 0.2);
66 | $penumbra: rgba($base-color, 0.14);
67 | $ambient: rgba($base-color, 0.12);
68 |
69 | box-shadow: map-get($light-elevation-umbra-map, $level) $umbra,
70 | map-get($light-elevation-penumbra-map, $level) $penumbra,
71 | map-get($light-elevation-ambient-map, $level) $ambient;
72 | }
73 | }
74 |
75 | @mixin elevation(
76 | $level: 1,
77 | $base-light-color: map-get($elevation-base-color, light),
78 | $base-dark-color: map-get($elevation-base-color, dark)
79 | ) {
80 | @at-root [data-theme='light'] & {
81 | @include shadow-elevation($level, $base-light-color);
82 | }
83 |
84 | @at-root [data-theme='dark'] & {
85 | @include shadow-elevation($level, $base-dark-color);
86 | }
87 | }
88 |
89 | @mixin transition($easing-token: standard) {
90 | transition-duration: map-get($easing-duration-pair, $easing-token);
91 | transition-timing-function: map-get($easing, $easing-token);
92 | @content;
93 | }
94 |
95 | @mixin state-layer(
96 | $hover-color: transparent,
97 | $active-color: '',
98 | $events: false,
99 | $element: false
100 | ) {
101 | @if ($active-color == '') {
102 | $active-color: $hover-color;
103 | }
104 |
105 | @if $element == true {
106 | &::before {
107 | content: '';
108 | position: absolute;
109 | width: 100%;
110 | height: 100%;
111 | top: 0;
112 | left: 0;
113 | opacity: 0;
114 |
115 | transition: {
116 | property: background-color, opacity;
117 | duration: map-get($duration, short-3);
118 | timing-function: map-get($easing, linear);
119 | }
120 | }
121 | }
122 |
123 | @if $events == true {
124 | &:hover::before {
125 | opacity: 0.08;
126 | background-color: $hover-color;
127 | }
128 |
129 | &:active::before {
130 | opacity: 0.1;
131 | background-color: $active-color;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/services/event-bus/event-bus.service.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @callback SubscriptionCallback
3 | * @param {...*} args
4 | */
5 |
6 | /**
7 | * @typedef {Object} Subscriber
8 | * @property {string} id
9 | * @property {?SubscriptionCallback} callback
10 | * @property {?Function} unsubscribe
11 | */
12 |
13 | /**
14 | * @typedef {Object} GetSubscriberMapOptions
15 | * @param {boolean} [createIfAbsent=false]
16 | */
17 |
18 | import deepClone from '@/utils/deep-clone.util.js';
19 |
20 | class EventBus {
21 | #eventsSubscriberMap;
22 |
23 | /**
24 | * Create a new EventBus
25 | */
26 | constructor() {
27 | this.#eventsSubscriberMap = {};
28 | }
29 |
30 | /**
31 | * @param {string} eventName
32 | * @param { GetSubscriberMapOptions } options
33 | * @returns {?Object.} Subscriber map for the event
34 | */
35 | _getEventSubscriberMap(eventName, { createIfAbsent = false } = {}) {
36 | if (!eventName) throw new Error('eventName is required');
37 |
38 | const subscriptionMap = this.#eventsSubscriberMap[eventName];
39 |
40 | if (!subscriptionMap && createIfAbsent)
41 | this.#eventsSubscriberMap[eventName] = {};
42 |
43 | return this.#eventsSubscriberMap[eventName];
44 | }
45 |
46 | /**
47 | *
48 | * @param {string} eventName
49 | * @returns {?Array.} List of subscribers
50 | */
51 | _getEventSubscribersAsList = (eventName) => {
52 | const subscriberMap = this._getEventSubscriberMap(eventName);
53 | if (!subscriberMap) return;
54 |
55 | const subscriberIdList = Object.getOwnPropertySymbols(subscriberMap);
56 |
57 | return subscriberIdList.map((subscriberId) => ({
58 | callback: subscriberMap[subscriberId],
59 | id: subscriberId,
60 | }));
61 | };
62 |
63 | /**
64 | *
65 | * @param {string} eventName
66 | * @returns {?Object.} Subscriber map for the event
67 | */
68 | _garbageCollector(eventName) {
69 | const subscriberList = this._getEventSubscribersAsList(eventName);
70 | if (!subscriberList) return;
71 |
72 | const subscriberMap = {};
73 | let isGarbageEvent = true;
74 |
75 | subscriberList.forEach((subscriber) => {
76 | const { id, callback } = subscriber;
77 | const hasCallback = !!callback;
78 |
79 | if (!hasCallback) return;
80 |
81 | subscriberMap[id] = callback;
82 | isGarbageEvent = false;
83 | });
84 |
85 | if (!isGarbageEvent)
86 | return (this.#eventsSubscriberMap[eventName] = subscriberMap);
87 |
88 | const { [eventName]: garbageEvent, ...otherEvents } =
89 | this.#eventsSubscriberMap;
90 |
91 | this.#eventsSubscriberMap = otherEvents;
92 | }
93 |
94 | /**
95 | *
96 | * @param {string} eventName
97 | * @param {Symbol} subscriptionId
98 | */
99 | _unsubscribe(eventName, subscriptionId) {
100 | const subscriberMap = this._getEventSubscriberMap(eventName);
101 |
102 | subscriberMap[subscriptionId] = null;
103 | this._garbageCollector(eventName);
104 | }
105 |
106 | /**
107 | *
108 | * @param {string} eventName
109 | * @param {Function} callback
110 | * @returns {Subscriber} A new subscriber
111 | */
112 | subscribeOn(eventName, callback) {
113 | const subscriberMap = this._getEventSubscriberMap(eventName, {
114 | createIfAbsent: true,
115 | });
116 | const subscriptionId = Symbol(eventName);
117 |
118 | subscriberMap[subscriptionId] = callback;
119 |
120 | return {
121 | id: subscriptionId,
122 | unsubscribe: () => this._unsubscribe(eventName, subscriptionId),
123 | };
124 | }
125 |
126 | // TODO: subscribeOnceOn
127 |
128 | /**
129 | *
130 | * @param {string} eventName
131 | * @param {...any} args
132 | */
133 | publish(eventName, ...args) {
134 | const subscriberList = this._getEventSubscribersAsList(eventName) ?? [];
135 |
136 | subscriberList.forEach((subscriber) => {
137 | const subscriptionArguments = deepClone(args);
138 | subscriber.callback(...subscriptionArguments);
139 | });
140 | }
141 | }
142 |
143 | export default EventBus;
144 |
--------------------------------------------------------------------------------
/src/components/common/progress-indicator/linear.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
57 |
58 |
160 |
--------------------------------------------------------------------------------
/src/services/mask/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | REGEX_TOKEN,
3 | REGEX_STARTING_CHAR,
4 | REGEX_ENDING_CHAR,
5 | DEFAULT_TOKEN,
6 | } from '@/constants/mask/mask.constant.js';
7 |
8 | class Mask {
9 | #patternCharList;
10 | #pattern;
11 | #token;
12 |
13 | /**
14 | * Return array of mapped pattern characters
15 | * @param {string} pattern
16 | * @returns {Array.} - Array of mapped pattern characters
17 | */
18 |
19 | #getPatternList(pattern) {
20 | const token = this.#token;
21 | const namedPatterns = {
22 | time: `${token}${token}:${token}${token}`,
23 | };
24 | const mappedPattern = namedPatterns[pattern] || pattern;
25 |
26 | return mappedPattern.split('');
27 | }
28 |
29 | /**
30 | * @param {string} pattern
31 | * @param {string} token
32 | * @constructor
33 | */
34 |
35 | constructor(pattern, token = DEFAULT_TOKEN) {
36 | this.#token = token;
37 | this.#pattern = pattern;
38 | this.#patternCharList = this.#getPatternList(pattern, token);
39 | }
40 |
41 | /**
42 | * Returns value is masked or not
43 | * @param {string} value
44 | * @returns {boolean} - isMasked
45 | */
46 |
47 | calcIsMasked(value) {
48 | if (!value) return false;
49 |
50 | const patternCharList = this.#patternCharList;
51 |
52 | const upperBound = Math.min(patternCharList.length, value.length);
53 | const subPatternList = patternCharList.slice(0, upperBound);
54 | const subValue = value.substring(0, upperBound);
55 | const regex = this.#toRegex(subPatternList, this.#token);
56 |
57 | return regex.test(subValue);
58 | }
59 |
60 | /**
61 | * Return regex that can be used to check value is masked or not
62 | * @param {Array.} patternCharList
63 | * @returns {RegExp}
64 | */
65 |
66 | #toRegex(patternCharList) {
67 | const regexPattern = patternCharList
68 | .map((char) => (char === this.#token ? REGEX_TOKEN : char))
69 | .join('');
70 | const regex = new RegExp(
71 | REGEX_STARTING_CHAR + regexPattern + REGEX_ENDING_CHAR
72 | );
73 | return regex;
74 | }
75 |
76 | /**
77 | * Returns augment that will be added to value according to pattern
78 | * @param {Object} payload
79 | * @param {string} payload.patternChar
80 | * @param {Array.} payload.valueCharList
81 | * @param {boolean} payload.isMasked
82 | * @returns {string} - augment
83 | */
84 |
85 | #getAugmentByPattern({ patternChar, valueCharList, isMasked }) {
86 | if (patternChar === this.#token) {
87 | const valueChar = valueCharList.shift();
88 | return valueChar || '';
89 | }
90 |
91 | return isMasked ? patternChar : '';
92 | }
93 |
94 | /**
95 | * Returns masked or unMasked value
96 | * @param {string} value
97 | * @param {boolean} isMasked
98 | * @returns {string} - masked to unMasked value
99 | */
100 |
101 | #getSanitizedValue(value, isMasked) {
102 | let sanitizedValue = '';
103 | const valueCharList = String(value)
104 | .split('')
105 | .filter((char) => !this.#patternCharList.includes(char));
106 |
107 | while (this.#patternCharList.length && valueCharList.length) {
108 | const patternChar = this.#patternCharList.shift();
109 | sanitizedValue += this.#getAugmentByPattern({
110 | patternChar,
111 | valueCharList,
112 | isMasked,
113 | });
114 | }
115 |
116 | sanitizedValue += valueCharList.join('');
117 |
118 | this.#patternCharList = this.#getPatternList(this.#pattern, this.#token);
119 |
120 | return sanitizedValue;
121 | }
122 |
123 | /**
124 | * Returns masked value
125 | * @param {string} value
126 | * @returns {string} - masked value
127 | */
128 |
129 | toMasked(value) {
130 | if (!value) return value;
131 |
132 | const maskedValue = this.#getSanitizedValue(value, true);
133 |
134 | return maskedValue;
135 | }
136 |
137 | /**
138 | * Returns unMasked value
139 | * @param {string} value
140 | * @returns {string} - unMasked value
141 | */
142 |
143 | toUnMasked(value) {
144 | if (!value) return value;
145 |
146 | const unMaskedValue = this.#getSanitizedValue(value, false);
147 |
148 | return unMaskedValue;
149 | }
150 | }
151 |
152 | export default Mask;
153 |
--------------------------------------------------------------------------------
/src/components/common/dialog/basic.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
15 |
16 | {{ headlineText }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
28 |
34 | {{ secondaryActionLabel }}
35 |
36 |
42 | {{ primaryActionLabel }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
89 |
90 |
159 |
--------------------------------------------------------------------------------
/src/components/common/button/icon.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
57 |
58 |
159 |
--------------------------------------------------------------------------------
/src/components/common/button/segmented.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
8 |
32 |
33 |
34 |
35 |
36 |
98 |
99 |
172 |
--------------------------------------------------------------------------------
/src/components/common/scrim/item.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
156 |
157 |
197 |
--------------------------------------------------------------------------------
/src/components/common/progress-indicator/circular.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 |
22 |
23 |
91 |
92 |
186 |
--------------------------------------------------------------------------------
/src/components/common/input/stepper.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
18 |
26 |
27 |
28 |
29 |
142 |
143 |
196 |
--------------------------------------------------------------------------------
/src/components/common/chip/filter.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 | {{ labelText }}
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
79 |
80 |
232 |
--------------------------------------------------------------------------------
/src/components/common/input/switch.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
66 |
67 |
233 |
--------------------------------------------------------------------------------
/src/components/common/field/filled-text.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
33 | {{ description }}
34 |
35 |
36 |
37 |
38 |
103 |
104 |
239 |
--------------------------------------------------------------------------------
/src/components/common/button/base.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
90 |
91 |
243 |
--------------------------------------------------------------------------------
/src/components/common/picker/date/base-calendar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ label }}
5 |
6 |
11 |
18 | {{ day.label }}
19 |
20 |
21 |
22 |
23 |
136 |
137 |
237 |
--------------------------------------------------------------------------------
/src/composable/use-draggable.composable.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} Position
3 | * @property {number} x - Horizontal axis position
4 | * @property {number} y - Vertical axis position
5 | */
6 |
7 | /**
8 | * useDraggable composable options
9 | * @typedef {Object} UseDraggableOptions
10 | * @property {string} [axis=both] - Drag direction
11 | * @property {HTMLElement} [containerElement=document.body] - Container element (draggable element boundaries)
12 | * @property {Position} [initialPosition={x: 0, y:0}] - Initial position of element
13 | * @property {boolean} [exact=false] - Only start dragging if exactly point to element
14 | * @property {boolean} [disabled=false] - Disable the dragging
15 | * @property {Function} dragStart - Call on drag start
16 | * @property {Function} dragMove - Call on drag move
17 | * @property {Function} dragEnd - Call on drag end
18 | * @property {boolean} [shouldUpdatePosition=true] - Update top and left css property of the target element
19 | */
20 |
21 | import { onMounted, ref, toValue } from 'vue';
22 | import { clamp } from '@/utils/math.util';
23 |
24 | const axisValues = Object.freeze({
25 | HORIZONTAL: 'horizontal',
26 | VERTICAL: 'vertical',
27 | BOTH: 'both',
28 | });
29 |
30 | const DEFAULT_OPTIONS = Object.freeze({
31 | axis: 'both',
32 | disabled: false,
33 | exact: false,
34 | containerElement: document.body,
35 | initialPosition: { x: 0, y: 0 },
36 | shouldUpdatePosition: true,
37 | });
38 |
39 | /**
40 | * Validate useDraggable composable options and throw error
41 | * if receive any invalid option
42 | * @param {UseDraggableOptions} options
43 | * @returns {undefined}
44 | */
45 | const validateOptions = (options) => {
46 | const axis = toValue(options.axis);
47 | const validAxises = Object.values(axisValues);
48 |
49 | if (!validAxises.includes(axis)) {
50 | throw new Error(`Axis must be one of ${validAxises}`);
51 | }
52 | };
53 |
54 | /**
55 | * useDraggable composable
56 | * @param {HTMLElement} element - Element to drag
57 | * @param {UsePromiseConfig} config - usePromise options
58 | * @returns {Object} - {position}
59 | */
60 | const useDraggable = (element, preferredOptions = {}) => {
61 | const options = { ...DEFAULT_OPTIONS, ...preferredOptions };
62 | const {
63 | axis,
64 | containerElement,
65 | initialPosition,
66 | exact,
67 | shouldUpdatePosition,
68 | } = options;
69 |
70 | validateOptions(options);
71 |
72 | /**
73 | * Set pointerMove and pointerUp events
74 | * @returns {undefined}
75 | */
76 | const setListeners = () => {
77 | document.addEventListener('pointermove', dragMove);
78 | document.addEventListener('pointerup', dragEnd);
79 | };
80 |
81 | /**
82 | * Remove pointerMove and pointerUp events
83 | * @returns {undefined}
84 | */
85 | const removeListeners = () => {
86 | document.removeEventListener('pointermove', dragMove);
87 | document.removeEventListener('pointerup', dragEnd);
88 | };
89 |
90 | let pointerStartPosition = { x: 0, y: 0 };
91 | let elementOffset = { x: 0, y: 0 };
92 | const position = ref(toValue(initialPosition));
93 | const distancePercentage = ref({ x: 0, y: 0 });
94 |
95 | /**
96 | * Initial the dragging requirements
97 | * @param {PointerEvent} event
98 | * @returns {undefined}
99 | */
100 | const dragStart = (event) => {
101 | const disabled = toValue(options.disabled);
102 | if (event.button !== 0 || disabled) {
103 | return;
104 | }
105 |
106 | const target = toValue(element);
107 | if (toValue(exact) && event.target !== target) {
108 | return;
109 | }
110 |
111 | pointerStartPosition = { x: event.clientX, y: event.clientY };
112 | elementOffset = { x: target.offsetLeft, y: target.offsetTop };
113 |
114 | setListeners();
115 |
116 | options.dragStart?.(position.value, event);
117 | };
118 |
119 | /**
120 | * Calculate clamped value horizontally
121 | * @param {PointerEvent} event
122 | * @param {Object} payload
123 | * @param {HTMLElement} payload.target
124 | * @param {HTMLElement} payload.container
125 | * @returns {number} - clamped horizontal value
126 | */
127 | const getClampedX = (event, { target, container }) => {
128 | const pointerCurrentXPosition = event.clientX;
129 |
130 | const pointerMovedDistance =
131 | elementOffset.x + pointerCurrentXPosition - pointerStartPosition.x;
132 |
133 | const containerWidth = container.clientWidth;
134 | const targetWidth = target.offsetWidth;
135 | const maxPosition = containerWidth - targetWidth;
136 |
137 | const clampedX = clamp(pointerMovedDistance, { max: maxPosition, min: 0 });
138 |
139 | distancePercentage.value.x = Math.round((clampedX * 100) / maxPosition);
140 |
141 | return clampedX;
142 | };
143 |
144 | /**
145 | * Calculate clamped value vertically
146 | * @param {PointerEvent} event
147 | * @param {Object} payload
148 | * @param {HTMLElement} payload.target
149 | * @param {HTMLElement} payload.container
150 | * @returns {number} - clamped vertical value
151 | */
152 | const getClampedY = (event, { target, container }) => {
153 | const pointerCurrentYPosition = event.clientY;
154 |
155 | const pointerMovedDistance =
156 | elementOffset.y + pointerCurrentYPosition - pointerStartPosition.y;
157 |
158 | const containerHeight = container.clientHeight;
159 | const targetHeight = target.offsetHeight;
160 | const maxPosition = containerHeight - targetHeight;
161 |
162 | const clampedY = clamp(pointerMovedDistance, { max: maxPosition, min: 0 });
163 |
164 | distancePercentage.value.y = Math.round((clampedY * 100) / maxPosition);
165 |
166 | return clampedY;
167 | };
168 |
169 | /**
170 | * Handle pointer movement
171 | * @param {PointerEvent} event
172 | * @returns {undefined}
173 | */
174 | const dragMove = (event) => {
175 | const target = toValue(element);
176 | const container = toValue(containerElement);
177 | const axisValue = toValue(axis);
178 | const shouldUpdate = toValue(shouldUpdatePosition);
179 |
180 | if (axisValue === 'both' || axisValue === 'horizontal') {
181 | const x = getClampedX(event, { target, container });
182 | position.value.x = x;
183 |
184 | if (shouldUpdate) {
185 | target.style.left = `${x}px`;
186 | }
187 | }
188 |
189 | if (axisValue === 'both' || axisValue === 'vertical') {
190 | const y = getClampedY(event, { target, container });
191 | position.value.y = y;
192 |
193 | if (shouldUpdate) {
194 | target.style.top = `${y}px`;
195 | }
196 | }
197 |
198 | options.dragMove?.(position.value, event);
199 | };
200 |
201 | /**
202 | * Handle pointer movement end
203 | * @param {PointerEvent} event
204 | * @returns {undefined}
205 | */
206 | const dragEnd = (event) => {
207 | removeListeners();
208 | options.dragEnd?.(position.value, event);
209 | };
210 |
211 | onMounted(() => {
212 | const target = toValue(element);
213 | target.addEventListener('pointerdown', dragStart);
214 | });
215 |
216 | return { position, distancePercentage };
217 | };
218 |
219 | export default useDraggable;
220 |
--------------------------------------------------------------------------------