@necrolab/dashboard 0.5.34 → 0.5.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@necrolab/dashboard",
3
- "version": "0.5.34",
3
+ "version": "0.5.35",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "node scripts/build.mjs",
@@ -7,19 +7,25 @@
7
7
  @apply flex items-center justify-between;
8
8
  padding-top: 0.75rem;
9
9
  padding-bottom: 0.5rem;
10
- flex-wrap: nowrap;
10
+ flex-wrap: nowrap !important;
11
11
  gap: 0.5rem;
12
+ min-height: 0;
12
13
 
13
14
  .page-header-card {
14
15
  @apply flex items-center gap-2.5 rounded-lg;
15
16
  padding: 0.375rem 0.75rem;
16
17
  background: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%);
17
18
  border: 1px solid var(--color-border-light);
18
- flex: 0 1 auto;
19
+ flex: 0 0 auto;
19
20
  min-width: 0;
20
- max-width: 60%;
21
21
  white-space: nowrap;
22
- overflow: hidden;
22
+
23
+ /* Mobile - allow shrinking if needed */
24
+ @media (max-width: 767px) {
25
+ flex: 0 1 auto;
26
+ max-width: calc(100% - 180px);
27
+ gap: 0.5rem;
28
+ }
23
29
 
24
30
  svg, img {
25
31
  width: 17px;
@@ -38,17 +38,33 @@
38
38
  .Toastify__toast-container {
39
39
  pointer-events: none;
40
40
  --toastify-toast-bd-radius: 6px;
41
+
42
+ /* Mobile responsive - prevent overflow and edge-to-edge toasts */
43
+ @media (max-width: 767px) {
44
+ width: calc(100vw - 2rem) !important;
45
+ max-width: 360px;
46
+ right: 1rem !important;
47
+ left: auto !important;
48
+ padding: 0 !important;
49
+ top: calc(env(safe-area-inset-top) + 1rem) !important;
50
+ }
41
51
  }
42
52
 
43
53
  .Toastify__toast {
44
54
  min-height: 50px !important;
45
- height: 50px !important;
55
+ height: auto !important;
46
56
  pointer-events: auto !important;
47
57
  margin-bottom: 6px !important;
48
58
 
49
59
  transition: transform 0.18s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.12s cubic-bezier(0.25, 0.1, 0.25, 1) !important;
50
60
  transform: translate3d(0, 0, 0);
51
61
 
62
+ /* Mobile responsive - ensure proper sizing */
63
+ @media (max-width: 767px) {
64
+ margin-bottom: 8px !important;
65
+ border-radius: 6px !important;
66
+ }
67
+
52
68
  &.Toastify__slide-enter-active {
53
69
  animation: slideInRight 0.22s cubic-bezier(0.25, 0.1, 0.25, 1);
54
70
  }
@@ -114,6 +114,7 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
114
114
 
115
115
  .mobile-icons {
116
116
  @apply ml-auto flex items-center gap-x-2 lg:hidden;
117
+ flex-shrink: 0;
117
118
 
118
119
  button {
119
120
  @apply flex h-8 w-8 items-center justify-center rounded transition-all duration-150 bg-dark-400;
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="queue-stats"
4
- :class="{ 'queue-stats--full': visibleCards === 4 }"
4
+ :class="[{ 'queue-stats--full': visibleCards === 4 }, $attrs.class]"
5
5
  :style="statsGridStyle"
6
6
  v-if="ui.queueStats.show"
7
7
  :key="key">
@@ -91,6 +91,10 @@ import { SkiIcon, TimerIcon, SandclockIcon } from "@/components/icons";
91
91
  import { useUIStore } from "@/stores/ui";
92
92
  import { ref, onUnmounted, computed, onMounted } from "vue";
93
93
 
94
+ defineOptions({
95
+ inheritAttrs: false
96
+ });
97
+
94
98
  const ui = useUIStore();
95
99
 
96
100
  const getQueuePassAmount = (width) => {
@@ -85,8 +85,8 @@
85
85
  </div>
86
86
  </div>
87
87
  <div class="col-span-2 overflow-hidden lg:col-span-3 xl:col-span-2">
88
- <h4 class="lg:text-2xs text-center text-xs leading-tight text-light-300">
89
- <span v-if="!props.task.reservedTicketsList">-</span>
88
+ <h4 class="lg:text-2xs text-center text-xs leading-tight">
89
+ <span v-if="!props.task.reservedTicketsList" class="text-light-300">-</span>
90
90
  <div v-else class="overflow-hidden">
91
91
  <div
92
92
  v-for="(l, index) in props.task.reservedTicketsList.split('\n')"
@@ -99,7 +99,12 @@
99
99
  'font-bold text-green-400': isTotalPrice(
100
100
  l,
101
101
  index,
102
- props.task.reservedTicketsList.split(' ')
102
+ props.task.reservedTicketsList.split('\n')
103
+ ),
104
+ 'text-light-300': !isTotalPrice(
105
+ l,
106
+ index,
107
+ props.task.reservedTicketsList.split('\n')
103
108
  )
104
109
  }">
105
110
  {{ l.trim() }}
@@ -109,7 +114,8 @@
109
114
  <span
110
115
  class="mt-1 block text-xs font-bold"
111
116
  :class="{
112
- 'text-red-400': taskTimeLeft === '00:00' || taskTimeLeft === 'No Cartholds'
117
+ 'text-red-400': taskTimeLeft === '00:00' || taskTimeLeft === 'No Cartholds',
118
+ 'text-light-300': taskTimeLeft !== '00:00' && taskTimeLeft !== 'No Cartholds'
113
119
  }">
114
120
  {{ taskTimeLeft !== "00:00" ? taskTimeLeft : "Expired" }}
115
121
  </span>
@@ -1,5 +1,6 @@
1
1
  <template>
2
- <div class="table-component relative box-border flex flex-col rounded-lg bg-dark-500 bg-clip-padding overflow-x-hidden shadow-sm">
2
+ <div
3
+ class="table-component relative box-border flex flex-col overflow-x-hidden rounded-lg bg-dark-500 bg-clip-padding shadow-sm">
3
4
  <Header class="grid-cols-10 gap-2 text-center lg:grid-cols-12">
4
5
  <div class="col-span-1 flex items-center justify-start lg:col-span-2">
5
6
  <Checkbox
@@ -9,26 +10,26 @@
9
10
  :isHeader="true" />
10
11
  <div class="mx-auto hidden items-center lg:flex" @click="ui.toggleSort('eventId')">
11
12
  <EventIcon class="lg:mr-3" />
12
- <h4 class="hidden lg:flex text-white">Event</h4>
13
+ <h4 class="hidden text-white lg:flex">Event</h4>
13
14
  <DownIcon v-if="ui.sortData.sortBy === 'eventId' && !ui.sortData.reversed" class="ml-1" />
14
15
  <UpIcon v-if="ui.sortData.sortBy === 'eventId' && ui.sortData.reversed" class="ml-1" />
15
16
  </div>
16
17
  </div>
17
18
  <div class="col-span-2 flex items-center justify-center lg:col-span-3 xl:col-span-2" v-once>
18
19
  <TicketIcon class="mr-0 lg:mr-3" />
19
- <h4 class="hidden lg:flex text-white">Tickets</h4>
20
+ <h4 class="hidden text-white lg:flex">Tickets</h4>
20
21
  </div>
21
22
  <div
22
23
  class="col-span-6 flex items-center justify-center md:col-span-5 lg:col-span-4 xl:col-span-5"
23
24
  @click="ui.toggleSort('status')">
24
25
  <StatusIcon class="mr-0 lg:mr-3" />
25
- <h4 class="hidden lg:flex text-white">Status</h4>
26
+ <h4 class="hidden text-white lg:flex">Status</h4>
26
27
  <DownIcon v-if="ui.sortData.sortBy === 'status' && !ui.sortData.reversed" class="ml-1" />
27
28
  <UpIcon v-if="ui.sortData.sortBy === 'status' && ui.sortData.reversed" class="ml-1" />
28
29
  </div>
29
30
  <div class="col-span-1 flex items-center justify-end md:col-span-2 md:justify-center lg:col-span-3" v-once>
30
31
  <ClickIcon class="mr-0 lg:mr-3" />
31
- <h4 class="hidden lg:flex text-white">Actions</h4>
32
+ <h4 class="hidden text-white lg:flex">Actions</h4>
32
33
  </div>
33
34
  <div class="absolute right-5 top-3.5 hidden items-center xl:flex">
34
35
  <h4 class="text-center text-xs text-white">ID</h4>
@@ -36,7 +37,7 @@
36
37
  </Header>
37
38
  <DynamicScroller
38
39
  v-if="virtualTaskItems.length && useVirtualScroller"
39
- class="hidden-scrollbars touch-pan-y min-h-0 overflow-y-auto overflow-x-hidden scrollable"
40
+ class="hidden-scrollbars scrollable min-h-0 touch-pan-y overflow-y-auto overflow-x-hidden"
40
41
  :style="{ height: maxTableHeight, maxHeight: maxTableHeight }"
41
42
  :items="virtualTaskItems"
42
43
  :min-item-size="virtualMinItemSize"
@@ -56,7 +57,7 @@
56
57
  ]"
57
58
  :data-index="index">
58
59
  <div
59
- class="shrink-0 border-b border-dark-650 min-h-14.5 md:min-h-17.25 has-[.event-details]:min-h-18.75 mobile-portrait:min-h-12.5 transition-colors duration-150 ease-in-out hover:!bg-dark-550">
60
+ class="mobile-portrait:min-h-12.5 min-h-14.5 shrink-0 border-b border-dark-650 transition-colors duration-150 ease-in-out hover:!bg-dark-550 has-[.event-details]:min-h-18.75 md:min-h-17.25">
60
61
  <Task
61
62
  v-if="props.tasks[item.taskId]"
62
63
  :task="props.tasks[item.taskId]"
@@ -68,12 +69,12 @@
68
69
  </DynamicScroller>
69
70
  <div
70
71
  v-else-if="virtualTaskItems.length"
71
- class="hidden-scrollbars touch-pan-y min-h-0 overflow-y-auto overflow-x-hidden scrollable"
72
+ class="hidden-scrollbars scrollable min-h-0 touch-pan-y overflow-y-auto overflow-x-hidden"
72
73
  :style="{ maxHeight: maxTableHeight }">
73
74
  <div
74
75
  v-for="(item, index) in virtualTaskItems"
75
76
  :key="item.taskId"
76
- class="shrink-0 border-b border-dark-650 min-h-14.5 md:min-h-17.25 has-[.event-details]:min-h-18.75 mobile-portrait:min-h-12.5 transition-colors duration-150 ease-in-out hover:!bg-dark-550">
77
+ class="mobile-portrait:min-h-12.5 min-h-14.5 shrink-0 border-b border-dark-650 transition-colors duration-150 ease-in-out hover:!bg-dark-550 has-[.event-details]:min-h-18.75 md:min-h-17.25">
77
78
  <Task
78
79
  v-if="props.tasks[item.taskId]"
79
80
  :task="props.tasks[item.taskId]"
@@ -83,18 +84,15 @@
83
84
  </div>
84
85
  <div
85
86
  v-else
86
- class="empty-state flex flex-col items-center justify-center py-8 text-center bg-dark-400 text-light-500 text-sm font-medium"
87
+ class="empty-state flex flex-col items-center justify-center bg-dark-400 py-8 text-center text-sm font-medium text-light-500"
87
88
  :style="{ minHeight: emptyStateHeight }">
88
- <div
89
- v-if="
90
- !ui.queueStats.queued && !ui.queueStats.sleeping && ui.queueStats.nextQueuePasses.length === 0
91
- ">
92
- <TasksIcon class="mx-auto empty-state-icon" />
89
+ <div v-if="!ui.queueStats.queued && !ui.queueStats.sleeping && ui.queueStats.nextQueuePasses.length === 0">
90
+ <TasksIcon class="empty-state-icon mx-auto" />
93
91
  <p class="text-sm text-light-400">No tasks yet</p>
94
92
  <p class="mt-1 text-xs text-light-500">Create tasks to get started</p>
95
93
  </div>
96
94
  <div v-else>
97
- <TasksIcon class="mx-auto empty-state-icon" />
95
+ <TasksIcon class="empty-state-icon mx-auto" />
98
96
  <p class="text-sm text-light-400">No tasks match current filters</p>
99
97
  <p class="mt-1 text-xs text-light-500">Adjust filters to see tasks</p>
100
98
  </div>
@@ -118,7 +116,7 @@ const props = defineProps({
118
116
  },
119
117
  searchQuery: {
120
118
  type: String,
121
- default: ''
119
+ default: ""
122
120
  },
123
121
  preferEventName: {
124
122
  type: Boolean,
@@ -203,7 +201,11 @@ const filteredTaskIds = computed(() => {
203
201
  continue;
204
202
  }
205
203
 
206
- if (task.siteId !== ui.currentCountry.siteId && !siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)) continue;
204
+ if (
205
+ task.siteId !== ui.currentCountry.siteId &&
206
+ !siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)
207
+ )
208
+ continue;
207
209
  if (ui.currentEvent && task.eventId !== ui.currentEvent) continue;
208
210
  if (!shouldTaskShow(task)) continue;
209
211
  if (!shouldTaskMatchSearch(task, searchLower)) continue;
@@ -264,7 +266,7 @@ const handleVirtualWheel = (event) => {
264
266
 
265
267
  const maxTableHeight = computed(() => {
266
268
  // Detect PWA mode (standalone display)
267
- const isPWA = window.matchMedia('(display-mode: standalone)').matches;
269
+ const isPWA = window.matchMedia("(display-mode: standalone)").matches;
268
270
 
269
271
  // Calculate available space for table
270
272
  const headerHeight = windowWidth.value >= 1024 ? 80 : 64; // Navbar padding (lg:pt-20 vs pt-16)
@@ -292,15 +294,17 @@ const maxTableHeight = computed(() => {
292
294
  })();
293
295
 
294
296
  // Reserve more space for UTILS on iPhone PWA to prevent overflow
295
- const utilitiesHeight = isPWA && windowWidth.value <= 768
296
- ? 260 // iPhone PWA: extra space for UTILS
297
- : windowWidth.value > 1024
298
- ? 200 // Desktop
299
- : windowWidth.value <= 768
300
- ? 220 // Mobile browser
301
- : 180; // Tablet
302
-
303
- const margins = windowWidth.value >= 1024 ? 30 : windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 8 : 12;
297
+ const utilitiesHeight =
298
+ isPWA && windowWidth.value <= 768
299
+ ? 150 // iPhone PWA: extra space for UTILS
300
+ : windowWidth.value > 1024
301
+ ? 200 // Desktop
302
+ : windowWidth.value <= 768
303
+ ? 150 // Mobile browser
304
+ : 210; // Tablet
305
+
306
+ const margins =
307
+ windowWidth.value >= 1024 ? 30 : windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 8 : 12;
304
308
 
305
309
  const totalUsedSpace =
306
310
  headerHeight + titleHeight + controlsHeight + filtersAndStatsHeight + utilitiesHeight + margins;
@@ -164,7 +164,6 @@ const getSectionNameMapping = () => {
164
164
  }
165
165
  });
166
166
 
167
- log("getSectionNameMapping:", sectionRealName);
168
167
  return sectionRealName;
169
168
  };
170
169
 
@@ -583,14 +582,11 @@ export default class FilterBuilder {
583
582
  // Use BOTH stroke and fill to cover entire section area, not just row lines
584
583
  this.cssClasses += `.svg-wrapper path.hover-highlight-stroke {stroke: ${colors.HIGHLIGHT} !important; fill: ${colors.HIGHLIGHT} !important; fill-opacity: 0.3 !important;}\n`;
585
584
  this.cssClasses += `.svg-wrapper path.hover-highlight-fill {fill: ${colors.HIGHLIGHT} !important;}\n`;
586
-
587
- log("Generated CSS:", this.cssClasses);
588
585
  }
589
586
 
590
587
  addLabelHandlers() {
591
588
  const labels = document.querySelectorAll("tspan");
592
589
  const gaSectionNameMapping = this.getSectionNameMapping();
593
- log("addLabelHandlers: gaSectionNameMapping", gaSectionNameMapping);
594
590
 
595
591
  for (let i = 0; i < labels.length; i++) {
596
592
  const label = labels[i];
package/src/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createApp } from "vue";
2
2
  import { createPinia } from "pinia";
3
3
  import VueVirtualScroller from "vue-virtual-scroller";
4
+ import Vue3Toastify from "vue3-toastify";
4
5
 
5
6
  import App from "./App.vue";
6
7
  import router from "./router";
@@ -18,4 +19,8 @@ app.config.performance = true;
18
19
  app.use(createPinia());
19
20
  app.use(router);
20
21
  app.use(VueVirtualScroller);
22
+ app.use(Vue3Toastify, {
23
+ newestOnTop: true,
24
+ position: "top-right"
25
+ });
21
26
  app.mount("#app");
package/src/stores/ui.js CHANGED
@@ -5,6 +5,10 @@ import { toast } from "vue3-toastify";
5
5
  import { createLogger } from "@/stores/logger";
6
6
  import { betterSort, sortTaskIds } from "@/libs/utils/array";
7
7
 
8
+ // Track active toast IDs for manual limit enforcement
9
+ const activeToasts = [];
10
+ const MAX_TOASTS = 2;
11
+
8
12
  const TOAST_CONFIG = {
9
13
  autoClose: 1400,
10
14
  pauseOnHover: false,
@@ -17,6 +21,27 @@ const TOAST_CONFIG = {
17
21
  newestOnTop: true
18
22
  };
19
23
 
24
+ // Helper to enforce toast limit
25
+ const showToastWithLimit = (type, message) => {
26
+ // If we're at the limit, remove the oldest toast
27
+ if (activeToasts.length >= MAX_TOASTS) {
28
+ const oldestId = activeToasts.shift();
29
+ toast.remove(oldestId);
30
+ }
31
+
32
+ // Show new toast and track its ID
33
+ const toastId = toast[type](message, TOAST_CONFIG);
34
+ activeToasts.push(toastId);
35
+
36
+ // Remove from tracking when it auto-closes
37
+ setTimeout(() => {
38
+ const index = activeToasts.indexOf(toastId);
39
+ if (index > -1) activeToasts.splice(index, 1);
40
+ }, TOAST_CONFIG.autoClose + 100);
41
+
42
+ return toastId;
43
+ };
44
+
20
45
  import mockTaskData from "@/stores/sampleData.js";
21
46
  import { useRouter } from "vue-router";
22
47
  import { DEBUG } from "@/utils/debug";
@@ -264,7 +289,6 @@ export const useUIStore = defineStore("ui", () => {
264
289
  input.style.userSelect = "auto";
265
290
  input.style.touchAction = "pan-x pan-y";
266
291
  });
267
-
268
292
  };
269
293
 
270
294
  const enableScroll = () => {
@@ -301,7 +325,6 @@ export const useUIStore = defineStore("ui", () => {
301
325
 
302
326
  // Restore scroll position
303
327
  window.scrollTo(0, parseInt(scrollY || "0") * -1);
304
-
305
328
  };
306
329
 
307
330
  const refreshQueueStats = () => {
@@ -538,7 +561,6 @@ export const useUIStore = defineStore("ui", () => {
538
561
  mainCheckbox,
539
562
  toggleMainCheckbox,
540
563
 
541
-
542
564
  // single
543
565
  deleteTask: (taskId) => {
544
566
  delete tasks.value[taskId];
@@ -607,8 +629,8 @@ export const useUIStore = defineStore("ui", () => {
607
629
  massEditPresaleCode: (eventId, presaleCode) => connection.sendMassEditPresaleCode(eventId, presaleCode),
608
630
 
609
631
  // alerts
610
- showError: (err) => toast.error(err, TOAST_CONFIG),
611
- showSuccess: (msg) => toast.success(msg, TOAST_CONFIG),
632
+ showError: (err) => showToastWithLimit("error", err),
633
+ showSuccess: (msg) => showToastWithLimit("success", msg),
612
634
  // Country
613
635
  currentCountry,
614
636
  setCurrentCountry: (country, closeModal, newModule) => {
@@ -37,21 +37,8 @@
37
37
  @deleteAll="ui.deleteTasks()" />
38
38
  </div>
39
39
 
40
- <div class="mb-2 flex flex-col gap-2 md:hidden">
40
+ <div class="mb-2 md:hidden">
41
41
  <Stats class="stats-component w-full" />
42
- <div class="flex-gap-2 items-center justify-end">
43
- <div
44
- class="flex h-8 items-center justify-between rounded-md border border-dark-650 bg-dark-400 px-2 text-xs font-medium text-white">
45
- <p class="text-2xs">Name</p>
46
- <Switch class="scale-75" v-model="preferEventName" />
47
- </div>
48
- <PriceSortToggle
49
- class="h-8 min-w-20 max-w-28 flex-shrink-0"
50
- :options="['All', 'Checkout']"
51
- :darker="true"
52
- :current="ui.taskFilter"
53
- @change="(e) => ui.setTaskFilter(e)" />
54
- </div>
55
42
  </div>
56
43
 
57
44
  <div class="mb-1 hidden flex-wrap items-center gap-2 md:flex xl:flex-nowrap lg:mb-2">
@@ -103,16 +90,29 @@
103
90
  class="input-default event-dropdown w-full hover:bg-dark-400"
104
91
  rightAmount="right-2" />
105
92
  </div>
106
- <div class="w-full">
107
- <div class="input-default flex items-center">
108
- <input
109
- v-model="taskSearchQuery"
110
- type="text"
111
- placeholder="Search tasks..."
112
- aria-label="Search tasks"
113
- class="transparent-input" />
114
- <span v-if="taskSearchQuery" class="ml-2 text-xs text-light-500">{{ filteredTaskCount }}</span>
93
+ <div class="flex w-full items-center gap-2">
94
+ <div class="flex-1">
95
+ <div class="input-default flex items-center">
96
+ <input
97
+ v-model="taskSearchQuery"
98
+ type="text"
99
+ placeholder="Search tasks..."
100
+ aria-label="Search tasks"
101
+ class="transparent-input" />
102
+ <span v-if="taskSearchQuery" class="ml-2 text-xs text-light-500">{{ filteredTaskCount }}</span>
103
+ </div>
104
+ </div>
105
+ <div
106
+ class="flex h-8 items-center justify-between rounded-md border border-dark-650 bg-dark-400 px-2 text-xs font-medium text-white flex-shrink-0">
107
+ <p class="text-2xs">Name</p>
108
+ <Switch class="scale-75" v-model="preferEventName" />
115
109
  </div>
110
+ <PriceSortToggle
111
+ class="h-8 min-w-20 max-w-28 flex-shrink-0"
112
+ :options="['All', 'Checkout']"
113
+ :darker="true"
114
+ :current="ui.taskFilter"
115
+ @change="(e) => ui.setTaskFilter(e)" />
116
116
  </div>
117
117
  </div>
118
118