@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.
Files changed (84) hide show
  1. package/dist/{MMenuItem-C6RwHl-l.js → MMenuItem-FyttjOj_.js} +399 -207
  2. package/dist/MMenuItem-FyttjOj_.js.map +1 -0
  3. package/dist/components/MAutocomplete.vue.d.ts +2 -0
  4. package/dist/components/MBox.vue.d.ts +2 -2
  5. package/dist/components/MButton.vue.d.ts +4 -0
  6. package/dist/components/MButtonGroup.vue.d.ts +28 -0
  7. package/dist/components/MCarousel.vue.d.ts +21 -12
  8. package/dist/components/MChatBubble.vue.d.ts +32 -0
  9. package/dist/components/MColorPickerModal.vue.d.ts +18 -0
  10. package/dist/components/MDataTable.vue.d.ts +1 -1
  11. package/dist/components/MDatePickerModal.vue.d.ts +21 -0
  12. package/dist/components/MDateRangePickerModal.vue.d.ts +24 -0
  13. package/dist/components/MDialog.vue.d.ts +9 -1
  14. package/dist/components/MExpansionPanel.vue.d.ts +3 -2
  15. package/dist/components/MFab.vue.d.ts +14 -2
  16. package/dist/components/MFlex.vue.d.ts +2 -2
  17. package/dist/components/MIconButton.vue.d.ts +4 -2
  18. package/dist/components/MInfiniteScroll.vue.d.ts +1 -1
  19. package/dist/components/MList.vue.d.ts +2 -0
  20. package/dist/components/MLoadingOverlay.vue.d.ts +1 -1
  21. package/dist/components/MMenu.vue.d.ts +3 -1
  22. package/dist/components/MMenuItem.vue.d.ts +14 -3
  23. package/dist/components/MMultiAutocomplete.vue.d.ts +2 -0
  24. package/dist/components/MMultiSelect.vue.d.ts +2 -0
  25. package/dist/components/MNotificationHost.vue.d.ts +3 -0
  26. package/dist/components/MSelect.vue.d.ts +2 -0
  27. package/dist/components/MSlider.vue.d.ts +15 -4
  28. package/dist/components/MSplitButton.vue.d.ts +39 -0
  29. package/dist/components/MSplitter.vue.d.ts +1 -1
  30. package/dist/components/MStack.vue.d.ts +2 -2
  31. package/dist/components/MTable.vue.d.ts +1 -1
  32. package/dist/components/MTimePickerModal.vue.d.ts +21 -0
  33. package/dist/components/MToolbar.vue.d.ts +30 -0
  34. package/dist/components/MTooltip.vue.d.ts +8 -2
  35. package/dist/components/MWindow.vue.d.ts +4 -2
  36. package/dist/composables/useDevice.d.ts +3 -0
  37. package/dist/composables/useNotification.d.ts +64 -0
  38. package/dist/composables/useToast.d.ts +8 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/m3ui-vue.css +1 -1
  41. package/dist/m3ui.js +5231 -2916
  42. package/dist/m3ui.js.map +1 -1
  43. package/dist/rich-text-editor.js +10 -10
  44. package/dist/styles.css +1 -1
  45. package/package.json +1 -1
  46. package/src/components/MAutocomplete.vue +108 -13
  47. package/src/components/MButton.vue +38 -16
  48. package/src/components/MButtonGroup.vue +158 -0
  49. package/src/components/MCarousel.vue +223 -101
  50. package/src/components/MChatBubble.vue +98 -0
  51. package/src/components/MColorPicker.vue +1 -4
  52. package/src/components/MColorPickerModal.vue +276 -0
  53. package/src/components/MDatePicker.vue +1 -4
  54. package/src/components/MDatePickerModal.vue +265 -0
  55. package/src/components/MDateRangePicker.vue +1 -4
  56. package/src/components/MDateRangePickerModal.vue +264 -0
  57. package/src/components/MDialog.vue +38 -1
  58. package/src/components/MExpansionPanel.vue +10 -32
  59. package/src/components/MFab.vue +92 -11
  60. package/src/components/MIconButton.vue +36 -6
  61. package/src/components/MList.vue +6 -3
  62. package/src/components/MListItem.vue +9 -3
  63. package/src/components/MMenu.vue +10 -5
  64. package/src/components/MMenuItem.vue +93 -10
  65. package/src/components/MMultiAutocomplete.vue +104 -10
  66. package/src/components/MMultiSelect.vue +106 -13
  67. package/src/components/MNotificationHost.vue +163 -0
  68. package/src/components/MSelect.vue +99 -16
  69. package/src/components/MSlider.vue +367 -134
  70. package/src/components/MSnackbar.vue +3 -0
  71. package/src/components/MSplitButton.vue +240 -0
  72. package/src/components/MTabs.vue +82 -39
  73. package/src/components/MTagInput.vue +11 -1
  74. package/src/components/MTimePicker.vue +1 -4
  75. package/src/components/MTimePickerModal.vue +458 -0
  76. package/src/components/MToolbar.vue +75 -0
  77. package/src/components/MTooltip.vue +60 -10
  78. package/src/components/MWindow.vue +83 -48
  79. package/src/components/_MContextMenuPanel.vue +2 -2
  80. package/src/composables/useDevice.ts +29 -0
  81. package/src/composables/useNotification.ts +93 -0
  82. package/src/composables/useToast.ts +42 -5
  83. package/src/index.ts +13 -0
  84. 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 onTitleBarMouseDown(e: MouseEvent) {
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 = e.clientX
97
- dragStartY = e.clientY
121
+ dragStartX = x
122
+ dragStartY = y
98
123
  dragStartPosX = posX.value
99
124
  dragStartPosY = posY.value
100
125
  bringToFront()
101
- document.addEventListener('mousemove', onMouseMove)
102
- document.addEventListener('mouseup', onMouseUp)
126
+ addListeners()
103
127
  }
104
128
 
105
129
  // ---- Resize ----
106
- function onResizeMouseDown(e: MouseEvent, direction: ResizeDirection) {
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 = e.clientX
113
- resizeStartY = e.clientY
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
- document.addEventListener('mousemove', onMouseMove)
120
- document.addEventListener('mouseup', onMouseUp)
144
+ addListeners()
121
145
  }
122
146
 
123
- function onMouseMove(e: MouseEvent) {
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 = e.clientX - dragStartX
128
- const dy = e.clientY - dragStartY
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 = e.clientX - resizeStartX
138
- const dy = e.clientY - resizeStartY
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
- // East
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
- const maxW = parentW - resizeStartPosX
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 maxW = resizeStartPosX + resizeStartW
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
- const maxH = parentH - resizeStartPosY
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 onMouseUp() {
186
+ function onPointerUp() {
172
187
  isDragging = false
173
188
  isResizing = false
174
- document.removeEventListener('mousemove', onMouseMove)
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
- document.removeEventListener('mousemove', onMouseMove)
214
- document.removeEventListener('mouseup', onMouseUp)
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="onTitleBarMouseDown"
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="onResizeMouseDown($event, handle.dir)"
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-sm bg-surface-container shadow-elevation-2"
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="overflow-hidden rounded-sm py-1">
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 duration = opts.duration ?? (variant === 'error' ? 6000 : 4000)
44
- toasts.value.push({ id, message, variant, duration, action: opts.action, icon: opts.icon, color: opts.color })
45
- if (duration > 0) setTimeout(() => dismiss(id), duration)
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'