@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.
Files changed (66) hide show
  1. package/package.json +1 -1
  2. package/src/App.vue +14 -480
  3. package/src/assets/css/components/buttons.scss +12 -68
  4. package/src/assets/css/components/headers.scss +1 -1
  5. package/src/assets/css/components/utilities.scss +91 -16
  6. package/src/assets/css/main.scss +22 -95
  7. package/src/components/Auth/LoginForm.vue +2 -2
  8. package/src/components/Console/ConsoleToolbar.vue +123 -0
  9. package/src/components/Editors/Account/Account.vue +4 -2
  10. package/src/components/Editors/Account/AccountView.vue +12 -37
  11. package/src/components/Editors/Account/CreateAccount.vue +3 -11
  12. package/src/components/Editors/AdminFileEditor.vue +421 -0
  13. package/src/components/Editors/Profile/CreateProfile.vue +4 -20
  14. package/src/components/Editors/Profile/Profile.vue +5 -4
  15. package/src/components/Editors/Profile/ProfileView.vue +13 -38
  16. package/src/components/Editors/ProxyFileEditor.vue +178 -0
  17. package/src/components/Filter/Filter.vue +6 -6
  18. package/src/components/Filter/FilterPreview.vue +4 -12
  19. package/src/components/Filter/PriceSortToggle.vue +1 -1
  20. package/src/components/Tasks/QuickSettings.vue +5 -5
  21. package/src/components/Tasks/Stats.vue +1 -1
  22. package/src/components/Tasks/Task.vue +5 -8
  23. package/src/components/Tasks/TaskView.vue +2 -1
  24. package/src/components/Tasks/ViewTask.vue +2 -2
  25. package/src/components/icons/index.js +0 -4
  26. package/src/components/ui/ActionButtonGroup.vue +2 -2
  27. package/src/components/ui/BalanceIndicator.vue +3 -3
  28. package/src/components/ui/EnableDisableToggle.vue +2 -2
  29. package/src/components/ui/FormField.vue +2 -2
  30. package/src/components/ui/IconLabel.vue +2 -2
  31. package/src/components/ui/InfoRow.vue +4 -4
  32. package/src/components/ui/Modal.vue +83 -9
  33. package/src/components/ui/Navbar.vue +3 -3
  34. package/src/components/ui/ReadonlyFieldsSection.vue +1 -1
  35. package/src/components/ui/StatusBadge.vue +1 -1
  36. package/src/components/ui/controls/CountryChooser.vue +5 -5
  37. package/src/components/ui/controls/atomic/MultiDropdown.vue +1 -1
  38. package/src/composables/useCodeEditor.js +117 -0
  39. package/src/composables/useDropdownPosition.js +0 -2
  40. package/src/composables/useEnableDisable.js +6 -0
  41. package/src/composables/useFilterCSS.js +71 -0
  42. package/src/composables/useFormValidation.js +92 -0
  43. package/src/composables/useGetAllTags.js +9 -0
  44. package/src/composables/useIOSViewportHandling.js +76 -0
  45. package/src/composables/useNotchHandling.js +306 -0
  46. package/src/composables/useTableRender.js +23 -0
  47. package/src/composables/useZoomPrevention.js +96 -0
  48. package/src/constants/tableLayout.js +14 -0
  49. package/src/libs/utils/array.js +1 -3
  50. package/src/libs/utils/dataGeneration.js +1 -1
  51. package/src/libs/utils/string.js +1 -26
  52. package/src/libs/utils/validation.js +2 -26
  53. package/src/stores/connection.js +0 -25
  54. package/src/stores/ui.js +21 -35
  55. package/src/utils/tableHelpers.js +1 -0
  56. package/src/views/Accounts.vue +9 -17
  57. package/src/views/Console.vue +15 -92
  58. package/src/views/Editor.vue +39 -938
  59. package/src/views/FilterBuilder.vue +9 -97
  60. package/src/views/Profiles.vue +9 -17
  61. package/src/views/Tasks.vue +4 -4
  62. package/src/assets/img/background.svg.backup +0 -11
  63. package/src/components/icons/SquareCheck.vue +0 -12
  64. package/src/components/icons/SquareUncheck.vue +0 -12
  65. package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
  66. /package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +0 -0
@@ -1,9 +1,17 @@
1
1
  <template>
2
- <div class="modal-mask" role="dialog" aria-modal="true" @touchmove.stop>
3
- <div class="modal-content" ref="target">
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="ui.toggleModal()" class="btn-icon border-none hover:bg-dark-400 ml-auto" aria-label="Close modal">
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
- // Lock body scroll
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")) ui.toggleModal();
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: rgba(0, 0, 0, 0.85);
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 rgba(0, 0, 0, 0.8),
112
- 0 0 0 1px rgba(255, 255, 255, 0.05);
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="h-10 w-10 rounded-full hidden lg:block mx-4"
48
+ class="size-10 rounded-full hidden lg:block mx-4"
49
49
  />
50
- <div v-else class="h-10 w-10 rounded-full hidden lg:block mx-4 bg-dark-400" />
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="h-10 w-10 rounded-full" />
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 gap-3 h-10">
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 items-center justify-center rounded-full border-2 transition-all duration-200',
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: rgba(255, 255, 255, 0.1);
119
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
120
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
121
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
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%, rgba(255, 255, 255, 0.2) 50%, transparent 100%);
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 gap-2">
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,6 @@
1
+ export function useEnableDisable(item, updateFn) {
2
+ const enable = async () => await updateFn({ ...item.value, enabled: true });
3
+ const disable = async () => await updateFn({ ...item.value, enabled: false });
4
+
5
+ return { enable, disable };
6
+ }
@@ -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,9 @@
1
+ export function useGetAllTags(results) {
2
+ let tags = ["Any"];
3
+ results.forEach((p) =>
4
+ p.tags.forEach((tag) => {
5
+ if (!tags.includes(tag)) tags.push(tag);
6
+ })
7
+ );
8
+ return tags;
9
+ }
@@ -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
+ }