@necrolab/dashboard 0.4.47 → 0.4.49

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 (39) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/exit +209 -0
  3. package/index.html +1 -1
  4. package/package.json +1 -1
  5. package/postinstall.js +9 -0
  6. package/public/manifest.json +8 -3
  7. package/src/assets/css/_input.scss +104 -111
  8. package/src/assets/css/_utilities.scss +441 -0
  9. package/src/assets/css/main.scss +228 -154
  10. package/src/components/Auth/LoginForm.vue +8 -8
  11. package/src/components/Editors/Account/Account.vue +156 -146
  12. package/src/components/Editors/Account/AccountCreator.vue +1 -1
  13. package/src/components/Editors/Account/AccountView.vue +13 -13
  14. package/src/components/Editors/Account/CreateAccount.vue +25 -16
  15. package/src/components/Editors/Profile/CreateProfile.vue +1 -1
  16. package/src/components/Editors/Profile/Profile.vue +1 -1
  17. package/src/components/Editors/Profile/ProfileCountryChooser.vue +83 -19
  18. package/src/components/Editors/Profile/ProfileView.vue +11 -11
  19. package/src/components/Tasks/CreateTaskAXS.vue +3 -3
  20. package/src/components/Tasks/CreateTaskTM.vue +7 -35
  21. package/src/components/Tasks/QuickSettings.vue +112 -9
  22. package/src/components/Tasks/Stats.vue +29 -25
  23. package/src/components/Tasks/Task.vue +489 -365
  24. package/src/components/Tasks/TaskView.vue +21 -23
  25. package/src/components/icons/Sandclock.vue +2 -2
  26. package/src/components/icons/Stadium.vue +1 -1
  27. package/src/components/ui/Modal.vue +37 -35
  28. package/src/components/ui/controls/CountryChooser.vue +200 -62
  29. package/src/components/ui/controls/atomic/Dropdown.vue +177 -91
  30. package/src/components/ui/controls/atomic/MultiDropdown.vue +247 -168
  31. package/src/composables/useClickOutside.js +21 -0
  32. package/src/composables/useDropdownPosition.js +174 -0
  33. package/src/stores/ui.js +5 -4
  34. package/src/views/Accounts.vue +2 -2
  35. package/src/views/Console.vue +25 -45
  36. package/src/views/Editor.vue +1194 -730
  37. package/src/views/Profiles.vue +2 -2
  38. package/src/views/Tasks.vue +170 -137
  39. package/tailwind.config.js +47 -21
@@ -1,283 +1,362 @@
1
1
  <template>
2
- <div @click="toggleOpened" class="dropdown">
3
- <span class="dropdown-display">
4
- <span class="dropdown-value" :class="capitalize ? 'capitalize' : ''">
5
- {{ displayValue }}
6
- </span>
7
- <div class="dropdown-counter">
8
- <span v-if="selectedOptions.length > 1" class="counter-badge">
9
- {{ selectedOptions.length }}
10
- </span>
11
- <DownIcon class="dropdown-arrow" :class="opened ? 'rotate-180' : ''" />
12
- </div>
2
+ <div @click="toggleOpened" class="dropdown" ref="dropdownRef">
3
+ <span class="dropdown-display">
4
+ <span class="dropdown-value" :class="capitalize ? 'capitalize' : ''">
5
+ {{ displayValue }}
6
+ </span>
7
+ <div class="dropdown-counter">
8
+ <span v-if="selectedOptions.length > 1" class="counter-badge">
9
+ {{ selectedOptions.length }}
13
10
  </span>
