@necrolab/dashboard 0.5.14 → 0.5.16

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 (120) 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 +140 -170
  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 +58 -15
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +119 -0
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -19
  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 +220 -0
  20. package/src/assets/css/main.scss +72 -75
  21. package/src/components/Auth/LoginForm.vue +5 -84
  22. package/src/components/Editors/Account/Account.vue +8 -10
  23. package/src/components/Editors/Account/AccountCreator.vue +28 -59
  24. package/src/components/Editors/Account/AccountView.vue +38 -86
  25. package/src/components/Editors/Account/CreateAccount.vue +8 -50
  26. package/src/components/Editors/Profile/CreateProfile.vue +74 -131
  27. package/src/components/Editors/Profile/Profile.vue +15 -17
  28. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  29. package/src/components/Editors/Profile/ProfileView.vue +46 -96
  30. package/src/components/Editors/TagLabel.vue +16 -55
  31. package/src/components/Editors/TagToggle.vue +20 -8
  32. package/src/components/Filter/Filter.vue +62 -75
  33. package/src/components/Filter/FilterPreview.vue +161 -135
  34. package/src/components/Filter/PriceSortToggle.vue +36 -43
  35. package/src/components/Table/Header.vue +1 -1
  36. package/src/components/Table/Table.vue +61 -12
  37. package/src/components/Tasks/CheckStock.vue +7 -16
  38. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  39. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  40. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  41. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  42. package/src/components/Tasks/EventDetailRow.vue +21 -0
  43. package/src/components/Tasks/MassEdit.vue +6 -16
  44. package/src/components/Tasks/QuickSettings.vue +140 -216
  45. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  46. package/src/components/Tasks/Stats.vue +19 -38
  47. package/src/components/Tasks/Task.vue +65 -268
  48. package/src/components/Tasks/TaskLabel.vue +9 -3
  49. package/src/components/Tasks/TaskView.vue +43 -63
  50. package/src/components/Tasks/Utilities.vue +10 -42
  51. package/src/components/Tasks/ViewTask.vue +23 -107
  52. package/src/components/icons/Close.vue +2 -8
  53. package/src/components/icons/Gear.vue +8 -8
  54. package/src/components/icons/Hash.vue +5 -0
  55. package/src/components/icons/Key.vue +2 -8
  56. package/src/components/icons/Pencil.vue +2 -8
  57. package/src/components/icons/Profile.vue +2 -8
  58. package/src/components/icons/Sell.vue +2 -8
  59. package/src/components/icons/Spinner.vue +4 -7
  60. package/src/components/icons/SquareCheck.vue +2 -8
  61. package/src/components/icons/SquareUncheck.vue +2 -8
  62. package/src/components/icons/Wildcard.vue +2 -8
  63. package/src/components/icons/index.js +3 -1
  64. package/src/components/ui/ActionButtonGroup.vue +113 -52
  65. package/src/components/ui/BalanceIndicator.vue +60 -0
  66. package/src/components/ui/EmptyState.vue +24 -0
  67. package/src/components/ui/EnableDisableToggle.vue +23 -0
  68. package/src/components/ui/FormField.vue +48 -48
  69. package/src/components/ui/IconLabel.vue +23 -0
  70. package/src/components/ui/InfoRow.vue +21 -54
  71. package/src/components/ui/Modal.vue +78 -37
  72. package/src/components/ui/Navbar.vue +60 -41
  73. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  74. package/src/components/ui/ReconnectIndicator.vue +111 -124
  75. package/src/components/ui/SectionCard.vue +6 -14
  76. package/src/components/ui/Splash.vue +2 -10
  77. package/src/components/ui/StatusBadge.vue +26 -28
  78. package/src/components/ui/TaskToggle.vue +54 -0
  79. package/src/components/ui/controls/CountryChooser.vue +27 -64
  80. package/src/components/ui/controls/EyeToggle.vue +1 -1
  81. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  82. package/src/components/ui/controls/atomic/Dropdown.vue +102 -95
  83. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -94
  84. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  85. package/src/composables/useColorMapping.js +15 -0
  86. package/src/composables/useCopyToClipboard.js +1 -1
  87. package/src/composables/useDateFormatting.js +21 -0
  88. package/src/composables/useDeviceDetection.js +14 -0
  89. package/src/composables/useDropdownPosition.js +5 -6
  90. package/src/composables/useDynamicTableHeight.js +31 -0
  91. package/src/composables/useRowSelection.js +0 -3
  92. package/src/composables/useTicketPricing.js +16 -0
  93. package/src/composables/useWindowDimensions.js +21 -0
  94. package/src/libs/Filter.js +14 -20
  95. package/src/libs/panzoom.js +1 -5
  96. package/src/libs/utils/array.js +60 -0
  97. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  98. package/src/libs/utils/eventUrl.js +40 -0
  99. package/src/libs/utils/string.js +28 -0
  100. package/src/libs/utils/time.js +20 -0
  101. package/src/libs/utils/validation.js +88 -0
  102. package/src/main.js +0 -2
  103. package/src/stores/connection.js +1 -4
  104. package/src/stores/logger.js +6 -12
  105. package/src/stores/sampleData.js +1 -2
  106. package/src/stores/ui.js +59 -36
  107. package/src/views/Accounts.vue +17 -31
  108. package/src/views/Console.vue +76 -176
  109. package/src/views/Editor.vue +217 -383
  110. package/src/views/FilterBuilder.vue +190 -373
  111. package/src/views/Login.vue +3 -28
  112. package/src/views/Profiles.vue +12 -22
  113. package/src/views/Tasks.vue +51 -38
  114. package/tailwind.config.js +82 -71
  115. package/workbox-config.cjs +47 -5
  116. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2416
  117. package/exit +0 -209
  118. package/run +0 -177
  119. package/switch-branch.sh +0 -41
  120. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
