@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
@@ -18,8 +18,7 @@
18
18
  class="dropdown-menu-portal multi scrollable"
19
19
  :style="menuStyle"
20
20
  @click.stop
21
- @wheel.stop
22
- @touchmove.stop>
21
+ @wheel.stop>
23
22
  <div class="option-list scrollable">
24
23
  <button
25
24
  v-for="(option, i) in props.options"
@@ -72,13 +71,34 @@ import { useClickOutside } from "@/composables/useClickOutside";
72
71
  const ui = useUIStore();
73
72
 
74
73
  const props = defineProps({
75
- onSelect: { type: Function },
76
- default: { type: String },
77
- options: { type: Array, required: true },
78
- rightAmount: { type: String },
79
- topPadding: { type: String },
80
- capitalize: { type: Boolean },
81
- includeAdjacentButtons: { type: Boolean, default: false }
74
+ onSelect: {
75
+ type: Function,
76
+ default: null
77
+ },
78
+ default: {
79
+ type: String,
80
+ default: 'Select options...'
81
+ },
82
+ options: {
83
+ type: Array,
84
+ required: true
85
+ },
86
+ rightAmount: {
87
+ type: String,
88
+ default: ''
89
+ },
90
+ topPadding: {
91
+ type: String,
92
+ default: ''
93
+ },
94
+ capitalize: {
95
+ type: Boolean,
96
+ default: false
97
+ },
98
+ includeAdjacentButtons: {
99
+ type: Boolean,
100
+ default: false
101
+ }
82
102
  });
83
103
 
84
104
  const selectedOptions = ref([]);
@@ -192,24 +212,22 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
192
212
 
193
213
  <style scoped>
194
214
  .dropdown {
195
- @apply relative w-full h-10 text-white ml-auto rounded-lg ring-0;
196
- background: linear-gradient(135deg, oklch(0.18 0 0) 0%, oklch(0.20 0 0) 100%);
197
- border: 1px solid oklch(0.26 0 0);
215
+ @apply relative w-full h-10 text-white ml-auto rounded-lg ring-0 border-2;
216
+ @apply bg-bg-input border-border;
198
217
  padding: 0.75rem;
199
218
  }
200
219
 
201
220
  .dropdown:hover {
202
- border-color: oklch(0.30 0 0);
221
+ @apply border-dark-650;
203
222
  }
204
223
 
205
224
  .dropdown:focus-within,
206
225
  .dropdown.opened {
207
- border-color: oklch(0.72 0.15 145) !important;
208
- outline: 1px solid oklch(0.72 0.15 145) !important;
209
- outline-offset: 0;
226
+ @apply border-primary outline outline-1 outline-primary;
227
+ outline-offset: 0 !important;
210
228
  }
211
229
 
