@m3ui-vue/m3ui-vue 0.3.1 → 0.4.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.
Files changed (83) hide show
  1. package/README.md +219 -59
  2. package/dist/{MMenuItem-C6RwHl-l.js → MMenuItem-FyttjOj_.js} +399 -207
  3. package/dist/MMenuItem-FyttjOj_.js.map +1 -0
  4. package/dist/components/MAutocomplete.vue.d.ts +2 -0
  5. package/dist/components/MBox.vue.d.ts +2 -2
  6. package/dist/components/MButton.vue.d.ts +4 -0
  7. package/dist/components/MButtonGroup.vue.d.ts +28 -0
  8. package/dist/components/MCarousel.vue.d.ts +21 -12
  9. package/dist/components/MColorPickerModal.vue.d.ts +18 -0
  10. package/dist/components/MDataTable.vue.d.ts +1 -1
  11. package/dist/components/MDatePickerModal.vue.d.ts +21 -0
  12. package/dist/components/MDateRangePickerModal.vue.d.ts +24 -0
  13. package/dist/components/MDialog.vue.d.ts +9 -1
  14. package/dist/components/MExpansionPanel.vue.d.ts +3 -2
  15. package/dist/components/MFab.vue.d.ts +14 -2
  16. package/dist/components/MFlex.vue.d.ts +2 -2
  17. package/dist/components/MIconButton.vue.d.ts +4 -2
  18. package/dist/components/MInfiniteScroll.vue.d.ts +1 -1
  19. package/dist/components/MList.vue.d.ts +2 -0
  20. package/dist/components/MLoadingOverlay.vue.d.ts +1 -1
  21. package/dist/components/MMenu.vue.d.ts +3 -1
  22. package/dist/components/MMenuItem.vue.d.ts +14 -3
  23. package/dist/components/MMultiAutocomplete.vue.d.ts +2 -0
  24. package/dist/components/MMultiSelect.vue.d.ts +2 -0
  25. package/dist/components/MNavigationDrawer.vue.d.ts +1 -1
  26. package/dist/components/MNotificationHost.vue.d.ts +3 -0
  27. package/dist/components/MSelect.vue.d.ts +2 -0
  28. package/dist/components/MSlider.vue.d.ts +15 -4
  29. package/dist/components/MSplitButton.vue.d.ts +39 -0
  30. package/dist/components/MSplitter.vue.d.ts +1 -1
  31. package/dist/components/MStack.vue.d.ts +2 -2
  32. package/dist/components/MTable.vue.d.ts +1 -1
  33. package/dist/components/MTimePickerModal.vue.d.ts +21 -0
  34. package/dist/components/MToolbar.vue.d.ts +30 -0
  35. package/dist/components/MTooltip.vue.d.ts +8 -2
  36. package/dist/components/MWindow.vue.d.ts +2 -2
  37. package/dist/composables/useDevice.d.ts +3 -0
  38. package/dist/composables/useNotification.d.ts +64 -0
  39. package/dist/composables/useToast.d.ts +8 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/m3ui-vue.css +1 -1
  42. package/dist/m3ui.js +5222 -2994
  43. package/dist/m3ui.js.map +1 -1
  44. package/dist/rich-text-editor.js +10 -10
  45. package/dist/styles.css +1 -1
  46. package/package.json +1 -1
  47. package/src/components/MAutocomplete.vue +109 -13
  48. package/src/components/MButton.vue +38 -16
  49. package/src/components/MButtonGroup.vue +158 -0
  50. package/src/components/MCarousel.vue +223 -101
  51. package/src/components/MColorPicker.vue +1 -4
  52. package/src/components/MColorPickerModal.vue +276 -0
  53. package/src/components/MDatePicker.vue +1 -4
  54. package/src/components/MDatePickerModal.vue +265 -0
  55. package/src/components/MDateRangePicker.vue +1 -4
  56. package/src/components/MDateRangePickerModal.vue +264 -0
  57. package/src/components/MDialog.vue +38 -1
  58. package/src/components/MExpansionPanel.vue +10 -32
  59. package/src/components/MFab.vue +92 -11
  60. package/src/components/MIconButton.vue +36 -6
  61. package/src/components/MList.vue +6 -3
  62. package/src/components/MListItem.vue +9 -3
  63. package/src/components/MMenu.vue +10 -5
  64. package/src/components/MMenuItem.vue +93 -10
  65. package/src/components/MMultiAutocomplete.vue +105 -10
  66. package/src/components/MMultiSelect.vue +106 -13
  67. package/src/components/MNotificationHost.vue +163 -0
  68. package/src/components/MSelect.vue +99 -16
  69. package/src/components/MSlider.vue +367 -134
  70. package/src/components/MSnackbar.vue +3 -0
  71. package/src/components/MSplitButton.vue +240 -0
  72. package/src/components/MTabs.vue +82 -39
  73. package/src/components/MTagInput.vue +11 -1
  74. package/src/components/MTimePicker.vue +1 -4
  75. package/src/components/MTimePickerModal.vue +458 -0
  76. package/src/components/MToolbar.vue +75 -0
  77. package/src/components/MTooltip.vue +60 -10
  78. package/src/components/_MContextMenuPanel.vue +2 -2
  79. package/src/composables/useDevice.ts +29 -0
  80. package/src/composables/useNotification.ts +93 -0
  81. package/src/composables/useToast.ts +42 -5
  82. package/src/index.ts +12 -0
  83. package/dist/MMenuItem-C6RwHl-l.js.map +0 -1
