appsalutely 0.1.0 → 0.1.2

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/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Merlin Zimmerman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # appsalutely
2
+
3
+ A utility library for building apps with FastAPI and Vue.js.
@@ -0,0 +1,23 @@
1
+ type __VLS_PublicProps = {
2
+ 'drawerOpen'?: boolean;
3
+ };
4
+ declare var __VLS_14: {}, __VLS_40: {}, __VLS_50: {};
5
+ type __VLS_Slots = {} & {
6
+ header?: (props: typeof __VLS_14) => any;
7
+ } & {
8
+ footer?: (props: typeof __VLS_40) => any;
9
+ } & {
10
+ drawer?: (props: typeof __VLS_50) => any;
11
+ };
12
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ "update:drawerOpen": (value: boolean) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ "onUpdate:drawerOpen"?: ((value: boolean) => any) | undefined;
16
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
18
+ export default _default;
19
+ type __VLS_WithSlots<T, S> = T & {
20
+ new (): {
21
+ $slots: S;
22
+ };
23
+ };
@@ -0,0 +1,20 @@
1
+ import { VBreadcrumbs } from 'vuetify/components';
2
+ type __VLS_Props = {
3
+ breadcrumbs?: VBreadcrumbs['$props']['items'];
4
+ fluid?: boolean;
5
+ title?: string;
6
+ };
7
+ declare var __VLS_10: {};
8
+ type __VLS_Slots = {} & {
9
+ default?: (props: typeof __VLS_10) => any;
10
+ };
11
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
+ fluid: boolean;
13
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
15
+ export default _default;
16
+ type __VLS_WithSlots<T, S> = T & {
17
+ new (): {
18
+ $slots: S;
19
+ };
20
+ };
@@ -0,0 +1,12 @@
1
+ declare var __VLS_1: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_1) => any;
4
+ };
5
+ declare const __VLS_component: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
7
+ export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -0,0 +1,24 @@
1
+ type __VLS_Props = {
2
+ title?: string;
3
+ submitButtonText?: string;
4
+ minWidth?: string | number;
5
+ maxWidth?: string | number;
6
+ loading?: boolean;
7
+ };
8
+ type __VLS_PublicProps = __VLS_Props & {
9
+ 'username'?: string;
10
+ 'password'?: string;
11
+ };
12
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ submit: (args_0: string, args_1: string) => any;
14
+ "update:username": (value: string) => any;
15
+ "update:password": (value: string) => any;
16
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
17
+ onSubmit?: ((args_0: string, args_1: string) => any) | undefined;
18
+ "onUpdate:username"?: ((value: string) => any) | undefined;
19
+ "onUpdate:password"?: ((value: string) => any) | undefined;
20
+ }>, {
21
+ title: string;
22
+ submitButtonText: string;
23
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { RouteLocationRaw } from 'vue-router';
2
+ type __VLS_Props = {
3
+ to: RouteLocationRaw;
4
+ icon: string;
5
+ text: string;
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ export default _default;
@@ -0,0 +1,27 @@
1
+ type __VLS_Props = {
2
+ title?: string;
3
+ subtitle?: string;
4
+ submitButtonText?: string;
5
+ minWidth?: string | number;
6
+ maxWidth?: string | number;
7
+ loading?: boolean;
8
+ length?: number;
9
+ autosubmit?: boolean;
10
+ };
11
+ type __VLS_PublicProps = __VLS_Props & {
12
+ 'otp'?: string;
13
+ };
14
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
+ submit: (args_0: string) => any;
16
+ "update:otp": (value: string) => any;
17
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
18
+ onSubmit?: ((args_0: string) => any) | undefined;
19
+ "onUpdate:otp"?: ((value: string) => any) | undefined;
20
+ }>, {
21
+ length: number;
22
+ title: string;
23
+ subtitle: string;
24
+ submitButtonText: string;
25
+ autosubmit: boolean;
26
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
27
+ export default _default;
package/dist/index.css ADDED
@@ -0,0 +1 @@
1
+ html{overflow:auto}html[data-theme=dark]{color-scheme:dark}html[data-theme=light]{color-scheme:light}
@@ -0,0 +1,10 @@
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
+ import useColorMode from './stores/colorMode';
8
+ import useFooter from './stores/footer';
9
+ import useNotify from './stores/notify';
10
+ export { AppBase, DashboardPage, FooterSection, LoginForm, NavDrawerLink, OTPForm, useColorMode, useFooter, useNotify, };
package/dist/index.js ADDED
@@ -0,0 +1,381 @@
1
+ import { ref as v, watch as S, computed as P, defineComponent as k, useModel as B, useSlots as A, onBeforeMount as E, createBlock as f, openBlock as m, unref as t, withCtx as s, createCommentVNode as h, createVNode as d, createElementBlock as w, renderSlot as V, createElementVNode as b, normalizeClass as j, toDisplayString as M, Fragment as T, renderList as F, normalizeStyle as z, mergeModels as C, withModifiers as U } from "vue";
2
+ import { useRouter as q, RouterView as R } from "vue-router";
3
+ import { VApp as G, VProgressLinear as H, VAppBar as O, VMain as J, VFooter as K, VBtn as x, VNavigationDrawer as Q, VList as X, VSnackbar as Y, VContainer as Z, VBreadcrumbs as ee, VCard as W, VForm as I, VCardText as N, VTextField as L, VListItem as te, VIcon as oe, VOtpInput as le } from "vuetify/components";
4
+ import { defineStore as D } from "pinia";
5
+ const ne = D("colorMode", () => {
6
+ const o = v(localStorage.getItem("colorMode") == "dark" ? "dark" : "light");
7
+ function n() {
8
+ o.value = o.value === "dark" ? "light" : "dark";
9
+ }
10
+ function l(r) {
11
+ localStorage.setItem("colorMode", r), document.documentElement.setAttribute("data-theme", r);
12
+ }
13
+ return S(o, (r) => l(r)), l(o.value), { mode: o, toggle: n };
14
+ }), re = D("footer", () => {
15
+ const o = v([]), n = P(() => o.value.length > 0 ? o.value[o.value.length - 1] : null);
16
+ function l(a) {
17
+ return a.id || (a.id = (/* @__PURE__ */ new Date()).getTime().toString()), a.timeout === void 0 && (a.timeout = 5e3), o.value.push(a), a.timeout && setTimeout(() => {
18
+ r(a.id);
19
+ }, a.timeout), a.id;
20
+ }
21
+ function r(a) {
22
+ o.value = o.value.filter((c) => c.id !== a);
23
+ }
24
+ return { messages: o, current: n, addMessage: l, removeMessage: r };
25
+ }), ae = D("notify", () => {
26
+ const o = v([]), n = v({});
27
+ function l(e) {
28
+ return e.id ? r(e.id) : e.id = (/* @__PURE__ */ new Date()).getTime().toString(), e.timeout === void 0 && (e.timeout = 5e3), o.value.push(e), e.timeout && (n.value[e.id] = setTimeout(() => {
29
+ r(e.id);
30
+ }, e.timeout)), e.id;
31
+ }
32
+ function r(e) {
33
+ o.value = o.value.filter((_) => _.id !== e), n.value[e] && (clearTimeout(n.value[e]), delete n.value[e]);
34
+ }
35
+ function a(e) {
36
+ return l({ text: e, type: "info" });
37
+ }
38
+ function c(e) {
39
+ return l({ text: e, type: "success" });
40
+ }
41
+ function u(e) {
42
+ return l({ text: e, type: "warning" });
43
+ }
44
+ function i(e) {
45
+ return l({ text: e, type: "error" });
46
+ }
47
+ return { messages: o, add: l, remove: r, info: a, success: c, warning: u, error: i };
48
+ }), ue = { class: "d-flex flex-column pa-4 h-100" }, ie = ["textContent"], xe = /* @__PURE__ */ k({
49
+ __name: "AppBase",
50
+ props: {
51
+ drawerOpen: { type: Boolean, default: !1 },
52
+ drawerOpenModifiers: {}
53
+ },
54
+ emits: ["update:drawerOpen"],
55
+ setup(o) {
56
+ const n = ne(), l = B(o, "drawerOpen"), r = v(!1), a = re(), c = v(!1), u = v(null), i = ae(), e = q(), _ = A();
57
+ return S(r, (g) => {
58
+ localStorage.setItem("drawerDocked", String(g));
59
+ }), E(() => {
60
+ e.beforeEach(() => {
61
+ u.value = setTimeout(() => {
62
+ c.value = !0;
63
+ }, 300);
64
+ }), e.afterEach(() => {
65
+ u.value && (clearTimeout(u.value), u.value = null), c.value = !1;
66
+ }), r.value = localStorage.getItem("drawerDocked") === "true", l.value = r.value;
67
+ }), (g, y) => (m(), f(t(G), {
68
+ theme: t(n).mode
69
+ }, {
70
+ default: s(() => [
71
+ c.value ? (m(), f(t(H), {
72
+ key: 0,
73
+ indeterminate: "",
74
+ class: "mb-n1",
75
+ style: { "z-index": "100000", opacity: "0.5" },
76
+ height: "1"
77
+ })) : h("", !0),
78
+ d(t(O), { height: "64" }, {
79
+ default: s(() => [
80
+ V(g.$slots, "header")
81
+ ]),
82
+ _: 3
83
+ }),
84
+ d(t(J), { class: "bg-surface-light" }, {
85
+ default: s(() => [
86
+ b("div", ue, [
87
+ d(t(R))
88
+ ])
89
+ ]),
90
+ _: 1
91
+ }),
92
+ t(_).footer ? (m(), f(t(O), {
93
+ key: 1,
94
+ height: "40",
95
+ location: "bottom"
96
+ }, {
97
+ default: s(() => [
98
+ d(t(K), { class: "py-0 w-100 d-flex align-center" }, {
99
+ default: s(() => [
100
+ t(a).current ? (m(), w("span", {
101
+ key: 0,
102
+ textContent: M(t(a).current.text),
103
+ class: j([t(a).current.type ? {} : { ["text-" + t(a).current.type]: !0 }, "overflow-hidden text-no-wrap"]),
104
+ style: { "text-overflow": "ellipsis" }
105
+ }, null, 10, ie)) : h("", !0),
106
+ t(a).current && t(a).current.actions ? (m(!0), w(T, { key: 1 }, F(Object.entries(t(a).current.actions), (p, $) => (m(), f(t(x), {
107
+ text: p[0],
108
+ onClick: p[1],
109
+ key: $,
110
+ density: "comfortable",
111
+ variant: "tonal",
112
+ class: "mx-2 px-2 text-none",
113
+ color: t(a).current.type
114
+ }, null, 8, ["text", "onClick", "color"]))), 128)) : h("", !0),
115
+ y[2] || (y[2] = b("div", { class: "me-auto" }, null, -1)),
116
+ V(g.$slots, "footer")
117
+ ]),
118
+ _: 3,
119
+ __: [2]
120
+ })
121
+ ]),
122
+ _: 3
123
+ })) : h("", !0),
124
+ t(_).drawer ? (m(), f(t(Q), {
125
+ key: 2,
126
+ modelValue: l.value,
127
+ "onUpdate:modelValue": y[1] || (y[1] = (p) => l.value = p),
128
+ width: "250",
129
+ "rail-width": "58",
130
+ rail: r.value,
131
+ mobile: r.value ? !1 : void 0
132
+ }, {
133
+ default: s(() => [
134
+ d(t(X), { class: "h-100 d-flex flex-column" }, {
135
+ default: s(() => [
136
+ V(g.$slots, "drawer"),
137
+ d(t(x), {
138
+ onClick: y[0] || (y[0] = (p) => r.value = !r.value),
139
+ icon: "mdi-pin-outline",
140
+ rounded: "rounded",
141
+ density: "comfortable",
142
+ title: r.value ? "Undock sidebar" : "Dock sidebar",
143
+ class: "my-1 mx-3 ms-auto",
144
+ variant: r.value ? "tonal" : "text"
145
+ }, null, 8, ["title", "variant"])
146
+ ]),
147
+ _: 3
148
+ })
149
+ ]),
150
+ _: 3
151
+ }, 8, ["modelValue", "rail", "mobile"])) : h("", !0),
152
+ (m(!0), w(T, null, F(t(i).messages, (p, $) => (m(), f(t(Y), {
153
+ key: $,
154
+ "model-value": !0,
155
+ text: p.text,
156
+ color: p.type,
157
+ location: "bottom end",
158
+ style: z({ bottom: `${$ * 56}px` }),
159
+ timeout: -1
160
+ }, {
161
+ actions: s(() => [
162
+ d(t(x), {
163
+ icon: "mdi-close",
164
+ onClick: (be) => t(i).remove(p.id)
165
+ }, null, 8, ["onClick"])
166
+ ]),
167
+ _: 2
168
+ }, 1032, ["text", "color", "style"]))), 128))
169
+ ]),
170
+ _: 3
171
+ }, 8, ["theme"]));
172
+ }
173
+ }), se = ["textContent"], ke = /* @__PURE__ */ k({
174
+ __name: "DashboardPage",
175
+ props: {
176
+ breadcrumbs: {},
177
+ fluid: { type: Boolean, default: !0 },
178
+ title: {}
179
+ },
180
+ setup(o) {
181
+ return (n, l) => (m(), f(t(Z), {
182
+ fluid: n.fluid,
183
+ class: "h-100 d-flex flex-column py-0 h-100"
184
+ }, {
185
+ default: s(() => [
186
+ n.breadcrumbs ? (m(), f(t(ee), {
187
+ key: 0,
188
+ items: n.breadcrumbs,
189
+ density: "comfortable",
190
+ class: "pa-0"
191
+ }, null, 8, ["items"])) : h("", !0),
192
+ n.title ? (m(), w("h1", {
193
+ key: 1,
194
+ textContent: M(n.title),
195
+ class: "text-h5 font-bold ma-1"
196
+ }, null, 8, se)) : h("", !0),
197
+ V(n.$slots, "default")
198
+ ]),
199
+ _: 3
200
+ }, 8, ["fluid"]));
201
+ }
202
+ }), de = (o, n) => {
203
+ const l = o.__vccOpts || o;
204
+ for (const [r, a] of n)
205
+ l[r] = a;
206
+ return l;
207
+ }, me = {}, ce = { class: "h-100 d-flex align-center" };
208
+ function fe(o, n) {
209
+ return m(), w(T, null, [
210
+ n[0] || (n[0] = b("span", {
211
+ class: "border-s mx-3",
212
+ style: { height: "32px" }
213
+ }, null, -1)),
214
+ b("span", ce, [
215
+ V(o.$slots, "default")
216
+ ])
217
+ ], 64);
218
+ }
219
+ const _e = /* @__PURE__ */ de(me, [["render", fe]]), pe = { class: "d-flex justify-center" }, $e = /* @__PURE__ */ k({
220
+ __name: "LoginForm",
221
+ props: /* @__PURE__ */ C({
222
+ title: { default: "Login" },
223
+ submitButtonText: { default: "Login" },
224
+ minWidth: {},
225
+ maxWidth: {},
226
+ loading: { type: Boolean }
227
+ }, {
228
+ username: { default: "" },
229
+ usernameModifiers: {},
230
+ password: { default: "" },
231
+ passwordModifiers: {}
232
+ }),
233
+ emits: /* @__PURE__ */ C(["submit"], ["update:username", "update:password"]),
234
+ setup(o, { emit: n }) {
235
+ const l = B(o, "username"), r = B(o, "password"), a = n, c = v(!1);
236
+ return (u, i) => (m(), f(t(W), {
237
+ title: u.title,
238
+ "min-width": u.minWidth,
239
+ "max-width": u.maxWidth
240
+ }, {
241
+ default: s(() => [
242
+ d(t(I), {
243
+ onSubmit: i[2] || (i[2] = U((e) => a("submit", l.value, r.value), ["prevent"])),
244
+ modelValue: c.value,
245
+ "onUpdate:modelValue": i[3] || (i[3] = (e) => c.value = e)
246
+ }, {
247
+ default: s(() => [
248
+ d(t(N), null, {
249
+ default: s(() => [
250
+ d(t(L), {
251
+ modelValue: l.value,
252
+ "onUpdate:modelValue": i[0] || (i[0] = (e) => l.value = e),
253
+ label: "Username",
254
+ rules: [(e) => !!e || "Username is required"],
255
+ "persistent-placeholder": "",
256
+ autofocus: ""
257
+ }, null, 8, ["modelValue", "rules"]),
258
+ d(t(L), {
259
+ modelValue: r.value,
260
+ "onUpdate:modelValue": i[1] || (i[1] = (e) => r.value = e),
261
+ label: "Password",
262
+ type: "password",
263
+ rules: [(e) => !!e || "Password is required"],
264
+ "persistent-placeholder": ""
265
+ }, null, 8, ["modelValue", "rules"]),
266
+ b("div", pe, [
267
+ d(t(x), {
268
+ disabled: !c.value,
269
+ type: "submit",
270
+ color: "primary",
271
+ variant: "flat",
272
+ text: u.submitButtonText,
273
+ loading: u.loading
274
+ }, null, 8, ["disabled", "text", "loading"])
275
+ ])
276
+ ]),
277
+ _: 1
278
+ })
279
+ ]),
280
+ _: 1
281
+ }, 8, ["modelValue"])
282
+ ]),
283
+ _: 1
284
+ }, 8, ["title", "min-width", "max-width"]));
285
+ }
286
+ }), ve = { class: "text-no-wrap overflow-hidden" }, Be = /* @__PURE__ */ k({
287
+ __name: "NavDrawerLink",
288
+ props: {
289
+ to: {},
290
+ icon: {},
291
+ text: {}
292
+ },
293
+ setup(o) {
294
+ return (n, l) => (m(), f(t(te), {
295
+ role: "option",
296
+ to: n.to,
297
+ slim: ""
298
+ }, {
299
+ prepend: s(() => [
300
+ d(t(oe), { icon: n.icon }, null, 8, ["icon"])
301
+ ]),
302
+ default: s(() => [
303
+ b("span", ve, M(n.text), 1)
304
+ ]),
305
+ _: 1
306
+ }, 8, ["to"]));
307
+ }
308
+ }), he = { class: "d-flex justify-center" }, Ce = /* @__PURE__ */ k({
309
+ __name: "OTPForm",
310
+ props: /* @__PURE__ */ C({
311
+ title: { default: "2FA Verification" },
312
+ subtitle: { default: "Enter the code from your authenticator app" },
313
+ submitButtonText: { default: "Verify" },
314
+ minWidth: {},
315
+ maxWidth: {},
316
+ loading: { type: Boolean },
317
+ length: { default: 6 },
318
+ autosubmit: { type: Boolean, default: !0 }
319
+ }, {
320
+ otp: { default: "" },
321
+ otpModifiers: {}
322
+ }),
323
+ emits: /* @__PURE__ */ C(["submit"], ["update:otp"]),
324
+ setup(o, { emit: n }) {
325
+ const l = B(o, "otp"), r = n, a = o, c = P(() => l.value.length === a.length);
326
+ return S(l, () => {
327
+ a.autosubmit && l.value.length === a.length && r("submit", l.value);
328
+ }), (u, i) => (m(), f(t(W), {
329
+ title: u.title,
330
+ subtitle: u.subtitle,
331
+ "min-width": u.minWidth,
332
+ "max-width": u.maxWidth
333
+ }, {
334
+ default: s(() => [
335
+ d(t(I), {
336
+ onSubmit: i[1] || (i[1] = U((e) => r("submit", l.value), ["prevent"])),
337
+ "model-value": c.value
338
+ }, {
339
+ default: s(() => [
340
+ d(t(N), null, {
341
+ default: s(() => [
342
+ d(t(le), {
343
+ modelValue: l.value,
344
+ "onUpdate:modelValue": i[0] || (i[0] = (e) => l.value = e),
345
+ label: "OTP",
346
+ "persistent-placeholder": "",
347
+ autofocus: "",
348
+ length: u.length
349
+ }, null, 8, ["modelValue", "length"]),
350
+ b("div", he, [
351
+ d(t(x), {
352
+ disabled: !c.value,
353
+ type: "submit",
354
+ color: "primary",
355
+ variant: "flat",
356
+ text: u.submitButtonText,
357
+ loading: u.loading
358
+ }, null, 8, ["disabled", "text", "loading"])
359
+ ])
360
+ ]),
361
+ _: 1
362
+ })
363
+ ]),
364
+ _: 1
365
+ }, 8, ["model-value"])
366
+ ]),
367
+ _: 1
368
+ }, 8, ["title", "subtitle", "min-width", "max-width"]));
369
+ }
370
+ });
371
+ export {
372
+ xe as AppBase,
373
+ ke as DashboardPage,
374
+ _e as FooterSection,
375
+ $e as LoginForm,
376
+ Be as NavDrawerLink,
377
+ Ce as OTPForm,
378
+ ne as useColorMode,
379
+ re as useFooter,
380
+ ae as useNotify
381
+ };
@@ -0,0 +1 @@
1
+ (function(s,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue"),require("vue-router"),require("vuetify/components"),require("pinia")):typeof define=="function"&&define.amd?define(["exports","vue","vue-router","vuetify/components","pinia"],e):(s=typeof globalThis<"u"?globalThis:s||self,e(s.appsalutely={},s.Vue,s.VueRouter,s.VuetifyComponents,s.Pinia))})(this,function(s,e,y,i,w){"use strict";const k=w.defineStore("colorMode",()=>{const o=e.ref(localStorage.getItem("colorMode")=="dark"?"dark":"light");function l(){o.value=o.value==="dark"?"light":"dark"}function r(n){localStorage.setItem("colorMode",n),document.documentElement.setAttribute("data-theme",n)}return e.watch(o,n=>r(n)),r(o.value),{mode:o,toggle:l}}),b=w.defineStore("footer",()=>{const o=e.ref([]),l=e.computed(()=>o.value.length>0?o.value[o.value.length-1]:null);function r(a){return a.id||(a.id=new Date().getTime().toString()),a.timeout===void 0&&(a.timeout=5e3),o.value.push(a),a.timeout&&setTimeout(()=>{n(a.id)},a.timeout),a.id}function n(a){o.value=o.value.filter(f=>f.id!==a)}return{messages:o,current:l,addMessage:r,removeMessage:n}}),B=w.defineStore("notify",()=>{const o=e.ref([]),l=e.ref({});function r(t){return t.id?n(t.id):t.id=new Date().getTime().toString(),t.timeout===void 0&&(t.timeout=5e3),o.value.push(t),t.timeout&&(l.value[t.id]=setTimeout(()=>{n(t.id)},t.timeout)),t.id}function n(t){o.value=o.value.filter(h=>h.id!==t),l.value[t]&&(clearTimeout(l.value[t]),delete l.value[t])}function a(t){return r({text:t,type:"info"})}function f(t){return r({text:t,type:"success"})}function d(t){return r({text:t,type:"warning"})}function u(t){return r({text:t,type:"error"})}return{messages:o,add:r,remove:n,info:a,success:f,warning:d,error:u}}),g={class:"d-flex flex-column pa-4 h-100"},x=["textContent"],C=e.defineComponent({__name:"AppBase",props:{drawerOpen:{type:Boolean,default:!1},drawerOpenModifiers:{}},emits:["update:drawerOpen"],setup(o){const l=k(),r=e.useModel(o,"drawerOpen"),n=e.ref(!1),a=b(),f=e.ref(!1),d=e.ref(null),u=B(),t=y.useRouter(),h=e.useSlots();return e.watch(n,p=>{localStorage.setItem("drawerDocked",String(p))}),e.onBeforeMount(()=>{t.beforeEach(()=>{d.value=setTimeout(()=>{f.value=!0},300)}),t.afterEach(()=>{d.value&&(clearTimeout(d.value),d.value=null),f.value=!1}),n.value=localStorage.getItem("drawerDocked")==="true",r.value=n.value}),(p,m)=>(e.openBlock(),e.createBlock(e.unref(i.VApp),{theme:e.unref(l).mode},{default:e.withCtx(()=>[f.value?(e.openBlock(),e.createBlock(e.unref(i.VProgressLinear),{key:0,indeterminate:"",class:"mb-n1",style:{"z-index":"100000",opacity:"0.5"},height:"1"})):e.createCommentVNode("",!0),e.createVNode(e.unref(i.VAppBar),{height:"64"},{default:e.withCtx(()=>[e.renderSlot(p.$slots,"header")]),_:3}),e.createVNode(e.unref(i.VMain),{class:"bg-surface-light"},{default:e.withCtx(()=>[e.createElementVNode("div",g,[e.createVNode(e.unref(y.RouterView))])]),_:1}),e.unref(h).footer?(e.openBlock(),e.createBlock(e.unref(i.VAppBar),{key:1,height:"40",location:"bottom"},{default:e.withCtx(()=>[e.createVNode(e.unref(i.VFooter),{class:"py-0 w-100 d-flex align-center"},{default:e.withCtx(()=>[e.unref(a).current?(e.openBlock(),e.createElementBlock("span",{key:0,textContent:e.toDisplayString(e.unref(a).current.text),class:e.normalizeClass([e.unref(a).current.type?{}:{["text-"+e.unref(a).current.type]:!0},"overflow-hidden text-no-wrap"]),style:{"text-overflow":"ellipsis"}},null,10,x)):e.createCommentVNode("",!0),e.unref(a).current&&e.unref(a).current.actions?(e.openBlock(!0),e.createElementBlock(e.Fragment,{key:1},e.renderList(Object.entries(e.unref(a).current.actions),(c,V)=>(e.openBlock(),e.createBlock(e.unref(i.VBtn),{text:c[0],onClick:c[1],key:V,density:"comfortable",variant:"tonal",class:"mx-2 px-2 text-none",color:e.unref(a).current.type},null,8,["text","onClick","color"]))),128)):e.createCommentVNode("",!0),m[2]||(m[2]=e.createElementVNode("div",{class:"me-auto"},null,-1)),e.renderSlot(p.$slots,"footer")]),_:3,__:[2]})]),_:3})):e.createCommentVNode("",!0),e.unref(h).drawer?(e.openBlock(),e.createBlock(e.unref(i.VNavigationDrawer),{key:2,modelValue:r.value,"onUpdate:modelValue":m[1]||(m[1]=c=>r.value=c),width:"250","rail-width":"58",rail:n.value,mobile:n.value?!1:void 0},{default:e.withCtx(()=>[e.createVNode(e.unref(i.VList),{class:"h-100 d-flex flex-column"},{default:e.withCtx(()=>[e.renderSlot(p.$slots,"drawer"),e.createVNode(e.unref(i.VBtn),{onClick:m[0]||(m[0]=c=>n.value=!n.value),icon:"mdi-pin-outline",rounded:"rounded",density:"comfortable",title:n.value?"Undock sidebar":"Dock sidebar",class:"my-1 mx-3 ms-auto",variant:n.value?"tonal":"text"},null,8,["title","variant"])]),_:3})]),_:3},8,["modelValue","rail","mobile"])):e.createCommentVNode("",!0),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(u).messages,(c,V)=>(e.openBlock(),e.createBlock(e.unref(i.VSnackbar),{key:V,"model-value":!0,text:c.text,color:c.type,location:"bottom end",style:e.normalizeStyle({bottom:`${V*56}px`}),timeout:-1},{actions:e.withCtx(()=>[e.createVNode(e.unref(i.VBtn),{icon:"mdi-close",onClick:W=>e.unref(u).remove(c.id)},null,8,["onClick"])]),_:2},1032,["text","color","style"]))),128))]),_:3},8,["theme"]))}}),N=["textContent"],S=e.defineComponent({__name:"DashboardPage",props:{breadcrumbs:{},fluid:{type:Boolean,default:!0},title:{}},setup(o){return(l,r)=>(e.openBlock(),e.createBlock(e.unref(i.VContainer),{fluid:l.fluid,class:"h-100 d-flex flex-column py-0 h-100"},{default:e.withCtx(()=>[l.breadcrumbs?(e.openBlock(),e.createBlock(e.unref(i.VBreadcrumbs),{key:0,items:l.breadcrumbs,density:"comfortable",class:"pa-0"},null,8,["items"])):e.createCommentVNode("",!0),l.title?(e.openBlock(),e.createElementBlock("h1",{key:1,textContent:e.toDisplayString(l.title),class:"text-h5 font-bold ma-1"},null,8,N)):e.createCommentVNode("",!0),e.renderSlot(l.$slots,"default")]),_:3},8,["fluid"]))}}),_=(o,l)=>{const r=o.__vccOpts||o;for(const[n,a]of l)r[n]=a;return r},M={},T={class:"h-100 d-flex align-center"};function $(o,l){return e.openBlock(),e.createElementBlock(e.Fragment,null,[l[0]||(l[0]=e.createElementVNode("span",{class:"border-s mx-3",style:{height:"32px"}},null,-1)),e.createElementVNode("span",T,[e.renderSlot(o.$slots,"default")])],64)}const F=_(M,[["render",$]]),E={class:"d-flex justify-center"},D=e.defineComponent({__name:"LoginForm",props:e.mergeModels({title:{default:"Login"},submitButtonText:{default:"Login"},minWidth:{},maxWidth:{},loading:{type:Boolean}},{username:{default:""},usernameModifiers:{},password:{default:""},passwordModifiers:{}}),emits:e.mergeModels(["submit"],["update:username","update:password"]),setup(o,{emit:l}){const r=e.useModel(o,"username"),n=e.useModel(o,"password"),a=l,f=e.ref(!1);return(d,u)=>(e.openBlock(),e.createBlock(e.unref(i.VCard),{title:d.title,"min-width":d.minWidth,"max-width":d.maxWidth},{default:e.withCtx(()=>[e.createVNode(e.unref(i.VForm),{onSubmit:u[2]||(u[2]=e.withModifiers(t=>a("submit",r.value,n.value),["prevent"])),modelValue:f.value,"onUpdate:modelValue":u[3]||(u[3]=t=>f.value=t)},{default:e.withCtx(()=>[e.createVNode(e.unref(i.VCardText),null,{default:e.withCtx(()=>[e.createVNode(e.unref(i.VTextField),{modelValue:r.value,"onUpdate:modelValue":u[0]||(u[0]=t=>r.value=t),label:"Username",rules:[t=>!!t||"Username is required"],"persistent-placeholder":"",autofocus:""},null,8,["modelValue","rules"]),e.createVNode(e.unref(i.VTextField),{modelValue:n.value,"onUpdate:modelValue":u[1]||(u[1]=t=>n.value=t),label:"Password",type:"password",rules:[t=>!!t||"Password is required"],"persistent-placeholder":""},null,8,["modelValue","rules"]),e.createElementVNode("div",E,[e.createVNode(e.unref(i.VBtn),{disabled:!f.value,type:"submit",color:"primary",variant:"flat",text:d.submitButtonText,loading:d.loading},null,8,["disabled","text","loading"])])]),_:1})]),_:1},8,["modelValue"])]),_:1},8,["title","min-width","max-width"]))}}),O={class:"text-no-wrap overflow-hidden"},L=e.defineComponent({__name:"NavDrawerLink",props:{to:{},icon:{},text:{}},setup(o){return(l,r)=>(e.openBlock(),e.createBlock(e.unref(i.VListItem),{role:"option",to:l.to,slim:""},{prepend:e.withCtx(()=>[e.createVNode(e.unref(i.VIcon),{icon:l.icon},null,8,["icon"])]),default:e.withCtx(()=>[e.createElementVNode("span",O,e.toDisplayString(l.text),1)]),_:1},8,["to"]))}}),P={class:"d-flex justify-center"},U=e.defineComponent({__name:"OTPForm",props:e.mergeModels({title:{default:"2FA Verification"},subtitle:{default:"Enter the code from your authenticator app"},submitButtonText:{default:"Verify"},minWidth:{},maxWidth:{},loading:{type:Boolean},length:{default:6},autosubmit:{type:Boolean,default:!0}},{otp:{default:""},otpModifiers:{}}),emits:e.mergeModels(["submit"],["update:otp"]),setup(o,{emit:l}){const r=e.useModel(o,"otp"),n=l,a=o,f=e.computed(()=>r.value.length===a.length);return e.watch(r,()=>{a.autosubmit&&r.value.length===a.length&&n("submit",r.value)}),(d,u)=>(e.openBlock(),e.createBlock(e.unref(i.VCard),{title:d.title,subtitle:d.subtitle,"min-width":d.minWidth,"max-width":d.maxWidth},{default:e.withCtx(()=>[e.createVNode(e.unref(i.VForm),{onSubmit:u[1]||(u[1]=e.withModifiers(t=>n("submit",r.value),["prevent"])),"model-value":f.value},{default:e.withCtx(()=>[e.createVNode(e.unref(i.VCardText),null,{default:e.withCtx(()=>[e.createVNode(e.unref(i.VOtpInput),{modelValue:r.value,"onUpdate:modelValue":u[0]||(u[0]=t=>r.value=t),label:"OTP","persistent-placeholder":"",autofocus:"",length:d.length},null,8,["modelValue","length"]),e.createElementVNode("div",P,[e.createVNode(e.unref(i.VBtn),{disabled:!f.value,type:"submit",color:"primary",variant:"flat",text:d.submitButtonText,loading:d.loading},null,8,["disabled","text","loading"])])]),_:1})]),_:1},8,["model-value"])]),_:1},8,["title","subtitle","min-width","max-width"]))}});s.AppBase=C,s.DashboardPage=S,s.FooterSection=F,s.LoginForm=D,s.NavDrawerLink=L,s.OTPForm=U,s.useColorMode=k,s.useFooter=b,s.useNotify=B,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,11 @@
1
+ declare const _default: import("pinia").StoreDefinition<"colorMode", Pick<{
2
+ mode: import("vue").Ref<"light" | "dark", "light" | "dark">;
3
+ toggle: () => void;
4
+ }, "mode">, Pick<{
5
+ mode: import("vue").Ref<"light" | "dark", "light" | "dark">;
6
+ toggle: () => void;
7
+ }, never>, Pick<{
8
+ mode: import("vue").Ref<"light" | "dark", "light" | "dark">;
9
+ toggle: () => void;
10
+ }, "toggle">>;
11
+ export default _default;
@@ -0,0 +1,98 @@
1
+ export interface FooterMessage {
2
+ id?: string;
3
+ text: string;
4
+ type?: 'info' | 'success' | 'warning' | 'error';
5
+ timeout?: number;
6
+ actions?: {
7
+ [key: string]: () => void;
8
+ };
9
+ }
10
+ declare const _default: import("pinia").StoreDefinition<"footer", Pick<{
11
+ messages: import("vue").Ref<{
12
+ id?: string | undefined;
13
+ text: string;
14
+ type?: "info" | "success" | "warning" | "error" | undefined;
15
+ timeout?: number | undefined;
16
+ actions?: {
17
+ [key: string]: () => void;
18
+ } | undefined;
19
+ }[], FooterMessage[] | {
20
+ id?: string | undefined;
21
+ text: string;
22
+ type?: "info" | "success" | "warning" | "error" | undefined;
23
+ timeout?: number | undefined;
24
+ actions?: {
25
+ [key: string]: () => void;
26
+ } | undefined;
27
+ }[]>;
28
+ current: import("vue").ComputedRef<{
29
+ id?: string | undefined;
30
+ text: string;
31
+ type?: "info" | "success" | "warning" | "error" | undefined;
32
+ timeout?: number | undefined;
33
+ actions?: {
34
+ [key: string]: () => void;
35
+ } | undefined;
36
+ } | null>;
37
+ addMessage: (message: FooterMessage) => string;
38
+ removeMessage: (id: string) => void;
39
+ }, "messages">, Pick<{
40
+ messages: import("vue").Ref<{
41
+ id?: string | undefined;
42
+ text: string;
43
+ type?: "info" | "success" | "warning" | "error" | undefined;
44
+ timeout?: number | undefined;
45
+ actions?: {
46
+ [key: string]: () => void;
47
+ } | undefined;
48
+ }[], FooterMessage[] | {
49
+ id?: string | undefined;
50
+ text: string;
51
+ type?: "info" | "success" | "warning" | "error" | undefined;
52
+ timeout?: number | undefined;
53
+ actions?: {
54
+ [key: string]: () => void;
55
+ } | undefined;
56
+ }[]>;
57
+ current: import("vue").ComputedRef<{
58
+ id?: string | undefined;
59
+ text: string;
60
+ type?: "info" | "success" | "warning" | "error" | undefined;
61
+ timeout?: number | undefined;
62
+ actions?: {
63
+ [key: string]: () => void;
64
+ } | undefined;
65
+ } | null>;
66
+ addMessage: (message: FooterMessage) => string;
67
+ removeMessage: (id: string) => void;
68
+ }, "current">, Pick<{
69
+ messages: import("vue").Ref<{
70
+ id?: string | undefined;
71
+ text: string;
72
+ type?: "info" | "success" | "warning" | "error" | undefined;
73
+ timeout?: number | undefined;
74
+ actions?: {
75
+ [key: string]: () => void;
76
+ } | undefined;
77
+ }[], FooterMessage[] | {
78
+ id?: string | undefined;
79
+ text: string;
80
+ type?: "info" | "success" | "warning" | "error" | undefined;
81
+ timeout?: number | undefined;
82
+ actions?: {
83
+ [key: string]: () => void;
84
+ } | undefined;
85
+ }[]>;
86
+ current: import("vue").ComputedRef<{
87
+ id?: string | undefined;
88
+ text: string;
89
+ type?: "info" | "success" | "warning" | "error" | undefined;
90
+ timeout?: number | undefined;
91
+ actions?: {
92
+ [key: string]: () => void;
93
+ } | undefined;
94
+ } | null>;
95
+ addMessage: (message: FooterMessage) => string;
96
+ removeMessage: (id: string) => void;
97
+ }, "addMessage" | "removeMessage">>;
98
+ export default _default;
@@ -0,0 +1,62 @@
1
+ export interface NotifyMessage {
2
+ id?: string;
3
+ text: string;
4
+ type?: 'info' | 'success' | 'warning' | 'error';
5
+ timeout?: number;
6
+ }
7
+ declare const _default: import("pinia").StoreDefinition<"notify", Pick<{
8
+ messages: import("vue").Ref<{
9
+ id?: string | undefined;
10
+ text: string;
11
+ type?: "info" | "success" | "warning" | "error" | undefined;
12
+ timeout?: number | undefined;
13
+ }[], NotifyMessage[] | {
14
+ id?: string | undefined;
15
+ text: string;
16
+ type?: "info" | "success" | "warning" | "error" | undefined;
17
+ timeout?: number | undefined;
18
+ }[]>;
19
+ add: (message: NotifyMessage) => string;
20
+ remove: (id: string) => void;
21
+ info: (text: string) => string;
22
+ success: (text: string) => string;
23
+ warning: (text: string) => string;
24
+ error: (text: string) => string;
25
+ }, "messages">, Pick<{
26
+ messages: import("vue").Ref<{
27
+ id?: string | undefined;
28
+ text: string;
29
+ type?: "info" | "success" | "warning" | "error" | undefined;
30
+ timeout?: number | undefined;
31
+ }[], NotifyMessage[] | {
32
+ id?: string | undefined;
33
+ text: string;
34
+ type?: "info" | "success" | "warning" | "error" | undefined;
35
+ timeout?: number | undefined;
36
+ }[]>;
37
+ add: (message: NotifyMessage) => string;
38
+ remove: (id: string) => void;
39
+ info: (text: string) => string;
40
+ success: (text: string) => string;
41
+ warning: (text: string) => string;
42
+ error: (text: string) => string;
43
+ }, never>, Pick<{
44
+ messages: import("vue").Ref<{
45
+ id?: string | undefined;
46
+ text: string;
47
+ type?: "info" | "success" | "warning" | "error" | undefined;
48
+ timeout?: number | undefined;
49
+ }[], NotifyMessage[] | {
50
+ id?: string | undefined;
51
+ text: string;
52
+ type?: "info" | "success" | "warning" | "error" | undefined;
53
+ timeout?: number | undefined;
54
+ }[]>;
55
+ add: (message: NotifyMessage) => string;
56
+ remove: (id: string) => void;
57
+ info: (text: string) => string;
58
+ success: (text: string) => string;
59
+ warning: (text: string) => string;
60
+ error: (text: string) => string;
61
+ }, "info" | "success" | "warning" | "error" | "add" | "remove">>;
62
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "appsalutely",
3
- "version": "0.1.0",
3
+ "description": "A utility library for building applications with FastAPI and Vue.js",
4
+ "version": "0.1.2",
4
5
  "type": "module",
5
6
  "module": "./dist/index.js",
6
7
  "main": "./dist/index.umd.js",
@@ -0,0 +1,3 @@
1
+ import { type UserConfig } from 'vite';
2
+ declare const _default: UserConfig;
3
+ export default _default;
package/vite.config.js ADDED
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from 'vite';
2
+ import Vue from '@vitejs/plugin-vue';
3
+ var config = {
4
+ build: {
5
+ lib: {
6
+ entry: 'src/index.ts',
7
+ name: 'appsalutely',
8
+ fileName: 'index',
9
+ },
10
+ rollupOptions: {
11
+ external: ['vue', 'vue-router', 'pinia', 'vuetify', 'vuetify/components'],
12
+ output: {
13
+ globals: {
14
+ vue: 'Vue',
15
+ 'vue-router': 'VueRouter',
16
+ pinia: 'Pinia',
17
+ vuetify: 'Vuetify',
18
+ 'vuetify/components': 'VuetifyComponents',
19
+ },
20
+ },
21
+ },
22
+ },
23
+ plugins: [Vue()],
24
+ };
25
+ export default defineConfig(config);
@@ -1,139 +0,0 @@
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>
@@ -1,22 +0,0 @@
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>
@@ -1,6 +0,0 @@
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>
@@ -1,56 +0,0 @@
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>
@@ -1,18 +0,0 @@
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>
@@ -1,54 +0,0 @@
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 DELETED
@@ -1,22 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- })
@@ -1,41 +0,0 @@
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
- })
@@ -1,51 +0,0 @@
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
- })