@m3ui-vue/m3ui-vue 0.2.7 → 0.3.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.
@@ -0,0 +1,223 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useId } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+ import { useFieldBg } from '../composables/useFieldBg'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ modelValue: string[]
9
+ label?: string
10
+ placeholder?: string
11
+ variant?: 'filled' | 'outlined'
12
+ disabled?: boolean
13
+ error?: string
14
+ hint?: string
15
+ required?: boolean
16
+ leadingIcon?: string
17
+ fieldBg?: string
18
+ maxTags?: number
19
+ duplicates?: boolean
20
+ clearable?: boolean
21
+ }>(),
22
+ {
23
+ modelValue: () => [],
24
+ variant: 'filled',
25
+ disabled: false,
26
+ required: false,
27
+ duplicates: false,
28
+ clearable: false,
29
+ },
30
+ )
31
+
32
+ const emit = defineEmits<{ 'update:modelValue': [string[]] }>()
33
+
34
+ const id = useId()
35
+ const focused = ref(false)
36
+ const inputValue = ref('')
37
+ const fieldEl = ref<HTMLElement | null>(null)
38
+ const inputEl = ref<HTMLInputElement | null>(null)
39
+ const { resolvedFieldBg } = useFieldBg(fieldEl, () => props.fieldBg)
40
+
41
+ const hasValue = computed(() => props.modelValue.length > 0)
42
+ const canAddMore = computed(() => props.maxTags == null || props.modelValue.length < props.maxTags)
43
+
44
+ function addTag(raw: string) {
45
+ const tag = raw.trim()
46
+ if (!tag) return
47
+ if (!props.duplicates && props.modelValue.includes(tag)) return
48
+ if (!canAddMore.value) return
49
+ emit('update:modelValue', [...props.modelValue, tag])
50
+ }
51
+
52
+ function removeTag(index: number, e?: Event) {
53
+ e?.stopPropagation()
54
+ const next = [...props.modelValue]
55
+ next.splice(index, 1)
56
+ emit('update:modelValue', next)
57
+ }
58
+
59
+ function onKeydown(e: KeyboardEvent) {
60
+ if (e.key === 'Enter' || e.key === ',') {
61
+ e.preventDefault()
62
+ addTag(inputValue.value)
63
+ inputValue.value = ''
64
+ } else if (e.key === 'Backspace' && inputValue.value === '' && props.modelValue.length > 0) {
65
+ removeTag(props.modelValue.length - 1)
66
+ }
67
+ }
68
+
69
+ function onInput(e: Event) {
70
+ const value = (e.target as HTMLInputElement).value
71
+ // Handle comma typed via input event (covers paste with commas too)
72
+ if (value.includes(',')) {
73
+ const parts = value.split(',')
74
+ for (const part of parts.slice(0, -1)) {
75
+ addTag(part)
76
+ }
77
+ inputValue.value = parts[parts.length - 1] ?? ''
78
+ } else {
79
+ inputValue.value = value
80
+ }
81
+ }
82
+
83
+ function onPaste(e: ClipboardEvent) {
84
+ const pasted = e.clipboardData?.getData('text') ?? ''
85
+ if (pasted.includes(',')) {
86
+ e.preventDefault()
87
+ const parts = pasted.split(',')
88
+ for (const part of parts) {
89
+ addTag(part)
90
+ }
91
+ }
92
+ }
93
+
94
+ function focusInput() {
95
+ if (!props.disabled) {
96
+ inputEl.value?.focus()
97
+ }
98
+ }
99
+
100
+ const triggerClasses = computed(() => {
101
+ const base = [
102
+ 'flex min-h-[56px] w-full cursor-text items-center gap-1.5 flex-wrap',
103
+ 'transition-[border-color,border-width] duration-150',
104
+ props.leadingIcon ? 'pl-12 pr-10' : 'pl-4 pr-10',
105
+ ]
106
+
107
+ if (props.variant === 'outlined') {
108
+ return [
109
+ ...base,
110
+ 'rounded-sm border bg-transparent py-2',
111
+ focused.value
112
+ ? (props.error ? 'border-2 border-error' : 'border-2 border-primary')
113
+ : (props.error ? 'border-error' : 'border-outline hover:border-on-surface'),
114
+ ].join(' ')
115
+ }
116
+
117
+ return [
118
+ ...base,
119
+ 'rounded-t-sm bg-surface-container-highest border-b pb-2',
120
+ hasValue.value || focused.value ? 'pt-7' : 'pt-4',
121
+ focused.value
122
+ ? (props.error ? 'border-b-2 border-error' : 'border-b-2 border-primary')
123
+ : (props.error ? 'border-error' : 'border-on-surface-variant hover:border-on-surface'),
124
+ ].join(' ')
125
+ })
126
+
127
+ const labelClasses = computed(() => {
128
+ const left = props.leadingIcon
129
+ ? (props.variant === 'outlined' ? 'left-11' : 'left-12')
130
+ : (props.variant === 'outlined' ? 'left-3' : 'left-4')
131
+
132
+ const floated = props.variant === 'outlined'
133
+ ? '-top-2.5 translate-y-0 text-label-small bg-[var(--field-bg)] px-1 right-auto max-w-[calc(100%-1.5rem)]'
134
+ : 'top-2 translate-y-0 text-label-small'
135
+
136
+ const unFloated = props.variant === 'filled'
137
+ ? 'top-[53%] -translate-y-1/2 text-body-large'
138
+ : 'top-1/2 -translate-y-1/2 text-body-large'
139
+ const active = focused.value || hasValue.value
140
+
141
+ return [
142
+ 'pointer-events-none absolute right-10 truncate transition-all duration-200',
143
+ left,
144
+ active ? floated : unFloated,
145
+ focused.value
146
+ ? (props.error ? 'text-error' : 'text-primary')
147
+ : (props.error ? 'text-error' : 'text-on-surface-variant'),
148
+ ].join(' ')
149
+ })
150
+ </script>
151
+
152
+ <template>
153
+ <div class="flex flex-col gap-1">
154
+ <div
155
+ ref="fieldEl"
156
+ class="relative"
157
+ :class="variant === 'outlined' ? 'mt-2' : ''"
158
+ :style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
159
+ >
160
+ <div
161
+ v-if="leadingIcon"
162
+ class="pointer-events-none absolute left-3.5 text-on-surface-variant"
163
+ :class="variant === 'filled' ? 'top-5' : 'top-4.5'"
164
+ >
165
+ <MIcon :name="leadingIcon" :size="20" />
166
+ </div>
167
+
168
+ <!-- Trigger field -->
169
+ <div
170
+ :id="id"
171
+ :class="triggerClasses"
172
+ @click="focusInput"
173
+ >
174
+ <span
175
+ v-for="(tag, i) in modelValue"
176
+ :key="i"
177
+ class="inline-flex items-center gap-1 rounded-full bg-secondary-container px-2 py-0.5 text-label-small text-on-secondary-container"
178
+ >
179
+ {{ tag }}
180
+ <button
181
+ v-if="!disabled"
182
+ type="button"
183
+ class="flex h-4 w-4 items-center justify-center rounded-full hover:bg-on-secondary-container/20"
184
+ @click="removeTag(i, $event)"
185
+ >
186
+ <MIcon name="close" :size="12" />
187
+ </button>
188
+ </span>
189
+
190
+ <input
191
+ ref="inputEl"
192
+ :value="inputValue"
193
+ type="text"
194
+ class="min-w-[60px] flex-1 bg-transparent text-body-large text-on-surface outline-none placeholder:text-on-surface-variant"
195
+ :placeholder="!hasValue && (!label || focused) ? placeholder : ''"
196
+ :disabled="disabled"
197
+ :readonly="!canAddMore"
198
+ @input="onInput"
199
+ @keydown="onKeydown"
200
+ @paste="onPaste"
201
+ @focus="focused = true"
202
+ @blur="focused = false"
203
+ />
204
+ </div>
205
+
206
+ <label :class="labelClasses">
207
+ {{ label }}<span v-if="required" class="text-error">&nbsp;*</span>
208
+ </label>
209
+
210
+ <button
211
+ v-if="clearable && hasValue && !disabled"
212
+ type="button"
213
+ class="absolute right-2 top-4 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-on-surface/8 hover:text-on-surface"
214
+ @click.stop="emit('update:modelValue', []); inputValue = ''"
215
+ >
216
+ <MIcon name="close" :size="18" />
217
+ </button>
218
+ </div>
219
+
220
+ <p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
221
+ <p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
222
+ </div>
223
+ </template>
@@ -136,7 +136,7 @@ function onInput(event: Event) {
136
136
  <div
137
137
  v-if="leadingIcon"
138
138
  class="pointer-events-none absolute left-3.5 text-on-surface-variant"
139
- :class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'"
139
+ :class="multiline ? 'top-[55%] -translate-y-1/2' : variant === 'filled' ? 'top-5' : 'top-4.5'"
140
140
  >
141
141
  <MIcon :name="leadingIcon" :size="20" />
142
142
  </div>
@@ -169,14 +169,13 @@ function onInput(event: Event) {
169
169
  {{ label }}<span v-if="required" class="text-error">&nbsp;*</span>
170
170
  </label>
171
171
 
172
- <div v-if="$slots.trailing" class="absolute right-2" :class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'">
172
+ <div v-if="$slots.trailing" class="absolute right-2 top-1/2 -translate-y-1/2">
173
173
  <slot name="trailing" />
174
174
  </div>
175
175
  <button
176
176
  v-else-if="showClear"
177
177
  type="button"
178
- class="absolute right-3 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-on-surface/8 hover:text-on-surface"
179
- :class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'"
178
+ class="absolute right-3 top-1/2 -translate-y-1/2 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-on-surface/8 hover:text-on-surface"
180
179
  @click="emit('update:modelValue', '')"
181
180
  >
182
181
  <MIcon name="close" :size="18" />
@@ -0,0 +1,330 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ modelValue: boolean
8
+ title?: string
9
+ icon?: string
10
+ width?: string
11
+ height?: string
12
+ minWidth?: number
13
+ minHeight?: number
14
+ maxWidth?: number
15
+ maxHeight?: number
16
+ resizable?: boolean
17
+ draggable?: boolean
18
+ closable?: boolean
19
+ minimizable?: boolean
20
+ x?: number
21
+ y?: number
22
+ resetPosition?: boolean
23
+ }>(),
24
+ {
25
+ width: '400px',
26
+ height: '300px',
27
+ minWidth: 200,
28
+ minHeight: 150,
29
+ resizable: true,
30
+ draggable: true,
31
+ closable: true,
32
+ minimizable: true,
33
+ x: 50,
34
+ y: 50,
35
+ resetPosition: false,
36
+ },
37
+ )
38
+
39
+ const emit = defineEmits<{
40
+ 'update:modelValue': [value: boolean]
41
+ close: []
42
+ minimize: []
43
+ }>()
44
+
45
+ // ---- State ----
46
+ const posX = ref(props.x)
47
+ const posY = ref(props.y)
48
+ const winWidth = ref(parseInt(props.width) || 400)
49
+ const winHeight = ref(parseInt(props.height) || 300)
50
+ const minimized = ref(false)
51
+ const zIndex = ref(100)
52
+
53
+ // Global z-index counter shared across instances
54
+ let globalZIndex = 100
55
+
56
+ const windowRef = ref<HTMLElement | null>(null)
57
+
58
+ // ---- Drag state ----
59
+ let isDragging = false
60
+ let dragStartX = 0
61
+ let dragStartY = 0
62
+ let dragStartPosX = 0
63
+ let dragStartPosY = 0
64
+
65
+ // ---- Resize state ----
66
+ type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
67
+ let isResizing = false
68
+ let resizeDir: ResizeDirection = 'se'
69
+ let resizeStartX = 0
70
+ let resizeStartY = 0
71
+ let resizeStartW = 0
72
+ let resizeStartH = 0
73
+ let resizeStartPosX = 0
74
+ let resizeStartPosY = 0
75
+
76
+ // ---- Computed ----
77
+ const windowStyle = computed(() => ({
78
+ left: `${posX.value}px`,
79
+ top: `${posY.value}px`,
80
+ width: `${winWidth.value}px`,
81
+ height: minimized.value ? 'auto' : `${winHeight.value}px`,
82
+ zIndex: zIndex.value,
83
+ }))
84
+
85
+ // ---- Bring to front ----
86
+ function bringToFront() {
87
+ globalZIndex++
88
+ zIndex.value = globalZIndex
89
+ }
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
+
114
+ // ---- Drag ----
115
+ function onTitleBarStart(e: MouseEvent | TouchEvent) {
116
+ if (!props.draggable) return
117
+ if ((e.target as HTMLElement).closest('button')) return
118
+ e.preventDefault()
119
+ const { x, y } = getXY(e)
120
+ isDragging = true
121
+ dragStartX = x
122
+ dragStartY = y
123
+ dragStartPosX = posX.value
124
+ dragStartPosY = posY.value
125
+ bringToFront()
126
+ addListeners()
127
+ }
128
+
129
+ // ---- Resize ----
130
+ function onResizeStart(e: MouseEvent | TouchEvent, direction: ResizeDirection) {
131
+ if (!props.resizable) return
132
+ e.preventDefault()
133
+ e.stopPropagation()
134
+ const { x, y } = getXY(e)
135
+ isResizing = true
136
+ resizeDir = direction
137
+ resizeStartX = x
138
+ resizeStartY = y
139
+ resizeStartW = winWidth.value
140
+ resizeStartH = winHeight.value
141
+ resizeStartPosX = posX.value
142
+ resizeStartPosY = posY.value
143
+ bringToFront()
144
+ addListeners()
145
+ }
146
+
147
+ function onPointerMove(e: MouseEvent | TouchEvent) {
148
+ e.preventDefault()
149
+ const { x, y } = getXY(e)
150
+
151
+ if (isDragging) {
152
+ const dx = x - dragStartX
153
+ const dy = y - dragStartY
154
+ const parent = windowRef.value?.parentElement
155
+ const maxX = parent ? parent.clientWidth - winWidth.value : Infinity
156
+ const maxY = parent ? parent.clientHeight - (minimized.value ? 40 : winHeight.value) : Infinity
157
+ posX.value = Math.max(0, Math.min(maxX, dragStartPosX + dx))
158
+ posY.value = Math.max(0, Math.min(maxY, dragStartPosY + dy))
159
+ }
160
+
161
+ if (isResizing) {
162
+ const dx = x - resizeStartX
163
+ const dy = y - resizeStartY
164
+ const dir = resizeDir
165
+ const parent = windowRef.value?.parentElement
166
+ const parentW = parent ? parent.clientWidth : Infinity
167
+ const parentH = parent ? parent.clientHeight : Infinity
168
+
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
+
172
+ if (dir.includes('e')) {
173
+ winWidth.value = Math.min(parentW - resizeStartPosX, clampW(resizeStartW + dx))
174
+ }
175
+ if (dir.includes('w')) {
176
+ const newW = Math.min(resizeStartPosX + resizeStartW, clampW(resizeStartW - dx))
177
+ posX.value = Math.max(0, resizeStartPosX + (resizeStartW - newW))
178
+ winWidth.value = newW
179
+ }
180
+ if (dir.includes('s')) {
181
+ winHeight.value = Math.min(parentH - resizeStartPosY, clampH(resizeStartH + dy))
182
+ }
183
+ }
184
+ }
185
+
186
+ function onPointerUp() {
187
+ isDragging = false
188
+ isResizing = false
189
+ removeListeners()
190
+ }
191
+
192
+ // ---- Actions ----
193
+ function close() {
194
+ emit('update:modelValue', false)
195
+ emit('close')
196
+ if (props.resetPosition) {
197
+ posX.value = props.x
198
+ posY.value = props.y
199
+ winWidth.value = parseInt(props.width) || 400
200
+ winHeight.value = parseInt(props.height) || 300
201
+ minimized.value = false
202
+ }
203
+ }
204
+
205
+ function toggleMinimize() {
206
+ minimized.value = !minimized.value
207
+ emit('minimize')
208
+ }
209
+
210
+ // ---- Focus on click ----
211
+ function onWindowMouseDown() {
212
+ bringToFront()
213
+ }
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
+
238
+ // ---- Sync initial props ----
239
+ watch(
240
+ () => props.x,
241
+ (v) => { posX.value = v },
242
+ )
243
+ watch(
244
+ () => props.y,
245
+ (v) => { posY.value = v },
246
+ )
247
+ watch(
248
+ () => props.modelValue,
249
+ (v) => { if (v) nextTick(clampToParent) },
250
+ )
251
+
252
+ // ---- Cleanup ----
253
+ onUnmounted(() => {
254
+ removeListeners()
255
+ resizeObserver?.disconnect()
256
+ })
257
+
258
+ // Resize handle definitions
259
+ const resizeHandles: { dir: ResizeDirection; class: string; cursor: string }[] = [
260
+ { dir: 's', class: 'bottom-0 left-[10px] right-[10px] h-[5px]', cursor: 'cursor-ns-resize' },
261
+ { dir: 'e', class: 'top-[10px] right-0 bottom-[10px] w-[5px]', cursor: 'cursor-ew-resize' },
262
+ { dir: 'w', class: 'top-[10px] left-0 bottom-[10px] w-[5px]', cursor: 'cursor-ew-resize' },
263
+ { dir: 'se', class: 'bottom-0 right-0 w-[12px] h-[12px]', cursor: 'cursor-nwse-resize' },
264
+ { dir: 'sw', class: 'bottom-0 left-0 w-[12px] h-[12px]', cursor: 'cursor-nesw-resize' },
265
+ ]
266
+ </script>
267
+
268
+ <template>
269
+ <div
270
+ v-if="modelValue"
271
+ ref="windowRef"
272
+ class="absolute flex flex-col rounded-xl bg-surface shadow-elevation-3 overflow-hidden"
273
+ :style="windowStyle"
274
+ @mousedown="onWindowMouseDown"
275
+ >
276
+ <!-- Title bar -->
277
+ <div
278
+ class="flex items-center gap-2 px-4 h-10 bg-surface-container select-none shrink-0"
279
+ :class="{ 'cursor-move': draggable }"
280
+ @mousedown="onTitleBarStart"
281
+ @touchstart="onTitleBarStart"
282
+ >
283
+ <slot name="header">
284
+ <MIcon v-if="icon" :name="icon" :size="18" class="text-on-surface-variant" />
285
+ <span class="text-title-small font-medium text-on-surface truncate flex-1">
286
+ {{ title }}
287
+ </span>
288
+ </slot>
289
+
290
+ <slot name="actions" />
291
+
292
+ <button
293
+ v-if="minimizable"
294
+ class="text-on-surface-variant hover:bg-on-surface/8 rounded-full w-7 h-7 flex items-center justify-center transition-colors"
295
+ @click="toggleMinimize"
296
+ >
297
+ <MIcon :name="minimized ? 'open_in_full' : 'minimize'" :size="16" />
298
+ </button>
299
+
300
+ <button
301
+ v-if="closable"
302
+ class="text-on-surface-variant hover:bg-error/12 hover:text-error rounded-full w-7 h-7 flex items-center justify-center transition-colors"
303
+ @click="close"
304
+ >
305
+ <MIcon name="close" :size="16" />
306
+ </button>
307
+ </div>
308
+
309
+ <!-- Body -->
310
+ <div
311
+ v-show="!minimized"
312
+ class="flex-1 overflow-auto"
313
+ >
314
+ <slot />
315
+ </div>
316
+
317
+ <!-- Resize handles -->
318
+ <template v-if="resizable && !minimized">
319
+ <div
320
+ v-for="handle in resizeHandles"
321
+ :key="handle.dir"
322
+ class="absolute"
323
+ :class="[handle.class, handle.cursor]"
324
+ @mousedown="onResizeStart($event, handle.dir)"
325
+ @touchstart="onResizeStart($event, handle.dir)"
326
+ />
327
+ </template>
328
+ </div>
329
+ </template>
330
+
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export type { M3Locale } from './composables/useLocale'
16
16
  // Components
