@m3ui-vue/m3ui-vue 0.4.7 → 0.5.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/dist/{MMenuItem-COdgD4m7.js → MMenuItem-Dw2ARZic.js} +30 -24
- package/dist/{MMenuItem-COdgD4m7.js.map → MMenuItem-Dw2ARZic.js.map} +1 -1
- package/dist/components/MAutocomplete.vue.d.ts +2 -1
- package/dist/components/MColorPicker.vue.d.ts +2 -1
- package/dist/components/MContextMenu.vue.d.ts +7 -21
- package/dist/components/MDatePicker.vue.d.ts +2 -1
- package/dist/components/MDateRangePicker.vue.d.ts +2 -1
- package/dist/components/MMaskField.vue.d.ts +2 -1
- package/dist/components/MMenuDivider.vue.d.ts +3 -0
- package/dist/components/MMenuItem.vue.d.ts +1 -0
- package/dist/components/MMultiAutocomplete.vue.d.ts +2 -1
- package/dist/components/MMultiSelect.vue.d.ts +2 -1
- package/dist/components/MNumberField.vue.d.ts +2 -1
- package/dist/components/MSelect.vue.d.ts +2 -1
- package/dist/components/MTagInput.vue.d.ts +2 -1
- package/dist/components/MTextField.vue.d.ts +2 -1
- package/dist/components/MTimePicker.vue.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +819 -884
- package/dist/m3ui.js.map +1 -1
- package/dist/rich-text-editor.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/MAbsolute.vue +3 -2
- package/src/components/MAutocomplete.vue +3 -2
- package/src/components/MCircleProgressBar.vue +7 -5
- package/src/components/MColorPicker.vue +4 -2
- package/src/components/MColorPickerModal.vue +1 -0
- package/src/components/MContextMenu.vue +57 -38
- package/src/components/MDatePicker.vue +3 -2
- package/src/components/MDateRangePicker.vue +3 -2
- package/src/components/MMaskField.vue +3 -2
- package/src/components/MMenuDivider.vue +3 -0
- package/src/components/MMenuItem.vue +2 -0
- package/src/components/MMultiAutocomplete.vue +3 -2
- package/src/components/MMultiSelect.vue +3 -2
- package/src/components/MNotificationHost.vue +19 -5
- package/src/components/MNumberField.vue +3 -2
- package/src/components/MSelect.vue +3 -2
- package/src/components/MSlider.vue +20 -16
- package/src/components/MSnackbar.vue +15 -5
- package/src/components/MTabs.vue +1 -1
- package/src/components/MTagInput.vue +3 -2
- package/src/components/MTextField.vue +3 -2
- package/src/components/MTimePicker.vue +3 -2
- package/src/components/MWindow.vue +5 -5
- package/src/index.ts +1 -1
- package/dist/components/_MContextMenuPanel.vue.d.ts +0 -13
- package/src/components/_MContextMenuPanel.vue +0 -137
|
@@ -37,7 +37,7 @@ const props = withDefaults(
|
|
|
37
37
|
const emit = defineEmits<{ 'update:modelValue': [number | [number, number]] }>()
|
|
38
38
|
|
|
39
39
|
const trackEl = ref<HTMLElement>()
|
|
40
|
-
const dragging = ref<false | 'single' |
|
|
40
|
+
const dragging = ref<false | 'single' | 0 | 1>(false)
|
|
41
41
|
const isVertical = computed(() => props.orientation === 'vertical')
|
|
42
42
|
const isRange = computed(() => props.variant === 'range')
|
|
43
43
|
const isCentered = computed(() => props.variant === 'centered')
|
|
@@ -100,11 +100,14 @@ function onPointerDown(e: PointerEvent) {
|
|
|
100
100
|
e.preventDefault()
|
|
101
101
|
const v = valueFromEvent(e)
|
|
102
102
|
if (isRange.value) {
|
|
103
|
-
const distLo = Math.abs(v - val.value.lo)
|
|
104
|
-
const distHi = Math.abs(v - val.value.hi)
|
|
105
|
-
dragging.value = distLo <= distHi ? 'lo' : 'hi'
|
|
106
103
|
const cur = props.modelValue as [number, number]
|
|
107
|
-
|
|
104
|
+
const dist0 = Math.abs(v - cur[0])
|
|
105
|
+
const dist1 = Math.abs(v - cur[1])
|
|
106
|
+
const idx: 0 | 1 = dist0 <= dist1 ? 0 : 1
|
|
107
|
+
dragging.value = idx
|
|
108
|
+
const next: [number, number] = [cur[0], cur[1]]
|
|
109
|
+
next[idx] = v
|
|
110
|
+
emit('update:modelValue', next)
|
|
108
111
|
} else {
|
|
109
112
|
dragging.value = 'single'
|
|
110
113
|
emit('update:modelValue', v)
|
|
@@ -115,11 +118,13 @@ function onPointerDown(e: PointerEvent) {
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
function onPointerMove(e: PointerEvent) {
|
|
118
|
-
if (
|
|
121
|
+
if (dragging.value === false) return
|
|
119
122
|
const v = valueFromEvent(e)
|
|
120
123
|
if (isRange.value) {
|
|
121
124
|
const cur = props.modelValue as [number, number]
|
|
122
|
-
|
|
125
|
+
const next: [number, number] = [cur[0], cur[1]]
|
|
126
|
+
next[dragging.value as 0 | 1] = v
|
|
127
|
+
emit('update:modelValue', next)
|
|
123
128
|
} else {
|
|
124
129
|
emit('update:modelValue', v)
|
|
125
130
|
}
|
|
@@ -169,7 +174,7 @@ const stopPositions = computed(() => {
|
|
|
169
174
|
})
|
|
170
175
|
|
|
171
176
|
const r = computed(() => s.value.radius)
|
|
172
|
-
const nd = computed(() =>
|
|
177
|
+
const nd = computed(() => dragging.value === false)
|
|
173
178
|
const tr = computed(() => nd.value ? '75ms ease' : '0s')
|
|
174
179
|
|
|
175
180
|
function thumbPos(pct: number) {
|
|
@@ -188,11 +193,11 @@ function thumbPos(pct: number) {
|
|
|
188
193
|
if (isVertical.value) {
|
|
189
194
|
base.left = '50%'
|
|
190
195
|
base.bottom = `${pct}%`
|
|
191
|
-
base.transform = `translateX(-50%) translateY(50%) scaleX(${dragging.value ? 1.08 : 1})`
|
|
196
|
+
base.transform = `translateX(-50%) translateY(50%) scaleX(${dragging.value !== false ? 1.08 : 1})`
|
|
192
197
|
} else {
|
|
193
198
|
base.left = `${pct}%`
|
|
194
199
|
base.top = '50%'
|
|
195
|
-
base.transform = `translateX(-50%) translateY(-50%) scaleY(${dragging.value ? 1.08 : 1})`
|
|
200
|
+
base.transform = `translateX(-50%) translateY(-50%) scaleY(${dragging.value !== false ? 1.08 : 1})`
|
|
196
201
|
}
|
|
197
202
|
return base
|
|
198
203
|
}
|
|
@@ -212,15 +217,14 @@ const displayValue = computed(() => {
|
|
|
212
217
|
})
|
|
213
218
|
|
|
214
219
|
const tooltipValue = computed(() => {
|
|
215
|
-
if (isRange.value)
|
|
216
|
-
|
|
217
|
-
return val.value.hi
|
|
218
|
-
}
|
|
220
|
+
if (isRange.value && typeof dragging.value === 'number')
|
|
221
|
+
return (props.modelValue as [number, number])[dragging.value]
|
|
219
222
|
return props.modelValue
|
|
220
223
|
})
|
|
221
224
|
|
|
222
225
|
const tooltipPct = computed(() => {
|
|
223
|
-
if (isRange.value
|
|
226
|
+
if (isRange.value && typeof dragging.value === 'number')
|
|
227
|
+
return toPct((props.modelValue as [number, number])[dragging.value])
|
|
224
228
|
return pctLo.value
|
|
225
229
|
})
|
|
226
230
|
</script>
|
|
@@ -408,7 +412,7 @@ const tooltipPct = computed(() => {
|
|
|
408
412
|
leave-to-class="opacity-0"
|
|
409
413
|
>
|
|
410
414
|
<div
|
|
411
|
-
v-if="showTooltip && dragging"
|
|
415
|
+
v-if="showTooltip && dragging !== false"
|
|
412
416
|
class="pointer-events-none absolute z-10 flex h-7 min-w-7 items-center justify-center rounded-full bg-inverse-surface px-2 text-label-small tabular-nums text-inverse-on-surface"
|
|
413
417
|
:style="tooltipPos(tooltipPct)"
|
|
414
418
|
>
|
|
@@ -94,7 +94,7 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
94
94
|
<Transition name="m3-badge">
|
|
95
95
|
<span
|
|
96
96
|
v-if="t.count >= 2"
|
|
97
|
-
class="absolute -top-1.5 -right-1.5 z-10 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-on-primary shadow-elevation-1"
|
|
97
|
+
class="toast-count-badge absolute -top-1.5 -right-1.5 z-10 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-on-primary shadow-elevation-1"
|
|
98
98
|
>
|
|
99
99
|
<Transition name="m3-count" mode="out-in">
|
|
100
100
|
<span :key="t.count">{{ t.count }}</span>
|
|
@@ -178,7 +178,6 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
178
178
|
transition:
|
|
179
179
|
grid-template-rows 220ms cubic-bezier(0.2, 0, 0, 1),
|
|
180
180
|
padding-bottom 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
181
|
-
overflow: hidden;
|
|
182
181
|
}
|
|
183
182
|
.m3-toast-bot-enter-active > .toast-inner {
|
|
184
183
|
transition:
|
|
@@ -198,7 +197,6 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
198
197
|
transition:
|
|
199
198
|
grid-template-rows 300ms cubic-bezier(0.2, 0, 0, 1),
|
|
200
199
|
padding-bottom 300ms cubic-bezier(0.2, 0, 0, 1);
|
|
201
|
-
overflow: hidden;
|
|
202
200
|
}
|
|
203
201
|
.m3-toast-bot-leave-active > .toast-inner {
|
|
204
202
|
transition:
|
|
@@ -213,13 +211,19 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
213
211
|
opacity: 0;
|
|
214
212
|
transform: scale(0.92);
|
|
215
213
|
}
|
|
214
|
+
.m3-toast-bot-leave-active > .toast-count-badge {
|
|
215
|
+
transition: opacity 150ms ease, transform 150ms ease;
|
|
216
|
+
}
|
|
217
|
+
.m3-toast-bot-leave-to > .toast-count-badge {
|
|
218
|
+
opacity: 0;
|
|
219
|
+
transform: scale(0.5);
|
|
220
|
+
}
|
|
216
221
|
|
|
217
222
|
/* ─── Top toasts ────────────────────────────────────────────────── */
|
|
218
223
|
.m3-toast-top-enter-active {
|
|
219
224
|
transition:
|
|
220
225
|
grid-template-rows 220ms cubic-bezier(0.2, 0, 0, 1),
|
|
221
226
|
padding-bottom 220ms cubic-bezier(0.2, 0, 0, 1);
|
|
222
|
-
overflow: hidden;
|
|
223
227
|
}
|
|
224
228
|
.m3-toast-top-enter-active > .toast-inner {
|
|
225
229
|
transition:
|
|
@@ -239,7 +243,6 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
239
243
|
transition:
|
|
240
244
|
grid-template-rows 300ms cubic-bezier(0.2, 0, 0, 1),
|
|
241
245
|
padding-bottom 300ms cubic-bezier(0.2, 0, 0, 1);
|
|
242
|
-
overflow: hidden;
|
|
243
246
|
}
|
|
244
247
|
.m3-toast-top-leave-active > .toast-inner {
|
|
245
248
|
transition:
|
|
@@ -254,6 +257,13 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
254
257
|
opacity: 0;
|
|
255
258
|
transform: scale(0.92);
|
|
256
259
|
}
|
|
260
|
+
.m3-toast-top-leave-active > .toast-count-badge {
|
|
261
|
+
transition: opacity 150ms ease, transform 150ms ease;
|
|
262
|
+
}
|
|
263
|
+
.m3-toast-top-leave-to > .toast-count-badge {
|
|
264
|
+
opacity: 0;
|
|
265
|
+
transform: scale(0.5);
|
|
266
|
+
}
|
|
257
267
|
|
|
258
268
|
/* badge appear/disappear */
|
|
259
269
|
.m3-badge-enter-active { transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
package/src/components/MTabs.vue
CHANGED
|
@@ -103,7 +103,7 @@ function select(tab: Tab) {
|
|
|
103
103
|
width: `${indicatorWidth}px`,
|
|
104
104
|
transition: stretching
|
|
105
105
|
? 'left 150ms cubic-bezier(0.4, 0, 0.2, 1), width 150ms cubic-bezier(0.4, 0, 0.2, 1)'
|
|
106
|
-
: 'left
|
|
106
|
+
: 'left 180ms cubic-bezier(0.34, 1.4, 0.64, 1), width 180ms cubic-bezier(0.34, 1.4, 0.64, 1)',
|
|
107
107
|
}"
|
|
108
108
|
/>
|
|
109
109
|
</div>
|
|
@@ -11,7 +11,8 @@ const props = withDefaults(
|
|
|
11
11
|
placeholder?: string
|
|
12
12
|
variant?: 'filled' | 'outlined'
|
|
13
13
|
disabled?: boolean
|
|
14
|
-
error?:
|
|
14
|
+
error?: boolean
|
|
15
|
+
errorLabel?: string
|
|
15
16
|
hint?: string
|
|
16
17
|
required?: boolean
|
|
17
18
|
leadingIcon?: string
|
|
@@ -235,7 +236,7 @@ const labelClasses = computed(() => {
|
|
|
235
236
|
</button>
|
|
236
237
|
</div>
|
|
237
238
|
|
|
238
|
-
<p v-if="error" class="px-4 text-body-small text-error">{{
|
|
239
|
+
<p v-if="error && errorLabel" class="px-4 text-body-small text-error">{{ errorLabel }}</p>
|
|
239
240
|
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
240
241
|
</div>
|
|
241
242
|
</template>
|
|
@@ -10,7 +10,8 @@ const props = withDefaults(
|
|
|
10
10
|
label: string;
|
|
11
11
|
type?: string;
|
|
12
12
|
variant?: "filled" | "outlined";
|
|
13
|
-
error?:
|
|
13
|
+
error?: boolean;
|
|
14
|
+
errorLabel?: string;
|
|
14
15
|
hint?: string;
|
|
15
16
|
disabled?: boolean;
|
|
16
17
|
required?: boolean;
|
|
@@ -188,7 +189,7 @@ function onInput(event: Event) {
|
|
|
188
189
|
</button>
|
|
189
190
|
</div>
|
|
190
191
|
|
|
191
|
-
<p v-if="error" class="px-4 text-body-small text-error">{{
|
|
192
|
+
<p v-if="error && errorLabel" class="px-4 text-body-small text-error">{{ errorLabel }}</p>
|
|
192
193
|
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
193
194
|
</div>
|
|
194
195
|
</template>
|
|
@@ -9,7 +9,8 @@ const props = withDefaults(defineProps<{
|
|
|
9
9
|
label?: string
|
|
10
10
|
placeholder?: string
|
|
11
11
|
disabled?: boolean
|
|
12
|
-
error?:
|
|
12
|
+
error?: boolean
|
|
13
|
+
errorLabel?: string
|
|
13
14
|
hint?: string
|
|
14
15
|
minuteStep?: number
|
|
15
16
|
use24h?: boolean
|
|
@@ -148,7 +149,7 @@ onUnmounted(() => {
|
|
|
148
149
|
</label>
|
|
149
150
|
</div>
|
|
150
151
|
|
|
151
|
-
<p v-if="error" class="px-4 text-body-small text-error">{{
|
|
152
|
+
<p v-if="error && errorLabel" class="px-4 text-body-small text-error">{{ errorLabel }}</p>
|
|
152
153
|
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
153
154
|
|
|
154
155
|
<Teleport to="body">
|
|
@@ -257,11 +257,11 @@ onUnmounted(() => {
|
|
|
257
257
|
|
|
258
258
|
// Resize handle definitions
|
|
259
259
|
const resizeHandles: { dir: ResizeDirection; class: string; cursor: string }[] = [
|
|
260
|
-
{ dir: 's', class: 'bottom-0 left-[
|
|
261
|
-
{ dir: 'e', class: 'top-[
|
|
262
|
-
{ dir: 'w', class: 'top-[
|
|
263
|
-
{ dir: 'se', class: 'bottom-0 right-0 w-[
|
|
264
|
-
{ dir: 'sw', class: 'bottom-0 left-0 w-[
|
|
260
|
+
{ dir: 's', class: 'bottom-0 left-[20px] right-[20px] h-[8px]', cursor: 'cursor-ns-resize' },
|
|
261
|
+
{ dir: 'e', class: 'top-[20px] right-0 bottom-[20px] w-[8px]', cursor: 'cursor-ew-resize' },
|
|
262
|
+
{ dir: 'w', class: 'top-[20px] left-0 bottom-[20px] w-[8px]', cursor: 'cursor-ew-resize' },
|
|
263
|
+
{ dir: 'se', class: 'bottom-0 right-0 w-[20px] h-[20px]', cursor: 'cursor-nwse-resize' },
|
|
264
|
+
{ dir: 'sw', class: 'bottom-0 left-0 w-[20px] h-[20px]', cursor: 'cursor-nesw-resize' },
|
|
265
265
|
]
|
|
266
266
|
</script>
|
|
267
267
|
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ export { default as MLoadingOverlay } from './components/MLoadingOverlay.vue'
|
|
|
81
81
|
// MMarkdown — import from '@m3ui-vue/m3ui-vue/markdown'
|
|
82
82
|
export { default as MMasonry } from './components/MMasonry.vue'
|
|
83
83
|
export { default as MMenu } from './components/MMenu.vue'
|
|
84
|
+
export { default as MMenuDivider } from './components/MMenuDivider.vue'
|
|
84
85
|
export { default as MMenuItem } from './components/MMenuItem.vue'
|
|
85
86
|
export { default as MMaskField } from './components/MMaskField.vue'
|
|
86
87
|
export type { MaskPreset } from './components/MMaskField.vue'
|
|
@@ -151,7 +152,6 @@ export type { Placement } from './components/MAbsolute.vue'
|
|
|
151
152
|
export type { BadgePosition, BadgeOverlap } from './components/MBadge.vue'
|
|
152
153
|
export type { CalendarEvent } from './components/MCalendar.vue'
|
|
153
154
|
export type { CarouselItem } from './components/MCarousel.vue'
|
|
154
|
-
export type { ContextMenuItem } from './components/MContextMenu.vue'
|
|
155
155
|
export type { DataTableColumn, DataTableFetchParams } from './components/MDataTable.vue'
|
|
156
156
|
export type { DateRange } from './components/MDateRangePicker.vue'
|
|
157
157
|
export type { DragDropItem } from './components/MDragDropList.vue'
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ContextMenuItem } from "./MContextMenu.vue";
|
|
2
|
-
type __VLS_Props = {
|
|
3
|
-
items: ContextMenuItem[];
|
|
4
|
-
x: number;
|
|
5
|
-
y: number;
|
|
6
|
-
};
|
|
7
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
-
close: () => any;
|
|
9
|
-
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
-
onClose?: (() => any) | undefined;
|
|
11
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
-
declare const _default: typeof __VLS_export;
|
|
13
|
-
export default _default;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, nextTick } from "vue";
|
|
3
|
-
import MIcon from "./MIcon.vue";
|
|
4
|
-
import MContextMenuPanel from "./_MContextMenuPanel.vue";
|
|
5
|
-
import type { ContextMenuItem } from "./MContextMenu.vue";
|
|
6
|
-
|
|
7
|
-
const props = defineProps<{
|
|
8
|
-
items: ContextMenuItem[];
|
|
9
|
-
x: number;
|
|
10
|
-
y: number;
|
|
11
|
-
}>();
|
|
12
|
-
|
|
13
|
-
const emit = defineEmits<{ close: [] }>();
|
|
14
|
-
|
|
15
|
-
const panel = ref<HTMLElement | null>(null);
|
|
16
|
-
const panelX = ref(props.x);
|
|
17
|
-
const panelY = ref(props.y);
|
|
18
|
-
const activeIndex = ref<number | null>(null);
|
|
19
|
-
const subPos = ref({ x: 0, y: 0 });
|
|
20
|
-
|
|
21
|
-
onMounted(async () => {
|
|
22
|
-
await nextTick();
|
|
23
|
-
if (!panel.value) return;
|
|
24
|
-
const el = panel.value;
|
|
25
|
-
panelX.value = Math.min(props.x, window.innerWidth - el.offsetWidth - 8);
|
|
26
|
-
panelY.value = Math.min(props.y, window.innerHeight - el.offsetHeight - 8);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
function onItemMouseEnter(index: number, item: ContextMenuItem, e: MouseEvent) {
|
|
30
|
-
if (item.divider || item.disabled) {
|
|
31
|
-
activeIndex.value = null;
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
if (!item.children?.length) {
|
|
35
|
-
activeIndex.value = null;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
activeIndex.value = index;
|
|
40
|
-
const itemEl = e.currentTarget as HTMLElement;
|
|
41
|
-
const itemRect = itemEl.getBoundingClientRect();
|
|
42
|
-
const panelRect = panel.value!.getBoundingClientRect();
|
|
43
|
-
|
|
44
|
-
let x = panelRect.right;
|
|
45
|
-
let y = itemRect.top;
|
|
46
|
-
if (x + 220 > window.innerWidth) x = panelRect.left - 220;
|
|
47
|
-
if (y + 300 > window.innerHeight) y = Math.max(8, window.innerHeight - 300);
|
|
48
|
-
|
|
49
|
-
subPos.value = { x, y };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function onItemClick(item: ContextMenuItem) {
|
|
53
|
-
if (item.disabled || item.divider || item.children?.length) return;
|
|
54
|
-
item.onClick?.();
|
|
55
|
-
emit("close");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function onPanelMouseLeave(e: MouseEvent) {
|
|
59
|
-
// Don't close if the mouse moved to another context menu panel (sibling sub-panel)
|
|
60
|
-
const related = e.relatedTarget as Element | null;
|
|
61
|
-
if (related?.closest(".m3-ctx-panel")) return;
|
|
62
|
-
activeIndex.value = null;
|
|
63
|
-
}
|
|
64
|
-
</script>
|
|
65
|
-
|
|
66
|
-
<template>
|
|
67
|
-
<div
|
|
68
|
-
ref="panel"
|
|
69
|
-
class="m3-ctx-panel absolute z-201 min-w-[200px] overflow-hidden rounded-lg bg-surface-container shadow-elevation-2"
|
|
70
|
-
:style="{ left: `${panelX}px`, top: `${panelY}px` }"
|
|
71
|
-
@mouseleave="onPanelMouseLeave"
|
|
72
|
-
>
|
|
73
|
-
<div class="py-1">
|
|
74
|
-
<template v-for="(item, i) in items" :key="i">
|
|
75
|
-
<hr v-if="item.divider" class="my-1 border-outline-variant" />
|
|
76
|
-
|
|
77
|
-
<component
|
|
78
|
-
v-else
|
|
79
|
-
:is="item.to && !item.disabled ? 'RouterLink' : 'div'"
|
|
80
|
-
:to="item.to && !item.disabled ? item.to : undefined"
|
|
81
|
-
class="relative flex cursor-default select-none items-center gap-3 px-4 py-2.5 text-body-large"
|
|
82
|
-
:class="[
|
|
83
|
-
item.disabled
|
|
84
|
-
? 'cursor-not-allowed opacity-38 text-on-surface'
|
|
85
|
-
: item.danger
|
|
86
|
-
? 'cursor-pointer text-error hover:bg-error/8'
|
|
87
|
-
: 'cursor-pointer text-on-surface hover:bg-on-surface/8',
|
|
88
|
-
activeIndex === i && !item.disabled
|
|
89
|
-
? item.danger
|
|
90
|
-
? 'bg-error/8'
|
|
91
|
-
: 'bg-on-surface/8'
|
|
92
|
-
: '',
|
|
93
|
-
]"
|
|
94
|
-
@mouseenter="onItemMouseEnter(i, item, $event)"
|
|
95
|
-
@click="onItemClick(item)"
|
|
96
|
-
>
|
|
97
|
-
<MIcon
|
|
98
|
-
v-if="item.icon"
|
|
99
|
-
:name="item.icon"
|
|
100
|
-
:size="18"
|
|
101
|
-
class="shrink-0"
|
|
102
|
-
:class="item.danger ? 'text-error' : 'text-on-surface-variant'"
|
|
103
|
-
/>
|
|
104
|
-
<span v-else class="w-[18px] shrink-0" />
|
|
105
|
-
|
|
106
|
-
<span class="flex-1">{{ item.label }}</span>
|
|
107
|
-
|
|
108
|
-
<span
|
|
109
|
-
v-if="item.shortcut"
|
|
110
|
-
class="text-label-small text-on-surface-variant"
|
|
111
|
-
>
|
|
112
|
-
{{ item.shortcut }}
|
|
113
|
-
</span>
|
|
114
|
-
|
|
115
|
-
<MIcon
|
|
116
|
-
v-if="item.children?.length"
|
|
117
|
-
name="chevron_right"
|
|
118
|
-
:size="18"
|
|
119
|
-
class="shrink-0 text-on-surface-variant"
|
|
120
|
-
/>
|
|
121
|
-
</component>
|
|
122
|
-
</template>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<!-- Sub-panel: sibling in the same Teleport layer.
|
|
127
|
-
No <Transition> wrapper — MContextMenuPanel is a fragment and cannot be
|
|
128
|
-
animated by Vue's Transition (produces a console warning). -->
|
|
129
|
-
<MContextMenuPanel
|
|
130
|
-
v-if="activeIndex !== null && items[activeIndex]?.children?.length"
|
|
131
|
-
:key="activeIndex"
|
|
132
|
-
:items="items[activeIndex]!.children!"
|
|
133
|
-
:x="subPos.x"
|
|
134
|
-
:y="subPos.y"
|
|
135
|
-
@close="emit('close')"
|
|
136
|
-
/>
|
|
137
|
-
</template>
|