Merge pull request #317 from dreamhubone/v3

add theme
This commit is contained in:
ping 2023-02-06 22:34:54 +08:00 committed by GitHub
commit 59a7034e5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
279 changed files with 16539 additions and 148 deletions

602
packages/dashboard/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,602 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/math')['logicAnd']
const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useAbs: typeof import('@vueuse/math')['useAbs']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAverage: typeof import('@vueuse/math')['useAverage']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFloor: typeof import('@vueuse/math')['useFloor']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMath: typeof import('@vueuse/math')['useMath']
const useMax: typeof import('@vueuse/math')['useMax']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMin: typeof import('@vueuse/math')['useMin']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePrecision: typeof import('@vueuse/math')['usePrecision']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSum: typeof import('@vueuse/math')['useSum']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToFixed: typeof import('@vueuse/math')['useToFixed']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useTrunc: typeof import('@vueuse/math')['useTrunc']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly logicAnd: UnwrapRef<typeof import('@vueuse/math')['logicAnd']>
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAverage: UnwrapRef<typeof import('@vueuse/math')['useAverage']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFloor: UnwrapRef<typeof import('@vueuse/math')['useFloor']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMath: UnwrapRef<typeof import('@vueuse/math')['useMath']>
readonly useMax: UnwrapRef<typeof import('@vueuse/math')['useMax']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePrecision: UnwrapRef<typeof import('@vueuse/math')['usePrecision']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToFixed: UnwrapRef<typeof import('@vueuse/math')['useToFixed']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useTrunc: UnwrapRef<typeof import('@vueuse/math')['useTrunc']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}

51
packages/dashboard/components.d.ts vendored Normal file
View File

@ -0,0 +1,51 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AddAuthenticatorAppDialog: typeof import('./src/plugins/vuetify/@core/components/AddAuthenticatorAppDialog.vue')['default']
AddEditAddressDialog: typeof import('./src/plugins/vuetify/@core/components/AddEditAddressDialog.vue')['default']
AppBarSearch: typeof import('./src/plugins/vuetify/@core/components/AppBarSearch.vue')['default']
AppCardActions: typeof import('./src/plugins/vuetify/@core/components/AppCardActions.vue')['default']
AppCardCode: typeof import('./src/plugins/vuetify/@core/components/AppCardCode.vue')['default']
AppDateTimePicker: typeof import('./src/plugins/vuetify/@core/components/AppDateTimePicker.vue')['default']
AppDrawerHeaderSection: typeof import('./src/plugins/vuetify/@core/components/AppDrawerHeaderSection.vue')['default']
AppOtpInput: typeof import('./src/plugins/vuetify/@core/components/AppOtpInput.vue')['default']
AppPricing: typeof import('./src/plugins/vuetify/@core/components/AppPricing.vue')['default']
AppSearchHeader: typeof import('./src/plugins/vuetify/@core/components/AppSearchHeader.vue')['default']
BuyNow: typeof import('./src/plugins/vuetify/@core/components/BuyNow.vue')['default']
CardAddEditDialog: typeof import('./src/plugins/vuetify/@core/components/CardAddEditDialog.vue')['default']
CardStatisticsHorizontal: typeof import('./src/plugins/vuetify/@core/components/CardStatisticsHorizontal.vue')['default']
CardStatisticsVertical: typeof import('./src/plugins/vuetify/@core/components/CardStatisticsVertical.vue')['default']
CardStatisticsWithImages: typeof import('./src/plugins/vuetify/@core/components/CardStatisticsWithImages.vue')['default']
ConfirmDialog: typeof import('./src/plugins/vuetify/@core/components/ConfirmDialog.vue')['default']
CustomCheckboxes: typeof import('./src/plugins/vuetify/@core/components/CustomCheckboxes.vue')['default']
CustomCheckboxesWithIcon: typeof import('./src/plugins/vuetify/@core/components/CustomCheckboxesWithIcon.vue')['default']
CustomCheckboxesWithImage: typeof import('./src/plugins/vuetify/@core/components/CustomCheckboxesWithImage.vue')['default']
CustomizerSection: typeof import('./src/plugins/vuetify/@core/components/CustomizerSection.vue')['default']
CustomRadios: typeof import('./src/plugins/vuetify/@core/components/CustomRadios.vue')['default']
CustomRadiosWithIcon: typeof import('./src/plugins/vuetify/@core/components/CustomRadiosWithIcon.vue')['default']
CustomRadiosWithImage: typeof import('./src/plugins/vuetify/@core/components/CustomRadiosWithImage.vue')['default']
DialogCloseBtn: typeof import('./src/plugins/vuetify/@core/components/DialogCloseBtn.vue')['default']
EnableOneTimePasswordDialog: typeof import('./src/plugins/vuetify/@core/components/EnableOneTimePasswordDialog.vue')['default']
ErrorHeader: typeof import('./src/plugins/vuetify/@core/components/ErrorHeader.vue')['default']
I18n: typeof import('./src/plugins/vuetify/@core/components/I18n.vue')['default']
MoreBtn: typeof import('./src/plugins/vuetify/@core/components/MoreBtn.vue')['default']
Notifications: typeof import('./src/plugins/vuetify/@core/components/Notifications.vue')['default']
PricingPlanDialog: typeof import('./src/plugins/vuetify/@core/components/PricingPlanDialog.vue')['default']
ReferAndEarnDialog: typeof import('./src/plugins/vuetify/@core/components/ReferAndEarnDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ShareProjectDialog: typeof import('./src/plugins/vuetify/@core/components/ShareProjectDialog.vue')['default']
Shortcuts: typeof import('./src/plugins/vuetify/@core/components/Shortcuts.vue')['default']
TheCustomizer: typeof import('./src/plugins/vuetify/@core/components/TheCustomizer.vue')['default']
ThemeSwitcher: typeof import('./src/plugins/vuetify/@core/components/ThemeSwitcher.vue')['default']
TwoFactorAuthDialog: typeof import('./src/plugins/vuetify/@core/components/TwoFactorAuthDialog.vue')['default']
UserInfoEditDialog: typeof import('./src/plugins/vuetify/@core/components/UserInfoEditDialog.vue')['default']
UserUpgradePlanDialog: typeof import('./src/plugins/vuetify/@core/components/UserUpgradePlanDialog.vue')['default']
}
}

View File

@ -11,10 +11,21 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@casl/ability": "^6.3.3",
"@casl/vue": "^2.2.1",
"@floating-ui/dom": "^1.2.0",
"@iconify/vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vueuse/core": "^9.12.0",
"@vueuse/math": "^9.12.0",
"pinia": "^2.0.28",
"vite-plugin-vue-layouts": "^0.7.0",
"vite-plugin-vuetify": "^1.0.2",
"vue": "^3.2.45",
"vue-router": "^4.1.6",
"vuetify": "^3.1.3"
"vue3-perfect-scrollbar": "^1.6.1",
"vuetify": "3.0.6",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.4",
@ -29,7 +40,11 @@
"prettier": "^2.7.1",
"sass": "^1.58.0",
"typescript": "~4.9.5",
"unplugin-auto-import": "^0.13.0",
"unplugin-vue-components": "^0.23.0",
"unplugin-vue-define-options": "1.1.4",
"vite": "^4.0.0",
"vite-plugin-pages": "^0.28.0",
"vue-tsc": "^1.0.12"
}
}

View File

@ -1,8 +1,22 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { useTheme } from 'vuetify'
import { useThemeConfig } from '@/plugins/vuetify/@core/composable/useThemeConfig'
import { hexToRgb } from '@/plugins/vuetify/@layouts/utils'
const { syncInitialLoaderTheme, syncVuetifyThemeWithTheme: syncConfigThemeWithVuetifyTheme, isAppRtl } = useThemeConfig()
const { global } = useTheme()
// Sync current theme with initial loader theme
syncInitialLoaderTheme()
syncConfigThemeWithVuetifyTheme()
</script>
<template>
<VLocaleProvider :rtl="isAppRtl">
<!-- This is required to set the background color of active nav link based on currently active global theme's primary -->
<VApp :style="`--v-global-theme-primary: ${hexToRgb(global.current.value.colors.primary)}`">
<RouterView />
</VApp>
</VLocaleProvider>
</template>

View File

@ -1,22 +1,26 @@
/* eslint-disable import/order */
import "@/plugins/vuetify/@iconify/icons-bundle";
import App from "@/App.vue";
import layoutsPlugin from "@/plugins/vuetify/layouts";
import vuetify from "@/plugins/vuetify";
import { loadFonts } from "@/plugins/vuetify/webfontloader";
import "@/plugins/vuetify/@core/scss/template/index.scss";
import "@/plugins/vuetify/styles/styles.scss";
import { createApp } from "vue";
import { createPinia } from "pinia";
// import router from "./router";
import router from "@/plugins/vuetify/router";
import App from "./App.vue";
import router from "./router";
import { createVuetify } from "vuetify";
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import './scss/index.scss'
loadFonts();
// Create vue app
const app = createApp(App);
// Use plugins
app.use(vuetify);
app.use(createPinia());
app.use(router);
app.use(createVuetify({
components,
directives,
}));
app.use(layoutsPlugin);
// Mount vue app
app.mount("#app");

View File

@ -1,3 +1,5 @@
<template>
<div> Hello, Dashboard</div>
<div>
dashboard view
</div>
</template>

View File

@ -2,8 +2,8 @@ import type { RouteRecordRaw } from "vue-router";
export const routes: RouteRecordRaw[] = [
{
path: '',
alias: ['dashboard'],
path: "/dashboard",
alias: ["dashboard"],
component: () => import("./View.vue"),
},
]
];

View File

