feat: initial commit - Band Management application

This commit is contained in:
2026-01-06 03:11:46 +01:00
commit 34e12e00b3
24543 changed files with 3991790 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# 2 space indentation
[*.{vue,scss,ts}]
indent_style = space
indent_size = 2
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

View File

@@ -0,0 +1,3 @@
NUXT_PUBLIC_API_BASE_URL=
NUXT_APP_BASE_URL=
MAPBOX_ACCESS_TOKEN=

View File

@@ -0,0 +1,364 @@
{
"globals": {
"$api": true,
"COOKIE_MAX_AGE_1_YEAR": true,
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"alphaDashValidator": true,
"alphaValidator": true,
"asyncComputed": true,
"autoResetRef": true,
"avatarText": true,
"betweenValidator": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"confirmedValidator": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGenericProjection": true,
"createGlobalState": true,
"createInjectionState": true,
"createPinia": true,
"createProjection": true,
"createReactiveFn": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"createUrl": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"definePage": true,
"defineStore": true,
"eagerComputed": true,
"effectScope": true,
"emailValidator": true,
"extendRef": true,
"formatDate": true,
"formatDateToMonthShort": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"hexToRgb": true,
"ignorableWatch": true,
"inject": true,
"injectLocal": true,
"integerValidator": true,
"isDefined": true,
"isEmpty": true,
"isEmptyArray": true,
"isNullOrUndefined": true,
"isObject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"isToday": true,
"kFormatter": true,
"lengthValidator": true,
"logicAnd": true,
"logicNot": true,
"logicOr": true,
"makeDestructurable": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"paginationMeta": true,
"passwordValidator": true,
"pausableWatch": true,
"prefixWithPlus": true,
"provide": true,
"provideLocal": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"regexValidator": true,
"registerPlugins": true,
"requiredValidator": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"resolveVuetifyTheme": true,
"rgbaToHex": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"urlValidator": true,
"useAbs": true,
"useActiveElement": true,
"useAnimate": true,
"useApi": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useAverage": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useCeil": true,
"useClamp": true,
"useClipboard": true,
"useClipboardItems": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCookie": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFloor": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGenerateImageVariant": true,
"useGeolocation": true,
"useI18n": true,
"useId": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMath": true,
"useMax": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMin": true,
"useModel": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePrecision": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePrevious": true,
"useProjection": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useResponsiveLeftSidebar": true,
"useRound": true,
"useRoute": true,
"useRouter": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSkins": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSum": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useTrunc": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

View File

@@ -0,0 +1,232 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'.eslintrc-auto-import.json',
'plugin:vue/vue3-recommended',
'plugin:import/recommended',
'plugin:promise/recommended',
'plugin:sonarjs/recommended',
'plugin:case-police/recommended',
'plugin:regexp/recommended',
// 'plugin:unicorn/recommended',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 13,
sourceType: 'module',
},
plugins: [
'vue',
'regex',
'regexp',
],
ignorePatterns: ['plugins/iconify/*.js', 'node_modules', 'dist', '*.d.ts', 'vendor', '*.json'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// indentation (Already present in TypeScript)
'comma-spacing': ['error', { before: false, after: true }],
'key-spacing': ['error', { afterColon: true }],
'n/prefer-global/process': ['off'],
'sonarjs/cognitive-complexity': ['off'],
'vue/first-attribute-linebreak': ['error', {
singleline: 'beside',
multiline: 'below',
}],
// indentation (Already present in TypeScript)
'indent': ['error', 2],
// Enforce trailing comma (Already present in TypeScript)
'comma-dangle': ['error', 'always-multiline'],
// Enforce consistent spacing inside braces of object (Already present in TypeScript)
'object-curly-spacing': ['error', 'always'],
// Enforce camelCase naming convention
'camelcase': 'error',
// Disable max-len
'max-len': 'off',
// we don't want it
'semi': ['error', 'never'],
// add parens ony when required in arrow function
'arrow-parens': ['error', 'as-needed'],
// add new line above comment
'newline-before-return': 'error',
// add new line above comment
'lines-around-comment': [
'error',
{
beforeBlockComment: true,
beforeLineComment: true,
allowBlockStart: true,
allowClassStart: true,
allowObjectStart: true,
allowArrayStart: true,
// We don't want to add extra space above closing SECTION
ignorePattern: '!SECTION',
},
],
// Ignore _ as unused variable
'array-element-newline': ['error', 'consistent'],
'array-bracket-newline': ['error', 'consistent'],
'vue/multi-word-component-names': 'off',
'padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: 'expression', next: 'const' },
{ blankLine: 'always', prev: 'const', next: 'expression' },
{ blankLine: 'always', prev: 'multiline-const', next: '*' },
{ blankLine: 'always', prev: '*', next: 'multiline-const' },
],
// Plugin: eslint-plugin-import
'import/prefer-default-export': 'off',
'import/newline-after-import': ['error', { count: 1 }],
'no-restricted-imports': ['error', 'vuetify/components', {
name: 'vue3-apexcharts',
message: 'apexcharts are auto imported',
}],
// For omitting extension for ts files
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
// ignore virtual files
'import/no-unresolved': [2, {
ignore: [
'~pages$',
'virtual:meta-layouts',
'#auth$',
'#components$',
// Ignore vite's ?raw imports
'.*\?raw',
],
}],
// Thanks: https://stackoverflow.com/a/63961972/10796681
'no-shadow': 'off',
// Plugin: eslint-plugin-promise
'promise/always-return': 'off',
'promise/catch-or-return': 'off',
// ESLint plugin vue
'vue/block-tag-newline': 'error',
'vue/component-api-style': 'error',
'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false, ignores: ['/^swiper-/'] }],
'vue/custom-event-name-casing': ['error', 'camelCase', {
ignores: [
'/^(click):[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?/',
],
}],
'vue/define-macros-order': 'error',
'vue/html-comment-content-newline': 'error',
'vue/html-comment-content-spacing': 'error',
'vue/html-comment-indent': 'error',
'vue/match-component-file-name': 'error',
'vue/no-child-content': 'error',
'vue/require-default-prop': 'off',
'vue/no-duplicate-attr-inheritance': 'error',
'vue/no-empty-component-block': 'error',
'vue/no-multiple-objects-in-class': 'error',
'vue/no-reserved-component-names': 'error',
'vue/no-template-target-blank': 'error',
'vue/no-useless-mustaches': 'error',
'vue/no-useless-v-bind': 'error',
'vue/padding-line-between-blocks': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/prefer-true-attribute-shorthand': 'error',
'vue/v-on-function-call': 'error',
'vue/no-restricted-class': ['error', '/^(p|m)(l|r)-/'],
'vue/valid-v-slot': ['error', {
allowModifiers: true,
}],
// -- Extension Rules
'vue/no-irregular-whitespace': 'error',
'vue/template-curly-spacing': 'error',
// -- Sonarlint
'sonarjs/no-duplicate-string': 'off',
'sonarjs/no-nested-template-literals': 'off',
// -- Unicorn
// 'unicorn/filename-case': 'off',
// 'unicorn/prevent-abbreviations': ['error', {
// replacements: {
// props: false,
// },
// }],
// https://github.com/gmullerb/eslint-plugin-regex
'regex/invalid': [
'error',
[
{
regex: '@/assets/images',
replacement: '@images',
message: 'Use \'@images\' path alias for image imports',
},
{
regex: '@/assets/styles',
replacement: '@styles',
message: 'Use \'@styles\' path alias for importing styles from \'assets/styles\'',
},
{
regex: '@core/\\w',
message: 'You can\'t use @core when you are in @layouts module',
files: {
inspect: '@layouts/.*',
},
},
{
regex: 'useLayouts\\(',
message: '`useLayouts` composable is only allowed in @layouts & @core directory. Please use `useThemeConfig` composable instead.',
files: {
inspect: '^(?!.*(@core|@layouts)).*',
},
},
],
// Ignore files
'\.eslintrc\.cjs',
],
},
settings: {
'import/resolver': {
node: true,
typescript: {
alwaysTryTypes: true,
project: '.nuxt/tsconfig.app.json',
},
},
},
}

View File

@@ -0,0 +1,207 @@
## GITATTRIBUTES FOR WEB PROJECTS
#
# These settings are for any web project.
#
# Details per file setting:
# text These files should be normalized (i.e. convert CRLF to LF).
# binary These files are binary and should be left untouched.
#
# Note that binary is a macro for -text -diff.
######################################################################
# Auto detect
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto
# Source code
*.bash text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.coffee text
*.css text diff=css
*.htm text diff=html
*.html text diff=html
*.inc text
*.ini text
*.js text
*.json text
*.jsx text
*.less text
*.ls text
*.map text -diff
*.od text
*.onlydata text
*.php text diff=php
*.pl text
*.ps1 text eol=crlf
*.py text diff=python
*.rb text diff=ruby
*.sass text
*.scm text
*.scss text diff=css
*.sh text eol=lf
.husky/* text eol=lf
*.sql text
*.styl text
*.tag text
*.ts text
*.tsx text
*.xml text
*.xhtml text diff=html
# Docker
Dockerfile text
# Documentation
*.ipynb text eol=lf
*.markdown text diff=markdown
*.md text diff=markdown
*.mdwn text diff=markdown
*.mdown text diff=markdown
*.mkd text diff=markdown
*.mkdn text diff=markdown
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
# Templates
*.dot text
*.ejs text
*.erb text
*.haml text
*.handlebars text
*.hbs text
*.hbt text
*.jade text
*.latte text
*.mustache text
*.njk text
*.phtml text
*.svelte text
*.tmpl text
*.tpl text
*.twig text
*.vue text
# Configs
*.cnf text
*.conf text
*.config text
.editorconfig text
.env text
.gitattributes text
.gitconfig text
.htaccess text
*.lock text -diff
package.json text eol=lf
package-lock.json text eol=lf -diff
pnpm-lock.yaml text eol=lf -diff
.prettierrc text
yarn.lock text -diff
*.toml text
*.yaml text
*.yml text
browserslist text
Makefile text
makefile text
# Heroku
Procfile text
# Graphics
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.gifv binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
# SVG treated as an asset (binary) by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
# Audio
*.kar binary
*.m4a binary
*.mid binary
*.midi binary
*.mp3 binary
*.ogg binary
*.ra binary
# Video
*.3gpp binary
*.3gp binary
*.as binary
*.asf binary
*.asx binary
*.avi binary
*.fla binary
*.flv binary
*.m4v binary
*.mng binary
*.mov binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogv binary
*.swc binary
*.swf binary
*.webm binary
# Archives
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
# Fonts
*.ttf binary
*.eot binary
*.otf binary
*.woff binary
*.woff2 binary
# Executables
*.exe binary
*.pyc binary
# RC files (like .babelrc or .eslintrc)
*.*rc text
# Ignore files (like .npmignore or .gitignore)
*.*ignore text

View File

@@ -0,0 +1,50 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
# 👉 Custom Git ignores
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/*.code-snippets
!.vscode/tours
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.yarn
# iconify dist files
plugins/iconify/icons.css
# Ignore MSW script
public/mockServiceWorker.js
# Env files
.env*
!.env.example

View File

@@ -0,0 +1,2 @@
auto-install-peers=true
shamefully-hoist=true

View File

@@ -0,0 +1,46 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-idiomatic-order",
"@stylistic/stylelint-config"
],
"plugins": [
"stylelint-use-logical-spec",
"@stylistic/stylelint-plugin"
],
"overrides": [
{
"files": [
"**/*.scss"
],
"customSyntax": "postcss-scss"
},
{
"files": [
"**/*.vue"
],
"customSyntax": "postcss-html"
}
],
"rules": {
"@stylistic/max-line-length": [
220,
{
"ignore": "comments"
}
],
"@stylistic/indentation": 2,
"liberty/use-logical-spec": true,
"selector-class-pattern": null,
"color-function-notation": null,
"annotation-no-unknown": [
true,
{
"ignoreAnnotations": [
"default"
]
}
],
"media-feature-range-notation": null
}
}

View File

@@ -0,0 +1,23 @@
{
"Add hand emoji": {
"prefix": "cm-hand-emoji",
"body": [
"👉"
],
"description": "Add hand emoji"
},
"Add info emoji": {
"prefix": "cm-info-emoji",
"body": [
""
],
"description": "Add info emoji"
},
"Add warning emoji": {
"prefix": "cm-warning-emoji",
"body": [
"❗"
],
"description": "Add warning emoji"
}
}

View File

@@ -0,0 +1,15 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"xabikos.javascriptsnippets",
"stylelint.vscode-stylelint",
"fabiospampinato.vscode-highlight",
"github.vscode-pull-request-github",
"vue.volar",
"antfu.iconify",
"cipchk.cssrem",
"matijao.vue-nuxt-snippets",
"dongido.sync-env"
]
}

View File

@@ -0,0 +1,96 @@
{
"editor.formatOnSave": true,
"files.insertFinalNewline": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.autoClosingBrackets": "always"
},
"[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
},
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[vue]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"volar.preview.port": 3000,
"volar.completion.preferredTagNameCase": "pascal",
"eslint.options": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "explicit"
},
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
"eslint.packageManager": "pnpm",
"stylelint.packageManager": "pnpm",
"stylelint.validate": [
"css",
"scss",
"vue"
],
"cSpell.words": [
"addressline",
"Composables",
"Customizer",
"destr",
"flagpack",
"Iconify",
"nuxt",
"ofetch",
"psudo",
"stylelint",
"touchless",
"triggerer",
"vuetify"
],
"commentAnchors.tags.anchors": {
"": {
"scope": "hidden",
"highlightColor": "#3498DB",
"styleComment": true,
"isItalic": false
},
"👉": {
"scope": "file",
"highlightColor": "#98C379",
"styleComment": true,
"isItalic": false
},
"❗": {
"scope": "hidden",
"highlightColor": "#FF2D00",
"styleComment": true,
"isItalic": false
}
},
"highlight.regexFlags": "gi",
"highlight.regexes": {
"(100vh|translate|margin:|padding:|margin-left|margin-right|rotate|text-align|border-top|border-right|border-bottom|border-left|float|background-position|transform|width|height|top|left|bottom|right|float|clear|(p|m)(l|r)-|border-(start|end)-(start|end)-radius)": [
{
"borderWidth": "1px",
"borderColor": "tomato",
"borderStyle": "solid"
}
],
"(overflow-x:|overflow-y:)": [
{
"borderWidth": "1px",
"borderColor": "green",
"borderStyle": "solid"
}
]
}
}

View File

@@ -0,0 +1,18 @@
{
"Vue TS - DefineProps": {
"prefix": "dprops",
"body": [
"defineProps<${1:Props}>()"
],
"description": "DefineProps in script setup"
},
"Vue TS - Props interface": {
"prefix": "iprops",
"body": [
"interface Props {",
" ${1}",
"}"
],
"description": "Create props interface in script setup"
}
}

View File

@@ -0,0 +1,63 @@
{
"script": {
"prefix": "vue-sfc-ts",
"body": [
"<script lang=\"ts\" setup>",
"",
"</script>",
"",
"<template>",
" ",
"</template>",
"",
"<style lang=\"scss\">",
"",
"</style>",
""
],
"description": "Vue SFC Typescript"
},
"template": {
"scope": "vue",
"prefix": "template",
"body": [
"<template>",
" $1",
"</template>"
],
"description": "Create <template> block"
},
"Script setup + TS": {
"prefix": "script-setup-ts",
"body": [
"<script setup lang=\"ts\">",
"${1}",
"</script>"
],
"description": "Script setup + TS"
},
"style": {
"scope": "vue",
"prefix": "style",
"body": [
"<style lang=\"scss\">",
"$1",
"</style>"
],
"description": "Create <style> block"
},
"use composable": {
"prefix": "use-composable",
"body": [
"const { $2 } = ${1:useComposable}()"
],
"description": "We frequently uses composable in our components and writing const {} = useModule() is tedious. This snippet helps you to write it quickly."
},
"template interpolation": {
"prefix": "cc",
"body": [
"{{ ${1} }}"
],
"description": "We are just making writing template interpolation easier."
}
}

View File

@@ -0,0 +1,52 @@
{
"Vuetify Menu -- Parent Activator": {
"prefix": "v-menu",
"body": [
"<v-btn color=\"primary\">",
" Activator",
" <v-menu activator=\"parent\">",
" <v-list>",
" <v-list-item",
" v-for=\"(item, index) in ['apple', 'banana', 'cherry']\"",
" :key=\"index\"",
" :value=\"index\"",
" >",
" <v-list-item-title>{{ item }}</v-list-item-title>",
" </v-list-item>",
" </v-list>",
" </v-menu>",
"</v-btn>"
],
"description": "We use menu component with parent activator mostly because it is compact and easy to understand."
},
"Vuetify CSS variable": {
"prefix": "v-css-var",
"body": [
"rgb(var(--v-${1:theme}))"
],
"description": "Vuetify CSS variable"
},
"Icon only button": {
"prefix": "IconBtn",
"body": [
"<IconBtn>",
" <VIcon icon=\"tabler-${1}\" />",
"</IconBtn>"
],
"description": "Icon only button"
},
"Radio Group": {
"prefix": "v-radio-grp",
"body": [
"<v-radio-group v-model=\"${1:modelValue}\">",
" <v-radio",
" v-for=\"item in ['apple', 'banana', 'cherry']\"",
" :key=\"item\"",
" :label=\"item\"",
" :value=\"item\"",
" />",
"</v-radio-group>"
],
"description": "Radio Group"
}
}

View File

@@ -0,0 +1,288 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import {
VList,
VListItem,
} from 'vuetify/components/VList'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
searchResults: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
required: false,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'search',
])
// 👉 Hotkey
// eslint-disable-next-line camelcase
const { ctrl_k, meta_k } = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown')
e.preventDefault()
},
})
const refSearchList = ref()
const refSearchInput = ref()
const searchQueryLocal = ref('')
// 👉 watching control + / to open dialog
/* eslint-disable camelcase */
watch([
ctrl_k,
meta_k,
], () => {
emit('update:isDialogVisible', true)
})
/* eslint-enable */
// 👉 clear search result and close the dialog
const clearSearchAndCloseDialog = () => {
searchQueryLocal.value = ''
emit('update:isDialogVisible', false)
}
const getFocusOnSearchList = e => {
if (e.key === 'ArrowDown') {
e.preventDefault()
refSearchList.value?.focus('next')
} else if (e.key === 'ArrowUp') {
e.preventDefault()
refSearchList.value?.focus('prev')
}
}
const dialogModelValueUpdate = val => {
searchQueryLocal.value = ''
emit('update:isDialogVisible', val)
}
watch(() => props.isDialogVisible, () => {
searchQueryLocal.value = ''
})
</script>
<template>
<VDialog
max-width="600"
:model-value="props.isDialogVisible"
:height="$vuetify.display.smAndUp ? '531' : '100%'"
:fullscreen="$vuetify.display.width < 600"
class="app-bar-search-dialog"
@update:model-value="dialogModelValueUpdate"
@keyup.esc="clearSearchAndCloseDialog"
>
<VCard
height="100%"
width="100%"
class="position-relative"
>
<VCardText
class="px-4"
style="padding-block: 1rem 1.2rem;"
>
<!-- 👉 Search Input -->
<VTextField
ref="refSearchInput"
v-model="searchQueryLocal"
autofocus
density="compact"
variant="plain"
class="app-bar-search-input"
@keyup.esc="clearSearchAndCloseDialog"
@keydown="getFocusOnSearchList"
@update:model-value="$emit('search', searchQueryLocal)"
>
<!-- 👉 Prepend Inner -->
<template #prepend-inner>
<div class="d-flex align-center text-high-emphasis me-1">
<VIcon
size="24"
icon="tabler-search"
/>
</div>
</template>
<!-- 👉 Append Inner -->
<template #append-inner>
<div class="d-flex align-start">
<div
class="text-base text-disabled cursor-pointer me-3"
@click="clearSearchAndCloseDialog"
>
[esc]
</div>
<VIcon
icon="tabler-x"
size="24"
@click="clearSearchAndCloseDialog"
/>
</div>
</template>
</VTextField>
</VCardText>
<!-- 👉 Divider -->
<VDivider />
<!-- 👉 Perfect Scrollbar -->
<PerfectScrollbar
:options="{ wheelPropagation: false, suppressScrollX: true }"
class="h-100"
>
<!-- 👉 Suggestions -->
<div
v-show="!!props.searchResults && !searchQueryLocal && $slots.suggestions"
class="h-100"
>
<slot name="suggestions" />
</div>
<template v-if="!isLoading">
<!-- 👉 Search List -->
<VList
v-show="searchQueryLocal.length && !!props.searchResults.length"
ref="refSearchList"
density="compact"
class="app-bar-search-list py-0"
>
<!-- 👉 list Item /List Sub header -->
<template
v-for="item in props.searchResults"
:key="item"
>
<slot
name="searchResult"
:item="item"
>
<VListItem>
{{ item }}
</VListItem>
</slot>
</template>
</VList>
<!-- 👉 No Data found -->
<div
v-show="!props.searchResults.length && searchQueryLocal.length"
class="h-100"
>
<slot name="noData">
<VCardText class="h-100">
<div class="app-bar-search-suggestions d-flex flex-column align-center justify-center text-high-emphasis pa-12">
<VIcon
size="64"
icon="tabler-file-alert"
/>
<div class="d-flex align-center flex-wrap justify-center gap-2 text-h5 mt-3">
<span>No Result For </span>
<span>"{{ searchQueryLocal }}"</span>
</div>
<slot name="noDataSuggestion" />
</div>
</VCardText>
</slot>
</div>
</template>
<!-- 👉 Loading -->
<template v-if="isLoading">
<VSkeletonLoader
v-for="i in 3"
:key="i"
type="list-item-two-line"
/>
</template>
</PerfectScrollbar>
</VCard>
</VDialog>
</template>
<style lang="scss">
.app-bar-search-suggestions {
.app-bar-search-suggestion {
&:hover {
color: rgb(var(--v-theme-primary));
}
}
}
.app-bar-search-dialog {
.app-bar-search-input {
.v-field__input {
padding-block-start: 0.2rem;
}
}
.app-bar-search-list {
.v-list-item,
.v-list-subheader {
font-size: 0.75rem;
padding-inline: 1rem;
}
.v-list-item {
border-radius: 6px;
margin-block-end: 0.125rem;
margin-inline: 0.5rem;
.v-list-item__append {
.enter-icon {
visibility: hidden;
}
}
&:hover,
&:active,
&:focus {
.v-list-item__append {
.enter-icon {
visibility: visible;
}
}
}
}
.v-list-subheader {
line-height: 1;
min-block-size: auto;
padding-block: 16px 8px;
padding-inline-start: 1rem;
text-transform: uppercase;
}
}
}
@supports selector(:focus-visible) {
.app-bar-search-dialog {
.v-list-item:focus-visible::after {
content: none;
}
}
}
</style>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
})
const emit = defineEmits(['cancel'])
</script>
<template>
<div class="pa-6 d-flex align-center">
<h5 class="text-h5">
{{ props.title }}
</h5>
<VSpacer />
<slot name="beforeClose" />
<IconBtn
size="small"
@click="$emit('cancel', $event)"
>
<VIcon
size="24"
icon="tabler-x"
/>
</IconBtn>
</div>
</template>

View File