14
- <transition name="dropdown-fade">
15
- <div
16
- class="dropdown-menu multi scrollable"
17
- v-if="opened"
18
- :style="{ maxHeight: selectedOptions.length > 0 ? '240px' : '160px' }"
19
- @click.stop
20
- @wheel.stop
21
- @touchmove.stop
11
+ <DownIcon class="dropdown-arrow" :class="opened ? 'rotate-180' : ''" />
12
+ </div>
13
+ </span>
14
+ <Teleport to="body">
15
+ <transition name="dropdown-fade">
16
+ <div
17
+ v-if="opened"
18
+ class="dropdown-menu-portal multi scrollable"
19
+ :style="menuStyle"
20
+ @click.stop
21
+ @wheel.stop
22
+ @touchmove.stop
23
+ >
24
+ <div class="option-list scrollable">
25
+ <button
26
+ v-for="(option, i) in props.options"
27
+ :key="option.value"
28
+ class="dropdown-item"
29
+ :class="i !== 0 ? 'border-t border-dark-650' : ''"
30
+ @click.stop="toggleOption(option.value)"
22
31
  >
23
- <div class="option-list scrollable">
24
- <button
25
- v-for="(option, i) in props.options"
26
- :key="option.value"
27
- class="dropdown-item"
28
- :class="i !== 0 ? 'border-t border-light-300' : ''"
29
- @click.stop="toggleOption(option.value)"
30
- >
31
- <span class="dropdown-item-text" :class="capitalize ? 'capitalize' : ''">
32
- {{ option.label }}
33
- </span>
34
- <CheckmarkIcon v-if="selectedOptions.includes(option.value)" class="ml-2" />
35
- </button>
36
- </div>
37
-
38
- <div v-if="selectedOptions.length > 0" class="selected-summary">
39
- <div class="flex items-center justify-between">
40
- <div class="selected-count">
41
- <span class="count-badge">
42
- {{ selectedOptions.length }}
43
- </span>
44
- <span class="count-label">
45
- item{{ selectedOptions.length === 1 ? "" : "s" }} selected
46
- </span>
47
- </div>
48
- <button class="clear-button" @click.stop="clearAll">Clear All</button>
49
- </div>
50
- </div>
32
+ <span class="dropdown-item-text" :class="capitalize ? 'capitalize' : ''">
33
+ {{ option.label }}
34
+ </span>
35
+ <CheckmarkIcon v-if="selectedOptions.includes(option.value)" class="ml-2" />
36
+ </button>
37
+ </div>
38
+
39
+ <div v-if="selectedOptions.length > 0" class="selected-summary">
40
+ <div class="flex items-center justify-between">
41
+ <div class="selected-count">
42
+ <span class="count-badge">
43
+ {{ selectedOptions.length }}
44
+ </span>
45
+ <span class="count-label">
46
+ item{{ selectedOptions.length === 1 ? "" : "s" }} selected
47
+ </span>
48
+ </div>
49
+ <button class="clear-button" @click.stop="clearAll">Clear All</button>
51
50
  </div>
52
- </transition>
53
- </div>
51
+ </div>
52
+ </div>
53
+ </transition>
54
+ </Teleport>
55
+ </div>
54
56
  </template>
55
57
 
56
58
  <script setup>
57
59
  import { ref, computed } from "vue";
58
60
  import { DownIcon, CheckmarkIcon } from "@/components/icons";
59
61
  import { useUIStore } from "@/stores/ui";
62
+ import { useDropdownPosition } from "@/composables/useDropdownPosition";
63
+ import { useClickOutside } from "@/composables/useClickOutside";
60
64
 
61
65
  const ui = useUIStore();
62
66
 
63
67
  const props = defineProps({
64
- onSelect: { type: Function },
65
- default: { type: String },
66
- options: { type: Array, required: true },
67
- rightAmount: { type: String },
68
- topPadding: { type: String },
69
- capitalize: { type: Boolean }
68
+ onSelect: { type: Function },
69
+ default: { type: String },
70
+ options: { type: Array, required: true },
71
+ rightAmount: { type: String },
72
+ topPadding: { type: String },
73
+ capitalize: { type: Boolean },
74
+ includeAdjacentButtons: { type: Boolean, default: false },
70
75
  });
71
76
 
72
77
  const selectedOptions = ref([]);
78
+ const dropdownRef = ref(null);
73
79
  const id = Math.random();
74
80
  const opened = computed(() => ui.currentDropdown === id);
75
81
 
