@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,163 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useNotification } from '../composables/useNotification'
4
+ import MIcon from './MIcon.vue'
5
+ import MSpinner from './MSpinner.vue'
6
+
7
+ const { notifications, position, dismiss } = useNotification()
8
+
9
+ const isTop = computed(() => position.value.startsWith('top'))
10
+
11
+ const containerClass = computed(() => {
12
+ const base = 'pointer-events-none fixed z-[300] flex flex-col'
13
+ switch (position.value) {
14
+ case 'top-left': return `${base} top-4 left-4 items-start`
15
+ case 'top-center': return `${base} top-4 left-1/2 -translate-x-1/2 items-center`
16
+ case 'top-right': return `${base} top-4 right-4 items-end`
17
+ case 'bottom-left': return `${base} bottom-4 left-4 items-start`
18
+ case 'bottom-right': return `${base} bottom-4 right-4 items-end`
19
+ default: return `${base} bottom-4 left-1/2 -translate-x-1/2 items-center`
20
+ }
21
+ })
22
+
23
+ const variantStyles: Record<string, { bg: string; icon: string; iconName: string }> = {
24
+ info: {
25
+ bg: 'bg-inverse-surface text-inverse-on-surface',
26
+ icon: 'text-inverse-on-surface/70',
27
+ iconName: 'info',
28
+ },
29
+ success: {
30
+ bg: 'bg-inverse-surface text-inverse-on-surface',
31
+ icon: 'text-[#4ade80]',
32
+ iconName: 'check_circle',
33
+ },
34
+ warning: {
35
+ bg: 'bg-inverse-surface text-inverse-on-surface',
36
+ icon: 'text-[#fcd34d]',
37
+ iconName: 'warning',
38
+ },
39
+ error: {
40
+ bg: 'bg-inverse-surface text-inverse-on-surface',
41
+ icon: 'text-[#f87171]',
42
+ iconName: 'error',
43
+ },
44
+ }
45
+
46
+ const getStyle = (variant: string) => variantStyles[variant] ?? variantStyles.info!
47
+ </script>
48
+
49
+ <template>
50
+ <div :class="containerClass">
51
+ <TransitionGroup :name="isTop ? 'm3-notif-top' : 'm3-notif-bot'">
52
+ <div v-for="n in notifications" :key="n.id" class="notif-row w-full min-w-56 max-w-sm">
53
+ <div
54
+ class="notif-inner pointer-events-auto flex items-center gap-2.5 rounded px-4 py-3 shadow-elevation-2"
55
+ :class="getStyle(n.variant).bg"
56
+ >
57
+ <MSpinner v-if="n.loading" :size="18" class="shrink-0" />
58
+ <MIcon
59
+ v-else
60
+ :name="n.icon ?? getStyle(n.variant).iconName"
61
+ :size="18"
62
+ class="shrink-0"
63
+ :class="getStyle(n.variant).icon"
64
+ />
65
+ <p class="flex-1 text-body-medium leading-snug">{{ n.message }}</p>
66
+ <div class="flex shrink-0 items-center gap-1">
67
+ <button
68
+ v-if="n.action"
69
+ type="button"
70
+ class="cursor-pointer rounded px-2 py-1 text-label-medium font-semibold text-inverse-primary transition-colors hover:bg-inverse-on-surface/10"
71
+ @click="n.action!.onClick(); dismiss(n.id)"
72
+ >
73
+ {{ n.action.label }}
74
+ </button>
75
+ <button
76
+ v-if="n.closable !== false"
77
+ type="button"
78
+ class="flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-inverse-on-surface/50 transition-colors hover:bg-inverse-on-surface/10"
79
+ @click="dismiss(n.id)"
80
+ >
81
+ <MIcon name="close" :size="16" />
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </TransitionGroup>
87
+ </div>
88
+ </template>
89
+
90
+ <style scoped>
91
+ .notif-row {
92
+ display: grid;
93
+ grid-template-rows: 1fr;
94
+ padding-bottom: 6px;
95
+ }
96
+ .notif-row > .notif-inner {
97
+ min-height: 0;
98
+ }
99
+
100
+ .m3-notif-bot-enter-active {
101
+ transition: grid-template-rows 200ms ease, padding-bottom 200ms ease;
102
+ overflow: hidden;
103
+ }
104
+ .m3-notif-bot-enter-active > .notif-inner {
105
+ transition: opacity 150ms ease, transform 200ms ease;
106
+ }
107
+ .m3-notif-bot-enter-from {
108
+ grid-template-rows: 0fr;
109
+ padding-bottom: 0;
110
+ }
111
+ .m3-notif-bot-enter-from > .notif-inner {
112
+ opacity: 0;
113
+ transform: translateY(12px) scale(0.95);
114
+ }
115
+
116
+ .m3-notif-bot-leave-active {
117
+ transition: grid-template-rows 250ms ease, padding-bottom 250ms ease;
118
+ overflow: hidden;
119
+ }
120
+ .m3-notif-bot-leave-active > .notif-inner {
121
+ transition: opacity 150ms ease, transform 150ms ease;
122
+ }
123
+ .m3-notif-bot-leave-to {
124
+ grid-template-rows: 0fr;
125
+ padding-bottom: 0;
126
+ }
127
+ .m3-notif-bot-leave-to > .notif-inner {
128
+ opacity: 0;
129
+ transform: scale(0.93);
130
+ }
131
+
132
+ .m3-notif-top-enter-active {
133
+ transition: grid-template-rows 200ms ease, padding-bottom 200ms ease;
134
+ overflow: hidden;
135
+ }
136
+ .m3-notif-top-enter-active > .notif-inner {
137
+ transition: opacity 150ms ease, transform 200ms ease;
138
+ }
139
+ .m3-notif-top-enter-from {
140
+ grid-template-rows: 0fr;
141
+ padding-bottom: 0;
142
+ }
143
+ .m3-notif-top-enter-from > .notif-inner {
144
+ opacity: 0;
145
+ transform: translateY(-12px) scale(0.95);
146
+ }
147
+
148
+ .m3-notif-top-leave-active {
149
+ transition: grid-template-rows 250ms ease, padding-bottom 250ms ease;
150
+ overflow: hidden;
151
+ }
152
+ .m3-notif-top-leave-active > .notif-inner {
153
+ transition: opacity 150ms ease, transform 150ms ease;
154
+ }
155
+ .m3-notif-top-leave-to {
156
+ grid-template-rows: 0fr;
157
+ padding-bottom: 0;
158
+ }
159
+ .m3-notif-top-leave-to > .notif-inner {
160
+ opacity: 0;
161
+ transform: scale(0.93);
162
+ }
163
+ </style>
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref, useId, onMounted, onUnmounted } from 'vue'
2
+ import { computed, ref, useId, onMounted, onUnmounted, watch } from 'vue'
3
3
  import MIcon from './MIcon.vue'
