@m3ui-vue/m3ui-vue 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +219 -59
  2. package/dist/{MMenuItem-C6RwHl-l.js → MMenuItem-FyttjOj_.js} +399 -207
  3. package/dist/MMenuItem-FyttjOj_.js.map +1 -0
  4. package/dist/components/MAutocomplete.vue.d.ts +2 -0
  5. package/dist/components/MBox.vue.d.ts +2 -2
  6. package/dist/components/MButton.vue.d.ts +4 -0
  7. package/dist/components/MButtonGroup.vue.d.ts +28 -0
  8. package/dist/components/MCarousel.vue.d.ts +21 -12
  9. package/dist/components/MColorPickerModal.vue.d.ts +18 -0
  10. package/dist/components/MDataTable.vue.d.ts +1 -1
  11. package/dist/components/MDatePickerModal.vue.d.ts +21 -0
  12. package/dist/components/MDateRangePickerModal.vue.d.ts +24 -0
  13. package/dist/components/MDialog.vue.d.ts +9 -1
  14. package/dist/components/MExpansionPanel.vue.d.ts +3 -2
  15. package/dist/components/MFab.vue.d.ts +14 -2
  16. package/dist/components/MFlex.vue.d.ts +2 -2
  17. package/dist/components/MIconButton.vue.d.ts +4 -2
  18. package/dist/components/MInfiniteScroll.vue.d.ts +1 -1
  19. package/dist/components/MList.vue.d.ts +2 -0
  20. package/dist/components/MLoadingOverlay.vue.d.ts +1 -1
  21. package/dist/components/MMenu.vue.d.ts +3 -1
  22. package/dist/components/MMenuItem.vue.d.ts +14 -3
  23. package/dist/components/MMultiAutocomplete.vue.d.ts +2 -0
  24. package/dist/components/MMultiSelect.vue.d.ts +2 -0
  25. package/dist/components/MNavigationDrawer.vue.d.ts +1 -1
  26. package/dist/components/MNotificationHost.vue.d.ts +3 -0
  27. package/dist/components/MSelect.vue.d.ts +2 -0
  28. package/dist/components/MSlider.vue.d.ts +15 -4
  29. package/dist/components/MSplitButton.vue.d.ts +39 -0
  30. package/dist/components/MSplitter.vue.d.ts +1 -1
  31. package/dist/components/MStack.vue.d.ts +2 -2
  32. package/dist/components/MTable.vue.d.ts +1 -1
  33. package/dist/components/MTimePickerModal.vue.d.ts +21 -0
  34. package/dist/components/MToolbar.vue.d.ts +30 -0
  35. package/dist/components/MTooltip.vue.d.ts +8 -2
  36. package/dist/components/MWindow.vue.d.ts +2 -2
  37. package/dist/composables/useDevice.d.ts +3 -0
  38. package/dist/composables/useNotification.d.ts +64 -0
  39. package/dist/composables/useToast.d.ts +8 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/m3ui-vue.css +1 -1
  42. package/dist/m3ui.js +5222 -2994
  43. package/dist/m3ui.js.map +1 -1
  44. package/dist/rich-text-editor.js +10 -10
  45. package/dist/styles.css +1 -1
  46. package/package.json +1 -1
  47. package/src/components/MAutocomplete.vue +109 -13
  48. package/src/components/MButton.vue +38 -16
  49. package/src/components/MButtonGroup.vue +158 -0
  50. package/src/components/MCarousel.vue +223 -101
  51. package/src/components/MColorPicker.vue +1 -4
  52. package/src/components/MColorPickerModal.vue +276 -0
  53. package/src/components/MDatePicker.vue +1 -4
  54. package/src/components/MDatePickerModal.vue +265 -0
  55. package/src/components/MDateRangePicker.vue +1 -4
  56. package/src/components/MDateRangePickerModal.vue +264 -0
  57. package/src/components/MDialog.vue +38 -1
  58. package/src/components/MExpansionPanel.vue +10 -32
  59. package/src/components/MFab.vue +92 -11
  60. package/src/components/MIconButton.vue +36 -6
  61. package/src/components/MList.vue +6 -3
  62. package/src/components/MListItem.vue +9 -3
  63. package/src/components/MMenu.vue +10 -5
  64. package/src/components/MMenuItem.vue +93 -10
  65. package/src/components/MMultiAutocomplete.vue +105 -10
  66. package/src/components/MMultiSelect.vue +106 -13
  67. package/src/components/MNotificationHost.vue +163 -0
  68. package/src/components/MSelect.vue +99 -16
  69. package/src/components/MSlider.vue +367 -134
  70. package/src/components/MSnackbar.vue +3 -0
  71. package/src/components/MSplitButton.vue +240 -0
  72. package/src/components/MTabs.vue +82 -39
  73. package/src/components/MTagInput.vue +11 -1
  74. package/src/components/MTimePicker.vue +1 -4
  75. package/src/components/MTimePickerModal.vue +458 -0
  76. package/src/components/MToolbar.vue +75 -0
  77. package/src/components/MTooltip.vue +60 -10
  78. package/src/components/_MContextMenuPanel.vue +2 -2
  79. package/src/composables/useDevice.ts +29 -0
  80. package/src/composables/useNotification.ts +93 -0
  81. package/src/composables/useToast.ts +42 -5
  82. package/src/index.ts +12 -0
  83. package/dist/MMenuItem-C6RwHl-l.js.map +0 -1
