@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.
- package/dist/{MMenuItem-DpoEsH91.js → MMenuItem-C6RwHl-l.js} +14 -14
- package/dist/{MMenuItem-DpoEsH91.js.map → MMenuItem-C6RwHl-l.js.map} +1 -1
- package/dist/components/MAutocomplete.vue.d.ts +29 -0
- package/dist/components/MChatBubble.vue.d.ts +32 -0
- package/dist/components/MMultiAutocomplete.vue.d.ts +33 -0
- package/dist/components/MMultiSelect.vue.d.ts +5 -3
- package/dist/components/MNavigationDrawer.vue.d.ts +1 -1
- package/dist/components/MSlider.vue.d.ts +1 -1
- package/dist/components/MTagInput.vue.d.ts +29 -0
- package/dist/components/MTextField.vue.d.ts +1 -1
- package/dist/components/MWindow.vue.d.ts +55 -0
- package/dist/index.d.ts +5 -0
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +1975 -973
- 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/MAppBar.vue +17 -13
- package/src/components/MAutocomplete.vue +373 -0
- package/src/components/MChatBubble.vue +98 -0
- package/src/components/MMaskField.vue +2 -3
- package/src/components/MMultiAutocomplete.vue +446 -0
- package/src/components/MMultiSelect.vue +38 -9
- package/src/components/MNumberField.vue +2 -2
- package/src/components/MSelect.vue +4 -4
- package/src/components/MTagInput.vue +223 -0
- package/src/components/MTextField.vue +3 -4
- package/src/components/MWindow.vue +330 -0
- package/src/index.ts +5 -0
|
@@ -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"> *</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="
|
|
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"> *</span>
|
|
170
170
|
</label>
|
|
171
171
|
|
|
172
|
-
<div v-if="$slots.trailing" class="absolute right-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'
|