4
+ import MIconButton from './MIconButton.vue'
4
5
  import { useFieldBg } from '../composables/useFieldBg'
6
+ import { useLocale } from '../composables/useLocale'
5
7
 
6
8
  export interface SelectOption {
7
9
  label: string
@@ -16,6 +18,7 @@ const props = withDefaults(
16
18
  label?: string
17
19
  placeholder?: string
18
20
  variant?: 'filled' | 'outlined'
21
+ mode?: 'docked' | 'modal'
19
22
  disabled?: boolean
20
23
  error?: string
21
24
  hint?: string
@@ -27,6 +30,7 @@ const props = withDefaults(
27
30
  {
28
31
  modelValue: undefined,
29
32
  variant: 'filled',
33
+ mode: 'docked',
30
34
  disabled: false,
31
35
  required: false,
32
36
  clearable: false,
@@ -35,8 +39,10 @@ const props = withDefaults(
35
39
 
36
40
  const emit = defineEmits<{ 'update:modelValue': [unknown] }>()
37
41
 
42
+ const locale = useLocale()
38
43
  const id = useId()
39
44
  const open = ref(false)
45
+ const modalOpen = ref(false)
40
46
  const fieldEl = ref<HTMLElement | null>(null)
41
47
  const { resolvedFieldBg } = useFieldBg(fieldEl, () => props.fieldBg)
42
48
  const dropdownEl = ref<HTMLElement | null>(null)
@@ -68,6 +74,10 @@ function computeDropPos() {
68
74
 
69
75
  function toggle() {
70
76
  if (props.disabled) return
77
+ if (props.mode === 'modal') {
78
+ modalOpen.value = !modalOpen.value
79
+ return
80
+ }
71
81
  if (!open.value) computeDropPos()
72
82
  open.value = !open.value
73
83
  }
@@ -76,6 +86,7 @@ function select(opt: SelectOption) {
76
86
  if (opt.disabled) return
77
87
  emit('update:modelValue', opt.value)
78
88
  open.value = false
89
+ modalOpen.value = false
79
90
  }
80
91
 
81
92
  function onOutsideClick(e: MouseEvent) {
@@ -85,23 +96,14 @@ function onOutsideClick(e: MouseEvent) {
85
96
 
86
97
  function onScroll(e: Event) {
87
98
  if (!open.value) return
88
- // Scrolling inside the dropdown list itself — do nothing
89
99
  if (dropdownEl.value?.contains(e.target as Node)) return
90
- // Recompute position to track the trigger element as the page scrolls
91
- if (!fieldEl.value) return
92
- const rect = fieldEl.value.getBoundingClientRect()
93
- // Only close if the trigger has scrolled completely out of the viewport
94
- if (rect.bottom < 0 || rect.top > window.innerHeight) {
95
- open.value = false
96
- return
97
- }
98
- computeDropPos()
100
+ open.value = false
99
101
  }
100
102
 
101
103
  function onKeydown(e: KeyboardEvent) {
102
- if (e.key === 'Escape') { open.value = false; return }
104
+ if (e.key === 'Escape') { open.value = false; modalOpen.value = false; return }
103
105
  if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); return }
104
- if (!open.value) return
106
+ if (!open.value && !modalOpen.value) return
105
107
  const opts = props.options.filter((o) => !o.disabled)
106
108
  const idx = opts.findIndex((o) => eq(o.value, props.modelValue))
107
109
  if (e.key === 'ArrowDown') {
@@ -116,6 +118,16 @@ function onKeydown(e: KeyboardEvent) {
116
118
  }
117
119
  }
118
120
 
121
+ watch(modalOpen, (v) => {
122
+ if (v) {
123
+ document.body.style.overflow = 'hidden'
124
+ } else {
125
+ document.body.style.overflow = ''
126
+ }
127
+ })
128
+
129
+ function closeModal() { modalOpen.value = false }
130
+
119
131
  onMounted(() => {
120
132
  document.addEventListener('mousedown', onOutsideClick)
121
133
  window.addEventListener('scroll', onScroll, true)
@@ -123,6 +135,7 @@ onMounted(() => {
123
135
  onUnmounted(() => {
124
136
  document.removeEventListener('mousedown', onOutsideClick)
125
137
  window.removeEventListener('scroll', onScroll, true)
138
+ document.body.style.overflow = ''
126
139
  })
127
140
 
128
141
  const triggerClasses = computed(() => {
@@ -226,9 +239,10 @@ const labelClasses = computed(() => {
226
239
  <!-- Arrow icon -->
227
240
  <div class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 flex h-6 items-center">
228
241
  <MIcon
229
- :name="open ? 'arrow_drop_up' : 'arrow_drop_down'"
242
+ name="arrow_drop_down"
230
243
  :size="24"
231
244
  class="text-on-surface-variant transition-transform duration-200"
245
+ :class="open || modalOpen ? 'rotate-180' : ''"
232
246
  />
233
247
  </div>
234
248
  </div>
@@ -237,7 +251,7 @@ const labelClasses = computed(() => {
237
251
  <p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
238
252
  </div>
239
253
 
240
- <!-- Dropdown teleported to body to escape overflow clipping -->
254
+ <!-- Docked dropdown -->
241
255
  <Teleport to="body">
242
256
  <Transition
243
257
  enter-active-class="transition-[opacity,transform] duration-150"
@@ -248,7 +262,7 @@ const labelClasses = computed(() => {
248
262
  leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
249
263
  >
250
264
  <div
251
- v-if="open"
265
+ v-if="open && mode === 'docked'"
252
266
  ref="dropdownEl"
253
267
  class="fixed z-500 max-h-60 overflow-auto rounded-sm bg-surface-container py-1 shadow-elevation-2"
254
268
  :style="dropPos"
@@ -282,5 +296,74 @@ const labelClasses = computed(() => {
282
296
  </p>
283
297
  </div>
284
298
  </Transition>
299
+
300
+ <!-- Modal -->
301
+ <Transition name="m3-select-modal">
302
+ <div
303
+ v-if="modalOpen && mode === 'modal'"
304
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
305
+ @click.self="closeModal"
306
+ >
307
+ <div class="select-modal-box flex max-h-[80vh] w-full max-w-sm flex-col overflow-hidden rounded-[28px] bg-surface-container-high shadow-elevation-3">
308
+ <!-- Header -->
309
+ <div class="flex items-center justify-between px-6 pt-6 pb-4">
310
+ <h2 class="text-headline-small text-on-surface">{{ label || placeholder }}</h2>
311
+ <MIconButton icon="close" :label="locale.close" @click="closeModal" />
312
+ </div>
313
+
314
+ <div class="h-px bg-outline-variant" />
315
+
316
+ <!-- Options list -->
317
+ <div class="flex-1 overflow-y-auto py-2">
318
+ <div
319
+ v-for="(opt, i) in options"
320
+ :key="i"
321
+ class="flex cursor-pointer items-center gap-4 px-6 py-3.5 text-body-large transition-colors"
322
+ :class="[
323
+ opt.disabled
324
+ ? 'cursor-not-allowed opacity-38 text-on-surface'
325
+ : 'text-on-surface hover:bg-on-surface/8',
326
+ eq(opt.value, modelValue) ? 'bg-primary/8' : '',
327
+ ]"
328
+ @click="select(opt)"
329
+ >
330
+ <MIcon
331
+ :name="eq(opt.value, modelValue) ? 'radio_button_checked' : 'radio_button_unchecked'"
332
+ :size="20"
333
+ :class="eq(opt.value, modelValue) ? 'text-primary' : 'text-on-surface-variant'"
334
+ />
335
+ <span :class="eq(opt.value, modelValue) ? 'text-primary font-medium' : ''">
336
+ {{ opt.label }}
337
+ </span>
338
+ </div>
339
+ <p
340
+ v-if="!options.length"
341
+ class="px-6 py-4 text-center text-body-medium text-on-surface-variant"
342
+ >
343
+ Sin opciones
344
+ </p>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ </Transition>
285
349
  </Teleport>
286
350
  </template>
351
+
352
+ <style scoped>
353
+ .m3-select-modal-enter-active,
354
+ .m3-select-modal-leave-active {
355
+ transition: opacity 0.15s ease;
356
+ }
357
+ .m3-select-modal-enter-from,
358
+ .m3-select-modal-leave-to {
359
+ opacity: 0;
360
+ }
361
+ .m3-select-modal-enter-active .select-modal-box,
362
+ .m3-select-modal-leave-active .select-modal-box {
363
+ transition: transform 0.15s ease;
364
+ }
365
+ .m3-select-modal-enter-from .select-modal-box,
366
+ .m3-select-modal-leave-to .select-modal-box {
367
+ transform: scale(0.95);
368
+ }
369
+ </style>