@m3ui-vue/m3ui-vue 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{MMenuItem-C6RwHl-l.js → MMenuItem-FyttjOj_.js} +399 -207
- package/dist/MMenuItem-FyttjOj_.js.map +1 -0
- package/dist/components/MAutocomplete.vue.d.ts +2 -0
- package/dist/components/MBox.vue.d.ts +2 -2
- package/dist/components/MButton.vue.d.ts +4 -0
- package/dist/components/MButtonGroup.vue.d.ts +28 -0
- package/dist/components/MCarousel.vue.d.ts +21 -12
- package/dist/components/MChatBubble.vue.d.ts +32 -0
- package/dist/components/MColorPickerModal.vue.d.ts +18 -0
- package/dist/components/MDataTable.vue.d.ts +1 -1
- package/dist/components/MDatePickerModal.vue.d.ts +21 -0
- package/dist/components/MDateRangePickerModal.vue.d.ts +24 -0
- package/dist/components/MDialog.vue.d.ts +9 -1
- package/dist/components/MExpansionPanel.vue.d.ts +3 -2
- package/dist/components/MFab.vue.d.ts +14 -2
- package/dist/components/MFlex.vue.d.ts +2 -2
- package/dist/components/MIconButton.vue.d.ts +4 -2
- package/dist/components/MInfiniteScroll.vue.d.ts +1 -1
- package/dist/components/MList.vue.d.ts +2 -0
- package/dist/components/MLoadingOverlay.vue.d.ts +1 -1
- package/dist/components/MMenu.vue.d.ts +3 -1
- package/dist/components/MMenuItem.vue.d.ts +14 -3
- package/dist/components/MMultiAutocomplete.vue.d.ts +2 -0
- package/dist/components/MMultiSelect.vue.d.ts +2 -0
- package/dist/components/MNotificationHost.vue.d.ts +3 -0
- package/dist/components/MSelect.vue.d.ts +2 -0
- package/dist/components/MSlider.vue.d.ts +15 -4
- package/dist/components/MSplitButton.vue.d.ts +39 -0
- package/dist/components/MSplitter.vue.d.ts +1 -1
- package/dist/components/MStack.vue.d.ts +2 -2
- package/dist/components/MTable.vue.d.ts +1 -1
- package/dist/components/MTimePickerModal.vue.d.ts +21 -0
- package/dist/components/MToolbar.vue.d.ts +30 -0
- package/dist/components/MTooltip.vue.d.ts +8 -2
- package/dist/components/MWindow.vue.d.ts +4 -2
- package/dist/composables/useDevice.d.ts +3 -0
- package/dist/composables/useNotification.d.ts +64 -0
- package/dist/composables/useToast.d.ts +8 -0
- package/dist/index.d.ts +13 -0
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +5231 -2916
- package/dist/m3ui.js.map +1 -1
- package/dist/rich-text-editor.js +10 -10
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/MAutocomplete.vue +108 -13
- package/src/components/MButton.vue +38 -16
- package/src/components/MButtonGroup.vue +158 -0
- package/src/components/MCarousel.vue +223 -101
- package/src/components/MChatBubble.vue +98 -0
- package/src/components/MColorPicker.vue +1 -4
- package/src/components/MColorPickerModal.vue +276 -0
- package/src/components/MDatePicker.vue +1 -4
- package/src/components/MDatePickerModal.vue +265 -0
- package/src/components/MDateRangePicker.vue +1 -4
- package/src/components/MDateRangePickerModal.vue +264 -0
- package/src/components/MDialog.vue +38 -1
- package/src/components/MExpansionPanel.vue +10 -32
- package/src/components/MFab.vue +92 -11
- package/src/components/MIconButton.vue +36 -6
- package/src/components/MList.vue +6 -3
- package/src/components/MListItem.vue +9 -3
- package/src/components/MMenu.vue +10 -5
- package/src/components/MMenuItem.vue +93 -10
- package/src/components/MMultiAutocomplete.vue +104 -10
- package/src/components/MMultiSelect.vue +106 -13
- package/src/components/MNotificationHost.vue +163 -0
- package/src/components/MSelect.vue +99 -16
- package/src/components/MSlider.vue +367 -134
- package/src/components/MSnackbar.vue +3 -0
- package/src/components/MSplitButton.vue +240 -0
- package/src/components/MTabs.vue +82 -39
- package/src/components/MTagInput.vue +11 -1
- package/src/components/MTimePicker.vue +1 -4
- package/src/components/MTimePickerModal.vue +458 -0
- package/src/components/MToolbar.vue +75 -0
- package/src/components/MTooltip.vue +60 -10
- package/src/components/MWindow.vue +83 -48
- package/src/components/_MContextMenuPanel.vue +2 -2
- package/src/composables/useDevice.ts +29 -0
- package/src/composables/useNotification.ts +93 -0
- package/src/composables/useToast.ts +42 -5
- package/src/index.ts +13 -0
- package/dist/MMenuItem-C6RwHl-l.js.map +0 -1
|
@@ -1,87 +1,213 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed,
|
|
2
|
+
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
|
|
5
5
|
export interface CarouselItem {
|
|
6
6
|
src: string
|
|
7
7
|
alt?: string
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
label?: string
|
|
9
|
+
supportingText?: string
|
|
10
|
+
ratio?: number
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const props = withDefaults(
|
|
13
14
|
defineProps<{
|
|
14
15
|
items: CarouselItem[]
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
layout?: 'hero' | 'multi-browse' | 'uncontained'
|
|
17
|
+
height?: string
|
|
17
18
|
showArrows?: boolean
|
|
18
19
|
showIndicators?: boolean
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
autoplay?: boolean
|
|
21
|
+
interval?: number
|
|
22
|
+
gap?: number
|
|
23
|
+
visibleItems?: number
|
|
24
|
+
animated?: boolean
|
|
21
25
|
}>(),
|
|
22
26
|
{
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
layout: 'hero',
|
|
28
|
+
height: '320px',
|
|
25
29
|
showArrows: true,
|
|
26
30
|
showIndicators: true,
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
autoplay: false,
|
|
32
|
+
interval: 5000,
|
|
33
|
+
gap: 8,
|
|
34
|
+
visibleItems: 3,
|
|
35
|
+
animated: false,
|
|
29
36
|
},
|
|
30
37
|
)
|
|
31
38
|
|
|
32
39
|
const emit = defineEmits<{ change: [index: number] }>()
|
|
33
40
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const trackEl = ref<HTMLElement>()
|
|
42
|
+
const itemEls = ref<HTMLElement[]>([])
|
|
43
|
+
const activeIndex = ref(0)
|
|
44
|
+
const scrollPos = ref(0)
|
|
45
|
+
const isPageBased = computed(() => props.layout === 'multi-browse' || props.layout === 'uncontained')
|
|
46
|
+
|
|
47
|
+
const totalPages = computed(() => {
|
|
48
|
+
if (!isPageBased.value) return props.items.length
|
|
49
|
+
if (!trackEl.value) return 1
|
|
50
|
+
const track = trackEl.value
|
|
51
|
+
return Math.max(1, Math.ceil(track.scrollWidth / track.clientWidth))
|
|
52
|
+
})
|
|
39
53
|
|
|
40
|
-
const
|
|
41
|
-
|
|
54
|
+
const activePage = computed(() => {
|
|
55
|
+
if (!isPageBased.value) return activeIndex.value
|
|
56
|
+
if (!trackEl.value) return 0
|
|
57
|
+
const track = trackEl.value
|
|
58
|
+
if (track.scrollWidth <= track.clientWidth) return 0
|
|
59
|
+
const _ = scrollPos.value
|
|
60
|
+
return Math.round(track.scrollLeft / track.clientWidth)
|
|
61
|
+
})
|
|
62
|
+
const itemTransforms = ref<string[]>([])
|
|
63
|
+
const imageTransforms = ref<string[]>([])
|
|
64
|
+
let autoTimer: ReturnType<typeof setInterval> | null = null
|
|
65
|
+
let rafId = 0
|
|
66
|
+
let scrollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
42
67
|
|
|
43
|
-
function
|
|
44
|
-
if (
|
|
45
|
-
direction.value = index > current.value ? 'right' : 'left'
|
|
46
|
-
transitioning.value = true
|
|
47
|
-
current.value = ((index % total.value) + total.value) % total.value
|
|
48
|
-
emit('change', current.value)
|
|
68
|
+
function setItemRef(el: any, i: number) {
|
|
69
|
+
if (el) itemEls.value[i] = el as HTMLElement
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
// ── Scroll handling (only transforms, never layout) ────────────
|
|
73
|
+
function onScroll() {
|
|
74
|
+
if (rafId) return
|
|
75
|
+
rafId = requestAnimationFrame(() => {
|
|
76
|
+
rafId = 0
|
|
77
|
+
updateTransforms()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (scrollTimeout) clearTimeout(scrollTimeout)
|
|
53
81
|
}
|
|
54
82
|
|
|
55
|
-
function
|
|
56
|
-
|
|
83
|
+
function updateTransforms() {
|
|
84
|
+
if (!trackEl.value) return
|
|
85
|
+
const track = trackEl.value
|
|
86
|
+
scrollPos.value = track.scrollLeft
|
|
87
|
+
const trackRect = track.getBoundingClientRect()
|
|
88
|
+
const trackW = trackRect.width
|
|
89
|
+
const centerX = trackRect.left + trackW / 2
|
|
90
|
+
const transforms: string[] = []
|
|
91
|
+
const imgTransforms: string[] = []
|
|
92
|
+
let closest = 0
|
|
93
|
+
let closestDist = Infinity
|
|
94
|
+
|
|
95
|
+
const refX = props.layout === 'multi-browse' ? trackRect.left : centerX
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < itemEls.value.length; i++) {
|
|
98
|
+
const el = itemEls.value[i]
|
|
99
|
+
if (!el) { transforms.push(''); imgTransforms.push(''); continue }
|
|
100
|
+
const rect = el.getBoundingClientRect()
|
|
101
|
+
const itemCenterX = rect.left + rect.width / 2
|
|
102
|
+
const dist = props.layout === 'multi-browse'
|
|
103
|
+
? Math.abs(rect.left - refX)
|
|
104
|
+
: Math.abs(itemCenterX - centerX)
|
|
105
|
+
const norm = Math.min(dist / (trackW * 0.5), 1)
|
|
106
|
+
|
|
107
|
+
if (props.layout === 'hero') {
|
|
108
|
+
imgTransforms.push(`translateX(${(itemCenterX - centerX) * -0.08}px) scale(1.1)`)
|
|
109
|
+
transforms.push('')
|
|
110
|
+
} else if (props.layout === 'uncontained' && props.animated) {
|
|
111
|
+
const s = 1 - norm * 0.12
|
|
112
|
+
transforms.push(`scale(${s})`)
|
|
113
|
+
imgTransforms.push('')
|
|
114
|
+
} else {
|
|
115
|
+
transforms.push('')
|
|
116
|
+
imgTransforms.push('')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (dist < closestDist) {
|
|
120
|
+
closestDist = dist
|
|
121
|
+
closest = i
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
itemTransforms.value = transforms
|
|
126
|
+
imageTransforms.value = imgTransforms
|
|
127
|
+
|
|
128
|
+
if (closest !== activeIndex.value) {
|
|
129
|
+
activeIndex.value = closest
|
|
130
|
+
emit('change', closest)
|
|
131
|
+
}
|
|
57
132
|
}
|
|
58
133
|
|
|
59
|
-
|
|
60
|
-
|
|
134
|
+
// ── Fixed item widths (never change during scroll) ─────────────
|
|
135
|
+
function itemWidth(i: number) {
|
|
136
|
+
switch (props.layout) {
|
|
137
|
+
case 'hero':
|
|
138
|
+
return `calc(100% - 48px)`
|
|
139
|
+
case 'multi-browse':
|
|
140
|
+
return `calc(${100 / props.visibleItems}% - ${props.gap * (props.visibleItems - 1) / props.visibleItems}px)`
|
|
141
|
+
case 'uncontained':
|
|
142
|
+
return `${(props.items[i]?.ratio ?? 1) * 260}px`
|
|
143
|
+
default:
|
|
144
|
+
return '80%'
|
|
145
|
+
}
|
|
61
146
|
}
|
|
62
147
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
148
|
+
function itemStyle(i: number) {
|
|
149
|
+
const style: Record<string, string> = {
|
|
150
|
+
flexShrink: '0',
|
|
151
|
+
width: itemWidth(i),
|
|
152
|
+
}
|
|
153
|
+
const t = itemTransforms.value[i]
|
|
154
|
+
if (t) {
|
|
155
|
+
style.transform = t
|
|
156
|
+
style.transition = 'transform 120ms ease-out'
|
|
67
157
|
}
|
|
158
|
+
return style
|
|
68
159
|
}
|
|
69
160
|
|
|
70
|
-
function
|
|
71
|
-
|
|
161
|
+
function imgStyle(i: number) {
|
|
162
|
+
const t = imageTransforms.value[i]
|
|
163
|
+
if (!t) return {}
|
|
164
|
+
return { transform: t }
|
|
72
165
|
}
|
|
73
166
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
167
|
+
// ── Navigation ─────────────────────────────────────────────────
|
|
168
|
+
function scrollToItem(index: number) {
|
|
169
|
+
const el = itemEls.value[index]
|
|
170
|
+
if (!el || !trackEl.value) return
|
|
171
|
+
const track = trackEl.value
|
|
172
|
+
const scrollLeft = el.offsetLeft - (track.clientWidth - el.offsetWidth) / 2
|
|
173
|
+
track.scrollTo({ left: scrollLeft, behavior: 'smooth' })
|
|
77
174
|
}
|
|
78
175
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
176
|
+
function scrollToPage(page: number) {
|
|
177
|
+
if (!trackEl.value) return
|
|
178
|
+
const track = trackEl.value
|
|
179
|
+
track.scrollTo({ left: page * track.clientWidth, behavior: 'smooth' })
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function next() {
|
|
183
|
+
if (isPageBased.value) {
|
|
184
|
+
if (!trackEl.value) return
|
|
185
|
+
trackEl.value.scrollBy({ left: trackEl.value.clientWidth, behavior: 'smooth' })
|
|
186
|
+
} else {
|
|
187
|
+
const i = activeIndex.value < props.items.length - 1 ? activeIndex.value + 1 : 0
|
|
188
|
+
scrollToItem(i)
|
|
83
189
|
}
|
|
84
|
-
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function prev() {
|
|
193
|
+
if (isPageBased.value) {
|
|
194
|
+
if (!trackEl.value) return
|
|
195
|
+
trackEl.value.scrollBy({ left: -trackEl.value.clientWidth, behavior: 'smooth' })
|
|
196
|
+
} else {
|
|
197
|
+
const i = activeIndex.value > 0 ? activeIndex.value - 1 : props.items.length - 1
|
|
198
|
+
scrollToItem(i)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function startAutoplay() {
|
|
203
|
+
stopAutoplay()
|
|
204
|
+
if (props.autoplay && props.items.length > 1) {
|
|
205
|
+
autoTimer = setInterval(next, props.interval)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stopAutoplay() {
|
|
210
|
+
if (autoTimer) { clearInterval(autoTimer); autoTimer = null }
|
|
85
211
|
}
|
|
86
212
|
|
|
87
213
|
function onKeydown(e: KeyboardEvent) {
|
|
@@ -89,58 +215,65 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
89
215
|
else if (e.key === 'ArrowLeft') prev()
|
|
90
216
|
}
|
|
91
217
|
|
|
218
|
+
const trackPadding = computed(() => {
|
|
219
|
+
if (props.layout === 'hero') return '0 24px'
|
|
220
|
+
if (props.layout === 'uncontained') return '0 24px'
|
|
221
|
+
return '0'
|
|
222
|
+
})
|
|
223
|
+
|
|
92
224
|
watch(() => props.autoplay, startAutoplay)
|
|
93
|
-
onMounted(
|
|
94
|
-
|
|
225
|
+
onMounted(() => {
|
|
226
|
+
startAutoplay()
|
|
227
|
+
nextTick(() => updateTransforms())
|
|
228
|
+
})
|
|
229
|
+
onBeforeUnmount(() => {
|
|
230
|
+
stopAutoplay()
|
|
231
|
+
if (rafId) cancelAnimationFrame(rafId)
|
|
232
|
+
})
|
|
95
233
|
|
|
96
|
-
defineExpose({ next, prev,
|
|
234
|
+
defineExpose({ next, prev, scrollToItem, scrollToPage })
|
|
97
235
|
</script>
|
|
98
236
|
|
|
99
237
|
<template>
|
|
100
238
|
<div
|
|
101
|
-
class="group relative overflow-hidden
|
|
102
|
-
:class="rounded ? 'rounded-2xl' : ''"
|
|
103
|
-
:style="{ aspectRatio }"
|
|
239
|
+
class="group relative overflow-hidden"
|
|
104
240
|
tabindex="0"
|
|
105
241
|
@keydown="onKeydown"
|
|
106
|
-
@touchstart.passive="onTouchStart"
|
|
107
|
-
@touchend.passive="onTouchEnd"
|
|
108
242
|
@mouseenter="stopAutoplay"
|
|
109
243
|
@mouseleave="startAutoplay"
|
|
110
244
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@
|
|
117
|
-
@after-leave="onTransitionEnd"
|
|
245
|
+
<div
|
|
246
|
+
ref="trackEl"
|
|
247
|
+
class="carousel-track flex overflow-x-auto"
|
|
248
|
+
:class="layout === 'multi-browse' ? 'snap-start' : 'snap-center'"
|
|
249
|
+
:style="{ height, gap: `${gap}px`, padding: trackPadding }"
|
|
250
|
+
@scroll.passive="onScroll"
|
|
118
251
|
>
|
|
119
252
|
<div
|
|
120
253
|
v-for="(item, i) in items"
|
|
121
|
-
v-show="i === current"
|
|
122
254
|
:key="i"
|
|
123
|
-
|
|
255
|
+
:ref="(el) => setItemRef(el, i)"
|
|
256
|
+
class="relative overflow-hidden rounded-2xl"
|
|
257
|
+
:style="itemStyle(i)"
|
|
124
258
|
>
|
|
125
259
|
<img
|
|
126
260
|
:src="item.src"
|
|
127
|
-
:alt="item.alt ?? item.
|
|
128
|
-
class="h-full w-full object-cover"
|
|
261
|
+
:alt="item.alt ?? item.label ?? ''"
|
|
262
|
+
class="pointer-events-none h-full w-full object-cover"
|
|
263
|
+
:style="imgStyle(i)"
|
|
129
264
|
/>
|
|
130
265
|
|
|
131
|
-
<!-- Overlay with text -->
|
|
132
266
|
<div
|
|
133
|
-
v-if="item.
|
|
134
|
-
class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent px-
|
|
267
|
+
v-if="item.label || item.supportingText"
|
|
268
|
+
class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent px-5 pb-4 pt-12"
|
|
135
269
|
>
|
|
136
|
-
<
|
|
137
|
-
<p v-if="item.
|
|
270
|
+
<p v-if="item.label" class="text-title-medium font-medium text-white">{{ item.label }}</p>
|
|
271
|
+
<p v-if="item.supportingText" class="mt-0.5 text-body-small text-white/80">{{ item.supportingText }}</p>
|
|
138
272
|
</div>
|
|
139
273
|
</div>
|
|
140
|
-
</
|
|
274
|
+
</div>
|
|
141
275
|
|
|
142
|
-
|
|
143
|
-
<template v-if="showArrows && total > 1">
|
|
276
|
+
<template v-if="showArrows && items.length > 1">
|
|
144
277
|
<button
|
|
145
278
|
type="button"
|
|
146
279
|
class="absolute left-3 top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-surface/80 text-on-surface shadow-elevation-1 opacity-0 backdrop-blur-sm transition-all duration-200 hover:bg-surface hover:shadow-elevation-2 group-hover:opacity-100"
|
|
@@ -157,47 +290,36 @@ defineExpose({ next, prev, goTo })
|
|
|
157
290
|
</button>
|
|
158
291
|
</template>
|
|
159
292
|
|
|
160
|
-
|
|
293
|
+
</div>
|
|
294
|
+
|
|
161
295
|
<div
|
|
162
|
-
v-if="showIndicators &&
|
|
163
|
-
class="
|
|
164
|
-
:class="currentItem?.title || currentItem?.subtitle ? 'pb-20' : 'pb-3'"
|
|
296
|
+
v-if="showIndicators && totalPages > 1"
|
|
297
|
+
class="flex justify-center gap-1.5 pt-3"
|
|
165
298
|
>
|
|
166
299
|
<button
|
|
167
|
-
v-for="
|
|
168
|
-
:key="
|
|
300
|
+
v-for="p in totalPages"
|
|
301
|
+
:key="p"
|
|
169
302
|
type="button"
|
|
170
303
|
class="h-2 cursor-pointer rounded-full transition-all duration-300"
|
|
171
|
-
:class="
|
|
172
|
-
@click="
|
|
304
|
+
:class="(p - 1) === activePage ? 'w-5 bg-on-surface' : 'w-2 bg-on-surface/30 hover:bg-on-surface/50'"
|
|
305
|
+
@click="isPageBased ? scrollToPage(p - 1) : scrollToItem(p - 1)"
|
|
173
306
|
/>
|
|
174
307
|
</div>
|
|
175
|
-
</div>
|
|
176
308
|
</template>
|
|
177
309
|
|
|
178
|
-
<style>
|
|
179
|
-
.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
transition: transform 500ms cubic-bezier(0.2, 0, 0, 1), opacity 500ms cubic-bezier(0.2, 0, 0, 1);
|
|
310
|
+
<style scoped>
|
|
311
|
+
.carousel-track {
|
|
312
|
+
scroll-snap-type: x mandatory;
|
|
313
|
+
-webkit-overflow-scrolling: touch;
|
|
314
|
+
scrollbar-width: none;
|
|
184
315
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
transform: translateX(100%);
|
|
188
|
-
opacity: 0;
|
|
189
|
-
}
|
|
190
|
-
.m3-carousel-right-leave-to {
|
|
191
|
-
transform: translateX(-100%);
|
|
192
|
-
opacity: 0;
|
|
316
|
+
.carousel-track::-webkit-scrollbar {
|
|
317
|
+
display: none;
|
|
193
318
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
transform: translateX(-100%);
|
|
197
|
-
opacity: 0;
|
|
319
|
+
.snap-center > * {
|
|
320
|
+
scroll-snap-align: center;
|
|
198
321
|
}
|
|
199
|
-
.
|
|
200
|
-
|
|
201
|
-
opacity: 0;
|
|
322
|
+
.snap-start > * {
|
|
323
|
+
scroll-snap-align: start;
|
|
202
324
|
}
|
|
203
325
|
</style>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import MAvatar from './MAvatar.vue'
|
|
4
|
+
import MIcon from './MIcon.vue'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
text?: string
|
|
9
|
+
sender?: string
|
|
10
|
+
time?: string
|
|
11
|
+
side?: 'left' | 'right'
|
|
12
|
+
avatar?: string
|
|
13
|
+
avatarSrc?: string
|
|
14
|
+
status?: 'sent' | 'delivered' | 'read'
|
|
15
|
+
color?: 'primary' | 'secondary' | 'tertiary'
|
|
16
|
+
tail?: boolean
|
|
17
|
+
size?: 'small' | 'medium' | 'large'
|
|
18
|
+
maxWidth?: string
|
|
19
|
+
}>(),
|
|
20
|
+
{ side: 'left', color: 'primary', tail: true, size: 'medium', maxWidth: '320px' },
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const isRight = computed(() => props.side === 'right')
|
|
24
|
+
|
|
25
|
+
const sizeClass = computed(() => {
|
|
26
|
+
if (props.size === 'small') return 'text-body-small'
|
|
27
|
+
if (props.size === 'large') return 'text-body-large'
|
|
28
|
+
return 'text-body-medium'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const colorMap: Record<string, { bg: string; text: string; timeCls: string }> = {
|
|
32
|
+
primary: { bg: 'bg-primary', text: 'text-on-primary', timeCls: 'text-on-primary/60' },
|
|
33
|
+
secondary: { bg: 'bg-secondary', text: 'text-on-secondary', timeCls: 'text-on-secondary/60' },
|
|
34
|
+
tertiary: { bg: 'bg-tertiary', text: 'text-on-tertiary', timeCls: 'text-on-tertiary/60' },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bubbleClasses = computed(() => {
|
|
38
|
+
const classes = ['rounded-2xl', 'px-3', 'py-2']
|
|
39
|
+
|
|
40
|
+
if (isRight.value) {
|
|
41
|
+
const c = colorMap[props.color]!
|
|
42
|
+
classes.push(c.bg, c.text)
|
|
43
|
+
if (props.tail) classes.push('rounded-br-sm')
|
|
44
|
+
} else {
|
|
45
|
+
classes.push('bg-surface-container-high', 'text-on-surface')
|
|
46
|
+
if (props.tail) classes.push('rounded-bl-sm')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return classes
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const timeClasses = computed(() =>
|
|
53
|
+
isRight.value ? colorMap[props.color]!.timeCls : 'text-on-surface-variant',
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const statusIcon = computed(() => {
|
|
57
|
+
if (!props.status) return null
|
|
58
|
+
return props.status === 'sent' ? 'check' : 'done_all'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const statusClasses = computed(() => {
|
|
62
|
+
if (props.status === 'sent') return 'text-on-primary/40'
|
|
63
|
+
if (props.status === 'delivered') return 'text-on-primary/60'
|
|
64
|
+
if (props.status === 'read') return 'text-on-primary'
|
|
65
|
+
return ''
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div class="flex gap-2" :class="isRight ? 'justify-end' : 'justify-start'">
|
|
71
|
+
<MAvatar
|
|
72
|
+
v-if="avatar && !isRight"
|
|
73
|
+
:name="avatar"
|
|
74
|
+
:size="32"
|
|
75
|
+
class="shrink-0 mt-auto"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<div class="w-fit" :style="{ maxWidth: maxWidth }">
|
|
79
|
+
<p v-if="sender && !isRight" class="text-label-small font-medium text-on-surface-variant mb-0.5 ml-1">
|
|
80
|
+
{{ sender }}
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<div :class="bubbleClasses">
|
|
84
|
+
<p :class="sizeClass"><slot>{{ text }}</slot></p>
|
|
85
|
+
|
|
86
|
+
<div v-if="time || (status && isRight)" class="flex items-center justify-end gap-1 mt-0.5">
|
|
87
|
+
<span v-if="time" class="text-label-small" :class="timeClasses">{{ time }}</span>
|
|
88
|
+
<MIcon
|
|
89
|
+
v-if="status && isRight"
|
|
90
|
+
:name="statusIcon!"
|
|
91
|
+
:size="14"
|
|
92
|
+
:class="statusClasses"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
@@ -146,10 +146,7 @@ function onOut(e: MouseEvent) {
|
|
|
146
146
|
function onScroll(e: Event) {
|
|
147
147
|
if (!open.value) return
|
|
148
148
|
if (panelEl.value?.contains(e.target as Node)) return
|
|
149
|
-
|
|
150
|
-
const rect = triggerEl.value.getBoundingClientRect()
|
|
151
|
-
if (rect.bottom < 0 || rect.top > window.innerHeight) { open.value = false; return }
|
|
152
|
-
computeDropPos()
|
|
149
|
+
open.value = false
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
watch(open, (v) => {
|