@ -0,0 +1,100 @@
<script setup lang="ts">
import pixinventQr from '@images/pages/pixinvent-qr.png'
interface Emit {
(e: 'update:isDialogVisible', value: boolean): void
(e: 'submit', value: string): void
}
interface Props {
authCode?: string
isDialogVisible: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const authCode = ref(structuredClone(toRaw(props.authCode)))
const formSubmit = () => {
if (authCode.value) {
emit('submit', authCode.value)
emit('update:isDialogVisible', false)
}
}
const resetAuthCode = () => {
authCode.value = structuredClone(toRaw(props.authCode))
emit('update:isDialogVisible', false)
}
</script>
<template>
<VDialog
max-width="600"
:model-value="props.isDialogVisible"
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
>
<VCard class="pa-5 pa-sm-8">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="resetAuthCode"
/>
<VCardItem>
<VCardTitle class="text-h5 font-weight-medium text-center">
Add Authenticator App
</VCardTitle>
</VCardItem>
<VCardText class="pt-6">
<h6 class="text-h6 font-weight-medium mb-3">
Authenticator Apps
</h6>
<p class="mb-6">
Using an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password, scan the QR code. It will generate a 6 digit code for you to enter below.
</p>
<div class="my-6">
<VImg
width="122"
:src="pixinventQr"
class="mx-auto"
/>
</div>
<VForm @submit.prevent="() => {}">
<VTextField
v-model="authCode"
name="auth-code"
label="Enter Authentication Code"
class="mb-4"
/>
<div class="d-flex justify-end flex-wrap gap-3">
<VBtn
color="secondary"
variant="tonal"
@click="resetAuthCode"
>
Cancel
</VBtn>
<VBtn
type="submit"
@click="formSubmit"
>
Continue
<VIcon
end
icon="mdi-chevron-right"
class="flip-in-rtl"
/>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@ -0,0 +1,261 @@
<script setup lang="ts">
interface BillingAddress {
companyName: string
billingEmail: string
taxID: string
vatNumber: string
address: string
contact: string
country: string
state: string
zipCode: string
isSaveDefaultAddress: boolean
}
interface Props {
billingAddress?: BillingAddress
isDialogVisible: boolean
}
interface Emit {
(e: 'update:isDialogVisible', value: boolean): void
(e: 'submit', value: BillingAddress): void
}
const props = withDefaults(defineProps<Props>(), {
billingAddress: () => ({
companyName: '',
billingEmail: '',
taxID: '',
vatNumber: '',
address: '',
contact: '',
country: '',
state: '',
zipCode: '',
isSaveDefaultAddress: true,
}),
})
const emit = defineEmits<Emit>()
const billingAddress = ref<BillingAddress>(structuredClone(toRaw(props.billingAddress)))
const resetForm = () => {
emit('update:isDialogVisible', false)
billingAddress.value = structuredClone(toRaw(props.billingAddress))
}
const onFormSubmit = () => {
emit('update:isDialogVisible', false)
emit('submit', billingAddress.value)
}
const selectedAddress = ref('Home')
const addressTypes = [
{
icon: 'mdi-home-outline',
title: 'Home',
time: 'Delivery Time (7am - 9pm)',
},
{
icon: 'mdi-briefcase-outline',
title: 'Office',
time: 'Delivery Time (10am - 6pm)',
},
]
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 800"
:model-value="props.isDialogVisible"
@update:model-value="val => $emit('update:isDialogVisible', val)"
>
<VCard
v-if="props.billingAddress"
class="pa-sm-8 pa-5"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="resetForm"
/>
<!-- 👉 Title -->
<VCardItem>
<VCardTitle class="text-h5 text-center">
{{ props.billingAddress.address ? 'Edit' : 'Add New' }} Address
</VCardTitle>
</VCardItem>
<VCardText class="pt-3">
<!-- 👉 Subtitle -->
<VCardSubtitle class="text-center mb-8">
Edit Address for future billing
</VCardSubtitle>
<VRow>
<VCol
v-for="type in addressTypes"
:key="type.title"
cols="12"
sm="6"
>
<div
class="custom-address-input border rounded cursor-pointer pa-4"
:class="selectedAddress === type.title ? 'bg-light-primary text-primary border-primary' : 'bg-light-secondary border-secondary'"
style="/* stylelint-disable-next-line max-empty-lines */
--v-border-opacity: 1;"
@click="selectedAddress = type.title"
>
<div class="d-flex align-center font-weight-medium gap-2 text-xl mb-1">
<VIcon
size="24"
:icon="type.icon"
/>
<span>{{ type.title }}</span>
</div>
<span>{{ type.time }}</span>
</div>
</VCol>
</VRow>
<!-- 👉 Form -->
<VForm
class="mt-4"
@submit.prevent="onFormSubmit"
>
<VRow>
<!-- 👉 Company Name -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.companyName"
label="Company Name"
/>
</VCol>
<!-- 👉 Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.billingEmail"
label="Email"
/>
</VCol>
<!-- 👉 Tax ID -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.taxID"
label="Tax ID"
/>
</VCol>
<!-- 👉 VAT Number -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.vatNumber"
label="VAT Number"
/>
</VCol>
<!-- 👉 Billing Address -->
<VCol cols="12">
<VTextarea
v-model="billingAddress.address"
rows="2"
label="Billing Address"
/>
</VCol>
<!-- 👉 Contact -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.contact"
label="Contact"
/>
</VCol>
<!-- 👉 Country -->
<VCol
cols="12"
md="6"
>
<VSelect
v-model="billingAddress.country"
label="Country"
:items="['USA', 'Uk', 'France', 'Germany', 'Japan']"
/>
</VCol>
<!-- 👉 State -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.state"
label="State"
/>
</VCol>
<!-- 👉 Zip Code -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="billingAddress.zipCode"
label="Zip Code"
/>
</VCol>
<!-- 👉 default address -->
<VCol cols="12">
<VSwitch
v-model="billingAddress.isSaveDefaultAddress"
label="Make this default shipping address"
/>
</VCol>
<!-- 👉 Submit and Cancel button -->
<VCol
cols="12"
class="text-center"
>
<VBtn
type="submit"
class="me-3"
>
submit
</VBtn>
<VBtn
variant="tonal"
color="secondary"
@click="resetForm"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@ -0,0 +1,378 @@
<script setup lang="ts">
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { VList, VListItem, VListSubheader } from 'vuetify/components'
interface Emit {
(e: 'update:isDialogVisible', value: boolean): void
(e: 'update:searchQuery', value: string): void
(e: 'itemSelected', value: any): void
}
interface Suggestion {
icon: string
title: string
url: object
}
interface Suggestions {
title: string
content: Suggestion[]
}
interface Props {
isDialogVisible: boolean
searchQuery: string
searchResults: any[]
suggestions?: Suggestions[]
noDataSuggestion?: Suggestion[]
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
// 👉 Hotkey
const { ctrl_k, meta_k } = useMagicKeys()
const refSearchList = ref<VList>()
const searchQuery = ref(structuredClone(toRaw(props.searchQuery)))
const refSearchInput = ref<HTMLInputElement>()
const isLocalDialogVisible = ref(structuredClone(toRaw(props.isDialogVisible)))
const searchResults = ref(structuredClone(toRaw(props.searchResults)))
// 👉 Watching props change
watch(props, () => {
isLocalDialogVisible.value = structuredClone(toRaw(props.isDialogVisible))
searchResults.value = structuredClone(toRaw(props.searchResults))
searchQuery.value = structuredClone(toRaw(props.searchQuery))
})
// 👉 watching control + / to open dialog
watch([ctrl_k, meta_k], () => {
isLocalDialogVisible.value = true
emit('update:isDialogVisible', true)
})
// 👉 clear search result and close the dialog
const clearSearchAndCloseDialog = () => {
emit('update:isDialogVisible', false)
emit('update:searchQuery', '')
}
watchEffect(() => {
if (!searchQuery.value.length)
searchResults.value = []
})
// 👉 get fucus on search list
const getFocusOnSearchList = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
refSearchList.value?.focus('next')
}
else if (e.key === 'ArrowUp') {
e.preventDefault()
refSearchList.value?.focus('prev')
}
}
const dialogModelValueUpdate = (val: boolean) => {
emit('update:isDialogVisible', val)
emit('update:searchQuery', '')
}
// 👉 resolve categories name
const resolveCategories = (val: string) => {
if (val === 'dashboards')
return 'Dashboards'
if (val === 'appsPages')
return 'Apps & Pages'
if (val === 'userInterface')
return 'User Interface'
if (val === 'formsTables')
return 'Forms Tables'
if (val === 'chartsMisc')
return 'Charts Misc'
return 'Misc'
}
</script>
<template>
<VDialog
max-width="600"
:model-value="isLocalDialogVisible"
:height="$vuetify.display.smAndUp ? '550' : '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="pt-1"
style="max-height: 65px;"
>
<!-- 👉 Search Input -->
<VTextField
ref="refSearchInput"
v-model="searchQuery"
autofocus
variant="plain"
class="app-bar-autocomplete-box"
@keyup.esc="clearSearchAndCloseDialog"
@keydown="getFocusOnSearchList"
@update:model-value="$emit('update:searchQuery', searchQuery)"
>
<!-- 👉 Prepend Inner -->
<template #prepend-inner>
<VIcon
icon="mdi-magnify"
class="text-high-emphasis"
/>
</template>
<!-- 👉 Append Inner -->
<template #append-inner>
<div class="d-flex align-center mt-n1">
<div
class="text-base text-disabled cursor-pointer me-2"
@click="clearSearchAndCloseDialog"
>
[esc]
</div>
<IconBtn
size="x-small"
@click="clearSearchAndCloseDialog"
>
<VIcon icon="mdi-close" />
</IconBtn>
</div>
</template>
</VTextField>
</VCardText>
<!-- 👉 Divider -->
<VDivider />
<!-- 👉 Perfect Scrollbar -->
<PerfectScrollbar
:options="{ wheelPropagation: false, suppressScrollX: true }"
class="h-100"
>
<!-- 👉 Search List -->
<VList
v-show="searchQuery.length && !!searchResults.length"
ref="refSearchList"
density="compact"
class="app-bar-search-list"
>
<!-- 👉 list Item /List Sub header -->
<template
v-for="item in searchResults"
:key="item.title"
>
<VListSubheader
v-if="'header' in item"
class="text-disabled"
>
{{ resolveCategories(item.title) }}
</VListSubheader>
<template v-else>
<slot
name="searchResult"
:item="item"
>
<VListItem
link
@click="$emit('itemSelected', item)"
>
<template #prepend>
<VIcon
size="20"
:icon="item.icon"
class="me-3"
/>
</template>
<template #append>
<VIcon
size="20"
icon="mdi-subdirectory-arrow-left"
class="enter-icon text-disabled"
/>
</template>
<VListItemTitle>
{{ item.title }}
</VListItemTitle>
</VListItem>
</slot>
</template>
</template>
</VList>
<!-- 👉 Suggestions -->
<div
v-show="!!searchResults && !searchQuery"
class="h-100"
>
<slot name="suggestions">
<VCardText class="app-bar-search-suggestions h-100 pa-10">
<VRow
v-if="props.suggestions"
class="gap-y-4"
>
<VCol
v-for="suggestion in props.suggestions"
:key="suggestion.title"
cols="12"
sm="6"
class="ps-6"
>
<p class="text-xs text-disabled text-uppercase">
{{ suggestion.title }}
</p>
<VList class="card-list">
<VListItem
v-for="item in suggestion.content"
:key="item.title"
link
:title="item.title"
class="app-bar-search-suggestion"
@click="$emit('itemSelected', item)"
>
<template #prepend>
<VIcon
:icon="item.icon"
size="20"
class="me-2"
/>
</template>
</VListItem>
</VList>
</VCol>
</VRow>
</VCardText>
</slot>
</div>
<!-- 👉 No Data found -->
<div
v-show="!searchResults.length && searchQuery.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 h-100">
<VIcon
size="75"
icon="mdi-file-remove-outline"
/>
<div class="d-flex align-center flex-wrap justify-center gap-2 text-h6 my-3">
<span>No Result For </span>
<span>"{{ searchQuery }}"</span>
</div>
<div
v-if="props.noDataSuggestion"
class="mt-8"
>
<span class="d-flex justify-center text-disabled">Try searching for</span>
<h6
v-for="suggestion in props.noDataSuggestion"
:key="suggestion.title"
class="app-bar-search-suggestion text-sm font-weight-regular cursor-pointer mt-3"
@click="$emit('itemSelected', suggestion)"
>
<VIcon
size="20"
:icon="suggestion.icon"
class="me-3"
/>
<span class="text-sm">{{ suggestion.title }}</span>
</h6>
</div>
</div>
</VCardText>
</slot>
</div>
</PerfectScrollbar>
</VCard>
</VDialog>
</template>
<style lang="scss">
.app-bar-search-suggestions {
.app-bar-search-suggestion {
&:hover {
color: rgb(var(--v-theme-primary));
}
}
}
.app-bar-autocomplete-box {
.v-field__input {
padding-block-end: 0.425rem;
padding-block-start: 0.9375rem;
}
.v-field__field input {
text-align: start !important;
}
}
.app-bar-search-dialog {
.v-list-item-title {
font-size: 0.875rem !important;
}
.app-bar-search-list {
.v-list-item,
.v-list-subheader {
font-size: 0.75rem;
padding-inline: 1.5rem !important;
}
.v-list-item {
.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: 0.6875rem 0.3125rem;
text-transform: uppercase;
}
}
}
</style>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@ -0,0 +1,152 @@
<script setup lang="ts">
interface Props {
collapsed?: boolean
noActions?: boolean
actionCollapsed?: boolean
actionRefresh?: boolean
actionRemove?: boolean
title?: string
}
interface Emit {
(e: 'collapsed', isContentCollapsed: boolean): void
(e: 'refresh', hideOverlay: () => void): void
(e: 'trash'): void
}
const props = withDefaults(defineProps<Props>(), {
collapsed: false,
noActions: false,
actionCollapsed: false,
actionRefresh: false,
actionRemove: false,
title: undefined,
})
const emit = defineEmits<Emit>()
// inherit Attribute make false
defineOptions({
inheritAttrs: false,
})
const isContentCollapsed = ref(props.collapsed)
const isCardRemoved = ref(false)
const isOverlayVisible = ref(false)
// hiding overlay
const hideOverlay = () => {
isOverlayVisible.value = false
}
// trigger collapse
const triggerCollapse = () => {
isContentCollapsed.value = !isContentCollapsed.value
emit('collapsed', isContentCollapsed.value)
}
// trigger refresh
const triggerRefresh = () => {
isOverlayVisible.value = true
emit('refresh', hideOverlay)
}
// 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="mdi-chevron-up"
:style="{ transform: isContentCollapsed ? 'rotate(-180deg)' : null }"
style="transition-duration: 0.28s;"
/>
</IconBtn>
<!-- 👉 Overlay button -->
<IconBtn
v-if="(!(actionRemove || actionCollapsed) || actionRefresh) && !noActions"
@click="triggerRefresh"
>
<VIcon
size="20"
icon="mdi-refresh"
/>
</IconBtn>
<!-- 👉 Close button -->
<IconBtn
v-if="(!(actionRefresh || actionCollapsed) || actionRemove) && !noActions"
@click="triggeredRemove"
>
<VIcon
size="20"
icon="mdi-close"
/>
</IconBtn>
</div>
<!-- !SECTION -->
</template>
</VCardItem>
<!-- 👉 card content -->
<VExpandTransition>
<div
v-show="!isContentCollapsed"
class="v-card-content"
>
<slot />
</div>
</VExpandTransition>
<!-- 👉 Overlay -->
<VOverlay
v-model="isOverlayVisible"
contained
persistent
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,125 @@
<script lang="ts" setup>
import 'prismjs'
import 'prismjs/themes/prism-tomorrow.css'
import type { Ref } from 'vue'
import Prism from 'vue-prism-component'
interface Props {
title: string
code: CodeProp
codeLanguage?: string
noPadding?: boolean
}
interface CodeProp {
ts: string
js: string
}
const props = withDefaults(defineProps<Props>(), {
codeLanguage: 'markup',
noPadding: false,
})
const preferredCodeLanguage = useStorage('preferredCodeLanguage', 'ts') as unknown as Ref<keyof CodeProp>
const isCodeShown = ref(false)
const { copy, copied } = useClipboard({ source: computed(() => props.code[preferredCodeLanguage.value]) })
</script>
<template>
<VCard>
<VCardItem>
<VCardTitle>{{ props.title }}</VCardTitle>
<template #append>
<IconBtn
:color="isCodeShown ? 'primary' : 'default'"
:class="isCodeShown ? '' : 'text-disabled'"
@click="isCodeShown = !isCodeShown"
>
<VIcon
size="20"
icon="mdi-code-tags"
/>
</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
variant="outlined"
density="compact"
>
<VBtn
size="x-small"
value="ts"
:color="preferredCodeLanguage === 'ts' ? 'primary' : 'default'"
>
<VIcon
size="x-large"
icon="mdi-language-typescript"
:color="preferredCodeLanguage === 'ts' ? 'primary' : 'secondary'"
/>
</VBtn>
<VBtn
size="x-small"
value="js"
:color="preferredCodeLanguage === 'js' ? 'primary' : 'default'"
>
<VIcon
size="x-large"
icon="mdi-language-javascript"
:color="preferredCodeLanguage === 'js' ? 'primary' : 'secondary'"
/>
</VBtn>
</VBtnToggle>
</div>
<div class="position-relative">
<Prism
:key="props.code[preferredCodeLanguage]"
:language="props.codeLanguage"
>
{{ props.code[preferredCodeLanguage] }}
</Prism>
<IconBtn
class="position-absolute app-card-code-copy-icon"
color="white"
@click="() => { copy() }"
>
<VIcon
:icon="copied ? 'mdi-check' : 'mdi-content-copy'"
size="20"
/>
</IconBtn>
</div>
</VCardText>
</div>
</VExpandTransition>
</VCard>
</template>
<style lang="scss">
@use "@styles/variables/_vuetify.scss";
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
border-radius: vuetify.$card-border-radius;
}
.app-card-code-copy-icon {
inset-block-start: 1.2em;
inset-inline-end: 0.8em;
}
</style>

View File

