@m3ui-vue/m3ui-vue 0.2.7 → 0.3.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-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/MMultiAutocomplete.vue.d.ts +33 -0
- package/dist/components/MMultiSelect.vue.d.ts +5 -3
- 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 +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +1897 -984
- 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/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 +295 -0
- package/src/index.ts +4 -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,295 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted, watch } 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
|
+
resizable?: boolean
|
|
15
|
+
draggable?: boolean
|
|
16
|
+
closable?: boolean
|
|
17
|
+
minimizable?: boolean
|
|
18
|
+
x?: number
|
|
19
|
+
y?: number
|
|
20
|
+
resetPosition?: boolean
|
|
21
|
+
}>(),
|
|
22
|
+
{
|
|
23
|
+
width: '400px',
|
|
24
|
+
height: '300px',
|
|
25
|
+
minWidth: 200,
|
|
26
|
+
minHeight: 150,
|
|
27
|
+
resizable: true,
|
|
28
|
+
draggable: true,
|
|
29
|
+
closable: true,
|
|
30
|
+
minimizable: true,
|
|
31
|
+
x: 50,
|
|
32
|
+
y: 50,
|
|
33
|
+
resetPosition: false,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{
|
|
38
|
+
'update:modelValue': [value: boolean]
|
|
39
|
+
close: []
|
|
40
|
+
minimize: []
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
// ---- State ----
|
|
44
|
+
const posX = ref(props.x)
|
|
45
|
+
const posY = ref(props.y)
|
|
46
|
+
const winWidth = ref(parseInt(props.width) || 400)
|
|
47
|
+
const winHeight = ref(parseInt(props.height) || 300)
|
|
48
|
+
const minimized = ref(false)
|
|
49
|
+
const zIndex = ref(100)
|
|
50
|
+
|
|
51
|
+
// Global z-index counter shared across instances
|
|
52
|
+
let globalZIndex = 100
|
|
53
|
+
|
|
54
|
+
const windowRef = ref<HTMLElement | null>(null)
|
|
55
|
+
|
|
56
|
+
// ---- Drag state ----
|
|
57
|
+
let isDragging = false
|
|
58
|
+
let dragStartX = 0
|
|
59
|
+
let dragStartY = 0
|
|
60
|
+
let dragStartPosX = 0
|
|
61
|
+
let dragStartPosY = 0
|
|
62
|
+
|
|
63
|
+
// ---- Resize state ----
|
|
64
|
+
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
|
|
65
|
+
let isResizing = false
|
|
66
|
+
let resizeDir: ResizeDirection = 'se'
|
|
67
|
+
let resizeStartX = 0
|
|
68
|
+
let resizeStartY = 0
|
|
69
|
+
let resizeStartW = 0
|
|
70
|
+
let resizeStartH = 0
|
|
71
|
+
let resizeStartPosX = 0
|
|
72
|
+
let resizeStartPosY = 0
|
|
73
|
+
|
|
74
|
+
// ---- Computed ----
|
|
75
|
+
const windowStyle = computed(() => ({
|
|
76
|
+
left: `${posX.value}px`,
|
|
77
|
+
top: `${posY.value}px`,
|
|
78
|
+
width: `${winWidth.value}px`,
|
|
79
|
+
height: minimized.value ? 'auto' : `${winHeight.value}px`,
|
|
80
|
+
zIndex: zIndex.value,
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
// ---- Bring to front ----
|
|
84
|
+
function bringToFront() {
|
|
85
|
+
globalZIndex++
|
|
86
|
+
zIndex.value = globalZIndex
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- Drag ----
|
|
90
|
+
function onTitleBarMouseDown(e: MouseEvent) {
|
|
91
|
+
if (!props.draggable) return
|
|
92
|
+
// Ignore clicks on buttons inside the title bar
|
|
93
|
+
if ((e.target as HTMLElement).closest('button')) return
|
|
94
|
+
e.preventDefault()
|
|
95
|
+
isDragging = true
|
|
96
|
+
dragStartX = e.clientX
|
|
97
|
+
dragStartY = e.clientY
|
|
98
|
+
dragStartPosX = posX.value
|
|
99
|
+
dragStartPosY = posY.value
|
|
100
|
+
bringToFront()
|
|
101
|
+
document.addEventListener('mousemove', onMouseMove)
|
|
102
|
+
document.addEventListener('mouseup', onMouseUp)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- Resize ----
|
|
106
|
+
function onResizeMouseDown(e: MouseEvent, direction: ResizeDirection) {
|
|
107
|
+
if (!props.resizable) return
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
e.stopPropagation()
|
|
110
|
+
isResizing = true
|
|
111
|
+
resizeDir = direction
|
|
112
|
+
resizeStartX = e.clientX
|
|
113
|
+
resizeStartY = e.clientY
|
|
114
|
+
resizeStartW = winWidth.value
|
|
115
|
+
resizeStartH = winHeight.value
|
|
116
|
+
resizeStartPosX = posX.value
|
|
117
|
+
resizeStartPosY = posY.value
|
|
118
|
+
bringToFront()
|
|
119
|
+
document.addEventListener('mousemove', onMouseMove)
|
|
120
|
+
document.addEventListener('mouseup', onMouseUp)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onMouseMove(e: MouseEvent) {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
|
|
126
|
+
if (isDragging) {
|
|
127
|
+
const dx = e.clientX - dragStartX
|
|
128
|
+
const dy = e.clientY - dragStartY
|
|
129
|
+
const parent = windowRef.value?.parentElement
|
|
130
|
+
const maxX = parent ? parent.clientWidth - winWidth.value : Infinity
|
|
131
|
+
const maxY = parent ? parent.clientHeight - (minimized.value ? 40 : winHeight.value) : Infinity
|
|
132
|
+
posX.value = Math.max(0, Math.min(maxX, dragStartPosX + dx))
|
|
133
|
+
posY.value = Math.max(0, Math.min(maxY, dragStartPosY + dy))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isResizing) {
|
|
137
|
+
const dx = e.clientX - resizeStartX
|
|
138
|
+
const dy = e.clientY - resizeStartY
|
|
139
|
+
const dir = resizeDir
|
|
140
|
+
const parent = windowRef.value?.parentElement
|
|
141
|
+
const parentW = parent ? parent.clientWidth : Infinity
|
|
142
|
+
const parentH = parent ? parent.clientHeight : Infinity
|
|
143
|
+
|
|
144
|
+
// East
|
|
145
|
+
if (dir.includes('e')) {
|
|
146
|
+
const maxW = parentW - resizeStartPosX
|
|
147
|
+
winWidth.value = Math.min(maxW, Math.max(props.minWidth, resizeStartW + dx))
|
|
148
|
+
}
|
|
149
|
+
// West
|
|
150
|
+
if (dir.includes('w')) {
|
|
151
|
+
const maxW = resizeStartPosX + resizeStartW
|
|
152
|
+
const newW = Math.min(maxW, Math.max(props.minWidth, resizeStartW - dx))
|
|
153
|
+
posX.value = Math.max(0, resizeStartPosX + (resizeStartW - newW))
|
|
154
|
+
winWidth.value = newW
|
|
155
|
+
}
|
|
156
|
+
// South
|
|
157
|
+
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
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onMouseUp() {
|
|
172
|
+
isDragging = false
|
|
173
|
+
isResizing = false
|
|
174
|
+
document.removeEventListener('mousemove', onMouseMove)
|
|
175
|
+
document.removeEventListener('mouseup', onMouseUp)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- Actions ----
|
|
179
|
+
function close() {
|
|
180
|
+
emit('update:modelValue', false)
|
|
181
|
+
emit('close')
|
|
182
|
+
if (props.resetPosition) {
|
|
183
|
+
posX.value = props.x
|
|
184
|
+
posY.value = props.y
|
|
185
|
+
winWidth.value = parseInt(props.width) || 400
|
|
186
|
+
winHeight.value = parseInt(props.height) || 300
|
|
187
|
+
minimized.value = false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function toggleMinimize() {
|
|
192
|
+
minimized.value = !minimized.value
|
|
193
|
+
emit('minimize')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- Focus on click ----
|
|
197
|
+
function onWindowMouseDown() {
|
|
198
|
+
bringToFront()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- Sync initial props ----
|
|
202
|
+
watch(
|
|
203
|
+
() => props.x,
|
|
204
|
+
(v) => { posX.value = v },
|
|
205
|
+
)
|
|
206
|
+
watch(
|
|
207
|
+
() => props.y,
|
|
208
|
+
(v) => { posY.value = v },
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// ---- Cleanup ----
|
|
212
|
+
onUnmounted(() => {
|
|
213
|
+
document.removeEventListener('mousemove', onMouseMove)
|
|
214
|
+
document.removeEventListener('mouseup', onMouseUp)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Resize handle definitions
|
|
218
|
+
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
|
+
{ dir: 's', class: 'bottom-0 left-[10px] right-[10px] h-[5px]', cursor: 'cursor-ns-resize' },
|
|
221
|
+
{ dir: 'e', class: 'top-[10px] right-0 bottom-[10px] w-[5px]', cursor: 'cursor-ew-resize' },
|
|
222
|
+
{ 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
|
+
{ dir: 'se', class: 'bottom-0 right-0 w-[12px] h-[12px]', cursor: 'cursor-nwse-resize' },
|
|
226
|
+
{ dir: 'sw', class: 'bottom-0 left-0 w-[12px] h-[12px]', cursor: 'cursor-nesw-resize' },
|
|
227
|
+
]
|
|
228
|
+
</script>
|
|
229
|
+
|
|
230
|
+
<template>
|
|
231
|
+
<div
|
|
232
|
+
v-if="modelValue"
|
|
233
|
+
ref="windowRef"
|
|
234
|
+
class="absolute flex flex-col rounded-xl bg-surface shadow-elevation-3 overflow-hidden"
|
|
235
|
+
:style="windowStyle"
|
|
236
|
+
@mousedown="onWindowMouseDown"
|
|
237
|
+
>
|
|
238
|
+
<!-- Title bar -->
|
|
239
|
+
<div
|
|
240
|
+
class="flex items-center gap-2 px-4 h-10 bg-surface-container select-none shrink-0"
|
|
241
|
+
:class="{ 'cursor-move': draggable }"
|
|
242
|
+
@mousedown="onTitleBarMouseDown"
|
|
243
|
+
>
|
|
244
|
+
<slot name="header">
|
|
245
|
+
<MIcon v-if="icon" :name="icon" :size="18" class="text-on-surface-variant" />
|
|
246
|
+
<span class="text-title-small font-medium text-on-surface truncate flex-1">
|
|
247
|
+
{{ title }}
|
|
248
|
+
</span>
|
|
249
|
+
</slot>
|
|
250
|
+
|
|
251
|
+
<slot name="actions" />
|
|
252
|
+
|
|
253
|
+
<button
|
|
254
|
+
v-if="minimizable"
|
|
255
|
+
class="text-on-surface-variant hover:bg-on-surface/8 rounded-full w-7 h-7 flex items-center justify-center transition-colors"
|
|
256
|
+
@click="toggleMinimize"
|
|
257
|
+
>
|
|
258
|
+
<MIcon :name="minimized ? 'open_in_full' : 'minimize'" :size="16" />
|
|
259
|
+
</button>
|
|
260
|
+
|
|
261
|
+
<button
|
|
262
|
+
v-if="closable"
|
|
263
|
+
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"
|
|
264
|
+
@click="close"
|
|
265
|
+
>
|
|
266
|
+
<MIcon name="close" :size="16" />
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Body -->
|
|
271
|
+
<div
|
|
272
|
+
v-show="!minimized"
|
|
273
|
+
class="flex-1 overflow-auto"
|
|
274
|
+
>
|
|
275
|
+
<slot />
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Resize handles -->
|
|
279
|
+
<template v-if="resizable && !minimized">
|
|
280
|
+
<div
|
|
281
|
+
v-for="handle in resizeHandles"
|
|
282
|
+
:key="handle.dir"
|
|
283
|
+
class="absolute"
|
|
284
|
+
:class="[handle.class, handle.cursor]"
|
|
285
|
+
@mousedown="onResizeMouseDown($event, handle.dir)"
|
|
286
|
+
/>
|
|
287
|
+
</template>
|
|
288
|
+
</div>
|
|
289
|
+
</template>
|
|
290
|
+
|
|
291
|
+
<style scoped>
|
|
292
|
+
.flex-col {
|
|
293
|
+
transition: height 0.2s ease;
|
|
294
|
+
}
|
|
295
|
+
</style>
|
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'
|
|
@@ -73,6 +74,7 @@ export { default as MMenu } from './components/MMenu.vue'
|
|
|
73
74
|
export { default as MMenuItem } from './components/MMenuItem.vue'
|
|
74
75
|
export { default as MMaskField } from './components/MMaskField.vue'
|
|
75
76
|
export type { MaskPreset } from './components/MMaskField.vue'
|
|
77
|
+
export { default as MMultiAutocomplete } from './components/MMultiAutocomplete.vue'
|
|
76
78
|
export { default as MMultiSelect } from './components/MMultiSelect.vue'
|
|
77
79
|
export type { MultiSelectOption } from './components/MMultiSelect.vue'
|
|
78
80
|
export { default as MNavigationBar } from './components/MNavigationBar.vue'
|
|
@@ -110,6 +112,7 @@ export { default as MSticky } from './components/MSticky.vue'
|
|
|
110
112
|
export { default as MSubtitle } from './components/MSubtitle.vue'
|
|
111
113
|
export { default as MSwitch } from './components/MSwitch.vue'
|
|
112
114
|
export { default as MTable } from './components/MTable.vue'
|
|
115
|
+
export { default as MTagInput } from './components/MTagInput.vue'
|
|
113
116
|
export { default as MTabs } from './components/MTabs.vue'
|
|
114
117
|
// MTerminal — import from '@m3ui-vue/m3ui-vue/terminal'
|
|
115
118
|
export { default as MText } from './components/MText.vue'
|
|
@@ -124,6 +127,7 @@ export { default as MTransferList } from './components/MTransferList.vue'
|
|
|
124
127
|
export { default as MTree } from './components/MTree.vue'
|
|
125
128
|
export { default as MTreeTable } from './components/MTreeTable.vue'
|
|
126
129
|
export { default as MVirtualTable } from './components/MVirtualTable.vue'
|
|
130
|
+
export { default as MWindow } from './components/MWindow.vue'
|
|
127
131
|
|
|
128
132
|
// Re-export types from components that define them
|
|
129
133
|
export type { EmojiCategory } from './data/emojis'
|