@@ -0,0 +1,359 @@
<script setup>
const props = defineProps({
items: {
type: Array,
required: true,
},
currentStep: {
type: Number,
required: false,
default: 0,
},
direction: {
type: String,
required: false,
default: 'horizontal',
},
iconSize: {
type: [
String,
Number,
],
required: false,
default: 60,
},
isActiveStepValid: {
type: Boolean,
required: false,
default: undefined,
},
align: {
type: String,
required: false,
default: 'default',
},
})
const emit = defineEmits(['update:currentStep'])
const currentStep = ref(props.currentStep || 0)
const activeOrCompletedStepsClasses = computed(() => index => index < currentStep.value ? 'stepper-steps-completed' : index === currentStep.value ? 'stepper-steps-active' : '')
const isHorizontalAndNotLastStep = computed(() => index => props.direction === 'horizontal' && props.items.length - 1 !== index)
// check if validation is enabled
const isValidationEnabled = computed(() => {
return props.isActiveStepValid !== undefined
})
watchEffect(() => {
if (props.currentStep !== undefined && props.currentStep < props.items.length && props.currentStep >= 0)
currentStep.value = props.currentStep
emit('update:currentStep', currentStep.value)
})
</script>
<template>
<VSlideGroup
v-model="currentStep"
class="app-stepper"
show-arrows
:direction="props.direction"
:class="`app-stepper-${props.align} ${props.items[0].icon ? 'app-stepper-icons' : ''}`"
>
<VSlideGroupItem
v-for="(item, index) in props.items"
:key="item.title"
:value="index"
>
<div
class="cursor-pointer app-stepper-step pa-1"
:class="[
(!props.isActiveStepValid && (isValidationEnabled)) && 'stepper-steps-invalid',
activeOrCompletedStepsClasses(index),
]"
@click="!isValidationEnabled && emit('update:currentStep', index)"
>
<!-- SECTION stepper step with icon -->
<template v-if="item.icon">
<div class="stepper-icon-step text-high-emphasis d-flex align-center ">
<!-- 👉 icon and title -->
<div
class="d-flex align-center gap-x-3 step-wrapper"
:class="[props.direction === 'horizontal' && 'flex-column']"
>
<div class="stepper-icon">
<template v-if="typeof item.icon === 'object'">
<Component :is="item.icon" />
</template>
<VIcon
v-else
:icon="item.icon"
:size="item.size || props.iconSize"
/>
</div>
<div>
<p class="stepper-title font-weight-medium mb-0">
{{ item.title }}
</p>
<p
v-if="item.subtitle"
class="stepper-subtitle mb-0"
>
{{ item.subtitle }}
</p>
</div>
</div>
<!-- 👉 append chevron -->
<VIcon
v-if="isHorizontalAndNotLastStep(index)"
class="flip-in-rtl stepper-chevron-indicator mx-6"
size="20"
icon="tabler-chevron-right"
/>
</div>
</template>
<!-- !SECTION -->
<!-- SECTION stepper step without icon -->
<template v-else>
<div class="d-flex align-center gap-x-3">
<div>
<!-- 👉 custom circle icon -->
<template v-if="index >= currentStep">
<VAvatar
v-if="(!isValidationEnabled || props.isActiveStepValid || index !== currentStep)"
size="38"
rounded
:variant="index === currentStep ? 'elevated' : 'tonal'"
:color="index === currentStep ? 'primary' : 'default'"
>
<h5
class="text-h5"
:style="index === currentStep ? { color: '#fff' } : ''"
>
{{ index + 1 }}
</h5>
</VAvatar>
<VAvatar
v-else
color="error"
size="38"
rounded
>
<VIcon
icon="tabler-alert-circle"
size="22"
/>
</VAvatar>
</template>
<!-- 👉 step completed icon -->
<VAvatar
v-else
class="stepper-icon"
variant="tonal"
color="primary"
size="38"
rounded
>
<h5
class="text-h5"
style="color: rgb(var(--v-theme-primary));"
>
{{ index + 1 }}
</h5>
</VAvatar>
</div>
<!-- 👉 title and subtitle -->
<div class="d-flex flex-column justify-center">
<div class="stepper-title font-weight-medium">
{{ item.title }}
</div>
<div
v-if="item.subtitle"
class="stepper-subtitle text-sm text-disabled"
>
{{ item.subtitle }}
</div>
</div>
<!-- 👉 stepper step icon -->
<div
v-if="isHorizontalAndNotLastStep(index)"
class="stepper-step-line stepper-chevron-indicator mx-6"
>
<VIcon
icon="tabler-chevron-right"
size="20"
/>
</div>
</div>
</template>
<!-- !SECTION -->
</div>
</VSlideGroupItem>
</VSlideGroup>
</template>
<style lang="scss">
@use "@core/scss/template/mixins" as templateMixins;
.app-stepper {
// 👉 stepper step with bg color
&.stepper-icon-step-bg {
.stepper-icon-step {
.step-wrapper {
flex-direction: row !important;
}
.stepper-icon {
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.375rem;
background-color: rgba(var(--v-theme-on-surface), var(--v-selected-opacity));
block-size: 2.375rem;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
inline-size: 2.375rem;
}
}
.stepper-steps-active {
.stepper-icon-step {
.stepper-icon {
@include templateMixins.custom-elevation(var(--v-theme-primary), "sm");
background-color: rgb(var(--v-theme-primary));
color: rgba(var(--v-theme-on-primary));
}
}
}
.stepper-steps-completed {
.stepper-icon-step {
.stepper-icon {
background: rgba(var(--v-theme-primary), var(--v-activated-opacity));
color: rgba(var(--v-theme-primary));
}
}
}
}
&.app-stepper-icons:not(.stepper-icon-step-bg) {
/* stylelint-disable-next-line no-descending-specificity */
.stepper-icon {
line-height: 0;
}
.step-wrapper {
padding: 1.25rem;
gap: 0.5rem;
min-inline-size: 9.375rem;
}
.stepper-chevron-indicator {
margin-inline: 1rem !important;
}
.stepper-steps-completed,
.stepper-steps-active {
.stepper-icon-step,
.stepper-step-icon,
.stepper-title,
.stepper-subtitle {
color: rgb(var(--v-theme-primary)) !important;
}
}
}
// 👉 stepper step with icon and default
.v-slide-group__content {
row-gap: 1rem;
/* stylelint-disable-next-line no-descending-specificity */
.stepper-title {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 0.9375rem;
font-weight: 500 !important;
}
/* stylelint-disable-next-line no-descending-specificity */
.stepper-subtitle {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
font-size: 0.8125rem;
line-height: 1.25rem;
}
/* stylelint-disable-next-line no-descending-specificity */
.stepper-chevron-indicator {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
/* stylelint-disable-next-line no-descending-specificity */
.stepper-steps-completed {
/* stylelint-disable-next-line no-descending-specificity */
.stepper-title,
.stepper-subtitle {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
}
.stepper-chevron-indicator {
color: rgb(var(--v-theme-primary));
}
}
/* stylelint-disable-next-line no-descending-specificity */
.stepper-steps-active {
.v-avatar.bg-primary {
@include templateMixins.custom-elevation(var(--v-theme-primary), "sm");
}
.v-avatar.bg-error {
@include templateMixins.custom-elevation(var(--v-theme-error), "sm");
}
}
.stepper-steps-invalid.stepper-steps-active {
.stepper-icon-step,
.step-number,
.stepper-title,
.stepper-subtitle {
color: rgb(var(--v-theme-error)) !important;
}
}
.app-stepper-step {
&:not(.stepper-steps-active,.stepper-steps-completed) .v-avatar--variant-tonal {
--v-activated-opacity: 0.06;
}
}
}
// 👉 stepper alignment
&.app-stepper-center {
.v-slide-group__content {
justify-content: center;
}
}
&.app-stepper-start {
.v-slide-group__content {
justify-content: start;
}
}
&.app-stepper-end {
.v-slide-group__content {
justify-content: end;
}
}
}
</style>

View File

@@ -0,0 +1,91 @@
<script setup>
const vm = getCurrentInstance()
const buyNowUrl = ref(vm?.appContext.config.globalProperties.buyNowUrl || 'https://1.envato.market/vuexy_admin')
watch(buyNowUrl, val => {
if (vm)
vm.appContext.config.globalProperties.buyNowUrl = val
})
</script>
<template>
<a
class="buy-now-button d-print-none"
role="button"
rel="noopener noreferrer"
:href="buyNowUrl"
target="_blank"
>
Buy Now
<span class="button-inner" />
</a>
</template>
<style lang="scss" scoped>
.buy-now-button,
.button-inner {
display: inline-flex;
box-sizing: border-box;
align-items: center;
justify-content: center;
border: 0;
border-radius: 6px;
margin: 0;
animation: anime 12s linear infinite;
appearance: none;
background: linear-gradient(-45deg, #ffa63d, #ff3d77, #338aff, #3cf0c5);
background-size: 600%;
color: rgba(255, 255, 255, 90%);
cursor: pointer;
font-size: 0.9375rem;
font-weight: 500;
letter-spacing: 0.43px;
line-height: 1.2;
min-inline-size: 50px;
outline: 0;
padding-block: 0.625rem;
padding-inline: 1.25rem;
text-decoration: none;
text-transform: none;
vertical-align: middle;
}
.buy-now-button {
position: fixed;
z-index: 999;
inset-block-end: 5%;
inset-inline-end: 87px;
&:hover {
color: white;
text-decoration: none;
}
.button-inner {
position: absolute;
z-index: -1;
filter: blur(12px);
inset: 0;
opacity: 0;
transition: opacity 200ms ease-in-out;
}
&:not(:hover) .button-inner {
opacity: 0.8;
}
}
@keyframes anime {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>

View File

@@ -0,0 +1,44 @@
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
color: {
type: String,
required: false,
default: 'primary',
},
icon: {
type: String,
required: true,
},
stats: {
type: String,
required: true,
},
})
</script>
<template>
<VCard>
<VCardText class="d-flex flex-column align-center justify-center">
<VAvatar
v-if="props.icon"
size="40"
variant="tonal"
rounded
:color="props.color"
>
<VIcon :icon="props.icon" />
</VAvatar>
<h5 class="text-h5 pt-2 mb-1">
{{ props.stats }}
</h5>
<div class="text-body-1">
{{ props.title }}
</div>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
divider: {
type: Boolean,
required: false,
default: true,
},
})
</script>
<template>
<VDivider v-if="props.divider" />
<div class="customizer-section">
<div>
<VChip
size="small"
color="primary"
>
<span class="font-weight-medium">{{ props.title }}</span>
</VChip>
</div>
<slot />
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup>
const props = defineProps({
icon: {
type: String,
required: false,
default: 'tabler-x',
},
iconSize: {
type: String,
required: false,
default: '20',
},
})
</script>
<template>
<IconBtn
variant="elevated"
size="30"
:ripple="false"
class="v-dialog-close-btn"
>
<VIcon
:icon="props.icon"
:size="props.iconSize"
/>
</IconBtn>
</template>

View File

@@ -0,0 +1,126 @@
<script setup>
import {
useDropZone,
useFileDialog,
useObjectUrl,
} from '@vueuse/core'
const dropZoneRef = ref()
const fileData = ref([])
const { open, onChange } = useFileDialog({ accept: 'image/*' })
function onDrop(DroppedFiles) {
DroppedFiles?.forEach(file => {
if (file.type.slice(0, 6) !== 'image/') {
// eslint-disable-next-line no-alert
alert('Only image files are allowed')
return
}
fileData.value.push({
file,
url: useObjectUrl(file).value ?? '',
})
})
}
onChange(selectedFiles => {
if (!selectedFiles)
return
for (const file of selectedFiles) {
fileData.value.push({
file,
url: useObjectUrl(file).value ?? '',
})
}
})
useDropZone(dropZoneRef, onDrop)
</script>
<template>
<div class="flex">
<div class="w-full h-auto relative">
<div
ref="dropZoneRef"
class="cursor-pointer"
@click="() => open()"
>
<div
v-if="fileData.length === 0"
class="d-flex flex-column justify-center align-center gap-y-2 pa-12 drop-zone rounded"
>
<IconBtn
variant="tonal"
class="rounded-sm"
>
<VIcon icon="tabler-upload" />
</IconBtn>
<h4 class="text-h4">
Drag and drop your image here.
</h4>
<span class="text-disabled">or</span>
<VBtn
variant="tonal"
size="small"
>
Browse Images
</VBtn>
</div>
<div
v-else
class="d-flex justify-center align-center gap-3 pa-8 drop-zone flex-wrap"
>
<VRow class="match-height w-100">
<template
v-for="(item, index) in fileData"
:key="index"
>
<VCol
cols="12"
sm="4"
>
<VCard :ripple="false">
<VCardText
class="d-flex flex-column"
@click.stop
>
<VImg
:src="item.url"
width="200px"
height="150px"
class="w-100 mx-auto"
/>
<div class="mt-2">
<span class="clamp-text text-wrap">
{{ item.file.name }}
</span>
<span>
{{ item.file.size / 1000 }} KB
</span>
</div>
</VCardText>
<VCardActions>
<VBtn
variant="text"
block
@click.stop="fileData.splice(index, 1)"
>
Remove File
</VBtn>
</VCardActions>
</VCard>
</VCol>
</template>
</VRow>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.drop-zone {
border: 1px dashed rgba(var(--v-theme-on-surface), var(--v-border-opacity));
}
</style>

View File

@@ -0,0 +1,48 @@
<script setup>
const props = defineProps({
languages: {
type: Array,
required: true,
},
location: {
type: null,
required: false,
default: 'bottom end',
},
})
const { locale } = useI18n({ useScope: 'global' })
</script>
<template>
<IconBtn>
<VIcon icon="tabler-language" />
<!-- Menu -->
<VMenu
activator="parent"
:location="props.location"
offset="12px"
width="175"
>
<!-- List -->
<VList
:selected="[locale]"
color="primary"
>
<!-- List item -->
<VListItem
v-for="lang in props.languages"
:key="lang.i18nLang"
:value="lang.i18nLang"
@click="locale = lang.i18nLang"
>
<!-- Language label -->
<VListItemTitle>
{{ lang.label }}
</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
const props = defineProps({
menuList: {
type: Array,
required: false,
},
itemProps: {
type: Boolean,
required: false,
},
iconSize: {
type: String,
required: false,
},
class: {
type: String,
required: false,
default: 'text-disabled',
},
})
</script>
<template>
<IconBtn :class="props.class">
<VIcon
:size="iconSize"
icon="tabler-dots-vertical"
/>
<VMenu
v-if="props.menuList"
activator="parent"
>
<VList
:items="props.menuList"
:item-props="props.itemProps"
/>
</VMenu>
</IconBtn>
</template>

View File

@@ -0,0 +1,251 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
notifications: {
type: Array,
required: true,
},
badgeProps: {
type: Object,
required: false,
default: undefined,
},
location: {
type: null,
required: false,
default: 'bottom end',
},
})
const emit = defineEmits([
'read',
'unread',
'remove',
'click:notification',
])
const isAllMarkRead = computed(() => {
return props.notifications.some(item => item.isSeen === false)
})
const markAllReadOrUnread = () => {
const allNotificationsIds = props.notifications.map(item => item.id)
if (!isAllMarkRead.value)
emit('unread', allNotificationsIds)
else
emit('read', allNotificationsIds)
}
const totalUnseenNotifications = computed(() => {
return props.notifications.filter(item => item.isSeen === false).length
})
const toggleReadUnread = (isSeen, Id) => {
if (isSeen)
emit('unread', [Id])
else
emit('read', [Id])
}
</script>
<template>
<IconBtn id="notification-btn">
<VBadge
v-bind="props.badgeProps"
:model-value="props.notifications.some(n => !n.isSeen)"
color="error"
dot
offset-x="2"
offset-y="3"
>
<VIcon icon="tabler-bell" />
</VBadge>
<VMenu
activator="parent"
width="380px"
:location="props.location"
offset="12px"
:close-on-content-click="false"
>
<VCard class="d-flex flex-column">
<!-- 👉 Header -->
<VCardItem class="notification-section">
<VCardTitle class="text-h6">
Notifications
</VCardTitle>
<template #append>
<VChip
v-show="props.notifications.some(n => !n.isSeen)"
size="small"
color="primary"
class="me-2"
>
{{ totalUnseenNotifications }} New
</VChip>
<IconBtn
v-show="props.notifications.length"
size="34"
@click="markAllReadOrUnread"
>
<VIcon
size="20"
color="high-emphasis"
:icon="!isAllMarkRead ? 'tabler-mail' : 'tabler-mail-opened' "
/>
<VTooltip
activator="parent"
location="start"
>
{{ !isAllMarkRead ? 'Mark all as unread' : 'Mark all as read' }}
</VTooltip>
</IconBtn>
</template>
</VCardItem>
<VDivider />
<!-- 👉 Notifications list -->
<PerfectScrollbar
:options="{ wheelPropagation: false }"
style="max-block-size: 23.75rem;"
>
<VList class="notification-list rounded-0 py-0">
<template
v-for="(notification, index) in props.notifications"
:key="notification.title"
>
<VDivider v-if="index > 0" />
<VListItem
link
lines="one"
min-height="66px"
class="list-item-hover-class"
@click="$emit('click:notification', notification)"
>
<!-- Slot: Prepend -->
<!-- Handles Avatar: Image, Icon, Text -->
<div class="d-flex align-start gap-3">
<VAvatar
:color="notification.color && !notification.img ? notification.color : undefined"
:variant="notification.img ? undefined : 'tonal' "
>
<span v-if="notification.text">{{ avatarText(notification.text) }}</span>
<VImg
v-if="notification.img"
:src="notification.img"
/>
<VIcon
v-if="notification.icon"
:icon="notification.icon"
/>
</VAvatar>
<div>
<p class="text-sm font-weight-medium mb-1">
{{ notification.title }}
</p>
<p
class="text-body-2 mb-2"
style=" letter-spacing: 0.4px !important; line-height: 18px;"
>
{{ notification.subtitle }}
</p>
<p
class="text-sm text-disabled mb-0"
style=" letter-spacing: 0.4px !important; line-height: 18px;"
>
{{ notification.time }}
</p>
</div>
<VSpacer />
<div class="d-flex flex-column align-end">
<VIcon
size="10"
icon="tabler-circle-filled"
:color="!notification.isSeen ? 'primary' : '#a8aaae'"
:class="`${notification.isSeen ? 'visible-in-hover' : ''}`"
class="mb-2"
@click.stop="toggleReadUnread(notification.isSeen, notification.id)"
/>
<VIcon
size="20"
icon="tabler-x"
class="visible-in-hover"
@click="$emit('remove', notification.id)"
/>
</div>
</div>
</VListItem>
</template>
<VListItem
v-show="!props.notifications.length"
class="text-center text-medium-emphasis"
style="block-size: 56px;"
>
<VListItemTitle>No Notification Found!</VListItemTitle>
</VListItem>
</VList>
</PerfectScrollbar>
<VDivider />
<!-- 👉 Footer -->
<VCardText
v-show="props.notifications.length"
class="pa-4"
>
<VBtn
block
size="small"
>
View All Notifications
</VBtn>
</VCardText>
</VCard>
</VMenu>
</IconBtn>
</template>
<style lang="scss">
.notification-section {
padding-block: 0.75rem;
padding-inline: 1rem;
}
.list-item-hover-class {
.visible-in-hover {
display: none;
}
&:hover {
.visible-in-hover {
display: block;
}
}
}
.notification-list.v-list {
.v-list-item {
border-radius: 0 !important;
margin: 0 !important;
padding-block: 0.75rem !important;
}
}
// Badge Style Override for Notification Badge
.notification-badge {
.v-badge__badge {
/* stylelint-disable-next-line liberty/use-logical-spec */
min-width: 18px;
padding: 0;
block-size: 18px;
}
}
</style>

View File

@@ -0,0 +1,203 @@
<script setup>
import { Placeholder } from '@tiptap/extension-placeholder'
import { TextAlign } from '@tiptap/extension-text-align'
import { Underline } from '@tiptap/extension-underline'
import { StarterKit } from '@tiptap/starter-kit'
import {
EditorContent,
useEditor,
} from '@tiptap/vue-3'
const props = defineProps({
modelValue: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
},
})
const emit = defineEmits(['update:modelValue'])
const editorRef = ref()
const editor = useEditor({
content: props.modelValue,
extensions: [
StarterKit,
TextAlign.configure({
types: [
'heading',
'paragraph',
],
}),
Placeholder.configure({ placeholder: props.placeholder ?? 'Write something here...' }),
Underline,
],
onUpdate() {
if (!editor.value)
return
emit('update:modelValue', editor.value.getHTML())
},
})
watch(() => props.modelValue, () => {
const isSame = editor.value?.getHTML() === props.modelValue
if (isSame)
return
editor.value?.commands.setContent(props.modelValue)
})
</script>
<template>
<div class="pa-6 productDescriptionEditor">
<!-- buttons -->
<div
v-if="editor"
class="d-flex gap-1 flex-wrap align-center"
>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive('bold') ? 'tonal' : 'text'"
:color="editor.isActive('bold') ? 'primary' : 'default'"
@click="editor.chain().focus().toggleBold().run()"
>
<VIcon
icon="tabler-bold"
class="font-weight-medium"
/>
</VBtn>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive('underline') ? 'tonal' : 'text'"
:color="editor.isActive('underline') ? 'primary' : 'default'"
@click="editor.commands.toggleUnderline()"
>
<VIcon icon="tabler-underline" />
</VBtn>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive('italic') ? 'tonal' : 'text'"
:color="editor.isActive('italic') ? 'primary' : 'default'"
@click="editor.chain().focus().toggleItalic().run()"
>
<VIcon
icon="tabler-italic"
class="font-weight-medium"
/>
</VBtn>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive('strike') ? 'tonal' : 'text'"
:color="editor.isActive('strike') ? 'primary' : 'default'"
@click="editor.chain().focus().toggleStrike().run()"
>
<VIcon
icon="tabler-strikethrough"
size="20"
/>
</VBtn>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive({ textAlign: 'left' }) ? 'tonal' : 'text'"
:color="editor.isActive({ textAlign: 'left' }) ? 'primary' : 'default'"
@click="editor.chain().focus().setTextAlign('left').run()"
>
<VIcon
icon="tabler-align-left"
size="20"
/>
</VBtn>
<VBtn
size="small"
icon
rounded
:color="editor.isActive({ textAlign: 'center' }) ? 'primary' : 'default'"
:variant="editor.isActive({ textAlign: 'center' }) ? 'tonal' : 'text'"
@click="editor.chain().focus().setTextAlign('center').run()"
>
<VIcon
icon="tabler-align-center"
size="20"
/>
</VBtn>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive({ textAlign: 'right' }) ? 'tonal' : 'text'"
:color="editor.isActive({ textAlign: 'right' }) ? 'primary' : 'default'"
@click="editor.chain().focus().setTextAlign('right').run()"
>
<VIcon
icon="tabler-align-right"
size="20"
/>
</VBtn>
<VBtn
size="small"
icon
rounded
:variant="editor.isActive({ textAlign: 'justify' }) ? 'tonal' : 'text'"
:color="editor.isActive({ textAlign: 'justify' }) ? 'primary' : 'default'"
@click="editor.chain().focus().setTextAlign('justify').run()"
>
<VIcon
icon="tabler-align-justified"
size="20"
/>
</VBtn>
</div>
<VDivider class="my-4" />
<EditorContent
ref="editorRef"
:editor="editor"
/>
</div>
</template>
<style lang="scss">
.productDescriptionEditor {
.ProseMirror {
padding: 0 !important;
min-block-size: 12vh;
p {
margin-block-end: 0;
}
p.is-editor-empty:first-child::before {
block-size: 0;
color: #adb5bd;
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
&-focused {
outline: none;
}
}
}
</style>

View File

@@ -0,0 +1,40 @@
<script setup>
const { y } = useWindowScroll()
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
</script>
<template>
<VScaleTransition
style="transform-origin: center;"
class="scroll-to-top d-print-none"
>
<VBtn
v-show="y > 200"
icon
density="comfortable"
@click="scrollToTop"
>
<VIcon
size="22"
icon="tabler-arrow-up"
/>
</VBtn>
</VScaleTransition>
</template>
<style lang="scss">
.scroll-to-top {
position: fixed !important;
// To keep button on top of v-layout. E.g. Email app
z-index: 999;
inset-block-end: 5%;
inset-inline-end: 25px;
}
</style>

View File

@@ -0,0 +1,92 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
togglerIcon: {
type: String,
required: false,
default: 'tabler-layout-grid-add',
},
shortcuts: {
type: Array,
required: true,
},
})
const router = useRouter()
</script>
<template>
<IconBtn>
<VIcon :icon="props.togglerIcon" />
<VMenu
activator="parent"
offset="12px"
location="bottom end"
>
<VCard
:width="$vuetify.display.smAndDown ? 330 : 380"
max-height="560"
class="d-flex flex-column"
>
<VCardItem class="py-3">
<h6 class="text-base font-weight-medium">
{{ $t("Shortcut") }}
</h6>
<template #append>
<IconBtn
size="small"
color="high-emphasis"
>
<VIcon
size="20"
icon="tabler-plus"
/>
</IconBtn>
</template>
</VCardItem>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VRow class="ma-0 mt-n1">
<VCol
v-for="(shortcut, index) in props.shortcuts"
:key="shortcut.title"
cols="6"
class="text-center border-t cursor-pointer pa-6 shortcut-icon"
:class="(index + 1) % 2 ? 'border-e' : ''"
@click="router.push(shortcut.to)"
>
<VAvatar
variant="tonal"
size="50"
>
<VIcon
size="26"
color="high-emphasis"
:icon="shortcut.icon"
/>
</VAvatar>
<h6 class="text-base font-weight-medium mt-3 mb-0">
{{ $t(shortcut.title) }}
</h6>
<p class="text-sm mb-0">
{{ $t(shortcut.subtitle) }}
</p>
</VCol>
</VRow>
</PerfectScrollbar>
</VCard>
</VMenu>
</IconBtn>
</template>
<style lang="scss">
.shortcut-icon:hover {
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
}
</style>

