@m3ui-vue/m3ui-vue 0.4.5 → 0.4.7

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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed } from "vue";
2
+ import { computed, ref, watch, onMounted, onUnmounted, nextTick } from "vue";
3
3
 
4
4
  const props = withDefaults(
5
5
  defineProps<{
@@ -8,47 +8,187 @@ const props = withDefaults(
8
8
  color?: "primary" | "secondary" | "tertiary" | "error";
9
9
  variant?: "linear" | "wavy";
10
10
  label?: string;
11
+ thickness?: number;
11
12
  }>(),
12
- {
13
- color: "primary",
14
- variant: "linear",
15
- },
13
+ { color: "primary", variant: "linear" },
16
14
  );
17
15
 
18
16
  const isIndeterminate = computed(() => props.indeterminate || props.value === undefined);
19
- const clampedValue = computed(() => Math.min(100, Math.max(0, props.value ?? 0)));
17
+ const clampedValue = computed(() => Math.min(100, Math.max(0, props.value ?? 0)));
20
18
 
21
19
  const colorMap: Record<
22
20
  "primary" | "secondary" | "tertiary" | "error",
23
21
  { bar: string; track: string; text: string }
24
22
  > = {
25
- primary: { bar: "bg-primary", track: "bg-primary-container", text: "text-primary" },
23
+ primary: { bar: "bg-primary", track: "bg-primary-container", text: "text-primary" },
26
24
  secondary: { bar: "bg-secondary", track: "bg-secondary-container", text: "text-secondary" },
27
- tertiary: { bar: "bg-tertiary", track: "bg-tertiary-container", text: "text-tertiary" },
28
- error: { bar: "bg-error", track: "bg-error-container", text: "text-error" },
25
+ tertiary: { bar: "bg-tertiary", track: "bg-tertiary-container", text: "text-tertiary" },
26
+ error: { bar: "bg-error", track: "bg-error-container", text: "text-error" },
29
27
  };
30
28
 
31
- // ── Wave geometry ────────────────────────────────────────────────────────
32
- // Smooth sine wave sampled as a single polyline path.
33
- // Period = 20px (one full up-down cycle). We render a wide strip so that
34
- // translating by exactly one period gives a seamless infinite scroll.
35
- const PERIOD = 20; // px per full sine cycle
36
- const AMP = 2.5; // amplitude (bar is 8px tall, mid at 4)
37
- const MID = 4;
38
- const VIEW_H = 8;
39
- const PERIODS = 80; // total cycles → 1600px strip
40
- const STEP = 1; // px sampling resolution
29
+ // ── Wave geometry ─────────────────────────────────────────────────────────
30
+ const PERIOD = 20;
31
+ const AMP = 2.5;
32
+ const PERIODS = 80;
33
+ const STEP = 2;
34
+ const MARGIN_PX = 4; // gap between wave right-end and track start
35
+ const WAVE_START = 10; // threshold % below which wave collapses to line
41
36
 
42
- const waveWidth = PERIOD * PERIODS;
37
+ // thickness-derived (computed so they react to prop changes)
38
+ const effectiveThickness = computed(() =>
39
+ props.thickness ?? (props.variant === "wavy" ? 3 : 4),
40
+ );
41
+ const waveMid = computed(() => AMP + effectiveThickness.value / 2);
42
+ const waveViewH = computed(() => 2 * AMP + effectiveThickness.value);
43
+
44
+ const waveWidth = PERIOD * PERIODS; // 1600px — covers any container width
43
45
 
44
- const wavePath = (() => {
46
+ // Static path for indeterminate — built once with the wavy default (thickness=3)
47
+ const STATIC_MID = AMP + 3 / 2; // = 4
48
+ const staticWavePath = (() => {
45
49
  let d = "";
46
50
  for (let x = 0; x <= waveWidth; x += STEP) {
47
- const y = MID - AMP * Math.sin((x / PERIOD) * Math.PI * 2);
48
- d += (x === 0 ? "M" : "L") + x + "," + y.toFixed(2) + " ";
51
+ const y = STATIC_MID - AMP * Math.sin((x / PERIOD) * Math.PI * 2);
52
+ d += `${x === 0 ? "M" : "L"}${x},${y.toFixed(2)} `;
49
53
  }
50
54
  return d.trim();
51
55
  })();
56
+
57
+ // ── rAF state (plain JS — no Vue refs to avoid 60fps reactive overhead) ───
58
+ //
59
+ // Architecture mirrors MCircleProgressBar:
60
+ // • displayedValue follows clampedValue with exponential smoothing (~300ms)
61
+ // • appearanceF animates 0↔1 at the WAVE_START threshold and at 100%
62
+ // • animPhase advances every frame to scroll the wave leftward
63
+ // clip-path and track.left are written via direct DOM setAttribute/style
64
+ // to avoid the CSS-transition vs rAF conflict.
65
+ let displayedValue = 0;
66
+ let appearanceF = 0;
67
+ let animPhase = 0;
68
+
69
+ type Anim = { from: number; to: number; t0: number; dur: number };
70
+ const appearAnim: Anim = { from: 0, to: 0, t0: -1, dur: 500 };
71
+
72
+ function easeInOut(t: number) {
73
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
74
+ }
75
+ function startAnim(a: Anim, from: number, to: number, now: number) {
76
+ if (Math.abs(from - to) < 0.001) { a.t0 = -1; a.to = to; return; }
77
+ a.from = from; a.to = to; a.t0 = now;
78
+ }
79
+ function tickAnim(a: Anim, now: number): number {
80
+ if (a.t0 < 0) return a.to;
81
+ const t = Math.min(1, (now - a.t0) / a.dur);
82
+ const v = a.from + (a.to - a.from) * easeInOut(t);
83
+ if (t >= 1) a.t0 = -1;
84
+ return v;
85
+ }
86
+
87
+ // ── Template refs for direct DOM updates ─────────────────────────────────
88
+ const waveClipEl = ref<HTMLDivElement | null>(null);
89
+ const trackEl = ref<HTMLDivElement | null>(null);
90
+ const wavePath = ref(""); // updated in tick → drives path's :d binding
91
+
92
+ // ── rAF tick ─────────────────────────────────────────────────────────────
93
+ let lastTime: number | null = null;
94
+ let rafId = 0;
95
+ const PHASE_PER_MS = (2 * Math.PI) / 800; // one full PERIOD scroll per 800ms
96
+ const VALUE_TAU = 130; // exponential τ → ~300ms 90%-settle
97
+
98
+ function buildWavePath(factor: number, ph: number): string {
99
+ const mid = waveMid.value;
100
+ const pts: string[] = [];
101
+ for (let x = 0; x <= waveWidth; x += STEP) {
102
+ const y = mid - AMP * factor * Math.sin((x / PERIOD) * Math.PI * 2 + ph);
103
+ pts.push(`${x === 0 ? "M" : "L"}${x},${y.toFixed(2)}`);
104
+ }
105
+ return pts.join(" ");
106
+ }
107
+
108
+ function tick(now: number) {
109
+ const dt = lastTime !== null ? now - lastTime : 0;
110
+ lastTime = now;
111
+
112
+ // 1. Wave phase scrolls leftward
113
+ animPhase -= dt * PHASE_PER_MS;
114
+
115
+ // 2. Smooth displayed value (exponential interpolation)
116
+ if (dt > 0) {
117
+ const k = 1 - Math.exp(-dt / VALUE_TAU);
118
+ displayedValue += (clampedValue.value - displayedValue) * k;
119
+ }
120
+
121
+ // 3. Advance appearance animation
122
+ appearanceF = tickAnim(appearAnim, now);
123
+
124
+ // 4. Update DOM directly
125
+ const margin = MARGIN_PX * appearanceF;
126
+ const dv = displayedValue;
127
+
128
+ if (waveClipEl.value)
129
+ waveClipEl.value.style.clipPath = `inset(0 ${(100 - dv).toFixed(3)}% 0 0)`;
130
+
131
+ if (trackEl.value) {
132
+ trackEl.value.style.left = `calc(${dv.toFixed(3)}% + ${margin.toFixed(2)}px)`;
133
+ trackEl.value.style.height = `${effectiveThickness.value}px`;
134
+ }
135
+
136
+ // 5. Rebuild wave path (drives :d via Vue ref)
137
+ wavePath.value = buildWavePath(appearanceF, animPhase);
138
+
139
+ rafId = requestAnimationFrame(tick);
140
+ }
141
+
142
+ // ── Watchers ─────────────────────────────────────────────────────────────
143
+ watch(clampedValue, (v, prev) => {
144
+ if (props.variant !== "wavy" || isIndeterminate.value) return;
145
+ const now = performance.now();
146
+
147
+ if (prev <= WAVE_START && v > WAVE_START && v < 100) startAnim(appearAnim, appearanceF, 1, now);
148
+ else if (prev > WAVE_START && v <= WAVE_START) startAnim(appearAnim, appearanceF, 0, now);
149
+ else if (v === 100) startAnim(appearAnim, appearanceF, 0, now);
150
+ else if (prev === 100 && v > WAVE_START) startAnim(appearAnim, appearanceF, 1, now);
151
+ });
152
+
153
+ function initAndStart() {
154
+ displayedValue = clampedValue.value;
155
+ const v = clampedValue.value;
156
+ appearanceF = (v > WAVE_START && v < 100) ? 1 : 0;
157
+ appearAnim.to = appearanceF;
158
+ appearAnim.t0 = -1;
159
+
160
+ // Set initial DOM values immediately to avoid a one-frame flash
161
+ const margin = MARGIN_PX * appearanceF;
162
+ if (waveClipEl.value)
163
+ waveClipEl.value.style.clipPath = `inset(0 ${(100 - v).toFixed(3)}% 0 0)`;
164
+ if (trackEl.value) {
165
+ trackEl.value.style.left = `calc(${v.toFixed(3)}% + ${margin.toFixed(2)}px)`;
166
+ trackEl.value.style.height = `${effectiveThickness.value}px`;
167
+ }
168
+ wavePath.value = buildWavePath(appearanceF, animPhase);
169
+
170
+ if (!rafId) rafId = requestAnimationFrame(tick);
171
+ }
172
+
173
+ function stopRaf() {
174
+ cancelAnimationFrame(rafId);
175
+ rafId = 0; lastTime = null;
176
+ appearAnim.t0 = -1;
177
+ }
178
+
179
+ onMounted(() => {
180
+ if (props.variant === "wavy" && !isIndeterminate.value) initAndStart();
181
+ });
182
+ onUnmounted(stopRaf);
183
+
184
+ watch(() => props.variant, async (v) => {
185
+ if (v === "wavy" && !isIndeterminate.value) { await nextTick(); initAndStart(); }
186
+ else stopRaf();
187
+ });
188
+ watch(isIndeterminate, async (v) => {
189
+ if (!v && props.variant === "wavy") { await nextTick(); initAndStart(); }
190
+ else stopRaf();
191
+ });
52
192
  </script>
53
193
 
54
194
  <template>
@@ -58,8 +198,9 @@ const wavePath = (() => {
58
198
  <!-- ── Linear variant ────────────────────────────────────────────────── -->
59
199
  <div
60
200
  v-if="variant === 'linear'"
61
- class="relative h-1 w-full overflow-hidden rounded-full"
201
+ class="relative w-full overflow-hidden rounded-full"
62
202
  :class="colorMap[color].track"
203
+ :style="{ height: `${effectiveThickness}px` }"
63
204
  role="progressbar"
64
205
  :aria-valuenow="isIndeterminate ? undefined : clampedValue"
65
206
  aria-valuemin="0"
@@ -81,54 +222,47 @@ const wavePath = (() => {
81
222
  <!-- ── Wavy variant ───────────────────────────────────────────────────── -->
82
223
  <div
83
224
  v-else
84
- class="relative h-2 w-full overflow-visible"
225
+ class="relative w-full overflow-visible"
226
+ :style="{ height: `${waveViewH}px` }"
85
227
  role="progressbar"
86
228
  :aria-valuenow="isIndeterminate ? undefined : clampedValue"
87
229
  aria-valuemin="0"
88
230
  aria-valuemax="100"
89
231
  >
90
- <!-- DETERMINATE -->
232
+ <!-- DETERMINATE — rAF drives clip-path, track.left, and wave path -->
91
233
  <template v-if="!isIndeterminate">
92
- <!-- Active (wavy) portion: clipped to value%, but the wave keeps flowing -->
234
+ <!-- Active wave, clipped to displayedValue% -->
93
235
  <div
236
+ ref="waveClipEl"
94
237
  class="absolute inset-0 overflow-hidden"
95
- :style="{
96
- clipPath: `inset(0 ${100 - clampedValue}% 0 0)`,
97
- transition: 'clip-path 300ms ease',
98
- }"
238
+ style="clip-path: inset(0 100% 0 0)"
99
239
  >
100
- <div
101
- class="absolute top-0 left-0 h-full animate-[m3-wave-flow_0.8s_linear_infinite]"
240
+ <svg
241
+ class="absolute top-0 left-0 h-full"
102
242
  :class="colorMap[color].text"
103
- :style="{ width: `${waveWidth}px` }"
243
+ :width="waveWidth"
244
+ :height="waveViewH"
245
+ :viewBox="`0 0 ${waveWidth} ${waveViewH}`"
104
246
  >
105
- <svg
106
- :width="waveWidth"
107
- :height="VIEW_H"
108
- :viewBox="`0 0 ${waveWidth} ${VIEW_H}`"
109
- class="h-full"
110
- xmlns="http://www.w3.org/2000/svg"
111
- >
112
- <path
113
- :d="wavePath"
114
- fill="none"
115
- stroke="currentColor"
116
- stroke-width="3"
117
- stroke-linecap="round"
118
- />
119
- </svg>
120
- </div>
247
+ <path
248
+ :d="wavePath"
249
+ fill="none"
250
+ stroke="currentColor"
251
+ :stroke-width="effectiveThickness"
252
+ stroke-linecap="round"
253
+ />
254
+ </svg>
121
255
  </div>
122
256
 
123
- <!-- Inactive (straight track) portion -->
257
+ <!-- Inactive track rAF owns left and height; no Vue-managed left -->
124
258
  <div
125
- class="absolute inset-y-0 right-0 flex items-center"
259
+ ref="trackEl"
260
+ class="absolute right-0"
126
261
  :class="colorMap[color].track"
127
- :style="{ left: `calc(${clampedValue}% + 4px)`, transition: 'left 300ms ease' }"
128
- style="border-radius: 9999px; height: 4px; top: 50%; transform: translateY(-50%)"
262
+ style="border-radius: 9999px; top: 50%; transform: translateY(-50%);"
129
263
  />
130
264
 
131
- <!-- Stop indicator (dot at the end of the track) -->
265
+ <!-- Stop dot at far right -->
132
266
  <div
133
267
  class="absolute rounded-full"
134
268
  :class="colorMap[color].bar"
@@ -136,13 +270,13 @@ const wavePath = (() => {
136
270
  right: '0',
137
271
  top: '50%',
138
272
  transform: 'translateY(-50%)',
139
- width: '4px',
140
- height: '4px',
273
+ width: `${effectiveThickness}px`,
274
+ height: `${effectiveThickness}px`,
141
275
  }"
142
276
  />
143
277
  </template>
144
278
 
145
- <!-- INDETERMINATE -->
279
+ <!-- INDETERMINATE — keeps original CSS-animated wide strip -->
146
280
  <div v-else class="absolute inset-0 overflow-hidden rounded-full">
147
281
  <div
148
282
  class="absolute top-0 left-0 h-full animate-[m3-wave-flow_0.9s_linear_infinite]"
@@ -151,16 +285,16 @@ const wavePath = (() => {
151
285
  >
152
286
  <svg
153
287
  :width="waveWidth"
154
- :height="VIEW_H"
155
- :viewBox="`0 0 ${waveWidth} ${VIEW_H}`"
288
+ :height="waveViewH"
289
+ :viewBox="`0 0 ${waveWidth} ${waveViewH}`"
156
290
  class="h-full"
157
291
  xmlns="http://www.w3.org/2000/svg"
158
292
  >
159
293
  <path
160
- :d="wavePath"
294
+ :d="staticWavePath"
161
295
  fill="none"
162
296
  stroke="currentColor"
163
- stroke-width="3"
297
+ :stroke-width="effectiveThickness"
164
298
  stroke-linecap="round"
165
299
  />
166
300
  </svg>
@@ -171,23 +305,14 @@ const wavePath = (() => {
171
305
  </template>
172
306
 
173
307
  <style>
174
- /* Scroll exactly one period (20px) so the loop is perfectly seamless. */
175
308
  @keyframes m3-wave-flow {
176
- from {
177
- transform: translateX(0);
178
- }
179
- to {
180
- transform: translateX(-20px);
181
- }
309
+ from { transform: translateX(0); }
310
+ to { transform: translateX(-20px); }
182
311
  }
183
312
 
184
313
  @keyframes m3-progress-indeterminate {
185
- 0% {
186
- left: -40%;
187
- }
188
- 100% {
189
- left: 100%;
190
- }
314
+ 0% { left: -40%; }
315
+ 100% { left: 100%; }
191
316
  }
192
317
 
193
318
  @media (prefers-reduced-motion: reduce) {
@@ -90,7 +90,18 @@ const getVariantStyle = (variant: string): VariantStyle =>
90
90
  <template>
91
91
  <div :class="containerClass">
92
92
  <TransitionGroup :name="isTop ? 'm3-toast-top' : 'm3-toast-bot'">
93
- <div v-for="t in toasts" :key="t.id" class="toast-row w-full min-w-64 max-w-xs">
93
+ <div v-for="t in toasts" :key="t.id" class="toast-row relative w-full min-w-64 max-w-xs">
94
+ <Transition name="m3-badge">
95
+ <span
96
+ v-if="t.count >= 2"
97
+ class="absolute -top-1.5 -right-1.5 z-10 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-on-primary shadow-elevation-1"
98
+ >
99
+ <Transition name="m3-count" mode="out-in">
100
+ <span :key="t.count">{{ t.count }}</span>
101
+ </Transition>
102
+ </span>
103
+ </Transition>
104
+
94
105
  <div
95
106
  class="toast-inner pointer-events-auto relative flex items-center gap-3 overflow-hidden rounded-2xl px-4 py-4 shadow-elevation-2"
96
107
  :class="t.color ? 'text-white ring-1 ring-inset ring-white/10' : getVariantStyle(t.variant).container"
@@ -98,7 +109,7 @@ const getVariantStyle = (variant: string): VariantStyle =>
98
109
  >
99
110
  <MSpinner v-if="t.loading" :size="20" class="shrink-0" />
100
111
  <MIcon
101
- v-else
112
+ v-else-if="t.icon !== null"
102
113
  :name="t.icon ?? getVariantStyle(t.variant).iconName"
103
114
  :size="20"
104
115
  class="shrink-0"
@@ -244,6 +255,17 @@ const getVariantStyle = (variant: string): VariantStyle =>
244
255
  transform: scale(0.92);
245
256
  }
246
257
 
258
+ /* badge appear/disappear */
259
+ .m3-badge-enter-active { transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); }
260
+ .m3-badge-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
261
+ .m3-badge-enter-from, .m3-badge-leave-to { opacity: 0; transform: scale(0.5); }
262
+
263
+ /* count number bump when incrementing */
264
+ .m3-count-enter-active { transition: opacity 0.12s ease, transform 0.12s ease; }
265
+ .m3-count-leave-active { transition: opacity 0.08s ease, transform 0.08s ease; }
266
+ .m3-count-enter-from { opacity: 0; transform: scale(1.5) translateY(-3px); }
267
+ .m3-count-leave-to { opacity: 0; transform: scale(0.6) translateY(2px); }
268
+
247
269
  @keyframes m3-toast-progress {
248
270
  from {
249
271
  transform: scaleX(1);
@@ -14,9 +14,10 @@ export interface Notification {
14
14
  variant: NotificationVariant
15
15
  duration: number
16
16
  loading?: boolean
17
- icon?: string
17
+ icon?: string | null // null = suppress icon entirely
18
18
  closable?: boolean
19
19
  action?: NotificationAction
20
+ count: number // ≥ 2 → show badge
20
21
  }
21
22
 
22
23
  let nextId = 1
@@ -44,7 +45,7 @@ function dismiss(id: number) {
44
45
  export interface NotificationOptions {
45
46
  duration?: number
46
47
  loading?: boolean
47
- icon?: string
48
+ icon?: string | null
48
49
  closable?: boolean
49
50
  action?: NotificationAction
50
51
  }
@@ -55,11 +56,27 @@ function show(
55
56
  options: NotificationOptions = {},
56
57
  ) {
57
58
  ensureMounted()
58
- const id = nextId++
59
59
  const isLoading = options.loading ?? false
60
+
61
+ // Dedup: identical non-loading, non-action notifications increment count
62
+ if (!isLoading && !options.action) {
63
+ const existing = notifications.value.find(
64
+ n => n.message === message && n.variant === variant && !n.loading && !n.action,
65
+ )
66
+ if (existing) {
67
+ existing.count++
68
+ const old = timers.get(existing.id)
69
+ if (old) clearTimeout(old)
70
+ if (existing.duration > 0)
71
+ timers.set(existing.id, setTimeout(() => dismiss(existing.id), existing.duration))
72
+ return existing.id
73
+ }
74
+ }
75
+
76
+ const id = nextId++
60
77
  const duration = isLoading ? 0 : (options.duration ?? 3000)
61
78
  const closable = options.closable ?? true
62
- notifications.value.push({ id, message, variant, duration, loading: isLoading, icon: options.icon, closable, action: options.action })
79
+ notifications.value.push({ id, message, variant, duration, loading: isLoading, icon: options.icon, closable, action: options.action, count: 1 })
63
80
  if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration))
64
81
  return id
65
82
  }
@@ -15,8 +15,9 @@ export interface Toast {
15
15
  duration: number
16
16
  loading?: boolean
17
17
  action?: ToastAction
18
- icon?: string
18
+ icon?: string | null // null = suppress icon entirely
19
19
  color?: string
20
+ count: number // ≥ 2 → show badge
20
21
  }
21
22
 
22
23
  let nextId = 1
@@ -45,7 +46,7 @@ export interface ToastOptions {
45
46
  duration?: number
46
47
  loading?: boolean
47
48
  action?: ToastAction
48
- icon?: string
49
+ icon?: string | null
49
50
  color?: string
50
51
  }
51
52
 
@@ -55,11 +56,27 @@ function show(
55
56
  options: number | ToastOptions = {},
56
57
  ) {
57
58
  ensureMounted()
58
- const id = nextId++
59
59
  const opts = typeof options === 'number' ? { duration: options } : options
60
60
  const isLoading = opts.loading ?? false
61
+
62
+ // Dedup: identical non-loading, non-action toasts increment count instead of stacking
63
+ if (!isLoading && !opts.action) {
64
+ const existing = toasts.value.find(
65
+ t => t.message === message && t.variant === variant && !t.loading && !t.action,
66
+ )
67
+ if (existing) {
68
+ existing.count++
69
+ const old = timers.get(existing.id)
70
+ if (old) clearTimeout(old)
71
+ if (existing.duration > 0)
72
+ timers.set(existing.id, setTimeout(() => dismiss(existing.id), existing.duration))
73
+ return existing.id
74
+ }
75
+ }
76
+
77
+ const id = nextId++
61
78
  const duration = isLoading ? 0 : (opts.duration ?? (variant === 'error' ? 6000 : 4000))
62
- toasts.value.push({ id, message, variant, duration, loading: isLoading, action: opts.action, icon: opts.icon, color: opts.color })
79
+ toasts.value.push({ id, message, variant, duration, loading: isLoading, action: opts.action, icon: opts.icon, color: opts.color, count: 1 })
63
80
  if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration))
64
81
  return id
65
82
  }
package/src/index.ts CHANGED
@@ -94,6 +94,7 @@ export { default as MNavigationRail } from './components/MNavigationRail.vue'
94
94
  export { default as MOverlay } from './components/MOverlay.vue'
95
95
  export { default as MPagination } from './components/MPagination.vue'
96
96
  export { default as MProgressBar } from './components/MProgressBar.vue'
97
+ export { default as MCircleProgressBar } from './components/MCircleProgressBar.vue'
97
98
  export { default as MRadio } from './components/MRadio.vue'
98
99
  export { default as MRadioGroup } from './components/MRadioGroup.vue'
99
100
  export { default as MRating } from './components/MRating.vue'