@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,180 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useId, useSlots } from "vue";
3
+ import MIcon from "./MIcon.vue";
4
+ import { useFieldBg } from "../composables/useFieldBg";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ modelValue: string | number;
9
+ label: string;
10
+ type?: string;
11
+ variant?: "filled" | "outlined";
12
+ error?: string;
13
+ hint?: string;
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ multiline?: boolean;
17
+ rows?: number;
18
+ autocomplete?: string;
19
+ leadingIcon?: string;
20
+ /**
21
+ * Background color behind the label in outlined variant.
22
+ * Defaults to the page surface color. Pass e.g. 'var(--color-surface-container-low)'
23
+ * when the input is inside a card.
24
+ */
25
+ fieldBg?: string;
26
+ }>(),
27
+ {
28
+ type: "text",
29
+ variant: "filled",
30
+ rows: 3,
31
+ },
32
+ );
33
+
34
+ const emit = defineEmits<{ "update:modelValue": [string] }>();
35
+
36
+ const id = useId();
37
+ const slots = useSlots();
38
+
39
+ const fieldBgEl = ref<HTMLElement | null>(null);
40
+ const { resolvedFieldBg } = useFieldBg(fieldBgEl, () => props.fieldBg);
41
+
42
+ const inputClasses = computed(() => {
43
+ const hasTrailing = !!slots.trailing;
44
+ const pl = props.leadingIcon ? "pl-12" : "pl-4";
45
+ const pr = hasTrailing ? "pr-12" : "pr-4";
46
+ const size = props.multiline ? "resize-y min-h-[56px]" : "h-14";
47
+ const base = [
48
+ "peer block w-full text-body-large text-on-surface outline-none placeholder:text-transparent",
49
+ "transition-[border-color,border-width] duration-150",
50
+ "disabled:cursor-not-allowed disabled:opacity-[0.38]",
51
+ size,
52
+ pl,
53
+ pr,
54
+ ];
55
+
56
+ if (props.variant === "outlined") {
57
+ return [
58
+ ...base,
59
+ "rounded-sm border bg-transparent py-4",
60
+ props.error
61
+ ? "border-error focus:border-2 focus:border-error"
62
+ : "border-outline hover:border-on-surface focus:border-2 focus:border-primary",
63
+ ].join(" ");
64
+ }
65
+
66
+ return [
67
+ ...base,
68
+ "rounded-t-sm bg-surface-container-highest border-b pt-6 pb-2",
69
+ props.error
70
+ ? "border-error focus:border-b-2 focus:border-error"
71
+ : "border-on-surface-variant hover:border-on-surface focus:border-b-2 focus:border-primary",
72
+ ].join(" ");
73
+ });
74
+
75
+ const labelClasses = computed(() => {
76
+ const left = props.leadingIcon
77
+ ? props.variant === "outlined"
78
+ ? "left-11"
79
+ : "left-12"
80
+ : props.variant === "outlined"
81
+ ? "left-3"
82
+ : "left-4";
83
+
84
+ const base = [
85
+ "pointer-events-none absolute truncate transition-all duration-200",
86
+ left,
87
+ "right-4",
88
+ "top-1/2 -translate-y-1/2 text-body-large",
89
+ ];
90
+
91
+ if (props.variant === "outlined") {
92
+ // When floated: drop right-4 (right-auto) and cap max-width so the label
93
+ // shrinks to its own text width. The bg then only covers the glyphs + px-1,
94
+ // cutting the border just where the text sits instead of a long strip.
95
+ return [
96
+ ...base,
97
+ "peer-focus:-top-2.5 peer-focus:translate-y-0 peer-focus:text-label-small peer-focus:right-auto peer-focus:max-w-[calc(100%-1.5rem)] peer-focus:bg-[var(--field-bg)] peer-focus:px-1",
98
+ "peer-[&:not(:placeholder-shown)]:-top-2.5 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:right-auto peer-[&:not(:placeholder-shown)]:max-w-[calc(100%-1.5rem)]",
99
+ "peer-[&:not(:placeholder-shown)]:text-label-small peer-[&:not(:placeholder-shown)]:bg-[var(--field-bg)] peer-[&:not(:placeholder-shown)]:px-1",
100
+ props.error
101
+ ? "text-error peer-focus:text-error"
102
+ : "text-on-surface-variant peer-focus:text-primary",
103
+ ].join(" ");
104
+ }
105
+
106
+ // Filled: label floats to top-2 (slightly higher than before)
107
+ return [
108
+ ...base,
109
+ "peer-focus:top-2 peer-focus:translate-y-0 peer-focus:text-label-small",
110
+ "peer-[&:not(:placeholder-shown)]:top-2 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:text-label-small",
111
+ props.error
112
+ ? "text-error peer-focus:text-error"
113
+ : "text-on-surface-variant peer-focus:text-primary",
114
+ ].join(" ");
115
+ });
116
+
117
+ function onInput(event: Event) {
118
+ const target = event.target as HTMLInputElement | HTMLTextAreaElement;
119
+ emit("update:modelValue", target.value);
120
+ }
121
+ </script>
122
+
123
+ <template>
124
+ <div class="flex flex-col gap-1">
125
+ <!--
126
+ --field-bg: background behind the floating label in outlined mode, so it
127
+ "cuts through" the border. Auto-detected from the nearest opaque ancestor
128
+ (see resolveBg); overridable via the fieldBg prop; falls back to surface.
129
+ -->
130
+ <div
131
+ ref="fieldBgEl"
132
+ class="relative"
133
+ :class="variant === 'outlined' ? 'mt-2' : ''"
134
+ :style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
135
+ >
136
+ <!-- Leading icon: centered in the 48px left zone (left-3.5 → center at 24px) -->
137
+ <div
138
+ v-if="leadingIcon"
139
+ class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-on-surface-variant"
140
+ >
141
+ <MIcon :name="leadingIcon" :size="20" />
142
+ </div>
143
+
144
+ <textarea
145
+ v-if="multiline"
146
+ :id="id"
147
+ :value="String(modelValue)"
148
+ :rows="rows"
149
+ :disabled="disabled"
150
+ :required="required"
151
+ placeholder=" "
152
+ :class="inputClasses"
153
+ @input="onInput"
154
+ />
155
+ <input
156
+ v-else
157
+ :id="id"
158
+ :type="type"
159
+ :value="modelValue"
160
+ :disabled="disabled"
161
+ :required="required"
162
+ :autocomplete="autocomplete"
163
+ placeholder=" "
164
+ :class="inputClasses"
165
+ @input="onInput"
166
+ />
167
+
168
+ <label :for="id" :class="labelClasses">
169
+ {{ label }}<span v-if="required" class="text-error">&nbsp;*</span>
170
+ </label>
171
+
172
+ <div v-if="$slots.trailing" class="absolute top-1/2 right-2 -translate-y-1/2">
173
+ <slot name="trailing" />
174
+ </div>
175
+ </div>
176
+
177
+ <p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
178
+ <p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
179
+ </div>
180
+ </template>
@@ -0,0 +1,227 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+ import { useFieldBg } from '../composables/useFieldBg'
5
+
6
+ const props = withDefaults(defineProps<{
7
+ modelValue: string | null
8
+ label?: string
9
+ disabled?: boolean
10
+ error?: string
11
+ hint?: string
12
+ minuteStep?: number
13
+ use24h?: boolean
14
+ fieldBg?: string
15
+ }>(), { minuteStep: 5, use24h: true })
16
+
17
+ const emit = defineEmits<{ 'update:modelValue': [string | null] }>()
18
+
19
+ const open = ref(false)
20
+ const triggerEl = ref<HTMLElement | null>(null)
21
+ const panelEl = ref<HTMLElement | null>(null)
22
+ const mode = ref<'hour' | 'minute'>('hour')
23
+ const dropPos = ref({ top: '0px', left: '0px' })
24
+ const { resolvedFieldBg } = useFieldBg(triggerEl, () => props.fieldBg)
25
+
26
+ const parsed = computed(() => {
27
+ if (!props.modelValue) return { h: 12, m: 0 }
28
+ const parts = props.modelValue.split(':').map(Number)
29
+ return { h: parts[0] ?? 12, m: parts[1] ?? 0 }
30
+ })
31
+
32
+ const selectedHour = ref(parsed.value.h)
33
+ const selectedMinute = ref(parsed.value.m)
34
+ watch(() => props.modelValue, () => {
35
+ selectedHour.value = parsed.value.h
36
+ selectedMinute.value = parsed.value.m
37
+ })
38
+
39
+ const hours = Array.from({ length: 24 }, (_, i) => i)
40
+ const minutes = computed(() => {
41
+ const arr: number[] = []
42
+ for (let m = 0; m < 60; m += props.minuteStep) arr.push(m)
43
+ return arr
44
+ })
45
+
46
+ function pad(n: number) { return String(n).padStart(2, '0') }
47
+
48
+ function selectHour(h: number) {
49
+ selectedHour.value = h
50
+ mode.value = 'minute'
51
+ }
52
+
53
+ function selectMinute(m: number) {
54
+ selectedMinute.value = m
55
+ emit('update:modelValue', `${pad(selectedHour.value)}:${pad(m)}`)
56
+ open.value = false
57
+ mode.value = 'hour'
58
+ }
59
+
60
+ function clear() {
61
+ emit('update:modelValue', null)
62
+ }
63
+
64
+ const displayValue = computed(() => {
65
+ if (!props.modelValue) return ''
66
+ return `${pad(parsed.value.h)}:${pad(parsed.value.m)}`
67
+ })
68
+
69
+ function computeDropPos() {
70
+ if (!triggerEl.value) return
71
+ const rect = triggerEl.value.getBoundingClientRect()
72
+ const panelH = 320
73
+ const spaceBelow = window.innerHeight - rect.bottom - 8
74
+ const above = spaceBelow < panelH && rect.top > panelH
75
+ dropPos.value = {
76
+ top: above ? `${rect.top - 4 - panelH}px` : `${rect.bottom + 4}px`,
77
+ left: `${rect.left}px`,
78
+ }
79
+ }
80
+
81
+ function onOut(e: MouseEvent) {
82
+ const t = e.target as Node
83
+ if (triggerEl.value?.contains(t)) return
84
+ if (panelEl.value?.contains(t)) return
85
+ open.value = false
86
+ }
87
+
88
+ function onScroll(e: Event) {
89
+ if (!open.value) return
90
+ if (panelEl.value?.contains(e.target as Node)) return
91
+ if (!triggerEl.value) return
92
+ const rect = triggerEl.value.getBoundingClientRect()
93
+ if (rect.bottom < 0 || rect.top > window.innerHeight) { open.value = false; return }
94
+ computeDropPos()
95
+ }
96
+
97
+ watch(open, (v) => {
98
+ if (v) {
99
+ mode.value = 'hour'
100
+ selectedHour.value = parsed.value.h
101
+ selectedMinute.value = parsed.value.m
102
+ computeDropPos()
103
+ setTimeout(() => document.addEventListener('mousedown', onOut), 0)
104
+ } else {
105
+ document.removeEventListener('mousedown', onOut)
106
+ }
107
+ })
108
+
109
+ onMounted(() => window.addEventListener('scroll', onScroll, true))
110
+ onUnmounted(() => {
111
+ window.removeEventListener('scroll', onScroll, true)
112
+ document.removeEventListener('mousedown', onOut)
113
+ })
114
+ </script>
115
+
116
+ <template>
117
+ <div class="flex flex-col gap-1">
118
+ <div ref="triggerEl" class="relative mt-2" :style="{ '--field-bg': resolvedFieldBg }">
119
+ <button
120
+ type="button"
121
+ class="flex h-14 w-full items-center gap-2 rounded-sm border bg-transparent px-4 text-left text-body-large transition-[border-color,border-width] duration-150"
122
+ :class="[
123
+ disabled ? 'pointer-events-none opacity-[0.38]' : 'cursor-pointer',
124
+ open
125
+ ? error ? 'border-2 border-error' : 'border-2 border-primary'
126
+ : error ? 'border-error' : 'border-outline hover:border-on-surface',
127
+ ]"
128
+ @click="!disabled && (open = !open)"
129
+ >
130
+ <MIcon name="schedule" :size="20" class="shrink-0 text-on-surface-variant" />
131
+ <span v-if="displayValue" class="flex-1 font-mono text-on-surface">{{ displayValue }}</span>
132
+ <span v-else class="flex-1 text-on-surface-variant">{{ label || 'Seleccionar hora' }}</span>
133
+ <MIcon
134
+ v-if="modelValue"
135
+ name="close"
136
+ :size="18"
137
+ class="shrink-0 cursor-pointer text-on-surface-variant hover:text-on-surface"
138
+ @click.stop="clear"
139
+ />
140
+ </button>
141
+ <label
142
+ v-if="label"
143
+ class="pointer-events-none absolute -top-2.5 left-3 bg-[var(--field-bg)] px-1 text-label-small transition-colors"
144
+ :class="open ? (error ? 'text-error' : 'text-primary') : error ? 'text-error' : 'text-on-surface-variant'"
145
+ >
146
+ {{ label }}
147
+ </label>
148
+ </div>
149
+
150
+ <p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
151
+ <p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
152
+
153
+ <Teleport to="body">
154
+ <Transition
155
+ enter-active-class="transition-[opacity,transform] duration-150"
156
+ enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
157
+ leave-active-class="transition-[opacity,transform] duration-100"
158
+ leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
159
+ >
160
+ <div
161
+ v-if="open"
162
+ ref="panelEl"
163
+ class="fixed z-[500] w-[280px] rounded-lg bg-surface-container shadow-elevation-3"
164
+ :style="dropPos"
165
+ >
166
+ <!-- Display -->
167
+ <div class="flex items-center justify-center gap-1 border-b border-outline-variant px-4 py-4">
168
+ <button
169
+ type="button"
170
+ class="rounded-lg px-3 py-2 font-mono text-headline-medium transition-colors"
171
+ :class="mode === 'hour' ? 'bg-primary-container text-on-primary-container cursor-default' : 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8'"
172
+ @click="mode = 'hour'"
173
+ >
174
+ {{ pad(selectedHour) }}
175
+ </button>
176
+ <span class="text-headline-medium text-on-surface-variant">:</span>
177
+ <button
178
+ type="button"
179
+ class="rounded-lg px-3 py-2 font-mono text-headline-medium transition-colors"
180
+ :class="mode === 'minute' ? 'bg-primary-container text-on-primary-container cursor-default' : 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8'"
181
+ @click="mode = 'minute'"
182
+ >
183
+ {{ pad(selectedMinute) }}
184
+ </button>
185
+ </div>
186
+
187
+ <!-- Grid -->
188
+ <div class="p-3">
189
+ <div v-if="mode === 'hour'" class="grid grid-cols-6 gap-1">
190
+ <button
191
+ v-for="h in hours"
192
+ :key="h"
193
+ type="button"
194
+ class="flex h-9 cursor-pointer items-center justify-center rounded-full text-body-medium transition-colors duration-100"
195
+ :class="
196
+ h === selectedHour
197
+ ? 'bg-primary text-on-primary'
198
+ : 'text-on-surface hover:bg-on-surface/8'
199
+ "
200
+ @click="selectHour(h)"
201
+ >
202
+ {{ pad(h) }}
203
+ </button>
204
+ </div>
205
+
206
+ <div v-else class="grid grid-cols-6 gap-1">
207
+ <button
208
+ v-for="m in minutes"
209
+ :key="m"
210
+ type="button"
211
+ class="flex h-9 cursor-pointer items-center justify-center rounded-full text-body-medium transition-colors duration-100"
212
+ :class="
213
+ m === selectedMinute
214
+ ? 'bg-primary text-on-primary'
215
+ : 'text-on-surface hover:bg-on-surface/8'
216
+ "
217
+ @click="selectMinute(m)"
218
+ >
219
+ {{ pad(m) }}
220
+ </button>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </Transition>
225
+ </Teleport>
226
+ </div>
227
+ </template>
@@ -0,0 +1,117 @@
1
+ <script setup lang="ts">
2
+ import MIcon from "./MIcon.vue";
3
+
4
+ export interface TimelineItem {
5
+ title: string;
6
+ description?: string;
7
+ date?: string;
8
+ icon?: string;
9
+ color?: "primary" | "secondary" | "tertiary" | "error" | "success";
10
+ dotColor?: string;
11
+ }
12
+
13
+ withDefaults(
14
+ defineProps<{
15
+ items: TimelineItem[];
16
+ dense?: boolean;
17
+ alternating?: boolean;
18
+ }>(),
19
+ { dense: false, alternating: false },
20
+ );
21
+
22
+ const dotBg: Record<string, string> = {
23
+ primary: "bg-primary text-on-primary",
24
+ secondary: "bg-secondary text-on-secondary",
25
+ tertiary: "bg-tertiary text-on-tertiary",
26
+ error: "bg-error text-on-error",
27
+ success: "bg-success text-on-success",
28
+ };
29
+ </script>
30
+
31
+ <template>
32
+ <div :class="alternating ? 'relative' : 'flex flex-col'">
33
+ <!-- Standard layout -->
34
+ <template v-if="!alternating">
35
+ <div
36
+ v-for="(item, i) in items"
37
+ :key="i"
38
+ class="relative flex gap-4"
39
+ :class="dense ? 'pb-4' : 'pb-8'"
40
+ >
41
+ <!-- Line + dot -->
42
+ <div class="flex flex-col items-center">
43
+ <div
44
+ class="z-[1] flex shrink-0 items-center justify-center rounded-full"
45
+ :class="[item.icon ? 'h-9 w-9' : 'h-3 w-3', dotBg[item.color ?? 'primary']]"
46
+ :style="item.dotColor ? { backgroundColor: item.dotColor } : undefined"
47
+ >
48
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" />
49
+ </div>
50
+ <div
51
+ v-if="i < items.length - 1"
52
+ class="w-[2px] flex-1"
53
+ :class="dotBg[item.color ?? 'primary']!.split(' ')[0] + '/30'"
54
+ style="min-height: 16px"
55
+ />
56
+ </div>
57
+
58
+ <!-- Content -->
59
+ <div :class="item.icon ? '' : 'pt-0'" class="-mt-0.5 flex-1">
60
+ <div class="flex items-baseline justify-between gap-2">
61
+ <p class="text-body-large font-medium text-on-surface">{{ item.title }}</p>
62
+ <span v-if="item.date" class="shrink-0 text-label-small text-on-surface-variant">{{
63
+ item.date
64
+ }}</span>
65
+ </div>
66
+ <p v-if="item.description" class="mt-1 text-body-medium text-on-surface-variant">
67
+ {{ item.description }}
68
+ </p>
69
+ <div v-if="$slots[`item-${i}`]" class="mt-2">
70
+ <slot :name="`item-${i}`" :item="item" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </template>
75
+
76
+ <!-- Alternating layout -->
77
+ <template v-else>
78
+ <div
79
+ v-for="(item, i) in items"
80
+ :key="i"
81
+ class="flex items-stretch"
82
+ :class="i % 2 === 0 ? 'flex-row' : 'flex-row-reverse'"
83
+ >
84
+ <!-- Content side -->
85
+ <div
86
+ class="flex-1"
87
+ :class="[i % 2 === 0 ? 'text-right' : 'text-left', dense ? 'pb-4' : 'pb-8']"
88
+ >
89
+ <p class="text-body-large font-medium text-on-surface">{{ item.title }}</p>
90
+ <p v-if="item.description" class="mt-1 text-body-medium text-on-surface-variant">
91
+ {{ item.description }}
92
+ </p>
93
+ <span
94
+ v-if="item.date"
95
+ class="mt-1 inline-block text-label-small text-on-surface-variant"
96
+ >{{ item.date }}</span
97
+ >
98
+ </div>
99
+
100
+ <!-- Center column: dot + continuous line -->
101
+ <div class="flex w-14 shrink-0 flex-col items-center">
102
+ <div
103
+ class="z-[1] flex shrink-0 items-center justify-center rounded-full"
104
+ :class="[item.icon ? 'h-9 w-9' : 'h-3.5 w-3.5', dotBg[item.color ?? 'primary']]"
105
+ :style="item.dotColor ? { backgroundColor: item.dotColor } : undefined"
106
+ >
107
+ <MIcon v-if="item.icon" :name="item.icon" :size="18" />
108
+ </div>
109
+ <div v-if="i < items.length - 1" class="w-[2px] flex-1 bg-outline-variant" />
110
+ </div>
111
+
112
+ <!-- Empty side -->
113
+ <div class="flex-1" />
114
+ </div>
115
+ </template>
116
+ </div>
117
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import { nextTick, ref } from 'vue'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ text: string
6
+ placement?: 'top' | 'bottom' | 'left' | 'right'
7
+ delay?: number
8
+ }>(), { placement: 'top', delay: 600 })
9
+
10
+ const visible = ref(false)
11
+ const tipEl = ref<HTMLElement>()
12
+ const triggerEl = ref<HTMLElement>()
13
+ const tipStyle = ref<Record<string, string>>({})
14
+ let timer: ReturnType<typeof setTimeout> | null = null
15
+
16
+ async function show() {
17
+ if (timer) clearTimeout(timer)
18
+ timer = setTimeout(async () => {
19
+ visible.value = true
20
+ await nextTick()
21
+ reposition()
22
+ }, props.delay)
23
+ }
24
+
25
+ function hide() {
26
+ if (timer) { clearTimeout(timer); timer = null }
27
+ visible.value = false
28
+ }
29
+
30
+ function reposition() {
31
+ if (!triggerEl.value || !tipEl.value) return
32
+ const tr = triggerEl.value.getBoundingClientRect()
33
+ const tt = tipEl.value.getBoundingClientRect()
34
+ const GAP = 6
35
+
36
+ let top = 0, left = 0
37
+ switch (props.placement) {
38
+ case 'top': top = tr.top - tt.height - GAP; left = tr.left + (tr.width - tt.width) / 2; break
39
+ case 'bottom': top = tr.bottom + GAP; left = tr.left + (tr.width - tt.width) / 2; break
40
+ case 'left': top = tr.top + (tr.height - tt.height) / 2; left = tr.left - tt.width - GAP; break
41
+ case 'right': top = tr.top + (tr.height - tt.height) / 2; left = tr.right + GAP; break
42
+ }
43
+
44
+ top = Math.max(6, Math.min(top, window.innerHeight - tt.height - 6))
45
+ left = Math.max(6, Math.min(left, window.innerWidth - tt.width - 6))
46
+ tipStyle.value = { top: `${top}px`, left: `${left}px` }
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <span
52
+ ref="triggerEl"
53
+ class="inline-flex"
54
+ @mouseenter="show"
55
+ @mouseleave="hide"
56
+ @focusin="show"
57
+ @focusout="hide"
58
+ >
59
+ <slot />
60
+ </span>
61
+
62
+ <Teleport to="body">
63
+ <Transition
64
+ enter-active-class="transition-opacity duration-150"
65
+ enter-from-class="opacity-0"
66
+ enter-to-class="opacity-100"
67
+ leave-active-class="transition-opacity duration-100"
68
+ leave-from-class="opacity-100"
69
+ leave-to-class="opacity-0"
70
+ >
71
+ <div
72
+ v-if="visible && text"
73
+ ref="tipEl"
74
+ class="pointer-events-none fixed z-[400] max-w-[220px] rounded bg-inverse-surface px-3 py-1.5 text-label-medium text-inverse-on-surface shadow-elevation-2"
75
+ :style="tipStyle"
76
+ role="tooltip"
77
+ >
78
+ {{ text }}
79
+ </div>
80
+ </Transition>
81
+ </Teleport>
82
+ </template>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import MIcon from './MIcon.vue'
3
+ import MIconButton from './MIconButton.vue'
4
+
5
+ withDefaults(defineProps<{
6
+ title?: string
7
+ variant?: 'center' | 'small' | 'medium' | 'large'
8
+ navigationIcon?: string
9
+ elevated?: boolean
10
+ }>(), { variant: 'small' })
11
+
12
+ defineEmits<{ navigation: [] }>()
13
+ </script>
14
+
15
+ <template>
16
+ <header
17
+ class="flex w-full flex-col bg-surface transition-shadow"
18
+ :class="elevated ? 'shadow-elevation-2' : ''"
19
+ >
20
+ <!-- Top row -->
21
+ <div class="flex h-16 items-center gap-1 px-2">
22
+ <!-- Navigation icon -->
23
+ <MIconButton
24
+ v-if="navigationIcon"
25
+ :icon="navigationIcon"
26
+ label="Navegación"
27
+ @click="$emit('navigation')"
28
+ />
29
+
30
+ <!-- Title: center or small variant -->
31
+ <h1
32
+ v-if="variant === 'center' || variant === 'small'"
33
+ class="flex-1 truncate px-2 text-title-large text-on-surface"
34
+ :class="variant === 'center' ? 'text-center' : ''"
35
+ >
36
+ <slot name="title">{{ title }}</slot>
37
+ </h1>
38
+
39
+ <!-- Spacer for medium/large (title is below) -->
40
+ <div v-else class="flex-1" />
41
+
42
+ <!-- Trailing actions -->
43
+ <div v-if="$slots.actions" class="flex items-center gap-1">
44
+ <slot name="actions" />
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Large title row for medium/large variants -->
49
+ <div
50
+ v-if="variant === 'medium' || variant === 'large'"
51
+ class="px-4 pb-6"
52
+ :class="variant === 'large' ? 'pt-4' : 'pt-1'"
53
+ >
54
+ <h1
55
+ class="text-on-surface"
56
+ :class="variant === 'large' ? 'text-headline-medium' : 'text-headline-small'"
57
+ >
58
+ <slot name="title">{{ title }}</slot>
59
+ </h1>
60
+ </div>
61
+ </header>
62
+ </template>