View File

@@ -0,0 +1,42 @@
<script setup>
const props = defineProps({
page: {
type: Number,
required: true,
},
itemsPerPage: {
type: Number,
required: true,
},
totalItems: {
type: Number,
required: true,
},
})
const emit = defineEmits(['update:page'])
const updatePage = value => {
emit('update:page', value)
}
</script>
<template>
<div>
<VDivider />
<div class="d-flex align-center justify-sm-space-between justify-center flex-wrap gap-3 px-6 py-3">
<p class="text-disabled mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalItems) }}
</p>
<VPagination
:model-value="page"
active-color="primary"
:length="Math.ceil(totalItems / itemsPerPage)"
:total-visible="$vuetify.display.xs ? 1 : Math.min(Math.ceil(totalItems / itemsPerPage), 5)"
@update:model-value="updatePage"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,640 @@
<script setup>
import { useStorage } from '@vueuse/core'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useTheme } from 'vuetify'
import {
staticPrimaryColor,
staticPrimaryDarkenColor,
} from '@/plugins/vuetify/theme'
import {
Direction,
Layout,
Skins,
Theme,
} from '@core/enums'
import { useConfigStore } from '@core/stores/config'
import horizontalLight from '@images/customizer-icons/horizontal-light.svg'
import {
AppContentLayoutNav,
ContentWidth,
} from '@layouts/enums'
import {
cookieRef,
namespaceConfig,
} from '@layouts/stores/config'
import { themeConfig } from '@themeConfig'
import borderSkin from '@images/customizer-icons/border-light.svg'
import collapsed from '@images/customizer-icons/collapsed-light.svg'
import compact from '@images/customizer-icons/compact-light.svg'
import defaultSkin from '@images/customizer-icons/default-light.svg'
import ltrSvg from '@images/customizer-icons/ltr-light.svg'
import rtlSvg from '@images/customizer-icons/rtl-light.svg'
import wideSvg from '@images/customizer-icons/wide-light.svg'
const isNavDrawerOpen = ref(false)
const configStore = useConfigStore()
// 👉 Primary Color
const vuetifyTheme = useTheme()
const colors = [
{
main: staticPrimaryColor,
darken: staticPrimaryDarkenColor,
},
{
main: '#0D9394',
darken: '#0C8485',
},
{
main: '#FFB400',
darken: '#E6A200',
},
{
main: '#FF4C51',
darken: '#E64449',
},
{
main: '#16B1FF',
darken: '#149FE6',
},
]
const customPrimaryColor = ref('#663131')
watch(() => configStore.theme, () => {
const cookiePrimaryColor = cookieRef(`${ vuetifyTheme.name.value }ThemePrimaryColor`, null).value
if (cookiePrimaryColor && !colors.some(color => color.main === cookiePrimaryColor))
customPrimaryColor.value = cookiePrimaryColor
}, { immediate: true })
const setPrimaryColor = useDebounceFn(color => {
vuetifyTheme.themes.value[vuetifyTheme.name.value].colors.primary = color.main
vuetifyTheme.themes.value[vuetifyTheme.name.value].colors['primary-darken-1'] = color.darken
// We need to store this color value in cookie so vuetify plugin can pick on next reload
cookieRef(`${ vuetifyTheme.name.value }ThemePrimaryColor`, null).value = color.main
cookieRef(`${ vuetifyTheme.name.value }ThemePrimaryDarkenColor`, null).value = color.darken
// Update initial loader color
useStorage(namespaceConfig('initial-loader-color'), null).value = color.main
}, 100)
// 👉 Mode
const themeMode = computed(() => {
return [
{
bgImage: 'tabler-sun',
value: Theme.Light,
label: 'Light',
},
{
bgImage: 'tabler-moon-stars',
value: Theme.Dark,
label: 'Dark',
},
{
bgImage: 'tabler-device-desktop-analytics',
value: Theme.System,
label: 'System',
},
]
})
// 👉 Skin
const themeSkin = computed(() => {
return [
{
bgImage: defaultSkin,
value: Skins.Default,
label: 'Default',
},
{
bgImage: borderSkin,
value: Skins.Bordered,
label: 'Bordered',
},
]
})
// 👉 Layout
const currentLayout = ref(configStore.isVerticalNavCollapsed ? 'collapsed' : configStore.appContentLayoutNav)
const layouts = computed(() => {
return [
{
bgImage: defaultSkin,
value: Layout.Vertical,
label: 'Vertical',
},
{
bgImage: collapsed,
value: Layout.Collapsed,
label: 'Collapsed',
},
{
bgImage: horizontalLight,
value: Layout.Horizontal,
label: 'Horizontal',
},
]
})
watch(currentLayout, () => {
if (currentLayout.value === 'collapsed') {
configStore.isVerticalNavCollapsed = true
configStore.appContentLayoutNav = AppContentLayoutNav.Vertical
} else {
configStore.isVerticalNavCollapsed = false
configStore.appContentLayoutNav = currentLayout.value
}
})
// watch vertical sidebar collapse state
watch(() => configStore.isVerticalNavCollapsed, () => {
currentLayout.value = configStore.isVerticalNavCollapsed ? 'collapsed' : configStore.appContentLayoutNav
})
// 👉 Content Width
const contentWidth = computed(() => {
return [
{
bgImage: compact,
value: ContentWidth.Boxed,
label: 'Compact',
},
{
bgImage: wideSvg,
value: ContentWidth.Fluid,
label: 'Wide',
},
]
})
// 👉 Direction
const currentDir = ref(configStore.isAppRTL ? 'rtl' : 'ltr')
const direction = computed(() => {
return [
{
bgImage: ltrSvg,
value: Direction.Ltr,
label: 'Left to right',
},
{
bgImage: rtlSvg,
value: Direction.Rtl,
label: 'Right to left',
},
]
})
watch(currentDir, () => {
if (currentDir.value === 'rtl')
configStore.isAppRTL = true
else
configStore.isAppRTL = false
})
// check if any value set in cookie
const isCookieHasAnyValue = ref(false)
const { locale } = useI18n({ useScope: 'global' })
const isActiveLangRTL = computed(() => {
const lang = themeConfig.app.i18n.langConfig.find(l => l.i18nLang === locale.value)
return lang?.isRTL ?? false
})
watch([
() => vuetifyTheme.current.value.colors.primary,
configStore.$state,
locale,
], () => {
const initialConfigValue = [
staticPrimaryColor,
staticPrimaryColor,
themeConfig.app.theme,
themeConfig.app.skin,
themeConfig.verticalNav.isVerticalNavSemiDark,
themeConfig.verticalNav.isVerticalNavCollapsed,
themeConfig.app.contentWidth,
isActiveLangRTL.value,
themeConfig.app.contentLayoutNav,
]
const themeConfigValue = [
vuetifyTheme.themes.value.light.colors.primary,
vuetifyTheme.themes.value.dark.colors.primary,
configStore.theme,
configStore.skin,
configStore.isVerticalNavSemiDark,
configStore.isVerticalNavCollapsed,
configStore.appContentWidth,
configStore.isAppRTL,
configStore.appContentLayoutNav,
]
currentDir.value = configStore.isAppRTL ? 'rtl' : 'ltr'
isCookieHasAnyValue.value = JSON.stringify(themeConfigValue) !== JSON.stringify(initialConfigValue)
}, {
deep: true,
immediate: true,
})
// remove all theme related values from localStorage
const resetCustomizer = async () => {
if (isCookieHasAnyValue.value) {
// reset themeConfig values
vuetifyTheme.themes.value.light.colors.primary = staticPrimaryColor
vuetifyTheme.themes.value.dark.colors.primary = staticPrimaryColor
vuetifyTheme.themes.value.light.colors['primary-darken-1'] = staticPrimaryDarkenColor
vuetifyTheme.themes.value.dark.colors['primary-darken-1'] = staticPrimaryDarkenColor
configStore.theme = themeConfig.app.theme
configStore.skin = themeConfig.app.skin
configStore.isVerticalNavSemiDark = themeConfig.verticalNav.isVerticalNavSemiDark
configStore.appContentLayoutNav = themeConfig.app.contentLayoutNav
configStore.appContentWidth = themeConfig.app.contentWidth
configStore.isAppRTL = isActiveLangRTL.value
configStore.isVerticalNavCollapsed = themeConfig.verticalNav.isVerticalNavCollapsed
useStorage(namespaceConfig('initial-loader-color'), null).value = staticPrimaryColor
currentLayout.value = themeConfig.app.contentLayoutNav
cookieRef('lightThemePrimaryColor', null).value = null
cookieRef('darkThemePrimaryColor', null).value = null
cookieRef('lightThemePrimaryDarkenColor', null).value = null
cookieRef('darkThemePrimaryDarkenColor', null).value = null
await nextTick()
isCookieHasAnyValue.value = false
customPrimaryColor.value = '#ffffff'
}
}
</script>
<template>
<div class="d-lg-block d-none">
<VBtn
icon
class="app-customizer-toggler rounded-s-lg rounded-0"
style="z-index: 1001;"
@click="isNavDrawerOpen = true"
>
<VIcon
size="22"
icon="tabler-settings"
/>
</VBtn>
<VNavigationDrawer
v-model="isNavDrawerOpen"
data-allow-mismatch
temporary
touchless
border="none"
location="end"
width="400"
elevation="10"
:scrim="false"
class="app-customizer"
>
<!-- 👉 Header -->
<div class="customizer-heading d-flex align-center justify-space-between">
<div>
<h6 class="text-h6">
Theme Customizer
</h6>
<p class="text-body-2 mb-0">
Customize & Preview in Real Time
</p>
</div>
<div class="d-flex align-center gap-1">
<VBtn
icon
variant="text"
size="small"
color="medium-emphasis"
@click="resetCustomizer"
>
<VBadge
v-show="isCookieHasAnyValue"
dot
color="error"
offset-x="-29"
offset-y="-14"
/>
<VIcon
size="24"
color="high-emphasis"
icon="tabler-refresh"
/>
</VBtn>
<VBtn
icon
variant="text"
color="medium-emphasis"
size="small"
@click="isNavDrawerOpen = false"
>
<VIcon
icon="tabler-x"
color="high-emphasis"
size="24"
/>
</VBtn>
</div>
</div>
<VDivider />
<PerfectScrollbar
tag="ul"
:options="{ wheelPropagation: false }"
>
<!-- SECTION Theming -->
<CustomizerSection
title="Theming"
:divider="false"
>
<!-- 👉 Primary Color -->
<div class="d-flex flex-column gap-2">
<h6 class="text-h6">
Primary Color
</h6>
<div
class="d-flex app-customizer-primary-colors"
style="column-gap: 0.75rem; margin-block-start: 2px;"
>
<div
v-for="color in colors"
:key="color.main"
style="
border-radius: 0.375rem;
outline: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
padding-block: 0.5rem;
padding-inline: 0.625rem;"
class="primary-color-wrapper cursor-pointer"
:class="vuetifyTheme.current.value.colors.primary === color.main ? 'active' : ''"
:style="vuetifyTheme.current.value.colors.primary === color.main ? `outline-color: ${color.main}; outline-width:2px;` : `--v-color:${color.main}`"
@click="setPrimaryColor(color)"
>
<div
style="border-radius: 0.375rem;block-size: 2.125rem; inline-size: 1.8938rem;"
:style="{ backgroundColor: color.main }"
/>
</div>
<div
class="primary-color-wrapper cursor-pointer d-flex align-center"
style="
border-radius: 0.375rem;
outline: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
padding-block: 0.5rem;
padding-inline: 0.625rem;"
:class="vuetifyTheme.current.value.colors.primary === customPrimaryColor ? 'active' : ''"
:style="vuetifyTheme.current.value.colors.primary === customPrimaryColor ? `outline-color: ${customPrimaryColor}; outline-width:2px;` : ''"
>
<VBtn
icon
size="30"
:color="vuetifyTheme.current.value.colors.primary === customPrimaryColor ? customPrimaryColor : $vuetify.theme.current.dark ? '#8692d029' : '#4b465c29'"
variant="flat"
style="border-radius: 0.375rem;"
>
<VIcon
size="20"
icon="tabler-color-picker"
:color="vuetifyTheme.current.value.colors.primary === customPrimaryColor ? 'rgb(var(--v-theme-on-primary))' : ''"
/>
</VBtn>
<VMenu
activator="parent"
:close-on-content-click="false"
>
<VList>
<VListItem>
<VColorPicker
v-model="customPrimaryColor"
mode="hex"
:modes="['hex']"
@update:model-value="setPrimaryColor({ main: customPrimaryColor, darken: customPrimaryColor })"
/>
</VListItem>
</VList>
</VMenu>
</div>
</div>
</div>
<!-- 👉 Theme -->
<div class="d-flex flex-column gap-2">
<h6 class="text-h6">
Theme
</h6>
<CustomRadiosWithImage
:key="configStore.theme"
v-model:selected-radio="configStore.theme"
:radio-content="themeMode"
:grid-column="{ cols: '4' }"
class="customizer-skins"
>
<template #label="item">
<span class="text-sm text-medium-emphasis mt-1">{{ item?.label }}</span>
</template>
<template #content="{ item }">
<div
class="customizer-skins-icon-wrapper d-flex align-center justify-center py-3 w-100"
style="min-inline-size: 100%;"
>
<VIcon
size="30"
:icon="item.bgImage"
color="high-emphasis"
/>
</div>
</template>
</CustomRadiosWithImage>
</div>
<!-- 👉 Skin -->
<div class="d-flex flex-column gap-2">
<h6 class="text-h6">
Skins
</h6>
<CustomRadiosWithImage
:key="configStore.skin"
v-model:selected-radio="configStore.skin"
:radio-content="themeSkin"
:grid-column="{ cols: '4' }"
>
<template #label="item">
<span class="text-sm text-medium-emphasis">{{ item?.label }}</span>
</template>
</CustomRadiosWithImage>
</div>
<!-- 👉 Semi Dark -->
<div
class="align-center justify-space-between"
:class="vuetifyTheme.global.name.value === 'light' && configStore.appContentLayoutNav === AppContentLayoutNav.Vertical ? 'd-flex' : 'd-none'"
>
<VLabel
for="customizer-semi-dark"
class="text-h6 text-high-emphasis"
>
Semi Dark Menu
</VLabel>
<div>
<VSwitch
id="customizer-semi-dark"
v-model="configStore.isVerticalNavSemiDark"
class="ms-2"
/>
</div>
</div>
</CustomizerSection>
<!-- !SECTION -->
<!-- SECTION LAYOUT -->
<CustomizerSection title="Layout">
<!-- 👉 Layouts -->
<div class="d-flex flex-column gap-2">
<h6 class="text-base font-weight-medium">
Layout
</h6>
<CustomRadiosWithImage
:key="currentLayout"
v-model:selected-radio="currentLayout"
:radio-content="layouts"
:grid-column="{ cols: '4' }"
>
<template #label="item">
<span class="text-sm text-medium-emphasis">{{ item.label }}</span>
</template>
</CustomRadiosWithImage>
</div>
<!-- 👉 Content Width -->
<div class="d-flex flex-column gap-2">
<h6 class="text-base font-weight-medium">
Content
</h6>
<CustomRadiosWithImage
:key="configStore.appContentWidth"
v-model:selected-radio="configStore.appContentWidth"
:radio-content="contentWidth"
:grid-column="{ cols: '4' }"
>
<template #label="item">
<span class="text-sm text-medium-emphasis">{{ item.label }}</span>
</template>
</CustomRadiosWithImage>
</div>
<!-- 👉 Direction -->
<div class="d-flex flex-column gap-2">
<h6 class="text-base font-weight-medium">
Direction
</h6>
<CustomRadiosWithImage
:key="currentDir"
v-model:selected-radio="currentDir"
:radio-content="direction"
:grid-column="{ cols: '4' }"
>
<template #label="item">
<span class="text-sm text-medium-emphasis">{{ item?.label }}</span>
</template>
</CustomRadiosWithImage>
</div>
</CustomizerSection>
<!-- !SECTION -->
</PerfectScrollbar>
</VNavigationDrawer>
</div>
</template>
<style lang="scss">
@use "@layouts/styles/mixins" as layoutMixins;
.app-customizer {
&.v-navigation-drawer--temporary:not(.v-navigation-drawer--active) {
transform: translateX(110%) !important;
@include layoutMixins.rtl {
transform: translateX(-110%) !important;
}
}
.customizer-section {
display: flex;
flex-direction: column;
padding: 1.5rem;
gap: 1.5rem;
}
.customizer-heading {
padding-block: 1rem;
padding-inline: 1.5rem;
}
.custom-input-wrapper {
.v-col {
padding-inline: 10px;
}
.v-label.custom-input {
border: none;
color: rgb(var(--v-theme-on-surface));
outline: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
}
.v-navigation-drawer__content {
display: flex;
flex-direction: column;
}
.v-label.custom-input.active {
border-color: transparent;
outline: 2px solid rgb(var(--v-theme-primary));
}
.v-label.custom-input:not(.active):hover {
border-color: rgba(var(--v-border-color), 0.22);
}
.customizer-skins {
.custom-input.active {
.customizer-skins-icon-wrapper {
background-color: rgba(var(--v-global-theme-primary), var(--v-selected-opacity));
}
}
}
.app-customizer-primary-colors {
.primary-color-wrapper:not(.active) {
&:hover {
outline-color: rgba(var(--v-border-color), 0.22) !important;
}
}
}
}
.app-customizer-toggler {
position: fixed !important;
inset-block-start: 20%;
inset-inline-end: 0;
}
</style>

View File

@@ -0,0 +1,57 @@
<script setup>
import { useConfigStore } from '@core/stores/config'
const props = defineProps({
themes: {
type: Array,
required: true,
},
})
const configStore = useConfigStore()
const selectedItem = ref([configStore.theme])
// Update icon if theme is changed from other sources
watch(() => configStore.theme, () => {
selectedItem.value = [configStore.theme]
}, { deep: true })
</script>
<template>
<IconBtn color="rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))">
<VIcon :icon="props.themes.find(t => t.name === configStore.theme)?.icon" />
<VTooltip
activator="parent"
open-delay="1000"
scroll-strategy="close"
>
<span class="text-capitalize">{{ configStore.theme }}</span>
</VTooltip>
<VMenu
activator="parent"
offset="12px"
:width="180"
>
<VList
v-model:selected="selectedItem"
mandatory
>
<VListItem
v-for="{ name, icon } in props.themes"
:key="name"
:value="name"
:prepend-icon="icon"
color="primary"
@click="() => { configStore.theme = name }"
>
<VListItemTitle class="text-capitalize">
{{ name }}
</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>

View File

@@ -0,0 +1,171 @@
<script setup>
import { Placeholder } from '@tiptap/extension-placeholder'
import { TextAlign } from '@tiptap/extension-text-align'
import { Underline } from '@tiptap/extension-underline'
import { StarterKit } from '@tiptap/starter-kit'
import {
EditorContent,
useEditor,
} from '@tiptap/vue-3'
const props = defineProps({
modelValue: {
type: String,
required: true,
},
placeholder: {
type: String,
required: false,
},
})
const emit = defineEmits(['update:modelValue'])
const editorRef = ref()
const editor = useEditor({
content: props.modelValue,
extensions: [
StarterKit,
TextAlign.configure({
types: [
'heading',
'paragraph',
],
}),
Placeholder.configure({ placeholder: props.placeholder ?? 'Write something here...' }),
Underline,
],
onUpdate() {
if (!editor.value)
return
emit('update:modelValue', editor.value.getHTML())
},
})
watch(() => props.modelValue, () => {
const isSame = editor.value?.getHTML() === props.modelValue
if (isSame)
return
editor.value?.commands.setContent(props.modelValue)
})
</script>
<template>
<div>
<div
v-if="editor"
class="d-flex gap-2 py-2 px-6 flex-wrap align-center editor"
>
<IconBtn
size="small"
rounded
:variant="editor.isActive('bold') ? 'tonal' : 'text'"
:color="editor.isActive('bold') ? 'primary' : 'default'"
@click="editor.chain().focus().toggleBold().run()"
>
<VIcon icon="tabler-bold" />
</IconBtn>
<IconBtn
size="small"
rounded
:variant="editor.isActive('underline') ? 'tonal' : 'text'"
:color="editor.isActive('underline') ? 'primary' : 'default'"
@click="editor.commands.toggleUnderline()"
>
<VIcon icon="tabler-underline" />
</IconBtn>
<IconBtn
size="small"
rounded
:variant="editor.isActive('italic') ? 'tonal' : 'text'"
:color="editor.isActive('italic') ? 'primary' : 'default'"
@click="editor.chain().focus().toggleItalic().run()"
>
<VIcon
icon="tabler-italic"
class="font-weight-medium"
/>
</IconBtn>
<IconBtn
size="small"
rounded
:variant="editor.isActive('strike') ? 'tonal' : 'text'"
:color="editor.isActive('strike') ? 'primary' : 'default'"
@click="editor.chain().focus().toggleStrike().run()"
>
<VIcon icon="tabler-strikethrough" />
</IconBtn>
<IconBtn
size="small"
rounded
:variant="editor.isActive({ textAlign: 'left' }) ? 'tonal' : 'text'"
:color="editor.isActive({ textAlign: 'left' }) ? 'primary' : 'default'"
@click="editor.chain().focus().setTextAlign('left').run()"
>
<VIcon icon="tabler-align-left" />
</IconBtn>
<IconBtn
size="small"
rounded
:color="editor.isActive({ textAlign: 'center' }) ? 'primary' : 'default'"
:variant="editor.isActive({ textAlign: 'center' }) ? 'tonal' : 'text'"
@click="editor.chain().focus().setTextAlign('center').run()"
>
<VIcon icon="tabler-align-center" />
</IconBtn>
<IconBtn
size="small"
rounded
:variant="editor.isActive({ textAlign: 'right' }) ? 'tonal' : 'text'"
:color="editor.isActive({ textAlign: 'right' }) ? 'primary' : 'default'"
@click="editor.chain().focus().setTextAlign('right').run()"
>
<VIcon icon="tabler-align-right" />
</IconBtn>
<IconBtn
size="small"
rounded
:variant="editor.isActive({ textAlign: 'justify' }) ? 'tonal' : 'text'"
:color="editor.isActive({ textAlign: 'justify' }) ? 'primary' : 'default'"
@click="editor.chain().focus().setTextAlign('justify').run()"
>
<VIcon icon="tabler-align-justified" />
</IconBtn>
</div>
<VDivider />
<EditorContent
ref="editorRef"
:editor="editor"
/>
</div>
</template>
<style lang="scss">
.ProseMirror {
padding: 0.5rem;
min-block-size: 15vh;
outline: none;
p {
margin-block-end: 0;
}
p.is-editor-empty:first-child::before {
block-size: 0;
color: #adb5bd;
content: attr(data-placeholder);
float: inline-start;
pointer-events: none;
}
}
</style>

View File

@@ -0,0 +1,58 @@
<script setup>
defineOptions({
name: 'AppAutocomplete',
inheritAttrs: false,
})
// const { class: _class, label, variant: _, ...restAttrs } = useAttrs()
const elementId = computed(() => {
const attrs = useAttrs()
const _elementIdToken = attrs.id
const _id = useId()
return _elementIdToken ? `app-autocomplete-${ _elementIdToken }` : _id
})
const label = computed(() => useAttrs().label)
</script>
<template>
<div
class="app-autocomplete flex-grow-1"
:class="$attrs.class"
>
<VLabel
v-if="label"
:for="elementId"
class="mb-1 text-body-2"
:text="label"
/>
<VAutocomplete
v-bind="{
...$attrs,
class: null,
label: undefined,
id: elementId,
variant: 'outlined',
menuProps: {
contentClass: [
'app-inner-list',
'app-autocomplete__content',
'v-autocomplete__content',
],
},
}"
>
<template
v-for="(_, name) in $slots"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps || {}"
/>
</template>
</VAutocomplete>
</div>
</template>

