@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.
- package/.claude/settings.local.json +2 -1
- package/exit +209 -0
- package/index.html +1 -1
- package/package.json +1 -1
- package/postinstall.js +9 -0
- package/public/manifest.json +8 -3
- package/src/assets/css/_input.scss +104 -111
- package/src/assets/css/_utilities.scss +441 -0
- package/src/assets/css/main.scss +228 -154
- package/src/components/Auth/LoginForm.vue +8 -8
- package/src/components/Editors/Account/Account.vue +156 -146
- package/src/components/Editors/Account/AccountCreator.vue +1 -1
- package/src/components/Editors/Account/AccountView.vue +13 -13
- package/src/components/Editors/Account/CreateAccount.vue +25 -16
- package/src/components/Editors/Profile/CreateProfile.vue +1 -1
- package/src/components/Editors/Profile/Profile.vue +1 -1
- package/src/components/Editors/Profile/ProfileCountryChooser.vue +83 -19
- package/src/components/Editors/Profile/ProfileView.vue +11 -11
- package/src/components/Tasks/CreateTaskAXS.vue +3 -3
- package/src/components/Tasks/CreateTaskTM.vue +7 -35
- package/src/components/Tasks/QuickSettings.vue +112 -9
- package/src/components/Tasks/Stats.vue +29 -25
- package/src/components/Tasks/Task.vue +489 -365
- package/src/components/Tasks/TaskView.vue +21 -23
- package/src/components/icons/Sandclock.vue +2 -2
- package/src/components/icons/Stadium.vue +1 -1
- package/src/components/ui/Modal.vue +37 -35
- package/src/components/ui/controls/CountryChooser.vue +200 -62
- package/src/components/ui/controls/atomic/Dropdown.vue +177 -91
- package/src/components/ui/controls/atomic/MultiDropdown.vue +247 -168
- package/src/composables/useClickOutside.js +21 -0
- package/src/composables/useDropdownPosition.js +174 -0
- package/src/stores/ui.js +5 -4
- package/src/views/Accounts.vue +2 -2
- package/src/views/Console.vue +25 -45
- package/src/views/Editor.vue +1194 -730
- package/src/views/Profiles.vue +2 -2
- package/src/views/Tasks.vue +170 -137
- package/tailwind.config.js +47 -21
|
@@ -1,283 +1,362 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
};
|
|
118
|
+
useClickOutside(dropdownRef, () => {
|
|
119
|
+
if (opened.value) {
|
|
120
|
+
ui.setCurrentDropdown("");
|
|
121
|
+
}
|
|
122
|
+
});
|
|
105
123
|
|
|
106
124
|
const toggleOpened = () => {
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
if (typeof props.onSelect === "function") {
|
|
156
|
+
props.onSelect(selectedOptions.value);
|
|
157
|
+
}
|
|
132
158
|
|
|
133
|
-
|
|
159
|
+
// Recalculate position after selection changes
|
|
160
|
+
updatePosition();
|
|
134
161
|
};
|
|
135
162
|
|
|
136
163
|
const clearAll = () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
188
|
+
selectedOptions.value = [props.default];
|
|
159
189
|
}
|
|
160
190
|
</script>
|
|
161
191
|
|
|
162
192
|
<style scoped>
|
|
163
193
|
.dropdown {
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
.dropdown {
|
|
210
|
+
@apply h-10;
|
|
211
|
+
padding: 0.625rem;
|
|
212
|
+
}
|
|
171
213
|
}
|
|
172
214
|
|
|
173
215
|
.dropdown-display {
|
|
174
|
-
|
|
216
|
+
@apply flex items-center justify-between z-10;
|
|
175
217
|
}
|
|
176
218
|
|
|
177
219
|
.dropdown-value {
|
|
178
|
-
|
|
220
|
+
@apply w-full overflow-hidden block truncate pr-2 text-sm;
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
.dropdown-counter {
|
|
182
|
-
|
|
224
|
+
@apply flex items-center gap-2 absolute right-2;
|
|
183
225
|
}
|
|
184
226
|
|
|
185
227
|
.counter-badge {
|
|
186
|
-
|
|
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
|
-
|
|
232
|
+
@apply min-w-4 min-h-4 transition-all duration-300;
|
|
191
233
|
}
|
|
192
234
|
|
|
193
|
-
.dropdown-menu {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
249
|
+
.dropdown-menu-portal::-webkit-scrollbar {
|
|
250
|
+
width: 6px;
|
|
210
251
|
}
|
|
211
252
|
|
|
212
|
-
.dropdown-menu::-webkit-scrollbar-track {
|
|
213
|
-
|
|
253
|
+
.dropdown-menu-portal::-webkit-scrollbar-track {
|
|
254
|
+
background: transparent;
|
|
214
255
|
}
|
|
215
256
|
|
|
216
|
-
.dropdown-menu::-webkit-scrollbar-thumb {
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
262
|
+
.dropdown-menu-portal::-webkit-scrollbar-thumb:hover {
|
|
263
|
+
background: #6b7280;
|
|
223
264
|
}
|
|
224
265
|
|
|
225
|
-
.dropdown-menu.multi .option-list {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
+
@apply rounded-t-xl;
|
|
238
297
|
}
|
|
239
298
|
|
|
240
299
|
.dropdown-item:last-child {
|
|
241
|
-
|
|
300
|
+
@apply rounded-b-xl;
|
|
242
301
|
}
|
|
243
302
|
|
|
244
303
|
.dropdown-item-text {
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
+
@apply flex items-center gap-2;
|
|
254
320
|
}
|
|
255
321
|
|
|
256
322
|
.count-badge {
|
|
257
|
-
|
|
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
|
-
|
|
327
|
+
@apply text-xs font-medium text-light-400;
|
|
262
328
|
}
|
|
263
329
|
|
|
264
330
|
.clear-button {
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|