@m3ui-vue/m3ui-vue 0.4.6 → 0.5.0
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/MCircleProgressBar.vue.d.ts +27 -0
- 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/MProgressBar.vue.d.ts +1 -0
- 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/composables/useNotification.d.ts +7 -4
- package/dist/composables/useToast.d.ts +7 -4
- package/dist/index.d.ts +2 -1
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +1291 -1026
- 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 +354 -0
- package/src/components/MColorPicker.vue +4 -3
- package/src/components/MColorPickerModal.vue +1 -1
- package/src/components/MContextMenu.vue +51 -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 +22 -2
- package/src/components/MNumberField.vue +3 -2
- package/src/components/MProgressBar.vue +200 -75
- package/src/components/MSelect.vue +3 -2
- package/src/components/MSlider.vue +20 -16
- package/src/components/MSnackbar.vue +24 -2
- 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/composables/useNotification.ts +21 -4
- package/src/composables/useToast.ts +21 -4
- package/src/index.ts +2 -1
- package/dist/components/_MContextMenuPanel.vue.d.ts +0 -13
- package/src/components/_MContextMenuPanel.vue +0 -137
|
@@ -20,7 +20,8 @@ const props = withDefaults(
|
|
|
20
20
|
variant?: 'filled' | 'outlined'
|
|
21
21
|
mode?: 'docked' | 'modal'
|
|
22
22
|
disabled?: boolean
|
|
23
|
-
error?:
|
|
23
|
+
error?: boolean
|
|
24
|
+
errorLabel?: string
|
|
24
25
|
hint?: string
|
|
25
26
|
required?: boolean
|
|
26
27
|
leadingIcon?: string
|
|
@@ -247,7 +248,7 @@ const labelClasses = computed(() => {
|
|
|
247
248
|
</div>
|
|
248
249
|
</div>
|
|
249
250
|
|
|
250
|
-
<p v-if="error" class="px-4 text-body-small text-error">{{
|
|
251
|
+
<p v-if="error && errorLabel" class="px-4 text-body-small text-error">{{ errorLabel }}</p>
|
|
251
252
|
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
252
253
|
</div>
|
|
253
254
|
|
|
@@ -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
|
>
|
|
@@ -90,7 +90,18 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
90
90
|
<template>
|
|
91
91
|
<div :class="containerClass">
|
|
92
92
|
<TransitionGroup :name="isTop ? 'm3-toast-top' : 'm3-toast-bot'">
|
|
93
|
-
<div v-for="t in toasts" :key="t.id" class="toast-row w-full min-w-64 max-w-xs">
|
|
93
|
+
<div v-for="t in toasts" :key="t.id" class="toast-row relative w-full min-w-64 max-w-xs">
|
|
94
|
+
<Transition name="m3-badge">
|
|
95
|
+
<span
|
|
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"
|
|
98
|
+
>
|
|
99
|
+
<Transition name="m3-count" mode="out-in">
|
|
100
|
+
<span :key="t.count">{{ t.count }}</span>
|
|
101
|
+
</Transition>
|
|
102
|
+
</span>
|
|
103
|
+
</Transition>
|
|
104
|
+
|
|
94
105
|
<div
|
|
95
106
|
class="toast-inner pointer-events-auto relative flex items-center gap-3 overflow-hidden rounded-2xl px-4 py-4 shadow-elevation-2"
|
|
96
107
|
:class="t.color ? 'text-white ring-1 ring-inset ring-white/10' : getVariantStyle(t.variant).container"
|
|
@@ -98,7 +109,7 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
98
109
|
>
|
|
99
110
|
<MSpinner v-if="t.loading" :size="20" class="shrink-0" />
|
|
100
111
|
<MIcon
|
|
101
|
-
v-else
|
|
112
|
+
v-else-if="t.icon !== null"
|
|
102
113
|
:name="t.icon ?? getVariantStyle(t.variant).iconName"
|
|
103
114
|
:size="20"
|
|
104
115
|
class="shrink-0"
|
|
@@ -244,6 +255,17 @@ const getVariantStyle = (variant: string): VariantStyle =>
|
|
|
244
255
|
transform: scale(0.92);
|
|
245
256
|
}
|
|
246
257
|
|
|
258
|
+
/* badge appear/disappear */
|
|
259
|
+
.m3-badge-enter-active { transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
|
260
|
+
.m3-badge-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
|
|
261
|
+
.m3-badge-enter-from, .m3-badge-leave-to { opacity: 0; transform: scale(0.5); }
|
|
262
|
+
|
|
263
|
+
/* count number bump when incrementing */
|
|
264
|
+
.m3-count-enter-active { transition: opacity 0.12s ease, transform 0.12s ease; }
|
|
265
|
+
.m3-count-leave-active { transition: opacity 0.08s ease, transform 0.08s ease; }
|
|
266
|
+
.m3-count-enter-from { opacity: 0; transform: scale(1.5) translateY(-3px); }
|
|
267
|
+
.m3-count-leave-to { opacity: 0; transform: scale(0.6) translateY(2px); }
|
|
268
|
+
|
|
247
269
|
@keyframes m3-toast-progress {
|
|
248
270
|
from {
|
|
249
271
|
transform: scaleX(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
|
|
|
@@ -14,9 +14,10 @@ export interface Notification {
|
|
|
14
14
|
variant: NotificationVariant
|
|
15
15
|
duration: number
|
|
16
16
|
loading?: boolean
|
|
17
|
-
icon?: string
|
|
17
|
+
icon?: string | null // null = suppress icon entirely
|
|
18
18
|
closable?: boolean
|
|
19
19
|
action?: NotificationAction
|
|
20
|
+
count: number // ≥ 2 → show badge
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let nextId = 1
|
|
@@ -44,7 +45,7 @@ function dismiss(id: number) {
|
|
|
44
45
|
export interface NotificationOptions {
|
|
45
46
|
duration?: number
|
|
46
47
|
loading?: boolean
|
|
47
|
-
icon?: string
|
|
48
|
+
icon?: string | null
|
|
48
49
|
closable?: boolean
|
|
49
50
|
action?: NotificationAction
|
|
50
51
|
}
|
|
@@ -55,11 +56,27 @@ function show(
|
|
|
55
56
|
options: NotificationOptions = {},
|
|
56
57
|
) {
|
|
57
58
|
ensureMounted()
|
|
58
|
-
const id = nextId++
|
|
59
59
|
const isLoading = options.loading ?? false
|
|
60
|
+
|
|
61
|
+
// Dedup: identical non-loading, non-action notifications increment count
|
|
62
|
+
if (!isLoading && !options.action) {
|
|
63
|
+
const existing = notifications.value.find(
|
|
64
|
+
n => n.message === message && n.variant === variant && !n.loading && !n.action,
|
|
65
|
+
)
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.count++
|
|
68
|
+
const old = timers.get(existing.id)
|
|
69
|
+
if (old) clearTimeout(old)
|
|
70
|
+
if (existing.duration > 0)
|
|
71
|
+
timers.set(existing.id, setTimeout(() => dismiss(existing.id), existing.duration))
|
|
72
|
+
return existing.id
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const id = nextId++
|
|
60
77
|
const duration = isLoading ? 0 : (options.duration ?? 3000)
|
|
61
78
|
const closable = options.closable ?? true
|
|
62
|
-
notifications.value.push({ id, message, variant, duration, loading: isLoading, icon: options.icon, closable, action: options.action })
|
|
79
|
+
notifications.value.push({ id, message, variant, duration, loading: isLoading, icon: options.icon, closable, action: options.action, count: 1 })
|
|
63
80
|
if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration))
|
|
64
81
|
return id
|
|
65
82
|
}
|
|
@@ -15,8 +15,9 @@ export interface Toast {
|
|
|
15
15
|
duration: number
|
|
16
16
|
loading?: boolean
|
|
17
17
|
action?: ToastAction
|
|
18
|
-
icon?: string
|
|
18
|
+
icon?: string | null // null = suppress icon entirely
|
|
19
19
|
color?: string
|
|
20
|
+
count: number // ≥ 2 → show badge
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let nextId = 1
|
|
@@ -45,7 +46,7 @@ export interface ToastOptions {
|
|
|
45
46
|
duration?: number
|
|
46
47
|
loading?: boolean
|
|
47
48
|
action?: ToastAction
|
|
48
|
-
icon?: string
|
|
49
|
+
icon?: string | null
|
|
49
50
|
color?: string
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -55,11 +56,27 @@ function show(
|
|
|
55
56
|
options: number | ToastOptions = {},
|
|
56
57
|
) {
|
|
57
58
|
ensureMounted()
|
|
58
|
-
const id = nextId++
|
|
59
59
|
const opts = typeof options === 'number' ? { duration: options } : options
|
|
60
60
|
const isLoading = opts.loading ?? false
|
|
61
|
+
|
|
62
|
+
// Dedup: identical non-loading, non-action toasts increment count instead of stacking
|
|
63
|
+
if (!isLoading && !opts.action) {
|
|
64
|
+
const existing = toasts.value.find(
|
|
65
|
+
t => t.message === message && t.variant === variant && !t.loading && !t.action,
|
|
66
|
+
)
|
|
67
|
+
if (existing) {
|
|
68
|
+
existing.count++
|
|
69
|
+
const old = timers.get(existing.id)
|
|
70
|
+
if (old) clearTimeout(old)
|
|
71
|
+
if (existing.duration > 0)
|
|
72
|
+
timers.set(existing.id, setTimeout(() => dismiss(existing.id), existing.duration))
|
|
73
|
+
return existing.id
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const id = nextId++
|
|
61
78
|
const duration = isLoading ? 0 : (opts.duration ?? (variant === 'error' ? 6000 : 4000))
|
|
62
|
-
toasts.value.push({ id, message, variant, duration, loading: isLoading, action: opts.action, icon: opts.icon, color: opts.color })
|
|
79
|
+
toasts.value.push({ id, message, variant, duration, loading: isLoading, action: opts.action, icon: opts.icon, color: opts.color, count: 1 })
|
|
63
80
|
if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration))
|
|
64
81
|
return id
|
|
65
82
|
}
|
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'
|
|
@@ -94,6 +95,7 @@ export { default as MNavigationRail } from './components/MNavigationRail.vue'
|
|
|
94
95
|
export { default as MOverlay } from './components/MOverlay.vue'
|
|
95
96
|
export { default as MPagination } from './components/MPagination.vue'
|
|
96
97
|
export { default as MProgressBar } from './components/MProgressBar.vue'
|
|
98
|
+
export { default as MCircleProgressBar } from './components/MCircleProgressBar.vue'
|
|
97
99
|
export { default as MRadio } from './components/MRadio.vue'
|
|
98
100
|
export { default as MRadioGroup } from './components/MRadioGroup.vue'
|
|
99
101
|
export { default as MRating } from './components/MRating.vue'
|
|
@@ -150,7 +152,6 @@ export type { Placement } from './components/MAbsolute.vue'
|
|
|
150
152
|
export type { BadgePosition, BadgeOverlap } from './components/MBadge.vue'
|
|
151
153
|
export type { CalendarEvent } from './components/MCalendar.vue'
|
|
152
154
|
export type { CarouselItem } from './components/MCarousel.vue'
|
|
153
|
-
export type { ContextMenuItem } from './components/MContextMenu.vue'
|
|
154
155
|
export type { DataTableColumn, DataTableFetchParams } from './components/MDataTable.vue'
|
|
155
156
|
export type { DateRange } from './components/MDateRangePicker.vue'
|
|
156
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>
|