View File

@@ -0,0 +1,58 @@
<script setup>
defineOptions({
name: 'AppCombobox',
inheritAttrs: false,
})
const elementId = computed(() => {
const attrs = useAttrs()
const _elementIdToken = attrs.id
const _id = useId()
return _elementIdToken ? `app-combobox-${ _elementIdToken }` : _id
})
const label = computed(() => useAttrs().label)
</script>
<template>
<div
class="app-combobox flex-grow-1"
:class="$attrs.class"
>
<VLabel
v-if="label"
:for="elementId"
class="mb-1 text-body-2"
:text="label"
/>
<VCombobox
v-bind="{
...$attrs,
class: null,
label: undefined,
variant: 'outlined',
id: elementId,
menuProps: {
contentClass: [
'app-inner-list',
'app-combobox__content',
'v-combobox__content',
$attrs.multiple !== undefined ? 'v-list-select-multiple' : '',
],
},
}"
>
<template
v-for="(_, name) in $slots"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps || {}"
/>
</template>
</VCombobox>
</div>
</template>

View File

@@ -0,0 +1,539 @@
<script setup>
import FlatPickr from 'vue-flatpickr-component'
import { useTheme } from 'vuetify'
import {
VField,
makeVFieldProps,
} from 'vuetify/lib/components/VField/VField'
import {
VInput,
makeVInputProps,
} from 'vuetify/lib/components/VInput/VInput'
import { filterInputAttrs } from 'vuetify/lib/util/helpers'
import { useConfigStore } from '@core/stores/config'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
autofocus: Boolean,
counter: [
Boolean,
Number,
String,
],
counterValue: Function,
prefix: String,
placeholder: String,
persistentPlaceholder: Boolean,
persistentCounter: Boolean,
suffix: String,
type: {
type: String,
default: 'text',
},
modelModifiers: Object,
...makeVInputProps({
density: 'comfortable',
hideDetails: 'auto',
}),
...makeVFieldProps({
variant: 'outlined',
color: 'primary',
}),
})
const emit = defineEmits([
'click:control',
'mousedown:control',
'update:focused',
'update:modelValue',
'click:clear',
])
defineOptions({
inheritAttrs: false,
})
const configStore = useConfigStore()
const attrs = useAttrs()
const [rootAttrs, compAttrs] = filterInputAttrs(attrs)
const inputProps = ref(VInput.filterProps(props))
const fieldProps = ref(VField.filterProps(props))
const refFlatPicker = ref()
const { focused } = useFocus(refFlatPicker)
const isCalendarOpen = ref(false)
const isInlinePicker = ref(false)
// flat picker prop manipulation
if (compAttrs.config && compAttrs.config.inline) {
isInlinePicker.value = compAttrs.config.inline
Object.assign(compAttrs, { altInputClass: 'inlinePicker' })
}
compAttrs.config = {
...compAttrs.config,
prevArrow: '<i class="tabler-chevron-left v-icon" style="font-size: 20px; height: 20px; width: 20px;"></i>',
nextArrow: '<i class="tabler-chevron-right v-icon" style="font-size: 20px; height: 20px; width: 20px;"></i>',
}
const onClear = el => {
el.stopPropagation()
nextTick(() => {
emit('update:modelValue', '')
emit('click:clear', el)
})
}
const vuetifyTheme = useTheme()
const vuetifyThemesName = Object.keys(vuetifyTheme.themes.value)
// Themes class added to flat-picker component for light and dark support
const updateThemeClassInCalendar = () => {
// Flatpickr don't render it's instance in mobile and device simulator
if (!refFlatPicker.value.fp.calendarContainer)
return
vuetifyThemesName.forEach(t => {
refFlatPicker.value.fp.calendarContainer.classList.remove(`v-theme--${ t }`)
})
refFlatPicker.value.fp.calendarContainer.classList.add(`v-theme--${ vuetifyTheme.global.name.value }`)
}
watch(() => configStore.theme, updateThemeClassInCalendar)
onMounted(() => {
updateThemeClassInCalendar()
})
const emitModelValue = val => {
emit('update:modelValue', val)
}
watch(() => props, () => {
fieldProps.value = VField.filterProps(props)
inputProps.value = VInput.filterProps(props)
}, {
deep: true,
immediate: true,
})
const elementId = computed(() => {
const _elementIdToken = fieldProps.value.id || fieldProps.value.label || inputProps.value.id
const _id = useId()
return _elementIdToken ? `app-picker-field-${ _elementIdToken }` : _id
})
</script>
<template>
<div class="app-picker-field">
<!-- v-input -->
<VLabel
v-if="fieldProps.label"
class="mb-1 text-body-2"
:for="elementId"
:text="fieldProps.label"
/>
<VInput
v-bind="{ ...inputProps, ...rootAttrs }"
:model-value="props.modelValue"
:hide-details="props.hideDetails"
:class="[{
'v-text-field--prefixed': props.prefix,
'v-text-field--suffixed': props.suffix,
'v-text-field--flush-details': ['plain', 'underlined'].includes(props.variant),
}, props.class]"
class="position-relative v-text-field"
:style="props.style"
>
<template #default="{ id, isDirty, isValid, isDisabled, isReadonly, validate }">
<!-- v-field -->
<VField
v-bind="{ ...fieldProps, label: undefined }"
:id="id.value"
role="textbox"
:active="focused || isDirty.value || isCalendarOpen"
:focused="focused || isCalendarOpen"
:dirty="isDirty.value || props.dirty"
:error="isValid.value === false"
:disabled="isDisabled.value"
@click:clear="onClear"
>
<template #default="{ props: vFieldProps }">
<div v-bind="vFieldProps">
<!-- flat-picker -->
<FlatPickr
v-if="!isInlinePicker"
v-bind="compAttrs"
ref="refFlatPicker"
:model-value="props.modelValue"
:placeholder="props.placeholder"
:readonly="isReadonly.value"
class="flat-picker-custom-style h-100 w-100"
:disabled="isReadonly.value"
@on-open="isCalendarOpen = true"
@on-close="isCalendarOpen = false; validate()"
@update:model-value="emitModelValue"
/>
<!-- simple input for inline prop -->
<input
v-if="isInlinePicker"
:value="props.modelValue"
:placeholder="props.placeholder"
:readonly="isReadonly.value"
class="flat-picker-custom-style h-100 w-100"
type="text"
>
</div>
</template>
</VField>
</template>
</VInput>
<!-- flat picker for inline props -->
<FlatPickr
v-if="isInlinePicker"
v-bind="compAttrs"
ref="refFlatPicker"
:model-value="props.modelValue"
@update:model-value="emitModelValue"
@on-open="isCalendarOpen = true"
@on-close="isCalendarOpen = false"
/>
</div>
</template>
<style lang="scss">
@use "@core/scss/template/mixins" as templateMixins;
/* stylelint-disable no-descending-specificity */
@use "flatpickr/dist/flatpickr.css";
@use "@core/scss/base/mixins";
.flat-picker-custom-style {
position: absolute;
color: inherit;
inline-size: 100%;
inset: 0;
outline: none;
padding-block: 0;
padding-inline: var(--v-field-padding-start);
}
$heading-color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
$body-color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
$disabled-color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity));
// hide the input when your picker is inline
input[altinputclass="inlinePicker"] {
display: none;
}
.flatpickr-time input.flatpickr-hour {
font-weight: 400;
}
.flatpickr-calendar {
@include mixins.elevation(6);
background-color: rgb(var(--v-theme-surface));
inline-size: 16.875rem;
.flatpickr-day:focus {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgba(var(--v-border-color), var(--v-border-opacity));
}
.flatpickr-rContainer {
.flatpickr-weekdays {
block-size: 1.25rem;
padding-inline: 0.5625rem;
}
.flatpickr-days {
min-inline-size: 16.875rem;
.dayContainer {
justify-content: center !important;
inline-size: 16.875rem;
min-inline-size: 16.875rem;
padding-block: 0.75rem 0.5rem;
.flatpickr-day {
block-size: 2.25rem;
font-size: 0.9375rem;
line-height: 2.25rem;
margin-block-start: 0 !important;
max-inline-size: 2.25rem;
}
}
}
}
.flatpickr-day {
color: $body-color;
&.today {
&:not(.selected) {
border: none !important;
background: rgba(var(--v-theme-primary), 0.24);
color: rgb(var(--v-theme-primary));
}
&:hover {
border: none !important;
background: rgba(var(--v-theme-primary), 0.24);
color: rgb(var(--v-theme-primary));
}
}
&.selected,
&.selected:hover {
border-color: rgb(var(--v-theme-primary));
background: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
@include templateMixins.custom-elevation(var(--v-theme-primary), "sm");
}
&.inRange,
&.inRange:hover {
border: none;
background: rgba(var(--v-theme-primary), var(--v-activated-opacity)) !important;
box-shadow: none !important;
color: rgb(var(--v-theme-primary));
}
&.startRange {
@include templateMixins.custom-elevation(var(--v-theme-primary), "sm");
}
&.endRange {
@include templateMixins.custom-elevation(var(--v-theme-primary), "sm");
}
&.startRange,
&.endRange,
&.startRange:hover,
&.endRange:hover {
border-color: rgb(var(--v-theme-primary));
background: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
}
&.selected.startRange + .endRange:not(:nth-child(7n + 1)),
&.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
&.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
box-shadow: -10px 0 0 rgb(var(--v-theme-primary));
}
&.flatpickr-disabled,
&.prevMonthDay:not(.startRange,.inRange),
&.nextMonthDay:not(.endRange,.inRange) {
opacity: var(--v-disabled-opacity);
}
&:hover {
border-color: transparent;
background: rgba(var(--v-theme-on-surface), 0.06);
}
}
.flatpickr-weekday {
color: $heading-color;
font-size: 0.8125rem;
font-weight: 400;
inline-size: 2.25rem;
line-height: 1.25rem;
}
.flatpickr-days {
inline-size: 16.875rem;
}
&::after,
&::before {
display: none;
}
.flatpickr-months {
.flatpickr-prev-month,
.flatpickr-next-month {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
fill: $body-color;
&:hover {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
&:hover i,
&:hover svg {
fill: $body-color;
}
}
}
.flatpickr-current-month span.cur-month {
font-weight: 300;
}
&.open {
// Open calendar above overlay
z-index: 2401;
}
&.hasTime.open {
.flatpickr-innerContainer + .flatpickr-time {
block-size: auto;
border-block-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.flatpickr-time {
border-block-start: none;
}
.flatpickr-hour,
.flatpickr-minute,
.flatpickr-am-pm {
font-size: 0.9375rem;
}
}
}
.v-theme--dark .flatpickr-calendar {
box-shadow: 0 3px 14px 0 rgb(15 20 34 / 38%);
}
// Time picker hover & focus bg color
.flatpickr-time input:hover,
.flatpickr-time .flatpickr-am-pm:hover,
.flatpickr-time input:focus,
.flatpickr-time .flatpickr-am-pm:focus {
background: transparent;
}
// Time picker
.flatpickr-time {
.flatpickr-am-pm,
.flatpickr-time-separator,
input {
color: $body-color;
}
.numInputWrapper {
span {
&.arrowUp {
&::after {
border-block-end-color: rgb(var(--v-border-color));
}
}
&.arrowDown {
&::after {
border-block-start-color: rgb(var(--v-border-color));
}
}
}
}
}
// Added bg color for flatpickr input only as it has default readonly attribute
.flatpickr-input[readonly],
.flatpickr-input ~ .form-control[readonly],
.flatpickr-human-friendly[readonly] {
background-color: inherit;
}
// week sections
.flatpickr-weekdays {
margin-block: 0.375rem;
}
// Month and year section
.flatpickr-current-month {
.flatpickr-monthDropdown-months {
appearance: none;
}
.flatpickr-monthDropdown-months,
.numInputWrapper {
padding: 2px;
border-radius: 4px;
color: $heading-color;
font-size: 0.9375rem;
font-weight: 400;
line-height: 1.375rem;
transition: all 0.15s ease-out;
span {
display: none;
}
.flatpickr-monthDropdown-month {
background-color: rgb(var(--v-theme-surface));
}
.numInput.cur-year {
font-weight: 400;
}
}
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover {
color: $body-color;
}
.flatpickr-months {
padding-block: 0.75rem;
padding-inline: 1rem;
.flatpickr-prev-month,
.flatpickr-next-month {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 5rem;
background: rgba(var(--v-theme-on-surface), var(--v-selected-opacity));
block-size: 1.875rem;
inline-size: 1.875rem;
inset-block-start: 15px !important;
&.flatpickr-disabled {
display: inline;
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
}
.flatpickr-next-month {
inset-inline-end: 1.05rem !important;
}
.flatpickr-prev-month {
/* stylelint-disable-next-line liberty/use-logical-spec */
right: 3.65rem;
left: unset !important;
}
.flatpickr-month {
display: flex;
align-items: center;
block-size: 2.125rem;
.flatpickr-current-month {
display: flex;
align-items: center;
padding: 0;
block-size: 1.75rem;
inset-inline-start: 0;
text-align: start;
}
}
}
</style>

View File

@@ -0,0 +1,51 @@
<script setup>
defineOptions({
name: 'AppSelect',
inheritAttrs: false,
})
const elementId = computed(() => {
const attrs = useAttrs()
const _elementIdToken = attrs.id
const _id = useId()
return _elementIdToken ? `app-select-${ _elementIdToken }` : _id
})
const label = computed(() => useAttrs().label)
</script>
<template>
<div
class="app-select flex-grow-1"
:class="$attrs.class"
>
<VLabel
v-if="label"
:for="elementId"
class="mb-1 text-body-2"
style="line-height: 15px;"
:text="label"
/>
<VSelect
v-bind="{
...$attrs,
class: null,
label: undefined,
variant: 'outlined',
id: elementId,
menuProps: { contentClass: ['app-inner-list', 'app-select__content', 'v-select__content', $attrs.multiple !== undefined ? 'v-list-select-multiple' : ''] },
}"
>
<template
v-for="(_, name) in $slots"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps || {}"
/>
</template>
</VSelect>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup>
defineOptions({
name: 'AppTextField',
inheritAttrs: false,
})
const elementId = computed(() => {
const attrs = useAttrs()
const _elementIdToken = attrs.id
const _id = useId()
return _elementIdToken ? `app-text-field-${ _elementIdToken }` : _id
})
const label = computed(() => useAttrs().label)
</script>
<template>
<div
class="app-text-field flex-grow-1"
:class="$attrs.class"
>
<VLabel
v-if="label"
:for="elementId"
class="mb-1 text-body-2 text-wrap"
style="line-height: 15px;"
:text="label"
/>
<VTextField
v-bind="{
...$attrs,
class: null,
label: undefined,
variant: 'outlined',
id: elementId,
}"
>
<template
v-for="(_, name) in $slots"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps || {}"
/>
</template>
</VTextField>
</div>
</template>

View File

@@ -0,0 +1,51 @@
<script setup>
defineOptions({
name: 'AppTextarea',
inheritAttrs: false,
})
// const { class: _class, label, variant: _, ...restAttrs } = useAttrs()
const elementId = computed(() => {
const attrs = useAttrs()
const _elementIdToken = attrs.id
const _id = useId()
return _elementIdToken ? `app-textarea-${ _elementIdToken }` : _id
})
const label = computed(() => useAttrs().label)
</script>
<template>
<div
class="app-textarea flex-grow-1"
:class="$attrs.class"
>
<VLabel
v-if="label"
:for="elementId"
class="mb-1 text-body-2"
:text="label"
/>
<VTextarea
v-bind="{
...$attrs,
class: null,
label: undefined,
variant: 'outlined',
id: elementId,
}"
>
<template
v-for="(_, name) in $slots"
#[name]="slotProps"
>
<slot
:name="name"
v-bind="slotProps || {}"
/>
</template>
</VTextarea>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script setup>
const props = defineProps({
selectedCheckbox: {
type: Array,
required: true,
},
checkboxContent: {
type: Array,
required: true,
},
gridColumn: {
type: null,
required: false,
},
})
const emit = defineEmits(['update:selectedCheckbox'])
const updateSelectedOption = value => {
if (typeof value !== 'boolean' && value !== null)
emit('update:selectedCheckbox', value)
}
</script>
<template>
<VRow
v-if="props.checkboxContent && props.selectedCheckbox"
class="custom-input-wrapper"
>
<VCol
v-for="item in props.checkboxContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-checkbox rounded cursor-pointer"
:class="props.selectedCheckbox.includes(item.value) ? 'active' : ''"
>
<div>
<VCheckbox
:model-value="props.selectedCheckbox"
:value="item.value"
@update:model-value="updateSelectedOption"
/>
</div>
<slot :item="item">
<div class="flex-grow-1">
<div class="d-flex align-center mb-2">
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<VSpacer />
<span
v-if="item.subtitle"
class="text-disabled text-body-2"
>{{ item.subtitle }}</span>
</div>
<p class="text-sm mb-0">
{{ item.desc }}
</p>
</div>
</slot>
</VLabel>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.custom-checkbox {
display: flex;
align-items: flex-start;
gap: 0.5rem;
.v-checkbox {
margin-block-start: -0.375rem;
}
.cr-title {
font-weight: 500;
line-height: 1.375rem;
}
}
</style>

View File