package/backend/api.js CHANGED
@@ -147,7 +147,7 @@ app.ws("/api/updates", async function (ws, req) {
147
147
  { event: "set-button-disabled", button: "add-accounts", value: false }
148
148
  ])
149
149
  );
150
- setTimeout(() => send({ event: "init-tasks", tasks: endpoints.getStrippedTasks(currentUser.name) }));
150
+ setTimeout(() => send({ event: "init-tasks", tasks: endpoints.getStrippedTasks() }));
151
151
  setTimeout(() => send({ event: "init-profiles", profiles: Bot.Profiles }));
152
152
  setTimeout(() => send({ event: "init-tm-accounts", accounts: Bot.TM.Accounts }));
153
153
  setTimeout(() => send({ event: "init-axs-accounts", accounts: Bot.AXS.Accounts }));
@@ -429,8 +429,7 @@ app.post("/api/proxies", async (req, res) => {
429
429
  });
430
430
 
431
431
  app.post("/api/userconfig/set", async (req, res) => {
432
- // eslint-disable-next-line no-unused-vars
433
- const { field, value } = req.body;
432
+ const { value } = req.body;
434
433
 
435
434
  users[0].proxyList = value;
436
435
 
@@ -0,0 +1,46 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import globals from "globals";
3
+ import vue from "eslint-plugin-vue";
4
+ import js from "@eslint/js";
5
+ import { FlatCompat } from "@eslint/eslintrc";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname } from "node:path";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const compat = new FlatCompat({
11
+ baseDirectory: __dirname,
12
+ recommendedConfig: js.configs.recommended,
13
+ allConfig: js.configs.all
14
+ });
15
+
16
+ export default defineConfig([
17
+ ...compat.extends("eslint:recommended", "plugin:vue/vue3-essential"),
18
+ {
19
+ languageOptions: {
20
+ globals: {
21
+ ...globals.browser,
22
+ ...globals.node,
23
+ Bot: true,
24
+ refreshTaskOnFrontEnd: true,
25
+ pushWSUpdate: true,
26
+ __APP_VERSION__: "readonly"
27
+ },
28
+ ecmaVersion: "latest",
29
+ sourceType: "module",
30
+ parserOptions: {}
31
+ },
32
+ plugins: {
33
+ vue
34
+ },
35
+ rules: {
36
+ "html.validate.scripts": 0,
37
+ "html.validate.styles": 0,
38
+ "vue/multi-word-component-names": "off",
39
+ "no-unused-vars": [
40
+ "warn",
41
+ { vars: "all", args: "after-used", ignoreRestSiblings: true }
42
+ ]
43
+ }
44
+ },
45
+ globalIgnores(["src/registerServiceWorker.js", "src/libs/panzoom.js"])
46
+ ]);
package/index.html CHANGED
@@ -9,6 +9,7 @@
9
9
  />
10
10
  <!-- Lock iPhone to portrait orientation only -->
11
11
  <meta name="apple-mobile-web-app-capable" content="yes" />
12
+ <meta name="mobile-web-app-capable" content="yes" />
12
13
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13
14
  <meta name="description" content="Necro Lab - dashboard" />
14
15
  <meta name="darkreader-lock" />
@@ -36,8 +37,8 @@
36
37
  />
37
38
 
38
39
  <!-- Preload critical assets -->
39
- <link rel="preload" as="image" href="/img/background.svg" />
40
40
  <link rel="preload" as="image" href="/img/logo_trans.png" />
41
+ <link rel="preload" as="image" href="/img/reconnect-logo.png" />
41
42
 
42
43
  <!-- Prefetch core navigation icons -->
43
44
  <link rel="prefetch" as="image" href="/img/close.svg" />
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@necrolab/dashboard",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "build": "rm -rf dist && npx workbox-cli generateSW workbox-config.cjs && vite build",
6
+ "build": "rm -rf dist && vite build && npx workbox-cli generateSW workbox-config.cjs",
7
7
  "dev": "node dev-server.js",
8
8
  "bot": "node dev-server.js",
9
9
  "expose": "node dev-server.js",
@@ -42,8 +42,11 @@
42
42
  },