212
- @media (max-width: 810px) {
230
+ @screen modal {
213
231
  .dropdown {
214
232
  @apply h-10;
215
233
  padding: 0.625rem;
@@ -225,40 +243,30 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
225
243
  }
226
244
 
227
245
  .dropdown-counter {
228
- @apply flex items-center gap-2 absolute;
229
- right: 0.75rem;
230
- top: 50%;
231
- transform: translateY(-50%);
246
+ @apply flex items-center gap-2 absolute right-2;
232
247
  }
233
248
 
234
249
  .counter-badge {
235
- @apply text-white text-xs font-semibold px-1.5 py-0.5 rounded-full text-center min-w-[18px] shadow-sm;
236
- background: oklch(0.72 0.15 145);
250
+ @apply text-white text-xs font-semibold px-1.5 py-0.5 rounded-full text-center min-w-4.5 shadow-sm;
251
+ @apply bg-primary;
237
252
  }
238
253
 
239
254
  .dropdown-arrow {
240
255
  @apply min-w-4 min-h-4 transition-all duration-300;
241
- display: flex;
242
- align-items: center;
243
- justify-content: center;
244
256
  }
245
257
 
246
258
  .dropdown-menu-portal {
247
- @apply rounded-xl shadow-2xl;
248
- background: linear-gradient(135deg, oklch(0.18 0 0) 0%, oklch(0.20 0 0) 100%);
249
- border: 1px solid oklch(0.26 0 0);
250
- backdrop-filter: blur(12px);
251
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2),
252
- 0 0 0 1px rgba(255, 255, 255, 0.05);
253
- overflow-x: auto !important;
254
- overflow-y: auto !important;
255
- overscroll-behavior: contain !important;
256
- touch-action: pan-x pan-y !important;
257
- -webkit-overflow-scrolling: touch !important;
258
- scrollbar-width: thin;
259
- scrollbar-color: oklch(0.35 0 0) oklch(0.19 0 0);
259
+ @apply rounded-xl z-modal border-2 border-dark-650 bg-dark-400 shadow-dropdown;
260
260
  min-width: 200px;
261
- max-width: 90vw;
261
+ max-width: min(90vw, 600px);
262
+ overflow-x: auto;
263
+ overflow-y: auto;
264
+ overscroll-behavior: contain;
265
+ touch-action: pan-x pan-y;
266
+ -webkit-overflow-scrolling: touch;
267
+ scrollbar-width: thin;
268
+ scrollbar-color: theme('colors.dark.750') theme('colors.dark.350');
269
+ scrollbar-gutter: stable;
262
270
  }
263
271
 
264
272
  .dropdown-menu-portal::-webkit-scrollbar {
@@ -267,31 +275,37 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
267
275
  }
268
276
 
269
277
  .dropdown-menu-portal::-webkit-scrollbar-track {
270
- background: oklch(0.19 0 0);
278
+ @apply bg-transparent;
279
+ margin: 12px 0;
271
280
  }
272
281
 
273
282
  .dropdown-menu-portal::-webkit-scrollbar-thumb {
274
- background: oklch(0.35 0 0);
275
- border-radius: 3px;
283
+ @apply bg-dark-750 rounded-full;
284
+ border: 2px solid transparent;
285
+ background-clip: padding-box;
276
286
  }
277
287
 
278
288
  .dropdown-menu-portal::-webkit-scrollbar-thumb:hover {
279
- background: oklch(0.45 0 0);
289
+ @apply bg-dark-800;
280
290
  }
281
291
 
282
292
  .dropdown-menu-portal.multi .option-list {
283
293
  @apply max-h-48 overflow-y-auto;
284
- overscroll-behavior: contain !important;
285
- touch-action: pan-y !important;
286
- -webkit-overflow-scrolling: touch !important;
294
+ overscroll-behavior: contain;
295
+ -webkit-overflow-scrolling: touch;
296
+ scrollbar-width: none;
297
+ -ms-overflow-style: none;
298
+ touch-action: pan-y;
299
+ }
300
+
301
+ .dropdown-menu-portal.multi .option-list::-webkit-scrollbar {
302
+ display: none;
287
303
  }
288
304
 
289
305
  .dropdown-item {
290
306
  @apply cursor-pointer text-left w-full text-white transition-all duration-200 flex justify-between items-center;
291
- padding: 0.75rem 1rem;
292
- font-size: 0.875rem;
293
- font-weight: 500;
294
- border-bottom: 1px solid rgba(61, 62, 68, 0.3);
307
+ @apply px-4 py-3 text-sm font-medium;
308
+ border-bottom: 1px solid theme('colors.dark.500 / 0.3');
295
309
  }
296
310
 
297
311
  .dropdown-item:last-child {
@@ -299,13 +313,11 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
299
313
  }
300
314
 
301
315
  .dropdown-item:hover {
302
- @apply bg-dark-600;
303
- color: oklch(1 0 0);
316
+ @apply bg-dark-550 text-white;
304
317
  }
305
318
 
306
319
  .dropdown-item:active {
307
- @apply bg-dark-650;
308
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);
320
+ @apply bg-dark-650 shadow-input-inset;
309
321
  }
310
322
 