17
17
  export { default as MAbsolute } from './components/MAbsolute.vue'
18
18
  export { default as MAlert } from './components/MAlert.vue'
19
+ export { default as MAutocomplete } from './components/MAutocomplete.vue'
19
20
  export { default as MAppBar } from './components/MAppBar.vue'
20
21
  export { default as MAppLayout } from './components/MAppLayout.vue'
21
22
  export { default as MAspectRatio } from './components/MAspectRatio.vue'
@@ -30,6 +31,7 @@ export { default as MCard } from './components/MCard.vue'
30
31
  export { default as MCarousel } from './components/MCarousel.vue'
31
32
  export { default as MCenter } from './components/MCenter.vue'
32
33
  // MChart — import from '@m3ui-vue/m3ui-vue/chart'
34
+ export { default as MChatBubble } from './components/MChatBubble.vue'
33
35
  export { default as MCheckbox } from './components/MCheckbox.vue'
34
36
  export { default as MChip } from './components/MChip.vue'
35
37
  // MCodeEditor — import from '@m3ui-vue/m3ui-vue/code-editor'
@@ -73,6 +75,7 @@ export { default as MMenu } from './components/MMenu.vue'
73
75
  export { default as MMenuItem } from './components/MMenuItem.vue'
74
76
  export { default as MMaskField } from './components/MMaskField.vue'