@@ -0,0 +1,97 @@
<script setup>
const props = defineProps({
selectedCheckbox: {
type: Array,
required: true,
},
checkboxContent: {
type: Array,
required: true,
},
gridColumn: {
type: null,
required: false,
},
})
const emit = defineEmits(['update:selectedCheckbox'])
const updateSelectedOption = value => {
if (typeof value !== 'boolean' && value !== null)
emit('update:selectedCheckbox', value)
}
</script>
<template>
<VRow
v-if="props.checkboxContent && props.selectedCheckbox"
class="custom-input-wrapper"
>
<VCol
v-for="item in props.checkboxContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-checkbox-icon rounded cursor-pointer"
:class="props.selectedCheckbox.includes(item.value) ? 'active' : ''"
>
<slot :item="item">
<div class="d-flex flex-column align-center text-center gap-2">
<VIcon
v-bind="item.icon"
class="text-high-emphasis"
/>
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<p class="text-sm clamp-text mb-0">
{{ item.desc }}
</p>
</div>
</slot>
<div>
<VCheckbox
:model-value="props.selectedCheckbox"
:value="item.value"
@update:model-value="updateSelectedOption"
/>
</div>
</VLabel>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.custom-checkbox-icon {
display: flex;
flex-direction: column;
gap: 0.5rem;
.v-checkbox {
margin-block-end: -0.375rem;
.v-selection-control__wrapper {
margin-inline-start: 0;
}
}
.cr-title {
font-weight: 500;
line-height: 1.375rem;
}
}
</style>
<style lang="scss">
.custom-checkbox-icon {
.v-checkbox {
margin-block-end: -0.375rem;
.v-selection-control__wrapper {
margin-inline-start: 0;
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
<script setup>
const props = defineProps({
selectedCheckbox: {
type: Array,
required: true,
},
checkboxContent: {
type: Array,
required: true,
},
gridColumn: {
type: null,
required: false,
},
})
const emit = defineEmits(['update:selectedCheckbox'])
const updateSelectedOption = value => {
if (typeof value !== 'boolean' && value !== null)
emit('update:selectedCheckbox', value)
}
</script>
<template>
<VRow
v-if="props.checkboxContent && props.selectedCheckbox"
class="custom-input-wrapper"
>
<VCol
v-for="item in props.checkboxContent"
:key="item.value"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-checkbox rounded cursor-pointer w-100"
:class="props.selectedCheckbox.includes(item.value) ? 'active' : ''"
>
<div>
<VCheckbox
:id="`custom-checkbox-with-img-${item.value}`"
:model-value="props.selectedCheckbox"
:value="item.value"
@update:model-value="updateSelectedOption"
/>
</div>
<img
:src="item.bgImage"
alt="bg-img"
class="custom-checkbox-image"
>
</VLabel>
<VLabel
v-if="item.label || $slots.label"
:for="`custom-checkbox-with-img-${item.value}`"
class="cursor-pointer"
>
<slot
name="label"
:label="item.label"
>
{{ item.label }}
</slot>
</VLabel>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.custom-checkbox {
position: relative;
padding: 0;
.custom-checkbox-image {
block-size: 100%;
inline-size: 100%;
min-inline-size: 100%;
}
.v-checkbox {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
visibility: hidden;
}
&:hover,
&.active {
.v-checkbox {
visibility: visible;
}
}
}
</style>

View File

@@ -0,0 +1,86 @@
<script setup>
const props = defineProps({
selectedRadio: {
type: String,
required: true,
},
radioContent: {
type: Array,
required: true,
},
gridColumn: {
type: null,
required: false,
},
})
const emit = defineEmits(['update:selectedRadio'])
const updateSelectedOption = value => {
if (value !== null)
emit('update:selectedRadio', value)
}
</script>
<template>
<VRadioGroup
v-if="props.radioContent"
:model-value="props.selectedRadio"
class="custom-input-wrapper"
@update:model-value="updateSelectedOption"
>
<VRow>
<VCol
v-for="item in props.radioContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-radio rounded cursor-pointer"
:class="props.selectedRadio === item.value ? 'active' : ''"
>
<div>
<VRadio
:name="item.value"
:value="item.value"
/>
</div>
<slot :item="item">
<div class="flex-grow-1">
<div class="d-flex align-center mb-2">
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<VSpacer />
<span
v-if="item.subtitle"
class="text-disabled text-body-2"
>{{ item.subtitle }}</span>
</div>
<p class="text-body-2 mb-0">
{{ item.desc }}
</p>
</div>
</slot>
</VLabel>
</VCol>
</VRow>
</VRadioGroup>
</template>
<style lang="scss" scoped>
.custom-radio {
display: flex;
align-items: flex-start;
gap: 0.25rem;
.v-radio {
margin-block-start: -0.45rem;
}
.cr-title {
font-weight: 500;
line-height: 1.375rem;
}
}
</style>

View File

@@ -0,0 +1,89 @@
<script setup>
const props = defineProps({
selectedRadio: {
type: String,
required: true,
},
radioContent: {
type: Array,
required: true,
},
gridColumn: {
type: null,
required: false,
},
})
const emit = defineEmits(['update:selectedRadio'])
const updateSelectedOption = value => {
if (value !== null)
emit('update:selectedRadio', value)
}
</script>
<template>
<VRadioGroup
v-if="props.radioContent"
:model-value="props.selectedRadio"
class="custom-input-wrapper"
@update:model-value="updateSelectedOption"
>
<VRow>
<VCol
v-for="item in props.radioContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-radio-icon rounded cursor-pointer"
:class="props.selectedRadio === item.value ? 'active' : ''"
>
<slot :item="item">
<div class="d-flex flex-column align-center text-center gap-2">
<VIcon
v-bind="item.icon"
class="text-high-emphasis"
/>
<h6 class="text-h6">
{{ item.title }}
</h6>
<p class="text-body-2 mb-0">
{{ item.desc }}
</p>
</div>
</slot>
<div>
<VRadio :value="item.value" />
</div>
</VLabel>
</VCol>
</VRow>
</VRadioGroup>
</template>
<style lang="scss" scoped>
.custom-radio-icon {
display: flex;
flex-direction: column;
gap: 0.5rem;
.v-radio {
margin-block-end: -0.5rem;
}
}
</style>
<style lang="scss">
.custom-radio-icon {
.v-radio {
margin-block-end: -0.25rem;
.v-selection-control__wrapper {
margin-inline-start: 0;
}
}
}
</style>

View File

@@ -0,0 +1,102 @@
<script setup>
const props = defineProps({
selectedRadio: {
type: String,
required: true,
},
radioContent: {
type: Array,
required: true,
},
gridColumn: {
type: null,
required: false,
},
})
const emit = defineEmits(['update:selectedRadio'])
const updateSelectedOption = value => {
if (value !== null)
emit('update:selectedRadio', value)
}
</script>
<template>
<VRadioGroup
v-if="props.radioContent"
:model-value="props.selectedRadio"
class="custom-input-wrapper"
@update:model-value="updateSelectedOption"
>
<VRow>
<VCol
v-for="item in props.radioContent"
:key="item.bgImage"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-radio rounded cursor-pointer w-100"
:class="props.selectedRadio === item.value ? 'active' : ''"
>
<slot
name="content"
:item="item"
>
<template v-if="typeof item.bgImage === 'object'">
<Component
:is="item.bgImage"
class="custom-radio-image"
/>
</template>
<img
v-else
:src="item.bgImage"
alt="bg-img"
class="custom-radio-image"
>
</slot>
<VRadio
:id="`custom-radio-with-img-${item.value}`"
:name="`custom-radio-with-img-${item.value}`"
:value="item.value"
/>
</VLabel>
<VLabel
v-if="item.label || $slots.label"
:for="`custom-radio-with-img-${item.value}`"
class="cursor-pointer"
>
<slot
name="label"
:label="item.label"
>
{{ item.label }}
</slot>
</VLabel>
</VCol>
</VRow>
</VRadioGroup>
</template>
<style lang="scss" scoped>
.custom-radio {
padding: 0 !important;
&.active {
border-width: 1px;
}
.custom-radio-image {
block-size: 100%;
inline-size: 100%;
min-inline-size: 100%;
}
.v-radio {
visibility: hidden;
}
}
</style>

View File

@@ -0,0 +1,183 @@
<script setup>
const props = defineProps({
collapsed: {
type: Boolean,
required: false,
default: false,
},
noActions: {
type: Boolean,
required: false,
default: false,
},
actionCollapsed: {
type: Boolean,
required: false,
default: false,
},
actionRefresh: {
type: Boolean,
required: false,
default: false,
},
actionRemove: {
type: Boolean,
required: false,
default: false,
},
loading: {
type: Boolean,
required: false,
skipCheck: true,
default: undefined,
},
title: {
type: String,
required: false,
default: undefined,
},
})
const emit = defineEmits([
'collapsed',
'refresh',
'trash',
'initialLoad',
'update:loading',
])
defineOptions({
inheritAttrs: false,
})
const _loading = ref(false)
const $loading = computed({
get() {
return props.loading !== undefined ? props.loading : _loading.value
},
set(value) {
props.loading !== undefined ? emit('update:loading', value) : _loading.value = value
},
})
const isContentCollapsed = ref(props.collapsed)
const isCardRemoved = ref(false)
// stop loading
const stopLoading = () => {
$loading.value = false
}
// trigger collapse
const triggerCollapse = () => {
isContentCollapsed.value = !isContentCollapsed.value
emit('collapsed', isContentCollapsed.value)
}
// trigger refresh
const triggerRefresh = () => {
$loading.value = true
emit('refresh', stopLoading)
}
// trigger removal
const triggeredRemove = () => {
isCardRemoved.value = true
emit('trash')
}
</script>
<template>
<VExpandTransition>
<!-- TODO remove div when transition work with v-card components: https://github.com/vuetifyjs/vuetify/issues/15111 -->
<div v-if="!isCardRemoved">
<VCard v-bind="$attrs">
<VCardItem>
<VCardTitle v-if="props.title || $slots.title">
<!-- 👉 Title slot and prop -->
<slot name="title">
{{ props.title }}
</slot>
</VCardTitle>
<template #append>
<!-- 👉 Before actions slot -->
<div>
<slot name="before-actions" />
<!-- SECTION Actions buttons -->
<!-- 👉 Collapse button -->
<IconBtn
v-if="(!(actionRemove || actionRefresh) || actionCollapsed) && !noActions"
@click="triggerCollapse"
>
<VIcon
size="20"
icon="tabler-chevron-up"
:style="{ transform: isContentCollapsed ? 'rotate(-180deg)' : undefined }"
style="transition-duration: 0.28s;"
/>
</IconBtn>
<!-- 👉 Overlay button -->
<IconBtn
v-if="(!(actionRemove || actionCollapsed) || actionRefresh) && !noActions"
@click="triggerRefresh"
>
<VIcon
size="20"
icon="tabler-refresh"
/>
</IconBtn>
<!-- 👉 Close button -->
<IconBtn
v-if="(!(actionRefresh || actionCollapsed) || actionRemove) && !noActions"
@click="triggeredRemove"
>
<VIcon
size="20"
icon="tabler-x"
/>
</IconBtn>
</div>
<!-- !SECTION -->
</template>
</VCardItem>
<!-- 👉 card content -->
<VExpandTransition>
<div
v-show="!isContentCollapsed"
class="v-card-content"
>
<slot />
</div>
</VExpandTransition>
<!-- 👉 Overlay -->
<VOverlay
v-model="$loading"
contained
persistent
scroll-strategy="none"
class="align-center justify-center"
>
<VProgressCircular indeterminate />
</VOverlay>
</VCard>
</div>
</VExpandTransition>
</template>
<style lang="scss">
.v-card-item {
+.v-card-content {
.v-card-text:first-child {
padding-block-start: 0;
}
}
}
</style>

View File

@@ -0,0 +1,159 @@
<script setup>
import { getSingletonHighlighter } from 'shiki'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const props = defineProps({
title: {
type: String,
required: true,
},
code: {
type: Object,
required: true,
},
codeLanguage: {
type: String,
required: false,
default: 'markup',
},
noPadding: {
type: Boolean,
required: false,
default: false,
},
})
const preferredCodeLanguage = useCookie('preferredCodeLanguage', {
default: () => 'ts',
maxAge: COOKIE_MAX_AGE_1_YEAR,
})
const isCodeShown = ref(false)
const { copy, copied } = useClipboard({ source: computed(() => props.code[preferredCodeLanguage.value]) })
const highlighter = await getSingletonHighlighter({
themes: [
'dracula',
'dracula-soft',
],
langs: ['vue'],
})
const codeSnippet = computed(() => highlighter.codeToHtml(props.code[preferredCodeLanguage.value], {
lang: 'vue',
theme: 'dracula',
}))
</script>
<template>
<!-- eslint-disable regex/invalid -->
<VCard class="app-card-code">
<VCardItem>
<VCardTitle>{{ props.title }}</VCardTitle>
<template #append>
<IconBtn
size="small"
:color="isCodeShown ? 'primary' : 'default'"
:class="isCodeShown ? '' : 'text-disabled'"
@click="isCodeShown = !isCodeShown"
>
<VIcon
size="20"
icon="tabler-code"
/>
</IconBtn>
</template>
</VCardItem>
<slot v-if="noPadding" />
<VCardText v-else>
<slot />
</VCardText>
<VExpandTransition>
<div v-show="isCodeShown">
<VDivider />
<VCardText class="d-flex gap-y-3 flex-column">
<div class="d-flex justify-end">
<VBtnToggle
v-model="preferredCodeLanguage"
mandatory
density="compact"
>
<VBtn
value="ts"
icon
:variant="preferredCodeLanguage === 'ts' ? 'tonal' : 'text'"
:color="preferredCodeLanguage === 'ts' ? 'primary' : ''"
>
<VIcon
icon="mdi-language-typescript"
:color="preferredCodeLanguage === 'ts' ? 'primary' : 'secondary'"
/>
</VBtn>
<VBtn
value="js"
icon
:variant="preferredCodeLanguage === 'js' ? 'tonal' : 'text'"
:color="preferredCodeLanguage === 'js' ? 'primary' : ''"
>
<VIcon
icon="mdi-language-javascript"
:color="preferredCodeLanguage === 'js' ? 'primary' : 'secondary'"
/>
</VBtn>
</VBtnToggle>
</div>
<div class="position-relative">
<PerfectScrollbar
style="border-radius: 6px;max-block-size: 500px;"
:options="{ wheelPropagation: false, suppressScrollX: false }"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="codeSnippet" />
</PerfectScrollbar>
<IconBtn
class="position-absolute app-card-code-copy-icon"
color="white"
@click="() => { copy() }"
>
<VIcon
:icon="copied ? 'tabler-check' : 'tabler-copy'"
size="20"
/>
</IconBtn>
</div>
</VCardText>
</div>
</VExpandTransition>
</VCard>
</template>
<style lang="scss">
@use "@styles/variables/vuetify";
code[class*="language-"],
pre[class*="language-"] {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 14px;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
border-radius: vuetify.$card-border-radius;
max-block-size: 500px;
}
.app-card-code-copy-icon {
inset-block-start: 1.2em;
inset-inline-end: 0.8em;
}
.app-card-code {
.shiki {
padding: 0.75rem;
text-wrap: wrap;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
color: {
type: String,
required: false,
default: 'primary',
},
icon: {
type: String,
required: true,
},
stats: {
type: String,
required: true,
},
})
</script>
<template>
<VCard>
<VCardText class="d-flex align-center justify-space-between">
<div>
<div class="d-flex align-center flex-wrap">
<h5 class="text-h5">
{{ props.stats }}
</h5>
</div>
<div class="text-subtitle-1">
{{ props.title }}
</div>
</div>
<VAvatar
:color="props.color"
:size="42"
rounded
variant="tonal"
>
<VIcon
:icon="props.icon"
size="26"
/>
</VAvatar>
</VCardText>
</VCard>
</template>

View File

@@ -0,0 +1,66 @@
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
color: {
type: String,
required: false,
default: 'primary',
},
icon: {
type: String,
required: true,
},
stats: {
type: String,
required: true,
},
height: {
type: Number,
required: true,
},
series: {
type: Array,
required: true,
},
chartOptions: {
type: null,
required: true,
},
})
</script>
<template>
<VCard>
<VCardText class="d-flex flex-column pb-0">
<VAvatar
v-if="props.icon"
size="42"
variant="tonal"
:color="props.color"
rounded
class="mb-2"
>
<VIcon
:icon="props.icon"
size="26"
/>
</VAvatar>
<h5 class="text-h5">
{{ props.stats }}
</h5>
<div class="text-sm">
{{ props.title }}
</div>
</VCardText>
<VueApexCharts
:series="props.series"
:options="props.chartOptions"
:height="props.height"
/>
</VCard>
</template>

View File

@@ -0,0 +1,11 @@
import { stringifyQuery } from 'ufo'
export const createUrl = (url, options) => computed(() => {
if (!options?.query)
return toValue(url)
const _url = toValue(url)
const _query = toValue(options?.query)
const queryObj = Object.fromEntries(Object.entries(_query).map(([key, val]) => [key, toValue(val)]))
return `${_url}${queryObj ? `?${stringifyQuery(queryObj)}` : ''}`
})

View File

@@ -0,0 +1,26 @@
import { useTheme } from 'vuetify'
import { useConfigStore } from '@core/stores/config'
// composable function to return the image variant as per the current theme and skin
export const useGenerateImageVariant = (imgLight, imgDark, imgLightBordered, imgDarkBordered, bordered = false) => {
const configStore = useConfigStore()
const { global } = useTheme()
return computed(() => {
if (global.name.value === 'light') {
if (configStore.skin === 'bordered' && bordered)
return imgLightBordered
else
return imgLight
}
if (global.name.value === 'dark') {
if (configStore.skin === 'bordered' && bordered)
return imgDarkBordered
else
return imgDark
}
// Add a default return statement
return imgLight
})
}

View File

@@ -0,0 +1,23 @@
import { useDisplay } from 'vuetify'
export const useResponsiveLeftSidebar = (mobileBreakpoint = undefined) => {
const { mdAndDown, name: currentBreakpoint } = useDisplay()
const _mobileBreakpoint = mobileBreakpoint || mdAndDown
const isLeftSidebarOpen = ref(true)
const setInitialValue = () => {
isLeftSidebarOpen.value = !_mobileBreakpoint.value
}
// Set the initial value of sidebar
setInitialValue()
watch(currentBreakpoint, () => {
// Reset left sidebar
isLeftSidebarOpen.value = !_mobileBreakpoint.value
})
return {
isLeftSidebarOpen,
}
}

View File

@@ -0,0 +1,37 @@
import { VThemeProvider } from 'vuetify/components/VThemeProvider'
import { useConfigStore } from '@core/stores/config'
import { AppContentLayoutNav } from '@layouts/enums'
// TODO: Use `VThemeProvider` from dist instead of lib (Using this component from dist causes navbar to loose sticky positioning)
export const useSkins = () => {
const configStore = useConfigStore()
const layoutAttrs = computed(() => ({
verticalNavAttrs: {
wrapper: h(VThemeProvider, { tag: 'div' }),
wrapperProps: {
withBackground: true,
theme: (configStore.isVerticalNavSemiDark && configStore.appContentLayoutNav === AppContentLayoutNav.Vertical)
? 'dark'
: undefined,
},
},
}))
const injectSkinClasses = () => {
if (typeof document !== 'undefined') {
const bodyClasses = document.body.classList
const genSkinClass = _skin => `skin--${_skin}`
watch(() => configStore.skin, (val, oldVal) => {
bodyClasses.remove(genSkinClass(oldVal))
bodyClasses.add(genSkinClass(val))
}, { immediate: true })
}
}
return {
injectSkinClasses,
layoutAttrs,
}
}

View File

@@ -0,0 +1,18 @@
export const Skins = {
Default: 'default',
Bordered: 'bordered',
}
export const Theme = {
Light: 'light',
Dark: 'dark',
System: 'system',
}
export const Layout = {
Vertical: 'vertical',
Horizontal: 'horizontal',
Collapsed: 'collapsed',
}
export const Direction = {
Ltr: 'ltr',
Rtl: 'rtl',
}

View File

@@ -0,0 +1,40 @@
export const defineThemeConfig = userConfig => {
return {
themeConfig: userConfig,
layoutConfig: {
app: {
title: userConfig.app.title,
logo: userConfig.app.logo,
contentWidth: userConfig.app.contentWidth,
contentLayoutNav: userConfig.app.contentLayoutNav,
overlayNavFromBreakpoint: userConfig.app.overlayNavFromBreakpoint,
i18n: {
enable: userConfig.app.i18n.enable,
},
iconRenderer: userConfig.app.iconRenderer,
},
navbar: {
type: userConfig.navbar.type,
navbarBlur: userConfig.navbar.navbarBlur,
},
footer: { type: userConfig.footer.type },
verticalNav: {
isVerticalNavCollapsed: userConfig.verticalNav.isVerticalNavCollapsed,
defaultNavItemIconProps: userConfig.verticalNav.defaultNavItemIconProps,
},
horizontalNav: {
type: userConfig.horizontalNav.type,
transition: userConfig.horizontalNav.transition,
popoverOffset: userConfig.horizontalNav.popoverOffset,
},
icons: {
chevronDown: userConfig.icons.chevronDown,
chevronRight: userConfig.icons.chevronRight,
close: userConfig.icons.close,
verticalNavPinned: userConfig.icons.verticalNavPinned,
verticalNavUnPinned: userConfig.icons.verticalNavUnPinned,
sectionTitlePlaceholder: userConfig.icons.sectionTitlePlaceholder,
},
},
}
}

View File

@@ -0,0 +1,81 @@
import { useStorage } from '@vueuse/core'
import { useTheme } from 'vuetify'
import { useConfigStore } from '@core/stores/config'
import { cookieRef, namespaceConfig } from '@layouts/stores/config'
import { themeConfig } from '@themeConfig'
const _syncAppRtl = () => {
const configStore = useConfigStore()
const storedLang = cookieRef('language', null)
const { locale } = useI18n({ useScope: 'global' })
// TODO: Handle case where i18n can't read persisted value
if (locale.value !== storedLang.value && storedLang.value)
locale.value = storedLang.value
// watch and change lang attribute of html on language change
watch(locale, val => {
// Update lang attribute of html tag
if (typeof document !== 'undefined')
document.documentElement.setAttribute('lang', val)
// Store selected language in cookie
storedLang.value = val
// set isAppRtl value based on selected language
if (themeConfig.app.i18n.langConfig && themeConfig.app.i18n.langConfig.length) {
themeConfig.app.i18n.langConfig.forEach(lang => {
if (lang.i18nLang === storedLang.value)
configStore.isAppRTL = lang.isRTL
})
}
}, { immediate: true })
}
const _handleSkinChanges = () => {
const { themes } = useTheme()
const configStore = useConfigStore()
// Create skin default color so that we can revert back to original (default skin) color when switch to default skin from bordered skin
Object.values(themes.value).forEach(t => {
t.colors['skin-default-background'] = t.colors.background
t.colors['skin-default-surface'] = t.colors.surface
})
watch(() => configStore.skin, val => {
Object.values(themes.value).forEach(t => {
t.colors.background = t.colors[`skin-${val}-background`]
t.colors.surface = t.colors[`skin-${val}-surface`]
})
}, { immediate: true })
}
/*
Set current theme's surface color in localStorage
Why? Because when initial loader is shown (before vue is ready) we need to what's the current theme's surface color.
We will use color stored in localStorage to set the initial loader's background color.
With this we will be able to show correct background color for the initial loader even before vue identify the current theme.
*/
const _syncInitialLoaderTheme = () => {
const vuetifyTheme = useTheme()
watch(() => useConfigStore().theme, () => {
// We are not using theme.current.colors.surface because watcher is independent and when this watcher is ran `theme` computed is not updated
useStorage(namespaceConfig('initial-loader-bg'), null).value = vuetifyTheme.current.value.colors.surface
useStorage(namespaceConfig('initial-loader-color'), null).value = vuetifyTheme.current.value.colors.primary
}, { immediate: true })
}
const initCore = () => {
_syncInitialLoaderTheme()
_handleSkinChanges()
// We don't want to trigger i18n in SK
if (themeConfig.app.i18n.enable)
_syncAppRtl()
}
export default initCore

View File

@@ -0,0 +1,689 @@
import { hexToRgb } from '@core/utils/colorConverter'
// 👉 Colors variables
const colorVariables = themeColors => {
const themeSecondaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['medium-emphasis-opacity']})`
const themeDisabledTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['disabled-opacity']})`
const themeBorderColor = `rgba(${hexToRgb(String(themeColors.variables['border-color']))},${themeColors.variables['border-opacity']})`
const themePrimaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['high-emphasis-opacity']})`
return { themeSecondaryTextColor, themeDisabledTextColor, themeBorderColor, themePrimaryTextColor }
}
export const getScatterChartConfig = themeColors => {
const scatterColors = {
series1: '#ff9f43',
series2: '#7367f0',
series3: '#28c76f',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
zoom: {
type: 'xy',
enabled: true,
},
},
legend: {
position: 'top',
horizontalAlign: 'left',
markers: { offsetX: -3 },
fontSize: '13px',
labels: { colors: themeSecondaryTextColor },
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
colors: [scatterColors.series1, scatterColors.series2, scatterColors.series3],
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
labels: {
style: { fontSize: '0.8125rem', colors: themeDisabledTextColor },
},
},
xaxis: {
tickAmount: 10,
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
formatter: val => Number.parseFloat(val).toFixed(1),
},
},
}
}
export const getLineChartSimpleConfig = themeColors => {
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
zoom: { enabled: false },
toolbar: { show: false },
},
colors: ['#ff9f43'],
stroke: { curve: 'straight' },
dataLabels: { enabled: false },
markers: {
strokeWidth: 7,
strokeOpacity: 1,
colors: ['#ff9f43'],
strokeColors: ['#fff'],
},
grid: {
padding: { top: -10 },
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
tooltip: {
custom(data) {
return `<div class='bar-chart pa-2'>
<span>${data.series[data.seriesIndex][data.dataPointIndex]}%</span>
</div>`
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
categories: [
'7/12',
'8/12',
'9/12',
'10/12',
'11/12',
'12/12',
'13/12',
'14/12',
'15/12',
'16/12',
'17/12',
'18/12',
'19/12',
'20/12',
'21/12',
],
},
}
}
export const getBarChartConfig = themeColors => {
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
colors: ['#00cfe8'],
dataLabels: { enabled: false },
plotOptions: {
bar: {
borderRadius: 8,
barHeight: '30%',
horizontal: true,
},
},
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: false },
},
padding: {
top: -10,
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
categories: ['MON, 11', 'THU, 14', 'FRI, 15', 'MON, 18', 'WED, 20', 'FRI, 21', 'MON, 23'],
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
}
}
export const getCandlestickChartConfig = themeColors => {
const candlestickColors = {
series1: '#28c76f',
series2: '#ea5455',
}
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
plotOptions: {
bar: { columnWidth: '40%' },
candlestick: {
colors: {
upward: candlestickColors.series1,
downward: candlestickColors.series2,
},
},
},
grid: {
padding: { top: -10 },
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
tooltip: { enabled: true },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
xaxis: {
type: 'datetime',
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
}
}
export const getRadialBarChartConfig = themeColors => {
const radialBarColors = {
series1: '#fdd835',
series2: '#32baff',
series3: '#00d4bd',
series4: '#7367f0',
series5: '#FFA1A1',
}
const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors)
return {
stroke: { lineCap: 'round' },
labels: ['Comments', 'Replies', 'Shares'],
legend: {
show: true,
fontSize: '13px',
position: 'bottom',
labels: {
colors: themeSecondaryTextColor,
},
markers: {
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
colors: [radialBarColors.series1, radialBarColors.series2, radialBarColors.series4],
plotOptions: {
radialBar: {
hollow: { size: '30%' },
track: {
margin: 15,
background: themeColors.variables['track-bg'],
},
dataLabels: {
name: {
fontSize: '2rem',
},
value: {
fontSize: '0.9375rem',
color: themeSecondaryTextColor,
},
total: {
show: true,
fontWeight: 400,
label: 'Comments',
fontSize: '1.125rem',
color: themePrimaryTextColor,
formatter(w) {
const totalValue = w.globals.seriesTotals.reduce((a, b) => {
return a + b
}, 0) / w.globals.series.length
if (totalValue % 1 === 0)
return `${totalValue}%`
else
return `${totalValue.toFixed(2)}%`
},
},
},
},
},
grid: {
padding: {
top: -30,
bottom: -25,
},
},
}
}
export const getDonutChartConfig = themeColors => {
const donutColors = {
series1: '#fdd835',
series2: '#00d4bd',
series3: '#826bf8',
series4: '#32baff',
series5: '#ffa1a1',
}
const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors)
return {
stroke: { width: 0 },
labels: ['Operational', 'Networking', 'Hiring', 'R&D'],
colors: [donutColors.series1, donutColors.series5, donutColors.series3, donutColors.series2],
dataLabels: {
enabled: true,
formatter: val => `${Number.parseInt(val, 10)}%`,
},
legend: {
position: 'bottom',
markers: { offsetX: -3 },
fontSize: '13px',
labels: { colors: themeSecondaryTextColor },
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
fontSize: '1.125rem',
},
value: {
fontSize: '1.125rem',
color: themeSecondaryTextColor,
formatter: val => `${Number.parseInt(val, 10)}`,
},
total: {
show: true,
fontSize: '1.125rem',
label: 'Operational',
formatter: () => '31%',
color: themePrimaryTextColor,
},
},
},
},
},
responsive: [
{
breakpoint: 992,
options: {
chart: {
height: 380,
},
legend: {
position: 'bottom',
},
},
},
{
breakpoint: 576,
options: {
chart: {
height: 320,
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
fontSize: '0.9375rem',
},
value: {
fontSize: '0.9375rem',
},
total: {
fontSize: '0.9375rem',
},
},
},
},
},
},
},
],
}
}
export const getAreaChartSplineConfig = themeColors => {
const areaColors = {
series3: '#e0cffe',
series2: '#b992fe',
series1: '#ab7efd',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
tooltip: { shared: false },
dataLabels: { enabled: false },
stroke: {
show: false,
curve: 'straight',
},
legend: {
position: 'top',
horizontalAlign: 'left',
fontSize: '13px',
labels: { colors: themeSecondaryTextColor },
markers: {
offsetY: 1,
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
colors: [areaColors.series3, areaColors.series2, areaColors.series1],
fill: {
opacity: 1,
type: 'solid',
},
grid: {
show: true,
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
categories: [
'7/12',
'8/12',
'9/12',
'10/12',
'11/12',
'12/12',
'13/12',
'14/12',
'15/12',
'16/12',
'17/12',
'18/12',
'19/12',
],
},
}
}
export const getColumnChartConfig = themeColors => {
const columnColors = {
series1: '#826af9',
series2: '#d2b0ff',
bg: '#f8d3ff',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
offsetX: -10,
stacked: true,
parentHeightOffset: 0,
toolbar: { show: false },
},
fill: { opacity: 1 },
dataLabels: { enabled: false },
colors: [columnColors.series1, columnColors.series2],
legend: {
position: 'top',
horizontalAlign: 'left',
fontSize: '13px',
labels: { colors: themeSecondaryTextColor },
markers: {
offsetY: 1,
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
stroke: {
show: true,
colors: ['transparent'],
},
plotOptions: {
bar: {
columnWidth: '15%',
colors: {
backgroundBarRadius: 10,
backgroundBarColors: [columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg],
},
},
},
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: true },
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
categories: ['7/12', '8/12', '9/12', '10/12', '11/12', '12/12', '13/12', '14/12', '15/12'],
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor, fontSize: '0.8125rem' },
},
},
responsive: [
{
breakpoint: 600,
options: {
plotOptions: {
bar: {
columnWidth: '35%',
},
},
},
},
],
}
}
export const getHeatMapChartConfig = themeColors => {
const { themeSecondaryTextColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
dataLabels: { enabled: false },
stroke: {
colors: [themeColors.colors.surface],
},
legend: {
position: 'bottom',
fontSize: '13px',
labels: {
colors: themeSecondaryTextColor,
},
markers: {
offsetY: 0,
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
heatmap: {
enableShades: false,
colorScale: {
ranges: [
{ to: 10, from: 0, name: '0-10', color: '#b9b3f8' },
{ to: 20, from: 11, name: '10-20', color: '#aba4f6' },
{ to: 30, from: 21, name: '20-30', color: '#9d95f5' },
{ to: 40, from: 31, name: '30-40', color: '#8f85f3' },
{ to: 50, from: 41, name: '40-50', color: '#8176f2' },
{ to: 60, from: 51, name: '50-60', color: '#7367f0' },
],
},
},
},
grid: {
padding: { top: -20 },
},
yaxis: {
labels: {
style: {
colors: themeDisabledTextColor,
fontSize: '0.8125rem',
},
},
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
}
}
export const getRadarChartConfig = themeColors => {
const radarColors = {
series1: '#9b88fa',
series2: '#ffa1a1',
}
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
dropShadow: {
top: 1,
blur: 8,
left: 1,
opacity: 0.2,
enabled: false,
},
},
markers: { size: 0 },
fill: { opacity: [1, 0.8] },
colors: [radarColors.series1, radarColors.series2],
stroke: {
width: 0,
show: false,
},
legend: {
fontSize: '13px',
labels: {
colors: themeSecondaryTextColor,
},
markers: {
offsetX: -3,
},
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
radar: {
polygons: {
strokeColors: themeBorderColor,
connectorColors: themeBorderColor,
},
},
},
grid: {
show: false,
padding: {
top: -20,
bottom: -20,
},
},
yaxis: { show: false },
xaxis: {
categories: ['Battery', 'Brand', 'Camera', 'Memory', 'Storage', 'Display', 'OS', 'Price'],
labels: {
style: {
fontSize: '0.8125rem',
colors: [
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
],
},
},
},
}
}

View File

@@ -0,0 +1,372 @@
import { hexToRgb } from '@core/utils/colorConverter'
// 👉 Colors variables
const colorVariables = themeColors => {
const themeSecondaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['medium-emphasis-opacity']})`
const themeDisabledTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['disabled-opacity']})`
const themeBorderColor = `rgba(${hexToRgb(String(themeColors.variables['border-color']))},${themeColors.variables['border-opacity']})`
return { labelColor: themeDisabledTextColor, borderColor: themeBorderColor, legendColor: themeSecondaryTextColor }
}
// SECTION config
// 👉 Latest Bar Chart Config
export const getLatestBarChartConfig = themeColors => {
const { borderColor, labelColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
scales: {
x: {
grid: {
borderColor,
drawBorder: false,
color: borderColor,
},
ticks: { color: labelColor },
},
y: {
min: 0,
max: 400,
grid: {
borderColor,
drawBorder: false,
color: borderColor,
},
ticks: {
stepSize: 100,
color: labelColor,
},
},
},
plugins: {
legend: { display: false },
},
}
}
// 👉 Horizontal Bar Chart Config
export const getHorizontalBarChartConfig = themeColors => {
const { borderColor, labelColor, legendColor } = colorVariables(themeColors)
return {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
elements: {
bar: {
borderRadius: {
topRight: 15,
bottomRight: 15,
},
},
},
layout: {
padding: { top: -4 },
},
scales: {
x: {
min: 0,
grid: {
drawTicks: false,
drawBorder: false,
color: borderColor,
},
ticks: { color: labelColor },
},
y: {
grid: {
borderColor,
display: false,
drawBorder: false,
},
ticks: { color: labelColor },
},
},
plugins: {
legend: {
align: 'end',
position: 'top',
labels: { color: legendColor },
},
},
}
}
// 👉 Line Chart Config
export const getLineChartConfig = themeColors => {
const { borderColor, labelColor, legendColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: { color: labelColor },
grid: {
borderColor,
drawBorder: false,
color: borderColor,
},
},
y: {
min: 0,
max: 400,
ticks: {
stepSize: 100,
color: labelColor,
},
grid: {
borderColor,
drawBorder: false,
color: borderColor,
},
},
},
plugins: {
legend: {
align: 'end',
position: 'top',
labels: {
padding: 25,
boxWidth: 10,
color: legendColor,
usePointStyle: true,
},
},
},
}
}
// 👉 Radar Chart Config
export const getRadarChartConfig = themeColors => {
const { borderColor, labelColor, legendColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
layout: {
padding: { top: -20 },
},
scales: {
r: {
ticks: {
display: false,
maxTicksLimit: 1,
color: labelColor,
},
grid: { color: borderColor },
pointLabels: { color: labelColor },
angleLines: { color: borderColor },
},
},
plugins: {
legend: {
position: 'top',
labels: {
padding: 25,
color: legendColor,
},
},
},
}
}
// 👉 Polar Chart Config
export const getPolarChartConfig = themeColors => {
const { legendColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
layout: {
padding: {
top: -5,
bottom: -45,
},
},
scales: {
r: {
grid: { display: false },
ticks: { display: false },
},
},
plugins: {
legend: {
position: 'right',
labels: {
padding: 25,
boxWidth: 9,
color: legendColor,
usePointStyle: true,
},
},
},
}
}
// 👉 Bubble Chart Config
export const getBubbleChartConfig = themeColors => {
const { borderColor, labelColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
min: 0,
max: 140,
grid: {
borderColor,
drawBorder: false,
color: borderColor,
},
ticks: {
stepSize: 10,
color: labelColor,
},
},
y: {
min: 0,
max: 400,
grid: {
borderColor,
drawBorder: false,
color: borderColor,
},
ticks: {
stepSize: 100,
color: labelColor,
},
},
},
plugins: {
legend: { display: false },
},
}
}
// 👉 Doughnut Chart Config
export const getDoughnutChartConfig = () => {
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
cutout: 80,
plugins: {
legend: {
display: false,
},
},
}
}
// 👉 Scatter Chart Config
export const getScatterChartConfig = themeColors => {
const { borderColor, labelColor, legendColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 800 },
layout: {
padding: { top: -20 },
},
scales: {
x: {
min: 0,
max: 140,
grid: {
borderColor,
drawTicks: false,
drawBorder: false,
color: borderColor,
},
ticks: {
stepSize: 10,
color: labelColor,
},
},
y: {
min: 0,
max: 400,
grid: {
borderColor,
drawTicks: false,
drawBorder: false,
color: borderColor,
},
ticks: {
stepSize: 100,
color: labelColor,
},
},
},
plugins: {
legend: {
align: 'start',
position: 'top',
labels: {
padding: 25,
boxWidth: 9,
color: legendColor,
usePointStyle: true,
},
},
},
}
}
// 👉 Line Area Chart Config
export const getLineAreaChartConfig = themeColors => {
const { borderColor, labelColor, legendColor } = colorVariables(themeColors)
return {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: { top: -20 },
},
scales: {
x: {
grid: {
borderColor,
color: 'transparent',
},
ticks: { color: labelColor },
},
y: {
min: 0,
max: 400,
grid: {
borderColor,
color: 'transparent',
},
ticks: {
stepSize: 100,
color: labelColor,
},
},
},
plugins: {
legend: {
align: 'start',
position: 'top',
labels: {
padding: 25,
boxWidth: 9,
color: legendColor,
usePointStyle: true,
},
},
},
}
}
// !SECTION

View File

@@ -0,0 +1,54 @@
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { Bar } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
export default defineComponent({
name: 'BarChart',
props: {
chartId: {
type: String,
default: 'bar-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => ([]),
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(Bar), {
data: props.chartData,
options: props.chartOptions,
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
})
},
})

View File

@@ -0,0 +1,54 @@
import { Chart as ChartJS, Legend, LinearScale, PointElement, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { Bubble } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, PointElement, LinearScale)
export default defineComponent({
name: 'BubbleChart',
props: {
chartId: {
type: String,
default: 'bubble-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => [],
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(Bubble), {
data: props.chartData,
options: props.chartOptions,
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
})
},
})

View File

@@ -0,0 +1,54 @@
import { ArcElement, CategoryScale, Chart as ChartJS, Legend, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { Doughnut } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale)
export default defineComponent({
name: 'DoughnutChart',
props: {
chartId: {
type: String,
default: 'doughnut-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => [],
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(Doughnut), {
data: props.chartData,
options: props.chartOptions,
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
})
},
})

View File

@@ -0,0 +1,54 @@
import { CategoryScale, Chart as ChartJS, Legend, LineElement, LinearScale, PointElement, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { Line } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale)
export default defineComponent({
name: 'LineChart',
props: {
chartId: {
type: String,
default: 'line-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => [],
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(Line), {
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
options: props.chartOptions,
data: props.chartData,
})
},
})

View File

@@ -0,0 +1,54 @@
import { ArcElement, Chart as ChartJS, Legend, RadialLinearScale, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { PolarArea } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, ArcElement, RadialLinearScale)
export default defineComponent({
name: 'PolarAreaChart',
props: {
chartId: {
type: String,
default: 'line-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => [],
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(PolarArea), {
data: props.chartData,
options: props.chartOptions,
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
})
},
})

View File

@@ -0,0 +1,54 @@
import { Chart as ChartJS, Filler, Legend, LineElement, PointElement, RadialLinearScale, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { Radar } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, PointElement, RadialLinearScale, LineElement, Filler)
export default defineComponent({
name: 'RadarChart',
props: {
chartId: {
type: String,
default: 'radar-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => [],
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(Radar), {
data: props.chartData,
options: props.chartOptions,
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
})
},
})

View File

@@ -0,0 +1,54 @@
import { CategoryScale, Chart as ChartJS, Legend, LineElement, LinearScale, PointElement, Title, Tooltip } from 'chart.js'
import { defineComponent } from 'vue'
import { Scatter } from 'vue-chartjs'
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, CategoryScale, LinearScale)
export default defineComponent({
name: 'ScatterChart',
props: {
chartId: {
type: String,
default: 'scatter-chart',
},
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 400,
},
cssClasses: {
default: '',
type: String,
},
styles: {
type: Object,
default: () => ({}),
},
plugins: {
type: Array,
default: () => [],
},
chartData: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
setup(props) {
return () => h(h(Scatter), {
data: props.chartData,
options: props.chartOptions,
chartId: props.chartId,
width: props.width,
height: props.height,
cssClasses: props.cssClasses,
styles: props.styles,
plugins: props.plugins,
})
},
})

View File

@@ -0,0 +1,195 @@
@use "@core/scss/base/mixins";
@use "@layouts/styles/placeholders";
@use "@layouts/styles/mixins" as layoutMixins;
@use "@configured-variables" as variables;
@use "@styles/variables/_vuetify.scss" as vuetify;
// 👉 Avatar group
.v-avatar-group {
display: flex;
align-items: center;
> * {
&:not(:first-child) {
margin-inline-start: -0.8rem;
}
transition: transform 0.25s ease, box-shadow 0.15s ease;
&:hover {
z-index: 2;
transform: translateY(-5px) scale(1.05);
@include mixins.elevation(3);
}
}
> .v-avatar {
border: 2px solid rgb(var(--v-theme-surface));
transition: transform 0.15s ease;
}
}
// 👉 Button outline with default color border color
.v-alert--variant-outlined,
.v-avatar--variant-outlined,
.v-btn.v-btn--variant-outlined,
.v-card--variant-outlined,
.v-chip--variant-outlined,
.v-list-item--variant-outlined {
&:not([class*="text-"]) {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
&.text-default {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
}
// 👉 Custom Input
.v-label.custom-input {
padding: 1rem;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
opacity: 1;
white-space: normal;
+.v-label {
letter-spacing: normal;
}
&:hover {
border-color: rgba(var(--v-border-color), 0.25);
}
&.active {
border-color: rgb(var(--v-theme-primary));
.v-icon {
color: rgb(var(--v-theme-primary)) !important;
}
}
&.custom-checkbox,
&.custom-radio {
.v-input__control {
grid-area: none;
}
}
}
// 👉 Datatable
.v-data-table-footer__pagination {
@include layoutMixins.rtl {
.v-btn {
.v-icon {
transform: rotate(180deg);
}
}
}
}
// Dialog responsive width
.v-dialog {
// dialog custom close btn
.v-dialog-close-btn {
position: absolute;
z-index: 1;
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
inset-block-start: 0.5rem;
inset-inline-end: 0.5rem;
.v-btn__overlay {
display: none;
}
}
.v-card {
@extend %style-scroll-bar;
}
}
@media (min-width: 600px) {
.v-dialog {
&.v-dialog-sm,
&.v-dialog-lg,
&.v-dialog-xl {
.v-overlay__content {
inline-size: 565px !important;
}
}
}
}
@media (min-width: 960px) {
.v-dialog {
&.v-dialog-lg,
&.v-dialog-xl {
.v-overlay__content {
inline-size: 865px !important;
}
}
}
}
@media (min-width: 1264px) {
.v-dialog.v-dialog-xl {
.v-overlay__content {
inline-size: 1165px !important;
}
}
}
// 👉 Expansion panel
.v-expansion-panels.customized-panels {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: vuetify.$border-radius-root;
.v-expansion-panel-title {
background-color: rgb(var(--v-theme-expansion-panel-text-custom-bg));
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
margin-block-end: -1px;
}
.v-expansion-panel-text__wrapper {
padding: 20px;
}
}
// v-tab with pill support
.v-tabs.v-tabs-pill {
.v-tab.v-btn {
border-radius: 6px !important;
transition: none;
.v-tab__slider {
visibility: hidden;
}
}
}
// loop for all colors bg
@each $color-name in variables.$theme-colors-name {
body .v-tabs.v-tabs-pill {
.v-slide-group__content {
gap: 0.25rem;
}
.v-tab--selected.text-#{$color-name} {
background-color: rgb(var(--v-theme-#{$color-name}));
color: rgb(var(--v-theme-on-#{$color-name})) !important;
}
}
}
// We are make even width of all v-timeline body
.v-timeline--vertical.v-timeline {
.v-timeline-item {
.v-timeline-item__body {
justify-self: stretch !important;
}
}
}
// 👉 Switch
.v-switch .v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb {
color: #fff !important;
}

View File

@@ -0,0 +1,16 @@
@use "@configured-variables" as variables;
//
// * Perfect Scrollbar
//
body.v-theme--dark {
.ps__rail-y,
.ps__rail-x {
background-color: transparent !important;
}
.ps__thumb-y {
background-color: variables.$plugin-ps-thumb-y-dark;
}
}

View File

@@ -0,0 +1,45 @@
@use "@core/scss/base/placeholders" as *;
@use "@core/scss/template/placeholders" as *;
@use "@core/scss/base/mixins";
.layout-wrapper.layout-nav-type-horizontal {
.layout-navbar-and-nav-container {
@extend %default-layout-horizontal-nav-navbar-and-nav-container;
}
// 👉 Navbar
.layout-navbar {
@extend %default-layout-horizontal-nav-navbar;
}
// 👉 Layout content container
.navbar-content-container {
display: flex;
align-items: center;
block-size: 100%;
}
.layout-horizontal-nav {
@extend %default-layout-horizontal-nav-nav;
.nav-items {
@extend %default-layout-horizontal-nav-nav-items-list;
}
}
// 👉 App footer
.layout-footer {
@at-root {
.layout-footer-sticky#{&} {
background-color: rgb(var(--v-theme-surface));
@include mixins.elevation(3);
}
}
}
// TODO: Use Vuetify grid sass variable here
.layout-page-content {
padding-block: 1.5rem;
}
}

View File

@@ -0,0 +1,103 @@
@use "@configured-variables" as variables;
@use "@core/scss/base/placeholders" as *;
@use "@core/scss/template/placeholders" as *;
@use "misc";
@use "@core/scss/base/mixins";
$header: ".layout-navbar";
@if variables.$layout-vertical-nav-navbar-is-contained {
$header: ".layout-navbar .navbar-content-container";
}
.layout-wrapper.layout-nav-type-vertical {
// SECTION Layout Navbar
// 👉 Elevated navbar
@if variables.$vertical-nav-navbar-style == "elevated" {
// Add transition
#{$header} {
transition: padding 0.2s ease, background-color 0.18s ease;
}
// If navbar is contained => Add border radius to header
@if variables.$layout-vertical-nav-navbar-is-contained {
#{$header} {
border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
}
// Scrolled styles for sticky navbar
@at-root {
/* This html selector with not selector is required when:
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and our style broke
*/
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-sticky,
&.window-scrolled.layout-navbar-sticky {
#{$header} {
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
}
.navbar-blur#{$header} {
@extend %blurry-bg;
}
}
}
}
// 👉 Floating navbar
@else if variables.$vertical-nav-navbar-style == "floating" {
// Regardless of navbar is contained or not => Apply overlay to .layout-navbar
.layout-navbar {
&.navbar-blur {
@extend %default-layout-vertical-nav-floating-navbar-overlay;
}
}
&:not(.layout-navbar-sticky) {
#{$header} {
margin-block-start: variables.$vertical-nav-floating-navbar-top;
}
}
#{$header} {
@if variables.$layout-vertical-nav-navbar-is-contained {
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
background-color: rgb(var(--v-theme-surface));
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
}
.navbar-blur#{$header} {
@extend %blurry-bg;
}
}
// !SECTION
// 👉 Layout footer
.layout-footer {
$ele-layout-footer: &;
.footer-content-container {
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness 0 0;
// Sticky footer
@at-root {
// .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer
.layout-footer-sticky#{$ele-layout-footer} {
.footer-content-container {
background-color: rgb(var(--v-theme-surface));
padding-block: 0;
padding-inline: 1.2rem;
@include mixins.elevation(3);
}
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
@use "@core/scss/base/placeholders";
@use "@core/scss/base/variables";
.layout-vertical-nav,
.layout-horizontal-nav {
ol,
ul {
list-style: none;
}
}
.layout-navbar {
@if variables.$navbar-high-emphasis-text {
@extend %layout-navbar;
}
}

View File

@@ -0,0 +1,194 @@
@use "@core/scss/base/placeholders" as *;
@use "@core/scss/template/placeholders" as *;
@use "@configured-variables" as variables;
@use "@layouts/styles/mixins" as layoutsMixins;
@use "@core/scss/base/mixins";
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
.layout-horizontal-nav {
@extend %nav;
// 👉 Icon styles
.nav-item-icon {
@extend %horizontal-nav-item-icon;
}
// 👉 Common styles for nav group & nav link
.nav-link,
.nav-group {
// 👉 Disabled nav items
&.disabled {
@extend %horizontal-nav-disabled;
}
// Set width of inner nav group and link
&.sub-item {
@extend %horizontal-nav-subitem;
}
}
// SECTION Nav Link
.nav-link {
@extend %nav-link;
a {
@extend %horizontal-nav-item;
// Adds before psudo element to style hover state
@include mixins.before-pseudo;
// Adds vuetify states
@include vuetifyStates.states($active: false);
}
// 👉 Top level nav link
&:not(.sub-item) {
a {
@extend %horizontal-nav-top-level-item;
&.router-link-active {
@extend %nav-link-active;
}
}
}
// 👉 Sub link
&.sub-item {
a {
&.router-link-active {
// We will not use active styles from material here because we want to use primary color for active link
@extend %horizontal-nav-sub-nav-link-active;
}
}
}
}
// !SECTION
// SECTION Nav Group
.nav-group {
.popper-triggerer {
.nav-group-label {
@extend %horizontal-nav-item;
}
}
> .popper-triggerer > .nav-group-label {
// Adds before psudo element to style hover state
@include mixins.before-pseudo;
// Adds vuetify states
@include vuetifyStates.states($active: false);
}
.popper-content {
@extend %horizontal-nav-popper-content-hidden;
@extend %horizontal-nav-popper-content;
background-color: rgb(var(--v-theme-surface));
// Set max-height for the popper content
> div {
max-block-size: variables.$horizontal-nav-popper-content-max-height;
}
}
// 👉 Top level group
&:not(.sub-item) {
> .popper-triggerer {
position: relative;
/*
The Bridge
This after pseudo will work as bridge when we have space between popper triggerer and popper content
Initially it will have pointer events none for normal behavior and once the content is shown it will
work as bridge by setting pointer events to `auto`
*/
&::after {
position: absolute;
block-size: variables.$horizontal-nav-popper-content-top;
content: "";
inline-size: 100%;
inset-block-start: 100%;
inset-inline-start: 0;
pointer-events: none;
}
}
// Enable the pseudo bridge when content is shown by setting pointer events to `auto`
&.show-content > .popper-triggerer::after {
/*
We have added `z-index: 2` because when there is horizontal nav item below the popper trigger (group)
without this style nav item below popper trigger (group) gets focus hence closes the popper content
*/
z-index: 2;
pointer-events: auto;
}
> .popper-triggerer > .nav-group-label {
@extend %horizontal-nav-top-level-item;
}
&.active {
> .popper-triggerer > .nav-group-label {
@extend %nav-link-active;
}
}
// Add space between popper wrapper & content
> .popper-content {
margin-block-start: variables.$horizontal-nav-popper-content-top !important;
}
}
// 👉 Sub group
&.sub-item {
&.active {
@include mixins.selected-states("> .popper-triggerer > .nav-group-label::before");
}
// Reduce the icon's size of nested group's nav links (Top level group > Sub group > [Nav links])
.sub-item {
.nav-item-icon {
@extend %third-level-nav-item-icon;
}
}
}
.nav-group-arrow {
font-size: variables.$horizontal-nav-group-arrow-icon-size;
/*
ml-auto won't matter in top level group (because we haven't specified fixed width for top level groups)
but we wrote generally because we don't want to become so specific
*/
margin-inline-start: auto;
}
&.popper-inline-end {
.nav-group-arrow {
transform: rotateZ(270deg);
@include layoutsMixins.rtl {
transform: rotateZ(90deg);
}
}
}
.nav-item-title {
@extend %horizontal-nav-item-title;
}
&.show-content {
> .popper-content {
@extend %horizontal-nav-popper-content-visible;
}
&:not(.active) {
@include mixins.selected-states("> .popper-triggerer > .nav-group-label::before");
}
}
}
// !SECTION
}

View File

@@ -0,0 +1,48 @@
@use "sass:map";
// Layout
@use "vertical-nav";
@use "horizontal-nav";
@use "default-layout";
@use "default-layout-w-vertical-nav";
@use "default-layout-w-horizontal-nav";
// Layouts package
@use "layouts";
// Skins
@use "skins";
// Components
@use "components";
// Utilities
@use "utilities";
// Route Transitions
@use "route-transitions";
// Misc
@use "misc";
// Dark
@use "dark";
// libs
@use "libs/perfect-scrollbar";
a {
color: rgb(var(--v-theme-primary));
text-decoration: none;
}
// Vuetify 3 don't provide margin bottom style like vuetify 2
p {
margin-block-end: 1rem;
}
// Iconify icon size
svg.iconify {
block-size: 1em;
inline-size: 1em;
}

View File

@@ -0,0 +1,63 @@
@use "@configured-variables" as variables;
/* This styles extends the existing layout package's styles for handling cases that aren't related to layouts package */
/*
When we use v-layout as immediate first child of `.page-content-container`, it adds display:flex and page doesn't get contained height
*/
// .layout-wrapper.layout-nav-type-vertical {
// &.layout-content-height-fixed {
// .page-content-container {
// > .v-layout:first-child > :not(.v-navigation-drawer):first-child {
// flex-grow: 1;
// block-size: 100%;
// }
// }
// }
// }
.layout-wrapper.layout-nav-type-vertical {
&.layout-content-height-fixed {
.page-content-container {
> .v-layout:first-child {
overflow: hidden;
min-block-size: 100%;
> .v-main {
// overflow-y: auto;
.v-main__wrap > :first-child {
block-size: 100%;
overflow-y: auto;
}
}
}
}
}
}
// Let div/v-layout take full height. E.g. Email App
.layout-wrapper.layout-nav-type-horizontal {
&.layout-content-height-fixed {
> .layout-page-content {
display: flex;
}
}
}
// 👉 Floating navbar styles
@if variables.$vertical-nav-navbar-style == "floating" {
// Add spacing above navbar if navbar is floating (was in %layout-navbar-sticky placeholder)
body .layout-wrapper.layout-nav-type-vertical.layout-navbar-sticky {
.layout-navbar {
inset-block-start: variables.$vertical-nav-floating-navbar-top;
}
/*
If it's floating navbar
Add `vertical-nav-floating-navbar-top` as margin top to .layout-page-content
*/
.layout-page-content {
margin-block-start: variables.$vertical-nav-floating-navbar-top;
}
}
}

View File

@@ -0,0 +1,20 @@
// scrollable-content allows creating fixed header and scrollable content for VNavigationDrawer (Used when perfect scrollbar is used)
.scrollable-content {
&.v-navigation-drawer {
.v-navigation-drawer__content {
display: flex;
overflow: hidden;
flex-direction: column;
}
}
}
// adding styling for code tag
code {
border-radius: 3px;
color: rgb(var(--v-code-color));
font-size: 90%;
font-weight: 400;
padding-block: 0.2em;
padding-inline: 0.4em;
}

View File

@@ -0,0 +1,63 @@
@use "sass:map";
@use "@styles/variables/vuetify.scss";
@mixin elevation($z, $important: false) {
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
}
// #region before-pseudo
// This mixin is inspired from vuetify for adding hover styles via before pseudo element
@mixin before-pseudo() {
position: relative;
&::before {
position: absolute;
border-radius: inherit;
background: currentcolor;
block-size: 100%;
content: "";
inline-size: 100%;
inset: 0;
opacity: 0;
pointer-events: none;
}
}
// #endregion before-pseudo
@mixin bordered-skin($component, $border-property: "border", $important: false) {
#{$component} {
box-shadow: none !important;
// stylelint-disable-next-line annotation-no-unknown
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
}
}
// #region selected-states
// Inspired from vuetify's active-states mixin
// focus => 0.12 & selected => 0.08
@mixin selected-states($selector) {
#{$selector} {
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
}
&:hover
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
&:focus-visible
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
@supports not selector(:focus-visible) {
&:focus {
#{$selector} {
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
}
}
}
}
// #endregion selected-states

View File

@@ -0,0 +1,70 @@
// 👉 Zoom fade
.app-transition-zoom-fade-enter-active,
.app-transition-zoom-fade-leave-active {
transition: transform 0.35s, opacity 0.28s ease-in-out;
}
.app-transition-zoom-fade-enter-from {
opacity: 0;
transform: scale(0.98);
}
.app-transition-zoom-fade-leave-to {
opacity: 0;
transform: scale(1.02);
}
// 👉 Fade
.app-transition-fade-enter-active,
.app-transition-fade-leave-active {
transition: opacity 0.25s ease-in-out;
}
.app-transition-fade-enter-from,
.app-transition-fade-leave-to {
opacity: 0;
}
// 👉 Fade bottom
.app-transition-fade-bottom-enter-active,
.app-transition-fade-bottom-leave-active {
transition: opacity 0.3s, transform 0.35s;
}
.app-transition-fade-bottom-enter-from {
opacity: 0;
transform: translateY(-0.6rem);
}
.app-transition-fade-bottom-leave-to {
opacity: 0;
transform: translateY(0.6rem);
}
// 👉 Slide fade
.app-transition-slide-fade-enter-active,
.app-transition-slide-fade-leave-active {
transition: opacity 0.3s, transform 0.35s;
}
.app-transition-slide-fade-enter-from {
opacity: 0;
transform: translateX(-0.6rem);
}
.app-transition-slide-fade-leave-to {
opacity: 0;
transform: translateX(0.6rem);
}
// 👉 Zoom out
.app-transition-zoom-out-enter-active,
.app-transition-zoom-out-leave-active {
transition: opacity 0.26s ease-in-out, transform 0.3s ease-out;
}
.app-transition-zoom-out-enter-from,
.app-transition-zoom-out-leave-to {
opacity: 0;
transform: scale(0.98);
}

View File

@@ -0,0 +1,189 @@
@use "@configured-variables" as variables;
@use "@layouts/styles/mixins" as layoutsMixins;
/* 👉 Demo spacers */
/* TODO: Use vuetify SCSS variable here; */
$card-spacer-content: 16px;
.demo-space-x {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-block-start: -$card-spacer-content;
& > * {
margin-block-start: $card-spacer-content;
margin-inline-end: $card-spacer-content;
}
}
.demo-space-y {
& > * {
margin-block-end: $card-spacer-content;
&:last-child {
margin-block-end: 0;
}
}
}
// 👉 Card match height
.match-height.v-row {
.v-card {
block-size: 100%;
}
}
// 👉 Whitespace
.whitespace-no-wrap {
white-space: nowrap;
}
// 👉 Colors
/*
Vuetify is applying `.text-white` class to badge icon but don't provide its styles
Moreover, we also use this class in some places
In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3
We also need !important to get correct color in badge icon
*/
.text-white {
color: #fff !important;
}
.text-white-variant {
color: rgba(211, 212, 220);
}
.text-link {
&:not(:hover) {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
}
.text-link {
&:not(:hover) {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
}
.bg-var-theme-background {
background-color: rgba(var(--v-theme-on-background), var(--v-hover-opacity)) !important;
}
.bg-global-primary {
background-color: rgb(var(--v-global-theme-primary)) !important;
color: rgb(var(--v-theme-on-primary)) !important;
}
// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })],
@each $color-name in variables.$theme-colors-name {
.bg-light-#{$color-name} {
background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important;
}
}
// 👉 clamp text
.clamp-text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
}
.custom-badge {
.v-badge__badge {
border-radius: 6px !important;
block-size: 12px !important;
inline-size: 12px !important;
}
}
.leading-normal {
line-height: normal !important;
}
// 👉 for rtl only
.flip-in-rtl {
@include layoutsMixins.rtl {
transform: scaleX(-1);
}
}
// 👉 Carousel
.carousel-delimiter-top-end {
.v-carousel__controls {
justify-content: end;
block-size: 40px;
inset-block-start: 0;
padding-inline: 1rem;
.v-btn--icon.v-btn--density-default {
block-size: calc(var(--v-btn-height) + -10px);
inline-size: calc(var(--v-btn-height) + -8px);
&.v-btn--active {
color: #fff;
}
.v-btn__overlay {
opacity: 0;
}
.v-ripple__container {
display: none;
}
.v-btn__content {
.v-icon {
block-size: 8px !important;
font-size: 8px !important;
inline-size: 8px !important;
}
}
}
}
@each $color-name in variables.$theme-colors-name {
&.dots-active-#{$color-name} {
.v-carousel__controls {
.v-btn--active {
color: rgb(var(--v-theme-#{$color-name})) !important;
}
}
}
}
}
.v-timeline-item {
.app-timeline-title {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 16px;
font-weight: 500;
line-height: 1.3125rem;
}
.app-timeline-meta {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: 12px;
line-height: 0.875rem;
}
.app-timeline-text {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
font-size: 14px;
line-height: 1.25rem;
}
.timeline-chip {
border-radius: 6px;
background: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
padding-block: 5px;
padding-inline: 10px;
}
}

View File

@@ -0,0 +1,90 @@
@use "sass:map";
@use "sass:list";
@use "@configured-variables" as variables;
// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps/
@function map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map.get($map, $key);
}
@return $map;
}
@function map-deep-set($map, $keys, $value) {
$maps: ($map,);
$result: null;
// If the last key is a map already
// Warn the user we will be overriding it with $value
@if type-of(nth($keys, -1)) == "map" {
@warn "The last key you specified is a map; it will be overrided with `#{$value}`.";
}
// If $keys is a single key
// Just merge and return
@if length($keys) == 1 {
@return map-merge($map, ($keys: $value));
}
// Loop from the first to the second to last key from $keys
// Store the associated map to this key in the $maps list
// If the key doesn't exist, throw an error
@for $i from 1 through length($keys) - 1 {
$current-key: list.nth($keys, $i);
$current-map: list.nth($maps, -1);
$current-get: map.get($current-map, $current-key);
@if not $current-get {
@error "Key `#{$key}` doesn't exist at current level in map.";
}
$maps: list.append($maps, $current-get);
}
// Loop from the last map to the first one
// Merge it with the previous one
@for $i from length($maps) through 1 {
$current-map: list.nth($maps, $i);
$current-key: list.nth($keys, $i);
$current-val: if($i == list.length($maps), $value, $result);
$result: map.map-merge($current-map, ($current-key: $current-val));
}
// Return result
@return $result;
}
// font size utility classes
@each $name, $size in variables.$font-sizes {
.text-#{$name} {
font-size: $size;
line-height: map.get(variables.$font-line-height, $name);
}
}
// truncate utility class
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// gap utility class
@each $name, $size in variables.$gap {
.gap-#{$name} {
gap: $size;
}
.gap-x-#{$name} {
column-gap: $size;
}
.gap-y-#{$name} {
row-gap: $size;
}
}
.list-none {
list-style-type: none;
}

