@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
package/src/App.vue
CHANGED
|
@@ -1,58 +1,34 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="layout min-h-screen h-screen">
|
|
3
|
+
<!-- iPhone Landscape Lock Overlay -->
|
|
4
|
+
<div v-if="showLandscapeLock" class="iphone-landscape-lock">
|
|
5
|
+
<div class="rotate-message">
|
|
6
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="rotate-icon">
|
|
7
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
8
|
+
</svg>
|
|
9
|
+
<p class="rotate-text">Please rotate your device to portrait mode</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
3
13
|
<transition name="fade">
|
|
4
14
|
<Splash v-if="isLoading" />
|
|
5
15
|
</transition>
|
|
6
|
-
<div class="refresh-wrapper">
|
|
7
|
-
<div
|
|
8
|
-
class="refresh-container w-fit mx-auto"
|
|
9
|
-
:style="{
|
|
10
|
-
'margin-top': maxPull() + 'px',
|
|
11
|
-
transform: `rotate(${ui.pullChange}deg)`
|
|
12
|
-
}">
|
|
13
|
-
<div
|
|
14
|
-
class="refresh-icon p-2 rounded-full mx-auto duration-200"
|
|
15
|
-
:class="{
|
|
16
|
-
'opacity-100': ui.pullChange > 250,
|
|
17
|
-
'opacity-0': ui.pullChange < 250
|
|
18
|
-
}">
|
|
19
|
-
<svg
|
|
20
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
-
fill="none"
|
|
22
|
-
viewBox="0 0 24 24"
|
|
23
|
-
class="stroke-2 w-4 stroke-dark-400"
|
|
24
|
-
:class="{ 'opacity-0': ui.pullChange == 0 }">
|
|
25
|
-
<path
|
|
26
|
-
strokeLinecap="round"
|
|
27
|
-
strokeLinejoin="round"
|
|
28
|
-
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
|
29
|
-
</svg>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
<h2
|
|
33
|
-
class="text-dark-400 text-center duration-200"
|
|
34
|
-
:class="{
|
|
35
|
-
'opacity-100': ui.pullChange > 250,
|
|
36
|
-
'opacity-0': ui.pullChange < 250
|
|
37
|
-
}">
|
|
38
|
-
Release to refresh
|
|
39
|
-
</h2>
|
|
40
|
-
</div>
|
|
41
16
|
<transition name="out-in">
|
|
42
17
|
<div v-if="spinner" key="reconnect-indicator" class="h-full">
|
|
43
18
|
<ReconnectIndicator :message="ui.spinnerMessage" />
|
|
44
19
|
</div>
|
|
45
20
|
<div v-else key="main-components" class="flex">
|
|
46
21
|
<Navbar v-if="layout == 'dashboard'" class="fixed" />
|
|
47
|
-
<div
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
<div class="router-wrapper w-full">
|
|
23
|
+
<router-view v-slot="{ Component, route }">
|
|
24
|
+
<transition name="page-transition" mode="out-in">
|
|
25
|
+
<component
|
|
26
|
+
:is="Component"
|
|
27
|
+
:key="route.path"
|
|
28
|
+
class="component-container w-full mx-auto px-3 xs:px-3 md:px-2 lg:px-6 xl:px-10 pb-2 ios-wrapper" />
|
|
29
|
+
</transition>
|
|
30
|
+
</router-view>
|
|
31
|
+
</div>
|
|
56
32
|
</div>
|
|
57
33
|
</transition>
|
|
58
34
|
</div>
|
|
@@ -60,595 +36,123 @@
|
|
|
60
36
|
|
|
61
37
|
<script setup>
|
|
62
38
|
import { storeToRefs } from "pinia";
|
|
63
|
-
import { ref, computed
|
|
39
|
+
import { ref, computed } from "vue";
|
|
64
40
|
import { useRouter } from "vue-router";
|
|
65
41
|
import Navbar from "@/components/ui/Navbar.vue";
|
|
66
42
|
import { useUIStore } from "@/stores/ui";
|
|
67
43
|
import Splash from "@/components/ui/Splash.vue";
|
|
68
44
|
import ReconnectIndicator from "@/components/ui/ReconnectIndicator.vue";
|
|
45
|
+
import { useZoomPrevention } from "@/composables/useZoomPrevention";
|
|
46
|
+
import { useIOSViewportHandling } from "@/composables/useIOSViewportHandling";
|
|
47
|
+
import { useNotchHandling } from "@/composables/useNotchHandling";
|
|
69
48
|
|
|
70
49
|
const ui = useUIStore();
|
|
50
|
+
const { showSpinner: spinner } = storeToRefs(ui);
|
|
71
51
|
const router = useRouter();
|
|
72
52
|
const isLoading = ref(false);
|
|
73
|
-
const spinner = storeToRefs(ui).showSpinner;
|
|
74
|
-
|
|
75
|
-
function isIOS() {
|
|
76
|
-
if (/iPad|iPhone|iPod/.test(navigator.platform)) {
|
|
77
|
-
return true;
|
|
78
|
-
} else {
|
|
79
|
-
return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isIpadOS() {
|
|
84
|
-
return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Prevent pinch-to-zoom gestures
|
|
88
|
-
let lastTouchDistance = 0;
|
|
89
53
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
}
|
|
94
|
-
}, { passive: false });
|
|
95
|
-
|
|
96
|
-
document.addEventListener('touchmove', (e) => {
|
|
97
|
-
if (e.touches.length > 1) {
|
|
98
|
-
e.preventDefault();
|
|
99
|
-
}
|
|
100
|
-
}, { passive: false });
|
|
101
|
-
|
|
102
|
-
document.addEventListener('gesturestart', (e) => {
|
|
103
|
-
e.preventDefault();
|
|
104
|
-
}, { passive: false });
|
|
105
|
-
|
|
106
|
-
document.addEventListener('gesturechange', (e) => {
|
|
107
|
-
e.preventDefault();
|
|
108
|
-
}, { passive: false });
|
|
109
|
-
|
|
110
|
-
document.addEventListener('gestureend', (e) => {
|
|
111
|
-
e.preventDefault();
|
|
112
|
-
}, { passive: false });
|
|
113
|
-
|
|
114
|
-
// Prevent double-tap zoom
|
|
115
|
-
let lastTouchEnd = 0;
|
|
116
|
-
document.addEventListener('touchend', (e) => {
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
if (now - lastTouchEnd <= 300) {
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
}
|
|
121
|
-
lastTouchEnd = now;
|
|
122
|
-
}, { passive: false });
|
|
123
|
-
|
|
124
|
-
if (!window.location.href.includes(":5173")) ui.startSpinner("Loading...");
|
|
54
|
+
// Initialize zoom prevention (all browsers)
|
|
55
|
+
const { KEY_CODES } = useZoomPrevention();
|
|
125
56
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
// Multiple aggressive attempts on Vue mount
|
|
129
|
-
handleNotch();
|
|
130
|
-
setTimeout(handleNotch, 10);
|
|
131
|
-
setTimeout(handleNotch, 50);
|
|
132
|
-
setTimeout(handleNotch, 150);
|
|
133
|
-
setTimeout(handleNotch, 300);
|
|
57
|
+
// Initialize iOS viewport handling
|
|
58
|
+
useIOSViewportHandling();
|
|
134
59
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const recurringCheck = setInterval(() => {
|
|
138
|
-
if (attempts++ > 10) {
|
|
139
|
-
clearInterval(recurringCheck);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
handleNotch();
|
|
143
|
-
}, 200);
|
|
144
|
-
});
|
|
60
|
+
// Initialize notch handling (iPhone-specific)
|
|
61
|
+
const { showLandscapeLock } = useNotchHandling(ui.logger);
|
|
145
62
|
|
|
146
|
-
//
|
|
63
|
+
// ESC key handling for modals
|
|
147
64
|
document.onkeydown = function (evt) {
|
|
148
65
|
evt = evt || window.event;
|
|
149
|
-
|
|
66
|
+
let isEscape = false;
|
|
150
67
|
if ("key" in evt) {
|
|
151
68
|
isEscape = evt.key === "Escape" || evt.key === "Esc";
|
|
152
69
|
} else {
|
|
153
|
-
isEscape = evt.keyCode ===
|
|
70
|
+
isEscape = evt.keyCode === KEY_CODES.ESCAPE;
|
|
154
71
|
}
|
|
155
72
|
if (isEscape) {
|
|
156
73
|
ui.activeModal = "";
|
|
157
74
|
}
|
|
158
75
|
};
|
|
159
76
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
// Prevent Ctrl/Cmd + Plus/Minus/0 (various keycodes for different browsers)
|
|
163
|
-
if (
|
|
164
|
-
(event.ctrlKey || event.metaKey) &&
|
|
165
|
-
(event.which === 61 || // = key
|
|
166
|
-
event.which === 107 || // Numpad +
|
|
167
|
-
event.which === 173 || // Firefox -
|
|
168
|
-
event.which === 109 || // Numpad -
|
|
169
|
-
event.which === 187 || // = key (Chrome)
|
|
170
|
-
event.which === 189 || // - key
|
|
171
|
-
event.keyCode === 61 ||
|
|
172
|
-
event.keyCode === 107 ||
|
|
173
|
-
event.keyCode === 173 ||
|
|
174
|
-
event.keyCode === 109 ||
|
|
175
|
-
event.keyCode === 187 ||
|
|
176
|
-
event.keyCode === 189 ||
|
|
177
|
-
event.key === '+' ||
|
|
178
|
-
event.key === '-' ||
|
|
179
|
-
event.key === '=' ||
|
|
180
|
-
event.key === '0')
|
|
181
|
-
) {
|
|
182
|
-
event.preventDefault();
|
|
183
|
-
}
|
|
184
|
-
}, { passive: false });
|
|
185
|
-
|
|
186
|
-
// Prevent Ctrl/Cmd + Mouse wheel zoom (desktop)
|
|
187
|
-
document.addEventListener("wheel", function (event) {
|
|
188
|
-
if (event.ctrlKey || event.metaKey) {
|
|
189
|
-
event.preventDefault();
|
|
190
|
-
}
|
|
191
|
-
}, { passive: false });
|
|
192
|
-
|
|
193
|
-
// Also block mousewheel for older browsers
|
|
194
|
-
document.addEventListener("mousewheel", function (event) {
|
|
195
|
-
if (event.ctrlKey || event.metaKey) {
|
|
196
|
-
event.preventDefault();
|
|
197
|
-
}
|
|
198
|
-
}, { passive: false });
|
|
199
|
-
|
|
200
|
-
// Block DOMMouseScroll for Firefox
|
|
201
|
-
document.addEventListener("DOMMouseScroll", function (event) {
|
|
202
|
-
if (event.ctrlKey || event.metaKey) {
|
|
203
|
-
event.preventDefault();
|
|
204
|
-
}
|
|
205
|
-
}, { passive: false });
|
|
206
|
-
// Nuclear iOS keyboard prevention - lock everything down
|
|
207
|
-
let isIOSDevice =
|
|
208
|
-
/iPad|iPhone|iPod/.test(navigator.platform) ||
|
|
209
|
-
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
|
|
210
|
-
|
|
211
|
-
if (isIOSDevice) {
|
|
212
|
-
const handleViewportResize = () => {
|
|
213
|
-
if (isIpadOS()) {
|
|
214
|
-
// For iPad, allow natural viewport behavior
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// For iPhone, maintain viewport stability
|
|
219
|
-
const vh = window.innerHeight * 0.01;
|
|
220
|
-
document.documentElement.style.setProperty("--vh", `${vh}px`);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// Initial setup
|
|
224
|
-
handleViewportResize();
|
|
225
|
-
|
|
226
|
-
// Listen for viewport changes
|
|
227
|
-
window.addEventListener("resize", handleViewportResize);
|
|
228
|
-
if (window.visualViewport) {
|
|
229
|
-
window.visualViewport.addEventListener("resize", handleViewportResize);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Simplified scroll control - let scrollable elements handle their own scrolling
|
|
234
|
-
// Page scroll is already prevented by overflow: hidden on html/body
|
|
235
|
-
|
|
236
|
-
// DOMMouseScroll handler removed - CSS overflow handles scroll prevention
|
|
237
|
-
|
|
238
|
-
// Wheel and scroll handlers removed - CSS overflow: hidden on html/body prevents page scroll
|
|
239
|
-
|
|
240
|
-
// Precise scroll control - only allow scrolling within specific scrollable elements
|
|
241
|
-
window.addEventListener(
|
|
242
|
-
"touchmove",
|
|
243
|
-
function (event) {
|
|
244
|
-
// Check if we're touching a scrollable element directly
|
|
245
|
-
const isScrollableTextarea =
|
|
246
|
-
event.target.tagName === "TEXTAREA" &&
|
|
247
|
-
(event.target.classList.contains("code-editor") || event.target.classList.contains("proxy-editor"));
|
|
248
|
-
|
|
249
|
-
// Check if we're in a scrollable container
|
|
250
|
-
const isInScrollableContainer =
|
|
251
|
-
event.target.closest(".stop-pan") ||
|
|
252
|
-
event.target.closest(".overflow-y-auto") ||
|
|
253
|
-
event.target.closest(".vue-recycle-scroller") ||
|
|
254
|
-
event.target.closest(".scroller") ||
|
|
255
|
-
event.target.closest(".scrollable");
|
|
256
|
-
|
|
257
|
-
// Check if we're in a table but only allow scrolling on actual scrollable content
|
|
258
|
-
const isInTable = event.target.closest(".table-component");
|
|
259
|
-
const isScrollableTableContent =
|
|
260
|
-
isInTable &&
|
|
261
|
-
(event.target.closest(".grid") || // Table rows
|
|
262
|
-
event.target.closest(".table-row") ||
|
|
263
|
-
isInScrollableContainer ||
|
|
264
|
-
(event.target.closest(".table-component") && !event.target.closest(".table-header")));
|
|
265
|
-
|
|
266
|
-
// Allow scrolling in navbar and modals (they handle their own boundaries)
|
|
267
|
-
const isInNavbar = event.target.closest(".navbar") || event.target.closest(".mobile-menu");
|
|
268
|
-
const isInModal = event.target.closest('[role="dialog"]');
|
|
269
|
-
|
|
270
|
-
// Only allow these specific cases
|
|
271
|
-
if (isScrollableTextarea || isScrollableTableContent || isInScrollableContainer || isInNavbar || isInModal) {
|
|
272
|
-
// For table content, ensure we stop propagation to prevent page scroll
|
|
273
|
-
if (isScrollableTableContent || isScrollableTextarea || isInScrollableContainer) {
|
|
274
|
-
event.stopPropagation();
|
|
275
|
-
}
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Prevent everything else
|
|
280
|
-
event.preventDefault();
|
|
281
|
-
},
|
|
282
|
-
{ passive: false }
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// PERFECT notch handling - simple and bulletproof
|
|
286
|
-
let notchTimeout;
|
|
287
|
-
let isNotchBusy = false;
|
|
288
|
-
let lastNotchState = null;
|
|
289
|
-
let animationTimeout;
|
|
290
|
-
let pendingUpdate = false;
|
|
291
|
-
|
|
292
|
-
function isLandscapeMode() {
|
|
293
|
-
// Check orientation first
|
|
294
|
-
let orientationLandscape = false;
|
|
295
|
-
|
|
296
|
-
if (typeof window.orientation !== "undefined") {
|
|
297
|
-
orientationLandscape = Math.abs(window.orientation) === 90;
|
|
298
|
-
} else if (screen.orientation && typeof screen.orientation.angle !== "undefined") {
|
|
299
|
-
orientationLandscape = Math.abs(screen.orientation.angle) === 90;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Always also check dimensions as backup
|
|
303
|
-
const dimensionLandscape = window.innerWidth > window.innerHeight;
|
|
304
|
-
const hasStrongLandscapeRatio = window.innerWidth / window.innerHeight > 1.5;
|
|
305
|
-
|
|
306
|
-
// For iPhones, if dimensions clearly indicate landscape, trust that
|
|
307
|
-
if (isIOS() && !isIpadOS() && dimensionLandscape && hasStrongLandscapeRatio) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Otherwise use orientation if available, fallback to dimensions
|
|
312
|
-
return orientationLandscape || (dimensionLandscape && hasStrongLandscapeRatio);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function hasDeviceNotch() {
|
|
316
|
-
// Only check for notch on actual devices that might have one
|
|
317
|
-
if (!isIOS() || isIpadOS()) return false;
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
CSS.supports("padding-left: env(safe-area-inset-left)") &&
|
|
321
|
-
CSS.supports("padding-right: env(safe-area-inset-right)")
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function handleNotch(force = false) {
|
|
326
|
-
// Simple debouncing
|
|
327
|
-
if (isNotchBusy && !force) return;
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
// Only for iPhone (not iPad)
|
|
331
|
-
if (!isIOS() || isIpadOS()) return;
|
|
332
|
-
|
|
333
|
-
const wrappers = document.querySelectorAll(".ios-wrapper");
|
|
334
|
-
if (wrappers.length === 0) {
|
|
335
|
-
setTimeout(() => handleNotch(force), 100);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const isLandscape = isLandscapeMode();
|
|
340
|
-
const hasNotch = hasDeviceNotch();
|
|
341
|
-
|
|
342
|
-
// Get orientation
|
|
343
|
-
let orientation = window.orientation;
|
|
344
|
-
if (typeof orientation === "undefined" && screen.orientation) {
|
|
345
|
-
orientation = screen.orientation.angle;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Create state signature to prevent redundant updates
|
|
349
|
-
const currentState = `${isLandscape}-${hasNotch}-${orientation}-${window.innerWidth}x${window.innerHeight}`;
|
|
350
|
-
if (lastNotchState === currentState && !force) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
lastNotchState = currentState;
|
|
354
|
-
|
|
355
|
-
isNotchBusy = true;
|
|
356
|
-
|
|
357
|
-
// Debug info
|
|
358
|
-
if (window.location.href.includes("localhost") || window.location.href.includes("5173")) {
|
|
359
|
-
console.log("🔥 Notch Debug:", {
|
|
360
|
-
isLandscape,
|
|
361
|
-
hasNotch,
|
|
362
|
-
orientation,
|
|
363
|
-
dimensions: `${window.innerWidth}x${window.innerHeight}`,
|
|
364
|
-
state: currentState
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (hasNotch && isLandscape) {
|
|
369
|
-
// Test actual safe area values to determine notch side
|
|
370
|
-
const testDiv = document.createElement("div");
|
|
371
|
-
testDiv.style.position = "fixed";
|
|
372
|
-
testDiv.style.top = "0";
|
|
373
|
-
testDiv.style.left = "0";
|
|
374
|
-
testDiv.style.visibility = "hidden";
|
|
375
|
-
testDiv.style.paddingLeft = "env(safe-area-inset-left)";
|
|
376
|
-
testDiv.style.paddingRight = "env(safe-area-inset-right)";
|
|
377
|
-
document.body.appendChild(testDiv);
|
|
378
|
-
|
|
379
|
-
const computedStyle = getComputedStyle(testDiv);
|
|
380
|
-
const leftInset = parseFloat(computedStyle.paddingLeft) || 0;
|
|
381
|
-
const rightInset = parseFloat(computedStyle.paddingRight) || 0;
|
|
382
|
-
|
|
383
|
-
document.body.removeChild(testDiv);
|
|
384
|
-
|
|
385
|
-
console.log("🔍 Safe area insets:", { leftInset, rightInset });
|
|
77
|
+
// Start spinner unless in development
|
|
78
|
+
if (!window.location.href.includes(":5173")) ui.startSpinner("Loading...");
|
|
386
79
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (leftInset > 0) {
|
|
390
|
-
// Notch on left
|
|
391
|
-
wrapper.style.paddingLeft = "env(safe-area-inset-left)";
|
|
392
|
-
wrapper.style.paddingRight = "0.5rem";
|
|
393
|
-
} else if (rightInset > 0) {
|
|
394
|
-
// Notch on right
|
|
395
|
-
wrapper.style.paddingLeft = "0.5rem";
|
|
396
|
-
wrapper.style.paddingRight = "env(safe-area-inset-right)";
|
|
397
|
-
} else {
|
|
398
|
-
// Fallback - no detectable notch
|
|
399
|
-
wrapper.style.paddingLeft = "0.5rem";
|
|
400
|
-
wrapper.style.paddingRight = "0.5rem";
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
} else {
|
|
404
|
-
// Portrait or no notch
|
|
405
|
-
const padding =
|
|
406
|
-
window.innerWidth > 1280
|
|
407
|
-
? "2.5rem"
|
|
408
|
-
: window.innerWidth > 1030
|
|
409
|
-
? "1.5rem"
|
|
410
|
-
: window.innerWidth > 768
|
|
411
|
-
? "0.5rem"
|
|
412
|
-
: "0.5rem";
|
|
80
|
+
const layout = computed(() => router.currentRoute.value.meta.layout);
|
|
81
|
+
</script>
|
|
413
82
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
});
|
|
419
|
-
}
|
|
83
|
+
<style lang="scss">
|
|
84
|
+
/* iPhone Landscape Lock Overlay */
|
|
85
|
+
.iphone-landscape-lock {
|
|
86
|
+
@apply fixed inset-0 bg-dark-300 z-max hidden items-center justify-center;
|
|
420
87
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
console.warn("Notch error:", error);
|
|
424
|
-
isNotchBusy = false;
|
|
88
|
+
@media (max-device-width: 430px) and (orientation: landscape) {
|
|
89
|
+
display: flex;
|
|
425
90
|
}
|
|
426
91
|
}
|
|
427
92
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
notchTimeout = setTimeout(handleNotch, 50);
|
|
93
|
+
.rotate-message {
|
|
94
|
+
@apply text-center p-8;
|
|
431
95
|
}
|
|
432
96
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (screen.orientation) {
|
|
437
|
-
screen.orientation.addEventListener("change", triggerNotch);
|
|
97
|
+
.rotate-icon {
|
|
98
|
+
@apply w-16 h-16 mx-auto mb-4 text-accent-green;
|
|
99
|
+
animation: rotate-pulse 2s ease-in-out infinite;
|
|
438
100
|
}
|
|
439
101
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
handleNotch(true); // Force first execution
|
|
443
|
-
setTimeout(() => handleNotch(true), 50);
|
|
444
|
-
setTimeout(() => handleNotch(true), 150);
|
|
445
|
-
setTimeout(() => handleNotch(true), 300);
|
|
446
|
-
setTimeout(() => handleNotch(true), 500);
|
|
447
|
-
setTimeout(() => handleNotch(true), 1000);
|
|
448
|
-
setTimeout(() => handleNotch(true), 2000); // Extra long delay for slow loads
|
|
102
|
+
.rotate-text {
|
|
103
|
+
@apply text-white text-lg font-medium;
|
|
449
104
|
}
|
|
450
105
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
initNotchHandling();
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
window.addEventListener("load", initNotchHandling);
|
|
459
|
-
|
|
460
|
-
// Also run when page becomes visible (handles app switching)
|
|
461
|
-
document.addEventListener("visibilitychange", () => {
|
|
462
|
-
if (!document.hidden) {
|
|
463
|
-
setTimeout(handleNotch, 100);
|
|
106
|
+
@keyframes rotate-pulse {
|
|
107
|
+
0%, 100% {
|
|
108
|
+
transform: rotate(0deg) scale(1);
|
|
109
|
+
opacity: 1;
|
|
464
110
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
window.addEventListener("focus", () => {
|
|
469
|
-
setTimeout(handleNotch, 100);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// Watch for ios-wrapper elements appearing in DOM
|
|
473
|
-
const observer = new MutationObserver((mutations) => {
|
|
474
|
-
let shouldTrigger = false;
|
|
475
|
-
|
|
476
|
-
mutations.forEach((mutation) => {
|
|
477
|
-
if (mutation.type === "childList") {
|
|
478
|
-
mutation.addedNodes.forEach((node) => {
|
|
479
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
480
|
-
if (node.classList?.contains("ios-wrapper") || node.querySelector?.(".ios-wrapper")) {
|
|
481
|
-
shouldTrigger = true;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
if (shouldTrigger) {
|
|
489
|
-
setTimeout(() => handleNotch(true), 10);
|
|
490
|
-
setTimeout(() => handleNotch(true), 100);
|
|
111
|
+
50% {
|
|
112
|
+
transform: rotate(90deg) scale(1.1);
|
|
113
|
+
opacity: 0.8;
|
|
491
114
|
}
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
// Start observing
|
|
495
|
-
if (document.body) {
|
|
496
|
-
observer.observe(document.body, {
|
|
497
|
-
childList: true,
|
|
498
|
-
subtree: true
|
|
499
|
-
});
|
|
500
|
-
} else {
|
|
501
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
502
|
-
observer.observe(document.body, {
|
|
503
|
-
childList: true,
|
|
504
|
-
subtree: true
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
115
|
}
|
|
508
116
|
|
|
509
|
-
|
|
510
|
-
const originalPushState = history.pushState;
|
|
511
|
-
const originalReplaceState = history.replaceState;
|
|
512
|
-
|
|
513
|
-
history.pushState = function (...args) {
|
|
514
|
-
originalPushState.apply(this, args);
|
|
515
|
-
triggerNotch();
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
history.replaceState = function (...args) {
|
|
519
|
-
originalReplaceState.apply(this, args);
|
|
520
|
-
triggerNotch();
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
window.addEventListener("popstate", triggerNotch);
|
|
524
|
-
|
|
525
|
-
// Expose for manual triggering
|
|
526
|
-
window.simulateRotate = handleNotch;
|
|
527
|
-
|
|
528
|
-
const pullStart = (e) => {
|
|
529
|
-
const { screenY } = e.targetTouches[0];
|
|
530
|
-
ui.setStartPoint(screenY);
|
|
531
|
-
};
|
|
532
|
-
const initLoading = () => {
|
|
533
|
-
// refreshCont.current.classList.add("loading");
|
|
534
|
-
setTimeout(() => {
|
|
535
|
-
isLoading.value = true;
|
|
536
|
-
}, 500);
|
|
537
|
-
setTimeout(() => {
|
|
538
|
-
window.location.reload();
|
|
539
|
-
}, 1500);
|
|
540
|
-
};
|
|
541
|
-
const pull = (e) => {
|
|
542
|
-
/**
|
|
543
|
-
* get the current user touch event data
|
|
544
|
-
*/
|
|
545
|
-
const touch = e.targetTouches[0];
|
|
546
|
-
/**
|
|
547
|
-
* get the touch position on the screen's Y axis
|
|
548
|
-
*/
|
|
549
|
-
const { screenY } = touch;
|
|
550
|
-
/**
|
|
551
|
-
* The length of the pull
|
|
552
|
-
*
|
|
553
|
-
* if the start touch position is lesser than the current touch position, calculate the difference, which gives the `pullLength`
|
|
554
|
-
*
|
|
555
|
-
* This tells us how much the user has pulled
|
|
556
|
-
*/
|
|
557
|
-
let pullLength = ui.startPoint < screenY ? Math.abs(screenY - ui.startPoint) : 0;
|
|
558
|
-
ui.setPullChange(pullLength);
|
|
559
|
-
};
|
|
560
|
-
const endPull = (e) => {
|
|
561
|
-
if (ui.pullChange > 250) {
|
|
562
|
-
e.preventDefault();
|
|
563
|
-
}
|
|
564
|
-
if (ui.pullChange > 200) initLoading();
|
|
565
|
-
ui.setStartPoint(0);
|
|
566
|
-
ui.setPullChange(0);
|
|
567
|
-
};
|
|
568
|
-
window.addEventListener("touchstart", pullStart);
|
|
569
|
-
window.addEventListener("touchmove", pull);
|
|
570
|
-
window.addEventListener("touchend", endPull);
|
|
571
|
-
|
|
572
|
-
function maxPull() {
|
|
573
|
-
if (ui.pullChange < 250) {
|
|
574
|
-
return ui.pullChange;
|
|
575
|
-
} else {
|
|
576
|
-
ui.setPullChange(250);
|
|
577
|
-
return 250;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
// Vue router integration - aggressive triggering on route changes
|
|
581
|
-
watch(
|
|
582
|
-
() => router.currentRoute.value.name,
|
|
583
|
-
() => {
|
|
584
|
-
// Immediate triggers
|
|
585
|
-
handleNotch();
|
|
586
|
-
triggerNotch();
|
|
587
|
-
|
|
588
|
-
// Staggered attempts
|
|
589
|
-
setTimeout(handleNotch, 10);
|
|
590
|
-
setTimeout(handleNotch, 50);
|
|
591
|
-
setTimeout(handleNotch, 150);
|
|
592
|
-
setTimeout(handleNotch, 300);
|
|
593
|
-
setTimeout(handleNotch, 500);
|
|
594
|
-
}
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
watch(
|
|
598
|
-
() => router.currentRoute.value.path,
|
|
599
|
-
() => {
|
|
600
|
-
// Immediate triggers
|
|
601
|
-
handleNotch();
|
|
602
|
-
triggerNotch();
|
|
603
|
-
|
|
604
|
-
// Staggered attempts
|
|
605
|
-
setTimeout(handleNotch, 25);
|
|
606
|
-
setTimeout(handleNotch, 100);
|
|
607
|
-
setTimeout(handleNotch, 250);
|
|
608
|
-
}
|
|
609
|
-
);
|
|
610
|
-
const layout = computed(() => router.currentRoute.value.meta.layout);
|
|
611
|
-
</script>
|
|
612
|
-
<style lang="scss">
|
|
613
|
-
// Override for scrollable containers - allow proper touch scrolling
|
|
117
|
+
/* Touch scrolling overrides for mobile */
|
|
614
118
|
.table-component,
|
|
615
119
|
.stop-pan,
|
|
616
|
-
.scrollable,
|
|
617
120
|
.overflow-y-auto,
|
|
618
121
|
.vue-recycle-scroller,
|
|
619
122
|
.console,
|
|
620
|
-
[class*="scroll"] {
|
|
123
|
+
[class*="scroll"]:not(.dropdown-menu-portal) {
|
|
621
124
|
touch-action: pan-y !important;
|
|
622
125
|
}
|
|
623
126
|
|
|
127
|
+
/* Dropdown menus need both x/y panning */
|
|
128
|
+
.dropdown-menu-portal {
|
|
129
|
+
touch-action: pan-x pan-y !important;
|
|
130
|
+
}
|
|
131
|
+
|
|
624
132
|
.table-component {
|
|
625
133
|
touch-action: pan-x pan-y !important;
|
|
626
134
|
}
|
|
627
135
|
|
|
136
|
+
/* Dropdown positioning */
|
|
628
137
|
.dropdown {
|
|
629
138
|
position: relative;
|
|
630
139
|
display: inline-block;
|
|
631
140
|
}
|
|
632
141
|
|
|
633
142
|
.dropdown-content {
|
|
634
|
-
@apply bg-dark-500 text-white shadow rounded-lg top-10 left-0;
|
|
143
|
+
@apply bg-dark-500 text-white shadow rounded-lg top-10 left-0 px-4 py-3;
|
|
635
144
|
position: absolute;
|
|
636
145
|
min-width: 160px;
|
|
637
|
-
padding: 12px 16px;
|
|
638
146
|
z-index: 1;
|
|
639
147
|
}
|
|
640
148
|
|
|
149
|
+
/* Router wrapper */
|
|
641
150
|
.router-wrapper {
|
|
642
|
-
@apply pt-
|
|
643
|
-
transition: margin 0.25s;
|
|
151
|
+
@apply pt-16 lg:pt-20;
|
|
644
152
|
z-index: 0;
|
|
645
153
|
}
|
|
646
154
|
|
|
647
|
-
|
|
648
|
-
transition: margin 0.25s;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Page navigation transitions
|
|
155
|
+
/* Page transition animations */
|
|
652
156
|
.page-transition-enter-active {
|
|
653
157
|
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
654
158
|
}
|