76
82
  const displayValue = computed(() => {
77
- if (selectedOptions.value.length === 0) {
78
- return props.default || "Select options...";
79
- }
80
-
81
- if (selectedOptions.value.length === 1) {
82
- const option = props.options.find((opt) => opt.value === selectedOptions.value[0]);
83
- return option ? option.label : selectedOptions.value[0];
84
- }
83
+ if (selectedOptions.value.length === 0) {
84
+ return props.default || "Select options...";
85
+ }
86
+
87
+ if (selectedOptions.value.length === 1) {
88
+ const option = props.options.find((opt) => opt.value === selectedOptions.value[0]);
89
+ return option ? option.label : selectedOptions.value[0];
90
+ }
91
+
92
+ if (selectedOptions.value.length <= 2) {
93
+ const labels = selectedOptions.value.map((val) => {
94
+ const option = props.options.find((opt) => opt.value === val);
95
+ return option ? option.label : val;
96
+ });
97
+ return labels.join(", ");
98
+ }
85
99
 
86
- if (selectedOptions.value.length <= 2) {
87
- const labels = selectedOptions.value.map((val) => {
88
- const option = props.options.find((opt) => opt.value === val);
89
- return option ? option.label : val;
90
- });
91
- return labels.join(", ");
92
- }
100
+ const firstOption = props.options.find((opt) => opt.value === selectedOptions.value[0]);
101
+ const firstName = firstOption ? firstOption.label : selectedOptions.value[0];
102
+ return `${firstName} +${selectedOptions.value.length - 1} more`;
103
+ });
93
104
 
94
- const firstOption = props.options.find((opt) => opt.value === selectedOptions.value[0]);
95
- const firstName = firstOption ? firstOption.label : selectedOptions.value[0];
96
- return `${firstName} +${selectedOptions.value.length - 1} more`;
105
+ // Use composables for positioning and click outside
106
+ const { menuStyle, updatePosition } = useDropdownPosition(dropdownRef, {
107
+ maxHeight: 280,
108
+ includeAdjacentButtons: props.includeAdjacentButtons,
109
+ estimateHeight: () => {
110
+ const optionsCount = props.options?.length || 0;
111
+ const summaryHeight = selectedOptions.value.length > 0 ? 70 : 0;
112
+ const baseMaxHeight = selectedOptions.value.length > 0 ? 280 : 200;
113
+ const optionListHeight = Math.min(optionsCount * 44, 200);
114
+ return Math.min(optionListHeight + summaryHeight, baseMaxHeight);
115
+ },
97
116
  });
98
117
 
99
- const getSelectedLabels = () => {
100
- return selectedOptions.value.map((val) => {
101
- const option = props.options.find((opt) => opt.value === val);
102
- return option ? option.label : val;
103
- });
104
- };
118
+ useClickOutside(dropdownRef, () => {
119
+ if (opened.value) {
120
+ ui.setCurrentDropdown("");
121
+ }
122
+ });
105
123
 
106
124
  const toggleOpened = () => {
107
- if (opened.value) ui.setCurrentDropdown("");
108
- else ui.setCurrentDropdown(id);
125
+ if (opened.value) {
126
+ ui.setCurrentDropdown("");
127
+ } else {
128
+ ui.setCurrentDropdown(id);
129
+ updatePosition();
130
+ }
109
131
  };
110
132
 
