appsalutely 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
2
+ charset = utf-8
3
+ indent_size = 2
4
+ indent_style = space
5
+ insert_final_newline = true
6
+ trim_trailing_whitespace = true
7
+
8
+ end_of_line = lf
9
+ max_line_length = 100
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "semi": false,
4
+ "singleQuote": true,
5
+ "printWidth": 100
6
+ }
@@ -0,0 +1,22 @@
1
+ import { globalIgnores } from 'eslint/config'
2
+ import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
3
+ import pluginVue from 'eslint-plugin-vue'
4
+ import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
5
+
6
+ // To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
7
+ // import { configureVueProject } from '@vue/eslint-config-typescript'
8
+ // configureVueProject({ scriptLangs: ['ts', 'tsx'] })
9
+ // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
10
+
11
+ export default defineConfigWithVueTs(
12
+ {
13
+ name: 'app/files-to-lint',
14
+ files: ['**/*.{ts,mts,tsx,vue}'],
15
+ },
16
+
17
+ globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
18
+
19
+ pluginVue.configs['flat/essential'],
20
+ vueTsConfigs.recommended,
21
+ skipFormatting,
22
+ )
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "appsalutely",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "module": "./dist/index.js",
6
+ "main": "./dist/index.umd.js",
7
+ "types": "./dist/index.d.ts",
8
+ "license": "MIT",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.umd.js"
13
+ },
14
+ "./index.css": "./dist/index.css"
15
+ },
16
+ "scripts": {
17
+ "build": "vite build && vue-tsc",
18
+ "type-check": "vue-tsc --build",
19
+ "lint": "eslint . --fix",
20
+ "format": "prettier --write src/"
21
+ },
22
+ "dependencies": {
23
+ "pinia": "^3.0.1",
24
+ "vue": "^3.5.13",
25
+ "vue-router": "^4.5.1",
26
+ "vuetify": "^3.8.5"
27
+ },
28
+ "devDependencies": {
29
+ "@tsconfig/node22": "^22.0.1",
30
+ "@types/node": "^22.14.0",
31
+ "@vitejs/plugin-vue": "^5.2.4",
32
+ "@vue/eslint-config-prettier": "^10.2.0",
33
+ "@vue/eslint-config-typescript": "^14.5.0",
34
+ "@vue/tsconfig": "^0.7.0",
35
+ "eslint": "^9.22.0",
36
+ "eslint-plugin-vue": "~10.0.0",
37
+ "jiti": "^2.4.2",
38
+ "npm-run-all2": "^7.0.2",
39
+ "prettier": "3.5.3",
40
+ "typescript": "~5.8.0",
41
+ "vite": "^6.2.4",
42
+ "vite-plugin-vue-devtools": "^7.7.2",
43
+ "vue-tsc": "^2.2.8"
44
+ }
45
+ }
@@ -0,0 +1,139 @@
1
+ <script setup lang="ts">
2
+ import { ref, onBeforeMount, watch, useSlots } from 'vue'
3
+ import { useRouter, RouterView } from 'vue-router'
4
+ import {
5
+ VApp,
6
+ VProgressLinear,
7
+ VAppBar,
8
+ VNavigationDrawer,
9
+ VList,
10
+ VMain,
11
+ VFooter,
12
+ VBtn,
13
+ VSnackbar,
14
+ } from 'vuetify/components'
15
+ import useColorMode from '../stores/colorMode'
16
+ import useFooter from '../stores/footer'
17
+ import useNotify from '../stores/notify'
18
+
19
+ const colorMode = useColorMode()
20
+ const drawerOpen = defineModel<boolean>('drawerOpen', { default: false })
21
+ const drawerDocked = ref(false)
22
+ const footer = useFooter()
23
+ const navigating = ref(false)
24
+ const navigatingTimeout = ref<number | null>(null)
25
+ const notify = useNotify()
26
+ const router = useRouter()
27
+ const slots = useSlots()
28
+
29
+ watch(drawerDocked, (newValue) => {
30
+ localStorage.setItem('drawerDocked', String(newValue))
31
+ })
32
+
33
+ onBeforeMount(() => {
34
+ router.beforeEach(() => {
35
+ navigatingTimeout.value = setTimeout(() => {
36
+ navigating.value = true
37
+ }, 300)
38
+ })
39
+ router.afterEach(() => {
40
+ if (navigatingTimeout.value) {
41
+ clearTimeout(navigatingTimeout.value)
42
+ navigatingTimeout.value = null
43
+ }
44
+ navigating.value = false
45
+ })
46
+ drawerDocked.value = localStorage.getItem('drawerDocked') === 'true'
47
+ drawerOpen.value = drawerDocked.value
48
+ })
49
+ </script>
50
+ <template>
51
+ <VApp :theme="colorMode.mode">
52
+ <VProgressLinear
53
+ indeterminate
54
+ v-if="navigating"
55
+ class="mb-n1"
56
+ style="z-index: 100000; opacity: 0.5"
57
+ height="1"
58
+ />
59
+ <VAppBar height="64">
60
+ <slot name="header" />
61
+ </VAppBar>
62
+ <VMain class="bg-surface-light">
63
+ <div class="d-flex flex-column pa-4 h-100">
64
+ <RouterView />
65
+ </div>
66
+ </VMain>
67
+ <VAppBar v-if="slots.footer" height="40" location="bottom">
68
+ <VFooter class="py-0 w-100 d-flex align-center">
69
+ <span
70
+ v-if="footer.current"
71
+ v-text="footer.current.text"
72
+ :class="footer.current.type ? {} : { ['text-' + footer.current.type]: true }"
73
+ style="text-overflow: ellipsis"
74
+ class="overflow-hidden text-no-wrap"
75
+ />
76
+ <template v-if="footer.current && footer.current.actions">
77
+ <VBtn
78
+ v-for="(action, index) of Object.entries(footer.current.actions)"
79
+ :text="action[0]"
80
+ @click="action[1]"
81
+ :key="index"
82
+ density="comfortable"
83
+ variant="tonal"
84
+ class="mx-2 px-2 text-none"
85
+ :color="footer.current.type"
86
+ />
87
+ </template>
88
+ <div class="me-auto" />
89
+ <slot name="footer" />
90
+ </VFooter>
91
+ </VAppBar>
92
+ <VNavigationDrawer
93
+ v-if="slots.drawer"
94
+ v-model="drawerOpen"
95
+ width="250"
96
+ rail-width="58"
97
+ :rail="drawerDocked"
98
+ :mobile="drawerDocked ? false : undefined"
99
+ >
100
+ <VList class="h-100 d-flex flex-column">
101
+ <slot name="drawer" />
102
+ <VBtn
103
+ @click="drawerDocked = !drawerDocked"
104
+ icon="mdi-pin-outline"
105
+ rounded="rounded"
106
+ density="comfortable"
107
+ :title="drawerDocked ? 'Undock sidebar' : 'Dock sidebar'"
108
+ class="my-1 mx-3 ms-auto"
109
+ :variant="drawerDocked ? 'tonal' : 'text'"
110
+ />
111
+ </VList>
112
+ </VNavigationDrawer>
113
+ <VSnackbar
114
+ v-for="(message, index) of notify.messages"
115
+ :key="index"
116
+ :model-value="true"
117
+ :text="message.text"
118
+ :color="message.type"
119
+ location="bottom end"
120
+ :style="{ bottom: `${index * 56}px` }"
121
+ :timeout="-1"
122
+ >
123
+ <template #actions>
124
+ <VBtn icon="mdi-close" @click="notify.remove(message.id!)" />
125
+ </template>
126
+ </VSnackbar>
127
+ </VApp>
128
+ </template>
129
+ <style>
130
+ html {
131
+ overflow: auto;
132
+ }
133
+ html[data-theme='dark'] {
134
+ color-scheme: dark;
135
+ }
136
+ html[data-theme='light'] {
137
+ color-scheme: light;
138
+ }
139
+ </style>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { VBreadcrumbs, VContainer } from 'vuetify/components'
3
+ import { defineProps, withDefaults } from 'vue'
4
+
5
+ withDefaults(
6
+ defineProps<{
7
+ breadcrumbs?: VBreadcrumbs['$props']['items']
8
+ fluid?: boolean
9
+ title?: string
10
+ }>(),
11
+ {
12
+ fluid: true,
13
+ },
14
+ )
15
+ </script>
16
+ <template>
17
+ <VContainer :fluid="fluid" class="h-100 d-flex flex-column py-0 h-100">
18
+ <VBreadcrumbs :items="breadcrumbs" density="comfortable" class="pa-0" v-if="breadcrumbs" />
19
+ <h1 v-if="title" v-text="title" class="text-h5 font-bold ma-1" />
20
+ <slot />
21
+ </VContainer>
22
+ </template>
@@ -0,0 +1,6 @@
1
+ <template>
2
+ <span class="border-s mx-3" style="height: 32px" />
3
+ <span class="h-100 d-flex align-center">
4
+ <slot />
5
+ </span>
6
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { defineProps, withDefaults, defineModel, defineEmits, ref } from 'vue'
3
+ import { VBtn, VCard, VCardText, VForm, VTextField } from 'vuetify/components'
4
+
5
+ const username = defineModel<string>('username', { default: '' })
6
+ const password = defineModel<string>('password', { default: '' })
7
+ const emit = defineEmits<{
8
+ submit: [string, string]
9
+ }>()
10
+ withDefaults(
11
+ defineProps<{
12
+ title?: string
13
+ submitButtonText?: string
14
+ minWidth?: string | number
15
+ maxWidth?: string | number
16
+ loading?: boolean
17
+ }>(),
18
+ {
19
+ title: 'Login',
20
+ submitButtonText: 'Login',
21
+ },
22
+ )
23
+ const valid = ref(false)
24
+ </script>
25
+ <template>
26
+ <VCard :title="title" :min-width="minWidth" :max-width="maxWidth">
27
+ <VForm @submit.prevent="emit('submit', username, password)" v-model="valid">
28
+ <VCardText>
29
+ <VTextField
30
+ v-model="username"
31
+ label="Username"
32
+ :rules="[(v) => !!v || 'Username is required']"
33
+ persistent-placeholder
34
+ autofocus
35
+ />
36
+ <VTextField
37
+ v-model="password"
38
+ label="Password"
39
+ type="password"
40
+ :rules="[(v) => !!v || 'Password is required']"
41
+ persistent-placeholder
42
+ />
43
+ <div class="d-flex justify-center">
44
+ <VBtn
45
+ :disabled="!valid"
46
+ type="submit"
47
+ color="primary"
48
+ variant="flat"
49
+ :text="submitButtonText"
50
+ :loading="loading"
51
+ />
52
+ </div>
53
+ </VCardText>
54
+ </VForm>
55
+ </VCard>
56
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { RouteLocationRaw } from 'vue-router'
3
+ import { VListItem, VIcon } from 'vuetify/components'
4
+
5
+ defineProps<{
6
+ to: RouteLocationRaw
7
+ icon: string
8
+ text: string
9
+ }>()
10
+ </script>
11
+ <template>
12
+ <VListItem role="option" :to="to" slim>
13
+ <template #prepend>
14
+ <VIcon :icon="icon" />
15
+ </template>
16
+ <span class="text-no-wrap overflow-hidden">{{ text }}</span>
17
+ </VListItem>
18
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { defineProps, withDefaults, defineModel, defineEmits, computed, watch } from 'vue'
3
+ import { VBtn, VCard, VCardText, VForm, VOtpInput } from 'vuetify/components'
4
+
5
+ const otp = defineModel<string>('otp', { default: '' })
6
+ const emit = defineEmits<{
7
+ submit: [string]
8
+ }>()
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ title?: string
12
+ subtitle?: string
13
+ submitButtonText?: string
14
+ minWidth?: string | number
15
+ maxWidth?: string | number
16
+ loading?: boolean
17
+ length?: number
18
+ autosubmit?: boolean
19
+ }>(),
20
+ {
21
+ title: '2FA Verification',
22
+ subtitle: 'Enter the code from your authenticator app',
23
+ submitButtonText: 'Verify',
24
+ length: 6,
25
+ autosubmit: true,
26
+ },
27
+ )
28
+ const valid = computed(() => otp.value.length === props.length)
29
+
30
+ watch(otp, () => {
31
+ if (props.autosubmit && otp.value.length === props.length) {
32
+ emit('submit', otp.value)
33
+ }
34
+ })
35
+ </script>
36
+ <template>
37
+ <VCard :title="title" :subtitle="subtitle" :min-width="minWidth" :max-width="maxWidth">
38
+ <VForm @submit.prevent="emit('submit', otp)" :model-value="valid">
39
+ <VCardText>
40
+ <VOtpInput v-model="otp" label="OTP" persistent-placeholder autofocus :length="length" />
41
+ <div class="d-flex justify-center">
42
+ <VBtn
43
+ :disabled="!valid"
44
+ type="submit"
45
+ color="primary"
46
+ variant="flat"
47
+ :text="submitButtonText"
48
+ :loading="loading"
49
+ />
50
+ </div>
51
+ </VCardText>
52
+ </VForm>
53
+ </VCard>
54
+ </template>
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ import AppBase from './components/AppBase.vue'
2
+ import DashboardPage from './components/DashboardPage.vue'
3
+ import FooterSection from './components/FooterSection.vue'
4
+ import LoginForm from './components/LoginForm.vue'
5
+ import NavDrawerLink from './components/NavDrawerLink.vue'
6
+ import OTPForm from './components/OTPForm.vue'
7
+
8
+ import useColorMode from './stores/colorMode'
9
+ import useFooter from './stores/footer'
10
+ import useNotify from './stores/notify'
11
+
12
+ export {
13
+ AppBase,
14
+ DashboardPage,
15
+ FooterSection,
16
+ LoginForm,
17
+ NavDrawerLink,
18
+ OTPForm,
19
+ useColorMode,
20
+ useFooter,
21
+ useNotify,
22
+ }
@@ -0,0 +1,16 @@
1
+ import { ref, watch } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export default defineStore('colorMode', () => {
5
+ const mode = ref<'light' | 'dark'>(localStorage.getItem('colorMode') == 'dark' ? 'dark' : 'light')
6
+ function toggle() {
7
+ mode.value = mode.value === 'dark' ? 'light' : 'dark'
8
+ }
9
+ function update(newMode: 'light' | 'dark') {
10
+ localStorage.setItem('colorMode', newMode)
11
+ document.documentElement.setAttribute('data-theme', newMode)
12
+ }
13
+ watch(mode, (newMode) => update(newMode))
14
+ update(mode.value)
15
+ return { mode, toggle }
16
+ })
@@ -0,0 +1,41 @@
1
+ import { ref, computed } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export interface FooterMessage {
5
+ id?: string
6
+ text: string
7
+ type?: 'info' | 'success' | 'warning' | 'error'
8
+ timeout?: number
9
+ actions?: {
10
+ [key: string]: () => void
11
+ }
12
+ }
13
+
14
+ export default defineStore('footer', () => {
15
+ const messages = ref<FooterMessage[]>([])
16
+ const current = computed(() => {
17
+ if (messages.value.length > 0) {
18
+ return messages.value[messages.value.length - 1]
19
+ }
20
+ return null
21
+ })
22
+ function addMessage(message: FooterMessage) {
23
+ if (!message.id) {
24
+ message.id = new Date().getTime().toString()
25
+ }
26
+ if (message.timeout === undefined) {
27
+ message.timeout = 5000
28
+ }
29
+ messages.value.push(message)
30
+ if (message.timeout) {
31
+ setTimeout(() => {
32
+ removeMessage(message.id!)
33
+ }, message.timeout)
34
+ }
35
+ return message.id!
36
+ }
37
+ function removeMessage(id: string) {
38
+ messages.value = messages.value.filter((message) => message.id !== id)
39
+ }
40
+ return { messages, current, addMessage, removeMessage }
41
+ })
@@ -0,0 +1,51 @@
1
+ import { ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export interface NotifyMessage {
5
+ id?: string
6
+ text: string
7
+ type?: 'info' | 'success' | 'warning' | 'error'
8
+ timeout?: number
9
+ }
10
+
11
+ export default defineStore('notify', () => {
12
+ const messages = ref<NotifyMessage[]>([])
13
+ const timeouts = ref<Record<string, number>>({})
14
+ function add(message: NotifyMessage) {
15
+ if (!message.id) {
16
+ message.id = new Date().getTime().toString()
17
+ } else {
18
+ remove(message.id)
19
+ }
20
+ if (message.timeout === undefined) {
21
+ message.timeout = 5000
22
+ }
23
+ messages.value.push(message)
24
+ if (message.timeout) {
25
+ timeouts.value[message.id] = setTimeout(() => {
26
+ remove(message.id!)
27
+ }, message.timeout)
28
+ }
29
+ return message.id!
30
+ }
31
+ function remove(id: string) {
32
+ messages.value = messages.value.filter((message) => message.id !== id)
33
+ if (timeouts.value[id]) {
34
+ clearTimeout(timeouts.value[id])
35
+ delete timeouts.value[id]
36
+ }
37
+ }
38
+ function info(text: string) {
39
+ return add({ text, type: 'info' })
40
+ }
41
+ function success(text: string) {
42
+ return add({ text, type: 'success' })
43
+ }
44
+ function warning(text: string) {
45
+ return add({ text, type: 'warning' })
46
+ }
47
+ function error(text: string) {
48
+ return add({ text, type: 'error' })
49
+ }
50
+ return { messages, add, remove, info, success, warning, error }
51
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "include": ["src/**/*.ts", "src/**/*.vue"],
4
+ "exclude": ["src/**/__tests__/*"],
5
+ "compilerOptions": {
6
+ "target": "ES2022",
7
+ "module": "ESNext",
8
+ "noEmit": false,
9
+ "emitDeclarationOnly": true,
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "outDir": "./dist",
14
+ "declaration": true,
15
+ "skipLibCheck": true,
16
+ "strict": true
17
+ },
18
+ "references": [
19
+ {
20
+ "path": "./tsconfig.node.json"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { defineConfig, type UserConfig } from 'vite'
2
+ import Vue from '@vitejs/plugin-vue'
3
+
4
+ const config: UserConfig = {
5
+ build: {
6
+ lib: {
7
+ entry: 'src/index.ts',
8
+ name: 'appsalutely',
9
+ fileName: 'index',
10
+ },
11
+ rollupOptions: {
12
+ external: ['vue', 'vue-router', 'pinia', 'vuetify', 'vuetify/components'],
13
+ output: {
14
+ globals: {
15
+ vue: 'Vue',
16
+ 'vue-router': 'VueRouter',
17
+ pinia: 'Pinia',
18
+ vuetify: 'Vuetify',
19
+ 'vuetify/components': 'VuetifyComponents',
20
+ },
21
+ },
22
+ },
23
+ },
24
+ plugins: [Vue()],
25
+ }
26
+ export default defineConfig(config)