View File

@@ -0,0 +1,198 @@
@use "vuetify/lib/styles/tools/functions" as *;
/*
TODO: Add docs on when to use placeholder vs when to use SASS variable
Placeholder
- When we want to keep customization to our self between templates use it
Variables
- When we want to allow customization from both user and our side
- You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header)
*/
@forward "@layouts/styles/variables" with (
// Adjust z-index so vertical nav & overlay stays on top of v-layout in v-main. E.g. Email app
$layout-vertical-nav-z-index: 1003,
$layout-overlay-z-index: 1002,
);
@use "@layouts/styles/variables" as *;
// 👉 Default layout
$navbar-high-emphasis-text: true !default;
// @forward "@layouts/styles/variables" with (
// $layout-vertical-nav-width: 350px !default,
// );
$theme-colors-name: (
"primary",
"secondary",
"error",
"info",
"success",
"warning"
) !default;
// 👉 Default layout with vertical nav
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
// 👉 Vertical nav
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 1rem !default;
$vertical-nav-horizontal-padding: 0.75rem !default;
// Vertical nav header height. Mostly we will align it with navbar height;
$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default;
$vertical-nav-navbar-elevation: 3 !default;
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
$vertical-nav-floating-navbar-top: 1rem !default;
// Vertical nav header padding
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
// Move logo when vertical nav is mini (collapsed but not hovered)
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
// Space between logo and title
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
// Section title margin top (when its not first child)
$vertical-nav-section-title-mt: 1.5rem !default;
// Section title margin bottom
$vertical-nav-section-title-mb: 0.5rem !default;
// Vertical nav icons
$vertical-nav-items-icon-size: 1.5rem !default;
$vertical-nav-items-nested-icon-size: 0.9rem !default;
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
// Transition duration for nav group arrow
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
// Timing function for nav group arrow
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
// 👉 Horizontal nav
/*
Heads up
==================
Here we assume we will always use shorthand property which will apply same padding on four side
This is because this have been used as value of top property by `.popper-content`
*/
$horizontal-nav-padding: 0.6875rem !default;
// Gap between top level horizontal nav items
$horizontal-nav-top-level-items-gap: 4px !default;
// Horizontal nav icons
$horizontal-nav-items-icon-size: 1.5rem !default;
$horizontal-nav-third-level-icon-size: 0.9rem !default;
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
$horizontal-nav-group-arrow-icon-size: 1.375rem !default;
// We used SCSS variable because we want to allow users to update max height of popper content
// 120px is combined height of navbar & horizontal nav
$horizontal-nav-popper-content-max-height: calc(100dvh - 120px - 4rem) !default;
// This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
// 👉 Plugins
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
// 👉 Vuetify
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
$vuetify-reduce-default-compact-button-icon-size: true !default;
// 👉 Custom variables
// for utility classes
$font-sizes: () !default;
$font-sizes: map-deep-merge(
(
"xs": 0.75rem,
"sm": 0.875rem,
"base": 1rem,
"lg": 1.125rem,
"xl": 1.25rem,
"2xl": 1.5rem,
"3xl": 1.875rem,
"4xl": 2.25rem,
"5xl": 3rem,
"6xl": 3.75rem,
"7xl": 4.5rem,
"8xl": 6rem,
"9xl": 8rem
),
$font-sizes
);
// line height
$font-line-height: () !default;
$font-line-height: map-deep-merge(
(
"xs": 1rem,
"sm": 1.25rem,
"base": 1.5rem,
"lg": 1.75rem,
"xl": 1.75rem,
"2xl": 2rem,
"3xl": 2.25rem,
"4xl": 2.5rem,
"5xl": 1,
"6xl": 1,
"7xl": 1,
"8xl": 1,
"9xl": 1
),
$font-line-height
);
// gap utility class
$gap: () !default;
$gap: map-deep-merge(
(
"0": 0,
"1": 0.25rem,
"2": 0.5rem,
"3": 0.75rem,
"4": 1rem,
"5": 1.25rem,
"6":1.5rem,
"7": 1.75rem,
"8": 2rem,
"9": 2.25rem,
"10": 2.5rem,
"11": 2.75rem,
"12": 3rem,
"14": 3.5rem,
"16": 4rem,
"20": 5rem,
"24": 6rem,
"28": 7rem,
"32": 8rem,
"36": 9rem,
"40": 10rem,
"44": 11rem,
"48": 12rem,
"52": 13rem,
"56": 14rem,
"60": 15rem,
"64": 16rem,
"72": 18rem,
"80": 20rem,
"96": 24rem
),
$gap
);

