@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.
Files changed (137) hide show
  1. package/backend/api.js +2 -3
  2. package/eslint.config.js +46 -0
  3. package/index.html +2 -1
  4. package/package.json +5 -2
  5. package/src/App.vue +70 -566
  6. package/src/assets/css/base/mixins.scss +72 -0
  7. package/src/assets/css/base/reset.scss +0 -2
  8. package/src/assets/css/base/scroll.scss +43 -36
  9. package/src/assets/css/base/typography.scss +9 -10
  10. package/src/assets/css/base/variables.scss +43 -0
  11. package/src/assets/css/components/accessibility.scss +37 -0
  12. package/src/assets/css/components/buttons.scss +61 -74
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +13 -21
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -22
  17. package/src/assets/css/components/tables.scss +5 -7
  18. package/src/assets/css/components/toasts.scss +7 -7
  19. package/src/assets/css/components/utilities.scss +295 -0
  20. package/src/assets/css/main.scss +55 -139
  21. package/src/components/Auth/LoginForm.vue +7 -86
  22. package/src/components/Console/ConsoleToolbar.vue +123 -0
  23. package/src/components/Editors/Account/Account.vue +12 -12
  24. package/src/components/Editors/Account/AccountView.vue +38 -111
  25. package/src/components/Editors/Account/CreateAccount.vue +11 -61
  26. package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +28 -59
  27. package/src/components/Editors/AdminFileEditor.vue +179 -0
  28. package/src/components/Editors/Profile/CreateProfile.vue +77 -150
  29. package/src/components/Editors/Profile/Profile.vue +20 -21
  30. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  31. package/src/components/Editors/Profile/ProfileView.vue +41 -116
  32. package/src/components/Editors/ProxyFileEditor.vue +86 -0
  33. package/src/components/Editors/TagLabel.vue +16 -55
  34. package/src/components/Editors/TagToggle.vue +20 -8
  35. package/src/components/Filter/Filter.vue +66 -79
  36. package/src/components/Filter/FilterPreview.vue +153 -135
  37. package/src/components/Filter/PriceSortToggle.vue +36 -43
  38. package/src/components/Table/Header.vue +1 -1
  39. package/src/components/Table/Table.vue +45 -51
  40. package/src/components/Tasks/CheckStock.vue +7 -16
  41. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  42. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  43. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  44. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  45. package/src/components/Tasks/EventDetailRow.vue +21 -0
  46. package/src/components/Tasks/MassEdit.vue +6 -16
  47. package/src/components/Tasks/QuickSettings.vue +140 -216
  48. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  49. package/src/components/Tasks/Stats.vue +20 -39
  50. package/src/components/Tasks/Task.vue +64 -270
  51. package/src/components/Tasks/TaskLabel.vue +9 -3
  52. package/src/components/Tasks/TaskView.vue +45 -64
  53. package/src/components/Tasks/Utilities.vue +10 -44
  54. package/src/components/Tasks/ViewTask.vue +23 -107
  55. package/src/components/icons/Close.vue +2 -8
  56. package/src/components/icons/Gear.vue +8 -8
  57. package/src/components/icons/Hash.vue +5 -0
  58. package/src/components/icons/Key.vue +2 -8
  59. package/src/components/icons/Pencil.vue +2 -8
  60. package/src/components/icons/Profile.vue +2 -8
  61. package/src/components/icons/Sell.vue +2 -8
  62. package/src/components/icons/Spinner.vue +4 -7
  63. package/src/components/icons/Wildcard.vue +2 -8
  64. package/src/components/icons/index.js +3 -5
  65. package/src/components/ui/ActionButtonGroup.vue +113 -52
  66. package/src/components/ui/BalanceIndicator.vue +60 -0
  67. package/src/components/ui/EmptyState.vue +24 -0
  68. package/src/components/ui/EnableDisableToggle.vue +23 -0
  69. package/src/components/ui/FormField.vue +49 -49
  70. package/src/components/ui/IconLabel.vue +23 -0
  71. package/src/components/ui/InfoRow.vue +21 -54
  72. package/src/components/ui/Modal.vue +161 -54
  73. package/src/components/ui/Navbar.vue +63 -44
  74. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  75. package/src/components/ui/ReconnectIndicator.vue +111 -124
  76. package/src/components/ui/SectionCard.vue +6 -14
  77. package/src/components/ui/Splash.vue +2 -10
  78. package/src/components/ui/StatusBadge.vue +26 -28
  79. package/src/components/ui/TaskToggle.vue +54 -0
  80. package/src/components/ui/controls/CountryChooser.vue +29 -66
  81. package/src/components/ui/controls/EyeToggle.vue +1 -1
  82. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  83. package/src/components/ui/controls/atomic/Dropdown.vue +103 -139
  84. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -120
  85. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  86. package/src/composables/useCodeEditor.js +117 -0
  87. package/src/composables/useColorMapping.js +15 -0
  88. package/src/composables/useCopyToClipboard.js +1 -1
  89. package/src/composables/useDateFormatting.js +21 -0
  90. package/src/composables/useDeviceDetection.js +14 -0
  91. package/src/composables/useDropdownPosition.js +1 -4
  92. package/src/composables/useDynamicTableHeight.js +31 -0
  93. package/src/composables/useEnableDisable.js +6 -0
  94. package/src/composables/useFilterCSS.js +71 -0
  95. package/src/composables/useFormValidation.js +92 -0
  96. package/src/composables/useGetAllTags.js +9 -0
  97. package/src/composables/useIOSViewportHandling.js +76 -0
  98. package/src/composables/useNotchHandling.js +306 -0
  99. package/src/composables/useRowSelection.js +0 -3
  100. package/src/composables/useTableRender.js +23 -0
  101. package/src/composables/useTicketPricing.js +16 -0
  102. package/src/composables/useWindowDimensions.js +21 -0
  103. package/src/composables/useZoomPrevention.js +96 -0
  104. package/src/constants/tableLayout.js +14 -0
  105. package/src/libs/Filter.js +14 -20
  106. package/src/libs/panzoom.js +1 -5
  107. package/src/libs/utils/array.js +58 -0
  108. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  109. package/src/libs/utils/eventUrl.js +40 -0
  110. package/src/libs/utils/string.js +3 -0
  111. package/src/libs/utils/time.js +20 -0
  112. package/src/libs/utils/validation.js +64 -0
  113. package/src/main.js +0 -2
  114. package/src/stores/connection.js +1 -29
  115. package/src/stores/logger.js +6 -12
  116. package/src/stores/sampleData.js +1 -2
  117. package/src/stores/ui.js +80 -71
  118. package/src/utils/tableHelpers.js +1 -0
  119. package/src/views/Accounts.vue +19 -38
  120. package/src/views/Console.vue +74 -253
  121. package/src/views/Editor.vue +47 -1114
  122. package/src/views/FilterBuilder.vue +190 -461
  123. package/src/views/Login.vue +3 -28
  124. package/src/views/Profiles.vue +17 -32
  125. package/src/views/Tasks.vue +51 -38
  126. package/tailwind.config.js +82 -71
  127. package/workbox-config.cjs +47 -5
  128. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2438
  129. package/exit +0 -209
  130. package/run +0 -177
  131. package/src/assets/css/base/color-fallbacks.scss +0 -10
  132. package/src/assets/img/background.svg.backup +0 -11
  133. package/src/components/icons/SquareCheck.vue +0 -18
  134. package/src/components/icons/SquareUncheck.vue +0 -18
  135. package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
  136. package/switch-branch.sh +0 -41
  137. /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 :class="['router-wrapper', { '-mt-[60px]': ui.pullChange > 1 }]"></div>
