@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,259 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useId, onMounted, onUnmounted } 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 | null
9
+ options: { label: string; value: string | number; disabled?: boolean }[]
10
+ label?: string
11
+ placeholder?: string
12
+ variant?: 'filled' | 'outlined'
13
+ disabled?: boolean
14
+ error?: string
15
+ hint?: string
16
+ required?: boolean
17
+ leadingIcon?: string
18
+ fieldBg?: string
19
+ }>(),
20
+ {
21
+ modelValue: null,
22
+ variant: 'filled',
23
+ disabled: false,
24
+ required: false,
25
+ },
26
+ )
27
+
28
+ const emit = defineEmits<{ 'update:modelValue': [string | number] }>()
29
+
30
+ const id = useId()
31
+ const open = ref(false)
32
+ const fieldEl = ref<HTMLElement | null>(null)
33
+ const { resolvedFieldBg } = useFieldBg(fieldEl, () => props.fieldBg)
34
+ const dropdownEl = ref<HTMLElement | null>(null)
35
+ const dropPos = ref({ top: '0px', left: '0px', width: '0px' })
36
+
37
+ const hasValue = computed(() => props.modelValue !== null && props.modelValue !== '')
38
+ const selectedLabel = computed(
39
+ () => props.options.find((o) => o.value === props.modelValue)?.label ?? '',
40
+ )
41
+
42
+ function computeDropPos() {
43
+ if (!fieldEl.value) return
44
+ const rect = fieldEl.value.getBoundingClientRect()
45
+ const spaceBelow = window.innerHeight - rect.bottom - 8
46
+ const dropH = Math.min(240, props.options.length * 52 + 8)
47
+ const openAbove = spaceBelow < dropH && rect.top > dropH
48
+ dropPos.value = {
49
+ top: openAbove ? `${rect.top - 4 - dropH}px` : `${rect.bottom + 4}px`,
50
+ left: `${rect.left}px`,
51
+ width: `${rect.width}px`,
52
+ }
53
+ }
54
+
55
+ function toggle() {
56
+ if (props.disabled) return
57
+ if (!open.value) computeDropPos()
58
+ open.value = !open.value
59
+ }
60
+
61
+ function select(opt: { value: string | number; disabled?: boolean }) {
62
+ if (opt.disabled) return
63
+ emit('update:modelValue', opt.value)
64
+ open.value = false
65
+ }
66
+
67
+ function onOutsideClick(e: MouseEvent) {
68
+ const t = e.target as Node
69
+ if (!fieldEl.value?.contains(t) && !dropdownEl.value?.contains(t)) open.value = false
70
+ }
71
+
72
+ function onScroll(e: Event) {
73
+ if (!open.value) return
74
+ // Scrolling inside the dropdown list itself — do nothing
75
+ if (dropdownEl.value?.contains(e.target as Node)) return
76
+ // Recompute position to track the trigger element as the page scrolls
77
+ if (!fieldEl.value) return
78
+ const rect = fieldEl.value.getBoundingClientRect()
79
+ // Only close if the trigger has scrolled completely out of the viewport
80
+ if (rect.bottom < 0 || rect.top > window.innerHeight) {
81
+ open.value = false
82
+ return
83
+ }
84
+ computeDropPos()
85
+ }
86
+
87
+ function onKeydown(e: KeyboardEvent) {
88
+ if (e.key === 'Escape') { open.value = false; return }
89
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); return }
90
+ if (!open.value) return
91
+ const opts = props.options.filter((o) => !o.disabled)
92
+ const idx = opts.findIndex((o) => o.value === props.modelValue)
93
+ if (e.key === 'ArrowDown') {
94
+ e.preventDefault()
95
+ const next = opts[(idx + 1) % opts.length]
96
+ if (next) emit('update:modelValue', next.value)
97
+ }
98
+ if (e.key === 'ArrowUp') {
99
+ e.preventDefault()
100
+ const prev = opts[(idx - 1 + opts.length) % opts.length]
101
+ if (prev) emit('update:modelValue', prev.value)
102
+ }
103
+ }
104
+
105
+ onMounted(() => {
106
+ document.addEventListener('mousedown', onOutsideClick)
107
+ window.addEventListener('scroll', onScroll, true)
108
+ })
109
+ onUnmounted(() => {
110
+ document.removeEventListener('mousedown', onOutsideClick)
111
+ window.removeEventListener('scroll', onScroll, true)
112
+ })
113
+
114
+ const triggerClasses = computed(() => {
115
+ const pl = props.leadingIcon ? 'pl-12' : 'pl-4'
116
+ const base = [
117
+ 'flex w-full cursor-pointer items-center pr-10 text-body-large transition-[border-color,border-width] duration-150',
118
+ pl,
119
+ ]
120
+
121
+ if (props.variant === 'outlined') {
122
+ return [
123
+ ...base,
124
+ 'h-14 rounded-sm border bg-transparent',
125
+ open.value
126
+ ? (props.error ? 'border-2 border-error' : 'border-2 border-primary')
127
+ : (props.error ? 'border-error' : 'border-outline hover:border-on-surface'),
128
+ ].join(' ')
129
+ }
130
+
131
+ return [
132
+ ...base,
133
+ 'h-14 rounded-t-sm bg-surface-container-highest border-b pt-6 pb-2',
134
+ open.value
135
+ ? (props.error ? 'border-b-2 border-error' : 'border-b-2 border-primary')
136
+ : (props.error ? 'border-error' : 'border-on-surface-variant hover:border-on-surface'),
137
+ ].join(' ')
138
+ })
139
+
140
+ const isFloated = computed(() => hasValue.value || open.value)
141
+
142
+ const labelClasses = computed(() => {
143
+ const left = props.leadingIcon
144
+ ? (props.variant === 'outlined' ? 'left-11' : 'left-12')
145
+ : (props.variant === 'outlined' ? 'left-3' : 'left-4')
146
+
147
+ const floated = props.variant === 'outlined'
148
+ ? '-top-2.5 translate-y-0 text-label-small bg-[var(--field-bg)] px-1 right-auto max-w-[calc(100%-1.5rem)]'
149
+ : 'top-2 translate-y-0 text-label-small'
150
+
151
+ const unFloated = 'top-1/2 -translate-y-1/2 text-body-large'
152
+
153
+ return [
154
+ 'pointer-events-none absolute right-10 truncate transition-all duration-200',
155
+ left,
156
+ isFloated.value ? floated : unFloated,
157
+ open.value
158
+ ? (props.error ? 'text-error' : 'text-primary')
159
+ : (props.error ? 'text-error' : 'text-on-surface-variant'),
160
+ ].join(' ')
161
+ })
162
+ </script>
163
+
164
+ <template>
165
+ <div class="flex flex-col gap-1">
166
+ <div
167
+ ref="fieldEl"
168
+ class="relative"
169
+ :class="variant === 'outlined' ? 'mt-2' : ''"
170
+ :style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
171
+ >
172
+ <!-- Leading icon -->
173
+ <div
174
+ v-if="leadingIcon"
175
+ class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-on-surface-variant"
176
+ >
177
+ <MIcon :name="leadingIcon" :size="20" />
178
+ </div>
179
+
180
+ <!-- Custom trigger -->
181
+ <div
182
+ :id="id"
183
+ :tabindex="disabled ? -1 : 0"
184
+ role="combobox"
185
+ :aria-expanded="open"
186
+ :aria-disabled="disabled"
187
+ :class="[triggerClasses, disabled ? 'pointer-events-none opacity-[0.38]' : '']"
188
+ @click="toggle"
189
+ @keydown="onKeydown"
190
+ >
191
+ <span v-if="hasValue" class="text-on-surface">{{ selectedLabel }}</span>
192
+ </div>
193
+
194
+ <!-- Floating label -->
195
+ <label :for="id" :class="labelClasses">
196
+ {{ label }}<span v-if="required" class="text-error">&nbsp;*</span>
197
+ </label>
198
+
199
+ <!-- Arrow icon -->
200
+ <div class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2">
201
+ <MIcon
202
+ :name="open ? 'arrow_drop_up' : 'arrow_drop_down'"
203
+ :size="24"
204
+ class="text-on-surface-variant transition-transform duration-200"
205
+ />
206
+ </div>
207
+ </div>
208
+
209
+ <p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
210
+ <p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
211
+ </div>
212
+
213
+ <!-- Dropdown teleported to body to escape overflow clipping -->
214
+ <Teleport to="body">
215
+ <Transition
216
+ enter-active-class="transition-[opacity,transform] duration-150"
217
+ enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
218
+ enter-to-class="opacity-100 translate-y-0 scale-100"
219
+ leave-active-class="transition-[opacity,transform] duration-100"
220
+ leave-from-class="opacity-100 translate-y-0 scale-100"
221
+ leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
222
+ >
223
+ <div
224
+ v-if="open"
225
+ ref="dropdownEl"
226
+ class="fixed z-[500] max-h-60 overflow-auto rounded-sm bg-surface-container py-1 shadow-elevation-2"
227
+ :style="dropPos"
228
+ >
229
+ <div
230
+ v-for="opt in options"
231
+ :key="opt.value"
232
+ class="flex cursor-pointer items-center gap-3 px-4 py-3 text-body-large"
233
+ :class="[
234
+ opt.disabled
235
+ ? 'cursor-not-allowed opacity-38 text-on-surface'
236
+ : 'text-on-surface hover:bg-on-surface/8',
237
+ opt.value === modelValue ? 'bg-primary/8 text-primary font-medium' : '',
238
+ ]"
239
+ @click="select(opt)"
240
+ >
241
+ <MIcon
242
+ v-if="opt.value === modelValue"
243
+ name="check"
244
+ :size="18"
245
+ class="shrink-0 text-primary"
246
+ />
247
+ <span v-else class="w-[18px] shrink-0" />
248
+ {{ opt.label }}
249
+ </div>
250
+ <p
251
+ v-if="!options.length"
252
+ class="px-4 py-3 text-center text-body-small text-on-surface-variant"
253
+ >
254
+ Sin opciones
255
+ </p>
256
+ </div>
257
+ </Transition>
258
+ </Teleport>
259
+ </template>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+
5
+ withDefaults(defineProps<{
6
+ modelValue: boolean
7
+ title?: string
8
+ width?: string
9
+ }>(), { width: 'w-80' })
10
+
11
+ const emit = defineEmits<{ 'update:modelValue': [boolean] }>()
12
+ const close = () => emit('update:modelValue', false)
13
+
14
+ const dragX = ref(0)
15
+ const dragging = ref(false)
16
+ let startX = 0
17
+
18
+ function onEdgePointerDown(e: PointerEvent) {
19
+ dragging.value = true
20
+ startX = e.clientX
21
+ dragX.value = 0
22
+ ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
23
+ }
24
+ function onEdgePointerMove(e: PointerEvent) {
25
+ if (!dragging.value) return
26
+ dragX.value = Math.max(0, e.clientX - startX)
27
+ }
28
+ function onEdgePointerUp() {
29
+ if (dragX.value > 100) close()
30
+ dragging.value = false
31
+ dragX.value = 0
32
+ }
33
+
34
+ const panelStyle = computed(() => ({
35
+ transform: `translateX(${dragX.value}px)`,
36
+ transition: dragging.value ? 'none' : undefined,
37
+ }))
38
+ </script>
39
+
40
+ <template>
41
+ <Teleport to="body">
42
+ <Transition name="ss" :duration="{ enter: 320, leave: 280 }">
43
+ <div v-if="modelValue" class="fixed inset-0 z-[200] flex justify-end">
44
+ <!-- Scrim -->
45
+ <div class="ss-scrim absolute inset-0 bg-black/40" @click="close" />
46
+
47
+ <!-- Panel -->
48
+ <aside
49
+ class="ss-panel relative flex h-full flex-col bg-surface-container-low shadow-elevation-3"
50
+ :class="[width, 'max-w-[90vw]']"
51
+ :style="panelStyle"
52
+ >
53
+ <!-- Drag edge -->
54
+ <div
55
+ class="absolute top-0 left-0 h-full w-3 cursor-ew-resize touch-none"
56
+ @pointerdown="onEdgePointerDown"
57
+ @pointermove="onEdgePointerMove"
58
+ @pointerup="onEdgePointerUp"
59
+ />
60
+
61
+ <!-- Header -->
62
+ <div v-if="title || $slots.header" class="flex shrink-0 items-center justify-between border-b border-outline-variant px-6 py-4">
63
+ <slot name="header">
64
+ <h2 class="text-title-large text-on-surface">{{ title }}</h2>
65
+ </slot>
66
+ <button
67
+ type="button"
68
+ class="flex h-9 w-9 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-on-surface/8"
69
+ @click="close"
70
+ >
71
+ <MIcon name="close" :size="20" />
72
+ </button>
73
+ </div>
74
+
75
+ <!-- Content -->
76
+ <div class="flex-1 overflow-y-auto px-6 py-4">
77
+ <slot />
78
+ </div>
79
+
80
+ <!-- Actions -->
81
+ <div v-if="$slots.actions" class="shrink-0 border-t border-outline-variant px-6 py-4">
82
+ <slot name="actions" />
83
+ </div>
84
+ </aside>
85
+ </div>
86
+ </Transition>
87
+ </Teleport>
88
+ </template>
89
+
90
+ <style scoped>
91
+ .ss-scrim {
92
+ transition: opacity 280ms ease;
93
+ }
94
+ .ss-enter-from .ss-scrim,
95
+ .ss-leave-to .ss-scrim {
96
+ opacity: 0;
97
+ }
98
+
99
+ .ss-panel {
100
+ transition:
101
+ transform 320ms cubic-bezier(0.2, 0, 0, 1),
102
+ opacity 240ms ease;
103
+ }
104
+ .ss-enter-from .ss-panel {
105
+ transform: translateX(40%);
106
+ opacity: 0;
107
+ }
108
+ .ss-leave-to .ss-panel {
109
+ transform: translateX(100%) !important;
110
+ opacity: 0;
111
+ }
112
+ </style>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{
3
+ variant?: 'text' | 'circular' | 'rectangular' | 'rounded'
4
+ width?: string
5
+ height?: string
6
+ lines?: number
7
+ animation?: 'pulse' | 'wave' | 'none'
8
+ }>(), {
9
+ variant: 'text',
10
+ animation: 'pulse',
11
+ lines: 1,
12
+ })
13
+ </script>
14
+
15
+ <template>
16
+ <!-- Multi-line text skeleton -->
17
+ <div v-if="variant === 'text' && lines > 1" class="flex flex-col gap-2.5">
18
+ <div
19
+ v-for="i in lines"
20
+ :key="i"
21
+ class="h-3.5 rounded-full bg-on-surface/10"
22
+ :class="animation === 'pulse' ? 'animate-pulse' : animation === 'wave' ? 'skeleton-wave' : ''"
23
+ :style="{ width: i === lines ? '60%' : (width ?? '100%') }"
24
+ />
25
+ </div>
26
+
27
+ <!-- Single element -->
28
+ <div
29
+ v-else
30
+ class="bg-on-surface/10"
31
+ :class="[
32
+ variant === 'circular' ? 'rounded-full' : variant === 'rounded' ? 'rounded-lg' : variant === 'text' ? 'rounded-full' : 'rounded-sm',
33
+ animation === 'pulse' ? 'animate-pulse' : animation === 'wave' ? 'skeleton-wave' : '',
34
+ ]"
35
+ :style="{
36
+ width: width ?? (variant === 'circular' ? '40px' : '100%'),
37
+ height: height ?? (variant === 'circular' ? '40px' : variant === 'text' ? '14px' : '100px'),
38
+ }"
39
+ />
40
+ </template>
41
+
42
+ <style scoped>
43
+ @keyframes skeleton-wave-move {
44
+ 0% { transform: translateX(-100%); }
45
+ 60% { transform: translateX(100%); }
46
+ 100% { transform: translateX(100%); }
47
+ }
48
+ .skeleton-wave {
49
+ position: relative;
50
+ overflow: hidden;
51
+ }
52
+ .skeleton-wave::after {
53
+ content: '';
54
+ position: absolute;
55
+ inset: 0;
56
+ background: linear-gradient(90deg, transparent 0%, var(--color-on-surface) 50%, transparent 100%);
57
+ opacity: 0.06;
58
+ animation: skeleton-wave-move 1.8s ease-in-out infinite;
59
+ }
60
+ </style>
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, ref } from "vue";
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ modelValue: number;
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ disabled?: boolean;
11
+ label?: string;
12
+ showValue?: boolean;
13
+ color?: "primary" | "secondary" | "tertiary" | "error";
14
+ }>(),
15
+ {
16
+ min: 0,
17
+ max: 100,
18
+ step: 1,
19
+ disabled: false,
20
+ showValue: false,
21
+ color: "primary",
22
+ },
23
+ );
24
+
25
+ const emit = defineEmits<{
26
+ "update:modelValue": [number];
27
+ }>();
28
+
29
+ const trackEl = ref<HTMLElement>();
30
+ const dragging = ref(false);
31
+
32
+ const pct = computed(() => {
33
+ const range = props.max - props.min;
34
+ return range === 0 ? 0 : ((props.modelValue - props.min) / range) * 100;
35
+ });
36
+
37
+ const colors: Record<string, { active: string; inactive: string; thumb: string }> = {
38
+ primary: {
39
+ active: "bg-primary",
40
+ inactive: "bg-primary-container",
41
+ thumb: "bg-primary",
42
+ },
43
+ secondary: {
44
+ active: "bg-secondary",
45
+ inactive: "bg-secondary-container",
46
+ thumb: "bg-secondary",
47
+ },
48
+ tertiary: {
49
+ active: "bg-tertiary",
50
+ inactive: "bg-tertiary-container",
51
+ thumb: "bg-tertiary",
52
+ },
53
+ error: {
54
+ active: "bg-error",
55
+ inactive: "bg-error-container",
56
+ thumb: "bg-error",
57
+ },
58
+ };
59
+
60
+ function clamp(v: number) {
61
+ const stepped = Math.round((v - props.min) / props.step) * props.step + props.min;
62
+
63
+ return Math.max(props.min, Math.min(props.max, stepped));
64
+ }
65
+
66
+ function valueFromX(clientX: number) {
67
+ if (!trackEl.value) return props.modelValue;
68
+
69
+ const rect = trackEl.value.getBoundingClientRect();
70
+
71
+ const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
72
+
73
+ return clamp(props.min + ratio * (props.max - props.min));
74
+ }
75
+
76
+ function onPointerDown(e: PointerEvent) {
77
+ if (props.disabled) return;
78
+
79
+ e.preventDefault();
80
+
81
+ dragging.value = true;
82
+
83
+ emit("update:modelValue", valueFromX(e.clientX));
84
+
85
+ window.addEventListener("pointermove", onPointerMove);
86
+ window.addEventListener("pointerup", onPointerUp);
87
+ window.addEventListener("pointercancel", onPointerUp);
88
+ }
89
+
90
+ function onPointerMove(e: PointerEvent) {
91
+ if (!dragging.value) return;
92
+
93
+ emit("update:modelValue", valueFromX(e.clientX));
94
+ }
95
+
96
+ function onPointerUp() {
97
+ dragging.value = false;
98
+
99
+ window.removeEventListener("pointermove", onPointerMove);
100
+ window.removeEventListener("pointerup", onPointerUp);
101
+ window.removeEventListener("pointercancel", onPointerUp);
102
+ }
103
+
104
+ function onKeyDown(e: KeyboardEvent) {
105
+ if (props.disabled) return;
106
+
107
+ const delta = {
108
+ ArrowRight: 1,
109
+ ArrowUp: 1,
110
+ ArrowLeft: -1,
111
+ ArrowDown: -1,
112
+ }[e.key];
113
+
114
+ if (delta !== undefined) {
115
+ e.preventDefault();
116
+
117
+ emit("update:modelValue", clamp(props.modelValue + delta * props.step));
118
+ }
119
+
120
+ if (e.key === "Home") {
121
+ e.preventDefault();
122
+ emit("update:modelValue", props.min);
123
+ }
124
+
125
+ if (e.key === "End") {
126
+ e.preventDefault();
127
+ emit("update:modelValue", props.max);
128
+ }
129
+ }
130
+
131
+ onBeforeUnmount(() => {
132
+ window.removeEventListener("pointermove", onPointerMove);
133
+ window.removeEventListener("pointerup", onPointerUp);
134
+ window.removeEventListener("pointercancel", onPointerUp);
135
+ });
136
+
137
+ const thumbStyle = computed(() => ({
138
+ left: `${pct.value}%`,
139
+ top: "50%",
140
+ transform: `translateX(-50%) translateY(-50%) scale(${dragging.value ? 1.15 : 1})`,
141
+ transition: dragging.value ? "transform 80ms ease" : "left 75ms ease, transform 80ms ease",
142
+ }));
143
+ </script>
144
+
145
+ <template>
146
+ <div class="flex flex-col gap-1 select-none">
147
+ <div v-if="label || showValue" class="flex items-center justify-between">
148
+ <span v-if="label" class="text-label-large text-on-surface">
149
+ {{ label }}
150
+ </span>
151
+
152
+ <span v-if="showValue" class="tabular-nums text-label-large text-on-surface-variant">
153
+ {{ modelValue }}
154
+ </span>
155
+ </div>
156
+
157
+ <div
158
+ ref="trackEl"
159
+ role="slider"
160
+ tabindex="0"
161
+ :aria-valuenow="modelValue"
162
+ :aria-valuemin="min"
163
+ :aria-valuemax="max"
164
+ :aria-disabled="disabled || undefined"
165
+ class="relative flex h-10 w-full touch-none cursor-pointer items-center outline-none"
166
+ :class="disabled && 'cursor-not-allowed opacity-[0.38]'"
167
+ @pointerdown="onPointerDown"
168
+ @keydown="onKeyDown"
169
+ >
170
+ <div class="relative h-1 w-full rounded-full" :class="colors[color]!.inactive">
171
+ <div
172
+ class="h-full rounded-full"
173
+ :class="colors[color]!.active"
174
+ :style="{
175
+ width: `${pct}%`,
176
+ transition: dragging ? 'none' : 'width 75ms ease',
177
+ }"
178
+ />
179
+ </div>
180
+
181
+ <div
182
+ class="pointer-events-none absolute h-5 w-5 rounded-full shadow-elevation-1"
183
+ :class="colors[color]!.thumb"
184
+ :style="thumbStyle"
185
+ />
186
+ </div>
187
+ </div>
188
+ </template>