create-nexwire 4.2.3 → 4.2.5
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/express/.vscode/extensions.json +6 -0
- package/express/.vscode/settings.json +37 -26
- package/express/biome.json +99 -66
- package/express/package.json +54 -27
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -60
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/database/schema.ts +0 -1
- package/express/src/framework/facade.ts +12 -1
- package/express/src/framework/maker-cli/db/core.mjs +291 -288
- package/express/src/framework/maker-cli/module/core.mjs +125 -87
- package/express/src/framework/maker-cli/module/index.mjs +3 -3
- package/express/src/framework/maker-cli/stubs/notification/controller.ts.stub +61 -62
- package/express/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -13
- package/express/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/express/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
- package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
- package/express/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
- package/express/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
- package/express/src/framework/notification/index.ts +49 -87
- package/express/src/framework/providers/adapter.ts +24 -10
- package/express/src/framework/providers/contracts.ts +80 -10
- package/express/src/framework/providers/registry.ts +27 -1
- package/express/src/resources/src/components/Checkbox.vue +32 -29
- package/express/src/resources/src/components/Input.vue +243 -223
- package/express/src/resources/src/components/InputGroup.vue +147 -145
- package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
- package/express/src/resources/src/components/Modal.vue +71 -55
- package/express/src/resources/src/components/Refresh.vue +19 -0
- package/express/src/resources/src/helpers/assets.ts +51 -0
- package/express/src/resources/src/main.ts +4 -2
- package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -1
- package/express/src/resources/src/pages/auth/login.vue +1 -1
- package/express/src/resources/src/pages/auth/register.vue +1 -1
- package/express/src/resources/src/pages/auth/resetPassword.vue +1 -1
- package/express/src/resources/src/pages/auth/verifyEmail.vue +1 -1
- package/express/src/resources/src/pages/dashboard/index.vue +66 -66
- package/express/src/resources/src/plugins/gum.ts +1 -1
- package/express/src/resources/src/stores/auth.ts +184 -164
- package/express/src/resources/vite.config.ts +9 -2
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/hono/.vscode/extensions.json +6 -0
- package/hono/.vscode/settings.json +37 -26
- package/hono/biome.json +25 -2
- package/hono/package.json +50 -23
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -60
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -89
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/database/schema.ts +0 -1
- package/hono/src/framework/facade.ts +12 -1
- package/hono/src/framework/maker-cli/db/core.mjs +291 -288
- package/hono/src/framework/maker-cli/module/core.mjs +93 -52
- package/hono/src/framework/maker-cli/module/index.mjs +3 -3
- package/hono/src/framework/maker-cli/stubs/notification/controller.ts.stub +63 -58
- package/hono/src/framework/maker-cli/stubs/notification/job.ts.stub +10 -12
- package/hono/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/hono/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +11 -10
- package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +1 -1
- package/hono/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +25 -25
- package/hono/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -25
- package/hono/src/framework/notification/index.ts +49 -87
- package/hono/src/framework/providers/adapter.ts +24 -10
- package/hono/src/framework/providers/contracts.ts +80 -10
- package/hono/src/framework/providers/registry.ts +27 -1
- package/hono/src/resources/src/components/Checkbox.vue +32 -29
- package/hono/src/resources/src/components/Input.vue +243 -234
- package/hono/src/resources/src/components/InputGroup.vue +147 -149
- package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
- package/hono/src/resources/src/components/Modal.vue +71 -55
- package/hono/src/resources/src/components/Refresh.vue +19 -0
- package/hono/src/resources/src/helpers/assets.ts +51 -0
- package/hono/src/resources/src/main.ts +4 -2
- package/hono/src/resources/src/pages/auth/forgetPassword.vue +1 -1
- package/hono/src/resources/src/pages/auth/login.vue +1 -1
- package/hono/src/resources/src/pages/auth/register.vue +1 -1
- package/hono/src/resources/src/pages/auth/resetPassword.vue +1 -1
- package/hono/src/resources/src/pages/auth/verifyEmail.vue +1 -1
- package/hono/src/resources/src/pages/dashboard/index.vue +66 -66
- package/hono/src/resources/src/plugins/gum.ts +1 -1
- package/hono/src/resources/src/stores/auth.ts +184 -164
- package/hono/src/resources/vite.config.ts +9 -2
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +10 -10
- package/express/src/modules/auth/database/models/notification.ts +0 -24
- package/hono/src/modules/auth/database/models/notification.ts +0 -24
|
@@ -1,213 +1,229 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div :class="topclass">
|
|
3
|
-
<div class="position-relative">
|
|
4
|
-
<label :for="inputId" class="d-flex align-items-center" :class="{ 'd-none': !inputLabel }">
|
|
5
|
-
<div class="position-relative">
|
|
6
|
-
<span class="text-capitalize mb-1" v-html="inputLabel"></span>
|
|
7
|
-
<span
|
|
8
|
-
class="position-absolute text-danger rounded-circle bg-danger must"
|
|
9
|
-
:class="{ 'd-none': !props.must }"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
:
|
|
25
|
-
:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
val =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
emit("update:modelValue", (target.value =
|
|
165
|
-
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="topclass">
|
|
3
|
+
<div class="position-relative">
|
|
4
|
+
<label :for="inputId" class="d-flex align-items-center" :class="{ 'd-none': !inputLabel }">
|
|
5
|
+
<div class="position-relative">
|
|
6
|
+
<span class="text-capitalize mb-1" v-html="inputLabel"></span>
|
|
7
|
+
<span
|
|
8
|
+
class="position-absolute text-danger rounded-circle bg-danger must"
|
|
9
|
+
:class="{ 'd-none': !props.must }"
|
|
10
|
+
></span>
|
|
11
|
+
</div>
|
|
12
|
+
<div
|
|
13
|
+
class="text-end ms-auto text-primary align-self-center"
|
|
14
|
+
:class="{ 'd-none': !props.hood }"
|
|
15
|
+
style="font-size: 12px; margin-top: 0.15rem"
|
|
16
|
+
v-html="hoodHtml"
|
|
17
|
+
></div>
|
|
18
|
+
</label>
|
|
19
|
+
|
|
20
|
+
<div class="input-group border-0 mb-3">
|
|
21
|
+
<input
|
|
22
|
+
ref="inputRef"
|
|
23
|
+
class="form-control shadow-none"
|
|
24
|
+
:type="showPassword ? 'text' : 'password'"
|
|
25
|
+
:value="props.modelValue"
|
|
26
|
+
:maxlength="maxLengthAttr"
|
|
27
|
+
:readonly="props.readonly"
|
|
28
|
+
v-bind="$attrs"
|
|
29
|
+
@input="updateModel"
|
|
30
|
+
/>
|
|
31
|
+
<button
|
|
32
|
+
class="input-group-text p-0 position-relative z-1"
|
|
33
|
+
type="button"
|
|
34
|
+
@click="togglePassword"
|
|
35
|
+
>
|
|
36
|
+
<i
|
|
37
|
+
class="py-2 px-3"
|
|
38
|
+
:class="showPassword ? 'bi bi-eye' : 'bi bi-eye-slash'"
|
|
39
|
+
style="font-size: 15px"
|
|
40
|
+
></i>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
<!-- <input class="form-control" v-model="model" :maxlength="max" v-bind="$attrs"> vue3.3 (2nd way)/3.4 -->
|
|
44
|
+
<div
|
|
45
|
+
class="position-absolute bottom-0 end-0 pe-1 text-secondary max"
|
|
46
|
+
:class="{ 'd-none': !max }"
|
|
47
|
+
>
|
|
48
|
+
{{ maxCounter }}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div
|
|
52
|
+
:id="`${inputId}Help`"
|
|
53
|
+
class="form-text mt-1 text-danger"
|
|
54
|
+
:class="{ 'd-none': !props.err }"
|
|
55
|
+
>
|
|
56
|
+
{{ props.err }}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import { computed, nextTick, onMounted, ref, useAttrs } from "vue";
|
|
63
|
+
import { browserDetect } from "@/plugins/browserDetect";
|
|
64
|
+
|
|
65
|
+
defineOptions({ name: "InputPasswordToggle", inheritAttrs: false });
|
|
66
|
+
const { isFirefox } = browserDetect;
|
|
67
|
+
|
|
68
|
+
const $attrs = useAttrs();
|
|
69
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
70
|
+
type InputPasswordCategory =
|
|
71
|
+
| "number"
|
|
72
|
+
| "decimal"
|
|
73
|
+
| "mobile"
|
|
74
|
+
| "tax-period"
|
|
75
|
+
| "bin"
|
|
76
|
+
| "challan-number"
|
|
77
|
+
| string;
|
|
78
|
+
|
|
79
|
+
interface InputPasswordProps {
|
|
80
|
+
focus?: boolean;
|
|
81
|
+
must?: boolean;
|
|
82
|
+
err?: string | boolean;
|
|
83
|
+
modelValue?: string | number;
|
|
84
|
+
category?: InputPasswordCategory;
|
|
85
|
+
hood?: string | boolean | number;
|
|
86
|
+
readonly?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const props = withDefaults(defineProps<InputPasswordProps>(), {
|
|
90
|
+
err: false,
|
|
91
|
+
modelValue: "",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const topclass = computed(() => ($attrs.topclass as string | undefined) || "mb-3");
|
|
95
|
+
const inputId = computed(() => ($attrs.id as string | undefined) || "");
|
|
96
|
+
const inputLabel = computed(() => ($attrs.label as string | undefined) || "");
|
|
97
|
+
const inputType = computed(() => ($attrs.type as string | undefined) || "text");
|
|
98
|
+
|
|
99
|
+
const max = computed<number | null>(() => {
|
|
100
|
+
const raw = $attrs.maxlength as string | number | undefined;
|
|
101
|
+
return raw != null ? Number(raw) : null;
|
|
102
|
+
});
|
|
103
|
+
const maxLengthAttr = computed<number | undefined>(() => max.value ?? undefined);
|
|
104
|
+
|
|
105
|
+
const hoodHtml = computed(() =>
|
|
106
|
+
props.hood === false || props.hood == null ? "" : String(props.hood),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const maxCounter = computed(() => {
|
|
110
|
+
if (max.value == null) {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isFirefox && inputType.value === "month") {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return max.value - String(props.modelValue).length;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const emit = defineEmits<(event: "update:modelValue", value: string | number) => void>();
|
|
122
|
+
const updateModel = (e: Event) => {
|
|
123
|
+
const target = e.target as HTMLInputElement;
|
|
124
|
+
|
|
125
|
+
if (props.category === "number") {
|
|
126
|
+
emit("update:modelValue", (target.value = target.value.replace(/[^\d]/gi, "")));
|
|
127
|
+
} else if (props.category === "decimal") {
|
|
128
|
+
let val = target.value.replace(/[^\d.]/g, "");
|
|
129
|
+
const parts = val.split(".");
|
|
130
|
+
val = parts[0];
|
|
131
|
+
if (parts.length > 1) {
|
|
132
|
+
val += `.${parts.slice(1).join("")}`;
|
|
133
|
+
}
|
|
134
|
+
emit("update:modelValue", (target.value = val));
|
|
135
|
+
} else if (props.category === "mobile") {
|
|
136
|
+
let digits = target.value.replace(/[^\d]/g, ""); // only digits
|
|
137
|
+
let val = digits;
|
|
138
|
+
if (target.value.includes("+")) {
|
|
139
|
+
val = `+${digits}`;
|
|
140
|
+
}
|
|
141
|
+
emit("update:modelValue", (target.value = val));
|
|
142
|
+
} else if (props.category === "tax-period") {
|
|
143
|
+
let val = target.value.replace(/[^\d-]/g, ""); // Only numbers and hyphen
|
|
144
|
+
val = val.replace(/-/g, ""); // Remove all hyphens
|
|
145
|
+
val = val.slice(0, 6); // Only 6 digits (YYYYMM)
|
|
146
|
+
|
|
147
|
+
if (val.length > 4) {
|
|
148
|
+
let year = val.slice(0, 4);
|
|
149
|
+
let month = val.slice(4, 6);
|
|
150
|
+
|
|
151
|
+
// Only correct if both month digits entered
|
|
152
|
+
if (month.length === 2) {
|
|
153
|
+
let m = parseInt(month, 10);
|
|
154
|
+
// Clamp month between 1 and 12
|
|
155
|
+
if (m < 1) m = 1;
|
|
156
|
+
if (m > 12) m = 12;
|
|
157
|
+
// Always pad to 2 digits
|
|
158
|
+
month = m.toString().padStart(2, "0");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
val = `${year}-${month}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
emit("update:modelValue", (target.value = val));
|
|
165
|
+
} else if (props.category === "bin") {
|
|
166
|
+
let digits = target.value.replace(/[^\d]/g, "");
|
|
167
|
+
digits = digits.slice(0, 14);
|
|
168
|
+
let formatted = digits;
|
|
169
|
+
if (digits.length > 9) {
|
|
170
|
+
formatted = `${digits.slice(0, 9)}-${digits.slice(9, 14)}`;
|
|
171
|
+
}
|
|
172
|
+
emit("update:modelValue", (target.value = formatted));
|
|
173
|
+
} else if (props.category === "challan-number") {
|
|
174
|
+
let digits = target.value.replace(/[^\d]/g, "");
|
|
175
|
+
digits = digits.slice(0, 16);
|
|
176
|
+
let formatted = digits;
|
|
177
|
+
if (digits.length > 4) {
|
|
178
|
+
formatted = `${digits.slice(0, 4)}-${digits.slice(4, 16)}`;
|
|
179
|
+
}
|
|
180
|
+
emit("update:modelValue", (target.value = formatted));
|
|
181
|
+
} else {
|
|
182
|
+
emit("update:modelValue", target.value);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const showPassword = ref(false);
|
|
187
|
+
const togglePassword = () => (showPassword.value = !showPassword.value);
|
|
188
|
+
|
|
189
|
+
onMounted(async () => {
|
|
190
|
+
const shouldFocus = props.focus !== false && props.focus !== undefined;
|
|
191
|
+
if (!shouldFocus) return;
|
|
192
|
+
|
|
193
|
+
await nextTick();
|
|
194
|
+
// preventScroll: focusing inside a modal must not scroll the modal/page
|
|
195
|
+
inputRef.value?.focus({ preventScroll: true });
|
|
196
|
+
});
|
|
197
|
+
</script>
|
|
198
|
+
|
|
199
|
+
<style lang="scss" scoped>
|
|
200
|
+
input {
|
|
201
|
+
&:read-only {
|
|
202
|
+
background-color: #f8f8f8;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
&::placeholder {
|
|
206
|
+
color: #bfbfbf;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
button:focus-visible {
|
|
211
|
+
border: none;
|
|
212
|
+
box-shadow: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.form-text {
|
|
216
|
+
font-size: 0.75rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.must {
|
|
220
|
+
width: 4px;
|
|
221
|
+
height: 4px;
|
|
222
|
+
margin-left: 2px;
|
|
223
|
+
margin-top: 5px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.max {
|
|
227
|
+
font-size: 0.6rem;
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
@@ -1,55 +1,71 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Teleport to="#modal-show">
|
|
3
|
-
<div
|
|
4
|
-
:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="#modal-show">
|
|
3
|
+
<div
|
|
4
|
+
:id="props.id"
|
|
5
|
+
ref="modal"
|
|
6
|
+
class="modal fade"
|
|
7
|
+
data-bs-backdrop="static"
|
|
8
|
+
data-bs-keyboard="false"
|
|
9
|
+
tabindex="-1"
|
|
10
|
+
:aria-labelledby="`${props.id}Label`"
|
|
11
|
+
>
|
|
12
|
+
<div
|
|
13
|
+
:class="`modal-dialog modal-dialog-centered modal-dialog-scrollable modal-${props.size}`"
|
|
14
|
+
>
|
|
15
|
+
<div class="modal-content border">
|
|
16
|
+
<div class="modal-header">
|
|
17
|
+
<div class="d-flex w-100 justify-content-between align-items-center">
|
|
18
|
+
<h5 id="modalScrollableTitle" class="modal-title me-3">{{ props.title }}</h5>
|
|
19
|
+
<button
|
|
20
|
+
ref="closeButton"
|
|
21
|
+
type="button"
|
|
22
|
+
class="btn-close shadow-none"
|
|
23
|
+
data-bs-dismiss="modal"
|
|
24
|
+
aria-label="Close"
|
|
25
|
+
></button>
|
|
26
|
+
<slot name="modal-header-extra"></slot>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="modal-body">
|
|
30
|
+
<slot name="modalbody"></slot>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="modal-footer py-2">
|
|
33
|
+
<slot name="modalfooter"></slot>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</Teleport>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import { Modal } from "bootstrap";
|
|
43
|
+
import { onMounted, ref } from "vue";
|
|
44
|
+
|
|
45
|
+
interface ModalProps {
|
|
46
|
+
id: string;
|
|
47
|
+
title?: string;
|
|
48
|
+
size?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const props = withDefaults(defineProps<ModalProps>(), {
|
|
52
|
+
title: "",
|
|
53
|
+
size: "md"
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const modal = ref<HTMLElement | null>(null);
|
|
57
|
+
let bsModal: Modal | null = null;
|
|
58
|
+
|
|
59
|
+
onMounted(() => {
|
|
60
|
+
if (modal.value) {
|
|
61
|
+
bsModal = new Modal(modal.value);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const open = () => bsModal?.show();
|
|
66
|
+
const close = () => bsModal?.hide();
|
|
67
|
+
|
|
68
|
+
defineExpose({ open, close });
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<style lang="scss" scoped></style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport v-if="target" :to="target">
|
|
3
|
+
<slot />
|
|
4
|
+
</Teleport>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import { onMounted, ref } from "vue";
|
|
9
|
+
|
|
10
|
+
defineOptions({ name: "Refresh", inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
const target = ref(null);
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
// grab the header container once it's in the DOM
|
|
15
|
+
target.value = document.getElementById("refresh");
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<style lang="scss" scoped></style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/helpers/assets.ts
|
|
2
|
+
|
|
3
|
+
// 1. Eagerly load ALL images in the entire src folder.
|
|
4
|
+
// This tells Vite: "Find every image, hash it, and give me the final URL."
|
|
5
|
+
const allImages = import.meta.glob("/src/**/*.{png,jpg,jpeg,svg,webp,gif}", {
|
|
6
|
+
eager: true,
|
|
7
|
+
import: "default"
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves ANY image path (using @, relative, or absolute) for both dev and prod.
|
|
12
|
+
*/
|
|
13
|
+
export default function asset(path: string): string {
|
|
14
|
+
// 1. If it's already a full URL or data URI, return it as is.
|
|
15
|
+
if (path.startsWith("http") || path.startsWith("data:")) {
|
|
16
|
+
return path;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Normalize the path.
|
|
20
|
+
// Vite's glob keys always start from the root, like "/src/assets/..."
|
|
21
|
+
let cleanPath = path;
|
|
22
|
+
|
|
23
|
+
// Convert alias `@/` to `/src/`
|
|
24
|
+
if (cleanPath.startsWith("@/")) {
|
|
25
|
+
cleanPath = cleanPath.replace("@/", "/src/");
|
|
26
|
+
}
|
|
27
|
+
// Convert absolute `/` to `/src/` (assuming you mean inside src)
|
|
28
|
+
else if (cleanPath.startsWith("/") && !cleanPath.startsWith("/src/")) {
|
|
29
|
+
cleanPath = `/src${cleanPath}`;
|
|
30
|
+
}
|
|
31
|
+
// Convert relative `../../` (This is the tricky part)
|
|
32
|
+
else if (cleanPath.startsWith("../")) {
|
|
33
|
+
// We can't easily resolve `../../` from a utility file, so we just
|
|
34
|
+
// try to find a matching suffix in the glob keys.
|
|
35
|
+
// (e.g., if you pass "../../assets/foo.png", we look for "assets/foo.png" inside /src)
|
|
36
|
+
const match = Object.keys(allImages).find((key) => key.endsWith(cleanPath.replace(/^(\.\.\/)+/, "")));
|
|
37
|
+
if (match) {
|
|
38
|
+
return allImages[match] as string;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Look up the normalized path in the glob object
|
|
43
|
+
const resolved = allImages[cleanPath] as string | undefined;
|
|
44
|
+
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
console.warn(`[getAssetUrl] Image not found: ${path}. Returning original path.`);
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|