43
43
  "main": "index.js",
44
44
  "devDependencies": {
45
+ "@eslint/eslintrc": "^3.3.3",
46
+ "@eslint/js": "^9.39.2",
45
47
  "eslint": "^9.17.0",
46
48
  "eslint-plugin-vue": "^9.32.0",
49
+ "globals": "^17.3.0",
47
50
  "workbox-cli": "^7.3.0"
48
51
  },
49
52
  "overrides": {
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>
@@ -66,27 +42,18 @@ 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 { DEBUG } from "@/utils/debug";
46
+ import { useDeviceDetection } from "@/composables/useDeviceDetection";
69
47
 
70
48
  const ui = useUIStore();
49
+ const { showSpinner: spinner } = storeToRefs(ui);
71
50
  const router = useRouter();
72
51
  const isLoading = ref(false);
73
- const spinner = storeToRefs(ui).showSpinner;
52
+ const showLandscapeLock = ref(false);
74
53
 
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
- }
54
+ const { isIOS, isIpadOS } = useDeviceDetection();
86
55
 
87
56
  // Prevent pinch-to-zoom gestures
88
- let lastTouchDistance = 0;
89
-
90
57
  document.addEventListener('touchstart', (e) => {
91
58
  if (e.touches.length > 1) {
92
59
  e.preventDefault();
@@ -123,34 +90,54 @@ document.addEventListener('touchend', (e) => {
123
90
 
124
91
  if (!window.location.href.includes(":5173")) ui.startSpinner("Loading...");
125
92
 
93
+ // Handle iPhone landscape lock
94
+ function updateLandscapeLock() {
95
+ if (isIOS() && !isIpadOS()) {
96
+ showLandscapeLock.value = isLandscapeMode();
97
+ }
98
+ }
99
+
100
+ // Update on orientation/resize
101
+ window.addEventListener("orientationchange", updateLandscapeLock);
102
+ window.addEventListener("resize", updateLandscapeLock);
103
+ updateLandscapeLock();
104
+
126
105
  // Ensure notch handling runs when Vue app mounts
127
106
  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);
134
-
135
- // Set up a recurring check for the first few seconds
107
+ // Optimized: Single debounced handler instead of multiple setTimeouts
136
108
  let attempts = 0;
137
- const recurringCheck = setInterval(() => {
138
- if (attempts++ > 10) {
139
- clearInterval(recurringCheck);
140
- return;
141
- }
142
- handleNotch();
143
- }, 200);
109
+ const maxAttempts = 5;
110
+ const delays = [0, 50, 150, 300, 500];
111
+
112
+ const scheduleNextAttempt = (index) => {
113
+ if (index >= maxAttempts) return;
114
+ setTimeout(() => {
115
+ handleNotch();
116
+ scheduleNextAttempt(index + 1);
117
+ }, delays[index]);
118
+ };
119
+
120
+ scheduleNextAttempt(0);
144
121
  });
145
122
 
146
- // close modals on esc
123
+ // Keyboard key codes
124
+ const KEY_CODES = {
125
+ ESCAPE: 27,
126
+ EQUAL: 61,
127
+ NUMPAD_PLUS: 107,
128
+ FIREFOX_MINUS: 173,
129
+ NUMPAD_MINUS: 109,
130
+ CHROME_EQUAL: 187,
131
+ MINUS: 189
132
+ };
133
+
147
134
  document.onkeydown = function (evt) {
148
135
  evt = evt || window.event;
149
- var isEscape = false;
136
+ let isEscape = false;
150
137
  if ("key" in evt) {
151
138
  isEscape = evt.key === "Escape" || evt.key === "Esc";
152
139
  } else {
153
- isEscape = evt.keyCode === 27;
140
+ isEscape = evt.keyCode === KEY_CODES.ESCAPE;
154
141
  }
155
142
  if (isEscape) {
156
143
  ui.activeModal = "";
@@ -162,18 +149,18 @@ document.addEventListener("keydown", function (event) {
162
149
  // Prevent Ctrl/Cmd + Plus/Minus/0 (various keycodes for different browsers)
163
150
  if (
164
151
  (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 ||
152
+ (event.which === KEY_CODES.EQUAL ||
153
+ event.which === KEY_CODES.NUMPAD_PLUS ||
154
+ event.which === KEY_CODES.FIREFOX_MINUS ||
155
+ event.which === KEY_CODES.NUMPAD_MINUS ||
156
+ event.which === KEY_CODES.CHROME_EQUAL ||
157
+ event.which === KEY_CODES.MINUS ||
158
+ event.keyCode === KEY_CODES.EQUAL ||
159
+ event.keyCode === KEY_CODES.NUMPAD_PLUS ||
160
+ event.keyCode === KEY_CODES.FIREFOX_MINUS ||
161
+ event.keyCode === KEY_CODES.NUMPAD_MINUS ||
162
+ event.keyCode === KEY_CODES.CHROME_EQUAL ||
163
+ event.keyCode === KEY_CODES.MINUS ||
177
164
  event.key === '+' ||
178
165
  event.key === '-' ||
179
166
  event.key === '=' ||
@@ -233,9 +220,7 @@ if (isIOSDevice) {
233
220
  // Simplified scroll control - let scrollable elements handle their own scrolling
234
221
  // Page scroll is already prevented by overflow: hidden on html/body
235
222
 
236
- // DOMMouseScroll handler removed - CSS overflow handles scroll prevention
237
223
 
238
- // Wheel and scroll handlers removed - CSS overflow: hidden on html/body prevents page scroll
239
224
 
240
225
  // Precise scroll control - only allow scrolling within specific scrollable elements
241
226
  window.addEventListener(
@@ -282,12 +267,9 @@ window.addEventListener(
282
267
  { passive: false }
283
268
  );
284
269
 
285
- // PERFECT notch handling - simple and bulletproof
286
270
  let notchTimeout;
287
271
  let isNotchBusy = false;
288
272
  let lastNotchState = null;
289
- let animationTimeout;
290
- let pendingUpdate = false;
291
273
 
292
274
  function isLandscapeMode() {
293
275
  // Check orientation first
@@ -354,9 +336,8 @@ function handleNotch(force = false) {
354
336
 
355
337
  isNotchBusy = true;
356
338
 
357
- // Debug info
358
- if (window.location.href.includes("localhost") || window.location.href.includes("5173")) {
359
- console.log("🔥 Notch Debug:", {
339
+ if (DEBUG) {
340
+ ui.logger.Debug("🔥 Notch Debug:", {
360
341
  isLandscape,
361
342
  hasNotch,
362
343
  orientation,
@@ -382,7 +363,7 @@ function handleNotch(force = false) {
382
363
 
383
364
  document.body.removeChild(testDiv);
384
365
 
385
- console.log("🔍 Safe area insets:", { leftInset, rightInset });
366
+ if (DEBUG) ui.logger.Debug("🔍 Safe area insets:", { leftInset, rightInset });
386
367
 
387
368
  // Apply styles instantly - NO ANIMATION
388
369
  wrappers.forEach((wrapper) => {
@@ -401,15 +382,22 @@ function handleNotch(force = false) {
401
382
  }
402
383
  });
403
384
  } else {
404
- // Portrait or no notch
385
+ // Portrait or no notch - Responsive padding constants
386
+ const RESPONSIVE_PADDING = {
387
+ XL: { minWidth: 1280, padding: "2.5rem" },
388
+ LG: { minWidth: 1030, padding: "1.5rem" },
389
+ MD: { minWidth: 768, padding: "0.5rem" },
390
+ DEFAULT: { padding: "0.5rem" }
391
+ };
392
+
405
393
  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";
394
+ window.innerWidth > RESPONSIVE_PADDING.XL.minWidth
395
+ ? RESPONSIVE_PADDING.XL.padding
396
+ : window.innerWidth > RESPONSIVE_PADDING.LG.minWidth
397
+ ? RESPONSIVE_PADDING.LG.padding
398
+ : window.innerWidth > RESPONSIVE_PADDING.MD.minWidth
399
+ ? RESPONSIVE_PADDING.MD.padding
400
+ : RESPONSIVE_PADDING.DEFAULT.padding;
413
401
 
414
402
  // Apply styles instantly - NO ANIMATION
415
403
  wrappers.forEach((wrapper) => {
@@ -420,7 +408,7 @@ function handleNotch(force = false) {
420
408
 
421
409
  isNotchBusy = false;
422
410
  } catch (error) {
423
- console.warn("Notch error:", error);
411
+ if (DEBUG) ui.logger.Yellow("Notch error:", error);
424
412
  isNotchBusy = false;
425
413
  }
426
414
  }
@@ -525,58 +513,7 @@ window.addEventListener("popstate", triggerNotch);
525
513
  // Expose for manual triggering
526
514
  window.simulateRotate = handleNotch;
527
515
 
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
- }
516
+ // Pull-to-refresh removed per user request
580
517
  // Vue router integration - aggressive triggering on route changes
581
518
  watch(
582
519
  () => router.currentRoute.value.name,
@@ -610,45 +547,78 @@ watch(
610
547
  const layout = computed(() => router.currentRoute.value.meta.layout);
611
548
  </script>
612
549
  <style lang="scss">
613
- // Override for scrollable containers - allow proper touch scrolling
550
+ /* iPhone Landscape Lock Overlay */
551
+ .iphone-landscape-lock {
552
+ @apply fixed inset-0 bg-dark-300 z-max hidden items-center justify-center;
553
+
554
+ @media (max-device-width: 430px) and (orientation: landscape) {
555
+ display: flex;
556
+ }
557
+ }
558
+
559
+ .rotate-message {
560
+ @apply text-center p-8;
561
+ }
562
+
563
+ .rotate-icon {
564
+ @apply w-16 h-16 mx-auto mb-4 text-accent-green;
565
+ animation: rotate-pulse 2s ease-in-out infinite;
566
+ }
567
+
568
+ .rotate-text {
569
+ @apply text-white text-lg font-medium;
570
+ }
571
+
572
+ @keyframes rotate-pulse {
573
+ 0%, 100% {
574
+ transform: rotate(0deg) scale(1);
575
+ opacity: 1;
576
+ }
577
+ 50% {
578
+ transform: rotate(90deg) scale(1.1);
579
+ opacity: 0.8;
580
+ }
581
+ }
582
+
583
+ /* Touch scrolling overrides for mobile */
614
584
  .table-component,
615
585
  .stop-pan,
616
- .scrollable,
617
586
  .overflow-y-auto,
618
587
  .vue-recycle-scroller,
619
588
  .console,
620
- [class*="scroll"] {
589
+ [class*="scroll"]:not(.dropdown-menu-portal) {
621
590
  touch-action: pan-y !important;
622
591
  }
623
592
 
593
+ /* Dropdown menus need both x/y panning */
594
+ .dropdown-menu-portal {
595
+ touch-action: pan-x pan-y !important;
596
+ }
597
+
624
598
  .table-component {
625
599
  touch-action: pan-x pan-y !important;
626
600
  }
627
601
 
602
+ /* Dropdown positioning */
628
603
  .dropdown {
629
604
  position: relative;
630
605
  display: inline-block;
631
606
  }
632
607
 
633
608
  .dropdown-content {
634
- @apply bg-dark-500 text-white shadow rounded-lg top-10 left-0;
609
+ @apply bg-dark-500 text-white shadow rounded-lg top-10 left-0 px-4 py-3;
635
610
  position: absolute;
636
611
  min-width: 160px;
637
- padding: 12px 16px;
638
612
  z-index: 1;
639
613
  }
640
614
 
615
+ /* Router wrapper */
641
616
  .router-wrapper {
642
- @apply pt-5;
643
- transition: margin 0.25s;
617
+ @apply pt-16 lg:pt-20;
644
618
  z-index: 0;
645
619
  }
646
620
 
647
- .refresh-container {
648
- transition: margin 0.25s;
649
- }
650
-
651
- // Page navigation transitions
621
+ /* Page transition animations */
652
622
  .page-transition-enter-active {
653
623
  transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
654
624
  }
@@ -0,0 +1,72 @@
1
+ /* ==========================================================================
2
+ MIXINS - Reusable SCSS patterns
3
+ ========================================================================== */
4
+
5
+ /**
6
+ * Scale hover effect - subtle zoom on hover/active
7
+ * Used across buttons and interactive elements
8
+ */
9
+ @mixin scale-hover {
10
+ &:hover {
11
+ transform: scale(1.03);
12
+ }
13
+
14
+ &:active {
15
+ transform: scale(0.98);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Scale hover for elements with disabled state
21
+ * Only applies transform when not disabled
22
+ */
23
+ @mixin scale-hover-safe {
24
+ &:hover:not(:disabled) {
25
+ transform: scale(1.03);
26
+ }
27
+
28
+ &:active:not(:disabled) {
29
+ transform: scale(0.98);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Focus ring with accent green color
35
+ * Replaces repeated outline pattern across components
36
+ */
37
+ @mixin focus-ring-accent {
38
+ @apply outline outline-1 outline-accent-green outline-offset-0;
39
+ }
40
+
41
+ /**
42
+ * User select control with vendor prefixes
43
+ * @param {string} $value - none, text, auto, all
44
+ */
45
+ @mixin user-select($value) {
46
+ -webkit-user-select: $value;
47
+ -moz-user-select: $value;
48
+ -ms-user-select: $value;
49
+ user-select: $value;
50
+ }
51
+
52
+ /**
53
+ * Touch scroll reset for dropdowns and scrollable areas
54
+ * Enables smooth touch scrolling on mobile devices
55
+ */
56
+ @mixin touch-scroll-reset {
57
+ overscroll-behavior: auto !important;
58
+ touch-action: pan-y !important;
59
+ -webkit-overflow-scrolling: touch !important;
60
+ }
61
+
62
+ /**
63
+ * Dark button base styling
64
+ * Common pattern for action buttons and modal controls
65
+ */
66
+ @mixin dark-button-base {
67
+ @apply bg-dark-400 border border-dark-650 text-white;
68
+ }
69
+
70
+ @mixin transition-standard {
71
+ @apply transition-all duration-150;
72
+ }
@@ -17,7 +17,6 @@ button {
17
17
  -webkit-tap-highlight-color: transparent;
18
18
  -webkit-touch-callout: none;
19
19
  outline: none;
20
- /* Prevent zoom on input focus (mobile) */
21
20
  font-size: 16px;
22
21
  }
23
22
 
@@ -37,7 +36,6 @@ select:focus {
37
36
  box-shadow: none !important;
38
37
  }
39
38
 
40
- /* Remove number input spinners */
41
39
  input[type="number"] {
42
40
  -moz-appearance: textfield;
43
41
  }