View File

@@ -0,0 +1,255 @@
@use "@core/scss/base/placeholders" as *;
@use "@core/scss/template/placeholders" as *;
@use "@layouts/styles/mixins" as layoutsMixins;
@use "@configured-variables" as variables;
@use "@core/scss/base/mixins" as mixins;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
.layout-nav-type-vertical {
// 👉 Layout Vertical nav
.layout-vertical-nav {
$sl-layout-nav-type-vertical: &;
@extend %nav;
@at-root {
// Add styles for collapsed vertical nav
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}.hovered {
@include mixins.elevation(6);
}
}
background-color: variables.$vertical-nav-background-color;
// 👉 Nav header
.nav-header {
overflow: hidden;
padding: variables.$vertical-nav-header-padding;
margin-inline: variables.$vertical-nav-header-inline-spacing;
min-block-size: variables.$vertical-nav-header-height;
// TEMPLATE: Check if we need to move this to master
.app-logo {
flex-shrink: 0;
transition: transform 0.25s ease-in-out;
@at-root {
// Move logo a bit to align center with the icons in vertical nav mini variant
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}:not(.hovered) .nav-header .app-logo {
transform: translateX(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini);
@include layoutsMixins.rtl {
transform: translateX(-(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini));
}
}
}
}
.header-action {
@extend %nav-header-action;
}
}
// 👉 Nav items shadow
.vertical-nav-items-shadow {
position: absolute;
z-index: 1;
background:
linear-gradient(
rgb(#{variables.$vertical-nav-background-color-rgb}) 5%,
rgba(#{variables.$vertical-nav-background-color-rgb}, 75%) 45%,
rgba(#{variables.$vertical-nav-background-color-rgb}, 20%) 80%,
transparent
);
block-size: 55px;
inline-size: 100%;
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease-in-out;
will-change: opacity;
@include layoutsMixins.rtl {
transform: translateX(8px);
}
}
&.scrolled {
.vertical-nav-items-shadow {
opacity: 1;
}
}
// Setting z-index 1 will make perfect scrollbar thumb appear on top of vertical nav items shadow;
.ps__rail-y {
z-index: 1;
}
// 👉 Nav section title
.nav-section-title {
@extend %vertical-nav-item;
@extend %vertical-nav-section-title;
margin-block-end: variables.$vertical-nav-section-title-mb;
&:not(:first-child) {
margin-block-start: variables.$vertical-nav-section-title-mt;
}
.placeholder-icon {
margin-inline: auto;
}
}
// Nav item badge
.nav-item-badge {
@extend %vertical-nav-item-badge;
}
// 👉 Nav group & Link
.nav-link,
.nav-group {
overflow: hidden;
> :first-child {
@extend %vertical-nav-item;
@extend %vertical-nav-item-interactive;
}
.nav-item-icon {
@extend %vertical-nav-items-icon;
}
&.disabled {
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
a {
outline: none;
}
}
// 👉 Vertical nav link
.nav-link {
@extend %nav-link;
> .router-link-exact-active {
@extend %nav-link-active;
}
> a {
// Adds before psudo element to style hover state
@include mixins.before-pseudo;
// Adds vuetify states
&:not(.router-link-active, .router-link-exact-active) {
@include vuetifyStates.states($active: false);
}
}
}
// 👉 Vertical nav group
.nav-group {
// Reduce the size of icon if link/group is inside group
.nav-group,
.nav-link {
.nav-item-icon {
@extend %vertical-nav-items-nested-icon;
}
}
// Hide icons after 2nd level
& .nav-group {
.nav-link,
.nav-group {
.nav-item-icon {
@extend %vertical-nav-items-icon-after-2nd-level;
}
}
}
.nav-group-arrow {
flex-shrink: 0;
transform-origin: center;
transition: transform variables.$vertical-nav-nav-group-arrow-transition-duration variables.$vertical-nav-nav-group-arrow-transition-timing-function;
will-change: transform;
}
// Rotate arrow icon if group is opened
&.open {
> .nav-group-label .nav-group-arrow {
transform: rotateZ(90deg);
}
}
// Nav group label
> :first-child {
// Adds before psudo element to style hover state
@include mixins.before-pseudo;
}
&:not(.active,.open) > :first-child {
// Adds vuetify states
@include vuetifyStates.states($active: false);
}
// Active & open states for nav group label
&.active,
&.open {
> :first-child {
@extend %vertical-nav-group-open-active;
}
}
}
}
}
// SECTION: Transitions
.vertical-nav-section-title-enter-active,
.vertical-nav-section-title-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
}
.vertical-nav-section-title-enter-from,
.vertical-nav-section-title-leave-to {
opacity: 0;
transform: translateX(15px);
@include layoutsMixins.rtl {
transform: translateX(-15px);
}
}
.transition-slide-x-enter-active,
.transition-slide-x-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
}
.transition-slide-x-enter-from,
.transition-slide-x-leave-to {
opacity: 0;
transform: translateX(-15px);
@include layoutsMixins.rtl {
transform: translateX(15px);
}
}
.vertical-nav-app-title-enter-active,
.vertical-nav-app-title-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
}
.vertical-nav-app-title-enter-from,
.vertical-nav-app-title-leave-to {
opacity: 0;
transform: translateX(-15px);
@include layoutsMixins.rtl {
transform: translateX(15px);
}
}
// !SECTION