@@ -1,16 +1,23 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, ref } from "vue";
2
+ import { computed, onBeforeUnmount, ref } from 'vue'
3
+ import MIcon from './MIcon.vue'
3
4
 
4
5
  const props = withDefaults(
5
6
  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";
7
+ modelValue: number | [number, number]
8
+ min?: number
9
+ max?: number
10
+ step?: number
11
+ disabled?: boolean
12
+ label?: string
13
+ showValue?: boolean
14
+ showTooltip?: boolean
15
+ orientation?: 'horizontal' | 'vertical'
16
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
17
+ variant?: 'standard' | 'centered' | 'range'
18
+ stops?: boolean
19
+ icon?: string
20
+ color?: 'primary' | 'secondary' | 'tertiary' | 'error'
14
21
  }>(),
15
22
  {
16
23
  min: 0,
@@ -18,171 +25,397 @@ const props = withDefaults(
18
25
  step: 1,
19
26
  disabled: false,
20
27
  showValue: false,
21
- color: "primary",
28
+ showTooltip: false,
29
+ orientation: 'horizontal',
30
+ size: 'xs',
31
+ variant: 'standard',
32
+ stops: false,
33
+ color: 'primary',
22
34
  },
23
- );
35
+ )
24
36
 
25
- const emit = defineEmits<{
26
- "update:modelValue": [number];
27
- }>();
37
+ const emit = defineEmits<{ 'update:modelValue': [number | [number, number]] }>()
28
38
 
29
- const trackEl = ref<HTMLElement>();
30
- const dragging = ref(false);
39
+ const trackEl = ref<HTMLElement>()
40
+ const dragging = ref<false | 'single' | 'lo' | 'hi'>(false)
41
+ const isVertical = computed(() => props.orientation === 'vertical')
42
+ const isRange = computed(() => props.variant === 'range')
43
+ const isCentered = computed(() => props.variant === 'centered')
31
44
 
32
- const pct = computed(() => {
33
- const range = props.max - props.min;
34
- return range === 0 ? 0 : ((props.modelValue - props.min) / range) * 100;
35
- });
45
+ const val = computed(() => {
46
+ if (isRange.value) {
47
+ const v = props.modelValue as [number, number]
48
+ return { lo: Math.min(v[0], v[1]), hi: Math.max(v[0], v[1]) }
49
+ }
50
+ return { lo: props.modelValue as number, hi: props.modelValue as number }
51
+ })
36
52
 
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
- };
53
+ function toPct(v: number) {
54
+ const range = props.max - props.min
55
+ return range === 0 ? 0 : ((v - props.min) / range) * 100
56
+ }
59
57
 