111
133
  const toggleOption = (option) => {
112
- const index = selectedOptions.value.indexOf(option);
113
- if (index === -1) {
114
- selectedOptions.value.push(option);
115
- } else {
116
- selectedOptions.value.splice(index, 1);
117
- }
118
-
119
- // Handle default logic
120
- if (selectedOptions.value.length === 0 && props.default) {
121
- selectedOptions.value = [props.default];
122
- }
123
- if (selectedOptions.value.length > 1 && selectedOptions.value.includes(props.default) && option !== props.default) {
124
- selectedOptions.value = selectedOptions.value.filter((e) => e !== props.default);
125
- } else if (option === props.default) {
126
- selectedOptions.value = [props.default];
127
- }
134
+ const index = selectedOptions.value.indexOf(option);
135
+ if (index === -1) {
136
+ selectedOptions.value.push(option);
137
+ } else {
138
+ selectedOptions.value.splice(index, 1);
139
+ }
140
+
141
+ // Handle default logic
142
+ if (selectedOptions.value.length === 0 && props.default) {
143
+ selectedOptions.value = [props.default];
144
+ }
145
+ if (
146
+ selectedOptions.value.length > 1 &&
147
+ selectedOptions.value.includes(props.default) &&
148
+ option !== props.default
149
+ ) {
150
+ selectedOptions.value = selectedOptions.value.filter((e) => e !== props.default);
151
+ } else if (option === props.default) {
152
+ selectedOptions.value = [props.default];
153
+ }
128
154
 
129
- if (typeof props.onSelect === "function") {
130
- props.onSelect(selectedOptions.value);
131
- }
155
+ if (typeof props.onSelect === "function") {
156
+ props.onSelect(selectedOptions.value);
157
+ }
132
158
 
133
- // Don't close dropdown when selecting options
159
+ // Recalculate position after selection changes
160
+ updatePosition();
134
161
  };
135
162
 
