@m3ui-vue/m3ui-vue 0.1.11 → 0.2.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-_n5OG5MT.js → MMenuItem-CIDblhtb.js} +45 -33
- package/dist/MMenuItem-CIDblhtb.js.map +1 -0
- package/dist/components/MCarousel.vue.d.ts +36 -0
- package/dist/components/MDataTable.vue.d.ts +17 -0
- package/dist/components/MFab.vue.d.ts +1 -1
- package/dist/components/MFlex.vue.d.ts +1 -1
- package/dist/components/MMaskField.vue.d.ts +24 -0
- package/dist/components/MMultiSelect.vue.d.ts +7 -5
- package/dist/components/MNavigationDrawer.vue.d.ts +9 -3
- package/dist/components/MNumberField.vue.d.ts +27 -0
- package/dist/components/MSelect.vue.d.ts +12 -9
- package/dist/components/MSplitter.vue.d.ts +1 -1
- package/dist/components/MStack.vue.d.ts +1 -1
- package/dist/components/MTable.vue.d.ts +1 -1
- package/dist/components/MTextField.vue.d.ts +2 -5
- package/dist/components/_MDrawerItemList.vue.d.ts +57 -0
- package/dist/index.d.ts +8 -1
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +1864 -1162
- 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/MCarousel.vue +203 -0
- package/src/components/MDataTable.vue +60 -5
- package/src/components/MMaskField.vue +198 -0
- package/src/components/MMultiSelect.vue +42 -17
- package/src/components/MNavigationDrawer.vue +282 -107
- package/src/components/MNumberField.vue +176 -0
- package/src/components/MSelect.vue +41 -14
- package/src/components/MTextField.vue +19 -10
- package/src/components/_MDrawerItemList.vue +105 -0
- package/src/index.ts +8 -1
- package/dist/MMenuItem-_n5OG5MT.js.map +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, useId } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import MIconButton from './MIconButton.vue'
|
|
5
|
+
import { useFieldBg } from '../composables/useFieldBg'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
modelValue: number | null
|
|
10
|
+
label: string
|
|
11
|
+
variant?: 'filled' | 'outlined'
|
|
12
|
+
min?: number
|
|
13
|
+
max?: number
|
|
14
|
+
step?: number
|
|
15
|
+
error?: string
|
|
16
|
+
hint?: string
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
required?: boolean
|
|
19
|
+
leadingIcon?: string
|
|
20
|
+
stepper?: boolean
|
|
21
|
+
fieldBg?: string
|
|
22
|
+
}>(),
|
|
23
|
+
{
|
|
24
|
+
variant: 'filled',
|
|
25
|
+
step: 1,
|
|
26
|
+
stepper: true,
|
|
27
|
+
modelValue: null,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{ 'update:modelValue': [number | null] }>()
|
|
32
|
+
|
|
33
|
+
const id = useId()
|
|
34
|
+
const fieldBgEl = ref<HTMLElement | null>(null)
|
|
35
|
+
const { resolvedFieldBg } = useFieldBg(fieldBgEl, () => props.fieldBg)
|
|
36
|
+
|
|
37
|
+
const hasValue = computed(() => props.modelValue !== null && props.modelValue !== undefined)
|
|
38
|
+
|
|
39
|
+
function clamp(val: number): number {
|
|
40
|
+
if (props.min !== undefined && val < props.min) return props.min
|
|
41
|
+
if (props.max !== undefined && val > props.max) return props.max
|
|
42
|
+
return val
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onInput(e: Event) {
|
|
46
|
+
const raw = (e.target as HTMLInputElement).value
|
|
47
|
+
if (raw === '') { emit('update:modelValue', null); return }
|
|
48
|
+
const n = Number(raw)
|
|
49
|
+
if (!Number.isNaN(n)) emit('update:modelValue', clamp(n))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function increment() {
|
|
53
|
+
if (props.disabled) return
|
|
54
|
+
const current = props.modelValue ?? 0
|
|
55
|
+
emit('update:modelValue', clamp(current + props.step))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decrement() {
|
|
59
|
+
if (props.disabled) return
|
|
60
|
+
const current = props.modelValue ?? 0
|
|
61
|
+
emit('update:modelValue', clamp(current - props.step))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onKeydown(e: KeyboardEvent) {
|
|
65
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); increment() }
|
|
66
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); decrement() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const inputClasses = computed(() => {
|
|
70
|
+
const pl = props.leadingIcon ? 'pl-12' : 'pl-4'
|
|
71
|
+
const pr = props.stepper ? 'pr-24' : 'pr-4'
|
|
72
|
+
const base = [
|
|
73
|
+
'peer block w-full text-body-large text-on-surface outline-none placeholder:text-transparent',
|
|
74
|
+
'transition-[border-color,border-width] duration-150',
|
|
75
|
+
'disabled:cursor-not-allowed disabled:opacity-[0.38]',
|
|
76
|
+
'h-14', pl, pr,
|
|
77
|
+
]
|
|
78
|
+
if (props.variant === 'outlined') {
|
|
79
|
+
return [...base,
|
|
80
|
+
'rounded-sm border bg-transparent py-4',
|
|
81
|
+
props.error
|
|
82
|
+
? 'border-error focus:border-2 focus:border-error'
|
|
83
|
+
: 'border-outline hover:border-on-surface focus:border-2 focus:border-primary',
|
|
84
|
+
].join(' ')
|
|
85
|
+
}
|
|
86
|
+
return [...base,
|
|
87
|
+
'rounded-t-sm bg-surface-container-highest border-b pt-6 pb-2',
|
|
88
|
+
props.error
|
|
89
|
+
? 'border-error focus:border-b-2 focus:border-error'
|
|
90
|
+
: 'border-on-surface-variant hover:border-on-surface focus:border-b-2 focus:border-primary',
|
|
91
|
+
].join(' ')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const labelClasses = computed(() => {
|
|
95
|
+
const left = props.leadingIcon
|
|
96
|
+
? (props.variant === 'outlined' ? 'left-11' : 'left-12')
|
|
97
|
+
: (props.variant === 'outlined' ? 'left-3' : 'left-4')
|
|
98
|
+
|
|
99
|
+
const unfloatedTop = props.variant === 'filled' ? 'top-[53%]' : 'top-1/2'
|
|
100
|
+
const base = [
|
|
101
|
+
'pointer-events-none absolute truncate transition-all duration-200',
|
|
102
|
+
left, 'right-4', `${unfloatedTop} -translate-y-1/2 text-body-large`,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if (props.variant === 'outlined') {
|
|
106
|
+
return [...base,
|
|
107
|
+
'peer-focus:-top-2.5 peer-focus:translate-y-0 peer-focus:text-label-small peer-focus:right-auto peer-focus:max-w-[calc(100%-1.5rem)] peer-focus:bg-[var(--field-bg)] peer-focus:px-1',
|
|
108
|
+
'peer-[&:not(:placeholder-shown)]:-top-2.5 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:right-auto peer-[&:not(:placeholder-shown)]:max-w-[calc(100%-1.5rem)]',
|
|
109
|
+
'peer-[&:not(:placeholder-shown)]:text-label-small peer-[&:not(:placeholder-shown)]:bg-[var(--field-bg)] peer-[&:not(:placeholder-shown)]:px-1',
|
|
110
|
+
props.error ? 'text-error peer-focus:text-error' : 'text-on-surface-variant peer-focus:text-primary',
|
|
111
|
+
].join(' ')
|
|
112
|
+
}
|
|
113
|
+
return [...base,
|
|
114
|
+
'peer-focus:top-2 peer-focus:translate-y-0 peer-focus:text-label-small',
|
|
115
|
+
'peer-[&:not(:placeholder-shown)]:top-2 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:text-label-small',
|
|
116
|
+
props.error ? 'text-error peer-focus:text-error' : 'text-on-surface-variant peer-focus:text-primary',
|
|
117
|
+
].join(' ')
|
|
118
|
+
})
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<div class="flex flex-col gap-1">
|
|
123
|
+
<div
|
|
124
|
+
ref="fieldBgEl"
|
|
125
|
+
class="relative"
|
|
126
|
+
:class="variant === 'outlined' ? 'mt-2' : ''"
|
|
127
|
+
:style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
v-if="leadingIcon"
|
|
131
|
+
class="pointer-events-none absolute left-3.5 text-on-surface-variant"
|
|
132
|
+
:class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'"
|
|
133
|
+
>
|
|
134
|
+
<MIcon :name="leadingIcon" :size="20" />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<input
|
|
138
|
+
:id="id"
|
|
139
|
+
type="number"
|
|
140
|
+
:value="modelValue ?? ''"
|
|
141
|
+
:disabled="disabled"
|
|
142
|
+
:required="required"
|
|
143
|
+
:min="min"
|
|
144
|
+
:max="max"
|
|
145
|
+
:step="step"
|
|
146
|
+
placeholder=" "
|
|
147
|
+
:class="inputClasses"
|
|
148
|
+
@input="onInput"
|
|
149
|
+
@keydown="onKeydown"
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
<label :for="id" :class="labelClasses">
|
|
153
|
+
{{ label }}<span v-if="required" class="text-error"> *</span>
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
<div v-if="stepper" class="absolute right-1 flex items-center gap-0.5" :class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'">
|
|
157
|
+
<MIconButton icon="remove" label="Decrease" :size="32" :disabled="disabled || (min !== undefined && (modelValue ?? 0) <= min)" @click="decrement" />
|
|
158
|
+
<MIconButton icon="add" label="Increase" :size="32" :disabled="disabled || (max !== undefined && (modelValue ?? 0) >= max)" @click="increment" />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
|
|
163
|
+
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
164
|
+
</div>
|
|
165
|
+
</template>
|
|
166
|
+
|
|
167
|
+
<style scoped>
|
|
168
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
169
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
170
|
+
-webkit-appearance: none;
|
|
171
|
+
margin: 0;
|
|
172
|
+
}
|
|
173
|
+
input[type="number"] {
|
|
174
|
+
-moz-appearance: textfield;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -3,10 +3,16 @@ import { computed, ref, useId, onMounted, onUnmounted } from 'vue'
|
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
import { useFieldBg } from '../composables/useFieldBg'
|
|
5
5
|
|
|
6
|
+
export interface SelectOption {
|
|
7
|
+
label: string
|
|
8
|
+
value: unknown
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
const props = withDefaults(
|
|
7
13
|
defineProps<{
|
|
8
|
-
modelValue:
|
|
9
|
-
options:
|
|
14
|
+
modelValue: unknown
|
|
15
|
+
options: SelectOption[]
|
|
10
16
|
label?: string
|
|
11
17
|
placeholder?: string
|
|
12
18
|
variant?: 'filled' | 'outlined'
|
|
@@ -15,17 +21,19 @@ const props = withDefaults(
|
|
|
15
21
|
hint?: string
|
|
16
22
|
required?: boolean
|
|
17
23
|
leadingIcon?: string
|
|
24
|
+
clearable?: boolean
|
|
18
25
|
fieldBg?: string
|
|
19
26
|
}>(),
|
|
20
27
|
{
|
|
21
|
-
modelValue:
|
|
28
|
+
modelValue: undefined,
|
|
22
29
|
variant: 'filled',
|
|
23
30
|
disabled: false,
|
|
24
31
|
required: false,
|
|
32
|
+
clearable: false,
|
|
25
33
|
},
|
|
26
34
|
)
|
|
27
35
|
|
|
28
|
-
const emit = defineEmits<{ 'update:modelValue': [
|
|
36
|
+
const emit = defineEmits<{ 'update:modelValue': [unknown] }>()
|
|
29
37
|
|
|
30
38
|
const id = useId()
|
|
31
39
|
const open = ref(false)
|
|
@@ -34,9 +42,15 @@ const { resolvedFieldBg } = useFieldBg(fieldEl, () => props.fieldBg)
|
|
|
34
42
|
const dropdownEl = ref<HTMLElement | null>(null)
|
|
35
43
|
const dropPos = ref({ top: '0px', left: '0px', width: '0px' })
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
function eq(a: unknown, b: unknown): boolean {
|
|
46
|
+
if (a === b) return true
|
|
47
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a == null || b == null) return false
|
|
48
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hasValue = computed(() => props.modelValue != null && props.modelValue !== '')
|
|
38
52
|
const selectedLabel = computed(
|
|
39
|
-
() => props.options.find((o) => o.value
|
|
53
|
+
() => props.options.find((o) => eq(o.value, props.modelValue))?.label ?? '',
|
|
40
54
|
)
|
|
41
55
|
|
|
42
56
|
function computeDropPos() {
|
|
@@ -58,7 +72,7 @@ function toggle() {
|
|
|
58
72
|
open.value = !open.value
|
|
59
73
|
}
|
|
60
74
|
|
|
61
|
-
function select(opt:
|
|
75
|
+
function select(opt: SelectOption) {
|
|
62
76
|
if (opt.disabled) return
|
|
63
77
|
emit('update:modelValue', opt.value)
|
|
64
78
|
open.value = false
|
|
@@ -89,7 +103,7 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
89
103
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); return }
|
|
90
104
|
if (!open.value) return
|
|
91
105
|
const opts = props.options.filter((o) => !o.disabled)
|
|
92
|
-
const idx = opts.findIndex((o) => o.value
|
|
106
|
+
const idx = opts.findIndex((o) => eq(o.value, props.modelValue))
|
|
93
107
|
if (e.key === 'ArrowDown') {
|
|
94
108
|
e.preventDefault()
|
|
95
109
|
const next = opts[(idx + 1) % opts.length]
|
|
@@ -148,7 +162,9 @@ const labelClasses = computed(() => {
|
|
|
148
162
|
? '-top-2.5 translate-y-0 text-label-small bg-[var(--field-bg)] px-1 right-auto max-w-[calc(100%-1.5rem)]'
|
|
149
163
|
: 'top-2 translate-y-0 text-label-small'
|
|
150
164
|
|
|
151
|
-
const unFloated =
|
|
165
|
+
const unFloated = props.variant === 'filled'
|
|
166
|
+
? 'top-[53%] -translate-y-1/2 text-body-large'
|
|
167
|
+
: 'top-1/2 -translate-y-1/2 text-body-large'
|
|
152
168
|
|
|
153
169
|
return [
|
|
154
170
|
'pointer-events-none absolute right-10 truncate transition-all duration-200',
|
|
@@ -196,8 +212,19 @@ const labelClasses = computed(() => {
|
|
|
196
212
|
{{ label }}<span v-if="required" class="text-error"> *</span>
|
|
197
213
|
</label>
|
|
198
214
|
|
|
215
|
+
<!-- Clear button -->
|
|
216
|
+
<button
|
|
217
|
+
v-if="clearable && hasValue && !disabled"
|
|
218
|
+
type="button"
|
|
219
|
+
class="absolute 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"
|
|
220
|
+
:class="variant === 'filled' ? 'right-9 top-[57%] -translate-y-1/2' : 'right-9 top-[55%] -translate-y-1/2'"
|
|
221
|
+
@click.stop="emit('update:modelValue', undefined as any); open = false"
|
|
222
|
+
>
|
|
223
|
+
<MIcon name="close" :size="18" />
|
|
224
|
+
</button>
|
|
225
|
+
|
|
199
226
|
<!-- Arrow icon -->
|
|
200
|
-
<div class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2">
|
|
227
|
+
<div class="pointer-events-none absolute right-2" :class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'">
|
|
201
228
|
<MIcon
|
|
202
229
|
:name="open ? 'arrow_drop_up' : 'arrow_drop_down'"
|
|
203
230
|
:size="24"
|
|
@@ -227,19 +254,19 @@ const labelClasses = computed(() => {
|
|
|
227
254
|
:style="dropPos"
|
|
228
255
|
>
|
|
229
256
|
<div
|
|
230
|
-
v-for="opt in options"
|
|
231
|
-
:key="
|
|
257
|
+
v-for="(opt, i) in options"
|
|
258
|
+
:key="i"
|
|
232
259
|
class="flex cursor-pointer items-center gap-3 px-4 py-3 text-body-large"
|
|
233
260
|
:class="[
|
|
234
261
|
opt.disabled
|
|
235
262
|
? 'cursor-not-allowed opacity-38 text-on-surface'
|
|
236
263
|
: 'text-on-surface hover:bg-on-surface/8',
|
|
237
|
-
opt.value
|
|
264
|
+
eq(opt.value, modelValue) ? 'bg-primary/8 text-primary font-medium' : '',
|
|
238
265
|
]"
|
|
239
266
|
@click="select(opt)"
|
|
240
267
|
>
|
|
241
268
|
<MIcon
|
|
242
|
-
v-if="opt.value
|
|
269
|
+
v-if="eq(opt.value, modelValue)"
|
|
243
270
|
name="check"
|
|
244
271
|
:size="18"
|
|
245
272
|
class="shrink-0 text-primary"
|
|
@@ -17,17 +17,14 @@ const props = withDefaults(
|
|
|
17
17
|
rows?: number;
|
|
18
18
|
autocomplete?: string;
|
|
19
19
|
leadingIcon?: string;
|
|
20
|
-
|
|
21
|
-
* Background color behind the label in outlined variant.
|
|
22
|
-
* Defaults to the page surface color. Pass e.g. 'var(--color-surface-container-low)'
|
|
23
|
-
* when the input is inside a card.
|
|
24
|
-
*/
|
|
20
|
+
clearable?: boolean;
|
|
25
21
|
fieldBg?: string;
|
|
26
22
|
}>(),
|
|
27
23
|
{
|
|
28
24
|
type: "text",
|
|
29
25
|
variant: "filled",
|
|
30
26
|
rows: 3,
|
|
27
|
+
clearable: false,
|
|
31
28
|
},
|
|
32
29
|
);
|
|
33
30
|
|
|
@@ -39,8 +36,10 @@ const slots = useSlots();
|
|
|
39
36
|
const fieldBgEl = ref<HTMLElement | null>(null);
|
|
40
37
|
const { resolvedFieldBg } = useFieldBg(fieldBgEl, () => props.fieldBg);
|
|
41
38
|
|
|
39
|
+
const showClear = computed(() => props.clearable && String(props.modelValue).length > 0 && !props.disabled)
|
|
40
|
+
|
|
42
41
|
const inputClasses = computed(() => {
|
|
43
|
-
const hasTrailing = !!slots.trailing;
|
|
42
|
+
const hasTrailing = !!slots.trailing || props.clearable;
|
|
44
43
|
const pl = props.leadingIcon ? "pl-12" : "pl-4";
|
|
45
44
|
const pr = hasTrailing ? "pr-12" : "pr-4";
|
|
46
45
|
const size = props.multiline ? "resize-y min-h-[56px]" : "h-14";
|
|
@@ -81,11 +80,12 @@ const labelClasses = computed(() => {
|
|
|
81
80
|
? "left-3"
|
|
82
81
|
: "left-4";
|
|
83
82
|
|
|
83
|
+
const unfloatedTop = props.variant === 'filled' ? 'top-[53%]' : 'top-1/2'
|
|
84
84
|
const base = [
|
|
85
85
|
"pointer-events-none absolute truncate transition-all duration-200",
|
|
86
86
|
left,
|
|
87
87
|
"right-4",
|
|
88
|
-
|
|
88
|
+
`${unfloatedTop} -translate-y-1/2 text-body-large`,
|
|
89
89
|
];
|
|
90
90
|
|
|
91
91
|
if (props.variant === "outlined") {
|
|
@@ -133,10 +133,10 @@ function onInput(event: Event) {
|
|
|
133
133
|
:class="variant === 'outlined' ? 'mt-2' : ''"
|
|
134
134
|
:style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
|
|
135
135
|
>
|
|
136
|
-
<!-- Leading icon: centered in the 48px left zone (left-3.5 → center at 24px) -->
|
|
137
136
|
<div
|
|
138
137
|
v-if="leadingIcon"
|
|
139
|
-
class="pointer-events-none absolute left-3.5
|
|
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'"
|
|
140
140
|
>
|
|
141
141
|
<MIcon :name="leadingIcon" :size="20" />
|
|
142
142
|
</div>
|
|
@@ -169,9 +169,18 @@ 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 top-1/2
|
|
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'">
|
|
173
173
|
<slot name="trailing" />
|
|
174
174
|
</div>
|
|
175
|
+
<button
|
|
176
|
+
v-else-if="showClear"
|
|
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'"
|
|
180
|
+
@click="emit('update:modelValue', '')"
|
|
181
|
+
>
|
|
182
|
+
<MIcon name="close" :size="18" />
|
|
183
|
+
</button>
|
|
175
184
|
</div>
|
|
176
185
|
|
|
177
186
|
<p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { defineComponent, inject, type PropType, type Ref } from 'vue'
|
|
3
|
+
import type { DrawerItem } from './MNavigationDrawer.vue'
|
|
4
|
+
import MIcon from './MIcon.vue'
|
|
5
|
+
|
|
6
|
+
export default defineComponent({
|
|
7
|
+
name: 'MDrawerItemList',
|
|
8
|
+
components: { MIcon },
|
|
9
|
+
props: {
|
|
10
|
+
items: { type: Array as PropType<DrawerItem[]>, required: true },
|
|
11
|
+
selected: { type: [String, Number] as PropType<string | number>, default: undefined },
|
|
12
|
+
depth: { type: Number, default: 0 },
|
|
13
|
+
},
|
|
14
|
+
setup() {
|
|
15
|
+
const openItems = inject<Record<string | number, boolean>>('nd-open-items')!
|
|
16
|
+
const toggleItem = inject<(item: DrawerItem) => void>('nd-toggle-item')!
|
|
17
|
+
const selectItem = inject<(item: DrawerItem) => void>('nd-select-item')!
|
|
18
|
+
const collapsed = inject<Ref<boolean>>('nd-collapsed')!
|
|
19
|
+
return { openItems, toggleItem, selectItem, collapsed }
|
|
20
|
+
},
|
|
21
|
+
methods: {
|
|
22
|
+
itemTag(item: DrawerItem) {
|
|
23
|
+
return item.to && !item.disabled ? 'RouterLink' : 'button'
|
|
24
|
+
},
|
|
25
|
+
onClick(item: DrawerItem) {
|
|
26
|
+
if (item.children?.length) this.toggleItem(item)
|
|
27
|
+
this.selectItem(item)
|
|
28
|
+
},
|
|
29
|
+
itemStyle(item: DrawerItem): Record<string, string> {
|
|
30
|
+
const transition = 'padding 300ms cubic-bezier(0.2, 0, 0, 1)'
|
|
31
|
+
if (this.collapsed) {
|
|
32
|
+
const pad = `${(48 - this.iSize(item)) / 2}px`
|
|
33
|
+
return { paddingLeft: pad, paddingRight: pad, transition }
|
|
34
|
+
}
|
|
35
|
+
return { paddingLeft: `${this.depth * 1.25 + 3}rem`, paddingRight: '0.75rem', transition }
|
|
36
|
+
},
|
|
37
|
+
iSize(item: DrawerItem): number {
|
|
38
|
+
return item.iconSize ?? 20
|
|
39
|
+
},
|
|
40
|
+
lClass(item: DrawerItem): string {
|
|
41
|
+
return item.labelClass ?? 'text-label-medium'
|
|
42
|
+
},
|
|
43
|
+
onEnter(el: Element, done: () => void) {
|
|
44
|
+
const e = el as HTMLElement
|
|
45
|
+
e.style.overflow = 'hidden'
|
|
46
|
+
const h = e.scrollHeight
|
|
47
|
+
e.animate(
|
|
48
|
+
[{ height: '0px' }, { height: h + 'px' }],
|
|
49
|
+
{ duration: 250, easing: 'cubic-bezier(0.2, 0, 0, 1)' },
|
|
50
|
+
).onfinish = () => { e.style.overflow = ''; done() }
|
|
51
|
+
},
|
|
52
|
+
onLeave(el: Element, done: () => void) {
|
|
53
|
+
const e = el as HTMLElement
|
|
54
|
+
e.style.overflow = 'hidden'
|
|
55
|
+
const h = e.scrollHeight
|
|
56
|
+
e.animate(
|
|
57
|
+
[{ height: h + 'px' }, { height: '0px' }],
|
|
58
|
+
{ duration: 200, easing: 'cubic-bezier(0.4, 0, 1, 1)' },
|
|
59
|
+
).onfinish = done
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<template v-for="item in items" :key="item.value">
|
|
67
|
+
<component
|
|
68
|
+
:is="itemTag(item)"
|
|
69
|
+
:to="item.to && !item.disabled ? item.to : undefined"
|
|
70
|
+
:type="item.to ? undefined : 'button'"
|
|
71
|
+
:title="collapsed ? item.label : undefined"
|
|
72
|
+
class="flex w-full shrink-0 items-center gap-2.5 overflow-hidden whitespace-nowrap rounded-full text-left transition-colors focus-visible:outline-none"
|
|
73
|
+
:class="[
|
|
74
|
+
item.py ?? 'py-1.5',
|
|
75
|
+
item.disabled
|
|
76
|
+
? 'cursor-not-allowed opacity-[0.38]'
|
|
77
|
+
: item.value === selected
|
|
78
|
+
? 'bg-secondary-container text-on-secondary-container'
|
|
79
|
+
: 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8',
|
|
80
|
+
]"
|
|
81
|
+
:style="itemStyle(item)"
|
|
82
|
+
:disabled="item.disabled && !item.to"
|
|
83
|
+
@click="onClick(item)"
|
|
84
|
+
>
|
|
85
|
+
<MIcon v-if="item.icon" :name="item.icon" :size="iSize(item)" class="shrink-0" />
|
|
86
|
+
<span class="nd-label min-w-0 flex-1" :class="lClass(item)">{{ item.label }}</span>
|
|
87
|
+
<MIcon
|
|
88
|
+
v-if="item.children?.length"
|
|
89
|
+
:name="openItems[item.value] ? 'expand_less' : 'expand_more'"
|
|
90
|
+
:size="16"
|
|
91
|
+
class="nd-label shrink-0 text-on-surface-variant"
|
|
92
|
+
/>
|
|
93
|
+
</component>
|
|
94
|
+
|
|
95
|
+
<Transition :css="false" @enter="onEnter" @leave="onLeave">
|
|
96
|
+
<div v-if="item.children?.length && openItems[item.value]">
|
|
97
|
+
<MDrawerItemList
|
|
98
|
+
:items="item.children"
|
|
99
|
+
:selected="selected"
|
|
100
|
+
:depth="depth + 1"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</Transition>
|
|
104
|
+
</template>
|
|
105
|
+
</template>
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { default as MBreadcrumbs } from './components/MBreadcrumbs.vue'
|
|
|
25
25
|
export { default as MButton } from './components/MButton.vue'
|
|
26
26
|
export { default as MCalendar } from './components/MCalendar.vue'
|
|
27
27
|
export { default as MCard } from './components/MCard.vue'
|
|
28
|
+
export { default as MCarousel } from './components/MCarousel.vue'
|
|
28
29
|
export { default as MCenter } from './components/MCenter.vue'
|
|
29
30
|
// MChart — import from '@m3ui-vue/m3ui-vue/chart'
|
|
30
31
|
export { default as MCheckbox } from './components/MCheckbox.vue'
|
|
@@ -67,8 +68,12 @@ export { default as MLoadingOverlay } from './components/MLoadingOverlay.vue'
|
|
|
67
68
|
export { default as MMasonry } from './components/MMasonry.vue'
|
|
68
69
|
export { default as MMenu } from './components/MMenu.vue'
|
|
69
70
|
export { default as MMenuItem } from './components/MMenuItem.vue'
|
|
71
|
+
export { default as MMaskField } from './components/MMaskField.vue'
|
|
72
|
+
export type { MaskPreset } from './components/MMaskField.vue'
|
|
70
73
|
export { default as MMultiSelect } from './components/MMultiSelect.vue'
|
|
74
|
+
export type { MultiSelectOption } from './components/MMultiSelect.vue'
|
|
71
75
|
export { default as MNavigationBar } from './components/MNavigationBar.vue'
|
|
76
|
+
export { default as MNumberField } from './components/MNumberField.vue'
|
|
72
77
|
export { default as MNavigationDrawer } from './components/MNavigationDrawer.vue'
|
|
73
78
|
export { default as MNavigationRail } from './components/MNavigationRail.vue'
|
|
74
79
|
export { default as MOverlay } from './components/MOverlay.vue'
|
|
@@ -86,6 +91,7 @@ export { default as MScrollable } from './components/MScrollable.vue'
|
|
|
86
91
|
export { default as MSection } from './components/MSection.vue'
|
|
87
92
|
export { default as MSegmentedButton } from './components/MSegmentedButton.vue'
|
|
88
93
|
export { default as MSelect } from './components/MSelect.vue'
|
|
94
|
+
export type { SelectOption } from './components/MSelect.vue'
|
|
89
95
|
export { default as MSideSheet } from './components/MSideSheet.vue'
|
|
90
96
|
export { default as MSkeleton } from './components/MSkeleton.vue'
|
|
91
97
|
export { default as MSlider } from './components/MSlider.vue'
|
|
@@ -122,8 +128,9 @@ export { emojiCategories, allEmojis, randomEmoji } from './data/emojis'
|
|
|
122
128
|
export type { Placement } from './components/MAbsolute.vue'
|
|
123
129
|
export type { BadgePosition, BadgeOverlap } from './components/MBadge.vue'
|
|
124
130
|
export type { CalendarEvent } from './components/MCalendar.vue'
|
|
131
|
+
export type { CarouselItem } from './components/MCarousel.vue'
|
|
125
132
|
export type { ContextMenuItem } from './components/MContextMenu.vue'
|
|
126
|
-
export type { DataTableColumn } from './components/MDataTable.vue'
|
|
133
|
+
export type { DataTableColumn, DataTableFetchParams } from './components/MDataTable.vue'
|
|
127
134
|
export type { DateRange } from './components/MDateRangePicker.vue'
|
|
128
135
|
export type { DragDropItem } from './components/MDragDropList.vue'
|
|
129
136
|
export type { UploadFile } from './components/MFileUpload.vue'
|