60
- function clamp(v: number) {
61
- const stepped = Math.round((v - props.min) / props.step) * props.step + props.min;
58
+ const pctLo = computed(() => toPct(val.value.lo))
59
+ const pctHi = computed(() => toPct(val.value.hi))
60
+ const centerPct = computed(() => toPct(0))
62
61
 
63
- return Math.max(props.min, Math.min(props.max, stepped));
62
+ const sizeMap = {
63
+ xs: { track: 16, thumbW: 4, thumbH: 28, dot: 4, gap: 6, hitArea: 38, radius: 6, iconSize: 14 },
64
+ sm: { track: 28, thumbW: 4, thumbH: 36, dot: 5, gap: 6, hitArea: 46, radius: 8, iconSize: 16 },
65
+ md: { track: 54, thumbW: 5, thumbH: 56, dot: 5, gap: 6, hitArea: 66, radius: 12, iconSize: 20 },
66
+ lg: { track: 72, thumbW: 5, thumbH: 72, dot: 6, gap: 8, hitArea: 82, radius: 14, iconSize: 24 },
67
+ xl: { track: 96, thumbW: 6, thumbH: 90, dot: 7, gap: 8, hitArea: 100, radius: 18, iconSize: 28 },
64
68
  }
65
69
 
66
- function valueFromX(clientX: number) {
67
- if (!trackEl.value) return props.modelValue;
70
+ const s = computed(() => sizeMap[props.size] ?? sizeMap.xs)
68
71
 
69
- const rect = trackEl.value.getBoundingClientRect();
70
-
71
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
72
+ const ct = computed(() => {
73
+ const map: Record<string, { active: string; inactive: string }> = {
74
+ primary: { active: 'var(--color-primary)', inactive: 'var(--color-primary-container)' },
75
+ secondary: { active: 'var(--color-secondary)', inactive: 'var(--color-secondary-container)' },
76
+ tertiary: { active: 'var(--color-tertiary)', inactive: 'var(--color-tertiary-container)' },
77
+ error: { active: 'var(--color-error)', inactive: 'var(--color-error-container)' },
78
+ }
79
+ return map[props.color] ?? map.primary!
80
+ })
72
81
 
73
- return clamp(props.min + ratio * (props.max - props.min));
82
+ function clamp(v: number) {
83
+ const stepped = Math.round((v - props.min) / props.step) * props.step + props.min
84
+ return Math.max(props.min, Math.min(props.max, +stepped.toFixed(10)))
74
85
  }
75
86
 
76
- function onPointerDown(e: PointerEvent) {
77
- if (props.disabled) return;
78
-
79
- e.preventDefault();
80
-
81
- dragging.value = true;
87
+ function ratioFromEvent(e: PointerEvent) {
88
+ if (!trackEl.value) return 0
89
+ const rect = trackEl.value.getBoundingClientRect()
90
+ if (isVertical.value) return 1 - Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height))
91
+ return Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
92
+ }
82
93
 
83
- emit("update:modelValue", valueFromX(e.clientX));
94
+ function valueFromEvent(e: PointerEvent) {
95
+ return clamp(props.min + ratioFromEvent(e) * (props.max - props.min))
96
+ }
84
97
 
85
- window.addEventListener("pointermove", onPointerMove);
86
- window.addEventListener("pointerup", onPointerUp);
87
- window.addEventListener("pointercancel", onPointerUp);
98
+ function onPointerDown(e: PointerEvent) {
99
+ if (props.disabled) return
100
+ e.preventDefault()
101
+ const v = valueFromEvent(e)
102
+ if (isRange.value) {
103
+ const distLo = Math.abs(v - val.value.lo)
104
+ const distHi = Math.abs(v - val.value.hi)
105
+ dragging.value = distLo <= distHi ? 'lo' : 'hi'
106
+ const cur = props.modelValue as [number, number]
107
+ emit('update:modelValue', dragging.value === 'lo' ? [v, cur[1]] : [cur[0], v])
108
+ } else {
109
+ dragging.value = 'single'
110
+ emit('update:modelValue', v)
111
+ }
112
+ window.addEventListener('pointermove', onPointerMove)
113
+ window.addEventListener('pointerup', onPointerUp)
114
+ window.addEventListener('pointercancel', onPointerUp)
88
115
  }
89
116
 
90
117
  function onPointerMove(e: PointerEvent) {
91
- if (!dragging.value) return;
92
-
93
- emit("update:modelValue", valueFromX(e.clientX));
118
+ if (!dragging.value) return
119
+ const v = valueFromEvent(e)
120
+ if (isRange.value) {
121
+ const cur = props.modelValue as [number, number]
122
+ emit('update:modelValue', dragging.value === 'lo' ? [v, cur[1]] : [cur[0], v])
123
+ } else {
124
+ emit('update:modelValue', v)
125
+ }
94
126
  }
95
127
 
96
128
  function onPointerUp() {
97
- dragging.value = false;
98
-
99
- window.removeEventListener("pointermove", onPointerMove);
100
- window.removeEventListener("pointerup", onPointerUp);
101
- window.removeEventListener("pointercancel", onPointerUp);
129
+ dragging.value = false
130
+ window.removeEventListener('pointermove', onPointerMove)
131
+ window.removeEventListener('pointerup', onPointerUp)
132
+ window.removeEventListener('pointercancel', onPointerUp)
102
133
  }
103
134
 
104
135
  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
-
136
+ if (props.disabled) return
137
+ const d: Record<string, number> = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 }
138
+ const delta = d[e.key]
114
139
  if (delta !== undefined) {
115
- e.preventDefault();
116
-
117
- emit("update:modelValue", clamp(props.modelValue + delta * props.step));
140
+ e.preventDefault()
141
+ if (isRange.value) {
142
+ const cur = props.modelValue as [number, number]
143
+ emit('update:modelValue', [clamp(cur[0] + delta * props.step), cur[1]])
144
+ } else {
145
+ emit('update:modelValue', clamp((props.modelValue as number) + delta * props.step))
146
+ }
118
147
  }
