@necrolab/dashboard 0.5.16 → 0.5.18
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/package.json +1 -1
- package/src/App.vue +14 -480
- package/src/assets/css/components/buttons.scss +12 -68
- package/src/assets/css/components/headers.scss +1 -1
- package/src/assets/css/components/utilities.scss +91 -16
- package/src/assets/css/main.scss +22 -95
- package/src/components/Auth/LoginForm.vue +2 -2
- package/src/components/Console/ConsoleToolbar.vue +123 -0
- package/src/components/Editors/Account/Account.vue +4 -2
- package/src/components/Editors/Account/AccountView.vue +12 -37
- package/src/components/Editors/Account/CreateAccount.vue +3 -11
- package/src/components/Editors/AdminFileEditor.vue +421 -0
- package/src/components/Editors/Profile/CreateProfile.vue +4 -20
- package/src/components/Editors/Profile/Profile.vue +5 -4
- package/src/components/Editors/Profile/ProfileView.vue +13 -38
- package/src/components/Editors/ProxyFileEditor.vue +178 -0
- package/src/components/Filter/Filter.vue +6 -6
- package/src/components/Filter/FilterPreview.vue +4 -12
- package/src/components/Filter/PriceSortToggle.vue +1 -1
- package/src/components/Tasks/QuickSettings.vue +5 -5
- package/src/components/Tasks/Stats.vue +1 -1
- package/src/components/Tasks/Task.vue +5 -8
- package/src/components/Tasks/TaskView.vue +2 -1
- package/src/components/Tasks/ViewTask.vue +2 -2
- package/src/components/icons/index.js +0 -4
- package/src/components/ui/ActionButtonGroup.vue +2 -2
- package/src/components/ui/BalanceIndicator.vue +3 -3
- package/src/components/ui/EnableDisableToggle.vue +2 -2
- package/src/components/ui/FormField.vue +2 -2
- package/src/components/ui/IconLabel.vue +2 -2
- package/src/components/ui/InfoRow.vue +4 -4
- package/src/components/ui/Modal.vue +83 -9
- package/src/components/ui/Navbar.vue +3 -3
- package/src/components/ui/ReadonlyFieldsSection.vue +1 -1
- package/src/components/ui/StatusBadge.vue +1 -1
- package/src/components/ui/controls/CountryChooser.vue +5 -5
- package/src/components/ui/controls/atomic/MultiDropdown.vue +1 -1
- package/src/composables/useCodeEditor.js +117 -0
- package/src/composables/useDropdownPosition.js +0 -2
- package/src/composables/useEnableDisable.js +6 -0
- package/src/composables/useFilterCSS.js +71 -0
- package/src/composables/useFormValidation.js +92 -0
- package/src/composables/useGetAllTags.js +9 -0
- package/src/composables/useIOSViewportHandling.js +76 -0
- package/src/composables/useNotchHandling.js +306 -0
- package/src/composables/useTableRender.js +23 -0
- package/src/composables/useZoomPrevention.js +96 -0
- package/src/constants/tableLayout.js +14 -0
- package/src/libs/utils/array.js +1 -3
- package/src/libs/utils/dataGeneration.js +1 -1
- package/src/libs/utils/string.js +1 -26
- package/src/libs/utils/validation.js +2 -26
- package/src/stores/connection.js +0 -25
- package/src/stores/ui.js +21 -35
- package/src/utils/tableHelpers.js +1 -0
- package/src/views/Accounts.vue +9 -17
- package/src/views/Console.vue +15 -92
- package/src/views/Editor.vue +39 -938
- package/src/views/FilterBuilder.vue +9 -97
- package/src/views/Profiles.vue +9 -17
- package/src/views/Tasks.vue +4 -4
- package/src/assets/img/background.svg.backup +0 -11
- package/src/components/icons/SquareCheck.vue +0 -12
- package/src/components/icons/SquareUncheck.vue +0 -12
- package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
- /package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +0 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="modal-mask"
|
|
4
|
+
:class="{ 'fade-in': !isClosing, 'fade-out': isClosing }"
|
|
5
|
+
role="dialog"
|
|
6
|
+
aria-modal="true"
|
|
7
|
+
@touchmove.stop>
|
|
8
|
+
<div
|
|
9
|
+
class="modal-content"
|
|
10
|
+
:class="{ 'modal-slide-in': !isClosing, 'modal-slide-out': isClosing }"
|
|
11
|
+
ref="target">
|
|
4
12
|
<div class="modal-header">
|
|
5
13
|
<slot name="header" />
|
|
6
|
-
<button @click="
|
|
14
|
+
<button @click="closeModal" class="btn-icon border-none hover:bg-dark-400 ml-auto" aria-label="Close modal">
|
|
7
15
|
<CloseIcon />
|
|
8
16
|
</button>
|
|
9
17
|
</div>
|
|
@@ -21,6 +29,15 @@ import { CloseIcon } from "@/components/icons";
|
|
|
21
29
|
|
|
22
30
|
const ui = useUIStore();
|
|
23
31
|
const target = ref(null);
|
|
32
|
+
const isClosing = ref(false);
|
|
33
|
+
|
|
34
|
+
const closeModal = () => {
|
|
35
|
+
isClosing.value = true;
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
ui.toggleModal();
|
|
38
|
+
isClosing.value = false; // Reset for next modal
|
|
39
|
+
}, 150); // Match the animation duration
|
|
40
|
+
};
|
|
24
41
|
|
|
25
42
|
// Store original body styles
|
|
26
43
|
let originalOverflow = "";
|
|
@@ -29,7 +46,7 @@ let originalTop = "";
|
|
|
29
46
|
let scrollY = 0;
|
|
30
47
|
|
|
31
48
|
onMounted(() => {
|
|
32
|
-
//
|
|
49
|
+
isClosing.value = false; // Ensure clean state on mount
|
|
33
50
|
scrollY = window.scrollY;
|
|
34
51
|
originalOverflow = document.body.style.overflow;
|
|
35
52
|
originalPosition = document.body.style.position;
|
|
@@ -42,7 +59,6 @@ onMounted(() => {
|
|
|
42
59
|
});
|
|
43
60
|
|
|
44
61
|
onUnmounted(() => {
|
|
45
|
-
// Restore body scroll
|
|
46
62
|
document.body.style.overflow = originalOverflow;
|
|
47
63
|
document.body.style.position = originalPosition;
|
|
48
64
|
document.body.style.top = originalTop;
|
|
@@ -51,7 +67,7 @@ onUnmounted(() => {
|
|
|
51
67
|
});
|
|
52
68
|
|
|
53
69
|
onClickOutside(target, (event) => {
|
|
54
|
-
if (event.target.classList.contains("modal-mask"))
|
|
70
|
+
if (event.target.classList.contains("modal-mask")) closeModal();
|
|
55
71
|
});
|
|
56
72
|
</script>
|
|
57
73
|
<style scoped>
|
|
@@ -64,7 +80,7 @@ onClickOutside(target, (event) => {
|
|
|
64
80
|
@apply z-modal-mask;
|
|
65
81
|
|
|
66
82
|
/* Background with opacity */
|
|
67
|
-
background-color:
|
|
83
|
+
background-color: oklch(0 0 0 / 0.85);
|
|
68
84
|
backdrop-filter: blur(8px);
|
|
69
85
|
|
|
70
86
|
/* Height with modern viewport units and fallback */
|
|
@@ -108,8 +124,8 @@ onClickOutside(target, (event) => {
|
|
|
108
124
|
@apply bg-dark-400 px-5 py-5;
|
|
109
125
|
@apply w-160 mb-80;
|
|
110
126
|
@apply overflow-y-visible;
|
|
111
|
-
box-shadow: 0 25px 50px -12px
|
|
112
|
-
0 0 0 1px
|
|
127
|
+
box-shadow: 0 25px 50px -12px oklch(0 0 0 / 0.8),
|
|
128
|
+
0 0 0 1px oklch(1 0 0 / 0.05);
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
/* Mobile breakpoint for modal-content */
|
|
@@ -156,4 +172,62 @@ onClickOutside(target, (event) => {
|
|
|
156
172
|
padding-bottom: 1rem !important;
|
|
157
173
|
}
|
|
158
174
|
}
|
|
175
|
+
|
|
176
|
+
/* Modal animations */
|
|
177
|
+
.fade-in {
|
|
178
|
+
animation: fadeIn 0.15s ease-out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.modal-slide-in {
|
|
182
|
+
animation: slideIn 0.15s ease-out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@keyframes fadeIn {
|
|
186
|
+
from {
|
|
187
|
+
opacity: 0;
|
|
188
|
+
}
|
|
189
|
+
to {
|
|
190
|
+
opacity: 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@keyframes slideIn {
|
|
195
|
+
from {
|
|
196
|
+
opacity: 0;
|
|
197
|
+
transform: translateY(-20px) scale(0.95);
|
|
198
|
+
}
|
|
199
|
+
to {
|
|
200
|
+
opacity: 1;
|
|
201
|
+
transform: translateY(0) scale(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Exit animations */
|
|
206
|
+
.fade-out {
|
|
207
|
+
animation: fadeOut 0.15s ease-in forwards;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.modal-slide-out {
|
|
211
|
+
animation: slideOut 0.15s ease-in forwards;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@keyframes fadeOut {
|
|
215
|
+
from {
|
|
216
|
+
opacity: 1;
|
|
217
|
+
}
|
|
218
|
+
to {
|
|
219
|
+
opacity: 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@keyframes slideOut {
|
|
224
|
+
from {
|
|
225
|
+
opacity: 1;
|
|
226
|
+
transform: translateY(0) scale(1);
|
|
227
|
+
}
|
|
228
|
+
to {
|
|
229
|
+
opacity: 0;
|
|
230
|
+
transform: translateY(-10px) scale(0.98);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
159
233
|
</style>
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
v-if="ui.profile?.profilePicture"
|
|
46
46
|
:src="ui.profile?.profilePicture"
|
|
47
47
|
alt="Profile Picture"
|
|
48
|
-
class="
|
|
48
|
+
class="size-10 rounded-full hidden lg:block mx-4"
|
|
49
49
|
/>
|
|
50
|
-
<div v-else class="
|
|
50
|
+
<div v-else class="size-10 rounded-full hidden lg:block mx-4 bg-dark-400" />
|
|
51
51
|
<CountryChooser class="hidden lg:block" />
|
|
52
52
|
|
|
53
53
|
<button class="flex lg:hidden ml-auto z-30" @click="toggleMenu" aria-label="Toggle navigation menu" :aria-expanded="menuOpen">
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
<span class="text-lightgray">Logged in as </span>
|
|
93
93
|
<span class="font-black"> {{ ui.profile?.name }}</span>
|
|
94
94
|
</h4>
|
|
95
|
-
<img :src="ui.profile?.profilePicture" alt="" class="
|
|
95
|
+
<img :src="ui.profile?.profilePicture" alt="" class="size-10 rounded-full" />
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
98
|
<div class="mx-auto mt-6 mb-14 flex gap-3 items-center">
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<div class="col-span-6">
|
|
10
10
|
<label class="flex items-center mb-2 text-light-200 font-medium">Status</label>
|
|
11
|
-
<div class="flex items-center
|
|
11
|
+
<div class="flex-gap-3 items-center h-10">
|
|
12
12
|
<StatusBadge :enabled="data.enabled" size="large" />
|
|
13
13
|
<span class="text-sm font-medium" :class="data.enabled ? 'text-green-400' : 'text-red-400'">
|
|
14
14
|
{{ data.enabled ? 'Enabled' : 'Disabled' }}
|
|
@@ -25,7 +25,7 @@ const disabledClasses = "bg-red-500/20 border-red-500/30";
|
|
|
25
25
|
<template>
|
|
26
26
|
<div
|
|
27
27
|
:class="[
|
|
28
|
-
'flex
|
|
28
|
+
'flex-center rounded-full border-2 transition-all duration-200',
|
|
29
29
|
enabled ? enabledClasses : disabledClasses,
|
|
30
30
|
sizeClasses[size]
|
|
31
31
|
]">
|
|
@@ -115,10 +115,10 @@ const selectCountry = (country, module) => {
|
|
|
115
115
|
@apply border rounded-md tracking-wider;
|
|
116
116
|
@apply flex items-center justify-center;
|
|
117
117
|
@apply relative;
|
|
118
|
-
border-color:
|
|
119
|
-
background: linear-gradient(135deg,
|
|
120
|
-
text-shadow: 0 1px 2px
|
|
121
|
-
box-shadow: 0 2px 4px
|
|
118
|
+
border-color: oklch(0.95 0 0 / 0.1);
|
|
119
|
+
background: linear-gradient(135deg, oklch(0.95 0 0 / 0.08) 0%, oklch(0.95 0 0 / 0.04) 100%);
|
|
120
|
+
text-shadow: 0 1px 2px oklch(0 0 0 / 0.3);
|
|
121
|
+
box-shadow: 0 2px 4px oklch(0 0 0 / 0.1);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
.country-header-item:first-of-type {
|
|
@@ -128,7 +128,7 @@ const selectCountry = (country, module) => {
|
|
|
128
128
|
.country-header-item::before {
|
|
129
129
|
content: '';
|
|
130
130
|
@apply absolute top-0 left-0 right-0 h-px rounded-t-md;
|
|
131
|
-
background: linear-gradient(90deg, transparent 0%,
|
|
131
|
+
background: linear-gradient(90deg, transparent 0%, oklch(0.95 0 0 / 0.2) 50%, transparent 100%);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
.country-item {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
<div v-if="selectedOptions.length > 0" class="selected-summary">
|
|
37
|
-
<div class="flex items-center justify-between
|
|
37
|
+
<div class="flex-gap-2 items-center justify-between">
|
|
38
38
|
<div class="selected-count">
|
|
39
39
|
<span class="count-badge">
|
|
40
40
|
{{ selectedOptions.length }}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { nextTick } from 'vue';
|
|
2
|
+
|
|
3
|
+
export function useCodeEditor(logger) {
|
|
4
|
+
let highlightTimer = null;
|
|
5
|
+
|
|
6
|
+
const highlightCode = (codeDisplay, codeEditor, currentContent, currentFile) => {
|
|
7
|
+
if (!codeDisplay || !codeEditor) return;
|
|
8
|
+
|
|
9
|
+
let language = "javascript";
|
|
10
|
+
|
|
11
|
+
if (currentFile) {
|
|
12
|
+
if (currentFile.endsWith(".json")) {
|
|
13
|
+
language = "json";
|
|
14
|
+
} else if (currentFile.endsWith(".js")) {
|
|
15
|
+
language = "javascript";
|
|
16
|
+
} else if (currentFile.endsWith(".txt") || currentFile.endsWith(".csv")) {
|
|
17
|
+
language = "text";
|
|
18
|
+
if (codeDisplay) {
|
|
19
|
+
codeDisplay.textContent = currentContent || "";
|
|
20
|
+
codeDisplay.className = "language-text code-highlight";
|
|
21
|
+
}
|
|
22
|
+
syncScroll(codeDisplay, codeEditor);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (highlightTimer) clearTimeout(highlightTimer);
|
|
28
|
+
|
|
29
|
+
highlightTimer = setTimeout(() => {
|
|
30
|
+
requestAnimationFrame(() => {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof window.Prism === "undefined" || !window.Prism.languages?.[language]) {
|
|
33
|
+
if (codeDisplay) {
|
|
34
|
+
codeDisplay.textContent = currentContent || "";
|
|
35
|
+
codeDisplay.className = `language-${language} code-highlight`;
|
|
36
|
+
}
|
|
37
|
+
syncScroll(codeDisplay, codeEditor);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const highlighted = window.Prism.highlight(
|
|
42
|
+
currentContent || "",
|
|
43
|
+
window.Prism.languages[language],
|
|
44
|
+
language
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (codeDisplay) {
|
|
48
|
+
codeDisplay.innerHTML = highlighted;
|
|
49
|
+
codeDisplay.className = `language-${language} code-highlight`;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (logger) logger.Error("Highlight error:", e);
|
|
53
|
+
if (codeDisplay) {
|
|
54
|
+
codeDisplay.textContent = currentContent || "";
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
syncScroll(codeDisplay, codeEditor);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}, 50);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const syncScroll = (codeDisplay, codeEditor) => {
|
|
64
|
+
if (!codeDisplay || !codeEditor) return;
|
|
65
|
+
|
|
66
|
+
codeDisplay.scrollTop = codeEditor.scrollTop;
|
|
67
|
+
codeDisplay.scrollLeft = codeEditor.scrollLeft;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleTab = (textarea, contentRef, onContentUpdate) => {
|
|
71
|
+
const start = textarea.selectionStart;
|
|
72
|
+
const end = textarea.selectionEnd;
|
|
73
|
+
|
|
74
|
+
const spaces = " ";
|
|
75
|
+
contentRef.value = contentRef.value.substring(0, start) + spaces + contentRef.value.substring(end);
|
|
76
|
+
|
|
77
|
+
nextTick(() => {
|
|
78
|
+
textarea.selectionStart = textarea.selectionEnd = start + spaces.length;
|
|
79
|
+
if (onContentUpdate) onContentUpdate();
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const formatJSON = (contentRef, currentFile, onContentUpdate) => {
|
|
84
|
+
try {
|
|
85
|
+
if (!currentFile.endsWith(".json")) return;
|
|
86
|
+
|
|
87
|
+
const formatted = JSON.stringify(JSON.parse(contentRef.value), null, 4);
|
|
88
|
+
contentRef.value = formatted;
|
|
89
|
+
|
|
90
|
+
nextTick(() => {
|
|
91
|
+
if (onContentUpdate) onContentUpdate();
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (logger) logger.Error("Could not format JSON", e);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const setLanguageClass = (codeDisplay, fileName) => {
|
|
99
|
+
if (!codeDisplay) return;
|
|
100
|
+
|
|
101
|
+
if (fileName.endsWith(".json")) {
|
|
102
|
+
codeDisplay.className = "language-json";
|
|
103
|
+
} else if (fileName.endsWith(".js")) {
|
|
104
|
+
codeDisplay.className = "language-javascript";
|
|
105
|
+
} else {
|
|
106
|
+
codeDisplay.className = "language-text";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
highlightCode,
|
|
112
|
+
syncScroll,
|
|
113
|
+
handleTab,
|
|
114
|
+
formatJSON,
|
|
115
|
+
setLanguageClass
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ref, nextTick, onMounted, onUnmounted } from "vue";
|
|
2
|
-
import { DEBUG } from "@/utils/debug";
|
|
3
2
|
|
|
4
3
|
export function useDropdownPosition(dropdownRef, options = {}) {
|
|
5
4
|
const menuStyle = ref({});
|
|
@@ -109,7 +108,6 @@ export function useDropdownPosition(dropdownRef, options = {}) {
|
|
|
109
108
|
maxHeight: `${maxHeight}px`
|
|
110
109
|
};
|
|
111
110
|
} catch (error) {
|
|
112
|
-
if (DEBUG) console.warn("Error calculating dropdown position:", error);
|
|
113
111
|
menuStyle.value = {
|
|
114
112
|
position: "fixed",
|
|
115
113
|
top: "0px",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
|
2
|
+
import { onBeforeRouteLeave } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
export function useFilterCSS(filterBuilder, svg) {
|
|
5
|
+
const STYLE_ELEMENT_ID = "filter-builder-styles";
|
|
6
|
+
let styleElement = null;
|
|
7
|
+
const cssUpdateTrigger = ref(0);
|
|
8
|
+
|
|
9
|
+
const injectStyles = () => {
|
|
10
|
+
const svgWrapper = document.getElementById("svg-wrapper");
|
|
11
|
+
if (!svgWrapper || !svg.value) return;
|
|
12
|
+
|
|
13
|
+
if (!styleElement) {
|
|
14
|
+
const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
|
|
15
|
+
if (existingStyle) existingStyle.remove();
|
|
16
|
+
|
|
17
|
+
styleElement = document.createElement("style");
|
|
18
|
+
styleElement.id = STYLE_ELEMENT_ID;
|
|
19
|
+
document.head.appendChild(styleElement);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const combinedCSS = filterBuilder.value.cssClasses + filterBuilder.value.temporaryCSS;
|
|
23
|
+
if (styleElement.textContent !== combinedCSS) {
|
|
24
|
+
styleElement.textContent = combinedCSS;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const setupCSSUpdates = () => {
|
|
29
|
+
const originalUpdateCss = filterBuilder.value.updateCss.bind(filterBuilder.value);
|
|
30
|
+
filterBuilder.value.updateCss = () => {
|
|
31
|
+
originalUpdateCss();
|
|
32
|
+
nextTick(() => {
|
|
33
|
+
injectStyles();
|
|
34
|
+
cssUpdateTrigger.value++;
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const originalHighlight = filterBuilder.value.highlight.bind(filterBuilder.value);
|
|
39
|
+
filterBuilder.value.highlight = (...args) => {
|
|
40
|
+
originalHighlight(...args);
|
|
41
|
+
nextTick(() => injectStyles());
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const originalClearHighlight = filterBuilder.value.clearHighlight.bind(filterBuilder.value);
|
|
45
|
+
filterBuilder.value.clearHighlight = () => {
|
|
46
|
+
originalClearHighlight();
|
|
47
|
+
nextTick(() => injectStyles());
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const cleanupStyles = () => {
|
|
52
|
+
const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
|
|
53
|
+
if (existingStyle) existingStyle.remove();
|
|
54
|
+
|
|
55
|
+
if (styleElement) {
|
|
56
|
+
styleElement.remove();
|
|
57
|
+
styleElement = null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
onMounted(() => {
|
|
62
|
+
cleanupStyles();
|
|
63
|
+
setupCSSUpdates();
|
|
64
|
+
watch(cssUpdateTrigger, () => injectStyles(), { immediate: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
onUnmounted(() => cleanupStyles());
|
|
68
|
+
onBeforeRouteLeave(() => cleanupStyles());
|
|
69
|
+
|
|
70
|
+
return { injectStyles, cssUpdateTrigger };
|
|
71
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
export function useFormValidation() {
|
|
4
|
+
const errors = ref([]);
|
|
5
|
+
|
|
6
|
+
const clearErrors = () => {
|
|
7
|
+
errors.value = [];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const addError = (fieldName) => {
|
|
11
|
+
if (!errors.value.includes(fieldName)) {
|
|
12
|
+
errors.value.push(fieldName);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const hasError = (fieldName) => {
|
|
17
|
+
return errors.value.includes(fieldName);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isValidEmail = (email) => {
|
|
21
|
+
return email && email.includes("@");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isValidPassword = (password, minLength = 5) => {
|
|
25
|
+
return password && password.length >= minLength;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isRequired = (value) => {
|
|
29
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
30
|
+
if (typeof value === 'number') return true;
|
|
31
|
+
return value != null && value !== undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const isValidCVV = (cvv) => {
|
|
35
|
+
return /^\d{3,4}$/.test(String(cvv));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isValidCardNumber = (cardNumber) => {
|
|
39
|
+
const clean = String(cardNumber).replace(/\s+/g, "");
|
|
40
|
+
return (
|
|
41
|
+
clean.match(/^4\d{15}$/) || // Visa (16 digits)
|
|
42
|
+
clean.match(/^5[1-5]\d{14}$/) || // Mastercard (16 digits)
|
|
43
|
+
clean.match(/^3[47]\d{13}$/) // AMEX (15 digits)
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isValidState = (state, country) => {
|
|
48
|
+
// State required only for US
|
|
49
|
+
if (country === "US") {
|
|
50
|
+
return state && state.length === 2;
|
|
51
|
+
}
|
|
52
|
+
return true; // Not required for other countries
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isValidZipCode = (zipCode) => {
|
|
56
|
+
return zipCode && zipCode.trim().length > 0;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const validateAccount = (account) => {
|
|
60
|
+
clearErrors();
|
|
61
|
+
|
|
62
|
+
if (!isValidEmail(account.email)) addError("email");
|
|
63
|
+
if (!isValidPassword(account.password)) addError("password");
|
|
64
|
+
|
|
65
|
+
return errors.value.length === 0;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const validateProfile = (profile) => {
|
|
69
|
+
clearErrors();
|
|
70
|
+
|
|
71
|
+
// Address validation
|
|
72
|
+
if (!isRequired(profile.zipCode)) addError("zipCode");
|
|
73
|
+
if (!isRequired(profile.address)) addError("address");
|
|
74
|
+
if (!isValidState(profile.state, profile.country)) addError("state");
|
|
75
|
+
if (!isRequired(profile.city)) addError("city");
|
|
76
|
+
if (!isRequired(profile.country)) addError("country");
|
|
77
|
+
|
|
78
|
+
// Card validation
|
|
79
|
+
if (!isValidCVV(profile.cvv)) addError("cvv");
|
|
80
|
+
if (!isValidCardNumber(profile.cardNumber)) addError("cardNumber");
|
|
81
|
+
if (!isRequired(profile.expYear)) addError("expYear");
|
|
82
|
+
if (!isRequired(profile.expMonth)) addError("expMonth");
|
|
83
|
+
|
|
84
|
+
return errors.value.length === 0;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
errors,
|
|
89
|
+
validateAccount,
|
|
90
|
+
validateProfile
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useDeviceDetection } from './useDeviceDetection';
|
|
2
|
+
|
|
3
|
+
export function useIOSViewportHandling() {
|
|
4
|
+
const { isIpadOS } = useDeviceDetection();
|
|
5
|
+
|
|
6
|
+
const isIOSDevice =
|
|
7
|
+
/iPad|iPhone|iPod/.test(navigator.platform) ||
|
|
8
|
+
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
|
|
9
|
+
|
|
10
|
+
if (!isIOSDevice) {
|
|
11
|
+
return { isIOSDevice: false };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const handleViewportResize = () => {
|
|
15
|
+
if (isIpadOS()) {
|
|
16
|
+
// For iPad, allow natural viewport behavior
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// For iPhone, maintain viewport stability
|
|
21
|
+
const vh = window.innerHeight * 0.01;
|
|
22
|
+
document.documentElement.style.setProperty("--vh", `${vh}px`);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Initial setup
|
|
26
|
+
handleViewportResize();
|
|
27
|
+
|
|
28
|
+
// Listen for viewport changes
|
|
29
|
+
window.addEventListener("resize", handleViewportResize);
|
|
30
|
+
if (window.visualViewport) {
|
|
31
|
+
window.visualViewport.addEventListener("resize", handleViewportResize);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Precise scroll control - only allow scrolling within specific scrollable elements
|
|
35
|
+
window.addEventListener(
|
|
36
|
+
"touchmove",
|
|
37
|
+
function (event) {
|
|
38
|
+
const isScrollableTextarea =
|
|
39
|
+
event.target.tagName === "TEXTAREA" &&
|
|
40
|
+
(event.target.classList.contains("code-editor") || event.target.classList.contains("proxy-editor"));
|
|
41
|
+
|
|
42
|
+
const isInScrollableContainer =
|
|
43
|
+
event.target.closest(".stop-pan") ||
|
|
44
|
+
event.target.closest(".overflow-y-auto") ||
|
|
45
|
+
event.target.closest(".vue-recycle-scroller") ||
|
|
46
|
+
event.target.closest(".scroller") ||
|
|
47
|
+
event.target.closest(".scrollable");
|
|
48
|
+
|
|
49
|
+
const isInTable = event.target.closest(".table-component");
|
|
50
|
+
const isScrollableTableContent =
|
|
51
|
+
isInTable &&
|
|
52
|
+
(event.target.closest(".grid") ||
|
|
53
|
+
event.target.closest(".table-row") ||
|
|
54
|
+
isInScrollableContainer ||
|
|
55
|
+
(event.target.closest(".table-component") && !event.target.closest(".table-header")));
|
|
56
|
+
|
|
57
|
+
const isInNavbar = event.target.closest(".navbar") || event.target.closest(".mobile-menu");
|
|
58
|
+
const isInModal = event.target.closest('[role="dialog"]');
|
|
59
|
+
|
|
60
|
+
if (isScrollableTextarea || isScrollableTableContent || isInScrollableContainer || isInNavbar || isInModal) {
|
|
61
|
+
if (isScrollableTableContent || isScrollableTextarea || isInScrollableContainer) {
|
|
62
|
+
event.stopPropagation();
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
},
|
|
69
|
+
{ passive: false }
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
isIOSDevice: true,
|
|
74
|
+
handleViewportResize
|
|
75
|
+
};
|
|
76
|
+
}
|