@m3ui-vue/m3ui-vue 0.1.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.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/components/MAlert.vue.d.ts +27 -0
  4. package/dist/components/MAppBar.vue.d.ts +24 -0
  5. package/dist/components/MAvatar.vue.d.ts +9 -0
  6. package/dist/components/MBadge.vue.d.ts +22 -0
  7. package/dist/components/MBottomSheet.vue.d.ts +26 -0
  8. package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
  9. package/dist/components/MButton.vue.d.ts +32 -0
  10. package/dist/components/MCalendar.vue.d.ts +23 -0
  11. package/dist/components/MCard.vue.d.ts +28 -0
  12. package/dist/components/MChart.vue.d.ts +13 -0
  13. package/dist/components/MCheckbox.vue.d.ts +26 -0
  14. package/dist/components/MChip.vue.d.ts +33 -0
  15. package/dist/components/MCodeEditor.vue.d.ts +35 -0
  16. package/dist/components/MColorPicker.vue.d.ts +18 -0
  17. package/dist/components/MCommandPalette.vue.d.ts +29 -0
  18. package/dist/components/MConfirmDialog.vue.d.ts +23 -0
  19. package/dist/components/MContainer.vue.d.ts +24 -0
  20. package/dist/components/MContextMenu.vue.d.ts +35 -0
  21. package/dist/components/MDataTable.vue.d.ts +83 -0
  22. package/dist/components/MDatePicker.vue.d.ts +21 -0
  23. package/dist/components/MDateRangePicker.vue.d.ts +24 -0
  24. package/dist/components/MDialog.vue.d.ts +30 -0
  25. package/dist/components/MDivider.vue.d.ts +11 -0
  26. package/dist/components/MDragDropList.vue.d.ts +40 -0
  27. package/dist/components/MEmptyState.vue.d.ts +21 -0
  28. package/dist/components/MExpansionPanel.vue.d.ts +28 -0
  29. package/dist/components/MFab.vue.d.ts +28 -0
  30. package/dist/components/MFileUpload.vue.d.ts +25 -0
  31. package/dist/components/MGrid.vue.d.ts +26 -0
  32. package/dist/components/MHotkeys.vue.d.ts +16 -0
  33. package/dist/components/MIcon.vue.d.ts +9 -0
  34. package/dist/components/MIconButton.vue.d.ts +14 -0
  35. package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
  36. package/dist/components/MJsonEditor.vue.d.ts +17 -0
  37. package/dist/components/MJsonViewer.vue.d.ts +14 -0
  38. package/dist/components/MKanban.vue.d.ts +53 -0
  39. package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
  40. package/dist/components/MMarkdown.vue.d.ts +11 -0
  41. package/dist/components/MMasonry.vue.d.ts +23 -0
  42. package/dist/components/MMenu.vue.d.ts +27 -0
  43. package/dist/components/MMenuItem.vue.d.ts +16 -0
  44. package/dist/components/MMultiSelect.vue.d.ts +34 -0
  45. package/dist/components/MNavigationBar.vue.d.ts +18 -0
  46. package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
  47. package/dist/components/MNavigationRail.vue.d.ts +32 -0
  48. package/dist/components/MPagination.vue.d.ts +12 -0
  49. package/dist/components/MProgressBar.vue.d.ts +13 -0
  50. package/dist/components/MRadio.vue.d.ts +17 -0
  51. package/dist/components/MRadioGroup.vue.d.ts +24 -0
  52. package/dist/components/MRating.vue.d.ts +23 -0
  53. package/dist/components/MResult.vue.d.ts +20 -0
  54. package/dist/components/MRichTextEditor.vue.d.ts +17 -0
  55. package/dist/components/MScheduler.vue.d.ts +35 -0
  56. package/dist/components/MSegmentedButton.vue.d.ts +24 -0
  57. package/dist/components/MSelect.vue.d.ts +29 -0
  58. package/dist/components/MSideSheet.vue.d.ts +28 -0
  59. package/dist/components/MSkeleton.vue.d.ts +14 -0
  60. package/dist/components/MSlider.vue.d.ts +24 -0
  61. package/dist/components/MSnackbar.vue.d.ts +3 -0
  62. package/dist/components/MSpinner.vue.d.ts +10 -0
  63. package/dist/components/MSplitter.vue.d.ts +26 -0
  64. package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
  65. package/dist/components/MStack.vue.d.ts +30 -0
  66. package/dist/components/MStatCard.vue.d.ts +24 -0
  67. package/dist/components/MStepper.vue.d.ts +33 -0
  68. package/dist/components/MSwitch.vue.d.ts +14 -0
  69. package/dist/components/MTable.vue.d.ts +73 -0
  70. package/dist/components/MTabs.vue.d.ts +20 -0
  71. package/dist/components/MTerminal.vue.d.ts +25 -0
  72. package/dist/components/MTextField.vue.d.ts +41 -0
  73. package/dist/components/MTimePicker.vue.d.ts +20 -0
  74. package/dist/components/MTimeline.vue.d.ts +31 -0
  75. package/dist/components/MTooltip.vue.d.ts +21 -0
  76. package/dist/components/MTopAppBar.vue.d.ts +29 -0
  77. package/dist/components/MTour.vue.d.ts +19 -0
  78. package/dist/components/MTransferList.vue.d.ts +23 -0
  79. package/dist/components/MTree.vue.d.ts +68 -0
  80. package/dist/components/MTreeTable.vue.d.ts +57 -0
  81. package/dist/components/MVirtualTable.vue.d.ts +40 -0
  82. package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
  83. package/dist/components/_MTreeNode.vue.d.ts +26 -0
  84. package/dist/composables/useColorPalette.d.ts +11 -0
  85. package/dist/composables/useFieldBg.d.ts +13 -0
  86. package/dist/composables/useTheme.d.ts +5 -0
  87. package/dist/composables/useToast.d.ts +59 -0
  88. package/dist/index.d.ts +112 -0
  89. package/dist/m3ui.css +2 -0
  90. package/dist/m3ui.js +7432 -0
  91. package/dist/m3ui.js.map +1 -0
  92. package/dist/plugin.d.ts +9 -0
  93. package/dist/styles/palettes.css +1253 -0
  94. package/dist/styles/theme.css +249 -0
  95. package/package.json +166 -0
  96. package/src/components/MAlert.vue +69 -0
  97. package/src/components/MAppBar.vue +40 -0
  98. package/src/components/MAvatar.vue +21 -0
  99. package/src/components/MBadge.vue +46 -0
  100. package/src/components/MBottomSheet.vue +113 -0
  101. package/src/components/MBreadcrumbs.vue +52 -0
  102. package/src/components/MButton.vue +111 -0
  103. package/src/components/MCalendar.vue +173 -0
  104. package/src/components/MCard.vue +56 -0
  105. package/src/components/MChart.vue +158 -0
  106. package/src/components/MCheckbox.vue +48 -0
  107. package/src/components/MChip.vue +87 -0
  108. package/src/components/MCodeEditor.vue +179 -0
  109. package/src/components/MColorPicker.vue +305 -0
  110. package/src/components/MCommandPalette.vue +213 -0
  111. package/src/components/MConfirmDialog.vue +43 -0
  112. package/src/components/MContainer.vue +36 -0
  113. package/src/components/MContextMenu.vue +66 -0
  114. package/src/components/MDataTable.vue +376 -0
  115. package/src/components/MDatePicker.vue +253 -0
  116. package/src/components/MDateRangePicker.vue +265 -0
  117. package/src/components/MDialog.vue +90 -0
  118. package/src/components/MDivider.vue +26 -0
  119. package/src/components/MDragDropList.vue +111 -0
  120. package/src/components/MEmptyState.vue +40 -0
  121. package/src/components/MExpansionPanel.vue +112 -0
  122. package/src/components/MFab.vue +220 -0
  123. package/src/components/MFileUpload.vue +206 -0
  124. package/src/components/MGrid.vue +99 -0
  125. package/src/components/MHotkeys.vue +122 -0
  126. package/src/components/MIcon.vue +9 -0
  127. package/src/components/MIconButton.vue +49 -0
  128. package/src/components/MInfiniteScroll.vue +68 -0
  129. package/src/components/MJsonEditor.vue +118 -0
  130. package/src/components/MJsonViewer.vue +106 -0
  131. package/src/components/MKanban.vue +147 -0
  132. package/src/components/MLoadingOverlay.vue +52 -0
  133. package/src/components/MMarkdown.vue +123 -0
  134. package/src/components/MMasonry.vue +87 -0
  135. package/src/components/MMenu.vue +113 -0
  136. package/src/components/MMenuItem.vue +15 -0
  137. package/src/components/MMultiSelect.vue +306 -0
  138. package/src/components/MNavigationBar.vue +62 -0
  139. package/src/components/MNavigationDrawer.vue +157 -0
  140. package/src/components/MNavigationRail.vue +80 -0
  141. package/src/components/MPagination.vue +37 -0
  142. package/src/components/MProgressBar.vue +200 -0
  143. package/src/components/MRadio.vue +89 -0
  144. package/src/components/MRadioGroup.vue +41 -0
  145. package/src/components/MRating.vue +108 -0
  146. package/src/components/MResult.vue +62 -0
  147. package/src/components/MRichTextEditor.vue +199 -0
  148. package/src/components/MScheduler.vue +225 -0
  149. package/src/components/MSegmentedButton.vue +75 -0
  150. package/src/components/MSelect.vue +259 -0
  151. package/src/components/MSideSheet.vue +112 -0
  152. package/src/components/MSkeleton.vue +60 -0
  153. package/src/components/MSlider.vue +188 -0
  154. package/src/components/MSnackbar.vue +244 -0
  155. package/src/components/MSpinner.vue +122 -0
  156. package/src/components/MSplitter.vue +97 -0
  157. package/src/components/MSpotlightSearch.vue +244 -0
  158. package/src/components/MStack.vue +67 -0
  159. package/src/components/MStatCard.vue +56 -0
  160. package/src/components/MStepper.vue +161 -0
  161. package/src/components/MSwitch.vue +63 -0
  162. package/src/components/MTable.vue +404 -0
  163. package/src/components/MTabs.vue +97 -0
  164. package/src/components/MTerminal.vue +146 -0
  165. package/src/components/MTextField.vue +180 -0
  166. package/src/components/MTimePicker.vue +227 -0
  167. package/src/components/MTimeline.vue +117 -0
  168. package/src/components/MTooltip.vue +82 -0
  169. package/src/components/MTopAppBar.vue +62 -0
  170. package/src/components/MTour.vue +226 -0
  171. package/src/components/MTransferList.vue +181 -0
  172. package/src/components/MTree.vue +164 -0
  173. package/src/components/MTreeTable.vue +159 -0
  174. package/src/components/MVirtualTable.vue +155 -0
  175. package/src/components/_MContextMenuPanel.vue +129 -0
  176. package/src/components/_MTreeNode.vue +171 -0
  177. package/src/composables/useColorPalette.ts +60 -0
  178. package/src/composables/useFieldBg.ts +91 -0
  179. package/src/composables/useTheme.ts +55 -0
  180. package/src/composables/useToast.ts +51 -0
  181. package/src/env.d.ts +1 -0
  182. package/src/index.ts +119 -0
  183. package/src/plugin.ts +18 -0
  184. package/src/styles/palettes.css +1253 -0
  185. package/src/styles/theme.css +249 -0
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ import MIcon from './MIcon.vue'
3
+
4
+ export interface BreadcrumbItem {
5
+ label: string
6
+ icon?: string
7
+ to?: string
8
+ disabled?: boolean
9
+ }
10
+
11
+ withDefaults(defineProps<{
12
+ items: BreadcrumbItem[]
13
+ separator?: string
14
+ }>(), { separator: 'chevron_right' })
15
+
16
+ defineEmits<{ select: [BreadcrumbItem, number] }>()
17
+ </script>
18
+
19
+ <template>
20
+ <nav aria-label="Breadcrumb" class="flex items-center gap-1 overflow-x-auto text-label-large">
21
+ <template v-for="(item, i) in items" :key="i">
22
+ <!-- Separator -->
23
+ <MIcon
24
+ v-if="i > 0"
25
+ :name="separator"
26
+ :size="18"
27
+ class="shrink-0 text-on-surface-variant"
28
+ />
29
+
30
+ <!-- Item -->
31
+ <button
32
+ v-if="i < items.length - 1 && !item.disabled"
33
+ type="button"
34
+ class="flex shrink-0 cursor-pointer items-center gap-1.5 rounded-sm px-1.5 py-1 text-primary transition-colors hover:bg-primary/8 focus-visible:outline-none"
35
+ @click="$emit('select', item, i)"
36
+ >
37
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" />
38
+ <span>{{ item.label }}</span>
39
+ </button>
40
+
41
+ <!-- Last item (current page) or disabled -->
42
+ <span
43
+ v-else
44
+ class="flex shrink-0 items-center gap-1.5 px-1.5 py-1"
45
+ :class="item.disabled ? 'text-on-surface/38' : 'font-medium text-on-surface'"
46
+ >
47
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" />
48
+ <span>{{ item.label }}</span>
49
+ </span>
50
+ </template>
51
+ </nav>
52
+ </template>
@@ -0,0 +1,111 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import MSpinner from './MSpinner.vue'
4
+ import MIcon from './MIcon.vue'
5
+
6
+ const NAMED_COLORS = ['primary', 'error'] as const
7
+ type NamedColor = (typeof NAMED_COLORS)[number]
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ variant?: 'filled' | 'tonal' | 'outlined' | 'text' | 'elevated'
12
+ /**
13
+ * Named semantic color ('primary' | 'error') OR any CSS color string
14
+ * ('red', '#e91e63', 'oklch(0.6 0.2 0)', …).
15
+ * When a CSS color is passed, --color-primary is overridden for this button.
16
+ */
17
+ color?: string
18
+ type?: 'button' | 'submit' | 'reset'
19
+ disabled?: boolean
20
+ loading?: boolean
21
+ icon?: string
22
+ }>(),
23
+ {
24
+ variant: 'filled',
25
+ color: 'primary',
26
+ type: 'button',
27
+ disabled: false,
28
+ loading: false,
29
+ },
30
+ )
31
+
32
+ const isCustomColor = computed(
33
+ () => !!props.color && !(NAMED_COLORS as readonly string[]).includes(props.color),
34
+ )
35
+
36
+ const customStyle = computed(() => {
37
+ if (!isCustomColor.value) return undefined
38
+ return {
39
+ '--color-primary': props.color,
40
+ '--color-on-primary': '#ffffff',
41
+ '--color-primary-container': props.color + '33',
42
+ '--color-on-primary-container': props.color,
43
+ }
44
+ })
45
+
46
+ const isError = computed(() => props.color === 'error')
47
+
48
+ // State-layer overlay: before: pseudo-element uses currentColor (the button's text color)
49
+ // so it's always the correct M3 state-layer color for every variant automatically.
50
+ const base =
51
+ 'relative inline-flex items-center justify-center gap-2 h-10 rounded-full text-label-large font-medium ' +
52
+ 'whitespace-nowrap overflow-hidden transition-[box-shadow,background-color,color] duration-150 select-none cursor-pointer ' +
53
+ 'disabled:cursor-not-allowed disabled:opacity-[0.38] disabled:shadow-none ' +
54
+ "before:content-[''] before:pointer-events-none before:absolute before:inset-0 " +
55
+ 'before:bg-current before:opacity-0 before:transition-opacity before:duration-150 ' +
56
+ 'enabled:hover:before:opacity-[0.08] enabled:active:before:opacity-[0.12]'
57
+
58
+ const variantClasses = computed(() => {
59
+ const err = isError.value
60
+ switch (props.variant) {
61
+ case 'filled':
62
+ return err
63
+ ? 'px-6 bg-error text-on-error enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
64
+ : 'px-6 bg-primary text-on-primary enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
65
+ case 'tonal':
66
+ return err
67
+ ? 'px-6 bg-error-container text-on-error-container enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
68
+ : 'px-6 bg-secondary-container text-on-secondary-container enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
69
+ case 'elevated':
70
+ return err
71
+ ? 'px-6 bg-surface-container-low text-error shadow-elevation-1 enabled:hover:shadow-elevation-2'
72
+ : 'px-6 bg-surface-container-low text-primary shadow-elevation-1 enabled:hover:shadow-elevation-2'
73
+ case 'outlined':
74
+ return err
75
+ ? 'px-6 border border-error text-error'
76
+ : 'px-6 border border-outline text-primary'
77
+ case 'text':
78
+ return err
79
+ ? 'px-3 text-error'
80
+ : 'px-3 text-primary'
81
+ default:
82
+ return ''
83
+ }
84
+ })
85
+
86
+ function createRipple(event: PointerEvent) {
87
+ if (props.disabled || props.loading) return
88
+ const button = event.currentTarget as HTMLElement
89
+ const rect = button.getBoundingClientRect()
90
+ const d = Math.max(rect.width, rect.height) * 2
91
+ const el = document.createElement('span')
92
+ el.className = 'm3-ripple'
93
+ 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`
94
+ button.appendChild(el)
95
+ el.addEventListener('animationend', () => el.remove(), { once: true })
96
+ }
97
+ </script>
98
+
99
+ <template>
100
+ <button
101
+ :type="type"
102
+ :disabled="disabled || loading"
103
+ :class="[base, variantClasses]"
104
+ :style="customStyle"
105
+ @pointerdown="createRipple"
106
+ >
107
+ <MSpinner v-if="loading" :size="18" />
108
+ <MIcon v-else-if="icon" :name="icon" :size="20" />
109
+ <slot />
110
+ </button>
111
+ </template>
@@ -0,0 +1,173 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+ import MIconButton from './MIconButton.vue'
5
+
6
+ export interface CalendarEvent {
7
+ id: string | number
8
+ title: string
9
+ date: string
10
+ color?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'success'
11
+ icon?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<{
15
+ events?: CalendarEvent[]
16
+ locale?: string
17
+ }>(), { events: () => [], locale: 'es-ES' })
18
+
19
+ const emit = defineEmits<{
20
+ dateClick: [string]
21
+ eventClick: [CalendarEvent]
22
+ }>()
23
+
24
+ const viewDate = ref(new Date())
25
+
26
+ const WEEKDAYS = (() => {
27
+ const f = new Intl.DateTimeFormat(props.locale, { weekday: 'short' })
28
+ return Array.from({ length: 7 }, (_, i) => f.format(new Date(2024, 0, i + 1)))
29
+ })()
30
+
31
+ const monthLabel = computed(() =>
32
+ new Intl.DateTimeFormat(props.locale, { month: 'long', year: 'numeric' }).format(viewDate.value)
33
+ )
34
+
35
+ function fmt(y: number, m: number, d: number) {
36
+ const dt = new Date(y, m, d)
37
+ return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`
38
+ }
39
+
40
+ const todayIso = fmt(new Date().getFullYear(), new Date().getMonth(), new Date().getDate())
41
+
42
+ interface CalendarDay {
43
+ date: number
44
+ iso: string
45
+ current: boolean
46
+ events: CalendarEvent[]
47
+ }
48
+
49
+ const calendarDays = computed<CalendarDay[]>(() => {
50
+ const y = viewDate.value.getFullYear()
51
+ const m = viewDate.value.getMonth()
52
+ const first = new Date(y, m, 1)
53
+ const startDay = (first.getDay() + 6) % 7
54
+ const daysInMonth = new Date(y, m + 1, 0).getDate()
55
+ const days: CalendarDay[] = []
56
+
57
+ const eventMap = new Map<string, CalendarEvent[]>()
58
+ for (const ev of props.events) {
59
+ if (!eventMap.has(ev.date)) eventMap.set(ev.date, [])
60
+ eventMap.get(ev.date)!.push(ev)
61
+ }
62
+
63
+ const prevMonth = new Date(y, m, 0).getDate()
64
+ for (let i = startDay - 1; i >= 0; i--) {
65
+ const d = prevMonth - i
66
+ const iso = fmt(y, m - 1, d)
67
+ days.push({ date: d, current: false, iso, events: eventMap.get(iso) ?? [] })
68
+ }
69
+ for (let d = 1; d <= daysInMonth; d++) {
70
+ const iso = fmt(y, m, d)
71
+ days.push({ date: d, current: true, iso, events: eventMap.get(iso) ?? [] })
72
+ }
73
+ const remaining = 42 - days.length
74
+ for (let d = 1; d <= remaining; d++) {
75
+ const iso = fmt(y, m + 1, d)
76
+ days.push({ date: d, current: false, iso, events: eventMap.get(iso) ?? [] })
77
+ }
78
+ return days
79
+ })
80
+
81
+ function prevMonth() { const d = new Date(viewDate.value); d.setMonth(d.getMonth() - 1); viewDate.value = d }
82
+ function nextMonth() { const d = new Date(viewDate.value); d.setMonth(d.getMonth() + 1); viewDate.value = d }
83
+ function goToday() { viewDate.value = new Date() }
84
+
85
+ const eventColor: Record<string, string> = {
86
+ primary: 'bg-primary text-on-primary',
87
+ secondary: 'bg-secondary text-on-secondary',
88
+ tertiary: 'bg-tertiary text-on-tertiary',
89
+ error: 'bg-error text-on-error',
90
+ success: 'bg-success text-on-success',
91
+ }
92
+ </script>
93
+
94
+ <template>
95
+ <div class="flex flex-col overflow-hidden rounded-lg border border-outline-variant">
96
+ <!-- Header -->
97
+ <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-4 py-3">
98
+ <div class="flex items-center gap-1">
99
+ <MIconButton icon="chevron_left" label="Mes anterior" :size="36" @click="prevMonth" />
100
+ <MIconButton icon="chevron_right" label="Mes siguiente" :size="36" @click="nextMonth" />
101
+ </div>
102
+ <h3 class="text-title-medium font-medium capitalize text-on-surface">{{ monthLabel }}</h3>
103
+ <button
104
+ type="button"
105
+ class="cursor-pointer rounded-full border border-outline px-3 py-1 text-label-medium text-on-surface transition-colors hover:bg-on-surface/8"
106
+ @click="goToday"
107
+ >
108
+ Hoy
109
+ </button>
110
+ </div>
111
+
112
+ <!-- Weekday headers -->
113
+ <div class="grid grid-cols-7 border-b border-outline-variant bg-surface-container-high">
114
+ <div
115
+ v-for="wd in WEEKDAYS"
116
+ :key="wd"
117
+ class="py-2 text-center text-label-small font-medium uppercase text-on-surface-variant"
118
+ >
119
+ {{ wd }}
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Days grid -->
124
+ <div class="grid grid-cols-7">
125
+ <div
126
+ v-for="(day, i) in calendarDays"
127
+ :key="i"
128
+ class="flex min-h-[80px] cursor-pointer flex-col border-b border-r border-outline-variant/50 p-1.5 transition-colors hover:bg-on-surface/[0.03]"
129
+ :class="[
130
+ !day.current ? 'bg-surface-container-lowest/50' : 'bg-surface',
131
+ (i + 1) % 7 === 0 ? 'border-r-0' : '',
132
+ i >= 35 ? 'border-b-0' : '',
133
+ ]"
134
+ @click="emit('dateClick', day.iso)"
135
+ >
136
+ <!-- Day number -->
137
+ <span
138
+ class="mb-0.5 flex h-6 w-6 items-center justify-center self-end rounded-full text-label-medium"
139
+ :class="
140
+ day.iso === todayIso
141
+ ? 'bg-primary text-on-primary font-medium'
142
+ : day.current
143
+ ? 'text-on-surface'
144
+ : 'text-on-surface-variant/40'
145
+ "
146
+ >
147
+ {{ day.date }}
148
+ </span>
149
+
150
+ <!-- Events -->
151
+ <div v-if="day.events.length" class="flex flex-col gap-0.5">
152
+ <button
153
+ v-for="ev in day.events.slice(0, 2)"
154
+ :key="ev.id"
155
+ type="button"
156
+ class="flex w-full cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-left text-label-small transition-opacity hover:opacity-80"
157
+ :class="eventColor[ev.color ?? 'primary']"
158
+ @click.stop="emit('eventClick', ev)"
159
+ >
160
+ <MIcon v-if="ev.icon" :name="ev.icon" :size="12" />
161
+ <span class="truncate">{{ ev.title }}</span>
162
+ </button>
163
+ <span
164
+ v-if="day.events.length > 2"
165
+ class="px-1 text-label-small text-on-surface-variant"
166
+ >
167
+ +{{ day.events.length - 2 }} más
168
+ </span>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ variant?: 'elevated' | 'filled' | 'outlined'
7
+ clickable?: boolean
8
+ elevated?: boolean
9
+ /** src URL for a full-bleed header image */
10
+ image?: string
11
+ imageAlt?: string
12
+ imageHeight?: string
13
+ }>(),
14
+ { variant: 'elevated', clickable: false, elevated: false },
15
+ )
16
+
17
+ const resolvedVariant = computed(() => (props.elevated ? 'elevated' : props.variant))
18
+
19
+ const variantClasses: Record<string, string> = {
20
+ elevated: 'bg-surface-container-low shadow-elevation-1',
21
+ filled: 'bg-surface-container-highest',
22
+ outlined: 'bg-surface border border-outline-variant',
23
+ }
24
+
25
+ // Expose the card's background as --field-bg so outlined text-field labels
26
+ // inside the card automatically match without needing the fieldBg prop.
27
+ const fieldBgByVariant: Record<string, string> = {
28
+ elevated: 'var(--color-surface-container-low)',
29
+ filled: 'var(--color-surface-container-highest)',
30
+ outlined: 'var(--color-surface)',
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div
36
+ class="overflow-hidden rounded-md transition-shadow duration-150"
37
+ :class="[
38
+ variantClasses[resolvedVariant],
39
+ clickable ? 'cursor-pointer hover:shadow-elevation-2 active:shadow-elevation-1' : '',
40
+ ]"
41
+ :style="{ '--field-bg': fieldBgByVariant[resolvedVariant] }"
42
+ >
43
+ <!-- Optional header image -->
44
+ <div v-if="image || $slots.media" :class="['w-full overflow-hidden', imageHeight ?? 'h-48']">
45
+ <img
46
+ v-if="image"
47
+ :src="image"
48
+ :alt="imageAlt ?? ''"
49
+ class="h-full w-full object-cover"
50
+ />
51
+ <slot v-else name="media" />
52
+ </div>
53
+
54
+ <slot />
55
+ </div>
56
+ </template>
@@ -0,0 +1,158 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, onMounted } from 'vue'
3
+ import {
4
+ Chart as ChartJS,
5
+ CategoryScale,
6
+ LinearScale,
7
+ PointElement,
8
+ LineElement,
9
+ BarElement,
10
+ ArcElement,
11
+ RadialLinearScale,
12
+ Filler,
13
+ Tooltip,
14
+ Legend,
15
+ Title,
16
+ type ChartData,
17
+ type ChartOptions,
18
+ } from 'chart.js'
19
+ import { Line, Bar, Pie, Doughnut, Radar } from 'vue-chartjs'
20
+
21
+ ChartJS.register(
22
+ CategoryScale,
23
+ LinearScale,
24
+ PointElement,
25
+ LineElement,
26
+ BarElement,
27
+ ArcElement,
28
+ RadialLinearScale,
29
+ Filler,
30
+ Tooltip,
31
+ Legend,
32
+ Title,
33
+ )
34
+
35
+ type ChartType = 'line' | 'bar' | 'pie' | 'doughnut' | 'radar'
36
+
37
+ const props = withDefaults(
38
+ defineProps<{
39
+ type: ChartType
40
+ data: ChartData<any>
41
+ options?: ChartOptions<any>
42
+ height?: string
43
+ }>(),
44
+ { height: '300px' },
45
+ )
46
+
47
+ function getM3Colors() {
48
+ const style = getComputedStyle(document.documentElement)
49
+ const get = (v: string) => style.getPropertyValue(v).trim()
50
+ return {
51
+ primary: get('--color-primary'),
52
+ onSurface: get('--color-on-surface'),
53
+ onSurfaceVariant: get('--color-on-surface-variant'),
54
+ outlineVariant: get('--color-outline-variant'),
55
+ surface: get('--color-surface'),
56
+ surfaceContainer: get('--color-surface-container'),
57
+ }
58
+ }
59
+
60
+ const m3Colors = ref(getM3Colors())
61
+
62
+ onMounted(() => { m3Colors.value = getM3Colors() })
63
+
64
+ const themeObserver = ref<MutationObserver | null>(null)
65
+ onMounted(() => {
66
+ themeObserver.value = new MutationObserver(() => { m3Colors.value = getM3Colors() })
67
+ themeObserver.value.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
68
+ })
69
+
70
+ watch(() => m3Colors.value, () => {}, { deep: true })
71
+
72
+ const mergedOptions = computed<ChartOptions<any>>(() => {
73
+ const c = m3Colors.value
74
+ const base: ChartOptions<any> = {
75
+ responsive: true,
76
+ maintainAspectRatio: false,
77
+ plugins: {
78
+ legend: {
79
+ labels: {
80
+ color: c.onSurface,
81
+ font: { family: "'Roboto', system-ui, sans-serif", size: 12 },
82
+ usePointStyle: true,
83
+ pointStyle: 'circle',
84
+ padding: 16,
85
+ },
86
+ },
87
+ tooltip: {
88
+ backgroundColor: c.surfaceContainer,
89
+ titleColor: c.onSurface,
90
+ bodyColor: c.onSurfaceVariant,
91
+ borderColor: c.outlineVariant,
92
+ borderWidth: 1,
93
+ cornerRadius: 12,
94
+ padding: 12,
95
+ titleFont: { family: "'Roboto', system-ui, sans-serif", size: 13, weight: '600' as const },
96
+ bodyFont: { family: "'Roboto', system-ui, sans-serif", size: 12 },
97
+ displayColors: true,
98
+ boxPadding: 4,
99
+ },
100
+ },
101
+ }
102
+
103
+ if (props.type !== 'pie' && props.type !== 'doughnut') {
104
+ base.scales = {
105
+ x: {
106
+ grid: { color: c.outlineVariant + '40', drawTicks: false },
107
+ ticks: { color: c.onSurfaceVariant, font: { size: 11 }, padding: 8 },
108
+ border: { color: c.outlineVariant },
109
+ },
110
+ y: {
111
+ grid: { color: c.outlineVariant + '40', drawTicks: false },
112
+ ticks: { color: c.onSurfaceVariant, font: { size: 11 }, padding: 8 },
113
+ border: { color: c.outlineVariant },
114
+ },
115
+ }
116
+ }
117
+
118
+ if (props.type === 'radar') {
119
+ base.scales = {
120
+ r: {
121
+ grid: { color: c.outlineVariant + '40' },
122
+ angleLines: { color: c.outlineVariant + '40' },
123
+ pointLabels: { color: c.onSurfaceVariant, font: { size: 11 } },
124
+ ticks: { color: c.onSurfaceVariant, backdropColor: 'transparent' },
125
+ },
126
+ }
127
+ }
128
+
129
+ return deepMerge(base, props.options ?? {})
130
+ })
131
+
132
+ function deepMerge(target: any, source: any): any {
133
+ const output = { ...target }
134
+ for (const key of Object.keys(source)) {
135
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
136
+ output[key] = deepMerge(target[key] ?? {}, source[key])
137
+ } else {
138
+ output[key] = source[key]
139
+ }
140
+ }
141
+ return output
142
+ }
143
+
144
+ const chartComponent = computed(() => {
145
+ const map = { line: Line, bar: Bar, pie: Pie, doughnut: Doughnut, radar: Radar }
146
+ return map[props.type]
147
+ })
148
+ </script>
149
+
150
+ <template>
151
+ <div class="rounded-lg border border-outline-variant bg-surface p-4" :style="{ height }">
152
+ <component
153
+ :is="chartComponent"
154
+ :data="data"
155
+ :options="mergedOptions"
156
+ />
157
+ </div>
158
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import MIcon from "./MIcon.vue";
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ modelValue: boolean;
7
+ indeterminate?: boolean;
8
+ disabled?: boolean;
9
+ label?: string;
10
+ }>(),
11
+ { indeterminate: false, disabled: false },
12
+ );
13
+
14
+ const emit = defineEmits<{ "update:modelValue": [boolean] }>();
15
+ </script>
16
+
17
+ <template>
18
+ <label
19
+ class="inline-flex items-center gap-2 select-none"
20
+ :class="disabled ? 'cursor-not-allowed opacity-[0.38]' : 'cursor-pointer'"
21
+ >
22
+ <span
23
+ class="relative inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center rounded-[3px] border-2 transition-colors"
24
+ :class="
25
+ modelValue || indeterminate
26
+ ? 'border-primary bg-primary text-on-primary'
27
+ : 'border-on-surface-variant text-transparent'
28
+ "
29
+ >
30
+ <input
31
+ type="checkbox"
32
+ class="sr-only"
33
+ :checked="modelValue"
34
+ :disabled="disabled"
35
+ @change="emit('update:modelValue', !modelValue)"
36
+ />
37
+ <MIcon
38
+ :name="indeterminate ? 'remove' : 'check'"
39
+ :size="14"
40
+ class="transition-[opacity,transform] duration-150"
41
+ :class="modelValue || indeterminate ? 'scale-100 opacity-100' : 'scale-0 opacity-0'"
42
+ />
43
+ </span>
44
+ <span v-if="label || $slots.default" class="text-body-large text-on-surface">
45
+ <slot>{{ label }}</slot>
46
+ </span>
47
+ </label>
48
+ </template>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+
5
+ const NAMED_TONES = ['neutral', 'primary', 'success', 'error', 'tertiary', 'secondary'] as const
6
+ type NamedTone = (typeof NAMED_TONES)[number]
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ tone?: string // named tone OR CSS color string
11
+ selected?: boolean
12
+ removable?: boolean
13
+ clickable?: boolean
14
+ disabled?: boolean
15
+ icon?: string
16
+ }>(),
17
+ {
18
+ tone: 'neutral',
19
+ selected: false,
20
+ removable: false,
21
+ clickable: false,
22
+ disabled: false,
23
+ },
24
+ )
25
+
26
+ const emit = defineEmits<{ click: []; remove: [] }>()
27
+
28
+ const isCustomColor = computed(
29
+ () => !!props.tone && !(NAMED_TONES as readonly string[]).includes(props.tone),
30
+ )
31
+
32
+ // When a CSS color is passed, apply it via CSS variables
33
+ const customStyle = computed(() => {
34
+ if (!isCustomColor.value) return undefined
35
+ return {
36
+ '--chip-bg': props.tone + '22',
37
+ '--chip-color': props.tone,
38
+ }
39
+ })
40
+
41
+ const toneClasses = computed(() => {
42
+ if (isCustomColor.value) {
43
+ return 'border border-transparent bg-[var(--chip-bg)] text-[var(--chip-color)]'
44
+ }
45
+ if (props.tone === 'neutral' && !props.selected) {
46
+ return 'border border-outline bg-transparent text-on-surface-variant'
47
+ }
48
+ const map: Record<string, string> = {
49
+ neutral: 'border border-transparent bg-secondary-container text-on-secondary-container',
50
+ primary: 'border border-transparent bg-primary-container text-on-primary-container',
51
+ secondary: 'border border-transparent bg-secondary-container text-on-secondary-container',
52
+ success: 'border border-transparent bg-success-container text-on-success-container',
53
+ error: 'border border-transparent bg-error-container text-on-error-container',
54
+ tertiary: 'border border-transparent bg-tertiary-container text-on-tertiary-container',
55
+ }
56
+ return map[props.tone ?? 'neutral'] ?? map.neutral
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <component
62
+ :is="clickable ? 'button' : 'span'"
63
+ :type="clickable ? 'button' : undefined"
64
+ :disabled="clickable && disabled ? true : undefined"
65
+ class="inline-flex h-8 items-center gap-1.5 rounded-sm px-3 text-label-large transition-colors"
66
+ :class="[
67
+ toneClasses,
68
+ clickable && !disabled ? 'cursor-pointer hover:bg-on-surface/8' : '',
69
+ disabled ? 'cursor-not-allowed opacity-[0.38]' : '',
70
+ ]"
71
+ :style="customStyle"
72
+ @click="clickable && !disabled && emit('click')"
73
+ >
74
+ <MIcon v-if="icon" :name="icon" :size="18" />
75
+ <slot />
76
+ <button
77
+ v-if="removable"
78
+ type="button"
79
+ class="-mr-1 ml-0.5 inline-flex items-center justify-center rounded-full hover:bg-on-surface/12"
80
+ aria-label="Quitar"
81
+ :disabled="disabled"
82
+ @click.stop="emit('remove')"
83
+ >
84
+ <MIcon name="close" :size="16" />
85
+ </button>
86
+ </component>
87
+ </template>