148
+ if (e.key === 'Home') { e.preventDefault(); emit('update:modelValue', isRange.value ? [props.min, (props.modelValue as [number, number])[1]] : props.min) }
149
+ if (e.key === 'End') { e.preventDefault(); emit('update:modelValue', isRange.value ? [(props.modelValue as [number, number])[0], props.max] : props.max) }
150
+ }
119
151
 
120
- if (e.key === "Home") {
121
- e.preventDefault();
122
- emit("update:modelValue", props.min);
152
+ onBeforeUnmount(() => {
153
+ window.removeEventListener('pointermove', onPointerMove)
154
+ window.removeEventListener('pointerup', onPointerUp)
155
+ window.removeEventListener('pointercancel', onPointerUp)
156
+ })
157
+
158
+ const stopPositions = computed(() => {
159
+ if (!props.stops) return []
160
+ const positions: number[] = []
161
+ const range = props.max - props.min
162
+ const iconClearPct = props.icon ? (s.value.iconSize + s.value.radius) / 2 : 0
163
+ for (let v = props.min + props.step; v < props.max; v += props.step) {
164
+ const pct = ((v - props.min) / range) * 100
165
+ if (props.icon && pct < iconClearPct) continue
166
+ positions.push(pct)
167
+ }
168
+ return positions.length <= 28 ? positions : []
169
+ })
170
+
171
+ const r = computed(() => s.value.radius)
172
+ const nd = computed(() => !dragging.value)
173
+ const tr = computed(() => nd.value ? '75ms ease' : 'none')
174
+
175
+ function thumbPos(pct: number) {
176
+ const tw = isVertical.value ? s.value.thumbH : s.value.thumbW
177
+ const th = isVertical.value ? s.value.thumbW : s.value.thumbH
178
+ const br = Math.min(tw, th) / 2
179
+ const base: Record<string, string> = {
180
+ position: 'absolute',
181
+ width: `${tw}px`,
182
+ height: `${th}px`,
183
+ borderRadius: `${br}px`,
184
+ backgroundColor: ct.value.active,
185
+ zIndex: '2',
186
+ transition: nd.value ? 'left 75ms ease, bottom 75ms ease, transform 80ms ease' : 'transform 80ms ease',
187
+ }
188
+ if (isVertical.value) {
189
+ base.left = '50%'
190
+ base.bottom = `${pct}%`
191
+ base.transform = `translateX(-50%) translateY(50%) scaleX(${dragging.value ? 1.08 : 1})`
192
+ } else {
193
+ base.left = `${pct}%`
194
+ base.top = '50%'
195
+ base.transform = `translateX(-50%) translateY(-50%) scaleY(${dragging.value ? 1.08 : 1})`
123
196
  }
197
+ return base
198
+ }
124
199
 
125
- if (e.key === "End") {
126
- e.preventDefault();
127
- emit("update:modelValue", props.max);
200
+ function tooltipPos(pct: number) {
201
+ const offsetH = s.value.track / 2 + 36
202
+ const offsetV = s.value.track / 2 + 40
203
+ if (isVertical.value) {
204
+ return { left: `calc(50% - ${offsetV}px)`, bottom: `${pct}%`, transform: 'translateY(50%)', transition: nd.value ? 'bottom ' + tr.value : 'none' }
128
205
  }
206
+ return { left: `${pct}%`, top: `calc(50% - ${offsetH}px)`, transform: 'translateX(-50%)', transition: nd.value ? 'left ' + tr.value : 'none' }
129
207
  }
130
208
 
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
- }));
209
+ const displayValue = computed(() => {
210
+ if (isRange.value) return `${val.value.lo} – ${val.value.hi}`
211
+ return String(props.modelValue)
212
+ })
213
+
214
+ const tooltipValue = computed(() => {
215
+ if (isRange.value) {
216
+ if (dragging.value === 'lo') return val.value.lo
217
+ return val.value.hi
218
+ }
219
+ return props.modelValue
220
+ })
221
+
222
+ const tooltipPct = computed(() => {
223
+ if (isRange.value) return dragging.value === 'lo' ? pctLo.value : pctHi.value
224
+ return pctLo.value
225
+ })
143
226
  </script>