75
77
  export type { MaskPreset } from './components/MMaskField.vue'
78
+ export { default as MMultiAutocomplete } from './components/MMultiAutocomplete.vue'
76
79
  export { default as MMultiSelect } from './components/MMultiSelect.vue'
77
80
  export type { MultiSelectOption } from './components/MMultiSelect.vue'
78
81
  export { default as MNavigationBar } from './components/MNavigationBar.vue'
@@ -110,6 +113,7 @@ export { default as MSticky } from './components/MSticky.vue'
110
113
  export { default as MSubtitle } from './components/MSubtitle.vue'
111
114
  export { default as MSwitch } from './components/MSwitch.vue'
112
115
  export { default as MTable } from './components/MTable.vue'
116
+ export { default as MTagInput } from './components/MTagInput.vue'
113
117
  export { default as MTabs } from './components/MTabs.vue'
114
118
  // MTerminal — import from '@m3ui-vue/m3ui-vue/terminal'
115
119
  export { default as MText } from './components/MText.vue'
@@ -124,6 +128,7 @@ export { default as MTransferList } from './components/MTransferList.vue'
124
128
  export { default as MTree } from './components/MTree.vue'
125
129
  export { default as MTreeTable } from './components/MTreeTable.vue'
126
130
  export { default as MVirtualTable } from './components/MVirtualTable.vue'
131
+ export { default as MWindow } from './components/MWindow.vue'
127
132
 
128
133
  // Re-export types from components that define them
129
134
  export type { EmojiCategory } from './data/emojis'