48
- <router-view v-slot="{ Component, route }">
49
- <transition name="page-transition" mode="out-in">
50
- <component
51
- :is="Component"
52
- :key="route.path"
53
- class="component-container w-full mx-auto px-3 xs:px-3 md:px-2 lg:px-6 xl:px-10 pb-2 mt-0 lg:mt-4 ios-wrapper" />
54
- </transition>
55
- </router-view>
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, watch, onMounted } from "vue";
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
- document.addEventListener('touchstart', (e) => {
91
- if (e.touches.length > 1) {
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
- // Ensure notch handling runs when Vue app mounts
127
- onMounted(() => {
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
- // Set up a recurring check for the first few seconds
136
- let attempts = 0;
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
- // close modals on esc
63
+ // ESC key handling for modals
147
64
  document.onkeydown = function (evt) {
148
65
  evt = evt || window.event;
149
- var isEscape = false;
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 === 27;
70
+ isEscape = evt.keyCode === KEY_CODES.ESCAPE;
154
71
  }
155
72
  if (isEscape) {
156
73
  ui.activeModal = "";
157
74
  }
158
75
  };
159
76
 
160
- // Prevent ALL keyboard zoom combinations
161
- document.addEventListener("keydown", function (event) {
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
- // Apply styles instantly - NO ANIMATION
388
- wrappers.forEach((wrapper) => {
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
- // Apply styles instantly - NO ANIMATION
415
- wrappers.forEach((wrapper) => {
416
- wrapper.style.paddingLeft = padding;
417
- wrapper.style.paddingRight = padding;
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
- isNotchBusy = false;
422
- } catch (error) {
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
- function triggerNotch() {
429
- clearTimeout(notchTimeout);
430
- notchTimeout = setTimeout(handleNotch, 50);
93
+ .rotate-message {
94
+ @apply text-center p-8;
431
95
  }
432
96
 
433
- // Event listeners
434
- window.addEventListener("orientationchange", triggerNotch);
435
- window.addEventListener("resize", triggerNotch);
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
- // Aggressive initial setup to handle all scenarios
441
- function initNotchHandling() {
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
- // Multiple initialization points
452
- if (document.readyState === "loading") {
453
- document.addEventListener("DOMContentLoaded", initNotchHandling);
454
- } else {
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
- // Run on focus (when returning to app)
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
- // Navigation handling
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-5;
643
- transition: margin 0.25s;
151
+ @apply pt-16 lg:pt-20;
644
152
  z-index: 0;
645
153
  }
646
154
 
647
- .refresh-container {
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
  }