144
227
 
145
228
  <template>
146
- <div class="flex flex-col gap-1 select-none">
229
+ <div class="flex select-none" :class="isVertical ? 'h-full flex-col items-center' : 'flex-col gap-1'">
147
230
  <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>
231
+ <span v-if="label" class="text-label-large text-on-surface">{{ label }}</span>
232
+ <span v-if="showValue" class="tabular-nums text-label-large text-on-surface-variant">{{ displayValue }}</span>
155
233
  </div>
156
234
 
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">
235
+ <div class="flex items-center" :class="isVertical ? 'flex-1 flex-col' : ''">
236
+ <!-- Track area -->
237
+ <div
238
+ ref="trackEl"
239
+ role="slider"
240
+ tabindex="0"
241
+ :aria-valuenow="isRange ? undefined : (modelValue as number)"
242
+ :aria-valuemin="min"
243
+ :aria-valuemax="max"
244
+ :aria-disabled="disabled || undefined"
245
+ :aria-orientation="orientation"
246
+ class="relative touch-none cursor-pointer outline-none"
247
+ :class="disabled && 'cursor-not-allowed opacity-[0.38]'"
248
+ :style="isVertical
249
+ ? { width: `${s.hitArea}px`, minHeight: '160px', height: '100%' }
250
+ : { height: `${s.hitArea}px`, width: '100%' }
251
+ "
252
+ @pointerdown="onPointerDown"
253
+ @keydown="onKeyDown"
254
+ >
255
+ <!-- Track container -->
171
256
  <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
