@geode/opengeodeweb-front 10.12.0 → 10.13.0-rc.1

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.
@@ -0,0 +1,58 @@
1
+ <script setup>
2
+ import ActionButton from "@ogw_front/components/ActionButton.vue";
3
+ import SearchBar from "@ogw_front/components/SearchBar.vue";
4
+
5
+ const { search, sortType, filterOptions, availableFilterOptions } = defineProps({
6
+ search: { type: String, required: true },
7
+ sortType: { type: String, required: true },
8
+ filterOptions: { type: Object, required: true },
9
+ availableFilterOptions: { type: Array, required: true },
10
+ });
11
+
12
+ const emit = defineEmits(["update:search", "toggle-sort"]);
13
+ </script>
14
+
15
+ <template>
16
+ <v-row dense align="center" class="pa-2">
17
+ <v-col>
18
+ <SearchBar
19
+ :model-value="search"
20
+ label="Search"
21
+ color="black"
22
+ base-color="black"
23
+ @update:model-value="emit('update:search', $event)"
24
+ />
25
+ </v-col>
26
+ <v-col cols="auto" class="d-flex align-center">
27
+ <ActionButton
28
+ :tooltip="'Sort by ' + (sortType === 'name' ? 'ID' : 'Name')"
29
+ :icon="
30
+ sortType === 'name' ? 'mdi-sort-alphabetical-ascending' : 'mdi-sort-numeric-ascending'
31
+ "
32
+ tooltipLocation="bottom"
33
+ @click="emit('toggle-sort')"
34
+ />
35
+ <v-menu :close-on-content-click="false">
36
+ <template #activator="{ props: menuProps }">
37
+ <ActionButton
38
+ tooltip="Filter options"
39
+ icon="mdi-filter-variant"
40
+ tooltipLocation="bottom"
41
+ class="ml-1"
42
+ v-bind="menuProps"
43
+ />
44
+ </template>
45
+ <v-list class="mt-1">
46
+ <v-list-item v-for="category_id in availableFilterOptions" :key="category_id">
47
+ <v-checkbox
48
+ v-model="filterOptions[category_id]"
49
+ :label="category_id"
50
+ hide-details
51
+ density="compact"
52
+ />
53
+ </v-list-item>
54
+ </v-list>
55
+ </v-menu>
56
+ </v-col>
57
+ </v-row>
58
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup>
2
+ const { item, showTooltip } = defineProps({
3
+ item: { type: Object, required: true },
4
+ showTooltip: { type: Boolean, default: false },
5
+ });
6
+
7
+ const emit = defineEmits(["contextmenu"]);
8
+
9
+ const actualItem = computed(() => item.raw || item);
10
+ </script>
11
+
12
+ <template>
13
+ <span
14
+ class="tree-item-label"
15
+ :class="{ 'inactive-item': actualItem.is_active === false }"
16
+ @contextmenu.prevent.stop="emit('contextmenu', $event)"
17
+ >
18
+ {{ actualItem.title }}
19
+ <v-tooltip v-if="showTooltip && actualItem.category" activator="parent" location="right">
20
+ <div class="d-flex flex-column pa-1">
21
+ <span class="text-caption"><strong>ID:</strong> {{ actualItem.id }}</span>
22
+ <span v-if="actualItem.title" class="text-caption"
23
+ ><strong>Name:</strong> {{ actualItem.title }}</span
24
+ >
25
+ <span class="text-caption font-italic border-t-sm d-flex align-center">
26
+ <strong class="mr-1">Status:</strong>
27
+ {{ actualItem.is_active ? "Active" : "Inactive" }}
28
+ </span>
29
+ </div>
30
+ </v-tooltip>
31
+ </span>
32
+ </template>
33
+
34
+ <style scoped>
35
+ .tree-item-label {
36
+ white-space: nowrap;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ max-width: 100%;
40
+ display: inline-flex;
41
+ align-items: center;
42
+ cursor: pointer;
43
+ }
44
+
45
+ .inactive-item {
46
+ opacity: 0.4;
47
+ font-style: italic;
48
+ }
49
+ </style>
@@ -0,0 +1,132 @@
1
+ <script setup>
2
+ const SCROLL_SYNC_DELAY = 50;
3
+ const SCROLL_THRESHOLD = 1;
4
+ const { title, closable, icon, mdiIcon, scrollTop } = defineProps({
5
+ title: { type: String, required: true },
6
+ closable: { type: Boolean, default: false },
7
+ icon: { type: String, default: "" },
8
+ mdiIcon: { type: String, default: "" },
9
+ scrollTop: { type: Number, default: 0 },
10
+ });
11
+
12
+ const emit = defineEmits(["close", "dragstart", "update:scrollTop"]);
13
+
14
+ const scrollContainer = ref(undefined);
15
+ let isApplyingScroll = false;
16
+ let resizeObserver = undefined;
17
+
18
+ function handleScroll(event) {
19
+ if (isApplyingScroll) {
20
+ return;
21
+ }
22
+ emit("update:scrollTop", event.target.scrollTop);
23
+ }
24
+
25
+ function applyScrollTop(val) {
26
+ if (scrollContainer.value) {
27
+ isApplyingScroll = true;
28
+ scrollContainer.value.scrollTop = val;
29
+ setTimeout(() => {
30
+ isApplyingScroll = false;
31
+ }, SCROLL_SYNC_DELAY);
32
+ }
33
+ }
34
+
35
+ onMounted(() => {
36
+ if (scrollContainer.value) {
37
+ applyScrollTop(scrollTop);
38
+
39
+ resizeObserver = new ResizeObserver(() => {
40
+ if (
41
+ scrollContainer.value &&
42
+ Math.abs(scrollContainer.value.scrollTop - scrollTop) > SCROLL_THRESHOLD
43
+ ) {
44
+ applyScrollTop(scrollTop);
45
+ }
46
+ });
47
+
48
+ resizeObserver.observe(scrollContainer.value);
49
+ }
50
+ });
51
+
52
+ onUnmounted(() => {
53
+ if (resizeObserver) {
54
+ resizeObserver.disconnect();
55
+ }
56
+ });
57
+
58
+ watch(
59
+ () => scrollTop,
60
+ (newVal) => {
61
+ if (
62
+ scrollContainer.value &&
63
+ Math.abs(scrollContainer.value.scrollTop - newVal) > SCROLL_THRESHOLD
64
+ ) {
65
+ applyScrollTop(newVal);
66
+ }
67
+ },
68
+ );
69
+ </script>
70
+
71
+ <template>
72
+ <v-card variant="outlined" class="tree-box d-flex flex-column">
73
+ <v-card-title
74
+ class="tree-box-header d-flex align-center"
75
+ :class="{ 'cursor-grab': closable }"
76
+ :draggable="closable"
77
+ @dragstart="emit('dragstart', $event)"
78
+ >
79
+ <v-img
80
+ v-if="icon"
81
+ :src="icon"
82
+ width="24"
83
+ height="24"
84
+ max-width="24"
85
+ class="mr-2"
86
+ style="filter: brightness(0); display: flex; align-items: center"
87
+ />
88
+ <v-icon v-else-if="mdiIcon" size="24" class="mr-2">{{ mdiIcon }}</v-icon>
89
+ <v-icon v-else-if="closable" size="24" class="mr-2">mdi-drag-variant</v-icon>
90
+ <span
91
+ class="text-subtitle-2 font-weight-bold d-inline-flex align-center"
92
+ style="height: 24px; line-height: 1"
93
+ >
94
+ {{ title }}
95
+ </span>
96
+ <v-spacer />
97
+ <v-btn
98
+ v-if="closable"
99
+ icon="mdi-close"
100
+ variant="text"
101
+ size="x-small"
102
+ @click="emit('close')"
103
+ />
104
+ </v-card-title>
105
+ <v-divider />
106
+ <v-card-text class="pa-0 flex-grow-1 overflow-hidden d-flex flex-column">
107
+ <div
108
+ ref="scrollContainer"
109
+ class="flex-grow-1 overflow-y-auto overflow-x-hidden"
110
+ @scroll="handleScroll"
111
+ >
112
+ <slot />
113
+ </div>
114
+ </v-card-text>
115
+ </v-card>
116
+ </template>
117
+
118
+ <style scoped>
119
+ .tree-box {
120
+ height: 100%;
121
+ border-radius: 16px;
122
+ background-color: transparent !important;
123
+ backdrop-filter: blur(2px);
124
+ border: 1px solid rgba(0, 0, 0, 0.2);
125
+ }
126
+
127
+ .tree-box-header {
128
+ height: 40px !important;
129
+ padding: 0 8px !important;
130
+ background-color: rgba(255, 255, 255, 0.1);
131
+ }
132
+ </style>
@@ -0,0 +1,320 @@
1
+ <script setup>
2
+ import GlobalObjects from "@ogw_front/components/Viewer/ObjectTree/Views/GlobalObjects.vue";
3
+ import ModelComponents from "@ogw_front/components/Viewer/ObjectTree/Views/ModelComponents.vue";
4
+ import ViewerObjectTreeBox from "@ogw_front/components/Viewer/ObjectTree/Box.vue";
5
+ import { geode_objects } from "@ogw_front/assets/geode_objects";
6
+ import { useTreeviewStore } from "@ogw_front/stores/treeview";
7
+
8
+ const WIDTH_MIN = 200;
9
+ const HEIGHT_MIN = 150;
10
+ const GAP_WIDTH = 10;
11
+ const PERCENT_100 = 100;
12
+
13
+ const TOTAL_PERCENT = 100;
14
+
15
+ const treeviewStore = useTreeviewStore();
16
+ const emit = defineEmits(["show-menu"]);
17
+
18
+ const mainView = computed(() => treeviewStore.opened_views[0]);
19
+ const additionalViews = computed(() => treeviewStore.opened_views.slice(1));
20
+
21
+ const totalWidth = computed(() => {
22
+ const hasAdditional = additionalViews.value.length > 0;
23
+ const gap = hasAdditional ? GAP_WIDTH : 0;
24
+ const secondColWidth = hasAdditional ? treeviewStore.additionalPanelWidth : 0;
25
+ return `${treeviewStore.panelWidth + secondColWidth + gap}px`;
26
+ });
27
+
28
+ const rowHeights = computed({
29
+ get: () => treeviewStore.rowHeights,
30
+ set: (val) => treeviewStore.setRowHeights(val),
31
+ });
32
+ const draggedIndex = ref(undefined);
33
+
34
+ watch(
35
+ () => additionalViews.value.length,
36
+ (newLength) => {
37
+ if (newLength > 0 && rowHeights.value.length !== newLength) {
38
+ treeviewStore.setRowHeights(Array.from({ length: newLength }).fill(PERCENT_100 / newLength));
39
+ }
40
+ },
41
+ { immediate: true },
42
+ );
43
+
44
+ function onDragStart(index) {
45
+ draggedIndex.value = index;
46
+ }
47
+
48
+ function onDragOver(event) {
49
+ event.preventDefault();
50
+ }
51
+
52
+ function onDrop(targetIndex) {
53
+ if (draggedIndex.value !== undefined && draggedIndex.value !== targetIndex) {
54
+ treeviewStore.moveView(draggedIndex.value, targetIndex);
55
+ }
56
+ draggedIndex.value = undefined;
57
+ }
58
+
59
+ function onResizeStart(event) {
60
+ const startWidth = treeviewStore.panelWidth;
61
+ const startX = event.clientX;
62
+ function resize(move_event) {
63
+ const deltaX = move_event.clientX - startX;
64
+ const newWidth = Math.max(WIDTH_MIN, startWidth + deltaX);
65
+ treeviewStore.setPanelWidth(newWidth);
66
+ document.body.style.userSelect = "none";
67
+ }
68
+ function stopResize() {
69
+ document.removeEventListener("mousemove", resize);
70
+ document.removeEventListener("mouseup", stopResize);
71
+ document.body.style.userSelect = "";
72
+ }
73
+ document.addEventListener("mousemove", resize);
74
+ document.addEventListener("mouseup", stopResize);
75
+ }
76
+
77
+ function onAdditionalResizeStart(event) {
78
+ const startWidth = treeviewStore.additionalPanelWidth;
79
+ const startX = event.clientX;
80
+ function resize(move_event) {
81
+ const deltaX = move_event.clientX - startX;
82
+ const newWidth = Math.max(WIDTH_MIN, startWidth + deltaX);
83
+ treeviewStore.setAdditionalPanelWidth(newWidth);
84
+ document.body.style.userSelect = "none";
85
+ }
86
+ function stopResize() {
87
+ document.removeEventListener("mousemove", resize);
88
+ document.removeEventListener("mouseup", stopResize);
89
+ document.body.style.userSelect = "";
90
+ }
91
+ document.addEventListener("mousemove", resize);
92
+ document.addEventListener("mouseup", stopResize);
93
+ }
94
+
95
+ function onVerticalResizeStart(event, index) {
96
+ const startY = event.clientY;
97
+ const startHeight1 = rowHeights.value[index];
98
+ const startHeight2 = rowHeights.value[index + 1];
99
+ const containerHeight = event.currentTarget.parentElement.offsetHeight;
100
+
101
+ function resize(move_event) {
102
+ const deltaY = move_event.clientY - startY;
103
+ const deltaPercent = (deltaY / containerHeight) * PERCENT_100;
104
+ const minHeightPercent = (HEIGHT_MIN / containerHeight) * PERCENT_100;
105
+
106
+ let newH1 = startHeight1 + deltaPercent;
107
+ let newH2 = startHeight2 - deltaPercent;
108
+
109
+ if (newH1 < minHeightPercent) {
110
+ newH1 = minHeightPercent;
111
+ newH2 = startHeight1 + startHeight2 - minHeightPercent;
112
+ } else if (newH2 < minHeightPercent) {
113
+ newH2 = minHeightPercent;
114
+ newH1 = startHeight1 + startHeight2 - minHeightPercent;
115
+ }
116
+
117
+ const newHeights = [...rowHeights.value];
118
+ newHeights[index] = newH1;
119
+ newHeights[index + 1] = newH2;
120
+ treeviewStore.setRowHeights(newHeights);
121
+
122
+ document.body.style.userSelect = "none";
123
+ document.body.style.cursor = "ns-resize";
124
+ }
125
+
126
+ function stopResize() {
127
+ document.removeEventListener("mousemove", resize);
128
+ document.removeEventListener("mouseup", stopResize);
129
+ document.body.style.userSelect = "";
130
+ document.body.style.cursor = "";
131
+ }
132
+ document.addEventListener("mousemove", resize);
133
+ document.addEventListener("mouseup", stopResize);
134
+ }
135
+ </script>
136
+
137
+ <template>
138
+ <div
139
+ v-if="treeviewStore.items.length > 0"
140
+ class="treeview-container d-flex"
141
+ :style="{ width: totalWidth }"
142
+ @contextmenu.prevent
143
+ @mousedown.stop
144
+ >
145
+ <div
146
+ class="column main-column"
147
+ :style="{
148
+ width: `${treeviewStore.panelWidth}px`,
149
+ }"
150
+ >
151
+ <ViewerObjectTreeBox
152
+ :title="mainView.title"
153
+ mdi-icon="mdi-file-tree-outline"
154
+ :scroll-top="mainView.scrollTop"
155
+ @update:scroll-top="treeviewStore.setScrollTop(mainView.id, $event)"
156
+ >
157
+ <GlobalObjects @show-menu="emit('show-menu', $event)" />
158
+ </ViewerObjectTreeBox>
159
+ </div>
160
+
161
+ <div v-if="additionalViews.length > 0" class="column-separator" @mousedown="onResizeStart" />
162
+
163
+ <div
164
+ v-if="additionalViews.length > 0"
165
+ class="column additional-column"
166
+ :style="{
167
+ width: `${treeviewStore.additionalPanelWidth}px`,
168
+ }"
169
+ >
170
+ <template v-for="(view, index) in additionalViews" :key="view.id">
171
+ <div
172
+ class="view-wrapper"
173
+ :class="{
174
+ 'drag-over': draggedIndex !== undefined && draggedIndex !== index + 1,
175
+ }"
176
+ :style="{ flex: `0 0 ${rowHeights[index]}%` }"
177
+ @dragover="onDragOver"
178
+ @drop="onDrop(index + 1)"
179
+ >
180
+ <ViewerObjectTreeBox
181
+ :title="view.title"
182
+ :icon="geode_objects[view.geode_object_type]?.image"
183
+ :scroll-top="view.scrollTop"
184
+ closable
185
+ @close="treeviewStore.closeView(index + 1)"
186
+ @dragstart="onDragStart(index + 1)"
187
+ @update:scroll-top="treeviewStore.setScrollTop(view.id, $event)"
188
+ >
189
+ <ModelComponents :id="view.id" @show-menu="emit('show-menu', $event)" />
190
+ </ViewerObjectTreeBox>
191
+ </div>
192
+ <div
193
+ v-if="index < additionalViews.length - 1"
194
+ class="v-split-resizer"
195
+ @mousedown="onVerticalResizeStart($event, index)"
196
+ />
197
+ </template>
198
+ </div>
199
+ <div
200
+ class="total-resizer"
201
+ @mousedown="
202
+ additionalViews.length > 0 ? onAdditionalResizeStart($event) : onResizeStart($event)
203
+ "
204
+ />
205
+ </div>
206
+ </template>
207
+
208
+ <style scoped>
209
+ .treeview-container {
210
+ position: absolute;
211
+ z-index: 2;
212
+ left: 0;
213
+ top: 0;
214
+ height: calc(100vh - 100px);
215
+ margin-top: 10px;
216
+ margin-left: 10px;
217
+ pointer-events: auto;
218
+ width: max-content;
219
+ min-width: min-content;
220
+ }
221
+
222
+ .layout-root {
223
+ display: flex;
224
+ height: 100%;
225
+ }
226
+
227
+ .column {
228
+ display: flex;
229
+ flex-direction: column;
230
+ height: 100%;
231
+ overflow-y: auto;
232
+ overflow-x: hidden;
233
+ flex-shrink: 0;
234
+ }
235
+
236
+ .main-column {
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .additional-column {
241
+ flex-shrink: 0;
242
+ }
243
+
244
+ .view-wrapper {
245
+ overflow: hidden;
246
+ padding: 2px;
247
+ transition: transform 0.2s;
248
+ min-height: 150px;
249
+ }
250
+
251
+ .view-wrapper.drag-over {
252
+ border: 2px dashed rgba(0, 0, 0, 0.2);
253
+ border-radius: 16px;
254
+ background-color: rgba(0, 0, 0, 0.02);
255
+ }
256
+
257
+ .column-separator {
258
+ width: 6px;
259
+ background-color: rgba(0, 0, 0, 0.05);
260
+ border-radius: 3px;
261
+ margin: 0 2px;
262
+ cursor: ew-resize;
263
+ position: relative;
264
+ }
265
+
266
+ .column-separator:hover {
267
+ background-color: rgba(0, 0, 0, 0.2);
268
+ }
269
+
270
+ .column-separator::after {
271
+ content: "";
272
+ position: absolute;
273
+ top: 10%;
274
+ bottom: 10%;
275
+ left: 2px;
276
+ width: 2px;
277
+ background-color: rgba(0, 0, 0, 0.1);
278
+ border-radius: 1px;
279
+ }
280
+
281
+ .v-split-resizer {
282
+ height: 6px;
283
+ position: relative;
284
+ cursor: ns-resize;
285
+ background-color: transparent;
286
+ z-index: 5;
287
+ }
288
+
289
+ .v-split-resizer::after {
290
+ content: "";
291
+ position: absolute;
292
+ top: 2px;
293
+ left: 10%;
294
+ right: 10%;
295
+ height: 2px;
296
+ background-color: rgba(0, 0, 0, 0.1);
297
+ border-radius: 1px;
298
+ transition: background-color 0.2s;
299
+ }
300
+
301
+ .v-split-resizer:hover::after {
302
+ background-color: rgba(0, 0, 0, 0.4);
303
+ height: 3px;
304
+ top: 1.5px;
305
+ }
306
+
307
+ .total-resizer {
308
+ position: absolute;
309
+ top: 0;
310
+ right: -3px;
311
+ width: 8px;
312
+ height: 100%;
313
+ cursor: ew-resize;
314
+ z-index: 10;
315
+ }
316
+
317
+ .total-resizer:hover {
318
+ background-color: rgba(0, 0, 0, 0.2);
319
+ }
320
+ </style>
@@ -0,0 +1,129 @@
1
+ <script setup>
2
+ import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
3
+ import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
4
+ import { compareSelections } from "@ogw_front/utils/treeview";
5
+ import { useDataStyleStore } from "@ogw_front/stores/data_style";
6
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
7
+ import { useTreeFilter } from "@ogw_front/composables/use_tree_filter";
8
+ import { useTreeviewStore } from "@ogw_front/stores/treeview";
9
+
10
+ const treeviewStore = useTreeviewStore();
11
+ const dataStyleStore = useDataStyleStore();
12
+ const hybridViewerStore = useHybridViewerStore();
13
+
14
+ const emit = defineEmits(["show-menu"]);
15
+
16
+ const mainView = computed(() => treeviewStore.opened_views[0]);
17
+ const opened = computed({
18
+ get: () => mainView.value?.opened || [],
19
+ set: (val) => treeviewStore.setOpened(mainView.value.id, val),
20
+ });
21
+
22
+ const {
23
+ search,
24
+ sortType,
25
+ filterOptions,
26
+ processedItems,
27
+ availableFilterOptions,
28
+ toggleSort,
29
+ customFilter,
30
+ } = useTreeFilter(toRef(() => treeviewStore.items));
31
+
32
+ watch(
33
+ () => treeviewStore.selection,
34
+ async (current, previous) => {
35
+ const oldSelection = previous || [];
36
+ if (current === oldSelection) {
37
+ return;
38
+ }
39
+
40
+ const { added, removed } = compareSelections(current, previous);
41
+
42
+ const allObjectIds = new Set(
43
+ treeviewStore.items.flatMap((group) => group.children.map((child) => child.id)),
44
+ );
45
+
46
+ const updates = [
47
+ ...added
48
+ .filter((id) => allObjectIds.has(id))
49
+ .map((id) => dataStyleStore.setVisibility(id, true)),
50
+ ...removed
51
+ .filter((id) => allObjectIds.has(id))
52
+ .map((id) => dataStyleStore.setVisibility(id, false)),
53
+ ];
54
+ await Promise.all(updates);
55
+ hybridViewerStore.remoteRender();
56
+ },
57
+ );
58
+
59
+ function isModel(item) {
60
+ const actualItem = item.raw || item;
61
+ return (
62
+ actualItem.viewer_type === "model" || ["BRep", "Section"].includes(actualItem.geode_object_type)
63
+ );
64
+ }
65
+ </script>
66
+
67
+ <template>
68
+ <div class="tree-view-container">
69
+ <ObjectTreeControls
70
+ v-model:search="search"
71
+ :sort-type="sortType"
72
+ :filter-options="filterOptions"
73
+ :available-filter-options="availableFilterOptions"
74
+ @toggle-sort="toggleSort"
75
+ />
76
+
77
+ <v-treeview
78
+ v-model:selected="treeviewStore.selection"
79
+ v-model:opened="opened"
80
+ :items="processedItems"
81
+ :search="search"
82
+ :custom-filter="customFilter"
83
+ class="transparent-treeview"
84
+ item-value="id"
85
+ select-strategy="classic"
86
+ selectable
87
+ >
88
+ <template #title="{ item }">
89
+ <ObjectTreeItemLabel
90
+ :item="item"
91
+ @contextmenu="emit('show-menu', { event: $event, itemId: item.id })"
92
+ />
93
+ </template>
94
+
95
+ <template #append="{ item }">
96
+ <v-btn
97
+ v-if="isModel(item)"
98
+ icon="mdi-magnify-expand"
99
+ size="medium"
100
+ class="ml-8"
101
+ variant="text"
102
+ v-tooltip="'Model\'s mesh components'"
103
+ @click.stop="
104
+ treeviewStore.displayAdditionalTree(item.id, item.title, item.geode_object_type)
105
+ "
106
+ />
107
+ </template>
108
+ </v-treeview>
109
+ </div>
110
+ </template>
111
+
112
+ <style scoped>
113
+ .transparent-treeview {
114
+ background-color: transparent;
115
+ margin: 4px 0;
116
+ }
117
+
118
+ :deep(.v-list-item) {
119
+ transition: background-color 0.2s ease;
120
+ }
121
+
122
+ :deep(.v-list-item--active > .v-list-item__overlay) {
123
+ opacity: 0 !important;
124
+ }
125
+
126
+ :deep(.v-list-item:hover > .v-list-item__overlay) {
127
+ opacity: 0.1 !important;
128
+ }
129
+ </style>