@necrolab/dashboard 0.5.15 → 0.5.17
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/backend/api.js +2 -3
- package/eslint.config.js +46 -0
- package/index.html +2 -1
- package/package.json +5 -2
- package/src/App.vue +70 -566
- package/src/assets/css/base/mixins.scss +72 -0
- package/src/assets/css/base/reset.scss +0 -2
- package/src/assets/css/base/scroll.scss +43 -36
- package/src/assets/css/base/typography.scss +9 -10
- package/src/assets/css/base/variables.scss +43 -0
- package/src/assets/css/components/accessibility.scss +37 -0
- package/src/assets/css/components/buttons.scss +61 -74
- package/src/assets/css/components/forms.scss +31 -32
- package/src/assets/css/components/headers.scss +13 -21
- package/src/assets/css/components/modals.scss +2 -2
- package/src/assets/css/components/search-groups.scss +28 -22
- package/src/assets/css/components/tables.scss +5 -7
- package/src/assets/css/components/toasts.scss +7 -7
- package/src/assets/css/components/utilities.scss +295 -0
- package/src/assets/css/main.scss +55 -139
- package/src/components/Auth/LoginForm.vue +7 -86
- package/src/components/Console/ConsoleToolbar.vue +123 -0
- package/src/components/Editors/Account/Account.vue +12 -12
- package/src/components/Editors/Account/AccountView.vue +38 -111
- package/src/components/Editors/Account/CreateAccount.vue +11 -61
- package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +28 -59
- package/src/components/Editors/AdminFileEditor.vue +179 -0
- package/src/components/Editors/Profile/CreateProfile.vue +77 -150
- package/src/components/Editors/Profile/Profile.vue +20 -21
- package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
- package/src/components/Editors/Profile/ProfileView.vue +41 -116
- package/src/components/Editors/ProxyFileEditor.vue +86 -0
- package/src/components/Editors/TagLabel.vue +16 -55
- package/src/components/Editors/TagToggle.vue +20 -8
- package/src/components/Filter/Filter.vue +66 -79
- package/src/components/Filter/FilterPreview.vue +153 -135
- package/src/components/Filter/PriceSortToggle.vue +36 -43
- package/src/components/Table/Header.vue +1 -1
- package/src/components/Table/Table.vue +45 -51
- package/src/components/Tasks/CheckStock.vue +7 -16
- package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
- package/src/components/Tasks/Controls/MobileControls.vue +5 -20
- package/src/components/Tasks/CreateTaskAXS.vue +20 -118
- package/src/components/Tasks/CreateTaskTM.vue +33 -189
- package/src/components/Tasks/EventDetailRow.vue +21 -0
- package/src/components/Tasks/MassEdit.vue +6 -16
- package/src/components/Tasks/QuickSettings.vue +140 -216
- package/src/components/Tasks/ScrapeVenue.vue +4 -13
- package/src/components/Tasks/Stats.vue +20 -39
- package/src/components/Tasks/Task.vue +64 -270
- package/src/components/Tasks/TaskLabel.vue +9 -3
- package/src/components/Tasks/TaskView.vue +45 -64
- package/src/components/Tasks/Utilities.vue +10 -44
- package/src/components/Tasks/ViewTask.vue +23 -107
- package/src/components/icons/Close.vue +2 -8
- package/src/components/icons/Gear.vue +8 -8
- package/src/components/icons/Hash.vue +5 -0
- package/src/components/icons/Key.vue +2 -8
- package/src/components/icons/Pencil.vue +2 -8
- package/src/components/icons/Profile.vue +2 -8
- package/src/components/icons/Sell.vue +2 -8
- package/src/components/icons/Spinner.vue +4 -7
- package/src/components/icons/Wildcard.vue +2 -8
- package/src/components/icons/index.js +3 -5
- package/src/components/ui/ActionButtonGroup.vue +113 -52
- package/src/components/ui/BalanceIndicator.vue +60 -0
- package/src/components/ui/EmptyState.vue +24 -0
- package/src/components/ui/EnableDisableToggle.vue +23 -0
- package/src/components/ui/FormField.vue +49 -49
- package/src/components/ui/IconLabel.vue +23 -0
- package/src/components/ui/InfoRow.vue +21 -54
- package/src/components/ui/Modal.vue +161 -54
- package/src/components/ui/Navbar.vue +63 -44
- package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
- package/src/components/ui/ReconnectIndicator.vue +111 -124
- package/src/components/ui/SectionCard.vue +6 -14
- package/src/components/ui/Splash.vue +2 -10
- package/src/components/ui/StatusBadge.vue +26 -28
- package/src/components/ui/TaskToggle.vue +54 -0
- package/src/components/ui/controls/CountryChooser.vue +29 -66
- package/src/components/ui/controls/EyeToggle.vue +1 -1
- package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
- package/src/components/ui/controls/atomic/Dropdown.vue +103 -139
- package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -120
- package/src/components/ui/controls/atomic/Switch.vue +21 -84
- package/src/composables/useCodeEditor.js +117 -0
- package/src/composables/useColorMapping.js +15 -0
- package/src/composables/useCopyToClipboard.js +1 -1
- package/src/composables/useDateFormatting.js +21 -0
- package/src/composables/useDeviceDetection.js +14 -0
- package/src/composables/useDropdownPosition.js +1 -4
- package/src/composables/useDynamicTableHeight.js +31 -0
- 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/useRowSelection.js +0 -3
- package/src/composables/useTableRender.js +23 -0
- package/src/composables/useTicketPricing.js +16 -0
- package/src/composables/useWindowDimensions.js +21 -0
- package/src/composables/useZoomPrevention.js +96 -0
- package/src/constants/tableLayout.js +14 -0
- package/src/libs/Filter.js +14 -20
- package/src/libs/panzoom.js +1 -5
- package/src/libs/utils/array.js +58 -0
- package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
- package/src/libs/utils/eventUrl.js +40 -0
- package/src/libs/utils/string.js +3 -0
- package/src/libs/utils/time.js +20 -0
- package/src/libs/utils/validation.js +64 -0
- package/src/main.js +0 -2
- package/src/stores/connection.js +1 -29
- package/src/stores/logger.js +6 -12
- package/src/stores/sampleData.js +1 -2
- package/src/stores/ui.js +80 -71
- package/src/utils/tableHelpers.js +1 -0
- package/src/views/Accounts.vue +19 -38
- package/src/views/Console.vue +74 -253
- package/src/views/Editor.vue +47 -1114
- package/src/views/FilterBuilder.vue +190 -461
- package/src/views/Login.vue +3 -28
- package/src/views/Profiles.vue +17 -32
- package/src/views/Tasks.vue +51 -38
- package/tailwind.config.js +82 -71
- package/workbox-config.cjs +47 -5
- package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2438
- package/exit +0 -209
- package/run +0 -177
- package/src/assets/css/base/color-fallbacks.scss +0 -10
- package/src/assets/img/background.svg.backup +0 -11
- package/src/components/icons/SquareCheck.vue +0 -18
- package/src/components/icons/SquareUncheck.vue +0 -18
- package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
- package/switch-branch.sh +0 -41
- /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
|
@@ -1,96 +1,33 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<label
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<label
|
|
3
|
+
class="relative inline-block w-[51px] h-[31px] max-modal:w-11 max-modal:min-w-11 max-modal:h-6.5"
|
|
4
|
+
:class="disabled ? 'pointer-events-none' : ''"
|
|
5
|
+
>
|
|
6
|
+
<input
|
|
7
|
+
type="checkbox"
|
|
8
|
+
v-model="value"
|
|
9
|
+
:disabled="disabled"
|
|
10
|
+
class="opacity-0 w-0 h-0 peer"
|
|
11
|
+
role="switch"
|
|
12
|
+
:aria-checked="value"
|
|
13
|
+
/>
|
|
14
|
+
<span
|
|
15
|
+
class="absolute inset-0 cursor-pointer border-2 rounded-[31px] transition-all duration-300 ease-out
|
|
16
|
+
before:absolute before:content-[''] before:size-[23px] before:left-0.5 before:bottom-0.5 before:rounded-full before:shadow-switch before:transition-all before:duration-300 before:ease-out
|
|
17
|
+
bg-dark-550 border-dark-550 before:bg-dark-200
|
|
18
|
+
peer-checked:bg-accent-green peer-checked:border-accent-green peer-checked:before:bg-white peer-checked:before:translate-x-5
|
|
19
|
+
peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:before:opacity-70
|
|
20
|
+
max-modal:before:size-[22px] max-modal:peer-checked:before:translate-x-[18px]"
|
|
21
|
+
></span>
|
|
5
22
|
</label>
|
|
6
23
|
</template>
|
|
7
24
|
|
|
8
25
|
<script setup>
|
|
9
26
|
const value = defineModel();
|
|
10
|
-
|
|
27
|
+
defineProps({
|
|
11
28
|
disabled: {
|
|
12
29
|
type: Boolean,
|
|
13
30
|
default: false
|
|
14
31
|
}
|
|
15
32
|
});
|
|
16
33
|
</script>
|
|
17
|
-
|
|
18
|
-
<style lang="scss" scoped>
|
|
19
|
-
/* iOS-style switch */
|
|
20
|
-
.switch {
|
|
21
|
-
@apply relative inline-block;
|
|
22
|
-
width: 51px;
|
|
23
|
-
height: 31px;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* Hide default HTML checkbox */
|
|
27
|
-
.switch input {
|
|
28
|
-
@apply opacity-0 w-0 h-0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* The slider */
|
|
32
|
-
.slider {
|
|
33
|
-
@apply absolute cursor-pointer inset-0;
|
|
34
|
-
@apply border-2;
|
|
35
|
-
background-color: oklch(0.26 0 0);
|
|
36
|
-
border-color: oklch(0.35 0 0);
|
|
37
|
-
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.slider:before {
|
|
41
|
-
@apply absolute;
|
|
42
|
-
@apply w-[23px] h-[23px] left-0.5 bottom-0.5;
|
|
43
|
-
content: "";
|
|
44
|
-
background-color: oklch(0.50 0 0);
|
|
45
|
-
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
46
|
-
box-shadow: 0 1px 3px oklch(0 0 0 / 0.3);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
input:checked + .slider {
|
|
50
|
-
background-color: oklch(0.72 0.15 145);
|
|
51
|
-
border-color: oklch(0.72 0.15 145);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
input:checked + .slider:before {
|
|
55
|
-
background-color: oklch(1 0 0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
input:checked + .slider:before {
|
|
59
|
-
transform: translateX(20px);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.switch.disabled {
|
|
63
|
-
@apply pointer-events-none;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
input:disabled + .slider {
|
|
67
|
-
@apply opacity-50 cursor-not-allowed;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
input:disabled + .slider:before {
|
|
71
|
-
@apply opacity-70;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* Rounded sliders */
|
|
75
|
-
.slider.round {
|
|
76
|
-
border-radius: 31px;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.slider.round:before {
|
|
80
|
-
border-radius: 50%;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@media (max-width: 810px) {
|
|
84
|
-
.switch {
|
|
85
|
-
@apply w-11 min-w-11 h-[26px];
|
|
86
|
-
|
|
87
|
-
.slider:before {
|
|
88
|
-
@apply w-[22px] h-[22px] left-0.5 bottom-0.5;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
input:checked + .slider:before {
|
|
93
|
-
@apply translate-x-[18px];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
</style>
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function useColorMapping() {
|
|
2
|
+
const colorToClass = (color) => {
|
|
3
|
+
const colorMap = {
|
|
4
|
+
green: "bg-green-400",
|
|
5
|
+
red: "bg-red-400",
|
|
6
|
+
yellow: "bg-yellow-400",
|
|
7
|
+
blue: "bg-blue-400",
|
|
8
|
+
error: "bg-red-400",
|
|
9
|
+
success: "bg-green-400"
|
|
10
|
+
};
|
|
11
|
+
return colorMap[color?.toLowerCase()] || "bg-white";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return { colorToClass };
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function useDateFormatting() {
|
|
2
|
+
const formatEventDate = (dateString) => {
|
|
3
|
+
if (!dateString) return '';
|
|
4
|
+
try {
|
|
5
|
+
const date = new Date(dateString);
|
|
6
|
+
const options = {
|
|
7
|
+
month: 'short',
|
|
8
|
+
day: 'numeric',
|
|
9
|
+
year: 'numeric',
|
|
10
|
+
hour: 'numeric',
|
|
11
|
+
minute: '2-digit',
|
|
12
|
+
hour12: true
|
|
13
|
+
};
|
|
14
|
+
return date.toLocaleString('en-US', options).replace(',', '');
|
|
15
|
+
} catch {
|
|
16
|
+
return dateString;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return { formatEventDate };
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function useDeviceDetection() {
|
|
2
|
+
const isIOS = () => {
|
|
3
|
+
if (/iPad|iPhone|iPod/.test(navigator.platform)) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isIpadOS = () => {
|
|
10
|
+
return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return { isIOS, isIpadOS };
|
|
14
|
+
}
|
|
@@ -8,8 +8,7 @@ export function useDropdownPosition(dropdownRef, options = {}) {
|
|
|
8
8
|
minWidth = null,
|
|
9
9
|
maxHeight = window.innerHeight * 0.8, // Use 80% of viewport height
|
|
10
10
|
estimateHeight = null,
|
|
11
|
-
includeAdjacentButtons = false
|
|
12
|
-
containerSelector = null
|
|
11
|
+
includeAdjacentButtons = false
|
|
13
12
|
} = options;
|
|
14
13
|
|
|
15
14
|
const calculateMenuPosition = () => {
|
|
@@ -109,8 +108,6 @@ export function useDropdownPosition(dropdownRef, options = {}) {
|
|
|
109
108
|
maxHeight: `${maxHeight}px`
|
|
110
109
|
};
|
|
111
110
|
} catch (error) {
|
|
112
|
-
console.warn("Error calculating dropdown position:", error);
|
|
113
|
-
// Fallback to basic positioning
|
|
114
111
|
menuStyle.value = {
|
|
115
112
|
position: "fixed",
|
|
116
113
|
top: "0px",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useWindowDimensions } from "./useWindowDimensions";
|
|
3
|
+
|
|
4
|
+
export function useDynamicTableHeight(options = {}) {
|
|
5
|
+
const { windowHeight, windowWidth } = useWindowDimensions();
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
topReservedSpace = 243,
|
|
9
|
+
bottomBuffer = 16,
|
|
10
|
+
rowHeight = 64,
|
|
11
|
+
minRowsToShow = 2
|
|
12
|
+
} = options;
|
|
13
|
+
|
|
14
|
+
const dynamicTableHeight = computed(() => {
|
|
15
|
+
// Detect PWA mode and small screens
|
|
16
|
+
const isPWA = window.matchMedia('(display-mode: standalone)').matches;
|
|
17
|
+
const isMobile = windowWidth.value <= 768;
|
|
18
|
+
|
|
19
|
+
// Extra buffer for iPhone PWA to prevent overflow
|
|
20
|
+
const extraBuffer = isPWA && isMobile ? 60 : 0;
|
|
21
|
+
|
|
22
|
+
const availableHeight = windowHeight.value - topReservedSpace - bottomBuffer - extraBuffer;
|
|
23
|
+
const minHeight = minRowsToShow * rowHeight;
|
|
24
|
+
const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
|
|
25
|
+
const exactHeight = maxCompleteRows * rowHeight;
|
|
26
|
+
|
|
27
|
+
return exactHeight + "px";
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return { dynamicTableHeight };
|
|
31
|
+
}
|
|
@@ -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
|
+
}
|