@m3ui-vue/m3ui-vue 0.3.0 → 0.4.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-C6RwHl-l.js → MMenuItem-FyttjOj_.js} +399 -207
- package/dist/MMenuItem-FyttjOj_.js.map +1 -0
- package/dist/components/MAutocomplete.vue.d.ts +2 -0
- package/dist/components/MBox.vue.d.ts +2 -2
- package/dist/components/MButton.vue.d.ts +4 -0
- package/dist/components/MButtonGroup.vue.d.ts +28 -0
- package/dist/components/MCarousel.vue.d.ts +21 -12
- package/dist/components/MChatBubble.vue.d.ts +32 -0
- package/dist/components/MColorPickerModal.vue.d.ts +18 -0
- package/dist/components/MDataTable.vue.d.ts +1 -1
- package/dist/components/MDatePickerModal.vue.d.ts +21 -0
- package/dist/components/MDateRangePickerModal.vue.d.ts +24 -0
- package/dist/components/MDialog.vue.d.ts +9 -1
- package/dist/components/MExpansionPanel.vue.d.ts +3 -2
- package/dist/components/MFab.vue.d.ts +14 -2
- package/dist/components/MFlex.vue.d.ts +2 -2
- package/dist/components/MIconButton.vue.d.ts +4 -2
- package/dist/components/MInfiniteScroll.vue.d.ts +1 -1
- package/dist/components/MList.vue.d.ts +2 -0
- package/dist/components/MLoadingOverlay.vue.d.ts +1 -1
- package/dist/components/MMenu.vue.d.ts +3 -1
- package/dist/components/MMenuItem.vue.d.ts +14 -3
- package/dist/components/MMultiAutocomplete.vue.d.ts +2 -0
- package/dist/components/MMultiSelect.vue.d.ts +2 -0
- package/dist/components/MNotificationHost.vue.d.ts +3 -0
- package/dist/components/MSelect.vue.d.ts +2 -0
- package/dist/components/MSlider.vue.d.ts +15 -4
- package/dist/components/MSplitButton.vue.d.ts +39 -0
- package/dist/components/MSplitter.vue.d.ts +1 -1
- package/dist/components/MStack.vue.d.ts +2 -2
- package/dist/components/MTable.vue.d.ts +1 -1
- package/dist/components/MTimePickerModal.vue.d.ts +21 -0
- package/dist/components/MToolbar.vue.d.ts +30 -0
- package/dist/components/MTooltip.vue.d.ts +8 -2
- package/dist/components/MWindow.vue.d.ts +4 -2
- package/dist/composables/useDevice.d.ts +3 -0
- package/dist/composables/useNotification.d.ts +64 -0
- package/dist/composables/useToast.d.ts +8 -0
- package/dist/index.d.ts +13 -0
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +5231 -2916
- package/dist/m3ui.js.map +1 -1
- package/dist/rich-text-editor.js +10 -10
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/MAutocomplete.vue +108 -13
- package/src/components/MButton.vue +38 -16
- package/src/components/MButtonGroup.vue +158 -0
- package/src/components/MCarousel.vue +223 -101
- package/src/components/MChatBubble.vue +98 -0
- package/src/components/MColorPicker.vue +1 -4
- package/src/components/MColorPickerModal.vue +276 -0
- package/src/components/MDatePicker.vue +1 -4
- package/src/components/MDatePickerModal.vue +265 -0
- package/src/components/MDateRangePicker.vue +1 -4
- package/src/components/MDateRangePickerModal.vue +264 -0
- package/src/components/MDialog.vue +38 -1
- package/src/components/MExpansionPanel.vue +10 -32
- package/src/components/MFab.vue +92 -11
- package/src/components/MIconButton.vue +36 -6
- package/src/components/MList.vue +6 -3
- package/src/components/MListItem.vue +9 -3
- package/src/components/MMenu.vue +10 -5
- package/src/components/MMenuItem.vue +93 -10
- package/src/components/MMultiAutocomplete.vue +104 -10
- package/src/components/MMultiSelect.vue +106 -13
- package/src/components/MNotificationHost.vue +163 -0
- package/src/components/MSelect.vue +99 -16
- package/src/components/MSlider.vue +367 -134
- package/src/components/MSnackbar.vue +3 -0
- package/src/components/MSplitButton.vue +240 -0
- package/src/components/MTabs.vue +82 -39
- package/src/components/MTagInput.vue +11 -1
- package/src/components/MTimePicker.vue +1 -4
- package/src/components/MTimePickerModal.vue +458 -0
- package/src/components/MToolbar.vue +75 -0
- package/src/components/MTooltip.vue +60 -10
- package/src/components/MWindow.vue +83 -48
- package/src/components/_MContextMenuPanel.vue +2 -2
- package/src/composables/useDevice.ts +29 -0
- package/src/composables/useNotification.ts +93 -0
- package/src/composables/useToast.ts +42 -5
- package/src/index.ts +13 -0
- package/dist/MMenuItem-C6RwHl-l.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
|
|
5
5
|
const props = withDefaults(
|
|
@@ -11,6 +11,8 @@ const props = withDefaults(
|
|
|
11
11
|
height?: string
|
|
12
12
|
minWidth?: number
|
|
13
13
|
minHeight?: number
|
|
14
|
+
maxWidth?: number
|
|
15
|
+
maxHeight?: number
|
|
14
16
|
resizable?: boolean
|
|
15
17
|
draggable?: boolean
|
|
16
18
|
closable?: boolean
|
|
@@ -86,46 +88,69 @@ function bringToFront() {
|
|
|
86
88
|
zIndex.value = globalZIndex
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
// ---- Pointer helpers ----
|
|
92
|
+
function getXY(e: MouseEvent | TouchEvent): { x: number; y: number } {
|
|
93
|
+
if ('touches' in e) {
|
|
94
|
+
const t = e.touches[0] ?? e.changedTouches[0]
|
|
95
|
+
return { x: t!.clientX, y: t!.clientY }
|
|
96
|
+
}
|
|
97
|
+
return { x: e.clientX, y: e.clientY }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addListeners() {
|
|
101
|
+
document.addEventListener('mousemove', onPointerMove)
|
|
102
|
+
document.addEventListener('mouseup', onPointerUp)
|
|
103
|
+
document.addEventListener('touchmove', onPointerMove, { passive: false })
|
|
104
|
+
document.addEventListener('touchend', onPointerUp)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function removeListeners() {
|
|
108
|
+
document.removeEventListener('mousemove', onPointerMove)
|
|
109
|
+
document.removeEventListener('mouseup', onPointerUp)
|
|
110
|
+
document.removeEventListener('touchmove', onPointerMove)
|
|
111
|
+
document.removeEventListener('touchend', onPointerUp)
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
// ---- Drag ----
|
|
90
|
-
function
|
|
115
|
+
function onTitleBarStart(e: MouseEvent | TouchEvent) {
|
|
91
116
|
if (!props.draggable) return
|
|
92
|
-
// Ignore clicks on buttons inside the title bar
|
|
93
117
|
if ((e.target as HTMLElement).closest('button')) return
|
|
94
118
|
e.preventDefault()
|
|
119
|
+
const { x, y } = getXY(e)
|
|
95
120
|
isDragging = true
|
|
96
|
-
dragStartX =
|
|
97
|
-
dragStartY =
|
|
121
|
+
dragStartX = x
|
|
122
|
+
dragStartY = y
|
|
98
123
|
dragStartPosX = posX.value
|
|
99
124
|
dragStartPosY = posY.value
|
|
100
125
|
bringToFront()
|
|
101
|
-
|
|
102
|
-
document.addEventListener('mouseup', onMouseUp)
|
|
126
|
+
addListeners()
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// ---- Resize ----
|
|
106
|
-
function
|
|
130
|
+
function onResizeStart(e: MouseEvent | TouchEvent, direction: ResizeDirection) {
|
|
107
131
|
if (!props.resizable) return
|
|
108
132
|
e.preventDefault()
|
|
109
133
|
e.stopPropagation()
|
|
134
|
+
const { x, y } = getXY(e)
|
|
110
135
|
isResizing = true
|
|
111
136
|
resizeDir = direction
|
|
112
|
-
resizeStartX =
|
|
113
|
-
resizeStartY =
|
|
137
|
+
resizeStartX = x
|
|
138
|
+
resizeStartY = y
|
|
114
139
|
resizeStartW = winWidth.value
|
|
115
140
|
resizeStartH = winHeight.value
|
|
116
141
|
resizeStartPosX = posX.value
|
|
117
142
|
resizeStartPosY = posY.value
|
|
118
143
|
bringToFront()
|
|
119
|
-
|
|
120
|
-
document.addEventListener('mouseup', onMouseUp)
|
|
144
|
+
addListeners()
|
|
121
145
|
}
|
|
122
146
|
|
|
123
|
-
function
|
|
147
|
+
function onPointerMove(e: MouseEvent | TouchEvent) {
|
|
124
148
|
e.preventDefault()
|
|
149
|
+
const { x, y } = getXY(e)
|
|
125
150
|
|
|
126
151
|
if (isDragging) {
|
|
127
|
-
const dx =
|
|
128
|
-
const dy =
|
|
152
|
+
const dx = x - dragStartX
|
|
153
|
+
const dy = y - dragStartY
|
|
129
154
|
const parent = windowRef.value?.parentElement
|
|
130
155
|
const maxX = parent ? parent.clientWidth - winWidth.value : Infinity
|
|
131
156
|
const maxY = parent ? parent.clientHeight - (minimized.value ? 40 : winHeight.value) : Infinity
|
|
@@ -134,45 +159,34 @@ function onMouseMove(e: MouseEvent) {
|
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
if (isResizing) {
|
|
137
|
-
const dx =
|
|
138
|
-
const dy =
|
|
162
|
+
const dx = x - resizeStartX
|
|
163
|
+
const dy = y - resizeStartY
|
|
139
164
|
const dir = resizeDir
|
|
140
165
|
const parent = windowRef.value?.parentElement
|
|
141
166
|
const parentW = parent ? parent.clientWidth : Infinity
|
|
142
167
|
const parentH = parent ? parent.clientHeight : Infinity
|
|
143
168
|
|
|
144
|
-
|
|
169
|
+
const clampW = (w: number) => Math.min(props.maxWidth ?? Infinity, Math.max(props.minWidth, w))
|
|
170
|
+
const clampH = (h: number) => Math.min(props.maxHeight ?? Infinity, Math.max(props.minHeight, h))
|
|
171
|
+
|
|
145
172
|
if (dir.includes('e')) {
|
|
146
|
-
|
|
147
|
-
winWidth.value = Math.min(maxW, Math.max(props.minWidth, resizeStartW + dx))
|
|
173
|
+
winWidth.value = Math.min(parentW - resizeStartPosX, clampW(resizeStartW + dx))
|
|
148
174
|
}
|
|
149
|
-
// West
|
|
150
175
|
if (dir.includes('w')) {
|
|
151
|
-
const
|
|
152
|
-
const newW = Math.min(maxW, Math.max(props.minWidth, resizeStartW - dx))
|
|
176
|
+
const newW = Math.min(resizeStartPosX + resizeStartW, clampW(resizeStartW - dx))
|
|
153
177
|
posX.value = Math.max(0, resizeStartPosX + (resizeStartW - newW))
|
|
154
178
|
winWidth.value = newW
|
|
155
179
|
}
|
|
156
|
-
// South
|
|
157
180
|
if (dir.includes('s')) {
|
|
158
|
-
|
|
159
|
-
winHeight.value = Math.min(maxH, Math.max(props.minHeight, resizeStartH + dy))
|
|
160
|
-
}
|
|
161
|
-
// North
|
|
162
|
-
if (dir === 'n' || dir === 'ne' || dir === 'nw') {
|
|
163
|
-
const maxH = resizeStartPosY + resizeStartH
|
|
164
|
-
const newH = Math.min(maxH, Math.max(props.minHeight, resizeStartH - dy))
|
|
165
|
-
posY.value = Math.max(0, resizeStartPosY + (resizeStartH - newH))
|
|
166
|
-
winHeight.value = newH
|
|
181
|
+
winHeight.value = Math.min(parentH - resizeStartPosY, clampH(resizeStartH + dy))
|
|
167
182
|
}
|
|
168
183
|
}
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
function
|
|
186
|
+
function onPointerUp() {
|
|
172
187
|
isDragging = false
|
|
173
188
|
isResizing = false
|
|
174
|
-
|
|
175
|
-
document.removeEventListener('mouseup', onMouseUp)
|
|
189
|
+
removeListeners()
|
|
176
190
|
}
|
|
177
191
|
|
|
178
192
|
// ---- Actions ----
|
|
@@ -198,6 +212,29 @@ function onWindowMouseDown() {
|
|
|
198
212
|
bringToFront()
|
|
199
213
|
}
|
|
200
214
|
|
|
215
|
+
// ---- Clamp to parent ----
|
|
216
|
+
function clampToParent() {
|
|
217
|
+
const parent = windowRef.value?.parentElement
|
|
218
|
+
if (!parent) return
|
|
219
|
+
const pw = parent.clientWidth
|
|
220
|
+
const ph = parent.clientHeight
|
|
221
|
+
if (winWidth.value > pw) winWidth.value = Math.max(props.minWidth, pw)
|
|
222
|
+
if (winHeight.value > ph) winHeight.value = Math.max(props.minHeight, ph)
|
|
223
|
+
if (posX.value + winWidth.value > pw) posX.value = Math.max(0, pw - winWidth.value)
|
|
224
|
+
if (posY.value + winHeight.value > ph) posY.value = Math.max(0, ph - winHeight.value)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let resizeObserver: ResizeObserver | null = null
|
|
228
|
+
|
|
229
|
+
onMounted(() => {
|
|
230
|
+
clampToParent()
|
|
231
|
+
const parent = windowRef.value?.parentElement
|
|
232
|
+
if (parent) {
|
|
233
|
+
resizeObserver = new ResizeObserver(clampToParent)
|
|
234
|
+
resizeObserver.observe(parent)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
201
238
|
// ---- Sync initial props ----
|
|
202
239
|
watch(
|
|
203
240
|
() => props.x,
|
|
@@ -207,21 +244,22 @@ watch(
|
|
|
207
244
|
() => props.y,
|
|
208
245
|
(v) => { posY.value = v },
|
|
209
246
|
)
|
|
247
|
+
watch(
|
|
248
|
+
() => props.modelValue,
|
|
249
|
+
(v) => { if (v) nextTick(clampToParent) },
|
|
250
|
+
)
|
|
210
251
|
|
|
211
252
|
// ---- Cleanup ----
|
|
212
253
|
onUnmounted(() => {
|
|
213
|
-
|
|
214
|
-
|
|
254
|
+
removeListeners()
|
|
255
|
+
resizeObserver?.disconnect()
|
|
215
256
|
})
|
|
216
257
|
|
|
217
258
|
// Resize handle definitions
|
|
218
259
|
const resizeHandles: { dir: ResizeDirection; class: string; cursor: string }[] = [
|
|
219
|
-
{ dir: 'n', class: 'top-0 left-[10px] right-[10px] h-[5px]', cursor: 'cursor-ns-resize' },
|
|
220
260
|
{ dir: 's', class: 'bottom-0 left-[10px] right-[10px] h-[5px]', cursor: 'cursor-ns-resize' },
|
|
221
261
|
{ dir: 'e', class: 'top-[10px] right-0 bottom-[10px] w-[5px]', cursor: 'cursor-ew-resize' },
|
|
222
262
|
{ dir: 'w', class: 'top-[10px] left-0 bottom-[10px] w-[5px]', cursor: 'cursor-ew-resize' },
|
|
223
|
-
{ dir: 'ne', class: 'top-0 right-0 w-[12px] h-[12px]', cursor: 'cursor-nesw-resize' },
|
|
224
|
-
{ dir: 'nw', class: 'top-0 left-0 w-[12px] h-[12px]', cursor: 'cursor-nwse-resize' },
|
|
225
263
|
{ dir: 'se', class: 'bottom-0 right-0 w-[12px] h-[12px]', cursor: 'cursor-nwse-resize' },
|
|
226
264
|
{ dir: 'sw', class: 'bottom-0 left-0 w-[12px] h-[12px]', cursor: 'cursor-nesw-resize' },
|
|
227
265
|
]
|
|
@@ -239,7 +277,8 @@ const resizeHandles: { dir: ResizeDirection; class: string; cursor: string }[] =
|
|
|
239
277
|
<div
|
|
240
278
|
class="flex items-center gap-2 px-4 h-10 bg-surface-container select-none shrink-0"
|
|
241
279
|
:class="{ 'cursor-move': draggable }"
|
|
242
|
-
@mousedown="
|
|
280
|
+
@mousedown="onTitleBarStart"
|
|
281
|
+
@touchstart="onTitleBarStart"
|
|
243
282
|
>
|
|
244
283
|
<slot name="header">
|
|
245
284
|
<MIcon v-if="icon" :name="icon" :size="18" class="text-on-surface-variant" />
|
|
@@ -282,14 +321,10 @@ const resizeHandles: { dir: ResizeDirection; class: string; cursor: string }[] =
|
|
|
282
321
|
:key="handle.dir"
|
|
283
322
|
class="absolute"
|
|
284
323
|
:class="[handle.class, handle.cursor]"
|
|
285
|
-
@mousedown="
|
|
324
|
+
@mousedown="onResizeStart($event, handle.dir)"
|
|
325
|
+
@touchstart="onResizeStart($event, handle.dir)"
|
|
286
326
|
/>
|
|
287
327
|
</template>
|
|
288
328
|
</div>
|
|
289
329
|
</template>
|
|
290
330
|
|
|
291
|
-
<style scoped>
|
|
292
|
-
.flex-col {
|
|
293
|
-
transition: height 0.2s ease;
|
|
294
|
-
}
|
|
295
|
-
</style>
|
|
@@ -66,11 +66,11 @@ function onPanelMouseLeave(e: MouseEvent) {
|
|
|
66
66
|
<template>
|
|
67
67
|
<div
|
|
68
68
|
ref="panel"
|
|
69
|
-
class="m3-ctx-panel absolute z-201 min-w-[200px] rounded-
|
|
69
|
+
class="m3-ctx-panel absolute z-201 min-w-[200px] overflow-hidden rounded-lg bg-surface-container shadow-elevation-2"
|
|
70
70
|
:style="{ left: `${panelX}px`, top: `${panelY}px` }"
|
|
71
71
|
@mouseleave="onPanelMouseLeave"
|
|
72
72
|
>
|
|
73
|
-
<div class="
|
|
73
|
+
<div class="py-1">
|
|
74
74
|
<template v-for="(item, i) in items" :key="i">
|
|
75
75
|
<hr v-if="item.divider" class="my-1 border-outline-variant" />
|
|
76
76
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
const isMobile = ref(false)
|
|
6
|
+
let listeners = 0
|
|
7
|
+
|
|
8
|
+
function update() {
|
|
9
|
+
isMobile.value = window.innerWidth < MOBILE_BREAKPOINT
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useDevice() {
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
if (listeners === 0) {
|
|
15
|
+
update()
|
|
16
|
+
window.addEventListener('resize', update)
|
|
17
|
+
}
|
|
18
|
+
listeners++
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
onUnmounted(() => {
|
|
22
|
+
listeners--
|
|
23
|
+
if (listeners === 0) {
|
|
24
|
+
window.removeEventListener('resize', update)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return { isMobile }
|
|
29
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ref, createApp, h } from 'vue'
|
|
2
|
+
import MNotificationHost from '../components/MNotificationHost.vue'
|
|
3
|
+
|
|
4
|
+
export type NotificationVariant = 'info' | 'success' | 'warning' | 'error'
|
|
5
|
+
export type NotificationPosition =
|
|
6
|
+
| 'top-left' | 'top-center' | 'top-right'
|
|
7
|
+
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
8
|
+
|
|
9
|
+
export interface NotificationAction { label: string; onClick: () => void }
|
|
10
|
+
|
|
11
|
+
export interface Notification {
|
|
12
|
+
id: number
|
|
13
|
+
message: string
|
|
14
|
+
variant: NotificationVariant
|
|
15
|
+
duration: number
|
|
16
|
+
loading?: boolean
|
|
17
|
+
icon?: string
|
|
18
|
+
closable?: boolean
|
|
19
|
+
action?: NotificationAction
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let nextId = 1
|
|
23
|
+
let mounted = false
|
|
24
|
+
const timers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
25
|
+
|
|
26
|
+
const notifications = ref<Notification[]>([])
|
|
27
|
+
const position = ref<NotificationPosition>('top-right')
|
|
28
|
+
|
|
29
|
+
function ensureMounted() {
|
|
30
|
+
if (mounted || typeof document === 'undefined') return
|
|
31
|
+
mounted = true
|
|
32
|
+
const container = document.createElement('div')
|
|
33
|
+
container.id = 'm3-notification-host'
|
|
34
|
+
document.body.appendChild(container)
|
|
35
|
+
createApp({ render: () => h(MNotificationHost) }).mount(container)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dismiss(id: number) {
|
|
39
|
+
const timer = timers.get(id)
|
|
40
|
+
if (timer) { clearTimeout(timer); timers.delete(id) }
|
|
41
|
+
notifications.value = notifications.value.filter((n) => n.id !== id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NotificationOptions {
|
|
45
|
+
duration?: number
|
|
46
|
+
loading?: boolean
|
|
47
|
+
icon?: string
|
|
48
|
+
closable?: boolean
|
|
49
|
+
action?: NotificationAction
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function show(
|
|
53
|
+
message: string,
|
|
54
|
+
variant: NotificationVariant = 'info',
|
|
55
|
+
options: NotificationOptions = {},
|
|
56
|
+
) {
|
|
57
|
+
ensureMounted()
|
|
58
|
+
const id = nextId++
|
|
59
|
+
const isLoading = options.loading ?? false
|
|
60
|
+
const duration = isLoading ? 0 : (options.duration ?? 3000)
|
|
61
|
+
const closable = options.closable ?? true
|
|
62
|
+
notifications.value.push({ id, message, variant, duration, loading: isLoading, icon: options.icon, closable, action: options.action })
|
|
63
|
+
if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration))
|
|
64
|
+
return id
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function update(id: number, patch: Partial<Omit<Notification, 'id'>>) {
|
|
68
|
+
const n = notifications.value.find((n) => n.id === id)
|
|
69
|
+
if (!n) return
|
|
70
|
+
Object.assign(n, patch)
|
|
71
|
+
if ('loading' in patch && !patch.loading) {
|
|
72
|
+
const timer = timers.get(id)
|
|
73
|
+
if (timer) clearTimeout(timer)
|
|
74
|
+
const dur = patch.duration ?? 3000
|
|
75
|
+
if (dur > 0) {
|
|
76
|
+
n.duration = dur
|
|
77
|
+
timers.set(id, setTimeout(() => dismiss(id), dur))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loading(msg: string, opts?: Omit<NotificationOptions, 'loading'>) {
|
|
83
|
+
return show(msg, 'info', { ...opts, loading: true, closable: opts?.closable ?? false })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const success = (msg: string, opts?: NotificationOptions) => show(msg, 'success', opts ?? {})
|
|
87
|
+
const error = (msg: string, opts?: NotificationOptions) => show(msg, 'error', opts ?? {})
|
|
88
|
+
const warning = (msg: string, opts?: NotificationOptions) => show(msg, 'warning', opts ?? {})
|
|
89
|
+
const info = (msg: string, opts?: NotificationOptions) => show(msg, 'info', opts ?? {})
|
|
90
|
+
|
|
91
|
+
export function useNotification() {
|
|
92
|
+
return { notifications, position, show, success, error, warning, info, loading, update, dismiss }
|
|
93
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ref } from 'vue'
|
|
1
|
+
import { ref, createApp, h } from 'vue'
|
|
2
|
+
import MSnackbar from '../components/MSnackbar.vue'
|
|
2
3
|
|
|
3
4
|
export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
|
|
4
5
|
export type ToastPosition =
|
|
@@ -12,22 +13,37 @@ export interface Toast {
|
|
|
12
13
|
message: string
|
|
13
14
|
variant: ToastVariant
|
|
14
15
|
duration: number
|
|
16
|
+
loading?: boolean
|
|
15
17
|
action?: ToastAction
|
|
16
18
|
icon?: string
|
|
17
19
|
color?: string
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
let nextId = 1
|
|
23
|
+
let mounted = false
|
|
24
|
+
const timers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
21
25
|
|
|
22
26
|
const toasts = ref<Toast[]>([])
|
|
23
27
|
const position = ref<ToastPosition>('bottom-center')
|
|
24
28
|
|
|
29
|
+
function ensureMounted() {
|
|
30
|
+
if (mounted || typeof document === 'undefined') return
|
|
31
|
+
mounted = true
|
|
32
|
+
const container = document.createElement('div')
|
|
33
|
+
container.id = 'm3-toast-host'
|
|
34
|
+
document.body.appendChild(container)
|
|
35
|
+
createApp({ render: () => h(MSnackbar) }).mount(container)
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
function dismiss(id: number) {
|
|
39
|
+
const timer = timers.get(id)
|
|
40
|
+
if (timer) { clearTimeout(timer); timers.delete(id) }
|
|
26
41
|
toasts.value = toasts.value.filter((t) => t.id !== id)
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
export interface ToastOptions {
|
|
30
45
|
duration?: number
|
|
46
|
+
loading?: boolean
|
|
31
47
|
action?: ToastAction
|
|
32
48
|
icon?: string
|
|
33
49
|
color?: string
|
|
@@ -38,19 +54,40 @@ function show(
|
|
|
38
54
|
variant: ToastVariant = 'info',
|
|
39
55
|
options: number | ToastOptions = {},
|
|
40
56
|
) {
|
|
57
|
+
ensureMounted()
|
|
41
58
|
const id = nextId++
|
|
42
59
|
const opts = typeof options === 'number' ? { duration: options } : options
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
const isLoading = opts.loading ?? false
|
|
61
|
+
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 })
|
|
63
|
+
if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration))
|
|
46
64
|
return id
|
|
47
65
|
}
|
|
48
66
|
|
|
67
|
+
function update(id: number, patch: Partial<Omit<Toast, 'id'>>) {
|
|
68
|
+
const t = toasts.value.find((t) => t.id === id)
|
|
69
|
+
if (!t) return
|
|
70
|
+
Object.assign(t, patch)
|
|
71
|
+
if ('loading' in patch && !patch.loading) {
|
|
72
|
+
const timer = timers.get(id)
|
|
73
|
+
if (timer) clearTimeout(timer)
|
|
74
|
+
const dur = patch.duration ?? 4000
|
|
75
|
+
if (dur > 0) {
|
|
76
|
+
t.duration = dur
|
|
77
|
+
timers.set(id, setTimeout(() => dismiss(id), dur))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loading(msg: string, opts?: Omit<ToastOptions, 'loading'>) {
|
|
83
|
+
return show(msg, 'info', { ...opts, loading: true })
|
|
84
|
+
}
|
|
85
|
+
|
|
49
86
|
const success = (msg: string, opts?: ToastOptions) => show(msg, 'success', opts ?? {})
|
|
50
87
|
const error = (msg: string, opts?: ToastOptions) => show(msg, 'error', opts ?? {})
|
|
51
88
|
const warning = (msg: string, opts?: ToastOptions) => show(msg, 'warning', opts ?? {})
|
|
52
89
|
const info = (msg: string, opts?: ToastOptions) => show(msg, 'info', opts ?? {})
|
|
53
90
|
|
|
54
91
|
export function useToast() {
|
|
55
|
-
return { toasts, position, show, success, error, warning, info, dismiss }
|
|
92
|
+
return { toasts, position, show, success, error, warning, info, loading, update, dismiss }
|
|
56
93
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type { Toast, ToastVariant, ToastPosition, ToastAction, ToastOptions } fr
|
|
|
12
12
|
export { useFieldBg } from './composables/useFieldBg'
|
|
13
13
|
export { useLocale, defaultLocale } from './composables/useLocale'
|
|
14
14
|
export type { M3Locale } from './composables/useLocale'
|
|
15
|
+
export { useDevice } from './composables/useDevice'
|
|
16
|
+
export { useNotification } from './composables/useNotification'
|
|
17
|
+
export type { Notification, NotificationVariant, NotificationPosition, NotificationOptions } from './composables/useNotification'
|
|
15
18
|
|
|
16
19
|
// Components
|
|
17
20
|
export { default as MAbsolute } from './components/MAbsolute.vue'
|
|
@@ -26,22 +29,28 @@ export { default as MBottomSheet } from './components/MBottomSheet.vue'
|
|
|
26
29
|
export { default as MBox } from './components/MBox.vue'
|
|
27
30
|
export { default as MBreadcrumbs } from './components/MBreadcrumbs.vue'
|
|
28
31
|
export { default as MButton } from './components/MButton.vue'
|
|
32
|
+
export { default as MButtonGroup } from './components/MButtonGroup.vue'
|
|
33
|
+
export type { ButtonGroupOption } from './components/MButtonGroup.vue'
|
|
29
34
|
export { default as MCalendar } from './components/MCalendar.vue'
|
|
30
35
|
export { default as MCard } from './components/MCard.vue'
|
|
31
36
|
export { default as MCarousel } from './components/MCarousel.vue'
|
|
32
37
|
export { default as MCenter } from './components/MCenter.vue'
|
|
33
38
|
// MChart — import from '@m3ui-vue/m3ui-vue/chart'
|
|
39
|
+
export { default as MChatBubble } from './components/MChatBubble.vue'
|
|
34
40
|
export { default as MCheckbox } from './components/MCheckbox.vue'
|
|
35
41
|
export { default as MChip } from './components/MChip.vue'
|
|
36
42
|
// MCodeEditor — import from '@m3ui-vue/m3ui-vue/code-editor'
|
|
37
43
|
export { default as MColorPicker } from './components/MColorPicker.vue'
|
|
44
|
+
export { default as MColorPickerModal } from './components/MColorPickerModal.vue'
|
|
38
45
|
export { default as MCommandPalette } from './components/MCommandPalette.vue'
|
|
39
46
|
export { default as MConfirmDialog } from './components/MConfirmDialog.vue'
|
|
40
47
|
export { default as MContainer } from './components/MContainer.vue'
|
|
41
48
|
export { default as MContextMenu } from './components/MContextMenu.vue'
|
|
42
49
|
export { default as MDataTable } from './components/MDataTable.vue'
|
|
43
50
|
export { default as MDatePicker } from './components/MDatePicker.vue'
|
|
51
|
+
export { default as MDatePickerModal } from './components/MDatePickerModal.vue'
|
|
44
52
|
export { default as MDateRangePicker } from './components/MDateRangePicker.vue'
|
|
53
|
+
export { default as MDateRangePickerModal } from './components/MDateRangePickerModal.vue'
|
|
45
54
|
export { default as MDialog } from './components/MDialog.vue'
|
|
46
55
|
export { default as MDivider } from './components/MDivider.vue'
|
|
47
56
|
export { default as MDragDropList } from './components/MDragDropList.vue'
|
|
@@ -103,6 +112,8 @@ export { default as MSlider } from './components/MSlider.vue'
|
|
|
103
112
|
export { default as MSnackbar } from './components/MSnackbar.vue'
|
|
104
113
|
export { default as MSpacer } from './components/MSpacer.vue'
|
|
105
114
|
export { default as MSpinner } from './components/MSpinner.vue'
|
|
115
|
+
export { default as MSplitButton } from './components/MSplitButton.vue'
|
|
116
|
+
export type { SplitButtonItem } from './components/MSplitButton.vue'
|
|
106
117
|
export { default as MSplitter } from './components/MSplitter.vue'
|
|
107
118
|
export { default as MSpotlightSearch } from './components/MSpotlightSearch.vue'
|
|
108
119
|
export { default as MStack } from './components/MStack.vue'
|
|
@@ -119,8 +130,10 @@ export { default as MText } from './components/MText.vue'
|
|
|
119
130
|
export { default as MTextField } from './components/MTextField.vue'
|
|
120
131
|
export { default as MTimeline } from './components/MTimeline.vue'
|
|
121
132
|
export { default as MTimePicker } from './components/MTimePicker.vue'
|
|
133
|
+
export { default as MTimePickerModal } from './components/MTimePickerModal.vue'
|
|
122
134
|
export { default as MTitle } from './components/MTitle.vue'
|
|
123
135
|
export { default as MTooltip } from './components/MTooltip.vue'
|
|
136
|
+
export { default as MToolbar } from './components/MToolbar.vue'
|
|
124
137
|
export { default as MTopAppBar } from './components/MTopAppBar.vue'
|
|
125
138
|
export { default as MTour } from './components/MTour.vue'
|
|
126
139
|
export { default as MTransferList } from './components/MTransferList.vue'
|