@ -0,0 +1,416 @@
<script setup lang="ts">
import FlatPickr from 'vue-flatpickr-component'
import { useTheme } from 'vuetify'
// @ts-expect-error There won't be declaration file for it
import { filterFieldProps, makeVFieldProps } from 'vuetify/lib/components/VField/VField'
// @ts-expect-error There won't be declaration file for it
import { filterInputProps, makeVInputProps } from 'vuetify/lib/components/VInput/VInput'
// @ts-expect-error There won't be declaration file for it
import { filterInputAttrs } from 'vuetify/lib/util/helpers'
import { useThemeConfig } from '@core/composable/useThemeConfig'
const props = defineProps({
...makeVInputProps({
hideDetails: 'auto',
}),
...makeVFieldProps({
variant: 'outlined',
color: 'primary',
}),
})
const emit = defineEmits<Emit>()
interface Emit {
(e: 'update:modelValue', val: string): void
(e: 'click:clear', el: MouseEvent): void
}
// inherit Attribute make false
defineOptions({
inheritAttrs: false,
})
const attrs = useAttrs()
const [rootAttrs, compAttrs] = filterInputAttrs(attrs)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [{ modelValue: _, ...inputProps }] = filterInputProps(props)
const [fieldProps] = filterFieldProps(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' })
}
// v-field clear prop
const onClear = (el: MouseEvent) => {
el.stopPropagation()
nextTick(() => {
emit('update:modelValue', '')
emit('click:clear', el)
})
}
const { theme } = useThemeConfig()
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(theme, updateThemeClassInCalendar)
onMounted(() => {
updateThemeClassInCalendar()
})
const emitModelValue = (val: string) => {
emit('update:modelValue', val)
}
</script>
<template>
<!-- v-input -->
<VInput
v-bind="{ ...inputProps, ...rootAttrs }"
:model-value="modelValue"
:hide-details="props.hideDetails"
class="position-relative"
>
<template #default="{ isDirty, isValid, isReadonly }">
<!-- v-field -->
<VField
v-bind="fieldProps"
:active="focused || isDirty.value || isCalendarOpen"
:focused="focused || isCalendarOpen"
role="textbox"
:dirty="isDirty.value || props.dirty"
:error="isValid.value === false"
@click:clear="onClear"
>
<template #default="{ props: vFieldProps }">
<div v-bind="vFieldProps">
<!-- flat-picker -->
<FlatPickr
v-if="!isInlinePicker"
v-bind="compAttrs"
ref="refFlatPicker"
:model-value="modelValue"
class="flat-picker-custom-style"
:disabled="isReadonly.value"
@on-open="isCalendarOpen = true"
@on-close="isCalendarOpen = false"
@update:model-value="emitModelValue"
/>
<!-- simple input for inline prop -->
<input
v-if="isInlinePicker"
:value="modelValue"
class="flat-picker-custom-style"
type="text"
>
</div>
</template>
</VField>
</template>
</VInput>
<!-- flat picker for inline props -->
<FlatPickr
v-if="isInlinePicker"
v-bind="compAttrs"
ref="refFlatPicker"
:model-value="modelValue"
@update:model-value="emitModelValue"
@on-open="isCalendarOpen = true"
@on-close="isCalendarOpen = false"
/>
</template>
<style lang="scss">
/* stylelint-disable no-descending-specificity */
@use "flatpickr/dist/flatpickr.css";
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
.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-medium-emphasis-opacity));
// hide the input when your picker is inline
input[altinputclass="inlinePicker"] {
display: none;
}
.flatpickr-calendar {
border-radius: 10px;
background-color: rgb(var(--v-theme-surface));
margin-block-start: 0.1875rem;
@include mixins_elevation.elevation(6);
&.hasTime .flatpickr-time {
border-block-start: none;
}
.numInputWrapper:hover {
background: transparent;
}
.flatpickr-rContainer {
.flatpickr-weekdays {
padding-inline: 0.625rem;
}
.flatpickr-days {
.dayContainer {
justify-content: center !important;
padding-block-end: 0.625rem;
padding-block-start: 0;
.flatpickr-day {
block-size: 2.625rem;
line-height: 2.625rem;
margin-block-start: 0 !important;
max-inline-size: 2.625rem;
}
}
}
}
.flatpickr-day {
color: $body-color;
&.today {
border-color: rgb(var(--v-theme-primary));
&:hover {
border-color: rgb(var(--v-theme-primary));
background: transparent;
color: $body-color;
}
}
&.selected,
&.selected:hover {
border-color: rgb(var(--v-theme-primary));
background: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
}
&.inRange,
&.inRange:hover {
border: none;
background: rgba(var(--v-theme-primary), 0.1) !important;
box-shadow: none !important;
color: rgb(var(--v-theme-primary));
}
&.startRange {
box-shadow: none;
}
&.endRange {
box-shadow: none;
}
&.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: rgba(var(--v-theme-surface-variant), var(--v-hover-opacity));
background: rgba(var(--v-theme-surface-variant), var(--v-hover-opacity));
}
}
.flatpickr-weekday {
color: $heading-color;
font-size: 14px;
font-weight: 500;
}
&::after,
&::before {
display: none;
}
.flatpickr-months {
padding-block-start: 0.625rem;
.flatpickr-prev-month,
.flatpickr-next-month {
fill: $body-color;
&: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-time {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
block-size: auto;
}
}
}
// 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;
opacity: 1 !important;
}
// week sections
.flatpickr-weekdays {
margin-block-start: 8px;
}
// 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: 1rem;
font-weight: 400;
transition: all 0.15s ease-out;
span {
display: none;
}
.cur-year {
font-weight: 400;
}
.flatpickr-monthDropdown-month {
background-color: rgb(var(--v-theme-surface));
}
}
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover {
color: $body-color;
}
.flatpickr-months {
padding-block: 0.3rem;
padding-inline: 0;
.flatpickr-prev-month,
.flatpickr-next-month {
inset-block-start: 0.625rem !important;
}
.flatpickr-next-month {
inset-inline-end: 0.375rem !important;
}
.flatpickr-prev-month {
inset-inline-start: 0.25rem !important;
}
}
</style>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
interface Props {
title: string
}
interface Emit {
(e: 'cancel', el: MouseEvent): void
}
const props = defineProps<Props>()
defineEmits<Emit>()
</script>
<template>
<div class="px-5 py-3 d-flex align-center bg-var-theme-background">
<h3 class="font-weight-medium text-xl">
{{ props.title }}
</h3>
<VSpacer />
<slot name="beforeClose" />
<IconBtn @click="$emit('cancel')">
<VIcon
size="18"
icon="mdi-close"
/>
</IconBtn>
</div>
</template>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
interface Props {
totalInput?: number
default?: string
}
interface Emit {
(e: 'updateOtp', val: string): void
}
const props = withDefaults(defineProps<Props>(), {
totalInput: 6,
default: '',
})
const emit = defineEmits<Emit>()
const digits = ref<string[]>([])
const refOtpComp = ref<HTMLInputElement | null>(null)
digits.value = props.default.split('')
const defaultStyle = {
style: 'max-width: 54px; text-align: center;',
}
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleKeyDown = (event: KeyboardEvent, index: number) => {
if (event.code !== 'Tab' && event.code !== 'ArrowRight' && event.code !== 'ArrowLeft')
event.preventDefault()
if (event.code === 'Backspace') {
digits.value[index - 1] = ''
if (refOtpComp.value !== null && index > 1) {
const inputEl = refOtpComp.value.children[index - 2].querySelector('input')
if (inputEl)
inputEl.focus()
}
}
const numberRegExp = /^([0-9])$/
if (numberRegExp.test(event.key)) {
digits.value[index - 1] = event.key
if (refOtpComp.value !== null && index !== 0 && index < refOtpComp.value.children.length) {
const inputEl = refOtpComp.value.children[index].querySelector('input')
if (inputEl)
inputEl.focus()
}
}
emit('updateOtp', digits.value.join(''))
}
</script>
<template>
<div>
<h6 class="text-base font-weight-bold mb-3">
Type your 6 digit security code
</h6>
<div
ref="refOtpComp"
class="d-flex align-center gap-4"
>
<VTextField
v-for="i in props.totalInput"
:key="i"
:model-value="digits[i - 1]"
v-bind="defaultStyle"
maxlength="1"
@keydown="handleKeyDown($event, i)"
/>
</div>
</div>
</template>
<style lang="scss">
.v-field__field {
input {
padding: 0.5rem;
font-size: 1.25rem;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,230 @@
<script setup lang="ts">
import pricingIllustration1 from '@images/misc/pricing-illustration-1.png'
import pricingIllustration2 from '@images/misc/pricing-illustration-2.png'
import pricingIllustration3 from '@images/misc/pricing-illustration-3.png'
interface Pricing {
title?: string
xs?: number | string
sm?: number | string
md?: string | number
lg?: string | number
xl?: string | number
}
const props = defineProps<Pricing>()
const annualMonthlyPlanPriceToggler = ref(true)
const pricingPlans = [
{
name: 'Basic',
tagLine: 'A simple start for everyone',
logo: pricingIllustration1,
monthlyPrice: 0,
yearlyPrice: 0,
isPopular: false,
current: true,
features: [
'100 responses a month',
'Unlimited forms and surveys',
'Unlimited fields',
'Basic form creation tools',
'Up to 2 subdomains',
],
},
{
name: 'Standard',
tagLine: 'For small to medium businesses',
logo: pricingIllustration2,
monthlyPrice: 49,
yearlyPrice: 480,
isPopular: true,
current: false,
features: [
'Unlimited responses',
'Unlimited forms and surveys',
'Instagram profile page',
'Google Docs integration',
'Custom “Thank you” page',
],
},
{
name: 'Enterprise',
tagLine: 'Solution for big organizations',
logo: pricingIllustration3,
monthlyPrice: 99,
yearlyPrice: 960,
isPopular: false,
current: false,
features: [
'PayPal payments',
'Logic Jumps',
'File upload with 5GB storage',
'Custom domain support',
'Stripe integration',
],
},
]
</script>
<template>
<!-- 👉 Title and subtitle -->
<div class="text-center">
<h4 class="pricing-title text-h4 mb-4">
{{ props.title ? props.title : 'Pricing Plans' }}
</h4>
<p class="text-sm mb-0">
All plans include 40+ advanced tools and features to boost your product.
</p>
<p class="text-sm mb-0">
Choose the best plan to fit your needs.
</p>
</div>
<!-- 👉 Annual and monthly price toggler -->
<div class="d-flex align-center justify-center mx-auto py-10">
<VLabel
for="pricing-plan-toggle"
class="me-2"
>
Monthly
</VLabel>
<div class="position-relative">
<div class="position-absolute price-chip d-none d-sm-flex">
<VIcon
icon="mdi-arrow-down-left"
size="24"
class="text-disabled flip-in-rtl mt-1 me-1"
/>
<VChip
text="Save up to 10%"
size="x-small"
color="primary"
/>
</div>
<VSwitch
id="pricing-plan-toggle"
v-model="annualMonthlyPlanPriceToggler"
label="Annual"
/>
</div>
</div>
<!-- SECTION pricing plans -->
<VRow>
<VCol
v-for="plan in pricingPlans"
:key="plan.logo"
v-bind="props"
cols="12"
>
<!-- 👉 Card -->
<VCard
flat
border
:class="plan.isPopular ? 'border-primary border-opacity-100' : ''"
>
<VCardText
style="height: 4.125rem;"
class="text-end"
>
<!-- 👉 Popular -->
<VChip
v-show="plan.isPopular"
color="primary"
size="small"
class="font-weight-semibold"
>
Popular
</VChip>
</VCardText>
<!-- 👉 Plan logo -->
<VCardText class="text-center">
<VImg
:height="100"
:src="plan.logo"
class="mx-auto mb-5"
/>
<!-- 👉 Plan name -->
<h5 class="text-h5 mb-2">
{{ plan.name }}
</h5>
<p class="mb-0">
{{ plan.tagLine }}
</p>
</VCardText>
<!-- 👉 Plan price -->
<VCardText class="position-relative text-center">
<div class="d-flex justify-center align-center">
<sup class="text-sm font-weight-medium me-1">$</sup>
<h1 class="text-5xl font-weight-medium text-primary">
{{ annualMonthlyPlanPriceToggler ? Math.floor(Number(plan.yearlyPrice) / 12) : plan.monthlyPrice }}
</h1>
<sub class="text-sm font-weight-medium ms-1 mt-4">/month</sub>
</div>
<!-- 👉 Annual Price -->
<span
v-show="annualMonthlyPlanPriceToggler"
class="position-absolute text-caption font-weight-medium"
style="inset-inline: 0;"
>
{{ plan.yearlyPrice === 0 ? 'free' : `USD ${plan.yearlyPrice}/Year` }}
</span>
</VCardText>
<!-- 👉 Plan features -->
<VCardText class="mt-2">
<VList class="card-list mb-5">
<VListItem
v-for="feature in plan.features"
:key="feature"
>
<template #prepend>
<VIcon
:size="14"
icon="mdi-circle-outline"
class="me-3"
/>
</template>
<VListItemTitle class="text-body-2">
{{ feature }}
</VListItemTitle>
</VListItem>
</VList>
<VBtn
block
:color="plan.current ? 'success' : 'primary'"
:variant="plan.isPopular ? 'elevated' : 'tonal'"
>
{{ plan.yearlyPrice === 0 ? 'Your Current Plan' : 'Upgrade' }}
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- !SECTION -->
</template>
<style lang="scss" scoped>
.price-chip {
display: flex;
inset-block-start: -1.5rem;
inset-inline-end: -6.5rem;
// inset-inline-start: 2rem;
}
.card-list {
--v-card-list-gap: 0.75rem;
}
</style>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import AppSearchHeaderBgDark from '@images/pages/app-search-header-bg-dark.png'
import AppSearchHeaderBgLight from '@images/pages/app-search-header-bg-light.png'
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
interface Props {
title?: string
subtitle?: string
customClass?: string
}
const props = defineProps<Props>()
defineOptions({
inheritAttrs: false,
})
const themeBackgroundImg = useGenerateImageVariant(AppSearchHeaderBgLight, AppSearchHeaderBgDark)
</script>
<template>
<!-- 👉 Search Banner -->
<VCard
flat
class="text-center search-header"
:class="props.customClass"
:style="`background: url(${themeBackgroundImg});`"
>
<VCardText>
<h5 class="text-h5 font-weight-semibold text-primary mb-2">
{{ props.title }}
</h5>
<p>{{ props.subtitle }}</p>
<!-- 👉 Search Input -->
<VTextField
v-bind="$attrs"
placeholder="Ask a question.."
class="search-header-input mx-auto my-6"
>
<template #prepend-inner>
<VIcon
icon="mdi-magnify"
size="23"
/>
</template>
</VTextField>
</VCardText>
</VCard>
</template>
<style lang="scss">
.search-header {
padding: 4rem !important;
background-size: cover !important;
}
// search input
.search-header-input {
border-radius: 0.5rem !important;
background-color: rgb(var(--v-theme-surface));
max-inline-size: 32.125rem;
.v-field__prepend-inner {
i {
inset-block-start: 3px !important;
}
}
}
@media (max-width: 37.5rem) {
.search-header {
padding: 1.5rem !important;
}
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts" setup>
const vm = getCurrentInstance()
const buyNowUrl = ref(vm?.appContext.config.globalProperties.buyNowUrl || 'https://1.envato.market/materialize_admin')
watch(buyNowUrl, val => {
if (vm)
vm.appContext.config.globalProperties.buyNowUrl = val
})
</script>
<template>
<VBtn
color="error"
class="product-buy-now"
:href="buyNowUrl"
target="_blank"
rel="noopener noreferrer"
>
Buy Now
</VBtn>
</template>
<style lang="scss" scoped>
.product-buy-now {
position: fixed;
// To keep buy now button on top of v-layout. E.g. Email app
z-index: 999;
inset-block-end: 5%;
inset-inline-end: 79px;
.v-application &.v-btn.v-btn--elevated {
box-shadow: 0 1px 20px 1px rgb(var(--v-theme-error)) !important;
&:hover {
box-shadow: none !important;
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<script setup lang="ts">
interface Details {
number: string
name: string
expiry: string
cvv: string
isPrimary: boolean
type: string
}
interface Emit {
(e: 'submit', value: Details): void
(e: 'update:isDialogVisible', value: boolean): void
}
interface Props {
cardDetails?: Details
isDialogVisible: boolean
}
const props = withDefaults(defineProps<Props>(), {
cardDetails: () => ({
number: '',
name: '',
expiry: '',
cvv: '',
isPrimary: false,
type: '',
}),
})
const emit = defineEmits<Emit>()
const cardDetails = ref<Details>(structuredClone(toRaw(props.cardDetails)))
watch(props, () => {
cardDetails.value = structuredClone(toRaw(props.cardDetails))
})
const formSubmit = () => {
emit('submit', cardDetails.value)
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 650 "
:model-value="props.isDialogVisible"
@update:model-value="val => $emit('update:isDialogVisible', val)"
>
<VCard class="pa-5 pa-sm-8">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="$emit('update:isDialogVisible', false)"
/>
<!-- 👉 Title -->
<VCardItem class="text-center">
<VCardTitle class="text-2xl mb-3">
{{ props.cardDetails.name ? 'Edit Card' : 'Add New New Card' }}
</VCardTitle>
<VCardSubtitle>
{{ props.cardDetails.name ? 'Edit your saved card details' : 'Add your saved card details' }}
</VCardSubtitle>
</VCardItem>
<VCardText class="mt-6">
<VForm @submit.prevent="() => {}">
<VRow>
<!-- 👉 Card Number -->
<VCol cols="12">
<VTextField
v-model="cardDetails.number"
label="Card Number"
type="number"
/>
</VCol>
<!-- 👉 Card Name -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="cardDetails.name"
label="Name"
/>
</VCol>
<!-- 👉 Card Expiry -->
<VCol
cols="6"
md="3"
>
<VTextField
v-model="cardDetails.expiry"
label="Expiry Date"
/>
</VCol>
<!-- 👉 Card CVV -->
<VCol
cols="6"
md="3"
>
<VTextField
v-model="cardDetails.cvv"
type="password"
label="CVV Code"
/>
</VCol>
<!-- 👉 Card Primary Set -->
<VCol cols="12">
<VSwitch
v-model="cardDetails.isPrimary"
label="Save Card for future billing?"
/>
</VCol>
<!-- 👉 Card actions -->
<VCol
cols="12"
class="text-center"
>
<VBtn
class="me-4"
type="submit"
@click="formSubmit"
>
Submit
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="$emit('update:isDialogVisible', false)"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { kFormatter } from '@core/utils/formatters'
interface Props {
title: string
color?: string
icon: string
stats: number
change: number
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
size="40"
rounded
:color="props.color"
variant="tonal"
class="me-4"
>
<VIcon
:icon="props.icon"
size="24"
/>
</VAvatar>
<div class="d-flex flex-column">
<div class="d-flex align-center flex-wrap">
<h6 class="text-h6">
{{ kFormatter(props.stats) }}
</h6>
<div
v-if="props.change"
:class="`${isPositive ? 'text-success' : 'text-error'}`"
>
<VIcon
size="24"
:icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'"
/>
<span class="text-caption">{{ Math.abs(props.change) }}%</span>
</div>
</div>
<span class="text-caption">{{ props.title }}</span>
</div>
</VCardText>
</VCard>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
interface Props {
title: string
color?: string
icon: string
stats: string
change: number
subtitle: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
v-if="props.icon"
rounded
size="38"
variant="tonal"
:color="props.color"
>
<VIcon
:icon="props.icon"
size="24"
/>
</VAvatar>
<VSpacer />
<div
v-if="props.change"
:class="isPositive ? 'text-success' : 'text-error'"
class="d-flex align-center text-sm font-weight-medium mt-n4"
>
<span>{{ isPositive ? `+${props.change}` : props.change }}%</span>
<VIcon :icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
</div>
</VCardText>
<VCardText>
<h6 class="text-h6 me-2 mt-2 mb-1">
{{ props.stats }}
</h6>
<p class="text-sm">
{{ props.title }}
</p>
<VChip
size="x-small"
class="font-weight-medium"
>
<span class="text-truncate">{{ props.subtitle }}</span>
</VChip>
</VCardText>
</VCard>
</template>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
interface Props {
title: string
subtitle: string
stats: string
change: number
image: string
imgWidth: number
color?: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText>
<h6 class="text-base font-weight-semibold mb-2">
{{ props.title }}
</h6>
<VChip
v-if="props.subtitle"
size="x-small"
:color="props.color"
class="mb-5"
>
{{ props.subtitle }}
</VChip>
<div class="d-flex align-center flex-wrap">
<h5 class="text-h5 me-2">
{{ props.stats }}
</h5>
<span
class="text-caption"
:class="isPositive ? 'text-success' : 'text-error'"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
</VCardText>
<VSpacer />
<div class="illustrator-img">
<VImg
v-if="props.image"
:src="props.image"
:width="props.imgWidth"
/>
</div>
</VCard>
</template>
<style lang="scss">
.illustrator-img {
position: absolute;
inset-block-end: 0;
inset-inline-end: 5%;
}
</style>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
interface Props {
confirmationMsg: string
isDialogVisible: boolean
}
interface Emit {
(e: 'update:isDialogVisible', value: boolean): void
(e: 'confirm', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const updateModelValue = (val: boolean) => {
emit('update:isDialogVisible', val)
}
const onConfirmation = () => {
emit('confirm', true)
updateModelValue(false)
}
const onCancel = () => {
emit('confirm', false)
emit('update:isDialogVisible', false)
}
</script>
<template>
<!-- 👉 Confirm Dialog -->
<VDialog
max-width="500"
:model-value="props.isDialogVisible"
@update:model-value="updateModelValue"
>
<VCard class="text-center px-10 py-6">
<VCardText>
<VBtn
icon
variant="outlined"
color="warning"
class="mb-4"
style="width: 88px; height: 88px; pointer-events: none;"
>
<span class="text-5xl">!</span>
</VBtn>
<h6 class="text-lg font-weight-medium">
{{ props.confirmationMsg }}
</h6>
</VCardText>
<VCardActions class="align-center justify-center gap-2">
<VBtn
variant="elevated"
@click="onConfirmation"
>
Confirm
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="onCancel"
>
Cancel
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@ -0,0 +1,75 @@
<script lang="ts" setup>
import type { CustomInputContent, GridColumn } from '@core/types'
interface Props {
selectedCheckbox: string[]
checkboxContent: CustomInputContent[]
gridColumn?: GridColumn
}
interface Emit {
(e: 'update:selectedCheckbox', value: string[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const selectedOption = ref(structuredClone(toRaw(props.selectedCheckbox)))
watch(selectedOption, () => {
emit('update:selectedCheckbox', selectedOption.value)
})
</script>
<template>
<VRow
v-if="props.checkboxContent && selectedOption"
v-model="selectedOption"
>
<VCol
v-for="item in props.checkboxContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-checkbox rounded cursor-pointer"
:class="selectedOption.includes(item.value) ? 'active' : ''"
>
<div>
<VCheckbox
v-model="selectedOption"
:value="item.value"
/>
</div>
<div class="flex-grow-1">
<div class="d-flex align-center mb-1">
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<VSpacer />
<span v-if="item.subtitle">{{ item.subtitle }}</span>
</div>
<p class="text-sm mb-0">
{{ item.desc }}
</p>
</div>
</VLabel>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.custom-checkbox {
display: flex;
align-items: flex-start;
gap: 0.375rem;
.v-checkbox {
margin-block-start: -0.375rem;
}
.cr-title {
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { CustomInputContent, GridColumn } from '@core/types'
interface Props {
selectedCheckbox: string[]
checkboxContent: CustomInputContent[]
gridColumn?: GridColumn
}
interface Emit {
(e: 'update:selectedCheckbox', value: string[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const selectedOption = ref(structuredClone(toRaw(props.selectedCheckbox)))
watch(selectedOption, () => {
emit('update:selectedCheckbox', selectedOption.value)
})
</script>
<template>
<VRow
v-if="props.checkboxContent && selectedOption"
v-model="selectedOption"
>
<VCol
v-for="item in props.checkboxContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-checkbox rounded cursor-pointer"
:class="selectedOption.includes(item.value) ? 'active' : ''"
>
<div class="d-flex flex-column align-center text-center gap-2">
<VIcon
size="32"
:icon="item.icon"
class="text-high-emphasis"
/>
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<p class="text-sm mb-0">
{{ item.desc }}
</p>
</div>
<div>
<VCheckbox
v-model="selectedOption"
:value="item.value"
/>
</div>
</VLabel>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.custom-checkbox {
display: flex;
flex-direction: column;
gap: 0.375rem;
.v-checkbox {
margin-block-end: -0.375rem;
}
.cr-title {
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,80 @@
<script lang="ts" setup>
import type { GridColumn } from '@core/types'
interface Props {
selectedCheckbox: string[]
checkboxContent: { bgImage: string; value: string }[]
gridColumn?: GridColumn
}
interface Emit {
(e: 'update:selectedCheckbox', value: string[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const selectedOption = ref(structuredClone(toRaw(props.selectedCheckbox)))
watch(selectedOption, () => {
emit('update:selectedCheckbox', selectedOption.value)
})
</script>
<template>
<VRow
v-if="props.checkboxContent && selectedOption"
v-model="selectedOption"
>
<VCol
v-for="item in props.checkboxContent"
:key="item.value"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-checkbox rounded cursor-pointer"
:class="selectedOption.includes(item.value) ? 'active' : ''"
>
<div>
<VCheckbox
v-model="selectedOption"
:value="item.value"
/>
</div>
<img
:src="item.bgImage"
alt="bg-img"
class="custom-checkbox-image"
>
</VLabel>
</VCol>
</VRow>
</template>
<style lang="scss" scoped>
.custom-checkbox {
position: relative;
padding: 0;
border-width: 2px;
transition: all 0.5s;
.custom-checkbox-image {
block-size: 100%;
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,74 @@
<script lang="ts" setup>
import type { CustomInputContent, GridColumn } from '@core/types'
interface Props {
selectedRadio: string
radioContent: CustomInputContent[]
gridColumn?: GridColumn
}
interface Emit {
(e: 'update:selectedRadio', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const selectedOption = ref(structuredClone(toRaw(props.selectedRadio)))
watch(selectedOption, () => {
emit('update:selectedRadio', selectedOption.value)
})
</script>
<template>
<VRadioGroup
v-if="props.radioContent"
v-model="selectedOption"
>
<VRow>
<VCol
v-for="item in props.radioContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-radio rounded cursor-pointer"
:class="selectedOption === item.value ? 'active' : ''"
>
<div>
<VRadio :value="item.value" />
</div>
<div class="flex-grow-1">
<div class="d-flex align-center mb-1">
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<VSpacer />
<span v-if="item.subtitle">{{ item.subtitle }}</span>
</div>
<p class="text-sm mb-0">
{{ item.desc }}
</p>
</div>
</VLabel>
</VCol>
</VRow>
</VRadioGroup>
</template>
<style lang="scss" scoped>
.custom-radio {
display: flex;
align-items: flex-start;
gap: 0.375rem;
.v-radio {
margin-block-start: -0.25rem;
}
.cr-title {
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,77 @@
<script lang="ts" setup>
import type { CustomInputContent, GridColumn } from '@core/types'
interface Props {
selectedRadio: string
radioContent: CustomInputContent[]
gridColumn?: GridColumn
}
interface Emit {
(e: 'update:selectedRadio', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const selectedOption = ref(structuredClone(toRaw(props.selectedRadio)))
watch(selectedOption, () => {
emit('update:selectedRadio', selectedOption.value)
})
</script>
<template>
<VRadioGroup
v-if="props.radioContent"
v-model="selectedOption"
>
<VRow>
<VCol
v-for="item in props.radioContent"
:key="item.title"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-radio rounded cursor-pointer"
:class="selectedOption === item.value ? 'active' : ''"
>
<div class="d-flex flex-column align-center text-center gap-2">
<VIcon
size="32"
:icon="item.icon"
class="text-high-emphasis"
/>
<h6 class="cr-title text-base">
{{ item.title }}
</h6>
<p class="text-sm mb-0">
{{ item.desc }}
</p>
</div>
<div>
<VRadio :value="item.value" />
</div>
</VLabel>
</VCol>
</VRow>
</VRadioGroup>
</template>
<style lang="scss" scoped>
.custom-radio {
display: flex;
flex-direction: column;
gap: 0.375rem;
.v-radio {
margin-block-end: -0.25rem;
}
.cr-title {
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,65 @@
<script lang="ts" setup>
import type { GridColumn } from '@core/types'
interface Props {
selectedRadio: string
radioContent: { bgImage: string; value: string }[]
gridColumn?: GridColumn
}
interface Emit {
(e: 'update:selectedRadio', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const selectedOption = ref(structuredClone(toRaw(props.selectedRadio)))
watch(selectedOption, () => {
emit('update:selectedRadio', selectedOption.value)
})
</script>
<template>
<VRadioGroup
v-if="props.radioContent"
v-model="selectedOption"
>
<VRow>
<VCol
v-for="item in props.radioContent"
:key="item.bgImage"
v-bind="gridColumn"
>
<VLabel
class="custom-input custom-radio rounded cursor-pointer"
:class="selectedOption === item.value ? 'active' : ''"
>
<img
:src="item.bgImage"
alt="bg-img"
class="custom-radio-image"
>
<VRadio :value="item.value" />
</VLabel>
</VCol>
</VRow>
</VRadioGroup>
</template>
<style lang="scss" scoped>
.custom-radio {
padding: 0;
border-width: 2px;
.custom-radio-image {
block-size: 100%;
inline-size: 100%;
}
.v-radio {
visibility: hidden;
}
}
</style>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
interface Props {
title: string
divider?: boolean
}
const props = withDefaults(defineProps<Props>(), {
divider: true,
})
</script>
<template>
<VDivider v-if="props.divider" />
<div class="customizer-section">
<p class="text-caption">
{{ props.title }}
</p>
<slot />
</div>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
interface Props {
icon?: string
iconSize?: string
}
const props = withDefaults(defineProps<Props>(), {
icon: 'mdi-close',
iconSize: '22',
})
</script>
<template>
<IconBtn class="v-dialog-close-btn">
<VIcon
:icon="props.icon"
:size="props.iconSize"
/>
</IconBtn>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
interface Emit {
(e: 'update:isDialogVisible', value: boolean): void
(e: 'submit', value: string): void
}
interface Props {
mobileNumber?: string
isDialogVisible: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const phoneNumber = ref(structuredClone(toRaw(props.mobileNumber)))
const formSubmit = () => {
if (phoneNumber.value) {
emit('submit', phoneNumber.value)
emit('update:isDialogVisible', false)
}
}
const resetPhoneNumber = () => {
phoneNumber.value = structuredClone(toRaw(props.mobileNumber))
emit('update:isDialogVisible', false)
}
</script>
<template>
<VDialog
max-width="600"
:model-value="props.isDialogVisible"
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
>
<VCard
title="Verify Your Mobile Number for SMS"
subtitle="Enter your mobile phone number with country code and we will send you a verification code."
class="pa-5 pa-sm-8"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="resetPhoneNumber"
/>
<VCardText>
<VForm @submit.prevent="() => {}">
<VTextField
v-model="phoneNumber"
name="mobile"
label="Phone Number"
class="mb-4"
/>
<div class="d-flex flex-wrap justify-end gap-3">
<VBtn
color="secondary"
variant="tonal"
@click="resetPhoneNumber"
>
Cancel
</VBtn>
<VBtn
type="submit"
@click="formSubmit"
>
continue
<VIcon
end
icon="mdi-chevron-right"
class="flip-in-rtl"
/>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
interface Props {
errorCode: string
errorTitle: string
errorDescription: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="text-center mb-4">
<!-- 👉 Title and subtitle -->
<h1 class="text-h1">
{{ props.errorCode }}
</h1>
<h5 class="text-h5 font-weight-semibold mb-3">
{{ props.errorTitle }}
</h5>
<p>{{ props.errorDescription }}</p>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import type { Anchor } from 'vuetify/lib/components'
import type { I18nLanguage } from '@layouts/types'
const props = withDefaults(defineProps<Props>(), {
location: 'bottom end',
})
defineEmits<{
(e: 'change', id: string): void
}>()
interface Props {
languages: I18nLanguage[]
location?: Anchor
}
const { locale } = useI18n({ useScope: 'global' })
watch(locale, val => {
document.documentElement.setAttribute('lang', val as string)
})
const currentLang = ref(['en'])
</script>
<template>
<IconBtn>
<VIcon icon="mdi-translate" />
<!-- Menu -->
<VMenu
activator="parent"
:location="props.location"
offset="14px"
>
<!-- List -->
<VList
v-model:selected="currentLang"
active-color="primary"
min-width="175px"
>
<!-- List item -->
<VListItem
v-for="lang in props.languages"
:key="lang.i18nLang"
:value="lang.i18nLang"
@click="locale = lang.i18nLang; $emit('change', lang.i18nLang)"
>
<!-- Language label -->
<VListItemTitle>{{ lang.label }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { ItemProps } from 'vuetify/composables/item'
interface Props {
menuList?: ItemProps[]
itemProps?: boolean
}
const props = defineProps<Props>()
</script>
<template>
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu
v-if="props.menuList"
activator="parent"
>
<VList
density="compact"
:items="props.menuList"
:item-props="props.itemProps"
/>
</VMenu>
</IconBtn>
</template>

View File

@ -0,0 +1,196 @@
<script lang="ts" setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import type { Anchor } from 'vuetify/lib/components'
import { avatarText } from '@core/utils/formatters'
import type { Notification } from '@layouts/types'
interface Props {
notifications: Notification[]
badgeProps?: unknown
location?: Anchor
}
interface Emit {
(e: 'read', value: number[]): void
(e: 'unread', value: number[]): void
(e: 'remove', value: number): void
(e: 'click:notification', value: Notification): void
}
const props = withDefaults(defineProps<Props>(), {
location: 'bottom end',
badgeProps: undefined,
})
const emit = defineEmits<Emit>()
const isAllMarkRead = computed(() => {
return props.notifications.some(item => item.isRead === true)
})
const markAllReadOrUnread = () => {
const allNotificationsIds = props.notifications.map(item => item.id)
if (isAllMarkRead.value)
emit('unread', allNotificationsIds)
else
emit('read', allNotificationsIds)
}
</script>
<template>
<VBadge
:model-value="!!props.badgeProps"
v-bind="props.badgeProps"
>
<IconBtn>
<VBadge
dot
:model-value="!!props.notifications.length"
color="error"
bordered
offset-x="1"
offset-y="1"
>
<VIcon icon="mdi-bell-outline" />
</VBadge>
<VMenu
activator="parent"
width="380px"
:location="props.location"
offset="14px"
:close-on-content-click="false"
>
<VCard class="d-flex flex-column">
<!-- 👉 Header -->
<VCardItem class="notification-section">
<VCardTitle class="text-lg">
Notifications
</VCardTitle>
<template #append>
<IconBtn
v-show="props.notifications.length"
@click="markAllReadOrUnread"
>
<VIcon :icon="isAllMarkRead ? 'mdi-email-open-outline' : 'mdi-email-outline' " />
<VTooltip
activator="parent"
location="start"
>
{{ isAllMarkRead ? 'Mark all as read' : 'Mark all as unread' }}
</VTooltip>
</IconBtn>
</template>
</VCardItem>
<VDivider />
<!-- 👉 Notifications list -->
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VList class="py-0">
<template
v-for="notification in props.notifications"
:key="notification.title"
>
<VListItem
link
lines="one"
min-height="66px"
class="list-item-hover-class"
@click="$emit('click:notification', notification)"
>
<!-- Slot: Prepend -->
<!-- Handles Avatar: Image, Icon, Text -->
<template #prepend>
<VListItemAction start>
<VAvatar
:color="notification.color || 'primary'"
:image="notification.img || undefined"
:icon="notification.icon || undefined"
size="40"
variant="tonal"
>
<span v-if="notification.text">{{ avatarText(notification.text) }}</span>
</VAvatar>
</VListItemAction>
</template>
<VListItemTitle>{{ notification.title }}</VListItemTitle>
<VListItemSubtitle>{{ notification.subtitle }}</VListItemSubtitle>
<span class="text-xs text-disabled">{{ notification.time }}</span>
<!-- Slot: Append -->
<template #append>
<div class="d-flex flex-column align-center gap-4">
<VBadge
dot
:color="notification.isRead ? 'primary' : '#a8aaae'"
:class="`${!notification.isRead ? 'visible-in-hover' : ''} ms-1`"
@click.stop="$emit(notification.isRead ? 'unread' : 'read', [notification.id])"
/>
<div style=" width: 28px;height: 28px;">
<IconBtn
size="x-small"
class="visible-in-hover"
@click="$emit('remove', notification.id)"
>
<VIcon
size="20"
icon="mdi-close"
/>
</IconBtn>
</div>
</div>
</template>
</VListItem>
<VDivider />
</template>
<VListItem
v-show="!props.notifications.length"
class="text-center text-medium-emphasis"
>
<VListItemTitle>No Notification Found!</VListItemTitle>
</VListItem>
</VList>
</PerfectScrollbar>
<!-- 👉 Footer -->
<VCardActions
v-show="props.notifications.length"
class="notification-footer"
>
<VBtn block>
VIEW ALL NOTIFICATIONS
</VBtn>
</VCardActions>
</VCard>
</VMenu>
</IconBtn>
</VBadge>
</template>
<style lang="scss">
.notification-section {
padding: 14px !important;
}
.notification-footer {
padding: 6px !important;
}
.list-item-hover-class {
.visible-in-hover {
display: none;
}
&:hover {
.visible-in-hover {
display: block;
}
}
}
</style>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
interface Props {
isDialogVisible: boolean
}
interface Emit {
(e: 'update:isDialogVisible', val: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const dialogVisibleUpdate = (val: boolean) => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
class="v-dialog-xl"
@update:model-value="dialogVisibleUpdate"
>
<VCard class="pricing-dialog pa-5 pa-sm-8">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="emit('update:isDialogVisible', false)"
/>
<VCardText>
<AppPricing
title="Subscription Plan"
lg="4"
/>
</VCardText>
<VCardText class="text-center">
<p class="text-sm">
Still Not Convinced? Start with a 14-day FREE trial!
</p>
<VBtn>Start Your Trial</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.pricing-dialog {
.pricing-title {
font-size: 1.5rem;
}
}
</style>

View File

@ -0,0 +1,185 @@
<script setup lang="ts">
interface Props {
isDialogVisible: boolean
}
interface Emit {
(e: 'update:isDialogVisible', val: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const dialogVisibleUpdate = (val: boolean) => {
emit('update:isDialogVisible', val)
}
const referAndEarnSteps = [
{
icon: 'mdi-message-outline',
title: 'Send Invitation 👍🏻',
subtitle: 'Send your referral link to your friend',
},
{
icon: 'mdi-clipboard-outline',
title: 'Registration 😎',
subtitle: 'Let them register to our services',
},
{
icon: 'mdi-medal-outline',
title: 'Free Trial 🎉',
subtitle: 'Your friend will get 30 days free trial',
},
]
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
max-width="900"
@update:model-value="dialogVisibleUpdate"
>
<VCard class="refer-and-earn-dialog">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="emit('update:isDialogVisible', false)"
/>
<VCardText class="pa-5 pa-sm-10">
<h5 class="text-h5 text-center mb-3">
Refer & Earn
</h5>
<p class="text-sm-body-1 text-center">
Invite your friend to vuexy, if they sign up, you and your friend will get 30 days free trial
</p>
<VRow class="text-center mt-6">
<VCol
v-for="step in referAndEarnSteps"
:key="step.title"
cols="12"
sm="4"
>
<VAvatar
variant="tonal"
size="100"
color="primary"
>
<VIcon
size="40"
:icon="step.icon"
/>
</VAvatar>
<h6 class="text-base mt-2 mb-3">
{{ step.title }}
</h6>
<span class="text-sm">{{ step.subtitle }}</span>
</VCol>
</VRow>
</VCardText>
<VDivider />
<VCardText class="pa-5 pa-sm-10">
<h6 class="text-h6 mb-4">
Invite your friends
</h6>
<p class="mb-3 text-sm">
Enter your friend's email address and invite them to join Materio 😍
</p>
<VForm
class="d-flex align-center gap-4"
@submit.prevent="() => {}"
>
<VTextField
density="compact"
placeholder="johnDoe@gmail.com"
/>
<VBtn type="submit">
Submit
</VBtn>
</VForm>
<h6 class="text-h6 mb-4 mt-8">
Share the referral link
</h6>
<p class="mb-2 text-sm">
You can also copy and send it or share it on your social media. 🚀
</p>
<VForm
class="d-flex align-center flex-wrap gap-3"
@submit.prevent="() => {}"
>
<VTextField
density="compact"
placeholder="http://referral.link"
class="refer-link-input me-1"
>
<template #append-inner>
<VBtn variant="text">
COPY LINK
</VBtn>
</template>
</VTextField>
<div class="d-flex gap-3">
<VBtn
icon
class="rounded"
color="#3B5998"
size="40"
>
<VIcon
color="white"
icon="mdi-facebook"
/>
</VBtn>
<VBtn
icon
class="rounded"
color="#55ACEE"
size="40"
>
<VIcon
color="white"
icon="mdi-twitter"
/>
</VBtn>
<VBtn
icon
class="rounded"
color="#007BB6"
size="40"
>
<VIcon
color="white"
icon="mdi-linkedin"
/>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.refer-link-input {
.v-field--appended {
padding-inline-end: 0;
}
.v-field__append-inner {
padding-block-start: 0.125rem;
}
}
</style>

View File

@ -0,0 +1,203 @@
<script setup lang="ts">
import avatar1 from '@images/avatars/avatar-1.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar3 from '@images/avatars/avatar-3.png'
import avatar4 from '@images/avatars/avatar-4.png'
import avatar5 from '@images/avatars/avatar-5.png'
import avatar6 from '@images/avatars/avatar-6.png'
import avatar7 from '@images/avatars/avatar-7.png'
import avatar8 from '@images/avatars/avatar-8.png'
interface Props {
isDialogVisible: boolean
}
interface Emit {
(e: 'update:isDialogVisible', val: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const dialogVisibleUpdate = (val: boolean) => {
emit('update:isDialogVisible', val)
}
type Permission = 'Owner' | 'Can Edit' | 'Can Comment' | 'Can View'
interface Member {
avatar: string
name: string
email: string
permission: Permission
}
const membersList: Member[] = [
{
avatar: avatar1,
name: 'Lester Palmer',
email: 'jerrod98@gmail.com',
permission: 'Can Edit',
},
{
avatar: avatar2,
name: 'Mattie Blair',
email: 'prudence.boehm@yahoo.com',
permission: 'Owner',
},
{
avatar: avatar3,
name: 'Marvin Wheeler',
email: 'rumet@jujpejah.net',
permission: 'Can Comment',
},
{
avatar: avatar4,
name: 'Nannie Ford',
email: 'negza@nuv.io',
permission: 'Can View',
},
{
avatar: avatar5,
name: 'Julian Murphy',
email: 'lunebame@umdomgu.net',
permission: 'Can Edit',
},
{
avatar: avatar6,
name: 'Sophie Gilbert',
email: 'ha@sugit.gov',
permission: 'Can View',
},
{
avatar: avatar7,
name: 'Chris Watkins',
email: 'zokap@mak.org',
permission: 'Can Comment',
},
{
avatar: avatar8,
name: 'Adelaide Nichols',
email: 'ujinomu@jigo.com',
permission: 'Can Edit',
},
]
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
max-width="900"
@update:model-value="dialogVisibleUpdate"
>
<VCard class="share-project-dialog pa-5 pa-sm-8">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
size="small"
@click="emit('update:isDialogVisible', false)"
/>
<VCardText>
<h5 class="text-h5 text-center mb-3">
Share Project
</h5>
<p class="text-sm text-center">
Share project with a team members
</p>
<p class="text-xl font-weight-medium mb-2">
Add Members
</p>
<VAutocomplete
:items="membersList"
item-title="name"
item-value="name"
placeholder="Add project members..."
density="compact"
>
<template #item="{ props: listItemProp, item }">
<VListItem v-bind="listItemProp">
<template #prepend>
<VAvatar
:image="item.raw.avatar"
size="30"
/>
</template>
</VListItem>
</template>
</VAutocomplete>
<h6 class="text-h6 mb-4 mt-8">
8 Members
</h6>
<VList class="card-list">
<VListItem
v-for="member in membersList"
:key="member.name"
>
<template #prepend>
<VAvatar :image="member.avatar" />
</template>
<VListItemTitle class="text-sm">
{{ member.name }}
</VListItemTitle>
<VListItemSubtitle>
{{ member.email }}
</VListItemSubtitle>
<template #append>
<VBtn
variant="text"
color="default"
:icon="$vuetify.display.xs"
>
<span class="d-none d-sm-block">{{ member.permission }}</span>
<VIcon icon="mdi-chevron-down" />
<VMenu activator="parent">
<VList :selected="[member.permission]">
<VListItem
v-for="(item, index) in ['Owner', 'Can Edit', 'Can Comment', 'Can View']"
:key="index"
:value="item"
>
<VListItemTitle>{{ item }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</template>
</VListItem>
</VList>
<div class="d-flex justify-space-between flex-wrap gap-3 mt-6">
<h6 class="text-sm font-weight-semibold d-flex align-start">
<VIcon
icon="mdi-account-group-outline"
class="me-2"
/>
<span>Public to Master - Pixinvent</span>
</h6>
<VBtn
variant="text"
prepend-icon="mdi-link"
>
Copy Project Link
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.share-project-dialog {
.card-list {
--v-card-list-gap: 1rem;
}
}
</style>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
interface Shortcut {
icon: string
title: string
subtitle: string
to: object | string
}
interface Props {
togglerIcon?: string
shortcuts: Shortcut[]
}
const props = withDefaults(defineProps<Props>(), {
togglerIcon: 'mdi-view-grid-plus-outline',
})
const router = useRouter()
</script>
<template>
<IconBtn>
<VIcon :icon="props.togglerIcon" />
<VMenu
activator="parent"
offset="14px"
location="bottom end"
>
<VCard
width="340"
max-height="560"
class="d-flex flex-column"
>
<VCardItem class="py-4">
<VCardTitle>Shortcuts</VCardTitle>
<template #append>
<IconBtn>
<VIcon icon="mdi-view-grid-plus-outline" />
</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-4"
:class="(index + 1) % 2 ? 'border-e' : ''"
@click="router.push(shortcut.to)"
>
<VAvatar
variant="tonal"
size="48"
>
<VIcon :icon="shortcut.icon" />
</VAvatar>
<h6 class="text-base font-weight-semibold mt-2 mb-0">
{{ shortcut.title }}
</h6>
<span class="text-sm">{{ shortcut.subtitle }}</span>
</VCol>
</VRow>
</PerfectScrollbar>
</VCard>
</VMenu>
</IconBtn>
</template>

View File

@ -0,0 +1,376 @@
<script setup lang="tsx">
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useTheme } from 'vuetify'
import { staticPrimaryColor } from '@/plugins/vuetify/theme'
import { useThemeConfig } from '@core/composable/useThemeConfig'
import { RouteTransitions, Skins } from '@core/enums'
import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layouts/enums'
import { themeConfig } from '@themeConfig'
// import { useTheme } from 'vuetify'
const isNavDrawerOpen = ref(false)
const {
theme,
skin,
appRouteTransition,
navbarType,
footerType,
isVerticalNavCollapsed,
isVerticalNavSemiDark,
appContentWidth,
appContentLayoutNav,
isAppRtl,
isNavbarBlurEnabled,
isLessThanOverlayNavBreakpoint,
} = useThemeConfig()
// 👉 Primary Color
const vuetifyTheme = useTheme()
// const vuetifyThemesName = Object.keys(vuetifyTheme.themes.value)
const initialThemeColors = JSON.parse(JSON.stringify(vuetifyTheme.current.value.colors))
const colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error']
// It will set primary color for current theme only
const setPrimaryColor = (color: string) => {
const currentThemeName = vuetifyTheme.name.value
vuetifyTheme.themes.value[currentThemeName].colors.primary = color
// We need to store this color value in localStorage so vuetify plugin can pick on next reload
localStorage.setItem(`${themeConfig.app.title}-${currentThemeName}ThemePrimaryColor`, color)
// Update initial loader color
localStorage.setItem(`${themeConfig.app.title}-initial-loader-color`, color)
}
/*
This will return static color for first indexed color
If we don't make first (primary) color as static then when another color is selected then we will have two theme colors with same hex codes and it will show two check marks
*/
const getBoxColor = (color: string, index: number) => index ? color : staticPrimaryColor
const { width: windowWidth } = useWindowSize()
const headerValues = computed(() => {
const entries = Object.entries(NavbarType)
if (appContentLayoutNav.value === AppContentLayoutNav.Horizontal)
return entries.filter(([_, val]) => val !== NavbarType.Hidden)
return entries
})
</script>
<template>
<template v-if="!isLessThanOverlayNavBreakpoint(windowWidth)">
<VBtn
icon
size="small"
class="app-customizer-toggler rounded-s rounded-0"
style="z-index: 1001;"
@click="isNavDrawerOpen = true"
>
<VIcon icon="mdi-cog" />
</VBtn>
<VNavigationDrawer
v-model="isNavDrawerOpen"
temporary
location="end"
width="400"
: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>
<span class="text-body-1">Customize & Preview in Real Time</span>
</div>
<IconBtn @click="isNavDrawerOpen = false">
<VIcon
icon="mdi-close"
size="20"
/>
</IconBtn>
</div>
<VDivider />
<PerfectScrollbar
tag="ul"
:options="{ wheelPropagation: false }"
>
<!-- SECTION Theming -->
<CustomizerSection
title="THEMING"
:divider="false"
>
<!-- 👉 Skin -->
<h6 class="text-base font-weight-regular">
Skins
</h6>
<VRadioGroup
v-model="skin"
inline
>
<VRadio
v-for="[key, val] in Object.entries(Skins)"
:key="key"
:label="key"
:value="val"
/>
</VRadioGroup>
<!-- 👉 Theme -->
<h6 class="mt-3 text-base font-weight-regular">
Theme
</h6>
<VRadioGroup
v-model="theme"
inline
>
<VRadio
v-for="themeOption in ['system', 'light', 'dark']"
:key="themeOption"
:label="themeOption"
:value="themeOption"
class="text-capitalize"
/>
</VRadioGroup>
<!-- 👉 Primary color -->
<h6 class="mt-3 text-base font-weight-regular">
Primary Color
</h6>
<div class="d-flex gap-x-4 mt-2">
<div
v-for="(color, index) in colors"
:key="color"
style="width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; transition: all 0.25s ease;"
:style="{ backgroundColor: getBoxColor(initialThemeColors[color], index) }"
class="cursor-pointer d-flex align-center justify-center"
:class="{ 'elevation-4': vuetifyTheme.current.value.colors.primary === getBoxColor(initialThemeColors[color], index) }"
@click="setPrimaryColor(getBoxColor(initialThemeColors[color], index))"
>
<VFadeTransition>
<VIcon
v-show="vuetifyTheme.current.value.colors.primary === (getBoxColor(initialThemeColors[color], index))"
icon="mdi-check"
color="white"
/>
</VFadeTransition>
</div>
</div>
</CustomizerSection>
<!-- !SECTION -->
<!-- SECTION LAYOUT -->
<CustomizerSection title="LAYOUT">
<!-- 👉 Content Width -->
<h6 class="text-base font-weight-regular">
Content width
</h6>
<VRadioGroup
v-model="appContentWidth"
inline
>
<VRadio
v-for="[key, val] in Object.entries(ContentWidth)"
:key="key"
:label="key"
:value="val"
/>
</VRadioGroup>
<!-- 👉 Navbar Type -->
<h6 class="mt-3 text-base font-weight-regular">
{{ appContentLayoutNav === AppContentLayoutNav.Vertical ? 'Navbar' : 'Header' }} Type
</h6>
<VRadioGroup
v-model="navbarType"
inline
>
<VRadio
v-for="[key, val] in headerValues"
:key="key"
:label="key"
:value="val"
/>
</VRadioGroup>
<!-- 👉 Footer Type -->
<h6 class="mt-3 text-base font-weight-regular">
Footer Type
</h6>
<VRadioGroup
v-model="footerType"
inline
>
<VRadio
v-for="[key, val] in Object.entries(FooterType)"
:key="key"
:label="key"
:value="val"
/>
</VRadioGroup>
<!-- 👉 Navbar blur -->
<div class="d-flex align-center justify-space-between">
<VLabel
for="customizer-navbar-blur"
class="text-high-emphasis"
>
Navbar Blur
</VLabel>
<div>
<VSwitch
id="customizer-navbar-blur"
v-model="isNavbarBlurEnabled"
class="ms-2"
/>
</div>
</div>
</CustomizerSection>
<!-- !SECTION -->
<!-- SECTION Menu -->
<CustomizerSection title="MENU">
<!-- 👉 Menu Type -->
<h6 class="text-base font-weight-regular">
Menu Type
</h6>
<VRadioGroup
v-model="appContentLayoutNav"
inline
>
<VRadio
v-for="[key, val] in Object.entries(AppContentLayoutNav)"
:key="key"
:label="key"
:value="val"
/>
</VRadioGroup>
<!-- 👉 Collapsed Menu -->
<div
v-if="appContentLayoutNav === AppContentLayoutNav.Vertical"
class="d-flex align-center justify-space-between"
>
<VLabel
for="customizer-menu-collapsed"
class="text-high-emphasis"
>
Collapsed Menu
</VLabel>
<div>
<VSwitch
id="customizer-menu-collapsed"
v-model="isVerticalNavCollapsed"
class="ms-2"
/>
</div>
</div>
<!-- 👉 Semi Dark Menu -->
<div
class="align-center justify-space-between"
:class="vuetifyTheme.global.name.value === 'light' && appContentLayoutNav === AppContentLayoutNav.Vertical ? 'd-flex' : 'd-none'"
>
<VLabel
for="customizer-menu-semi-dark"
class="text-high-emphasis"
>
Semi Dark Menu
</VLabel>
<div>
<VSwitch
id="customizer-menu-semi-dark"
v-model="isVerticalNavSemiDark"
class="ms-2"
/>
</div>
</div>
</CustomizerSection>
<!-- !SECTION -->
<!-- SECTION MISC -->
<CustomizerSection title="MISC">
<!-- 👉 RTL -->
<div class="d-flex align-center justify-space-between">
<VLabel
for="customizer-rtl"
class="text-high-emphasis"
>
RTL
</VLabel>
<div>
<VSwitch
id="customizer-rtl"
v-model="isAppRtl"
class="ms-2"
/>
</div>
</div>
<!-- 👉 Route Transition -->
<div class="mt-6">
<VRow>
<VCol
cols="5"
class="d-flex align-center"
>
<VLabel
for="route-transition"
class="text-high-emphasis"
>
Router Transition
</VLabel>
</VCol>
<VCol cols="7">
<VSelect
id="route-transition"
v-model="appRouteTransition"
:items="Object.entries(RouteTransitions).map(([key, value]) => ({ key, value }))"
item-title="key"
item-value="value"
single-line
/>
</VCol>
</VRow>
</div>
</CustomizerSection>
<!-- !SECTION -->
</PerfectScrollbar>
</VNavigationDrawer>
</template>
</template>
<style lang="scss">
.app-customizer {
.customizer-section {
padding: 1.25rem;
}
.customizer-heading {
padding-block: 0.875rem;
padding-inline: 1.25rem;
}
.v-navigation-drawer__content {
display: flex;
flex-direction: column;
}
}
.app-customizer-toggler {
position: fixed !important;
inset-block-start: 50%;
inset-inline-end: 0;
transform: translateY(-50%);
}
</style>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { useThemeConfig } from '@core/composable/useThemeConfig'
import type { ThemeSwitcherTheme } from '@layouts/types'
const props = defineProps<{
themes: ThemeSwitcherTheme[]
}>()
const { theme } = useThemeConfig()
const { state: currentThemeName, next: getNextThemeName, index: currentThemeIndex } = useCycleList(props.themes.map(t => t.name), { initialValue: theme.value })
const changeTheme = () => {
theme.value = getNextThemeName()
}
// Update icon if theme is changed from other sources
watch(theme, val => {
currentThemeName.value = val
})
</script>
<template>
<IconBtn @click="changeTheme">
<VIcon :icon="props.themes[currentThemeIndex].icon" />
<VTooltip
activator="parent"
open-delay="1000"
>
<span class="text-capitalize">{{ currentThemeName }}</span>
</VTooltip>
</IconBtn>
</template>

View File

@ -0,0 +1,158 @@
<script setup lang="ts">
import AddAuthenticatorAppDialog from '@core/components/AddAuthenticatorAppDialog.vue'
import EnableOneTimePasswordDialog from '@core/components/EnableOneTimePasswordDialog.vue'
interface Emit {
(e: 'update:isDialogVisible', value: boolean): void
}
interface Props {
isDialogVisible: boolean
smsCode?: string
authAppCode?: string
}
const props = withDefaults(defineProps<Props>(), {
isDialogVisible: false,
smsCode: '',
authAppCode: '',
})
const emit = defineEmits<Emit>()
const authMethods = [
{
icon: 'mdi-cog-outline',
title: 'Authenticator Apps',
subtitle: 'Get code from an app like Google Authenticator or Microsoft Authenticator.',
method: 'authApp',
},
{
icon: 'mdi-message-outline',
title: 'SMS',
subtitle: 'We will send a code via SMS if you need to use your backup login method.',
method: 'sms',
},
]
const selectedMethod = ref(['authApp'])
const isAuthAppDialogVisible = ref(false)
const isSmsDialogVisible = ref(false)
const openSelectedMethodDialog = () => {
if (selectedMethod.value[0] === 'authApp') {
isAuthAppDialogVisible.value = true
isSmsDialogVisible.value = false
emit('update:isDialogVisible', false)
}
if (selectedMethod.value[0] === 'sms') {
isAuthAppDialogVisible.value = false
isSmsDialogVisible.value = true
emit('update:isDialogVisible', false)
}
}
</script>
<template>
<VDialog
max-width="900"
:model-value="props.isDialogVisible"
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
>
<VCard class="pa-5 pa-sm-8">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="$emit('update:isDialogVisible', false)"
/>
<VCardItem class="text-center">
<VCardTitle class="text-h5 mb-3">
Select Authentication Method
</VCardTitle>
<VCardSubtitle>
You also need to select a method by which the proxy authenticates to the directory serve.
</VCardSubtitle>
</VCardItem>
<VCardText>
<VList
v-model:selected="selectedMethod"
mandatory
class="card-list auth-method-card"
:class="$vuetify.display.xs ? 'responsive-card' : ''"
>
<VListItem
v-for="item of authMethods"
:key="item.title"
rounded
border
:value="item.method"
class="py-5 px-6 my-6"
:class="selectedMethod[0] === item.method ? 'bg-light-primary border-primary' : 'bg-light-secondary border-secondary'"
style="/* stylelint-disable-next-line max-empty-lines */
--v-border-opacity: 1;"
>
<template #prepend>
<VIcon
:icon="item.icon"
size="38"
/>
</template>
<VListItemTitle class="text-xl font-weight-medium">
{{ item.title }}
</VListItemTitle>
<p class="text-base mb-0">
{{ item.subtitle }}
</p>
</VListItem>
</VList>
<div class="text-end">
<VBtn @click="openSelectedMethodDialog">
continue
<VIcon
end
icon="mdi-chevron-right"
class="flip-in-rtl"
/>
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
<AddAuthenticatorAppDialog
v-model:isDialogVisible="isAuthAppDialogVisible"
:auth-code="props.authAppCode"
/>
<EnableOneTimePasswordDialog
v-model:isDialogVisible="isSmsDialogVisible"
:mobile-number="props.smsCode"
/>
</template>
<style lang="scss">
.auth-method-card {
&.card-list .v-list-item {
padding-block: 20px !important;
padding-inline: 30px !important;
}
&.responsive-card {
.v-list-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
text-align: center;
.v-list-item__prepend {
svg {
margin: 0;
}
}
}
}
}
</style>

View File

@ -0,0 +1,226 @@
<script setup lang="ts">
interface UserData {
id: number | null
fullName: string
company: string
role: string
username: string
country: string
contact: string
email: string
currentPlan: string
status: string
avatar: string
taskDone: number | null
projectDone: number | null
taxId: string
language: string
}
interface Props {
userData?: UserData
isDialogVisible: boolean
}
interface Emit {
(e: 'submit', value: UserData): void
(e: 'update:isDialogVisible', val: boolean): void
}
const props = withDefaults(defineProps<Props>(), {
userData: () => ({
id: 0,
fullName: '',
company: '',
role: '',
username: '',
country: '',
contact: '',
email: '',
currentPlan: '',
status: '',
avatar: '',
taskDone: null,
projectDone: null,
taxId: '',
language: '',
}),
})
const emit = defineEmits<Emit>()
const userData = ref<UserData>(structuredClone(toRaw(props.userData)))
const isUseAsBillingAddress = ref(false)
watch(props, () => {
userData.value = structuredClone(toRaw(props.userData))
})
const onFormSubmit = () => {
emit('update:isDialogVisible', false)
emit('submit', userData.value)
}
const onFormReset = () => {
userData.value = structuredClone(toRaw(props.userData))
emit('update:isDialogVisible', false)
}
const dialogVisibleUpdate = (val: boolean) => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 800"
:model-value="props.isDialogVisible"
@update:model-value="dialogVisibleUpdate"
>
<VCard class="pa-sm-9 pa-5">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="onFormReset"
/>
<VCardItem class="text-center">
<VCardTitle class="text-h5 mb-2">
Edit User Information
</VCardTitle>
<VCardSubtitle>
Updating user details will receive a privacy audit.
</VCardSubtitle>
</VCardItem>
<VCardText>
<!-- 👉 Form -->
<VForm
class="mt-6"
@submit.prevent="onFormSubmit"
>
<VRow>
<!-- 👉 Full Name -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userData.fullName"
label="Full Name"
/>
</VCol>
<!-- 👉 Username -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userData.username"
label="Username"
/>
</VCol>
<!-- 👉 Billing Email -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userData.email"
label="Billing Email"
/>
</VCol>
<!-- 👉 Status -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userData.status"
label="Status"
/>
</VCol>
<!-- 👉 Tax Id -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userData.taxId"
label="Tax Id"
/>
</VCol>
<!-- 👉 Contact -->
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userData.contact"
label="Contact"
/>
</VCol>
<!-- 👉 Language -->
<VCol
cols="12"
md="6"
>
<VSelect
v-model="userData.language"
label="Language"
:items="['English', 'Spanish', 'Portuguese', 'Russian', 'French', 'German']"
/>
</VCol>
<!-- 👉 Country -->
<VCol
cols="12"
md="6"
>
<VSelect
v-model="userData.country"
label="Country"
:items="['USA', 'UK', 'Spain', 'Russia', 'France', 'Germany']"
/>
</VCol>
<!-- 👉 Switch -->
<VCol cols="12">
<VSwitch
v-model="isUseAsBillingAddress"
density="compact"
label="Use as a billing address?"
/>
</VCol>
<!-- 👉 Submit and Cancel -->
<VCol
cols="12"
class="d-flex flex-wrap justify-center gap-4"
>
<VBtn type="submit">
Submit
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="onFormReset"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@ -0,0 +1,89 @@
<script setup lang="ts">
interface Emit {
(e: 'update:isDialogVisible', val: boolean): void
}
interface Prop {
isDialogVisible: boolean
}
const props = defineProps<Prop>()
defineEmits<Emit>()
const selectedPlan = ref('standard')
const plansList = [
{ text: 'Basic - $0/month', value: 'basic' },
{ text: 'Standard - $99/month', value: 'standard' },
{ text: 'Enterprise - $499/month', value: 'enterprise' },
{ text: 'Company - $999/month', value: 'company' },
]
</script>
<template>
<!-- 👉 upgrade plan -->
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 650"
:model-value="props.isDialogVisible"
@update:model-value="val => $emit('update:isDialogVisible', val)"
>
<VCard class="py-8">
<!-- 👉 dialog close btn -->
<DialogCloseBtn
variant="text"
size="small"
@click="$emit('update:isDialogVisible', false)"
/>
<VCardItem class="text-center">
<VCardTitle class="text-h5 mb-5">
Upgrade Plan
</VCardTitle>
<VCardSubtitle>
Choose the best plan for user.
</VCardSubtitle>
</VCardItem>
<VCardText class="d-flex align-center flex-wrap flex-sm-nowrap px-15">
<VSelect
v-model="selectedPlan"
label="Choose Plan"
:items="plansList"
item-title="text"
item-value="value"
density="compact"
class="me-3"
/>
<VBtn class="mt-3 mt-sm-0">
Upgrade
</VBtn>
</VCardText>
<VDivider class="my-3" />
<VCardText class="px-15">
<p class="font-weight-medium mb-2">
User current plan is standard plan
</p>
<div class="d-flex justify-space-between flex-wrap">
<div class="d-flex align-center me-3">
<sup class="text-primary">$</sup>
<h3 class="text-h3 font-weight-semibold text-primary">
99
</h3>
<sub class="text-body-1 mt-3">/ month</sub>
</div>
<VBtn
color="error"
variant="tonal"
class="mt-3"
>
Cancel Subscription
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
</template>

View File

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

View File

@ -0,0 +1,29 @@
import type { Ref } from 'vue'
import { useDisplay } from 'vuetify'
export const useResponsiveLeftSidebar = (mobileBreakpoint: Ref<boolean> | undefined = 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,36 @@
import { VThemeProvider } from 'vuetify/components'
import { AppContentLayoutNav } from '@layouts/enums'
// TODO: Use `VThemeProvider` from dist instead of lib (Using this component from dist causes navbar to loose sticky positioning)
import { useThemeConfig } from '@core/composable/useThemeConfig'
export const useSkins = () => {
const { isVerticalNavSemiDark, skin, appContentLayoutNav } = useThemeConfig()
const layoutAttrs = computed(() => ({
verticalNavAttrs: {
wrapper: h(VThemeProvider, { tag: 'aside' }),
wrapperProps: {
withBackground: true,
theme: isVerticalNavSemiDark.value && appContentLayoutNav.value === AppContentLayoutNav.Vertical
? 'dark'
: undefined,
},
},
}))
const injectSkinClasses = () => {
const bodyClasses = document.body.classList
const genSkinClass = (_skin?: string) => `skin--${_skin}`
watch(skin, (val, oldVal) => {
bodyClasses.remove(genSkinClass(oldVal))
bodyClasses.add(genSkinClass(val))
}, { immediate: true })
}
return {
injectSkinClasses,
layoutAttrs,
}
}

View File

@ -0,0 +1,154 @@
import { useTheme } from 'vuetify'
import { useLayouts } from '@layouts'
import { themeConfig } from '@themeConfig'
export const isDarkPreferred = usePreferredDark()
export const useThemeConfig = () => {
const theme = computed({
get() {
return themeConfig.app.theme.value
},
set(value: typeof themeConfig.app.theme.value) {
themeConfig.app.theme.value = value
localStorage.setItem(`${themeConfig.app.title}-theme`, value.toString())
// We will not reset semi dark value when turning off dark mode because some user think it as bug
// if (value !== 'light')
// // eslint-disable-next-line @typescript-eslint/no-use-before-define
// isVerticalNavSemiDark.value = false
},
})
const isVerticalNavSemiDark = computed({
get() {
return themeConfig.verticalNav.isVerticalNavSemiDark.value
},
set(value: typeof themeConfig.verticalNav.isVerticalNavSemiDark.value) {
themeConfig.verticalNav.isVerticalNavSemiDark.value = value
localStorage.setItem(`${themeConfig.app.title}-isVerticalNavSemiDark`, value.toString())
},
})
const syncVuetifyThemeWithTheme = () => {
const vuetifyTheme = useTheme()
watch([theme, isDarkPreferred], ([val, _]) => {
vuetifyTheme.global.name.value = val === 'system'
? isDarkPreferred.value
? 'dark'
: 'light'
: val
})
}
/*
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(theme, val => {
// We are not using theme.current.colors.surface because watcher is independent and when this watcher is ran `theme` computed is not updated
localStorage.setItem(`${themeConfig.app.title}-initial-loader-bg`, vuetifyTheme.current.value.colors.surface)
localStorage.setItem(`${themeConfig.app.title}-initial-loader-color`, vuetifyTheme.current.value.colors.primary)
}, {
immediate: true,
})
}
const skin = computed({
get() {
return themeConfig.app.skin.value
},
set(value: typeof themeConfig.app.skin.value) {
themeConfig.app.skin.value = value
localStorage.setItem(`${themeConfig.app.title}-skin`, value)
},
})
const appRouteTransition = computed({
get() {
return themeConfig.app.routeTransition.value
},
set(value: typeof themeConfig.app.routeTransition.value) {
themeConfig.app.routeTransition.value = value
localStorage.setItem(`${themeConfig.app.title}-transition`, value)
},
})
// `@layouts` exports
const {
navbarType,
isNavbarBlurEnabled,
footerType,
isVerticalNavCollapsed,
appContentWidth,
appContentLayoutNav,
horizontalNavType,
isLessThanOverlayNavBreakpoint,
isAppRtl,
switchToVerticalNavOnLtOverlayNavBreakpoint,
} = useLayouts()
// const syncRtlWithRtlLang = (rtlLangs: string[], rtlDefaultLocale: string, ltrDefaultLocale: string) => {
// const { locale } = useI18n({ useScope: 'global' })
// watch(isAppRtl, val => {
// if (val)
// locale.value = rtlDefaultLocale
// else locale.value = ltrDefaultLocale
// })
// watch(locale, val => {
// if (rtlLangs.includes(val))
// isAppRtl.value = true
// else isAppRtl.value = false
// })
// watch(
// [isAppRtl, locale],
// ([valIsAppRTL, valLocale], [oldValIsAppRtl, oldValLocale]) => {
// const isRtlUpdated = valIsAppRTL !== oldValIsAppRtl
// if (isRtlUpdated) {
// if (valIsAppRTL)
// locale.value = rtlDefaultLocale
// else locale.value = ltrDefaultLocale
// }
// else {
// if (rtlLangs.includes(valLocale))
// isAppRtl.value = true
// else isAppRtl.value = false
// }
// },
// )
// }
return {
theme,
isVerticalNavSemiDark,
syncVuetifyThemeWithTheme,
syncInitialLoaderTheme,
skin,
appRouteTransition,
// @layouts exports
navbarType,
isNavbarBlurEnabled,
footerType,
isVerticalNavCollapsed,
appContentWidth,
appContentLayoutNav,
horizontalNavType,
isLessThanOverlayNavBreakpoint,
isAppRtl,
switchToVerticalNavOnLtOverlayNavBreakpoint,
// syncRtlWithRtlLang,
}
}

View File

@ -0,0 +1,14 @@
export const Skins = {
Default: 'default',
Bordered: 'bordered',
} as const
export const RouteTransitions = {
// 'Zoom Fade': 'app-transition-zoom-fade',
// 'Fade Bottom': 'app-transition-fade-bottom',
// 'Slide Fade': 'app-transition-slide-fade',
// 'Zoom out': 'app-transition-zoom-out',
Fade: 'app-transition-fade',
None: 'none',
} as const

View File

@ -0,0 +1,97 @@
import type { ThemeConfig, UserThemeConfig } from './types'
import { RouteTransitions, Skins } from '@core/enums'
import type { UserConfig as LayoutConfig } from '@layouts/types'
export const defineThemeConfig = (
userConfig: UserThemeConfig,
): { themeConfig: ThemeConfig; layoutConfig: LayoutConfig } => {
const localStorageTheme = localStorage.getItem(`${userConfig.app.title}-theme`)
const localStorageIsVerticalNavSemiDark = localStorage.getItem(`${userConfig.app.title}-isVerticalNavSemiDark`)
const localStorageSkin = (() => {
const storageValue = localStorage.getItem(`${userConfig.app.title}-skin`)
return Object.values(Skins).find(v => v === storageValue)
})()
const localStorageTransition = (() => {
const storageValue = localStorage.getItem(`${userConfig.app.title}-transition`)
return Object.values(RouteTransitions).find(v => v === storageValue)
})()
return {
themeConfig: {
app: {
title: userConfig.app.title,
logo: userConfig.app.logo,
contentWidth: ref(userConfig.app.contentWidth),
contentLayoutNav: ref(userConfig.app.contentLayoutNav),
overlayNavFromBreakpoint: userConfig.app.overlayNavFromBreakpoint,
enableI18n: userConfig.app.enableI18n,
theme: ref(localStorageTheme || userConfig.app.theme),
isRtl: ref(userConfig.app.isRtl),
skin: ref(localStorageSkin || userConfig.app.skin),
routeTransition: ref(localStorageTransition || userConfig.app.routeTransition),
iconRenderer: userConfig.app.iconRenderer,
},
navbar: {
type: ref(userConfig.navbar.type),
navbarBlur: ref(userConfig.navbar.navbarBlur),
},
footer: { type: ref(userConfig.footer.type) },
verticalNav: {
isVerticalNavCollapsed: ref(userConfig.verticalNav.isVerticalNavCollapsed),
defaultNavItemIconProps: userConfig.verticalNav.defaultNavItemIconProps,
isVerticalNavSemiDark: ref(localStorageIsVerticalNavSemiDark ? JSON.parse(localStorageIsVerticalNavSemiDark) : userConfig.verticalNav.isVerticalNavSemiDark),
},
horizontalNav: {
type: ref(userConfig.horizontalNav.type),
transition: userConfig.horizontalNav.transition,
},
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,
},
},
layoutConfig: {
app: {
title: userConfig.app.title,
logo: userConfig.app.logo,
contentWidth: userConfig.app.contentWidth,
contentLayoutNav: userConfig.app.contentLayoutNav,
overlayNavFromBreakpoint: userConfig.app.overlayNavFromBreakpoint,
enableI18n: userConfig.app.enableI18n,
isRtl: userConfig.app.isRtl,
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,
},
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,703 @@
import type { ThemeInstance } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
// 👉 Colors variables
const colorVariables = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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 },
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: { colors: themeDisabledTextColor },
},
},
xaxis: {
tickAmount: 10,
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
formatter: (val: string) => parseFloat(val).toFixed(1),
},
},
}
}
export const getLineChartSimpleConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
custom(data: any) {
return `<div class='bar-chart pa-2'>
<span>${data.series[data.seriesIndex][data.dataPointIndex]}%</span>
</div>`
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
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: ThemeInstance['themes']['value']['colors']) => {
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,
startingShape: 'rounded',
},
},
grid: {
borderColor: themeBorderColor,
xaxis: {
lines: { show: false },
},
padding: {
top: -10,
},
},
yaxis: {
labels: {
style: { colors: themeDisabledTextColor },
},
},
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 },
},
},
}
}
export const getCandlestickChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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 },
},
},
xaxis: {
type: 'datetime',
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
},
}
}
export const getRadialBarChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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,
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.colors['grey-100'],
},
dataLabels: {
name: {
fontSize: '2rem',
},
value: {
fontSize: '1rem',
color: themeSecondaryTextColor,
},
total: {
show: true,
fontWeight: 400,
label: 'Comments',
fontSize: '1.125rem',
color: themePrimaryTextColor,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
formatter(w: { globals: { seriesTotals: any[]; series: string | any[] } }) {
const totalValue
= w.globals.seriesTotals.reduce((a: number, b: number) => {
return a + b
}, 0) / w.globals.series.length
if (totalValue % 1 === 0)
return `${totalValue}%`
else
return `${totalValue.toFixed(2)}%`
},
},
},
},
},
grid: {
padding: {
top: -35,
bottom: -30,
},
},
}
}
export const getDonutChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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: string) => `${parseInt(val, 10)}%`,
},
legend: {
position: 'bottom',
markers: { offsetX: -3 },
labels: { colors: themeSecondaryTextColor },
itemMargin: {
vertical: 3,
horizontal: 10,
},
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
fontSize: '1.5rem',
},
value: {
fontSize: '1.5rem',
color: themeSecondaryTextColor,
formatter: (val: string) => `${parseInt(val, 10)}`,
},
total: {
show: true,
fontSize: '1.5rem',
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: '1rem',
},
value: {
fontSize: '1rem',
},
total: {
fontSize: '1rem',
},
},
},
},
},
},
},
],
}
}
export const getAreaChartSplineConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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',
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 },
},
},
xaxis: {
axisBorder: { show: false },
axisTicks: { color: themeBorderColor },
crosshairs: {
stroke: { color: themeBorderColor },
},
labels: {
style: { colors: themeDisabledTextColor },
},
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: ThemeInstance['themes']['value']['colors']) => {
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',
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 },
},
},
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 },
},
},
responsive: [
{
breakpoint: 600,
options: {
plotOptions: {
bar: {
columnWidth: '35%',
},
},
},
},
],
}
}
export const getHeatMapChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
const { themeSecondaryTextColor, themeDisabledTextColor } = colorVariables(themeColors)
return {
chart: {
parentHeightOffset: 0,
toolbar: { show: false },
},
dataLabels: { enabled: false },
stroke: {
colors: [themeColors.colors.surface],
},
legend: {
position: 'bottom',
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,
},
},
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
}
}
export const getRadarChartConfig = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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: {
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: {
colors: [
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
themeDisabledTextColor,
],
},
},
},
}
}

View File

@ -0,0 +1,373 @@
import type { ThemeInstance } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
// 👉 Colors variables
const colorVariables = (themeColors: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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: ThemeInstance['themes']['value']['colors']) => {
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'bar'>[]>,
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { Chart as ChartJS, Legend, LinearScale, PointElement, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'bubble'>[]>,
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { ArcElement, CategoryScale, Chart as ChartJS, Legend, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'doughnut'>[]>,
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { CategoryScale, Chart as ChartJS, Legend, LineElement, LinearScale, PointElement, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'line'>[]>,
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { ArcElement, Chart as ChartJS, Legend, RadialLinearScale, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'polarArea'>[]>,
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { Chart as ChartJS, Filler, Legend, LineElement, PointElement, RadialLinearScale, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'radar'>[]>,
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,58 @@
import type { PluginOptionsByType } from 'chart.js'
import { CategoryScale, Chart as ChartJS, Legend, LineElement, LinearScale, PointElement, Title, Tooltip } from 'chart.js'
import type { PropType } from 'vue'
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 as PropType<Partial<CSSStyleDeclaration>>,
default: () => ({}),
},
plugins: {
type: Array as PropType<PluginOptionsByType<'scatter'>[]>,
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,139 @@
@use "mixins";
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@layouts/styles/placeholders";
@use "@configured-variables" as variables;
// 👉 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.elevation(3);
}
}
> .v-avatar {
border: 2px solid rgb(var(--v-theme-surface));
}
}
// 👉 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 {
&: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
.custom-input {
padding: 1rem;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
opacity: 1;
transition: border-color 0.5s;
white-space: normal;
&:hover {
border-color: rgba(var(--v-border-color), 0.25);
}
&.active {
border-color: rgb(var(--v-theme-primary));
}
}
// 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.9375rem;
inset-inline-end: 0.9375rem;
}
.v-card {
@extend %style-scroll-bar;
}
}
@media (min-width: 576px) {
.v-dialog {
&.v-dialog-sm,
&.v-dialog-lg,
&.v-dialog-xl {
inline-size: 565px !important;
}
}
}
@media (min-width: 992px) {
.v-dialog {
&.v-dialog-lg,
&.v-dialog-xl {
inline-size: 865px !important;
}
}
}
@media (min-width: 1200px) {
.v-dialog.v-dialog-xl,
.v-dialog.v-dialog-xl .v-overlay__content > .v-card {
inline-size: 1165px !important;
}
}
// v-tab with pill support
.v-tabs.v-tabs-pill {
.v-tab.v-btn {
border-radius: 0.25rem !important;
transition: none;
.v-tab__slider {
visibility: hidden;
}
}
.v-slide-group__content {
transition: none;
}
}
// loop for all colors bg
@each $color-name in variables.$theme-colors-name {
.v-tabs.v-tabs-pill {
.v-slide-group-item--active.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;
}
}
}

View File

@ -0,0 +1,16 @@
@use "@configured-variables" as variables;
//
//* Perfect Scrollbar
//
.v-application.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 "vuetify/lib/styles/tools/elevation" as elevation;
@use "@/plugins/vuetify/@core/scss/base/placeholders" as *;
@use "@/plugins/vuetify/@core/scss/template/placeholders" as *;
.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 elevation.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 "@/plugins/vuetify/@core/scss/base/placeholders" as *;
@use "@/plugins/vuetify/@core/scss/template/placeholders" as *;
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "misc";
$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 out 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.elevation(3);
}
}
}
}
}
}

View File

@ -0,0 +1,8 @@
@use "@/plugins/vuetify/@core/scss/base/placeholders";
@use "@configured-variables" as variables;
.layout-navbar {
@if variables.$navbar-high-emphasis-text {
@extend %layout-navbar;
}
}

View File

@ -0,0 +1,189 @@
@use "@/plugins/vuetify/@core/scss/base/placeholders" as *;
@use "@/plugins/vuetify/@core/scss/template/placeholders" as *;
@use "@configured-variables" as variables;
@use "@layouts/styles/mixins" as layoutsMixins;
@use "@/plugins/vuetify/@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 {
opacity: var(--v-disabled-opacity);
pointer-events: none;
}
// 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);
}
// 👉 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;
}
}
> .popper-content {
// Add space between popper wrapper & 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: 1.375rem;
/*
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;
}
.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;
}
}
&.show-content > .popper-content {
@extend %horizontal-nav-popper-content-visible;
}
}
// !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)
.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,73 @@
// @use "@styles/variables/_vuetify.scss";
// 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;
}
}
@mixin bordered-skin($component, $border-property: "border", $important: false) {
#{$component} {
background-color: rgb(var(--v-theme-background));
box-shadow: none !important;
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
}
}
// Inspired from vuetify's active-states mixin
// focus => 0.12 & selected => 0.08
@mixin selected-states($selector) {
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected")} * var(--v-theme-overlay-multiplier));
// }
// &:hover
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "hover")} * var(--v-theme-overlay-multiplier));
// }
// &:focus-visible
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier));
// }
// @supports not selector(:focus-visible) {
// &:focus {
// #{$selector} {
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier));
// }
// }
// }
#{$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));
}
}
}
}

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,116 @@
@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;
}
.bg-var-theme-background {
background-color: rgba(var(--v-theme-background), var(--v-medium-emphasis-opacity)) !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;
}
}
// 👉 Typography
.font-weight-semibold {
font-weight: 600 !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);
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
inline-size: calc(var(--v-btn-height) + -10px);
&.v-btn--active {
color: #fff;
}
.v-btn__overlay {
opacity: 0;
}
}
}
@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;
}
}
}
}
}

View File

@ -0,0 +1,152 @@
@use "sass:map";
@use "sass:list";
// 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
// font size
$font-sizes: (
"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 line-height
$font-line-height: (
"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
);
@each $name, $size in $font-sizes {
.text-#{$name} {
font-size: $size;
line-height: map.get($font-line-height, $name);
}
}
// truncate utility class
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// gap utility class
$gap: (
"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
);
@each $name, $size in $gap {
.gap-#{$name} {
gap: $size;
}
.gap-x-#{$name} {
column-gap: $size;
}
.gap-y-#{$name} {
row-gap: $size;
}
}

View File

@ -0,0 +1,141 @@
/*
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: 1004,
$layout-overlay-z-index: 1003,
);
@use "@layouts/styles/variables" as *;
// 👉 Default layout
$navbar-high-emphasis-text: true !default;
// @forward "@layouts/styles/variables" with (
// $layout-vertical-nav-width: 350px !default,
// );
$css-vars: (
/*
- Skins
- CSS var
- Theme
*/
"default": (
"--v-theme-background": (
"light": (244 ,245, 250),
"dark": (40 ,36, 61),
),
"--v-theme-surface": (
"light": (255, 255, 255),
"dark": (49, 45, 75),
),
),
"bordered": (
"--v-theme-background": (
"light": (255 ,255, 255),
"dark": (49, 45, 75),
),
"--v-theme-surface": (
"light": (255, 255, 255),
"dark": (49, 45, 75),
),
),
) !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;
// 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((var(--vh, 1vh) * 100) - 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;

View File

@ -0,0 +1,245 @@
@use "@/plugins/vuetify/@core/scss/base/placeholders" as *;
@use "@/plugins/vuetify/@core/scss/template/placeholders" as *;
@use "@layouts/styles/mixins" as layoutsMixins;
@use "@configured-variables" as variables;
@use "@/plugins/vuetify/@core/scss/base/mixins" as mixins;
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
@use "vuetify/lib/styles/tools/elevation" as elevation;
.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 elevation.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));
}
}
}
}
.app-title {
margin-inline-start: variables.$vertical-nav-header-logo-title-spacing;
}
.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;
transform: translateX(-8px);
transition: opacity 0.15s ease-in-out;
will-change: opacity;
@include layoutsMixins.rtl {
transform: translateX(8px);
}
}
&.scrolled {
.vertical-nav-items-shadow {
opacity: 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;
}
}
// 👉 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
@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;
// 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;
}
}
}
}
}
// 👉 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);
}
}

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;
inset-inline-end: 0.0625rem;
}
.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;
}
.ps__thumb-x,
.ps__thumb-y {
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
}

View File

@ -0,0 +1 @@
@use "overrides";

View File

@ -0,0 +1,243 @@
@use "@/plugins/vuetify/@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: calc(var(--vh, 1vh) * 100);
}
// 👉 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));
}
.v-application,
.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;
}
}
}
// 👉 Theme
.v-theme--light {
--v-theme-background:
var(
--skin-theme-background,
#{utils.map-deep-get(variables.$css-vars, "default", "--v-theme-background", "light")}
) !important;
--v-theme-surface:
var(
--skin-theme-surface,
#{utils.map-deep-get(variables.$css-vars, "default", "--v-theme-surface", "light")}
) !important;
}
.v-theme--dark {
--v-theme-background:
var(
--skin-theme-background,
#{utils.map-deep-get(variables.$css-vars, "default", "--v-theme-background", "dark")}
) !important;
--v-theme-surface:
var(
--skin-theme-surface,
#{utils.map-deep-get(variables.$css-vars, "default", "--v-theme-surface", "dark")}
) !important;
}
// 👉 Button
@if variables.$vuetify-reduce-default-compact-button-icon-size {
.v-btn--density-compact.v-btn--size-default {
.v-btn__content > svg {
width: 22px;
height: 22px;
font-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-selection-control--density-comfortable {
&.v-checkbox-btn,
&.v-radio,
&.v-radio-btn {
.v-selection-control__wrapper {
margin-inline-start: -0.5625rem;
}
}
}
.v-selection-control--density-compact {
&.v-radio,
&.v-radio-btn,
&.v-checkbox-btn {
.v-selection-control__wrapper {
margin-inline-start: -0.3125rem;
}
}
}
.v-selection-control--density-default {
&.v-checkbox-btn,
&.v-radio,
&.v-radio-btn {
.v-selection-control__wrapper {
margin-inline-start: -0.6875rem;
}
}
}
.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: var(--v-high-emphasis-opacity);
}
}
// 👉 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;
}
}
}
// 👉 Table
.v-table {
color: rgba(var(--v-theme-on-background), 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;
&:not(.v-field-label--floating) {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
}
// 👉 Overlay
.v-overlay__scrim {
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity));
}

View File

@ -0,0 +1,55 @@
// 👉 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,
// States
$states: (
"hover": 0.08,
"focus": 0.1,
"selected": 0.12,
"activated": 0.1,
"pressed": 0.14,
"dragged": 0.1
) !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,
// 👉 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,
$button-icon-density: ("default": 2, "comfortable": 0, "compact": -1 ) !default,
// 👉 VTimeline
$timeline-dot-size: 34px !default,
);

View File

@ -0,0 +1,27 @@
@use "vuetify/lib/styles/tools/elevation" as elevation;
@use "@configured-variables" as variables;
@use "misc";
%default-layout-horizontal-nav-navbar-and-nav-container {
@include elevation.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,46 @@
@use "vuetify/lib/styles/tools/elevation" as elevation;
@use "@configured-variables" as variables;
@use "misc";
%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 elevation.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-end: 0;
inset-inline-start: 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,94 @@
@use "@layouts/styles/mixins" as layoutsMixins;
@use "vuetify/lib/styles/tools/elevation" as elevation;
@use "@configured-variables" as variables;
@use "@layouts/styles/placeholders";
// Horizontal nav item styles (including nested)
%horizontal-nav-item {
padding-block: 0.563rem;
padding-inline: 1rem;
}
// Top level horizontal nav item styles (`a` tag & group label)
%horizontal-nav-top-level-item {
border-radius: 0.4rem;
}
// 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-end: 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
*/
margin-inline-start: calc((variables.$horizontal-nav-items-icon-size - variables.$horizontal-nav-third-level-icon-size) / 2);
}
// Horizontal nav item title
%horizontal-nav-item-title {
margin-inline-end: 0.3rem;
white-space: nowrap;
}
// Popper content styles
%horizontal-nav-popper-content {
@include elevation.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 "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
// 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.elevation(3);
}
%nav-link {
a {
color: inherit;
}
}

View File

@ -0,0 +1,81 @@
@use "@/plugins/vuetify/@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-end: $vertical-nav-items-nested-icon-margin-inline + variables.$vertical-nav-items-icon-margin-inline-end;
margin-inline-start: $vertical-nav-items-nested-icon-margin-inline;
}
%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
%vertical-nav-section-title {
// Setting height will prevent jerking when text & icon is toggled
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,90 @@
@use "sass:map";
@use "@/plugins/vuetify/@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 .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;
}
} @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;
}
/*
Missing components:
- Stepper
*/
.v-theme--light {
--skin-theme-background:
#{utils.map-deep-get(
variables.$css-vars,
"bordered",
"--v-theme-background",
"light"
)};
--skin-theme-surface:
#{utils.map-deep-get(
variables.$css-vars,
"bordered",
"--v-theme-surface",
"light"
)};
}
.v-theme--dark {
--skin-theme-background:
#{utils.map-deep-get(
variables.$css-vars,
"bordered",
"--v-theme-background",
"dark"
)};
--skin-theme-surface:
#{utils.map-deep-get(
variables.$css-vars,
"bordered",
"--v-theme-surface",
"dark"
)};
}
// Vertical Nav
.layout-vertical-nav {
border-inline-end: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
}
}

View File

@ -0,0 +1 @@
@use "bordered";

View File

@ -0,0 +1,108 @@
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@configured-variables" as variables;
// 👉 Expansion panels
.v-expansion-panel-title,
.v-expansion-panel-title--active,
.v-expansion-panel-title:hover,
.v-expansion-panel-title:focus,
.v-expansion-panel-title:focus-visible,
.v-expansion-panel-title--active:focus,
.v-expansion-panel-title--active:hover {
.v-expansion-panel-title__overlay {
opacity: 0 !important;
}
}
.v-expansion-panels {
:first-child {
border-start-end-radius: variables.$expansion-panel-border-radius-custom;
border-start-start-radius: variables.$expansion-panel-border-radius-custom;
}
:last-child {
border-end-end-radius: variables.$expansion-panel-border-radius-custom;
border-end-start-radius: variables.$expansion-panel-border-radius-custom;
}
}
// 👉 Set Elevation when panel open
.v-expansion-panels:not(.v-expansion-panels--variant-accordion) {
.v-expansion-panel.v-expansion-panel--active {
.v-expansion-panel__shadow {
@include mixins_elevation.elevation(3);
}
}
}
// v-tab with pill support
.v-tabs:not(.v-tabs-pill) {
&.v-tabs--vertical {
border-inline-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
}
.v-tab__slider {
inset-inline-end: 0;
inset-inline-start: unset;
}
.v-tabs.v-tabs-pill:not(.v-tabs--stacked) {
&.v-tabs--density-default {
--v-tabs-height: 38px;
}
.v-tab.v-btn {
border-radius: 0.5rem !important;
}
}
// 👉 added box shadow
.v-timeline-item {
.v-timeline-divider__dot {
.v-timeline-divider__inner-dot {
box-shadow: 0 0 0 0.1875rem rgb(var(--v-theme-on-surface-variant));
@each $color-name in variables.$theme-colors-name {
&.bg-#{$color-name} {
box-shadow: 0 0 0 0.1875rem rgba(var(--v-theme-#{$color-name}), 0.12);
}
}
}
}
}
// 👉 Timeline Outlined style
.v-timeline-variant-outlined.v-timeline {
.v-timeline-divider__dot {
.v-timeline-divider__inner-dot {
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-on-surface-variant));
@each $color-name in variables.$theme-colors-name {
background-color: rgb(var(--v-theme-surface)) !important;
&.bg-#{$color-name} {
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-#{$color-name}));
}
}
}
}
}
// 👉 Slider
.v-slider-thumb {
.v-slider-thumb__label {
background-color: variables.$slider-thumb-label-color;
color: rgb(var(--v-theme-on-primary));
}
.v-slider-thumb__label::before {
color: variables.$slider-thumb-label-color;
}
}
// 👉 switch inactive thumb style
.v-switch__thumb {
color: variables.$switch-thumb-inactive-color;
}

View File

@ -0,0 +1,13 @@
.layout-horizontal-nav {
.nav-group {
.nav-group-arrow {
font-size: 1.5rem;
}
&:not(.active) {
.nav-group-arrow {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
}
}
}

View File

@ -0,0 +1,11 @@
@use "@configured-variables" as variables;
.bg-card {
background: rgb(var(--v-theme-surface)) !important;
}
.table-header-bg {
th {
background-color: rgb(var(--v-theme-grey-200));
}
}

View File

@ -0,0 +1,41 @@
@use "sass:string";
/*
This function is helpful when we have multi dimensional value
Assume we have padding variable `$nav-padding-horizontal: 10px;`
With above variable let's say we use it in some style:
```scss
.selector {
margin-left: $nav-padding-horizontal;
}
```
Now, problem is we can also have value as `$nav-padding-horizontal: 10px 15px;`
In this case above style will be invalid.
This function will extract the left most value from the variable value.
$nav-padding-horizontal: 10px; => 10px;
$nav-padding-horizontal: 10px 15px; => 10px;
This is safe:
```scss
.selector {
margin-left: get-first-value($nav-padding-horizontal);
}
```
*/
@function get-first-value($var) {
$start-at: string.index(#{$var}, " ");
@if $start-at {
@return string.slice(
#{$var},
0,
$start-at
);
} @else {
@return $var;
}
}

View File

@ -0,0 +1,71 @@
@use "sass:map";
@use "utils";
$vertical-nav-horizontal-padding-margin-custom: 1.75rem;
// We created this SCSS var to extract the start padding
// Docs: https://sass-lang.com/documentation/modules/string
// $vertical-nav-horizontal-padding => 0 8px;
// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-margin-custom) !default;
@forward "@/plugins/vuetify/@core/scss/base/variables" with(
$css-vars: (
/*
- Skins
- CSS var
- Theme
*/
"default": (
"--v-theme-background": (
"light": (247,247,249),
"dark": (40,42,66),
),
"--v-theme-surface": (
"light": (255, 255, 255),
"dark": (48,51,78),
),
),
"bordered": (
"--v-theme-background": (
"light": (255 ,255, 255),
"dark": (40,42,66),
),
"--v-theme-surface": (
"light": (255, 255, 255),
"dark": (40,42,66),
),
),
) !default,
);
// 👉 Vertical nav
// This is used by nav items & nav header
$vertical-nav-horizontal-spacing: 0.75rem !default;
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing 0.25rem !default;
$vertical-nav-horizontal-padding: 1rem 0.75rem !default;
// Section title margin bottom
$vertical-nav-section-title-mb: 0.75rem !default;
// Vertical nav header padding
$vertical-nav-header-padding: $vertical-nav-horizontal-padding !default;
$vertical-nav-items-nested-icon-size: 0.5rem !default;
// 👉 expansion panel
$expansion-panel-border-radius-custom: 8px !default;
// 👉 range-slider
$slider-thumb-label-color: rgb(117, 117, 117) !default;
// 👉 switch
$switch-thumb-inactive-color: rgb(250, 250, 250) !default;
// 👉 Horizontal nav
// Horizontal nav icons
$horizontal-nav-third-level-icon-size: 0.5rem !default;
$horizontal-nav-items-icon-margin-inline-end: 0.75rem !default;

View File

@ -0,0 +1,32 @@
@use "@configured-variables" as variables;
$divider-gap: 0.625rem;
.layout-vertical-nav {
.nav-section-title {
.title-text {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
column-gap: $divider-gap;
&::before {
flex: 0 1 calc(variables.$vertical-nav-horizontal-padding-start - $divider-gap);
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
content: "";
margin-inline-start: -#{variables.$vertical-nav-horizontal-padding-start};
}
}
}
// nested level nav icon
.nav-group {
.nav-group,
.nav-link :not(.router-link-active) {
.nav-item-icon {
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
}
}
}
}

View File

@ -0,0 +1,12 @@
@use "sass:map";
@forward "@/plugins/vuetify/@core/scss/base";
// Layout
@use "vertical-nav";
@use "horizontal-nav";
// Components
@use "components";
// Utilities
@use "utilities";

View File

@ -0,0 +1,95 @@
@use "@styles/variables/_vuetify.scss" as vuetify;
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
@use "@layouts/styles/mixins" as layoutsMixins;
.apexcharts-canvas {
&line[stroke="transparent"] {
display: "none";
}
.apexcharts-tooltip {
@include mixins_elevation.elevation(3);
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-surface));
.apexcharts-tooltip-title {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-surface));
font-weight: 600;
}
&.apexcharts-theme-light {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
&.apexcharts-theme-dark {
color: white;
}
.apexcharts-tooltip-series-group:first-of-type {
padding-block-end: 0;
}
}
.apexcharts-xaxistooltip {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-grey-50));
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
&::after {
border-block-end-color: rgb(var(--v-theme-grey-50));
}
&::before {
border-block-end-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
}
.apexcharts-yaxistooltip {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
background: rgb(var(--v-theme-grey-50));
&::after {
border-inline-start-color: rgb(var(--v-theme-grey-50));
}
&::before {
border-inline-start-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
}
.apexcharts-xaxistooltip-text,
.apexcharts-yaxistooltip-text {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
}
.apexcharts-yaxis .apexcharts-yaxis-texts-g .apexcharts-yaxis-label {
@include layoutsMixins.rtl {
text-anchor: start;
}
}
.apexcharts-text,
.apexcharts-tooltip-text,
.apexcharts-datalabel-label,
.apexcharts-datalabel,
.apexcharts-xaxistooltip-text,
.apexcharts-yaxistooltip-text,
.apexcharts-legend-text {
font-family: vuetify.$body-font-family !important;
}
.apexcharts-pie-label {
fill: white;
filter: none;
}
.apexcharts-marker {
box-shadow: none;
}
.apexcharts-legend-marker {
margin-inline-end: 0.3875rem !important;
}
}

View File

@ -0,0 +1,254 @@
@use "vuetify/lib/styles/tools/elevation" as elevation;
.fc {
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
--fc-border-color: rgba(var(--v-border-color), var(--v-border-opacity));
--fc-neutral-bg-color: rgb(var(--v-theme-background));
--fc-list-event-hover-bg-color: rgba(var(--v-theme-on-surface), 0.02);
--fc-page-bg-color: rgb(var(--v-theme-surface));
--fc-event-border-color: currentcolor;
a {
color: inherit;
}
.fc-timegrid-divider {
padding: 0;
}
.fc-col-header-cell-cushion {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 0.875rem;
font-weight: 600;
}
.fc-toolbar .fc-toolbar-title {
margin-inline-start: 0.25rem;
}
.fc-toolbar.fc-header-toolbar {
margin-block-end: 1rem;
}
.fc-event-time {
font-size: 0.75rem;
}
.fc-timegrid-event {
.fc-event-title {
font-size: 0.875rem;
}
}
.fc-prev-button {
padding-inline-start: 0;
}
.fc-prev-button,
.fc-next-button {
padding: 0.25rem;
}
.fc-col-header .fc-col-header-cell .fc-col-header-cell-cushion {
padding: 0.5rem;
text-decoration: none !important;
}
.fc-timegrid .fc-timegrid-slots .fc-timegrid-slot {
block-size: 3rem;
}
// Removed double border on left in list view
.fc-list {
border-inline-start-color: transparent;
font-size: 0.875rem;
.fc-list-day-cushion.fc-cell-shaded {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-weight: 600;
}
.fc-list-event-time,
.fc-list-event-title {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
.fc-list-day .fc-list-day-text,
.fc-list-day .fc-list-day-side-text {
text-decoration: none;
}
}
.fc-timegrid-axis {
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
font-size: 0.75rem;
text-transform: capitalize;
}
.fc-timegrid-slot-label-frame {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 0.75rem;
text-align: center;
text-transform: uppercase;
}
.fc-header-toolbar {
flex-wrap: wrap;
column-gap: 0.5rem;
margin-block: 1rem;
margin-inline: 1rem 1.25rem;
row-gap: 1rem;
}
.fc-toolbar-chunk {
display: flex;
align-items: center;
.fc-button-group {
.fc-button-primary {
&,
&:hover,
&:not(.disabled):active {
border-color: transparent;
background-color: transparent;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
}
&:focus {
box-shadow: none !important;
}
}
}
&:last-child {
.fc-button-group {
border: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 0.375rem;
.fc-button {
font-size: 0.9rem;
letter-spacing: 0.0187rem;
padding-inline: 1rem;
text-transform: uppercase;
&:not(:last-child) {
border-inline-end: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity));
}
&.fc-button-active {
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
color: rgb(var(--v-theme-primary));
}
}
}
}
}
.fc-toolbar-title {
display: inline-block;
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
font-size: 1.25rem;
font-weight: 500;
}
.fc-scrollgrid-section {
th {
border-inline-end-color: transparent;
}
}
// Calendar content container
.fc-view-harness {
min-block-size: 40.625rem;
}
.fc-event {
border-color: transparent;
margin-block-end: 0.3rem;
padding-block: 0.1875rem;
padding-inline: 0.3125rem;
}
.fc-event-main {
color: inherit;
font-size: 0.75rem;
padding-inline: 0.25rem;
}
tbody[role="rowgroup"] {
> tr > td[role="presentation"] {
border: none;
}
}
.fc-scrollgrid {
border-inline-start: none;
}
.fc-daygrid-day {
padding: 0.3125rem;
}
.fc-daygrid-day-number {
padding-block: 0.5rem;
padding-inline: 0.75rem;
}
.fc-list-event-dot {
color: inherit;
--fc-event-border-color: currentcolor;
}
.fc-list-event {
background-color: transparent !important;
}
.fc-popover {
@include elevation.elevation(3);
border-radius: 6px;
.fc-popover-header,
.fc-popover-body {
padding: 0.5rem;
}
.fc-popover-title {
margin: 0;
font-size: 1rem;
font-weight: 500;
}
}
// 👉 sidebar toggler
.fc-toolbar-chunk {
.fc-button-group {
align-items: center;
.fc-button .fc-icon {
vertical-align: bottom;
}
// Below two `background-image` styles contains static color due to browser limitation of not parsing the css var inside CSS url()
.fc-drawerToggler-button {
display: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
background-position: 50%;
background-repeat: no-repeat;
block-size: 1.5625rem;
font-size: 0;
inline-size: 1.5625rem;
margin-inline-end: 0.25rem;
@media (max-width: 1264px) {
display: block !important;
}
.v-theme--dark & {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
}
}
}
}
}

View File

@ -0,0 +1,43 @@
// 👉 Avatar
.v-avatar {
font-size: 1.125rem;
}
$alert-icon-size: 22px;
$alert-prominent-icon-size: 38px;
// 👉 Alert
.v-alert {
&:not(.v-alert--prominent) {
.v-icon {
block-size: $alert-icon-size !important;
font-size: $alert-icon-size !important;
inline-size: $alert-icon-size !important;
}
}
&.v-alert--prominent {
.v-icon {
block-size: $alert-prominent-icon-size !important;
font-size: $alert-prominent-icon-size !important;
inline-size: $alert-prominent-icon-size !important;
}
}
}
// 👉 Table
.v-table {
th {
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
font-weight: 500;
}
}
// // 👉 Timeline
.v-timeline {
.v-timeline-item:not(:last-child) {
.v-timeline-item__body {
margin-block-end: 0.625rem;
}
}
}

View File

@ -0,0 +1,225 @@
$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);
$font-family-custom: "Inter", sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
@forward "../../../base/libs/vuetify/variables" with (
// 👉 font-family
$body-font-family: $font-family-custom !default,
$border-radius-root: 8px !default,
$shadow-key-umbra: (
0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)),
1: (0 1px 2px -1px var(--v-shadow-key-umbra-opacity)),
2: (0 1px 3px -1px var(--v-shadow-key-umbra-opacity)),
3: (0 1px 6px -1px var(--v-shadow-key-umbra-opacity)),
4: (0 1px 7px -2px var(--v-shadow-key-umbra-opacity)),
5: (0 2px 8px -2px var(--v-shadow-key-umbra-opacity)),
6: (0 2px 9px -2px var(--v-shadow-key-umbra-opacity)),
7: (0 2px 10px -3px var(--v-shadow-key-umbra-opacity)),
8: (0 3px 11px -3px var(--v-shadow-key-umbra-opacity)),
9: (0 4px 12px -3px var(--v-shadow-key-umbra-opacity)),
10: (0 5px 13px -4px var(--v-shadow-key-umbra-opacity)),
11: (0 6px 14px -4px var(--v-shadow-key-umbra-opacity)),
12: (0 6px 15px -4px var(--v-shadow-key-umbra-opacity)),
13: (0 7px 14px -5px var(--v-shadow-key-umbra-opacity)),
14: (0 6px 17px -5px var(--v-shadow-key-umbra-opacity)),
15: (0 7px 18px -5px var(--v-shadow-key-umbra-opacity)),
16: (0 7px 19px -6px var(--v-shadow-key-umbra-opacity)),
17: (0 7px 20px -6px var(--v-shadow-key-umbra-opacity)),
18: (0 8px 21px -6px var(--v-shadow-key-umbra-opacity)),
19: (0 8px 22px -7px var(--v-shadow-key-umbra-opacity)),
20: (0 9px 23px -7px var(--v-shadow-key-umbra-opacity)),
21: (0 9px 24px -7px var(--v-shadow-key-umbra-opacity)),
22: (0 9px 25px -8px var(--v-shadow-key-umbra-opacity)),
23: (0 10px 26px -8px var(--v-shadow-key-umbra-opacity)),
24: (0 10px 27px -8px var(--v-shadow-key-umbra-opacity))
) !default,
$shadow-key-penumbra: (
0: (0 0 0 0 $shadow-key-penumbra-opacity-custom),
1: (0 1px 2px 1px $shadow-key-penumbra-opacity-custom),
2: (0 2px 3px 1px $shadow-key-penumbra-opacity-custom),
3: (0 2px 4px 1px $shadow-key-penumbra-opacity-custom),
4: (0 3px 5px 1px $shadow-key-penumbra-opacity-custom),
5: (0 3px 6px 1px $shadow-key-penumbra-opacity-custom),
6: (0 4px 7px 1px $shadow-key-penumbra-opacity-custom),
7: (0 4px 8px 1px $shadow-key-penumbra-opacity-custom),
8: (0 6px 9px 1px $shadow-key-penumbra-opacity-custom),
9: (0 5px 10px 1px $shadow-key-penumbra-opacity-custom),
10: (0 6px 12px 3px $shadow-key-penumbra-opacity-custom),
11: (0 8px 12px 1px $shadow-key-penumbra-opacity-custom),
12: (0 10px 13px 2px $shadow-key-penumbra-opacity-custom),
13: (0 12px 14px 2px $shadow-key-penumbra-opacity-custom),
14: (0 12px 15px 2px $shadow-key-penumbra-opacity-custom),
15: (0 14px 16px 2px $shadow-key-penumbra-opacity-custom),
16: (0 15px 17px 2px $shadow-key-penumbra-opacity-custom),
17: (0 16px 18px 2px $shadow-key-penumbra-opacity-custom),
18: (0 17px 19px 2px $shadow-key-penumbra-opacity-custom),
19: (0 18px 20px 2px $shadow-key-penumbra-opacity-custom),
20: (0 18px 21px 3px $shadow-key-penumbra-opacity-custom),
21: (0 18px 22px 3px $shadow-key-penumbra-opacity-custom),
22: (0 20px 23px 3px $shadow-key-penumbra-opacity-custom),
23: (0 22px 24px 3px $shadow-key-penumbra-opacity-custom),
24: (0 22px 25px 3px $shadow-key-penumbra-opacity-custom)
) !default,
$shadow-key-ambient: (
0: (0 0 0 0 $shadow-key-ambient-opacity-custom),
1: (0 1px 2px 2px $shadow-key-ambient-opacity-custom),
2: (0 1px 3px 2px $shadow-key-ambient-opacity-custom),
3: (0 1px 4px 2px $shadow-key-ambient-opacity-custom),
4: (0 1px 4px 2px $shadow-key-ambient-opacity-custom),
5: (0 1px 5px 4px $shadow-key-ambient-opacity-custom),
6: (0 2px 6px 4px $shadow-key-ambient-opacity-custom),
7: (0 2px 7px 4px $shadow-key-ambient-opacity-custom),
8: (0 3px 8px 4px $shadow-key-ambient-opacity-custom),
9: (0 4px 9px 5px $shadow-key-ambient-opacity-custom),
10: (0 5px 10px 5px $shadow-key-ambient-opacity-custom),
11: (0 6px 11px 5px $shadow-key-ambient-opacity-custom),
12: (0 5px 12px 5px $shadow-key-ambient-opacity-custom),
13: (0 5px 14px 6px $shadow-key-ambient-opacity-custom),
14: (0 5px 14px 6px $shadow-key-ambient-opacity-custom),
15: (0 5px 15px 6px $shadow-key-ambient-opacity-custom),
16: (0 5px 16px 6px $shadow-key-ambient-opacity-custom),
17: (0 5px 17px 7px $shadow-key-ambient-opacity-custom),
18: (0 6px 18px 7px $shadow-key-ambient-opacity-custom),
19: (0 6px 19px 7px $shadow-key-ambient-opacity-custom),
20: (0 7px 20px 7px $shadow-key-ambient-opacity-custom),
21: (0 7px 21px 7px $shadow-key-ambient-opacity-custom),
22: (0 7px 22px 7px $shadow-key-ambient-opacity-custom),
23: (0 8px 23px 7px $shadow-key-ambient-opacity-custom),
24: (0 8px 24px 7px $shadow-key-ambient-opacity-custom)
) !default,
// 👉 typography
$typography: (
"h1": (
"weight": 500,
"line-height": 7rem,
"letter-spacing": -0.0938rem,
),
"h2": (
"weight": 500,
"line-height": 4.5rem,
"letter-spacing": -0.0313rem,
),
"h3": (
"weight": 500,
"line-height": 3.5rem,
),
"h4": (
"weight": 500,
"letter-spacing": 0.0156rem,
),
"h5": (
"weight": 500,
),
"h6": (
"letter-spacing": 0.0094rem,
),
"subtitle-1": (
"letter-spacing": 0.0094rem,
),
"subtitle-2": (
"line-height": 1.3125rem,
"letter-spacing": 0.0063rem,
),
"body-1": (
"letter-spacing": 0.0094rem,
),
"body-2": (
"line-height": 1.3125rem,
"letter-spacing": 0.0094rem,
),
"caption": (
"line-height": 0.875rem,
"letter-spacing": 0.025rem,
),
"button": (
"line-height": 1.5rem,
"letter-spacing": 0.025rem,
),
"overline": (
"weight": 400,
"line-height": 0.875rem,
"letter-spacing": 0.0625rem,
),
) !default,
// 👉 alert
$alert-padding: 17.5px !default,
$alert-title-font-size: 16px !default,
$alert-prepend-margin-inline-end: 13px !default,
$alert-background: rgb(var(--v-theme-alert-background)) !default,
// 👉 buttons
$button-height: 38px,
$button-line-height: 24px,
$button-padding-ratio: 1.8,
// 👉 card
$card-border-radius: 10px !default,
// 👉 chips
$chip-font-size: 13px !default,
$chip-close-size: 22px !default,
// 👉 dialogs
$dialog-card-header-padding: 20px 20px 0 !default,
$dialog-card-text-padding: 20px 20px !default,
$dialog-card-header-text-padding-top: 20px !default,
$dialog-border-radius: 10px !default,
// 👉 expansion panel
$expansion-panel-border-radius: 0 !default,
$expansion-panel-active-title-min-height: 50px !default,
$expansion-panel-title-min-height: 50px !default,
$expansion-panel-title-padding: 16px 20px !default,
$expansion-panel-text-padding: 8px 20px 16px !default,
// 👉 list item
$list-item-padding:12px 16px !default,
$list-item-icon-margin-end: 14px !default,
$list-nav-padding: 16px !default,
$rounded: (
"shaped": 24px 0,
) !default,
// 👉 overlay
$overlay-opacity: 50% !default,
// 👉 pagination
$pagination-item-margin: 3px !default,
// 👉 snackbar
$snackbar-content-padding: 6px 16px,
$snackbar-background: rgb(var(--v-theme-snackbar-background)),
$snackbar-color: rgb(var(--v-theme-on-snackbar-background)),
// 👉 tooltip
$tooltip-padding: 4px 8px !default,
$tooltip-background-color: rgba(var(--v-theme-tooltip-background), 0.9) !default,
$tooltip-border-radius: 6px !default,
$tooltip-font-size: 0.6875rem !default,
$tooltip-line-height: 16px !default,
// 👉 Timeline
$timeline-dot-divider-background: transparent !default,
$timeline-item-padding: 16px !default,
// 👉 Table
$table-header-height: 54px !default,
$table-row-height: 50px !default,
// 👉 range slider
$slider-track-active-size: 4px !default,
$slider-thumb-label-padding: 4px 12px !default,
$slider-thumb-label-font-size: 14px !default,
$slider-thumb-label-height: 29px !default,
);

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