@mt2025ui/mt-design 1.0.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.
- package/.vscode/extensions.json +3 -0
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/index.html +14 -0
- package/package.json +25 -0
- package/public/vite.svg +1 -0
- package/src/App.vue +142 -0
- package/src/assets/iconfont/iconfont.js +70 -0
- package/src/assets/svg/DragIcon.vue +14 -0
- package/src/assets/svg/LockIcon.vue +14 -0
- package/src/assets/svg/UnlockIcon.vue +14 -0
- package/src/assets/vue.svg +1 -0
- package/src/components/MtAttach/MtAttachmentDisplay.vue +240 -0
- package/src/components/MtAttach/MtAttachmentUpload.vue +138 -0
- package/src/components/MtIcon/MtIcon.vue +48 -0
- package/src/components/MtIcon/index.ts +4 -0
- package/src/components/MtLayout/MtContainer.vue +97 -0
- package/src/components/MtLayout/MtFloatingPanel.vue +583 -0
- package/src/components/MtLayout/MtLayout.vue +99 -0
- package/src/components/MtLayout/MtLayoutItem.vue +1049 -0
- package/src/components/MtLayout/icons/CloseIcon.vue +7 -0
- package/src/components/MtLayout/icons/LockIcon.vue +7 -0
- package/src/components/MtLayout/icons/MenuIcon.vue +5 -0
- package/src/components/MtLayout/icons/UnlockIcon.vue +7 -0
- package/src/components/MtLayout/index.ts +7 -0
- package/src/components/MtLayout/registry.ts +15 -0
- package/src/example/mtFloating/Demo.vue +266 -0
- package/src/example/mtIcon/Demo.vue +151 -0
- package/src/example/mtLayout/Demo.vue +105 -0
- package/src/index.ts +29 -0
- package/src/main.ts +7 -0
- package/src/style.css +88 -0
- package/tsconfig.app.json +17 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +31 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="mt-layout-item"
|
|
4
|
+
:class="[
|
|
5
|
+
{ 'is-collapsed': isCollapsed },
|
|
6
|
+
{ 'has-containers': containers.length > 0 },
|
|
7
|
+
]"
|
|
8
|
+
:style="itemStyle"
|
|
9
|
+
ref="itemRef"
|
|
10
|
+
>
|
|
11
|
+
<!-- Header -->
|
|
12
|
+
<div class="mt-item-header" v-if="!hideHeader">
|
|
13
|
+
<!-- Left: Title/Tabs -->
|
|
14
|
+
<div class="mt-header-left">
|
|
15
|
+
<!-- Tabs Mode -->
|
|
16
|
+
<div v-if="containers.length > 0" class="mt-tabs-wrapper">
|
|
17
|
+
<div class="mt-tabs" ref="tabsRef">
|
|
18
|
+
<div
|
|
19
|
+
v-for="c in containers"
|
|
20
|
+
:key="c.id"
|
|
21
|
+
class="mt-tab"
|
|
22
|
+
:class="{ active: activeContainerId === c.id }"
|
|
23
|
+
:draggable="!gridLock"
|
|
24
|
+
@click="activateContainer(c.id)"
|
|
25
|
+
@dragstart="onTabDragStart($event, c.id)"
|
|
26
|
+
@dragover.prevent
|
|
27
|
+
@drop.stop="onTabDrop($event, c.id)"
|
|
28
|
+
>
|
|
29
|
+
<span class="mt-tab-text">{{ c.title }}</span>
|
|
30
|
+
<button class="mt-tab-close" @click.stop="closeContainer(c.id)">
|
|
31
|
+
<CloseIcon />
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Overflow Menu Button -->
|
|
37
|
+
<div v-if="showOverflowBtn" class="mt-tabs-overflow">
|
|
38
|
+
<button
|
|
39
|
+
class="mt-icon-btn overflow-btn"
|
|
40
|
+
@click.stop="toggleOverflowMenu"
|
|
41
|
+
ref="overflowBtnRef"
|
|
42
|
+
>
|
|
43
|
+
<MenuIcon />
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<!-- Dropdown Menu -->
|
|
47
|
+
<Teleport to="body">
|
|
48
|
+
<div
|
|
49
|
+
v-if="showOverflowMenu"
|
|
50
|
+
class="mt-overflow-menu"
|
|
51
|
+
:style="overflowMenuStyle"
|
|
52
|
+
@click.stop
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
v-for="c in containers"
|
|
56
|
+
:key="c.id"
|
|
57
|
+
class="mt-menu-item"
|
|
58
|
+
:class="{ active: activeContainerId === c.id }"
|
|
59
|
+
@click="selectFromMenu(c.id)"
|
|
60
|
+
>
|
|
61
|
+
<span class="mt-menu-text">{{ c.title }}</span>
|
|
62
|
+
<button
|
|
63
|
+
class="mt-menu-close"
|
|
64
|
+
@click.stop="closeContainer(c.id)"
|
|
65
|
+
>
|
|
66
|
+
<CloseIcon />
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</Teleport>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<!-- Title Mode -->
|
|
74
|
+
<div v-else class="mt-title">
|
|
75
|
+
{{ title || "Grid" }}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Right: Collapse & Grid Lock -->
|
|
80
|
+
<div class="mt-header-right">
|
|
81
|
+
<button
|
|
82
|
+
v-if="collapsible"
|
|
83
|
+
class="mt-icon-btn collapse-btn"
|
|
84
|
+
@click="toggleCollapse"
|
|
85
|
+
>
|
|
86
|
+
<span class="arrow" :class="{ rotated: isCollapsed }">▼</span>
|
|
87
|
+
</button>
|
|
88
|
+
<button class="mt-icon-btn lock-btn" @click="toggleGridLock">
|
|
89
|
+
<LockIcon v-if="gridLock" />
|
|
90
|
+
<UnlockIcon v-else />
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Content -->
|
|
96
|
+
<div
|
|
97
|
+
class="mt-item-content"
|
|
98
|
+
:id="contentId"
|
|
99
|
+
v-show="!isCollapsed"
|
|
100
|
+
@dragover.prevent
|
|
101
|
+
@drop="onContentDrop"
|
|
102
|
+
>
|
|
103
|
+
<slot></slot>
|
|
104
|
+
<!-- If no containers and no default slot content, maybe show a drop zone hint? -->
|
|
105
|
+
<div v-if="containers.length === 0 && !$slots.default" class="empty-hint">
|
|
106
|
+
Empty Grid
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Splitter (if not last) -->
|
|
111
|
+
<div
|
|
112
|
+
v-if="!isLast"
|
|
113
|
+
class="mt-layout-splitter"
|
|
114
|
+
:class="[
|
|
115
|
+
{ locked: splitLock },
|
|
116
|
+
direction === 'horizontal' ? 'splitter-h' : 'splitter-v',
|
|
117
|
+
]"
|
|
118
|
+
@mousedown="onSplitterMouseDown"
|
|
119
|
+
>
|
|
120
|
+
<div class="splitter-handle"></div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
124
|
+
|
|
125
|
+
<script setup lang="ts">
|
|
126
|
+
import {
|
|
127
|
+
ref,
|
|
128
|
+
computed,
|
|
129
|
+
inject,
|
|
130
|
+
provide,
|
|
131
|
+
onMounted,
|
|
132
|
+
onBeforeUnmount,
|
|
133
|
+
reactive,
|
|
134
|
+
watch,
|
|
135
|
+
nextTick,
|
|
136
|
+
} from "vue";
|
|
137
|
+
import LockIcon from "./icons/LockIcon.vue";
|
|
138
|
+
import UnlockIcon from "./icons/UnlockIcon.vue";
|
|
139
|
+
import CloseIcon from "./icons/CloseIcon.vue";
|
|
140
|
+
import MenuIcon from "./icons/MenuIcon.vue";
|
|
141
|
+
import { registerItem, unregisterItem, getItem } from "./registry";
|
|
142
|
+
|
|
143
|
+
const props = defineProps({
|
|
144
|
+
title: String,
|
|
145
|
+
collapsible: {
|
|
146
|
+
type: Boolean,
|
|
147
|
+
default: true,
|
|
148
|
+
},
|
|
149
|
+
splitLock: {
|
|
150
|
+
type: Boolean,
|
|
151
|
+
default: false,
|
|
152
|
+
},
|
|
153
|
+
minSize: {
|
|
154
|
+
type: [Number, String],
|
|
155
|
+
default: 50,
|
|
156
|
+
},
|
|
157
|
+
initSize: {
|
|
158
|
+
type: [Number, String],
|
|
159
|
+
default: undefined,
|
|
160
|
+
},
|
|
161
|
+
hideHeader: {
|
|
162
|
+
type: Boolean,
|
|
163
|
+
default: false,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const isCollapsed = ref(false);
|
|
168
|
+
const gridLock = ref(false);
|
|
169
|
+
const itemRef = ref<HTMLElement | null>(null);
|
|
170
|
+
const itemSize = ref<string | undefined>(undefined);
|
|
171
|
+
const tabsRef = ref<HTMLElement | null>(null);
|
|
172
|
+
const overflowBtnRef = ref<HTMLElement | null>(null);
|
|
173
|
+
const showOverflowBtn = ref(false);
|
|
174
|
+
const showOverflowMenu = ref(false);
|
|
175
|
+
const overflowMenuStyle = reactive({
|
|
176
|
+
top: "0px",
|
|
177
|
+
left: "0px",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Parent Layout Context
|
|
181
|
+
const layout = inject<any>("mt-layout");
|
|
182
|
+
const itemId = `item-${Math.random().toString(36).substr(2, 9)}`;
|
|
183
|
+
const contentId = `content-${itemId}`;
|
|
184
|
+
|
|
185
|
+
// Check if last item
|
|
186
|
+
const isLast = computed(() => layout?.isLastItem(itemId));
|
|
187
|
+
const direction = computed(() => layout?.direction.value || "horizontal");
|
|
188
|
+
|
|
189
|
+
// Container Management
|
|
190
|
+
const containers = reactive<any[]>([]);
|
|
191
|
+
const activeContainerId = ref<string | null>(null);
|
|
192
|
+
|
|
193
|
+
const registerContainer = (c: any) => {
|
|
194
|
+
containers.push(c);
|
|
195
|
+
if (containers.length === 1) {
|
|
196
|
+
activateContainer(c.id);
|
|
197
|
+
} else if (containers.length > 1) {
|
|
198
|
+
// If adding a new container and we already have some, switch to it?
|
|
199
|
+
// Usually yes.
|
|
200
|
+
activateContainer(c.id);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const unregisterContainer = (id: string) => {
|
|
205
|
+
const idx = containers.findIndex((c) => c.id === id);
|
|
206
|
+
if (idx > -1) {
|
|
207
|
+
containers.splice(idx, 1);
|
|
208
|
+
if (activeContainerId.value === id) {
|
|
209
|
+
activeContainerId.value = containers.length > 0 ? containers[0].id : null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const releaseContainer = (id: string) => {
|
|
215
|
+
const idx = containers.findIndex((c) => c.id === id);
|
|
216
|
+
if (idx > -1) {
|
|
217
|
+
const [c] = containers.splice(idx, 1);
|
|
218
|
+
if (activeContainerId.value === id) {
|
|
219
|
+
activeContainerId.value = containers.length > 0 ? containers[0].id : null;
|
|
220
|
+
}
|
|
221
|
+
return c;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const updateContainerTitle = (id: string, title: string) => {
|
|
227
|
+
const c = containers.find((c) => c.id === id);
|
|
228
|
+
if (c) c.title = title;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const activateContainer = (id: string) => {
|
|
232
|
+
activeContainerId.value = id;
|
|
233
|
+
containers.forEach((c) => {
|
|
234
|
+
c.setActive(c.id === id);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const closeContainer = (id: string) => {
|
|
239
|
+
if (gridLock.value) return;
|
|
240
|
+
// For closing, we just unregister.
|
|
241
|
+
// The MtContainer component will likely be unmounted by Vue if it was in a v-for?
|
|
242
|
+
// No, if it's static in slot, it won't be unmounted.
|
|
243
|
+
// We need to tell the container to hide or destroy itself?
|
|
244
|
+
// But we can't destroy the user's component.
|
|
245
|
+
// We just remove it from the tabs. The content will disappear because `isActive` becomes false.
|
|
246
|
+
unregisterContainer(id);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
provide("mt-layout-item", {
|
|
250
|
+
itemId,
|
|
251
|
+
contentId,
|
|
252
|
+
registerContainer,
|
|
253
|
+
unregisterContainer,
|
|
254
|
+
updateContainerTitle,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const checkOverflow = () => {
|
|
258
|
+
if (tabsRef.value) {
|
|
259
|
+
const el = tabsRef.value;
|
|
260
|
+
showOverflowBtn.value = el.scrollWidth > el.clientWidth;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const toggleOverflowMenu = () => {
|
|
265
|
+
showOverflowMenu.value = !showOverflowMenu.value;
|
|
266
|
+
if (showOverflowMenu.value && overflowBtnRef.value) {
|
|
267
|
+
nextTick(() => {
|
|
268
|
+
const btnRect = overflowBtnRef.value!.getBoundingClientRect();
|
|
269
|
+
const menuWidth = 200; // Min width of menu
|
|
270
|
+
const windowWidth = window.innerWidth;
|
|
271
|
+
|
|
272
|
+
overflowMenuStyle.top = `${btnRect.bottom + 4}px`;
|
|
273
|
+
|
|
274
|
+
// Default: Align right edge of menu with right edge of button
|
|
275
|
+
let left = btnRect.right - menuWidth;
|
|
276
|
+
|
|
277
|
+
// Check if it overflows left edge of screen
|
|
278
|
+
if (left < 0) {
|
|
279
|
+
// Align left edge of menu with left edge of button
|
|
280
|
+
left = btnRect.left;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if it overflows right edge of screen (shouldn't happen with default right alignment unless button is very far left, but good to check)
|
|
284
|
+
if (left + menuWidth > windowWidth) {
|
|
285
|
+
// Force it to fit within window
|
|
286
|
+
left = windowWidth - menuWidth - 4;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
overflowMenuStyle.left = `${left}px`;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const closeOverflowMenu = () => {
|
|
295
|
+
showOverflowMenu.value = false;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const selectFromMenu = (id: string) => {
|
|
299
|
+
// Move to first position
|
|
300
|
+
const idx = containers.findIndex((c) => c.id === id);
|
|
301
|
+
if (idx > 0) {
|
|
302
|
+
const item = containers.splice(idx, 1)[0];
|
|
303
|
+
containers.unshift(item);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
activateContainer(id);
|
|
307
|
+
closeOverflowMenu();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
311
|
+
|
|
312
|
+
// Listen for outside clicks to close menu
|
|
313
|
+
onMounted(() => {
|
|
314
|
+
document.addEventListener("click", closeOverflowMenu);
|
|
315
|
+
// Initial check if tabsRef exists (it might if containers has items initially)
|
|
316
|
+
if (tabsRef.value) {
|
|
317
|
+
resizeObserver = new ResizeObserver(() => {
|
|
318
|
+
checkOverflow();
|
|
319
|
+
});
|
|
320
|
+
resizeObserver.observe(tabsRef.value);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
watch(tabsRef, (newVal) => {
|
|
325
|
+
if (newVal) {
|
|
326
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
327
|
+
resizeObserver = new ResizeObserver(() => {
|
|
328
|
+
checkOverflow();
|
|
329
|
+
});
|
|
330
|
+
resizeObserver.observe(newVal);
|
|
331
|
+
checkOverflow();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
onBeforeUnmount(() => {
|
|
336
|
+
document.removeEventListener("click", closeOverflowMenu);
|
|
337
|
+
if (resizeObserver) {
|
|
338
|
+
resizeObserver.disconnect();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Watch for changes that might affect overflow
|
|
343
|
+
watch(
|
|
344
|
+
() => containers.length,
|
|
345
|
+
() => {
|
|
346
|
+
nextTick(checkOverflow);
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// itemSize watcher is handled by ResizeObserver now
|
|
351
|
+
|
|
352
|
+
// Resizing Logic
|
|
353
|
+
const onSplitterMouseDown = (e: MouseEvent) => {
|
|
354
|
+
if (props.splitLock || gridLock.value) return;
|
|
355
|
+
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
const startX = e.clientX;
|
|
358
|
+
const startY = e.clientY;
|
|
359
|
+
|
|
360
|
+
// Get initial size of current item
|
|
361
|
+
const rect = itemRef.value?.getBoundingClientRect();
|
|
362
|
+
if (!rect) return;
|
|
363
|
+
|
|
364
|
+
// Get computed style to check for border width
|
|
365
|
+
let borderSize = 0;
|
|
366
|
+
if (itemRef.value) {
|
|
367
|
+
const computedStyle = window.getComputedStyle(itemRef.value);
|
|
368
|
+
if (layout.direction.value === "horizontal") {
|
|
369
|
+
borderSize =
|
|
370
|
+
parseFloat(computedStyle.borderLeftWidth) +
|
|
371
|
+
parseFloat(computedStyle.borderRightWidth);
|
|
372
|
+
} else {
|
|
373
|
+
borderSize =
|
|
374
|
+
parseFloat(computedStyle.borderTopWidth) +
|
|
375
|
+
parseFloat(computedStyle.borderBottomWidth);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Get initial size without border
|
|
380
|
+
const startSize =
|
|
381
|
+
(layout.direction.value === "horizontal" ? rect.width : rect.height) -
|
|
382
|
+
borderSize;
|
|
383
|
+
|
|
384
|
+
// Determine resizing mode:
|
|
385
|
+
// Mode 1 (current item has fixed and next item is flex (no itemSize) or no next item): Resize current item (if it has fixed width or no next item).
|
|
386
|
+
// Mode 2 (current item has fixed and next item has fixed): resize current item and next item inverted.
|
|
387
|
+
// Mode 3 (current item is flex (no itemSize) and next item has fixed): resize next item inverted.
|
|
388
|
+
|
|
389
|
+
let targetItem: any = null;
|
|
390
|
+
let resizeMode: "current" | "next" | "both" = "current";
|
|
391
|
+
let nextItemStartSize = 0;
|
|
392
|
+
let nextItem: any = null;
|
|
393
|
+
|
|
394
|
+
if (layout && layout.items) {
|
|
395
|
+
const index = layout.items.findIndex((i: any) => i.id === itemId);
|
|
396
|
+
if (index > -1 && index < layout.items.length - 1) {
|
|
397
|
+
nextItem = layout.items[index + 1];
|
|
398
|
+
targetItem = nextItem;
|
|
399
|
+
|
|
400
|
+
// Check if next item has fixed size (explicitly set via setSize)
|
|
401
|
+
const nextItemHasFixedSize = nextItem.getSize && !!nextItem.getSize();
|
|
402
|
+
|
|
403
|
+
// Get next item's current size for calculation if needed
|
|
404
|
+
if (nextItemHasFixedSize) {
|
|
405
|
+
const nextSizeStr = nextItem.getSize();
|
|
406
|
+
if (typeof nextSizeStr === "string" && nextSizeStr.endsWith("px")) {
|
|
407
|
+
nextItemStartSize = parseInt(nextSizeStr);
|
|
408
|
+
} else {
|
|
409
|
+
// If next item has size but not px? Fallback to DOM size
|
|
410
|
+
const nextEl = itemRef.value?.nextElementSibling;
|
|
411
|
+
if (nextEl) {
|
|
412
|
+
const rect = nextEl.getBoundingClientRect();
|
|
413
|
+
// Get border size for next item
|
|
414
|
+
let nextBorderSize = 0;
|
|
415
|
+
const nextComputedStyle = window.getComputedStyle(nextEl);
|
|
416
|
+
if (layout.direction.value === "horizontal") {
|
|
417
|
+
nextBorderSize =
|
|
418
|
+
parseFloat(nextComputedStyle.borderLeftWidth) +
|
|
419
|
+
parseFloat(nextComputedStyle.borderRightWidth);
|
|
420
|
+
} else {
|
|
421
|
+
nextBorderSize =
|
|
422
|
+
parseFloat(nextComputedStyle.borderTopWidth) +
|
|
423
|
+
parseFloat(nextComputedStyle.borderBottomWidth);
|
|
424
|
+
}
|
|
425
|
+
// IMPORTANT: Check direction for correct dimension and subtract border size
|
|
426
|
+
nextItemStartSize =
|
|
427
|
+
(layout.direction.value === "horizontal"
|
|
428
|
+
? rect.width
|
|
429
|
+
: rect.height) - nextBorderSize;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
// Next item is flex (no explicit size set)
|
|
434
|
+
nextItemStartSize = 0;
|
|
435
|
+
|
|
436
|
+
// For Mode 3, we still need the current DOM size to convert to fixed
|
|
437
|
+
if (!itemSize.value) {
|
|
438
|
+
const nextEl = itemRef.value?.nextElementSibling;
|
|
439
|
+
if (nextEl) {
|
|
440
|
+
const rect = nextEl.getBoundingClientRect();
|
|
441
|
+
// Get border size for next item
|
|
442
|
+
let nextBorderSize = 0;
|
|
443
|
+
const nextComputedStyle = window.getComputedStyle(nextEl);
|
|
444
|
+
if (layout.direction.value === "horizontal") {
|
|
445
|
+
nextBorderSize =
|
|
446
|
+
parseFloat(nextComputedStyle.borderLeftWidth) +
|
|
447
|
+
parseFloat(nextComputedStyle.borderRightWidth);
|
|
448
|
+
} else {
|
|
449
|
+
nextBorderSize =
|
|
450
|
+
parseFloat(nextComputedStyle.borderTopWidth) +
|
|
451
|
+
parseFloat(nextComputedStyle.borderBottomWidth);
|
|
452
|
+
}
|
|
453
|
+
// IMPORTANT: Check direction for correct dimension and subtract border size
|
|
454
|
+
nextItemStartSize =
|
|
455
|
+
(layout.direction.value === "horizontal"
|
|
456
|
+
? rect.width
|
|
457
|
+
: rect.height) - nextBorderSize;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Determine resize mode based on current and next item's size status
|
|
463
|
+
if (itemSize.value) {
|
|
464
|
+
// Current item has fixed size
|
|
465
|
+
if (nextItemHasFixedSize) {
|
|
466
|
+
// Next item also has fixed size - Mode 2
|
|
467
|
+
resizeMode = "both";
|
|
468
|
+
} else {
|
|
469
|
+
// Next item is flex - Mode 1
|
|
470
|
+
resizeMode = "current";
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
// Current item is flex - Mode 3
|
|
474
|
+
resizeMode = "next";
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// No next item - Mode 1
|
|
478
|
+
resizeMode = "current";
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
// No layout or items - Mode 1
|
|
482
|
+
resizeMode = "current";
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const onMouseMove = (moveEvent: MouseEvent) => {
|
|
486
|
+
const deltaX = moveEvent.clientX - startX;
|
|
487
|
+
const deltaY = moveEvent.clientY - startY;
|
|
488
|
+
|
|
489
|
+
const delta = layout.direction.value === "horizontal" ? deltaX : deltaY;
|
|
490
|
+
|
|
491
|
+
if (resizeMode === "next" && targetItem) {
|
|
492
|
+
// Mode 3: Current item is flex, next item has fixed size
|
|
493
|
+
// Dragging right (positive delta) -> Next item should shrink (negative change)
|
|
494
|
+
// Dragging down (positive delta) -> Next item should shrink (negative change)
|
|
495
|
+
const newSize = Math.max(
|
|
496
|
+
Number(props.minSize),
|
|
497
|
+
nextItemStartSize - delta
|
|
498
|
+
);
|
|
499
|
+
targetItem.setSize(`${newSize}px`);
|
|
500
|
+
} else if (resizeMode === "both" && targetItem) {
|
|
501
|
+
// Mode 2: Both current and next items have fixed sizes
|
|
502
|
+
// Resize current item and next item inverted
|
|
503
|
+
const newCurrentSize = Math.max(Number(props.minSize), startSize + delta);
|
|
504
|
+
const newNextSize = Math.max(
|
|
505
|
+
Number(props.minSize),
|
|
506
|
+
nextItemStartSize - delta
|
|
507
|
+
);
|
|
508
|
+
itemSize.value = `${newCurrentSize}px`;
|
|
509
|
+
targetItem.setSize(`${newNextSize}px`);
|
|
510
|
+
} else {
|
|
511
|
+
// Mode 1: Current item has fixed size, next item is flex or no next item
|
|
512
|
+
// Standard behavior: Resize current item
|
|
513
|
+
// Dragging right/down (positive delta) -> Current item should grow
|
|
514
|
+
const newSize = Math.max(Number(props.minSize), startSize + delta);
|
|
515
|
+
itemSize.value = `${newSize}px`;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const onMouseUp = () => {
|
|
520
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
521
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
525
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Drag and Drop Logic (Tabs)
|
|
529
|
+
const onTabDragStart = (e: DragEvent, id: string) => {
|
|
530
|
+
if (gridLock.value || !e.dataTransfer) return;
|
|
531
|
+
e.dataTransfer.setData("text/mt-container-id", id);
|
|
532
|
+
e.dataTransfer.setData("text/mt-source-item-id", itemId);
|
|
533
|
+
e.dataTransfer.effectAllowed = "move";
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const onTabDrop = (e: DragEvent, targetId: string) => {
|
|
537
|
+
if (gridLock.value) return;
|
|
538
|
+
|
|
539
|
+
const sourceId = e.dataTransfer?.getData("text/mt-container-id");
|
|
540
|
+
const sourceItemId = e.dataTransfer?.getData("text/mt-source-item-id");
|
|
541
|
+
|
|
542
|
+
if (!sourceId || !sourceItemId) return;
|
|
543
|
+
|
|
544
|
+
if (sourceItemId === itemId) {
|
|
545
|
+
// Reorder within same item
|
|
546
|
+
const oldIndex = containers.findIndex((c) => c.id === sourceId);
|
|
547
|
+
const newIndex = containers.findIndex((c) => c.id === targetId);
|
|
548
|
+
|
|
549
|
+
if (oldIndex > -1 && newIndex > -1 && oldIndex !== newIndex) {
|
|
550
|
+
// Move element
|
|
551
|
+
const item = containers.splice(oldIndex, 1)[0];
|
|
552
|
+
containers.splice(newIndex, 0, item);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// If different item, it's handled by onContentDrop or we could handle it here too.
|
|
556
|
+
// But dropping on a tab usually implies inserting AT that position, whereas dropping on content implies appending.
|
|
557
|
+
// For simplicity, we only handle local reorder here.
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const onContentDrop = (e: DragEvent) => {
|
|
561
|
+
if (gridLock.value) return;
|
|
562
|
+
|
|
563
|
+
const containerId = e.dataTransfer?.getData("text/mt-container-id");
|
|
564
|
+
const sourceItemId = e.dataTransfer?.getData("text/mt-source-item-id");
|
|
565
|
+
|
|
566
|
+
if (containerId && sourceItemId && sourceItemId !== itemId) {
|
|
567
|
+
const sourceItem = getItem(sourceItemId);
|
|
568
|
+
if (sourceItem) {
|
|
569
|
+
const container = sourceItem.releaseContainer(containerId);
|
|
570
|
+
if (container) {
|
|
571
|
+
registerContainer(container);
|
|
572
|
+
container.setOwner(contentId, itemId);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const toggleCollapse = () => {
|
|
579
|
+
if (!gridLock.value) {
|
|
580
|
+
// Assuming lock also prevents collapse? Requirement doesn't say, but usually yes.
|
|
581
|
+
// Requirement: "layout grid support hide/expand, each layout grid also needs switch property also control whether can hide/expand"
|
|
582
|
+
// That switch property is `collapsible`.
|
|
583
|
+
isCollapsed.value = !isCollapsed.value;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const toggleGridLock = () => {
|
|
588
|
+
gridLock.value = !gridLock.value;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const itemStyle = computed(() => {
|
|
592
|
+
const style: any = {};
|
|
593
|
+
|
|
594
|
+
// If collapsed, we force inline styles to null/empty so CSS classes can take over.
|
|
595
|
+
// Crucially, we must clear width/height and flex.
|
|
596
|
+
if (isCollapsed.value) {
|
|
597
|
+
if (layout.direction.value === "horizontal") {
|
|
598
|
+
style.width = null;
|
|
599
|
+
style.height = null;
|
|
600
|
+
} else {
|
|
601
|
+
style.height = null;
|
|
602
|
+
style.width = null;
|
|
603
|
+
}
|
|
604
|
+
style.flex = null;
|
|
605
|
+
return style;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (itemSize.value) {
|
|
609
|
+
if (layout.direction.value === "horizontal") {
|
|
610
|
+
style.width = itemSize.value;
|
|
611
|
+
style.flex = "none";
|
|
612
|
+
} else {
|
|
613
|
+
style.height = itemSize.value;
|
|
614
|
+
style.flex = "none";
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
style.flex = "1";
|
|
618
|
+
}
|
|
619
|
+
return style;
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
onMounted(() => {
|
|
623
|
+
if (props.initSize) {
|
|
624
|
+
itemSize.value =
|
|
625
|
+
typeof props.initSize === "number"
|
|
626
|
+
? `${props.initSize}px`
|
|
627
|
+
: props.initSize;
|
|
628
|
+
}
|
|
629
|
+
layout?.addItem({
|
|
630
|
+
id: itemId,
|
|
631
|
+
getSize: () => itemSize.value,
|
|
632
|
+
setSize: (v: string | undefined) => (itemSize.value = v),
|
|
633
|
+
});
|
|
634
|
+
registerItem(itemId, {
|
|
635
|
+
releaseContainer,
|
|
636
|
+
// other methods if needed
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
onBeforeUnmount(() => {
|
|
641
|
+
layout?.removeItem(itemId);
|
|
642
|
+
unregisterItem(itemId);
|
|
643
|
+
});
|
|
644
|
+
</script>
|
|
645
|
+
|
|
646
|
+
<style lang="scss" scoped>
|
|
647
|
+
.mt-layout-item {
|
|
648
|
+
position: relative;
|
|
649
|
+
display: flex;
|
|
650
|
+
flex-direction: column;
|
|
651
|
+
min-width: 0;
|
|
652
|
+
min-height: 0;
|
|
653
|
+
background: #fff;
|
|
654
|
+
border: 1px solid #e0e0e0;
|
|
655
|
+
transition: flex 0.1s; // smooth resize? maybe no
|
|
656
|
+
|
|
657
|
+
&.is-collapsed {
|
|
658
|
+
flex: 0 0 auto !important;
|
|
659
|
+
|
|
660
|
+
// Vertical collapse (default)
|
|
661
|
+
height: 32px;
|
|
662
|
+
overflow: hidden;
|
|
663
|
+
|
|
664
|
+
.mt-item-content {
|
|
665
|
+
display: none;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Horizontal collapse overrides
|
|
670
|
+
.mt-layout-horizontal > & {
|
|
671
|
+
&.is-collapsed {
|
|
672
|
+
width: 32px; // Collapsed width
|
|
673
|
+
height: auto; // Reset height
|
|
674
|
+
|
|
675
|
+
.mt-item-header {
|
|
676
|
+
flex-direction: column;
|
|
677
|
+
width: 32px;
|
|
678
|
+
height: 100%;
|
|
679
|
+
padding: 4px 0;
|
|
680
|
+
|
|
681
|
+
.mt-header-left {
|
|
682
|
+
flex-direction: column;
|
|
683
|
+
width: 100%;
|
|
684
|
+
|
|
685
|
+
.mt-title {
|
|
686
|
+
padding: 4px 0;
|
|
687
|
+
white-space: nowrap;
|
|
688
|
+
writing-mode: vertical-rl;
|
|
689
|
+
text-orientation: mixed;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.mt-tabs-wrapper {
|
|
693
|
+
display: none; // Hide tabs when horizontally collapsed
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.mt-header-right {
|
|
698
|
+
flex-direction: column;
|
|
699
|
+
width: 100%;
|
|
700
|
+
margin-top: auto;
|
|
701
|
+
|
|
702
|
+
.collapse-btn {
|
|
703
|
+
margin: 0 0 4px;
|
|
704
|
+
|
|
705
|
+
.arrow {
|
|
706
|
+
// Adjust rotation for vertical bar
|
|
707
|
+
transform: rotate(-90deg);
|
|
708
|
+
|
|
709
|
+
&.rotated {
|
|
710
|
+
transform: rotate(90deg);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.mt-item-header {
|
|
720
|
+
display: flex;
|
|
721
|
+
align-items: center;
|
|
722
|
+
justify-content: space-between;
|
|
723
|
+
height: 32px;
|
|
724
|
+
padding: 0 4px;
|
|
725
|
+
user-select: none;
|
|
726
|
+
background: #f0f0f0;
|
|
727
|
+
border-bottom: 1px solid #e0e0e0;
|
|
728
|
+
|
|
729
|
+
.mt-header-left {
|
|
730
|
+
display: flex;
|
|
731
|
+
flex: 1;
|
|
732
|
+
align-items: center;
|
|
733
|
+
overflow: hidden;
|
|
734
|
+
|
|
735
|
+
.mt-title {
|
|
736
|
+
padding-left: 4px;
|
|
737
|
+
font-size: 12px;
|
|
738
|
+
font-weight: 600;
|
|
739
|
+
color: #444;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.mt-tabs-wrapper {
|
|
743
|
+
position: relative;
|
|
744
|
+
display: flex;
|
|
745
|
+
flex: 1;
|
|
746
|
+
align-items: flex-end;
|
|
747
|
+
height: 100%;
|
|
748
|
+
padding-top: 4px;
|
|
749
|
+
overflow: hidden;
|
|
750
|
+
|
|
751
|
+
.mt-tabs {
|
|
752
|
+
display: flex;
|
|
753
|
+
flex: 1;
|
|
754
|
+
align-items: flex-end;
|
|
755
|
+
height: 100%;
|
|
756
|
+
overflow: hidden; /* Hide scrollbar, use overflow menu */
|
|
757
|
+
|
|
758
|
+
.mt-tab {
|
|
759
|
+
display: flex;
|
|
760
|
+
flex-shrink: 0; /* Prevent tabs from shrinking too small */
|
|
761
|
+
align-items: center;
|
|
762
|
+
min-width: 80px;
|
|
763
|
+
max-width: 160px;
|
|
764
|
+
height: 28px;
|
|
765
|
+
padding: 0 10px;
|
|
766
|
+
margin-right: 1px;
|
|
767
|
+
font-size: 12px;
|
|
768
|
+
color: #666;
|
|
769
|
+
cursor: pointer;
|
|
770
|
+
background: #e6e6e6;
|
|
771
|
+
border: 1px solid transparent;
|
|
772
|
+
border-bottom: none;
|
|
773
|
+
border-radius: 4px 4px 0 0;
|
|
774
|
+
|
|
775
|
+
&.active {
|
|
776
|
+
position: relative;
|
|
777
|
+
z-index: 1;
|
|
778
|
+
font-weight: 500;
|
|
779
|
+
color: #333;
|
|
780
|
+
background: #fff;
|
|
781
|
+
border-color: #e0e0e0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
&:hover:not(.active) {
|
|
785
|
+
color: #444;
|
|
786
|
+
background: #eaeaea;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.mt-tab-text {
|
|
790
|
+
flex: 1;
|
|
791
|
+
overflow: hidden;
|
|
792
|
+
text-overflow: ellipsis;
|
|
793
|
+
white-space: nowrap;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.mt-tab-close {
|
|
797
|
+
display: flex;
|
|
798
|
+
align-items: center;
|
|
799
|
+
justify-content: center;
|
|
800
|
+
padding: 2px;
|
|
801
|
+
margin-left: 6px;
|
|
802
|
+
color: #666;
|
|
803
|
+
cursor: pointer;
|
|
804
|
+
background: none;
|
|
805
|
+
border: none;
|
|
806
|
+
border-radius: 4px;
|
|
807
|
+
opacity: 0;
|
|
808
|
+
|
|
809
|
+
svg {
|
|
810
|
+
width: 12px;
|
|
811
|
+
height: 12px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
&:hover {
|
|
815
|
+
color: #333;
|
|
816
|
+
background-color: rgb(0 0 0 / 10%);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
&:hover .mt-tab-close,
|
|
821
|
+
&.active .mt-tab-close {
|
|
822
|
+
opacity: 1;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.mt-tabs-overflow {
|
|
828
|
+
position: absolute;
|
|
829
|
+
right: 0;
|
|
830
|
+
bottom: 0;
|
|
831
|
+
z-index: 2;
|
|
832
|
+
display: flex;
|
|
833
|
+
align-items: center;
|
|
834
|
+
height: 28px;
|
|
835
|
+
padding: 0 4px;
|
|
836
|
+
background: #f0f0f0; /* Match header bg */
|
|
837
|
+
border-left: 1px solid #e0e0e0;
|
|
838
|
+
|
|
839
|
+
.overflow-btn {
|
|
840
|
+
display: flex;
|
|
841
|
+
align-items: center;
|
|
842
|
+
justify-content: center;
|
|
843
|
+
width: 20px;
|
|
844
|
+
height: 20px;
|
|
845
|
+
padding: 4px;
|
|
846
|
+
color: #666;
|
|
847
|
+
cursor: pointer;
|
|
848
|
+
background-color: transparent !important;
|
|
849
|
+
border: none;
|
|
850
|
+
border-radius: 4px;
|
|
851
|
+
|
|
852
|
+
svg {
|
|
853
|
+
width: 14px;
|
|
854
|
+
height: 14px;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
&:hover {
|
|
858
|
+
color: #333;
|
|
859
|
+
background-color: rgb(0 0 0 / 5%) !important;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.mt-header-right {
|
|
867
|
+
display: flex;
|
|
868
|
+
align-items: center;
|
|
869
|
+
|
|
870
|
+
.mt-icon-btn {
|
|
871
|
+
display: flex;
|
|
872
|
+
align-items: center;
|
|
873
|
+
justify-content: center;
|
|
874
|
+
padding: 4px;
|
|
875
|
+
color: #666;
|
|
876
|
+
cursor: pointer;
|
|
877
|
+
background: none;
|
|
878
|
+
background-color: transparent !important; /* Force transparent */
|
|
879
|
+
border: none;
|
|
880
|
+
border-radius: 4px;
|
|
881
|
+
|
|
882
|
+
svg {
|
|
883
|
+
width: 14px;
|
|
884
|
+
height: 14px;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
&:hover {
|
|
888
|
+
color: #333;
|
|
889
|
+
background-color: rgb(0 0 0 / 5%) !important;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.collapse-btn {
|
|
894
|
+
width: 20px;
|
|
895
|
+
height: 20px;
|
|
896
|
+
margin-right: 4px;
|
|
897
|
+
|
|
898
|
+
.arrow {
|
|
899
|
+
display: inline-block;
|
|
900
|
+
font-size: 10px;
|
|
901
|
+
color: #666;
|
|
902
|
+
transition: transform 0.2s;
|
|
903
|
+
|
|
904
|
+
&.rotated {
|
|
905
|
+
transform: rotate(-90deg);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
&:hover .arrow {
|
|
910
|
+
color: #333;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.mt-item-content {
|
|
917
|
+
position: relative;
|
|
918
|
+
flex: 1;
|
|
919
|
+
padding: 8px;
|
|
920
|
+
overflow: auto;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* Custom scrollbar styles */
|
|
924
|
+
.mt-item-content::-webkit-scrollbar {
|
|
925
|
+
width: 8px;
|
|
926
|
+
height: 8px;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.mt-item-content::-webkit-scrollbar-track {
|
|
930
|
+
background: #f1f1f1;
|
|
931
|
+
border-radius: 4px;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.mt-item-content::-webkit-scrollbar-thumb {
|
|
935
|
+
background: #c1c1c1;
|
|
936
|
+
border-radius: 4px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.mt-item-content::-webkit-scrollbar-thumb:hover {
|
|
940
|
+
background: #a1a1a1;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* When containers are present, let the container handle scrolling and remove padding */
|
|
944
|
+
&.has-containers .mt-item-content {
|
|
945
|
+
padding: 0;
|
|
946
|
+
overflow: hidden;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.mt-layout-splitter {
|
|
950
|
+
position: absolute;
|
|
951
|
+
z-index: 10;
|
|
952
|
+
background-color: transparent;
|
|
953
|
+
transition: background-color 0.2s;
|
|
954
|
+
|
|
955
|
+
&.splitter-h {
|
|
956
|
+
top: 0;
|
|
957
|
+
right: -2px; // overlap a bit
|
|
958
|
+
bottom: 0;
|
|
959
|
+
width: 4px;
|
|
960
|
+
cursor: col-resize;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
&.splitter-v {
|
|
964
|
+
right: 0;
|
|
965
|
+
bottom: -2px; // overlap a bit
|
|
966
|
+
left: 0;
|
|
967
|
+
height: 4px;
|
|
968
|
+
cursor: row-resize;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
&:hover {
|
|
972
|
+
background-color: #1890ff;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
&.locked {
|
|
976
|
+
cursor: not-allowed;
|
|
977
|
+
|
|
978
|
+
&:hover {
|
|
979
|
+
background-color: #ff4d4f;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.mt-overflow-menu {
|
|
986
|
+
position: fixed;
|
|
987
|
+
z-index: 99999; /* Ensure highest z-index */
|
|
988
|
+
min-width: 200px;
|
|
989
|
+
max-height: 300px;
|
|
990
|
+
padding: 4px 0;
|
|
991
|
+
overflow-y: auto;
|
|
992
|
+
background: #fff;
|
|
993
|
+
border: 1px solid #e0e0e0;
|
|
994
|
+
border-radius: 4px;
|
|
995
|
+
box-shadow: 0 2px 12px rgb(0 0 0 / 15%);
|
|
996
|
+
|
|
997
|
+
.mt-menu-item {
|
|
998
|
+
display: flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
padding: 8px 12px;
|
|
1001
|
+
font-size: 12px;
|
|
1002
|
+
color: #333;
|
|
1003
|
+
cursor: pointer;
|
|
1004
|
+
|
|
1005
|
+
&:hover {
|
|
1006
|
+
background: #f5f5f5;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
&.active {
|
|
1010
|
+
color: #1890ff;
|
|
1011
|
+
background: #e6f7ff;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.mt-menu-text {
|
|
1015
|
+
flex: 1;
|
|
1016
|
+
overflow: hidden;
|
|
1017
|
+
text-overflow: ellipsis;
|
|
1018
|
+
white-space: nowrap;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.mt-menu-close {
|
|
1022
|
+
display: flex;
|
|
1023
|
+
align-items: center;
|
|
1024
|
+
justify-content: center;
|
|
1025
|
+
padding: 2px;
|
|
1026
|
+
margin-left: 8px;
|
|
1027
|
+
color: #999;
|
|
1028
|
+
cursor: pointer;
|
|
1029
|
+
background: none;
|
|
1030
|
+
border: none;
|
|
1031
|
+
border-radius: 4px;
|
|
1032
|
+
|
|
1033
|
+
svg {
|
|
1034
|
+
width: 12px;
|
|
1035
|
+
height: 12px;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
&:hover {
|
|
1039
|
+
color: #666;
|
|
1040
|
+
background-color: rgb(0 0 0 / 10%);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
:deep(.mt-item-header *) {
|
|
1047
|
+
outline: none;
|
|
1048
|
+
}
|
|
1049
|
+
</style>
|