311
323
  .dropdown-item:first-child {
@@ -323,13 +335,12 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
323
335
 
324
336
  /* Checkmark styling */
325
337
  .dropdown-item svg {
326
- @apply w-4 h-4;
327
- color: oklch(0.72 0.15 145);
338
+ @apply w-4 h-4 text-primary;
328
339
  }
329
340
 
330
341
  .selected-summary {
331
342
  @apply border-t bg-dark-550 w-full px-4 py-3;
332
- border-top: 1px solid rgba(61, 62, 68, 0.5);
343
+ border-top: 1px solid theme('colors.dark.500 / 0.5');
333
344
  }
334
345
 
335
346
  .selected-count {
@@ -338,7 +349,7 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
338
349
 
339
350
  .count-badge {
340
351
  @apply text-white text-xs font-semibold px-2 py-1 rounded-full shadow-sm;
341
- background: oklch(0.72 0.15 145);
352
+ @apply bg-primary;
342
353
  }
343
354
 
344
355
  .count-label {
@@ -347,53 +358,20 @@ if (props.default && !selectedOptions.value.includes(props.default)) {
347
358
 
348
359
  .clear-button {
349
360
  @apply text-xs text-white transition-all duration-200 font-medium px-2 py-1.5 rounded-lg shadow-sm flex items-center justify-center;
350
- background: oklch(0.55 0.22 25);
361
+ @apply bg-red-400 hover:bg-red-300;
351
362
  min-width: 32px;
352
363
  }
353
364
 
354
- .clear-button:hover {
355
- background: oklch(0.60 0.22 25);
356
- }
357
-
358
365
  .done-button {
359
366
  @apply text-xs text-white transition-all duration-200 font-medium px-2 py-1.5 rounded-lg shadow-sm flex items-center justify-center;
360
- background: oklch(0.72 0.15 145);
367
+ @apply bg-primary hover:bg-green-400;
361
368
  min-width: 32px;
362
369
  }
363
370
 
364
- .done-button:hover {
365
- background: oklch(0.68 0.15 145);
366
- }
367
-
368
371
  @media (min-width: 640px) {
369
372
  .clear-button,
370
373
  .done-button {
371
374
  padding: 0.375rem 0.75rem;
372
375
  }
373
376
  }
374
-
375
- /* Transition animations */
376
- .dropdown-fade-enter-active {
377
- @apply transition-all duration-300;
378
- }
379
-
380
- .dropdown-fade-leave-active {
381
- @apply transition-all duration-200;
382
- }
383
-
384
- .dropdown-fade-enter-from {
385
- @apply opacity-0;
386
- transform: translateY(-8px) scale(0.95);
387
- }
388
-
389
- .dropdown-fade-leave-to {
390
- @apply opacity-0;
391
- transform: translateY(-4px) scale(0.98);
392
- }
393
-
394
- .dropdown-fade-enter-to,
395
- .dropdown-fade-leave-from {
396
- @apply opacity-100;
397
- transform: translateY(0) scale(1);
398
- }
399
377
  </style>
@@ -1,96 +1,33 @@
1
1
  <template>
2
- <label class="switch" :class="{ 'disabled': disabled }">
3
- <input type="checkbox" v-model="value" :disabled="disabled" />
4
- <span class="slider round"></span>
2
+ <label
3
+ class="relative inline-block w-[51px] h-[31px] max-modal:w-11 max-modal:min-w-11 max-modal:h-6.5"
4
+ :class="disabled ? 'pointer-events-none' : ''"
5
+ >
6
+ <input
7
+ type="checkbox"
8
+ v-model="value"
9
+ :disabled="disabled"
10
+ class="opacity-0 w-0 h-0 peer"
11
+ role="switch"
12
+ :aria-checked="value"
13
+ />
14
+ <span
15
+ class="absolute inset-0 cursor-pointer border-2 rounded-[31px] transition-all duration-300 ease-out
16
+ before:absolute before:content-[''] before:size-[23px] before:left-0.5 before:bottom-0.5 before:rounded-full before:shadow-switch before:transition-all before:duration-300 before:ease-out
17
+ bg-dark-550 border-dark-550 before:bg-dark-200
18
+ peer-checked:bg-accent-green peer-checked:border-accent-green peer-checked:before:bg-white peer-checked:before:translate-x-5
19
+ peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:before:opacity-70
20
+ max-modal:before:size-[22px] max-modal:peer-checked:before:translate-x-[18px]"
21
+ ></span>
5
22
  </label>
6
23
  </template>
7
24
 
8
25
  <script setup>
9
26
  const value = defineModel();
10
- const props = defineProps({
27
+ defineProps({
11
28
  disabled: {
12
29
  type: Boolean,
13
30
  default: false
14
31
  }
15
32
  });
16
33
  </script>
17
-
18
- <style lang="scss" scoped>
19
- /* iOS-style switch */
20
- .switch {
21
- @apply relative inline-block;
22
- width: 51px;
23
- height: 31px;
24
- }
25
-
26
- /* Hide default HTML checkbox */
27
- .switch input {
28
- @apply opacity-0 w-0 h-0;
29
- }
30
-
31
- /* The slider */
32
- .slider {
33
- @apply absolute cursor-pointer inset-0;
34
- @apply border-2;
35
- background-color: oklch(0.26 0 0);
36
- border-color: oklch(0.35 0 0);
37
- transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
38
- }
39
-
40
- .slider:before {
41
- @apply absolute;
42
- @apply w-[23px] h-[23px] left-0.5 bottom-0.5;
43
- content: "";
44
- background-color: oklch(0.50 0 0);
45
- transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
46
- box-shadow: 0 1px 3px oklch(0 0 0 / 0.3);
47
- }
48
-
49
- input:checked + .slider {
50
- background-color: oklch(0.72 0.15 145);
51
- border-color: oklch(0.72 0.15 145);
52
- }
53
-
54
- input:checked + .slider:before {
55
- background-color: oklch(1 0 0);
56
- }
57
-
58
- input:checked + .slider:before {
59
- transform: translateX(20px);
60
- }
61
-
62
- .switch.disabled {
63
- @apply pointer-events-none;
64
- }
65
-
66
- input:disabled + .slider {
67
- @apply opacity-50 cursor-not-allowed;
68
- }
69
-
70
- input:disabled + .slider:before {
71
- @apply opacity-70;
72
- }
73
-
74
- /* Rounded sliders */
75
- .slider.round {
76
- border-radius: 31px;
77
- }
78
-
79
- .slider.round:before {
80
- border-radius: 50%;
81
- }
82
-
83
- @media (max-width: 810px) {
84
- .switch {
85
- @apply w-11 min-w-11 h-[26px];
86
-
87
- .slider:before {
88
- @apply w-[22px] h-[22px] left-0.5 bottom-0.5;
89
- }
90
- }
91
-
92
- input:checked + .slider:before {
93
- @apply translate-x-[18px];
94
- }
95
- }
96
- </style>
@@ -0,0 +1,15 @@
1
+ export function useColorMapping() {
2
+ const colorToClass = (color) => {
3
+ const colorMap = {
4
+ green: "bg-green-400",
5
+ red: "bg-red-400",
6
+ yellow: "bg-yellow-400",
7
+ blue: "bg-blue-400",
8
+ error: "bg-red-400",
9
+ success: "bg-green-400"
10
+ };
11
+ return colorMap[color?.toLowerCase()] || "bg-white";
12
+ };
13
+
14
+ return { colorToClass };
15
+ }
@@ -17,7 +17,7 @@ export function useCopyToClipboard() {
17
17
  })
18
18
  .catch((err) => {
19
19
  ui.showError('Failed to copy')
20
- console.error('Copy failed:', err)
20
+ ui.logger.Error('Copy failed:', err)
21
21
  })
22
22
  }
23
23
 
@@ -0,0 +1,21 @@
1
+ export function useDateFormatting() {
2
+ const formatEventDate = (dateString) => {
3
+ if (!dateString) return '';
4
+ try {
5
+ const date = new Date(dateString);
6
+ const options = {
7
+ month: 'short',
8
+ day: 'numeric',
9
+ year: 'numeric',
10
+ hour: 'numeric',
11
+ minute: '2-digit',
12
+ hour12: true
13
+ };
14
+ return date.toLocaleString('en-US', options).replace(',', '');
15
+ } catch {
16
+ return dateString;
17
+ }
18
+ };
19
+
20
+ return { formatEventDate };
21
+ }
@@ -0,0 +1,14 @@
1
+ export function useDeviceDetection() {
2
+ const isIOS = () => {
3
+ if (/iPad|iPhone|iPod/.test(navigator.platform)) {
4
+ return true;
5
+ }
6
+ return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
7
+ };
8
+
9
+ const isIpadOS = () => {
10
+ return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
11
+ };
12
+
13
+ return { isIOS, isIpadOS };
14
+ }
@@ -1,4 +1,5 @@
1
1
  import { ref, nextTick, onMounted, onUnmounted } from "vue";
2
+ import { DEBUG } from "@/utils/debug";
2
3
 
3
4
  export function useDropdownPosition(dropdownRef, options = {}) {
4
5
  const menuStyle = ref({});
@@ -6,10 +7,9 @@ export function useDropdownPosition(dropdownRef, options = {}) {
6
7
  offset = { x: -1, y: 4 },
7
8
  zIndex = 50000,
8
9
  minWidth = null,
9
- maxHeight = 160,
10
+ maxHeight = window.innerHeight * 0.8, // Use 80% of viewport height
10
11
  estimateHeight = null,
11
- includeAdjacentButtons = false,
12
- containerSelector = null
12
+ includeAdjacentButtons = false
13
13
  } = options;
14
14
 
15
15
  const calculateMenuPosition = () => {
@@ -24,7 +24,7 @@ export function useDropdownPosition(dropdownRef, options = {}) {
24
24
  const menuHeight = estimateHeight ? estimateHeight() : maxHeight;
25
25
 
26
26
  // Calculate width including adjacent buttons if specified
27
- let menuWidth = minWidth || rect.width + 2;
27
+ let menuWidth = minWidth || rect.width;
28
28
 
29
29
  if (includeAdjacentButtons) {
30
30
  // Look for parent container with buttons
@@ -109,8 +109,7 @@ export function useDropdownPosition(dropdownRef, options = {}) {
109
109
  maxHeight: `${maxHeight}px`
110
110
  };
111
111
  } catch (error) {
112
- console.warn("Error calculating dropdown position:", error);
113
- // Fallback to basic positioning
112
+ if (DEBUG) console.warn("Error calculating dropdown position:", error);
114
113
  menuStyle.value = {
115
114
  position: "fixed",
116
115
  top: "0px",
@@ -0,0 +1,31 @@
1
+ import { computed } from "vue";
2
+ import { useWindowDimensions } from "./useWindowDimensions";
3
+
4
+ export function useDynamicTableHeight(options = {}) {
5
+ const { windowHeight, windowWidth } = useWindowDimensions();
6
+
7
+ const {
8
+ topReservedSpace = 243,
9
+ bottomBuffer = 16,
10
+ rowHeight = 64,
11
+ minRowsToShow = 2
12
+ } = options;
13
+
14
+ const dynamicTableHeight = computed(() => {
15
+ // Detect PWA mode and small screens
16
+ const isPWA = window.matchMedia('(display-mode: standalone)').matches;
17
+ const isMobile = windowWidth.value <= 768;
18
+
19
+ // Extra buffer for iPhone PWA to prevent overflow
20
+ const extraBuffer = isPWA && isMobile ? 60 : 0;
21
+
22
+ const availableHeight = windowHeight.value - topReservedSpace - bottomBuffer - extraBuffer;
23
+ const minHeight = minRowsToShow * rowHeight;
24
+ const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
25
+ const exactHeight = maxCompleteRows * rowHeight;
26
+
27
+ return exactHeight + "px";
28
+ });
29
+
30
+ return { dynamicTableHeight };
31
+ }
@@ -12,7 +12,6 @@ export function useRowSelection(toggleCallback) {
12
12
  const DOUBLE_TAP_DELAY = 300
13
13
 
14
14
  const handleDoubleClick = (event) => {
15
- // Don't trigger on button or checkbox clicks
16
15
  if (event.target.closest('button') || event.target.closest('.checkbox')) {
17
16
  return
18
17
  }
@@ -24,7 +23,6 @@ export function useRowSelection(toggleCallback) {
24
23
  const tapGap = currentTime - lastTapTime
25
24
 
26
25
  if (tapGap < DOUBLE_TAP_DELAY && tapGap > 0) {
27
- // Don't trigger on button or checkbox taps
28
26
  if (!event.target.closest('button') && !event.target.closest('.checkbox')) {
29
27
  event.preventDefault()
30
28
  toggleCallback()
@@ -34,7 +32,6 @@ export function useRowSelection(toggleCallback) {
34
32
  }
35
33
 
36
34
  const handleTouchEnd = (event) => {
37
- // Prevent default touch behavior on buttons/checkboxes
38
35
  if (event.target.closest('button') || event.target.closest('.checkbox')) {
39
36
  return
40
37
  }
@@ -0,0 +1,16 @@
1
+ export function useTicketPricing() {
2
+ const isTotalPrice = (line, index, lines) => {
3
+ const trimmed = line.trim();
4
+ if (!trimmed) return false;
5
+
6
+ const nonEmptyLines = lines.filter(l => l.trim());
7
+ const isLastLine = index === lines.lastIndexOf(nonEmptyLines[nonEmptyLines.length - 1]);
8
+
9
+ if (!isLastLine) return false;
10
+
11
+ const totalPricePattern = /^([$€£¥₹₽¢]|[A-Z]{3})\s*[\d,]+\.?\d*$/;
12
+ return totalPricePattern.test(trimmed) && !trimmed.includes('(') && !trimmed.includes(')');
13
+ };
14
+
15
+ return { isTotalPrice };
16
+ }
@@ -0,0 +1,21 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+
3
+ export function useWindowDimensions() {
4
+ const windowHeight = ref(window.innerHeight);
5
+ const windowWidth = ref(window.innerWidth);
6
+
7
+ const updateDimensions = () => {
8
+ windowHeight.value = window.innerHeight;
9
+ windowWidth.value = window.innerWidth;
10
+ };
11
+
12
+ onMounted(() => {
13
+ window.addEventListener("resize", updateDimensions);
14
+ });
15
+
16
+ onUnmounted(() => {
17
+ window.removeEventListener("resize", updateDimensions);
18
+ });
19
+
20
+ return { windowHeight, windowWidth };
21
+ }
@@ -1,5 +1,5 @@
1
- const debug = false;
2
- const log = (...args) => debug && console.log("[filter]", ...args);
1
+ import { DEBUG } from "@/utils/debug";
2
+ const log = (...args) => DEBUG && console.log("[filter]", ...args);
3
3
 
4
4
  const colors = {
5
5
  HIGHLIGHT: "#d3f8e2",
@@ -30,27 +30,23 @@ const isWheelchair = (p) => {
30
30
  };
31
31
 
32
32
  const sortAlphaNum = (a, b) => {
33
- var reA = /[^a-zA-Z]/g;
34
- var reN = /[^0-9]/g;
35
- var AInt = parseInt(a, 10);
36
- var BInt = parseInt(b, 10);
33
+ const reA = /[^a-zA-Z]/g;
34
+ const reN = /[^0-9]/g;
35
+ const AInt = parseInt(a, 10);
36
+ const BInt = parseInt(b, 10);
37
37
 
38
38
  if (isNaN(AInt) && isNaN(BInt)) {
39
- var aA = a.replace(reA, "");
40
- var bA = b.replace(reA, "");
39
+ const aA = a.replace(reA, "");
40
+ const bA = b.replace(reA, "");
41
41
  if (aA === bA) {
42
- var aN = parseInt(a.replace(reN, ""), 10);
43
- var bN = parseInt(b.replace(reN, ""), 10);
42
+ const aN = parseInt(a.replace(reN, ""), 10);
43
+ const bN = parseInt(b.replace(reN, ""), 10);
44
44
  return aN === bN ? 0 : aN > bN ? 1 : -1;
45
45
  } else {
46
46
  return aA > bA ? 1 : -1;
47
47
  }
48
- } else if (isNaN(AInt)) {
49
- //A is not an Int
50
- return 1; //to make alphanumeric sort first return -1 here
51
48
  } else if (isNaN(BInt)) {
52
- //B is not an Int
53
- return -1; //to make alphanumeric sort first return 1 here
49
+ return -1;
54
50
  } else {
55
51
  return AInt > BInt ? 1 : -1;
56
52
  }
@@ -313,7 +309,7 @@ export default class FilterBuilder {
313
309
  });
314
310
 
315
311
  if (existingFilter) {
316
- console.log("Filter already exists:", existingFilter);
312
+ if (DEBUG) log("Filter already exists:", existingFilter);
317
313
  return; // Don't add the filter if it already exists
318
314
  }
319
315
 
@@ -385,7 +381,7 @@ export default class FilterBuilder {
385
381
  } else {
386
382
  color = this.expandedFilter === filter.id ? colors.SELECTED_EXPANDED : colors.SELECTED;
387
383
  }
388
-
384
+
389
385
  switch (type) {
390
386
  case this.filterTypes.NORMAL:
391
387
  // If it has no 'rows' property
@@ -416,7 +412,6 @@ export default class FilterBuilder {
416
412
  break;
417
413
 
418
414
  case this.filterTypes.CATCH_ALL_FLOOR:
419
- // this.cssClasses += floors.map((f) => `path[name="${f}"] {fill: ${color} !important;}`).join("\n") + "\n";
420
415
  break;
421
416
 
422
417
  case this.filterTypes.INVALID:
@@ -432,7 +427,7 @@ export default class FilterBuilder {
432
427
  const color = colors.UNSELECTABLE;
433
428
  this.cssClasses += `.svg-wrapper path[section="${section}"][row="${row}"] {stroke: ${color} !important;}\n`;
434
429
  });
435
-
430
+
436
431
  log("Generated CSS:", this.cssClasses);
437
432
  }
438
433
 
@@ -533,7 +528,6 @@ export default class FilterBuilder {
533
528
 
534
529
  deleteFilterById(id) {
535
530
  this.filters = this.filters.filter((f) => f.id !== id);
536
- // this.updateHooks = this.updateHooks.filter((i) => id !== i);
537
531
  if (this.expandedFilter === id) this.expandedFilter = "";
538
532
  this.updateCss();
539
533
  this.updateHooks.forEach((fn) => fn());