- />
257
+ class="absolute"
258
+ :style="isVertical
259
+ ? { left: '50%', top: '0', bottom: '0', width: `${s.track}px`, transform: 'translateX(-50%)', borderRadius: `${r}px` }
260
+ : { top: '50%', left: '0', right: '0', height: `${s.track}px`, transform: 'translateY(-50%)', borderRadius: `${r}px` }
261
+ "
262
+ >
263
+ <!-- Standard: active from left/bottom -->
264
+ <template v-if="variant === 'standard'">
265
+ <div class="absolute" :style="{
266
+ ...(isVertical
267
+ ? { left: 0, right: 0, bottom: 0, height: `calc(${pctLo}% - ${s.gap}px)` }
268
+ : { top: 0, bottom: 0, left: 0, width: `calc(${pctLo}% - ${s.gap}px)` }
269
+ ),
270
+ borderRadius: `${r}px`, backgroundColor: ct.active, transition: `all ${tr}`,
271
+ }" />
272
+ <div class="absolute" :style="{
273
+ ...(isVertical
274
+ ? { left: 0, right: 0, top: 0, height: `calc(${100 - pctLo}% - ${s.gap}px)` }
275
+ : { top: 0, bottom: 0, right: 0, width: `calc(${100 - pctLo}% - ${s.gap}px)` }
276
+ ),
277
+ borderRadius: `${r}px`, backgroundColor: ct.inactive, transition: `all ${tr}`,
278
+ }" />
279
+ </template>
280
+
281
+ <!-- Centered: active from center -->
282
+ <template v-else-if="variant === 'centered'">
283
+ <!-- Active: from center to thumb (with gap from thumb) -->
284
+ <div class="absolute" :style="{
285
+ ...(isVertical
286
+ ? pctLo >= centerPct
287
+ ? { left: 0, right: 0, bottom: `${centerPct}%`, height: `calc(${pctLo - centerPct}% - ${s.gap}px)` }
288
+ : { left: 0, right: 0, bottom: `calc(${pctLo}% + ${s.gap}px)`, height: `calc(${centerPct - pctLo}% - ${s.gap}px)` }
289
+ : pctLo >= centerPct
290
+ ? { top: 0, bottom: 0, left: `${centerPct}%`, width: `calc(${pctLo - centerPct}% - ${s.gap}px)` }
291
+ : { top: 0, bottom: 0, left: `calc(${pctLo}% + ${s.gap}px)`, width: `calc(${centerPct - pctLo}% - ${s.gap}px)` }
292
+ ),
293
+ borderRadius: `${r}px`, backgroundColor: ct.active, transition: `all ${tr}`,
294
+ }" />
295
+ <!-- Inactive: above/right of thumb -->
296
+ <div class="absolute" :style="{
297
+ ...(isVertical
298
+ ? { left: 0, right: 0, top: 0, height: `calc(${100 - Math.max(pctLo, centerPct)}% - ${s.gap}px)` }
299
+ : { top: 0, bottom: 0, right: 0, width: `calc(${100 - Math.max(pctLo, centerPct)}% - ${s.gap}px)` }
300
+ ),
301
+ borderRadius: `${r}px`, backgroundColor: ct.inactive, transition: `all ${tr}`,
302
+ }" />
303
+ <!-- Inactive: below/left of thumb -->
304
+ <div class="absolute" :style="{
305
+ ...(isVertical
306
+ ? { left: 0, right: 0, bottom: 0, height: `calc(${Math.min(pctLo, centerPct)}% - ${s.gap}px)` }
307
+ : { top: 0, bottom: 0, left: 0, width: `calc(${Math.min(pctLo, centerPct)}% - ${s.gap}px)` }
308
+ ),
309
+ borderRadius: `${r}px`, backgroundColor: ct.inactive, transition: `all ${tr}`,
310
+ }" />
311
+ </template>
312
+
313
+ <!-- Range: active between thumbs -->
314
+ <template v-else-if="variant === 'range'">
315
+ <div class="absolute" :style="{
316
+ ...(isVertical
317
+ ? { left: 0, right: 0, bottom: 0, height: `calc(${pctLo}% - ${s.gap}px)` }
318
+ : { top: 0, bottom: 0, left: 0, width: `calc(${pctLo}% - ${s.gap}px)` }
319
+ ),
320
+ borderRadius: `${r}px`, backgroundColor: ct.inactive, transition: `all ${tr}`,
321
+ }" />
322
+ <div class="absolute" :style="{
323
+ ...(isVertical
324
+ ? { left: 0, right: 0, bottom: `calc(${pctLo}% + ${s.gap}px)`, height: `calc(${pctHi - pctLo}% - ${s.gap * 2}px)` }
325
+ : { top: 0, bottom: 0, left: `calc(${pctLo}% + ${s.gap}px)`, width: `calc(${pctHi - pctLo}% - ${s.gap * 2}px)` }
326
+ ),
327
+ borderRadius: `${r}px`, backgroundColor: ct.active, transition: `all ${tr}`,
328
+ }" />
329
+ <div class="absolute" :style="{
330
+ ...(isVertical
331
+ ? { left: 0, right: 0, top: 0, height: `calc(${100 - pctHi}% - ${s.gap}px)` }
332
+ : { top: 0, bottom: 0, right: 0, width: `calc(${100 - pctHi}% - ${s.gap}px)` }
333
+ ),
334
+ borderRadius: `${r}px`, backgroundColor: ct.inactive, transition: `all ${tr}`,
335
+ }" />
336
+ </template>
337
+
338
+ <!-- Start dot / icon -->
339
+ <MIcon
340
+ v-if="icon"
341
+ :name="icon"
342
+ :size="s.iconSize"
343
+ class="absolute z-[1]"
344
+ :style="{
345
+ color: (variant === 'standard' && pctLo > 0) || (variant === 'range' && pctLo > 0) ? ct.inactive : ct.active,
346
+ ...(isVertical
347
+ ? { left: '50%', bottom: `${s.radius}px`, transform: 'translateX(-50%)' }
348
+ : { top: '50%', left: `${s.radius}px`, transform: 'translateY(-50%)' }
349
+ ),
350
+ transition: 'color 150ms ease',
351
+ }"
352
+ />
353
+ <div
354
+ v-else-if="!stops"
355
+ class="absolute rounded-full"
356
+ :style="{
357
+ width: `${s.dot}px`, height: `${s.dot}px`,
358
+ backgroundColor: (variant === 'standard' && pctLo > 0) || (variant === 'range' && pctLo > 0) ? ct.inactive : ct.active,
359
+ ...(isVertical
360
+ ? { left: '50%', bottom: `${s.radius}px`, transform: 'translateX(-50%)' }
361
+ : { top: '50%', left: `${s.radius}px`, transform: 'translateY(-50%)' }
362
+ ),
363
+ transition: 'background-color 150ms ease',
364
+ }"
365
+ />
366
+ <!-- End dot -->
367
+ <div
368
+ v-if="!stops"
369
+ class="absolute rounded-full"
370
+ :style="{
371
+ width: `${s.dot}px`, height: `${s.dot}px`,
372
+ backgroundColor: (variant === 'standard' && pctLo >= 100) || (variant === 'range' && pctHi >= 100) ? ct.active : ct.inactive,
373
+ ...(isVertical
374
+ ? { left: '50%', top: `${s.radius}px`, transform: 'translateX(-50%)' }
375
+ : { top: '50%', right: `${s.radius}px`, transform: 'translateY(-50%)' }
376
+ ),
377
+ transition: 'background-color 150ms ease',
378
+ }"
379
+ />
380
+
381
+ <!-- Stops (skip first and last to avoid duplicating end dots) -->
382
+ <template v-if="stops">
383
+ <div
384
+ v-for="(pos, i) in stopPositions"
385
+ :key="i"
386
+ class="absolute rounded-full"
387
+ :style="{
388
+ width: `${s.dot}px`, height: `${s.dot}px`,
389
+ backgroundColor: pos <= pctLo ? ct.inactive : ct.active,
390
+ ...(isVertical
391
+ ? { left: '50%', bottom: `${pos}%`, transform: 'translateX(-50%) translateY(50%)' }
392
+ : { top: '50%', left: `${pos}%`, transform: 'translateX(-50%) translateY(-50%)' }
393
+ ),
394
+ }"
395
+ />
396
+ </template>
397
+ </div>
398
+
399
+ <!-- Thumb(s) -->
400
+ <div class="pointer-events-none" :style="thumbPos(pctLo)" />
401
+ <div v-if="isRange" class="pointer-events-none" :style="thumbPos(pctHi)" />
402
+
403
+ <!-- Tooltip -->
404
+ <Transition
405
+ enter-active-class="transition-opacity duration-100"
406
+ enter-from-class="opacity-0"
407
+ leave-active-class="transition-opacity duration-75"
408
+ leave-to-class="opacity-0"
409
+ >
410
+ <div
411
+ v-if="showTooltip && dragging"
412
+ class="pointer-events-none absolute z-10 flex h-7 min-w-7 items-center justify-center rounded-full bg-inverse-surface px-2 text-label-small tabular-nums text-inverse-on-surface"
413
+ :style="tooltipPos(tooltipPct)"
414
+ >
415
+ {{ tooltipValue }}
416
+ </div>
417
+ </Transition>
179
418
  </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
419
  </div>
187
420
  </div>
188
421
  </template>
@@ -2,6 +2,7 @@
2
2
  import { computed } from "vue";
3
3
  import { useToast } from "../composables/useToast";
4
4
  import MIcon from "./MIcon.vue";
5
+ import MSpinner from "./MSpinner.vue";
5
6
  import { useLocale } from '../composables/useLocale';
6
7
 
7
8
  const props = defineProps<{
@@ -95,7 +96,9 @@ const getVariantStyle = (variant: string): VariantStyle =>
95
96
  :class="t.color ? 'text-white ring-1 ring-inset ring-white/10' : getVariantStyle(t.variant).container"
96
97
  :style="t.color ? { backgroundColor: t.color } : undefined"
97
98
  >
99
+ <MSpinner v-if="t.loading" :size="20" class="shrink-0" />
98
100
  <MIcon
101
+ v-else
99
102
  :name="t.icon ?? getVariantStyle(t.variant).iconName"
100
103
  :size="20"
101
104
  class="shrink-0"