View File

@@ -0,0 +1,35 @@
$ps-size: 0.25rem;
$ps-hover-size: 0.375rem;
$ps-track-size: 0.5rem;
.ps__thumb-y {
inline-size: $ps-size !important;
inset-inline-end: 0.0625rem;
}
.ps__thumb-y,
.ps__thumb-x {
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
}
.ps__thumb-x {
block-size: $ps-size !important;
}
.ps__rail-x {
background: transparent !important;
block-size: $ps-track-size;
}
.ps__rail-y {
background: transparent !important;
inline-size: $ps-track-size !important;
inset-inline-end: 0.125rem !important;
inset-inline-start: unset !important;
}
.ps__rail-y.ps--clicking .ps__thumb-y,
.ps__rail-y:focus > .ps__thumb-y,
.ps__rail-y:hover > .ps__thumb-y {
inline-size: $ps-hover-size !important;
}

View File

@@ -0,0 +1,262 @@
@use "@core/scss/base/utils";
@use "@configured-variables" as variables;
// 👉 Application
// We need accurate vh in mobile devices as well
.v-application__wrap {
/* stylelint-disable-next-line liberty/use-logical-spec */
min-height: 100dvh;
}
// 👉 Typography
h1,
h2,
h3,
h4,
h5,
h6,
.text-h1,
.text-h2,
.text-h3,
.text-h4,
.text-h5,
.text-h6,
.text-button,
.text-overline,
.v-card-title {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
body,
.text-body-1,
.text-body-2,
.text-subtitle-1,
.text-subtitle-2 {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
// 👉 Grid
// Remove margin-bottom of v-input_details inside grid (validation error message)
.v-row {
.v-col,
[class^="v-col-*"] {
.v-input__details {
margin-block-end: 0;
}
}
}
// 👉 Button
// Update tonal variant disabled opacity
.v-btn--disabled {
opacity: 0.65;
}
@if variables.$vuetify-reduce-default-compact-button-icon-size {
.v-btn--density-compact.v-btn--size-default {
.v-btn__content > svg {
block-size: 22px;
font-size: 22px;
inline-size: 22px;
}
}
}
// 👉 Card
// Removes padding-top for immediately placed v-card-text after itself
.v-card-text {
& + & {
padding-block-start: 0 !important;
}
}
/*
👉 Checkbox & Radio Ripple
TODO Checkbox and switch component. Remove it when vuetify resolve the extra spacing: https://github.com/vuetifyjs/vuetify/issues/15519
We need this because form elements likes checkbox and switches are by default set to height of textfield height which is way big than we want
Tested with checkbox & switches
*/
.v-checkbox.v-input,
.v-switch.v-input {
--v-input-control-height: auto;
flex: unset;
}
.v-radio-group {
.v-selection-control-group {
.v-radio:not(:last-child) {
margin-inline-end: 0.9rem;
}
}
}
/*
👉 Tabs
Disable tab transition
This is for tabs where we don't have card wrapper to tabs and have multiple cards as tab content.
This class will disable transition and adds `overflow: unset` on `VWindow` to allow spreading shadow
*/
.disable-tab-transition {
overflow: unset !important;
.v-window__container {
block-size: auto !important;
}
.v-window-item:not(.v-window-item--active) {
display: none !important;
}
.v-window__container .v-window-item {
transform: none !important;
}
}
// 👉 List
.v-list {
// Set icons opacity to .87
.v-list-item__prepend > .v-icon,
.v-list-item__append > .v-icon {
opacity: 1;
}
}
// 👉 Card list
/*
Custom class
Remove list spacing inside card
This is because card title gets padding of 20px and list item have padding of 16px. Moreover, list container have padding-bottom as well.
*/
.card-list {
--v-card-list-gap: 20px;
&.v-list {
padding-block: 0;
}
.v-list-item {
min-block-size: unset;
min-block-size: auto !important;
padding-block: 0 !important;
padding-inline: 0 !important;
> .v-ripple__container {
opacity: 0;
}
&:not(:last-child) {
padding-block-end: var(--v-card-list-gap) !important;
}
}
.v-list-item:hover,
.v-list-item:focus,
.v-list-item:active,
.v-list-item.active {
> .v-list-item__overlay {
opacity: 0 !important;
}
}
}
// 👉 Divider
.v-divider {
color: rgb(var(--v-border-color));
}
.v-divider.v-divider--vertical {
block-size: inherit;
}
// 👉 DataTable
.v-data-table {
/* stylelint-disable-next-line no-descending-specificity */
.v-checkbox-btn .v-selection-control__wrapper {
margin-inline-start: 0 !important;
}
.v-selection-control {
display: flex !important;
}
.v-pagination {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
}
// 👉 v-field
.v-field:hover .v-field__outline {
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
}
// 👉 VLabel
.v-label {
opacity: 1 !important;
&:not(.v-field-label--floating) {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
}
// 👉 Overlay
.v-overlay__scrim,
.v-navigation-drawer__scrim {
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)) !important;
opacity: 1 !important;
}
// 👉 VMessages
.v-messages {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
opacity: 1 !important;
}
// 👉 Alert close btn
.v-alert__close {
.v-btn--icon .v-icon {
--v-icon-size-multiplier: 1.5;
}
}
// 👉 Badge icon alignment
.v-badge__badge {
display: flex;
align-items: center;
}
// 👉 Btn focus outline style removed
.v-btn:focus-visible::after {
opacity: 0 !important;
}
// .v-select chip spacing for slot
.v-input:not(.v-select--chips) .v-select__selection {
.v-chip {
margin-block: 2px var(--select-chips-margin-bottom);
}
}
// 👉 VCard and VList subtitle color
.v-list-item-subtitle {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
// 👉 placeholders
.v-field__input {
@at-root {
& input::placeholder,
input#{&}::placeholder,
textarea#{&}::placeholder {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
opacity: 1 !important;
}
}
}

View File

@@ -0,0 +1,62 @@
@use "sass:map";
/* 👉 Shadow opacities */
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
/* 👉 Card transition properties */
$card-transition-property-custom: box-shadow, opacity;
@forward "vuetify/settings" with (
// 👉 General settings
$color-pack: false !default,
// 👉 Shadow opacity
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
// 👉 Card
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
$card-elevation: 6 !default,
$card-title-line-height: 1.6 !default,
$card-actions-min-height: unset !default,
$card-text-padding: 1.25rem !default,
$card-item-padding: 1.25rem !default,
$card-actions-padding: 0 12px 12px !default,
$card-transition-property: $card-transition-property-custom !default,
$card-subtitle-opacity: 1 !default,
// 👉 Expansion Panel
$expansion-panel-active-title-min-height: 48px !default,
// 👉 List
$list-item-icon-margin-end: 16px !default,
$list-item-icon-margin-start: 16px !default,
$list-item-subtitle-opacity: 1 !default,
// 👉 Navigation Drawer
$navigation-drawer-content-overflow-y: hidden !default,
// 👉 Tooltip
$tooltip-background-color: rgba(59, 55, 68, 0.9) !default,
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
$tooltip-font-size: 0.75rem !default,
// 👉 VTimeline
$timeline-dot-size: 34px !default,
// 👉 table
$table-transition-property: height !default,
// 👉 VOverlay
$overlay-opacity: 1 !default,
// 👉 VContainer
$container-max-widths: (
"xl": 1440px,
"xxl": 1440px
) !default,
);

View File

@@ -0,0 +1,27 @@
@use "@configured-variables" as variables;
@use "misc";
@use "@core/scss/base/mixins";
%default-layout-horizontal-nav-navbar-and-nav-container {
@include mixins.elevation(3);
// 1000 is v-window z-index
z-index: 1001;
background-color: rgb(var(--v-theme-surface));
&.header-blur {
@extend %blurry-bg;
}
}
%default-layout-horizontal-nav-navbar {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
%default-layout-horizontal-nav-nav {
padding-block: variables.$horizontal-nav-padding;
}
%default-layout-horizontal-nav-nav-items-list {
gap: variables.$horizontal-nav-top-level-items-gap;
}

View File

@@ -0,0 +1,45 @@
@use "@configured-variables" as variables;
@use "misc";
@use "@core/scss/base/mixins";
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
background-color: rgb(var(--v-theme-surface));
}
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
@include mixins.elevation(variables.$vertical-nav-navbar-elevation);
// If navbar is contained => Squeeze navbar content on scroll
@if variables.$layout-vertical-nav-navbar-is-contained {
padding-inline: 1.2rem;
}
}
%default-layout-vertical-nav-floating-navbar-overlay {
isolation: isolate;
&::after {
position: absolute;
z-index: -1;
/* stylelint-disable property-no-vendor-prefix */
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
/* stylelint-enable */
background:
linear-gradient(
180deg,
rgba(var(--v-theme-background), 70%) 44%,
rgba(var(--v-theme-background), 43%) 73%,
rgba(var(--v-theme-background), 0%)
);
background-repeat: repeat;
block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem);
content: "";
inset-block-start: -(variables.$vertical-nav-floating-navbar-top);
inset-inline: 0 0;
/* stylelint-disable property-no-vendor-prefix */
-webkit-mask: linear-gradient(black, black 18%, transparent 100%);
mask: linear-gradient(black, black 18%, transparent 100%);
/* stylelint-enable */
}
}

View File

@@ -0,0 +1,3 @@
%layout-navbar {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}

View File

@@ -0,0 +1,98 @@
@use "@layouts/styles/mixins" as layoutsMixins;
@use "@core/scss/base/variables";
@use "@layouts/styles/placeholders";
@use "@core/scss/base/mixins";
// Horizontal nav item styles (including nested)
%horizontal-nav-item {
padding-block: 0.6rem;
padding-inline: 1rem;
}
// Top level horizontal nav item styles (`a` tag & group label)
%horizontal-nav-top-level-item {
border-radius: 0.4rem;
}
%horizontal-nav-disabled {
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
// Active styles for sub nav link
%horizontal-nav-sub-nav-link-active {
background: rgba(var(--v-theme-primary), 0.1);
color: rgb(var(--v-theme-primary));
}
/*
This style is required when you don't provide any transition to horizontal nav items via themeConfig `themeConfig.horizontalNav.transition`
Also, you have to disable it if you are using transition
*/
// Popper content styles when it's hidden
%horizontal-nav-popper-content-hidden {
// display: none;
// opacity: 0;
// pointer-events: none;
// transform: translateY(7px);
// transition: transform 0.25s ease-in-out, opacity 0.15s ease-in-out;
}
/*
This style is required when you don't provide any transition to horizontal nav items via themeConfig `themeConfig.horizontalNav.transition`
Also, you have to disable it if you are using transition
*/
// Popper content styles when it's shown
%horizontal-nav-popper-content-visible {
// display: block;
// opacity: 1;
// pointer-events: auto;
// pointer-events: auto;
// transform: translateY(0);
}
// Horizontal nav item icon (Including sub nav items)
%horizontal-nav-item-icon {
font-size: variables.$horizontal-nav-items-icon-size;
margin-inline-end: variables.$horizontal-nav-items-icon-margin-inline-end;
}
// Horizontal nav subitem
%horizontal-nav-subitem {
min-inline-size: 12rem;
.nav-item-title {
margin-inline-end: 1rem;
}
}
// Styles for third level item icon/ (e.g. Reduce the icon's size of nested group's nav links (Top level group > Sub group > [Nav links]))
%third-level-nav-item-icon {
font-size: variables.$horizontal-nav-third-level-icon-size;
margin-inline: calc((variables.$horizontal-nav-items-icon-size - variables.$horizontal-nav-third-level-icon-size) / 2) 0.75rem;
/*
`margin-inline` will be (normal icon font-size - small icon font-size) / 2
(1.5rem - 0.9rem) / 2 => 0.6rem / 2 => 0.3rem
*/
}
// Horizontal nav item title
%horizontal-nav-item-title {
margin-inline-end: 0.3rem;
white-space: nowrap;
}
// Popper content styles
%horizontal-nav-popper-content {
@include mixins.elevation(4);
border-radius: 6px;
padding-block: 0.3rem;
> div {
@extend %style-scroll-bar;
}
}

View File

@@ -0,0 +1,7 @@
@forward "horizontal-nav";
@forward "vertical-nav";
@forward "nav";
@forward "default-layout";
@forward "default-layout-vertical-nav";
@forward "default-layout-horizontal-nav";
@forward "misc";

View File

@@ -0,0 +1,7 @@
%blurry-bg {
/* stylelint-disable property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
/* stylelint-enable */
background-color: rgb(var(--v-theme-surface), 0.9);
}

View File

@@ -0,0 +1,33 @@
@use "@core/scss/base/mixins";
// This is common style that needs to be applied to both navs
%nav {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
.nav-item-title {
letter-spacing: 0.15px;
}
.nav-section-title {
letter-spacing: 0.4px;
}
}
/*
Active nav link styles for horizontal & vertical nav
For horizontal nav it will be only applied to top level nav items
For vertical nav it will be only applied to nav links (not nav groups)
*/
%nav-link-active {
background-color: rgb(var(--v-global-theme-primary));
color: rgb(var(--v-theme-on-primary));
@include mixins.elevation(3);
}
%nav-link {
a {
color: inherit;
}
}

View File

@@ -0,0 +1,80 @@
@use "@core/scss/base/mixins";
@use "@configured-variables" as variables;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
%nav-header-action {
font-size: 1.25rem;
}
// Nav items styles (including section title)
%vertical-nav-item {
margin-block: 0;
margin-inline: variables.$vertical-nav-horizontal-spacing;
padding-block: 0;
padding-inline: variables.$vertical-nav-horizontal-padding;
white-space: nowrap;
}
// This is same as `%vertical-nav-item` except section title is excluded
%vertical-nav-item-interactive {
border-radius: 0.4rem;
block-size: 2.75rem;
/*
We will use `margin-block-end` instead of `margin-block` to give more space for shadow to appear.
With `margin-block`, due to small space (space gets divided between top & bottom) shadow cuts
*/
margin-block-end: 0.375rem;
}
// Common styles for nav item icon styles
// Nav group's children icon styles are not here (Adjusts height, width & margin)
%vertical-nav-items-icon {
flex-shrink: 0;
font-size: variables.$vertical-nav-items-icon-size;
margin-inline-end: variables.$vertical-nav-items-icon-margin-inline-end;
}
// Icon styling for icon nested inside another nav item (2nd level)
%vertical-nav-items-nested-icon {
/*
`margin-inline` will be (normal icon font-size - small icon font-size) / 2
(1.5rem - 0.9rem) / 2 => 0.6rem / 2 => 0.3rem
*/
$vertical-nav-items-nested-icon-margin-inline: calc((variables.$vertical-nav-items-icon-size - variables.$vertical-nav-items-nested-icon-size) / 2);
font-size: variables.$vertical-nav-items-nested-icon-size;
margin-inline: $vertical-nav-items-nested-icon-margin-inline $vertical-nav-items-nested-icon-margin-inline + variables.$vertical-nav-items-icon-margin-inline-end;
}
%vertical-nav-items-icon-after-2nd-level {
visibility: hidden;
}
// Open & Active nav group styles
%vertical-nav-group-open-active {
@include mixins.selected-states("&::before");
}
// Section title
// Setting height will prevent jerking when text & icon is toggled
%vertical-nav-section-title {
block-size: 1.5rem;
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: 0.75rem;
text-transform: uppercase;
}
// Vertical nav item badge styles
%vertical-nav-item-badge {
display: inline-block;
border-radius: 1.5rem;
font-size: 0.8em;
font-weight: 500;
line-height: 1;
padding-block: 0.25em;
padding-inline: 0.55em;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}

View File

@@ -0,0 +1,72 @@
@use "sass:map";
@use "@core/scss/base/mixins";
@use "@configured-variables" as variables;
@use "../utils";
$header: ".layout-navbar";
@if variables.$layout-vertical-nav-navbar-is-contained {
$header: ".layout-navbar .navbar-content-container";
}
.skin--bordered {
@include mixins.bordered-skin(".v-card:not(.v-card--flat)");
@include mixins.bordered-skin(".v-menu .v-overlay__content > .v-card, .v-menu .v-overlay__content > .v-sheet, .v-menu .v-overlay__content > .v-list");
@include mixins.bordered-skin(".popper-content");
// Navbar
// -- Horizontal
@include mixins.bordered-skin(".layout-navbar-and-nav-container", "border-bottom");
// -- Vertical
// We have added `.layout-navbar-sticky` as well in selector because we don't want to add borders if navbar is static
@if variables.$layout-vertical-nav-navbar-is-contained {
@include mixins.bordered-skin(".layout-nav-type-vertical.window-scrolled.layout-navbar-sticky #{$header}");
.layout-nav-type-vertical.window-scrolled #{$header} {
border-block-start: none !important;
}
}
// stylelint-disable-next-line @stylistic/indentation
@else {
@include mixins.bordered-skin(".layout-nav-type-vertical.window-scrolled.layout-navbar-sticky #{$header}", "border-bottom");
}
// Footer
// -- Vertical
@include mixins.bordered-skin(".layout-nav-type-vertical.layout-footer-sticky .layout-footer .footer-content-container");
.layout-nav-type-vertical.layout-footer-sticky .layout-footer .footer-content-container {
border-block-end: none;
}
// -- Horizontal
@include mixins.bordered-skin(".layout-nav-type-horizontal.layout-footer-sticky .layout-footer");
.layout-nav-type-horizontal.layout-footer-sticky .layout-footer {
border-block-end: none;
}
// Vertical Nav
.layout-vertical-nav {
border-inline-end: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
}
// Expansion Panels
.v-expansion-panels:not(.customized-panels) {
.v-expansion-panel__shadow {
box-shadow: none !important;
}
.v-expansion-panel {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
&:not(:last-child) {
margin-block-end: -1px;
}
&::after {
content: none;
}
}
}
}

View File

@@ -0,0 +1,9 @@
.layout-wrapper.layout-nav-type-horizontal {
.layout-navbar-and-nav-container {
.app-logo {
.app-title {
font-size: 22px;
}
}
}
}

View File

@@ -0,0 +1,20 @@
@use "@core/scss/base/mixins";
.layout-wrapper.layout-nav-type-vertical {
// 👉 Layout footer
.layout-footer {
$ele-layout-footer: &;
.footer-content-container {
// Sticky footer
@at-root {
// .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer
.layout-footer-sticky#{$ele-layout-footer} {
.footer-content-container {
@include mixins.elevation(6);
}
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
@use "@core/scss/template/placeholders" as *;
.layout-horizontal-nav {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
// SECTION Nav Group
.nav-group,
.nav-link {
.popper-content {
.nav-link.sub-item a,
.nav-group-label {
@extend %nav-group-label-and-nav-link-style;
}
.nav-group.active {
> .popper-triggerer .nav-group-label {
font-weight: 500;
}
}
.nav-group.sub-item .sub-item {
.nav-group-label .nav-item-icon {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
a:not(.router-link-exact-active) .nav-item-icon {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
}
.nav-link.sub-item {
.router-link-active.router-link-exact-active {
font-weight: 500;
}
}
}
}
// !SECTION
}

View File

@@ -0,0 +1,6 @@
@use "sass:map";
@use "@configured-variables" as variables;
@mixin custom-elevation($color, $size) {
box-shadow: (map.get(variables.$shadow-params, $size) rgba($color, map.get(variables.$shadow-opacity, $size)));
}

View File

@@ -0,0 +1,49 @@
.v-timeline-item {
.app-timeline-title {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 15px;
font-weight: 500;
line-height: 1.3125rem;
}
.app-timeline-meta {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: 11px;
line-height: 0.875rem;
}
.app-timeline-text {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
font-size: 13px;
line-height: 1.25rem;
}
}
// Temporary solution as v-spacer style is not getting applied in build version. will remove this after release.
// VSpacer
.v-spacer {
flex-grow: 1;
}
// app-logo & app-logo-title
.app-logo {
display: flex;
align-items: center;
column-gap: 0.75rem;
.app-logo-title {
font-size: 1.375rem;
font-weight: 700;
letter-spacing: 0.25px;
line-height: 1.5rem;
text-transform: capitalize;
}
}
.text-white-variant {
color: rgba(255, 255, 255, 78%) !important;
}
.bg-custom-background {
background-color: rgb(var(--v-table-header-color));
}

View File

@@ -0,0 +1,102 @@
@forward "@core/scss/base/variables" with (
$default-layout-with-vertical-nav-navbar-footer-roundness: 6px !default,
$vertical-nav-navbar-style: "floating" !default, // options: elevated, floating
// 👉 Vertical nav
$vertical-nav-background-color-rgb: var(--v-theme-surface) !default,
// This is used to keep consistency between nav items and nav header left & right margin
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 0.75rem !default,
// Section title margin top (when its not first child)
$vertical-nav-section-title-mt: 1rem !default,
$vertical-nav-navbar-elevation: 4 !default,
$vertical-nav-horizontal-padding: 0.75rem !default,
$layout-vertical-nav-collapsed-width: 70px !default,
// Move logo when vertical nav is mini (collapsed but not hovered)
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -1px !default,
// Section title margin bottom
$vertical-nav-section-title-mb: 0.375rem !default,
// Vertical nav header padding
$vertical-nav-header-padding: 1.25rem 0.5rem !default,
// Vertical nav icons
$vertical-nav-items-icon-size: 1.375rem !default,
$vertical-nav-items-nested-icon-size: 0.75rem !default,
// 👉Footer
$layout-vertical-nav-footer-height: 54px !default,
// Gap between top level horizontal nav items
$horizontal-nav-top-level-items-gap: 6px !default,
$horizontal-nav-items-icon-margin-inline-end: 0.5rem !default,
$horizontal-nav-popper-content-top: 0.375rem !default,
$horizontal-nav-group-arrow-icon-size: 1.25rem !default,
$horizontal-nav-third-level-icon-size: 0.75rem !default,
/*
Heads up
==================
Here we assume we will always use shorthand property which will apply same padding on four side
This is because this have been used as value of top property by `.popper-content`
*/
$horizontal-nav-padding: 0.5rem !default,
// 👉 Navbar
$layout-vertical-nav-navbar-height: 54px !default,
$layout-horizontal-nav-navbar-height: 54px !default,
// Font sizes
$font-sizes: (
"xs": 0.6875rem,
"sm": 0.8125rem,
"base": 0.9375rem,
"lg": 1.125rem,
"xl": 1.5rem,
"2xl": 1.75rem,
"3xl": 2rem,
"4xl": 2.375rem,
"5xl": 3rem,
"6xl": 3.5rem,
"7xl": 4rem,
"8xl": 4.5rem,
"9xl": 5.25rem,
) !default,
// Line heights
$font-line-height: (
"xs": 0.9375rem,
"sm": 1.25rem,
"base": 1.375rem,
"lg": 1.75rem,
"xl": 2.375rem,
"2xl": 2.625rem,
"3xl": 2.75rem,
"4xl": 3.25rem,
"5xl": 1,
"6xl": 1,
"7xl": 1,
"8xl": 1,
"9xl": 1
) !default,
);
/* Custom shadow opacity */
$shadow-opacity: (
"sm": 0.3,
"md": 0.4,
"lg": 0.5,
) !default;
/* Custom shadow params */
$shadow-params: (
"sm": 0 2px 6px 0,
"md": 0 4px 16px 0,
"lg": 0 6px 20px 0,
) !default;

Some files were not shown because too many files have changed in this diff Show More