@@ -0,0 +1,240 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, ref } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+
5
+ export interface SplitButtonItem {
6
+ label: string
7
+ icon?: string
8
+ disabled?: boolean
9
+ onClick?: () => void
10
+ }
11
+
12
+ const props = withDefaults(defineProps<{
13
+ label: string
14
+ icon?: string
15
+ variant?: 'filled' | 'tonal' | 'outlined' | 'elevated'
16
+ color?: 'primary' | 'secondary' | 'tertiary'
17
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
18
+ disabled?: boolean
19
+ items?: SplitButtonItem[]
20
+ }>(), {
21
+ variant: 'tonal',
22
+ color: 'primary',
23
+ size: 'sm',
24
+ disabled: false,
25
+ })
26
+
27
+ const emit = defineEmits<{ click: [MouseEvent] }>()
28
+
29
+ const open = ref(false)
30
+ const containerEl = ref<HTMLElement>()
31
+ const menuEl = ref<HTMLElement>()
32
+ const menuStyle = ref<Record<string, string>>({})
33
+
34
+ const colorStyles = computed(() => {
35
+ const map: Record<string, Record<string, { bg: string; text: string; hover: string; divider: string }>> = {
36
+ filled: {
37
+ primary: { bg: 'bg-primary', text: 'text-on-primary', hover: 'hover:brightness-110', divider: 'bg-on-primary/20' },
38
+ secondary: { bg: 'bg-secondary', text: 'text-on-secondary', hover: 'hover:brightness-110', divider: 'bg-on-secondary/20' },
39
+ tertiary: { bg: 'bg-tertiary', text: 'text-on-tertiary', hover: 'hover:brightness-110', divider: 'bg-on-tertiary/20' },
40
+ },
41
+ tonal: {
42
+ primary: { bg: 'bg-primary-container', text: 'text-on-primary-container', hover: 'hover:bg-primary-container/80', divider: 'bg-on-primary-container/20' },
43
+ secondary: { bg: 'bg-secondary-container', text: 'text-on-secondary-container', hover: 'hover:bg-secondary-container/80', divider: 'bg-on-secondary-container/20' },
44
+ tertiary: { bg: 'bg-tertiary-container', text: 'text-on-tertiary-container', hover: 'hover:bg-tertiary-container/80', divider: 'bg-on-tertiary-container/20' },
45
+ },
46
+ outlined: {
47
+ primary: { bg: 'bg-transparent', text: 'text-primary', hover: 'hover:bg-primary/8', divider: 'bg-outline' },
48
+ secondary: { bg: 'bg-transparent', text: 'text-secondary', hover: 'hover:bg-secondary/8', divider: 'bg-outline' },
49
+ tertiary: { bg: 'bg-transparent', text: 'text-tertiary', hover: 'hover:bg-tertiary/8', divider: 'bg-outline' },
50
+ },
51
+ elevated: {
52
+ primary: { bg: 'bg-surface-container-low', text: 'text-primary', hover: 'hover:bg-surface-container', divider: 'bg-outline-variant' },
53
+ secondary: { bg: 'bg-surface-container-low', text: 'text-secondary', hover: 'hover:bg-surface-container', divider: 'bg-outline-variant' },
54
+ tertiary: { bg: 'bg-surface-container-low', text: 'text-tertiary', hover: 'hover:bg-surface-container', divider: 'bg-outline-variant' },
55
+ },
56
+ }
57
+ return map[props.variant]?.[props.color] ?? map.tonal!.primary!
58
+ })
59
+
60
+ const containerClasses = computed(() => {
61
+ return 'inline-flex items-center gap-0.5'
62
+ })
63
+
64
+ const sizeConfig = computed(() => {
65
+ const map = {
66
+ xs: { h: 'h-8', w: 'w-8', px: 'px-3', text: 'text-label-medium', icon: 16, arrow: 16, r: 16, ri: 4 },
67
+ sm: { h: 'h-10', w: 'w-10', px: 'px-4', text: 'text-label-large', icon: 18, arrow: 18, r: 20, ri: 6 },
68
+ md: { h: 'h-14', w: 'w-14', px: 'px-5', text: 'text-title-medium', icon: 20, arrow: 20, r: 28, ri: 6 },
69
+ lg: { h: 'h-16', w: 'w-16', px: 'px-6', text: 'text-title-large', icon: 22, arrow: 22, r: 32, ri: 7 },
70
+ xl: { h: 'h-20', w: 'w-20', px: 'px-7', text: 'text-headline-small', icon: 24, arrow: 24, r: 40, ri: 8 },
71
+ }
72
+ return map[props.size] ?? map.sm
73
+ })
74
+
75
+ function computeMenuPos() {
76
+ if (!containerEl.value) return
77
+ const rect = containerEl.value.getBoundingClientRect()
78
+ const menuH = (props.items?.length ?? 0) * 44 + 16
79
+ const spaceBelow = window.innerHeight - rect.bottom - 8
80
+ const above = spaceBelow < menuH && rect.top > menuH
81
+ menuStyle.value = {
82
+ position: 'fixed',
83
+ zIndex: '500',
84
+ left: `${rect.left}px`,
85
+ minWidth: `${rect.width}px`,
86
+ ...(above
87
+ ? { bottom: `${window.innerHeight - rect.top + 4}px` }
88
+ : { top: `${rect.bottom + 4}px` }),
89
+ }
90
+ }
91
+
92
+ function toggleMenu() {
93
+ if (props.disabled) return
94
+ if (!open.value) computeMenuPos()
95
+ open.value = !open.value
96
+ }
97
+
98
+ function createRipple(event: PointerEvent) {
99
+ const button = event.currentTarget as HTMLElement
100
+ const rect = button.getBoundingClientRect()
101
+ const d = Math.max(rect.width, rect.height) * 2
102
+ const el = document.createElement('span')
103
+ el.className = 'm3-ripple'
104
+ el.style.cssText = `width:${d}px;height:${d}px;top:${event.clientY - rect.top - d / 2}px;left:${event.clientX - rect.left - d / 2}px`
105
+ button.appendChild(el)
106
+ el.addEventListener('animationend', () => el.remove(), { once: true })
107
+ }
108
+
109
+ function selectItem(item: SplitButtonItem) {
110
+ if (item.disabled) return
111
+ item.onClick?.()
112
+ open.value = false
113
+ }
114
+
115
+ function onOutside(e: MouseEvent) {
116
+ const t = e.target as Node
117
+ if (containerEl.value?.contains(t)) return
118
+ if (menuEl.value?.contains(t)) return
119
+ open.value = false
120
+ }
121
+
122
+ function onScroll(e: Event) {
123
+ if (!open.value) return
124
+ if (menuEl.value?.contains(e.target as Node)) return
125
+ open.value = false
126
+ }
127
+
128
+ onMounted(() => {
129
+ document.addEventListener('mousedown', onOutside)
130
+ window.addEventListener('scroll', onScroll, true)
131
+ })
132
+ onUnmounted(() => {
133
+ document.removeEventListener('mousedown', onOutside)
134
+ window.removeEventListener('scroll', onScroll, true)
135
+ })
136
+ </script>
137
+
138
+ <template>
139
+ <div ref="containerEl" :class="containerClasses">
140
+ <!-- Main action -->
141
+ <button
142
+ type="button"
143
+ class="relative flex cursor-pointer items-center gap-2 overflow-hidden font-medium transition-colors"
144
+ :class="[
145
+ sizeConfig.h, sizeConfig.px, sizeConfig.text,
146
+ colorStyles.bg,
147
+ colorStyles.text,
148
+ colorStyles.hover,
149
+ variant === 'outlined' ? 'border border-outline' : '',
150
+ variant === 'elevated' ? 'shadow-elevation-1' : '',
151
+ disabled ? 'pointer-events-none opacity-[0.38]' : '',
152
+ ]"
153
+ :style="{ borderRadius: `${sizeConfig.r}px ${sizeConfig.ri}px ${sizeConfig.ri}px ${sizeConfig.r}px` }"
154
+ :disabled="disabled"
155
+ @pointerdown="createRipple"
156
+ @click="emit('click', $event)"
157
+ >
158
+ <MIcon v-if="icon" :name="icon" :size="sizeConfig.icon" />
159
+ {{ label }}
160
+ </button>
161
+
162
+ <!-- Toggle -->
163
+ <button
164
+ type="button"
165
+ class="toggle-btn relative flex cursor-pointer items-center justify-center overflow-hidden"
166
+ :class="[
167
+ sizeConfig.h, sizeConfig.w,
168
+ colorStyles.bg,
169
+ colorStyles.text,
170
+ colorStyles.hover,
171
+ variant === 'outlined' ? 'border border-outline' : '',
172
+ variant === 'elevated' ? 'shadow-elevation-1' : '',
173
+ disabled ? 'pointer-events-none opacity-[0.38]' : '',
174
+ ]"
175
+ :style="{ borderRadius: open ? `${sizeConfig.r}px` : `${sizeConfig.ri}px ${sizeConfig.r}px ${sizeConfig.r}px ${sizeConfig.ri}px` }"
176
+ :disabled="disabled"
177
+ @pointerdown="createRipple"
178
+ @click="toggleMenu"
179
+ >
180
+ <MIcon
181
+ name="arrow_drop_down"
182
+ :size="sizeConfig.arrow"
183
+ class="transition-transform duration-200"
184
+ :class="open ? 'rotate-180' : ''"
185
+ />
186
+ </button>
187
+ </div>
188
+
189
+ <!-- Dropdown menu -->
190
+ <Teleport to="body">
191
+ <Transition
192
+ enter-active-class="transition-[opacity,transform] duration-150"
193
+ enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
194
+ leave-active-class="transition-[opacity,transform] duration-100"
195
+ leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
196
+ >
197
+ <div
198
+ v-if="open && items?.length"
199
+ ref="menuEl"
200
+ class="overflow-hidden rounded-xl bg-surface-container py-2 shadow-elevation-2"
201
+ :style="menuStyle"
202
+ >
203
+ <button
204
+ v-for="(item, i) in items"
205
+ :key="i"
206
+ type="button"
207
+ class="flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left text-body-medium transition-colors"
208
+ :class="item.disabled ? 'cursor-not-allowed opacity-[0.38] text-on-surface' : 'text-on-surface hover:bg-on-surface/8'"
209
+ :disabled="item.disabled"
210
+ @click="selectItem(item)"
211
+ >
212
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" class="shrink-0 text-on-surface-variant" />
213
+ {{ item.label }}
214
+ </button>
215
+ </div>
216
+ </Transition>
217
+
218
+ <!-- Custom content -->
219
+ <Transition
220
+ enter-active-class="transition-[opacity,transform] duration-150"
221
+ enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
222
+ leave-active-class="transition-[opacity,transform] duration-100"
223
+ leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
224
+ >
225
+ <div
226
+ v-if="open && !items?.length && $slots.menu"
227
+ ref="menuEl"
228
+ :style="menuStyle"
229
+ >
230
+ <slot name="menu" :close="() => open = false" />
231
+ </div>
232
+ </Transition>
233
+ </Teleport>
234
+ </template>
235
+
236
+ <style scoped>
237
+ .toggle-btn {
238
+ transition: border-radius 250ms ease, background-color 150ms ease;
239
+ }
240
+ </style>
@@ -17,24 +17,55 @@ const props = withDefaults(defineProps<{
17
17
 
18
18
  const emit = defineEmits<{ 'update:modelValue': [string | number] }>()
19
19
 
20
- // Primary tab: refs for indicator position
21
20
  const tabEls = ref<HTMLElement[]>([])
22
21
  const indicatorLeft = ref(0)
23
22
  const indicatorWidth = ref(0)
23
+ const stretching = ref(false)
24
+ let stretchTimer: ReturnType<typeof setTimeout> | null = null
24
25
 
25
- function updateIndicator() {
26
+ function getTabRect(index: number) {
27
+ const el = tabEls.value[index]
28
+ if (!el) return null
29
+ return { left: el.offsetLeft, width: el.offsetWidth }
30
+ }
31
+
32
+ function updateIndicator(animate = true) {
26
33
  nextTick(() => {
27
34
  const idx = props.tabs.findIndex((t) => t.value === props.modelValue)
28
- const el = tabEls.value[idx]
29
- if (!el) return
30
- indicatorLeft.value = el.offsetLeft
31
- indicatorWidth.value = el.offsetWidth
35
+ const rect = getTabRect(idx)
36
+ if (!rect) return
37
+
38
+ if (props.variant === 'primary') {
39
+ const inset = Math.min(rect.width * 0.2, 16)
40
+ if (animate && indicatorWidth.value > 0) {
41
+ const oldLeft = indicatorLeft.value
42
+ const oldRight = oldLeft + indicatorWidth.value
43
+ const newLeft = rect.left + inset
44
+ const newRight = rect.left + rect.width - inset
45
+ stretching.value = true
46
+ indicatorLeft.value = Math.min(oldLeft, newLeft)
47
+ indicatorWidth.value = Math.max(oldRight, newRight) - indicatorLeft.value
48
+ if (stretchTimer) clearTimeout(stretchTimer)
49
+ stretchTimer = setTimeout(() => {
50
+ stretching.value = false
51
+ indicatorLeft.value = newLeft
52
+ indicatorWidth.value = newRight - newLeft
53
+ }, 150)
54
+ } else {
55
+ const insetLeft = rect.left + inset
56
+ indicatorLeft.value = insetLeft
57
+ indicatorWidth.value = rect.width - inset * 2
58
+ }
59
+ } else {
60
+ indicatorLeft.value = rect.left
61
+ indicatorWidth.value = rect.width
62
+ }
32
63
  })
33
64
  }
34
65
 
35
- onMounted(updateIndicator)
36
- watch(() => props.modelValue, updateIndicator)
37
- watch(() => props.tabs, updateIndicator, { deep: true })
66
+ onMounted(() => updateIndicator(false))
67
+ watch(() => props.modelValue, () => updateIndicator(true))
68
+ watch(() => props.tabs, () => updateIndicator(false), { deep: true })
38
69
 
39
70
  function select(tab: Tab) {
40
71
  if (!tab.disabled) emit('update:modelValue', tab.value)
@@ -42,22 +73,22 @@ function select(tab: Tab) {
42
73
  </script>
43
74
 
44
75
  <template>
45
- <!-- ── Primary: underline with sliding indicator ──────────────────────── -->
46
- <div v-if="variant === 'primary'" class="relative border-b border-outline-variant">
47
- <div class="flex overflow-x-auto" style="scrollbar-width: none">
76
+ <!-- ── Primary: sliding short indicator with stretch ──────────────── -->
77
+ <div v-if="variant === 'primary'" class="border-b border-outline-variant">
78
+ <div class="relative flex overflow-x-auto" style="scrollbar-width: none">
48
79
  <button
49
80
  v-for="tab in tabs"
50
81
  :key="tab.value"
51
82
  :ref="(el) => { if (el) tabEls[tabs.indexOf(tab)] = el as HTMLElement }"
52
83
  type="button"
53
- class="relative flex h-12 shrink-0 flex-col items-center justify-center gap-0.5 px-6 text-label-large transition-colors duration-150 focus-visible:outline-none"
84
+ class="relative flex shrink-0 flex-col items-center justify-center gap-1 px-6 text-label-large transition-colors duration-150 focus-visible:outline-none"
54
85
  :class="[
86
+ tab.icon ? 'h-14 pb-1.5' : 'h-12 pb-1.5',
55
87
  tab.value === modelValue
56
88
  ? 'text-primary'
57
89
  : tab.disabled
58
90
  ? 'cursor-not-allowed text-on-surface/38'
59
91
  : 'cursor-pointer text-on-surface-variant hover:text-on-surface',
60
- tab.icon ? 'pt-2 pb-1' : '',
61
92
  ]"
62
93
  :disabled="tab.disabled"
63
94
  @click="select(tab)"
@@ -65,33 +96,45 @@ function select(tab: Tab) {
65
96
  <MIcon v-if="tab.icon" :name="tab.icon" :size="20" />
66
97
  <span>{{ tab.label }}</span>
67
98
  </button>
99
+ <div
100
+ class="absolute bottom-0 h-[3px] rounded-t-full bg-primary"
101
+ :style="{
102
+ left: `${indicatorLeft}px`,
103
+ width: `${indicatorWidth}px`,
104
+ transition: stretching
105
+ ? 'left 150ms cubic-bezier(0.4, 0, 0.2, 1), width 150ms cubic-bezier(0.4, 0, 0.2, 1)'
106
+ : 'left 250ms cubic-bezier(0.34, 1.56, 0.64, 1), width 250ms cubic-bezier(0.34, 1.56, 0.64, 1)',
107
+ }"
108
+ />
68
109
  </div>
69
- <!-- Sliding indicator -->
70
- <div
71
- class="absolute bottom-0 h-[3px] rounded-t-sm bg-primary transition-[left,width] duration-200 ease-[cubic-bezier(0.2,0,0,1)]"
72
- :style="{ left: `${indicatorLeft}px`, width: `${indicatorWidth}px` }"
73
- />
74
110
  </div>
75
111
 
76
- <!-- ── Secondary: pill style ──────────────────────────────────────────── -->
77
- <div v-else class="flex flex-wrap gap-1 rounded-full bg-surface-container p-1">
78
- <button
79
- v-for="tab in tabs"
80
- :key="tab.value"
81
- type="button"
82
- class="flex h-8 items-center gap-2 rounded-full px-4 text-label-large transition-all duration-150 focus-visible:outline-none"
83
- :class="
84
- tab.value === modelValue
85
- ? 'bg-secondary-container text-on-secondary-container shadow-elevation-1'
86
- : tab.disabled
87
- ? 'cursor-not-allowed text-on-surface/38'
88
- : 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8 hover:text-on-surface'
89
- "
90
- :disabled="tab.disabled"
91
- @click="select(tab)"
92
- >
93
- <MIcon v-if="tab.icon" :name="tab.icon" :size="16" />
94
- {{ tab.label }}
95
- </button>
112
+ <!-- ── Secondary: full-width underline indicator ─────────────────── -->
113
+ <div v-else class="border-b border-outline-variant">
114
+ <div class="relative flex overflow-x-auto" style="scrollbar-width: none">
115
+ <button
116
+ v-for="tab in tabs"
117
+ :key="tab.value"
118
+ :ref="(el) => { if (el) tabEls[tabs.indexOf(tab)] = el as HTMLElement }"
119
+ type="button"
120
+ class="relative flex h-12 shrink-0 items-center justify-center gap-2 px-6 text-label-large transition-colors duration-150 focus-visible:outline-none"
121
+ :class="
122
+ tab.value === modelValue
123
+ ? 'text-on-surface'
124
+ : tab.disabled
125
+ ? 'cursor-not-allowed text-on-surface/38'
126
+ : 'cursor-pointer text-on-surface-variant hover:text-on-surface'
127
+ "
128
+ :disabled="tab.disabled"
129
+ @click="select(tab)"
130
+ >
131
+ <MIcon v-if="tab.icon" :name="tab.icon" :size="18" />
132
+ {{ tab.label }}
133
+ </button>
134
+ <div
135
+ class="absolute bottom-0 h-[2px] bg-primary transition-[left,width] duration-200 ease-[cubic-bezier(0.2,0,0,1)]"
136
+ :style="{ left: `${indicatorLeft}px`, width: `${indicatorWidth}px` }"
137
+ />
138
+ </div>
96
139
  </div>
97
140
  </template>
@@ -91,6 +91,14 @@ function onPaste(e: ClipboardEvent) {
91
91
  }
92
92
  }
93
93
 
94
+ function onBlur() {
95
+ if (inputValue.value.trim()) {
96
+ addTag(inputValue.value)
97
+ inputValue.value = ''
98
+ }
99
+ focused.value = false
100
+ }
101
+
94
102
  function focusInput() {
95
103
  if (!props.disabled) {
96
104
  inputEl.value?.focus()
@@ -191,6 +199,8 @@ const labelClasses = computed(() => {
191
199
  ref="inputEl"
192
200
  :value="inputValue"
193
201
  type="text"
202
+ enterkeyhint="done"
203
+ autocomplete="off"
194
204
  class="min-w-[60px] flex-1 bg-transparent text-body-large text-on-surface outline-none placeholder:text-on-surface-variant"
195
205
  :placeholder="!hasValue && (!label || focused) ? placeholder : ''"
196
206
  :disabled="disabled"
@@ -199,7 +209,7 @@ const labelClasses = computed(() => {
199
209
  @keydown="onKeydown"
200
210
  @paste="onPaste"
201
211
  @focus="focused = true"
202
- @blur="focused = false"
212
+ @blur="onBlur"
203
213
  />
204
214
  </div>
205
215
 
@@ -92,10 +92,7 @@ function onOut(e: MouseEvent) {
92
92
  function onScroll(e: Event) {
93
93
  if (!open.value) return
94
94
  if (panelEl.value?.contains(e.target as Node)) return
95
- if (!triggerEl.value) return
96
- const rect = triggerEl.value.getBoundingClientRect()
97
- if (rect.bottom < 0 || rect.top > window.innerHeight) { open.value = false; return }
98
- computeDropPos()
95
+ open.value = false
99
96
  }
100
97
 
101
98
  watch(open, (v) => {