136
163
  const clearAll = () => {
137
- // Default to first option instead of empty
138
- if (props.options && props.options.length > 0) {
139
- selectedOptions.value = [props.options[0].value];
140
- if (typeof props.onSelect === "function") {
141
- props.onSelect([props.options[0].value]);
142
- }
143
- } else if (props.default) {
144
- selectedOptions.value = [props.default];
145
- if (typeof props.onSelect === "function") {
146
- props.onSelect([props.default]);
147
- }
148
- } else {
149
- selectedOptions.value = [];
150
- if (typeof props.onSelect === "function") {
151
- props.onSelect([]);
152
- }
164
+ // Default to first option instead of empty
165
+ if (props.options && props.options.length > 0) {
166
+ selectedOptions.value = [props.options[0].value];
167
+ if (typeof props.onSelect === "function") {
168
+ props.onSelect([props.options[0].value]);
169
+ }
170
+ } else if (props.default) {
171
+ selectedOptions.value = [props.default];
172
+ if (typeof props.onSelect === "function") {
173
+ props.onSelect([props.default]);
153
174
  }
175
+ } else {
176
+ selectedOptions.value = [];
177
+ if (typeof props.onSelect === "function") {
178
+ props.onSelect([]);
179
+ }
180
+ }
181
+
182
+ // Recalculate position after clearing
183
+ updatePosition();
154
184
  };
155
185
 
156
186
  // Initialize with default option if provided
157
187
  if (props.default && !selectedOptions.value.includes(props.default)) {
158
- selectedOptions.value = [props.default];
188
+ selectedOptions.value = [props.default];
159
189
  }
160
190
  </script>
161
191
 
162
192
  <style scoped>
163
193
  .dropdown {
164
- @apply relative w-full p-2 h-12 text-white ml-auto rounded-lg ring-0;
194
+ @apply relative w-full h-12 text-white ml-auto rounded-lg ring-0;
195
+ background: linear-gradient(135deg, #2a2b30 0%, #2e2f34 100%);
196
+ border: 1px solid #3d3e44;
197
+ padding: 0.75rem;
198
+ }
199
+
200
+ .dropdown:hover {
201
+ @apply border-dark-400;
202
+ }
203
+
204
+ .dropdown:focus-within {
205
+ @apply border-blue-500;
165
206
  }
166
207
 
167
208
  @media (max-width: 810px) {
168
- .dropdown {
169
- @apply h-10;
170
- }
209
+ .dropdown {
210
+ @apply h-10;
211
+ padding: 0.625rem;
212
+ }
171
213
  }
172
214
 
173
215
  .dropdown-display {
174
- @apply flex items-center justify-between z-10;
216
+ @apply flex items-center justify-between z-10;
175
217
  }
176
218
 
177
219
  .dropdown-value {
178
- @apply w-full overflow-hidden block truncate pr-2;
220
+ @apply w-full overflow-hidden block truncate pr-2 text-sm;
179
221
  }
180
222
 
181
223
  .dropdown-counter {
182
- @apply flex items-center gap-2 absolute right-2;
224
+ @apply flex items-center gap-2 absolute right-2;
183
225
  }
184
226
 
185
227
  .counter-badge {
186
- @apply bg-green-500 text-white text-xs font-semibold px-1.5 py-0.5 rounded-full text-center min-w-[18px];
228
+ @apply bg-green-500 text-white text-xs font-semibold px-1.5 py-0.5 rounded-full text-center min-w-[18px] shadow-sm;
187
229
  }
188
230
 
189
231
  .dropdown-arrow {
190
- @apply min-w-4 min-h-4 transition-transform duration-200;
232
+ @apply min-w-4 min-h-4 transition-all duration-300;
191
233
  }
192
234
 
193
- .dropdown-menu {
194
- @apply absolute border border-light-300 rounded-lg shadow-lg overflow-y-auto;
195
- top: 2.5rem;
196
- left: -1px;
197
- width: calc(100% + 2px);
198
- z-index: 10000;
199
- box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
200
- background-color: #2e2f34;
201
- overscroll-behavior: contain !important;
202
- touch-action: pan-y !important;
203
- -webkit-overflow-scrolling: touch !important;
204
- scrollbar-width: thin; /* Firefox - show thin scrollbar for testing */
205
- scrollbar-color: #555 #2e2f34; /* Firefox scrollbar colors */
235
+ .dropdown-menu-portal {
236
+ @apply rounded-xl shadow-2xl overflow-hidden;
237
+ background: linear-gradient(135deg, #2a2b30 0%, #2e2f34 100%);
238
+ border: 1px solid #3d3e44;
239
+ backdrop-filter: blur(12px);
240
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2),
241
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
242
+ overscroll-behavior: contain !important;
243
+ touch-action: pan-y !important;
244
+ -webkit-overflow-scrolling: touch !important;
245
+ scrollbar-width: thin;
246
+ scrollbar-color: #4b5563 transparent;
206
247
  }
207
248
 
208
- .dropdown-menu::-webkit-scrollbar {
209
- width: 8px; /* Chrome, Safari, Edge - show scrollbar for testing */
249
+ .dropdown-menu-portal::-webkit-scrollbar {
250
+ width: 6px;
210
251
  }
211
252
 
212
- .dropdown-menu::-webkit-scrollbar-track {
213
- background: #2e2f34;
253
+ .dropdown-menu-portal::-webkit-scrollbar-track {
254
+ background: transparent;
214
255
  }
215
256
 
216
- .dropdown-menu::-webkit-scrollbar-thumb {
217
- background: #555;
218
- border-radius: 4px;
257
+ .dropdown-menu-portal::-webkit-scrollbar-thumb {
258
+ background: #4b5563;
259
+ border-radius: 3px;
219
260
  }
220
261
 
221
- .dropdown-menu::-webkit-scrollbar-thumb:hover {
222
- background: #777;
262
+ .dropdown-menu-portal::-webkit-scrollbar-thumb:hover {
263
+ background: #6b7280;
223
264
  }
224
265
 
225
- .dropdown-menu.multi .option-list {
226
- @apply max-h-40 overflow-y-auto;
227
- overscroll-behavior: contain !important;
228
- touch-action: pan-y !important;
229
- -webkit-overflow-scrolling: touch !important;
266
+ .dropdown-menu-portal.multi .option-list {
267
+ @apply max-h-48 overflow-y-auto;
268
+ overscroll-behavior: contain !important;
269
+ touch-action: pan-y !important;
270
+ -webkit-overflow-scrolling: touch !important;
230
271
  }
231
272
 
232
273
  .dropdown-item {
233
- @apply cursor-pointer text-left w-full py-2 px-3 text-white hover:bg-dark-400 transition-all duration-150 flex justify-between items-center;
274
+ @apply cursor-pointer text-left w-full text-white transition-all duration-200 flex justify-between items-center;
275
+ padding: 0.75rem 1rem;
276
+ font-size: 0.875rem;
277
+ font-weight: 500;
278
+ border-bottom: 1px solid rgba(61, 62, 68, 0.3);
279
+ }
280
+
281
+ .dropdown-item:last-child {
282
+ border-bottom: none;
283
+ }
284
+
285
+ .dropdown-item:hover {
286
+ @apply bg-dark-600;
287
+ color: #ffffff;
288
+ }
289
+
290
+ .dropdown-item:active {
291
+ @apply bg-dark-650;
292
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);
234
293
  }
235
294
 
236
295
  .dropdown-item:first-child {
237
- @apply rounded-t-lg;
296
+ @apply rounded-t-xl;
238
297
  }
239
298
 
240
299
  .dropdown-item:last-child {
241
- @apply rounded-b-lg;
300
+ @apply rounded-b-xl;
242
301
  }
243
302
 
244
303
  .dropdown-item-text {
245
- @apply overflow-hidden;
304
+ @apply overflow-hidden;
305
+ }
306
+
307
+ /* Checkmark styling */
308
+ .dropdown-item svg {
309
+ @apply w-4 h-4;
310
+ color: #10b981;
246
311
  }
247
312
 
248
313
  .selected-summary {
249
- @apply border-t border-light-300 bg-dark-600 w-full px-4 py-3;
314
+ @apply border-t bg-dark-550 w-full px-4 py-3;
315
+ border-top: 1px solid rgba(61, 62, 68, 0.5);
250
316
  }
251
317
 
252
318
  .selected-count {
253
- @apply flex items-center gap-2;
319
+ @apply flex items-center gap-2;
254
320
  }
255
321
 
256
322
  .count-badge {
257
- @apply bg-green-500 text-white text-xs font-semibold px-2 py-1 rounded-full;
323
+ @apply bg-green-500 text-white text-xs font-semibold px-2 py-1 rounded-full shadow-sm;
258
324
  }
259
325
 
260
326
  .count-label {
261
- @apply text-xs text-light-400 font-medium;
327
+ @apply text-xs font-medium text-light-400;
262
328
  }
263
329
 
264
330
  .clear-button {
265
- @apply text-xs bg-dark-500 hover:bg-dark-400 text-white transition-all duration-150 font-medium px-2 py-1 rounded border border-light-300 hover:border-light-400;
331
+ @apply text-xs bg-red-500 text-white transition-colors duration-200 font-medium px-3 py-1.5 rounded-lg shadow-sm;
332
+ }
333
+
334
+ .clear-button:hover {
335
+ @apply bg-red-400;
266
336
  }
267
337
 
268
338
  /* Transition animations */
269
- .dropdown-fade-enter-active,
339
+ .dropdown-fade-enter-active {
340
+ @apply transition-all duration-300;
341
+ }
342
+
270
343
  .dropdown-fade-leave-active {
271
- @apply transition-all duration-200;
344
+ @apply transition-all duration-200;
345
+ }
346
+
347
+ .dropdown-fade-enter-from {
348
+ @apply opacity-0;
349
+ transform: translateY(-8px) scale(0.95);
272
350
  }
273
351
 
274
- .dropdown-fade-enter-from,
275
352
  .dropdown-fade-leave-to {
276
- @apply opacity-0 transform scale-95 -translate-y-1;
353
+ @apply opacity-0;
354
+ transform: translateY(-4px) scale(0.98);
277
355
  }
278
356
 
279
357
  .dropdown-fade-enter-to,
280
358
  .dropdown-fade-leave-from {
281
- @apply opacity-100 transform scale-100 translate-y-0;
359
+ @apply opacity-100;
360
+ transform: translateY(0) scale(1);
282
361
  }
283
362
  </style>
@@ -0,0 +1,21 @@
1
+ import { onMounted, onUnmounted } from "vue";
2
+
3
+ export function useClickOutside(elementRef, callback) {
4
+ const handleClickOutside = (event) => {
5
+ if (elementRef.value && !elementRef.value.contains(event.target)) {
6
+ callback();
7
+ }
8
+ };
9
+
10
+ onMounted(() => {
11
+ document.addEventListener("click", handleClickOutside);
12
+ });
13
+
14
+ onUnmounted(() => {
15
+ document.removeEventListener("click", handleClickOutside);
16
+ });
17
+
18
+ return {
19
+ handleClickOutside
20
+ };
21
+ }