@mozaic-ds/vue 2.9.0 → 2.11.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 (44) hide show
  1. package/README.md +4 -6
  2. package/dist/mozaic-vue.css +1 -1
  3. package/dist/mozaic-vue.d.ts +424 -249
  4. package/dist/mozaic-vue.js +1545 -1063
  5. package/dist/mozaic-vue.js.map +1 -1
  6. package/dist/mozaic-vue.umd.cjs +5 -5
  7. package/dist/mozaic-vue.umd.cjs.map +1 -1
  8. package/package.json +14 -13
  9. package/src/components/carousel/MCarousel.spec.ts +138 -0
  10. package/src/components/carousel/MCarousel.stories.ts +94 -0
  11. package/src/components/carousel/MCarousel.vue +154 -0
  12. package/src/components/carousel/README.md +18 -0
  13. package/src/components/flag/MFlag.stories.ts +1 -1
  14. package/src/components/kpiitem/MKpiItem.spec.ts +71 -0
  15. package/src/components/kpiitem/MKpiItem.stories.ts +69 -0
  16. package/src/components/kpiitem/MKpiItem.vue +89 -0
  17. package/src/components/kpiitem/README.md +15 -0
  18. package/src/components/loader/MLoader.spec.ts +2 -2
  19. package/src/components/loader/MLoader.vue +2 -2
  20. package/src/components/phonenumber/MPhoneNumber.spec.ts +110 -1
  21. package/src/components/phonenumber/MPhoneNumber.stories.ts +14 -0
  22. package/src/components/phonenumber/MPhoneNumber.vue +16 -6
  23. package/src/components/phonenumber/README.md +2 -0
  24. package/src/components/starrating/MStarRating.spec.ts +203 -0
  25. package/src/components/starrating/MStarRating.stories.ts +82 -0
  26. package/src/components/starrating/MStarRating.vue +187 -0
  27. package/src/components/starrating/README.md +31 -0
  28. package/src/components/statusbadge/README.md +1 -1
  29. package/src/components/statusdot/README.md +1 -1
  30. package/src/components/statusmessage/MStatusMessage.spec.ts +76 -0
  31. package/src/components/statusmessage/MStatusMessage.stories.ts +52 -0
  32. package/src/components/statusmessage/MStatusMessage.vue +70 -0
  33. package/src/components/statusmessage/README.md +11 -0
  34. package/src/components/statusnotification/README.md +1 -1
  35. package/src/components/steppercompact/MStepperCompact.spec.ts +98 -0
  36. package/src/components/steppercompact/MStepperCompact.stories.ts +43 -0
  37. package/src/components/steppercompact/MStepperCompact.vue +105 -0
  38. package/src/components/steppercompact/README.md +13 -0
  39. package/src/components/tag/MTag.vue +2 -1
  40. package/src/components/tag/README.md +1 -1
  41. package/src/components/textinput/MTextInput.stories.ts +1 -1
  42. package/src/components/toaster/README.md +1 -1
  43. package/src/components/usingPresets.mdx +1 -1
  44. package/src/main.ts +4 -0
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <div class="mc-kpi" :class="rootClasses">
3
+ <span v-if="isMedium && label" class="mc-kpi__label">
4
+ {{ label }}
5
+ </span>
6
+ <div class="mc-kpi__content">
7
+ <div class="mc-kpi__main">
8
+ <span v-if="isLarge && label" class="mc-kpi__label">
9
+ {{ label }}
10
+ </span>
11
+ <span class="mc-kpi__value">{{ value }}</span>
12
+ </div>
13
+ <div v-if="trend || information" class="mc-kpi__aside">
14
+ <span v-if="isLarge && information" class="mc-kpi__detail">
15
+ {{ information }}
16
+ </span>
17
+
18
+ <component v-if="trend" :is="getIconComponent" class="mc-kpi__icon" />
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { computed, type Component } from 'vue';
26
+ import ArrowBottomRight24 from '@mozaic-ds/icons-vue/src/components/ArrowBottomRight24/ArrowBottomRight24.vue';
27
+ import ArrowTopRight24 from '@mozaic-ds/icons-vue/src/components/ArrowTopRight24/ArrowTopRight24.vue';
28
+ import Less24 from '@mozaic-ds/icons-vue/src/components/Less24/Less24.vue';
29
+ /**
30
+ * A KPI Item is used to display Key Performance Indicators (KPIs) within an interface, providing a quick and clear visualization of essential data. It often includes contextual elements such as labels, trends, or status indicators to help users interpret the information at a glance. KPI Items are commonly used in dashboards, reports, and analytics tools to highlight critical metrics and facilitate data-driven decision-making.
31
+ */
32
+ const props = withDefaults(
33
+ defineProps<{
34
+ /**
35
+ * The current value of the kpi item.
36
+ */
37
+ value: string;
38
+ /**
39
+ * Defines the evolution of the kpi.
40
+ */
41
+ trend?: 'increasing' | 'decreasing' | 'stable';
42
+ /**
43
+ * Label of the kpi item.
44
+ */
45
+ label?: string;
46
+ /**
47
+ * Allows to define the kpi item status.
48
+ */
49
+ status?: 'info' | 'warning' | 'error' | 'success' | 'neutral';
50
+ /**
51
+ * The evolution information defining the kpi.
52
+ */
53
+ information?: string;
54
+ /**
55
+ * Allows to define the kpi item size.
56
+ */
57
+ size?: 's' | 'm' | 'l';
58
+ }>(),
59
+ {
60
+ size: 's',
61
+ status: 'info',
62
+ },
63
+ );
64
+
65
+ const isMedium = computed(() => props.size === 'm');
66
+ const isLarge = computed(() => props.size === 'l');
67
+
68
+ const iconMap: Record<
69
+ NonNullable<Exclude<typeof props.trend, undefined>>,
70
+ Component
71
+ > = {
72
+ increasing: ArrowTopRight24,
73
+ decreasing: ArrowBottomRight24,
74
+ stable: Less24,
75
+ };
76
+
77
+ const getIconComponent = computed<Component | undefined>(() =>
78
+ props.trend ? iconMap[props.trend] : undefined,
79
+ );
80
+
81
+ const rootClasses = computed(() => [
82
+ `mc-kpi--${props.size}`,
83
+ `mc-kpi--${props.status}`,
84
+ ]);
85
+ </script>
86
+
87
+ <style lang="scss" scoped>
88
+ @use '@mozaic-ds/styles/components/kpi-item';
89
+ </style>
@@ -0,0 +1,15 @@
1
+ # MKpiItem
2
+
3
+ A KPI Item is used to display Key Performance Indicators (KPIs) within an interface, providing a quick and clear visualization of essential data. It often includes contextual elements such as labels, trends, or status indicators to help users interpret the information at a glance. KPI Items are commonly used in dashboards, reports, and analytics tools to highlight critical metrics and facilitate data-driven decision-making.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `value*` | The current value of the kpi item. | `string` | - |
11
+ | `trend` | Defines the evolution of the kpi. | `"increasing"` `"decreasing"` `"stable"` | - |
12
+ | `label` | Label of the kpi item. | `string` | - |
13
+ | `status` | Allows to define the kpi item status. | `"info"` `"warning"` `"error"` `"success"` `"neutral"` | `"info"` |
14
+ | `information` | The evolution information defining the kpi. | `string` | - |
15
+ | `size` | Allows to define the kpi item size. | `"s"` `"m"` `"l"` | `"s"` |
@@ -134,12 +134,12 @@ describe('MLoader component', () => {
134
134
  it('sets correct viewBox for xs size', async () => {
135
135
  const wrapper = mount(MLoader, { props: { size: 'xs' } });
136
136
  const svg = wrapper.find('svg.mc-loader__icon');
137
- expect(svg.attributes('viewBox')).toBe('0 0 24 24');
137
+ expect(svg.attributes('viewBox')).toBe('0 0 20 20');
138
138
  });
139
139
 
140
140
  it('sets correct circle radius for xs size', async () => {
141
141
  const wrapper = mount(MLoader, { props: { size: 'xs' } });
142
142
  const circle = wrapper.find('circle.mc-loader__path');
143
- expect(circle.attributes('r')).toBe('3');
143
+ expect(circle.attributes('r')).toBe('6');
144
144
  });
145
145
  });
@@ -61,7 +61,7 @@ const setViewBox = computed(() => {
61
61
 
62
62
  switch (props.size) {
63
63
  case 'xs':
64
- viewBox = '0 0 24 24';
64
+ viewBox = '0 0 20 20';
65
65
  break;
66
66
  case 's':
67
67
  viewBox = '0 0 24 24';
@@ -80,7 +80,7 @@ const setCircleRadius = computed(() => {
80
80
 
81
81
  switch (props.size) {
82
82
  case 'xs':
83
- circleRadius = 3;
83
+ circleRadius = 6;
84
84
  break;
85
85
  case 's':
86
86
  circleRadius = 6;
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
2
  import { mount, VueWrapper } from '@vue/test-utils';
3
3
  import { nextTick } from 'vue';
4
4
  import MPhoneNumber from './MPhoneNumber.vue';
5
- import { isValidPhoneNumber } from 'libphonenumber-js';
5
+ import { isValidPhoneNumber, type CountryCode } from 'libphonenumber-js';
6
6
 
7
7
  vi.mock('libphonenumber-js', () => ({
8
8
  default: vi.fn(),
@@ -291,4 +291,113 @@ describe('MPhoneNumber', () => {
291
291
  expect((input.element as HTMLInputElement).value).toBe('+33123456789');
292
292
  });
293
293
  });
294
+
295
+ describe('Locale', () => {
296
+ it('should use French locale by default', () => {
297
+ const options = wrapper.findAll('option');
298
+ const franceOption = options.find(
299
+ (opt) => opt.attributes('value') === 'FR',
300
+ );
301
+ expect(franceOption?.text()).toContain('France');
302
+ });
303
+
304
+ it('should display country names in specified locale', () => {
305
+ wrapper = mount(MPhoneNumber, {
306
+ props: { ...defaultProps, locale: 'en' },
307
+ });
308
+ const options = wrapper.findAll('option');
309
+ const franceOption = options.find(
310
+ (opt) => opt.attributes('value') === 'FR',
311
+ );
312
+ // Country names should be translated according to the locale
313
+ expect(franceOption?.text()).toBeTruthy();
314
+ });
315
+
316
+ it('should update country names when locale prop changes', async () => {
317
+ await wrapper.setProps({ locale: 'es' });
318
+ await nextTick();
319
+ const options = wrapper.findAll('option');
320
+ expect(options.length).toBeGreaterThan(1);
321
+ });
322
+ });
323
+
324
+ describe('Country Codes', () => {
325
+ it('should display all countries by default', () => {
326
+ const options = wrapper.findAll('option');
327
+ // Minus 1 for the empty hidden option
328
+ expect(options.length - 1).toBe(5); // Mocked to return 5 countries
329
+ });
330
+
331
+ it('should display only specified countries when countryCodes prop is provided', () => {
332
+ wrapper = mount(MPhoneNumber, {
333
+ props: {
334
+ ...defaultProps,
335
+ countryCodes: ['FR', 'US', 'GB'] as CountryCode[],
336
+ },
337
+ });
338
+ const options = wrapper.findAll('option');
339
+ // Plus 1 for the empty hidden option
340
+ expect(options.length).toBe(4);
341
+ expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe(
342
+ true,
343
+ );
344
+ expect(options.some((opt) => opt.attributes('value') === 'US')).toBe(
345
+ true,
346
+ );
347
+ expect(options.some((opt) => opt.attributes('value') === 'GB')).toBe(
348
+ true,
349
+ );
350
+ });
351
+
352
+ it('should update available countries when countryCodes prop changes', async () => {
353
+ await wrapper.setProps({ countryCodes: ['FR', 'DE'] as CountryCode[] });
354
+ await nextTick();
355
+ const options = wrapper.findAll('option');
356
+ // Plus 1 for the empty hidden option
357
+ expect(options.length).toBe(3);
358
+ expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe(
359
+ true,
360
+ );
361
+ expect(options.some((opt) => opt.attributes('value') === 'DE')).toBe(
362
+ true,
363
+ );
364
+ });
365
+
366
+ it('should work with single country in countryCodes', () => {
367
+ wrapper = mount(MPhoneNumber, {
368
+ props: {
369
+ ...defaultProps,
370
+ countryCodes: ['FR'] as CountryCode[],
371
+ },
372
+ });
373
+ const options = wrapper.findAll('option');
374
+ // Plus 1 for the empty hidden option
375
+ expect(options.length).toBe(2);
376
+ expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe(
377
+ true,
378
+ );
379
+ });
380
+
381
+ it('should combine locale and countryCodes props', () => {
382
+ wrapper = mount(MPhoneNumber, {
383
+ props: {
384
+ ...defaultProps,
385
+ locale: 'en',
386
+ countryCodes: ['US', 'GB', 'CA'] as CountryCode[],
387
+ },
388
+ });
389
+ const options = wrapper.findAll('option');
390
+ // Plus 1 for the empty hidden option
391
+ expect(options.length).toBe(4);
392
+ expect(options.some((opt) => opt.attributes('value') === 'US')).toBe(
393
+ true,
394
+ );
395
+ expect(options.some((opt) => opt.attributes('value') === 'GB')).toBe(
396
+ true,
397
+ );
398
+ expect(options.some((opt) => opt.attributes('value') === 'CA')).toBe(
399
+ true,
400
+ );
401
+ });
402
+ });
294
403
  });
@@ -86,3 +86,17 @@ export const ReadOnly: Story = {
86
86
  readonly: true,
87
87
  },
88
88
  };
89
+
90
+ export const LimitedCountries: Story = {
91
+ args: {
92
+ countryCodes: ['FR', 'US', 'GB', 'DE', 'ES', 'IT'],
93
+ },
94
+ parameters: {
95
+ docs: {
96
+ description: {
97
+ story:
98
+ 'Limit the country selector to only specific countries instead of showing all available countries.',
99
+ },
100
+ },
101
+ },
102
+ };
@@ -116,6 +116,14 @@ const props = withDefaults(
116
116
  * If `true`, display the country flag selector
117
117
  */
118
118
  flag?: boolean;
119
+ /**
120
+ * Locale for displaying country names (e.g., 'fr', 'en', 'es', 'pt').
121
+ */
122
+ locale?: string;
123
+ /**
124
+ * List of country codes to display in the selector. If not provided, all countries will be shown.
125
+ */
126
+ countryCodes?: CountryCode[];
119
127
  }>(),
120
128
  {
121
129
  modelValue: '',
@@ -123,12 +131,15 @@ const props = withDefaults(
123
131
  size: 'm',
124
132
  prefix: true,
125
133
  flag: true,
134
+ locale: 'en',
126
135
  },
127
136
  );
128
137
 
129
138
  const phoneNumber = ref(props.modelValue);
130
139
  const selectedCountry = ref<CountryCode>(props.defaultCountry);
131
- const countries = getCountries();
140
+ const countries = computed(() => {
141
+ return props.countryCodes || getCountries();
142
+ });
132
143
 
133
144
  const dynamicPlaceholder = computed(() => {
134
145
  if (props.placeholder && props.placeholder.length > 0) {
@@ -155,12 +166,11 @@ const parsedNumber = computed(() => {
155
166
  }
156
167
  });
157
168
 
158
- const getCountryName = (
159
- countryCode: CountryCode,
160
- locale: string = 'fr',
161
- ): string => {
169
+ const getCountryName = (countryCode: CountryCode): string => {
162
170
  try {
163
- const regionNames = new Intl.DisplayNames([locale], { type: 'region' });
171
+ const regionNames = new Intl.DisplayNames([props.locale], {
172
+ type: 'region',
173
+ });
164
174
  return regionNames.of(countryCode) || countryCode;
165
175
  } catch {
166
176
  return countryCode;
@@ -17,6 +17,8 @@ A phone number input is a specialized input field designed to capture and valida
17
17
  | `readonly` | If `true`, the input is read-only (cannot be edited). | `boolean` | - |
18
18
  | `prefix` | If `true`, display the country calling code prefix (+33, +1, etc.). | `boolean` | `true` |
19
19
  | `flag` | If `true`, display the country flag selector | `boolean` | `true` |
20
+ | `locale` | Locale for displaying country names (e.g., 'fr', 'en', 'es', 'pt'). | `string` | `"en"` |
21
+ | `countryCodes` | List of country codes to display in the selector. If not provided, all countries will be shown. | `CountryCode[]` | - |
20
22
 
21
23
  ## Events
22
24
 
@@ -0,0 +1,203 @@
1
+ import { shallowMount, mount } from '@vue/test-utils';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { nextTick } from 'vue';
4
+ import MStarRating from './MStarRating.vue';
5
+ import StarFilled24 from '@mozaic-ds/icons-vue/src/components/StarFilled24/StarFilled24.vue';
6
+ import StarHalf24 from '@mozaic-ds/icons-vue/src/components/StarHalf24/StarHalf24.vue';
7
+
8
+ function mockRect(el: Element, { left = 0, width = 100 } = {}) {
9
+ Object.defineProperty(el, 'getBoundingClientRect', {
10
+ value: () => ({
11
+ left,
12
+ width,
13
+ top: 0,
14
+ bottom: 0,
15
+ right: left + width,
16
+ height: 0,
17
+ }),
18
+ configurable: true,
19
+ });
20
+ }
21
+
22
+ describe('MStarRating', () => {
23
+ it('renders 5 stars by default', () => {
24
+ const wrapper = shallowMount(MStarRating, { props: { modelValue: 0 } });
25
+ const stars = wrapper.findAll('.mc-star-rating__icon');
26
+ expect(stars.length).toBe(5);
27
+ });
28
+
29
+ it('renders 1 star when compact mode is enabled', () => {
30
+ const wrapper = shallowMount(MStarRating, {
31
+ props: { modelValue: 0, compact: true },
32
+ });
33
+ const stars = wrapper.findAll('.mc-star-rating__icon');
34
+ expect(stars.length).toBe(1);
35
+ });
36
+
37
+ it('does not render half star on input mode', async () => {
38
+ const wrapper = shallowMount(MStarRating, {
39
+ props: { modelValue: 0, readonly: false },
40
+ });
41
+ const stars = wrapper.findAll('.mc-star-rating__icon');
42
+ const first = stars[0];
43
+ mockRect(first.element, { left: 0, width: 100 });
44
+ await first.trigger('mousemove', { clientX: 10 });
45
+ expect(wrapper.findComponent(StarHalf24).exists()).toBe(false);
46
+ });
47
+
48
+ it('render half star on readonly mode if possible', () => {
49
+ const wrapper = shallowMount(MStarRating, {
50
+ props: { modelValue: 1.5, readonly: true },
51
+ });
52
+ const filledStars = wrapper.findAllComponents(StarFilled24);
53
+ const halfStars = wrapper.findAllComponents(StarHalf24);
54
+ expect(filledStars.length).toBe(1);
55
+ expect(halfStars.length).toBe(1);
56
+ });
57
+
58
+ it('emits update:modelValue with the correct value when clicking a star', async () => {
59
+ const wrapper = shallowMount(MStarRating, {
60
+ props: { modelValue: 2, readonly: false },
61
+ });
62
+ const stars = wrapper.findAll('.mc-star-rating__icon');
63
+ const first = stars[0];
64
+ mockRect(first.element, { left: 0, width: 100 });
65
+ await first.trigger('click', { clientX: 10 });
66
+ const emitted = wrapper.emitted('update:modelValue') || [];
67
+ expect(emitted.length).toBe(1);
68
+ expect(emitted[0][0]).toBe(1);
69
+ });
70
+
71
+ it('emits the correct values when pressing ArrowRight', async () => {
72
+ const wrapper = shallowMount(MStarRating, {
73
+ props: { modelValue: 2, readonly: false },
74
+ });
75
+
76
+ const ratingInput = wrapper.find('.mc-star-rating__wrapper');
77
+
78
+ await ratingInput.trigger('keydown', { key: 'ArrowRight' });
79
+ const emitted = wrapper.emitted('update:modelValue') || [];
80
+ expect(emitted.length).toBe(1);
81
+ expect(emitted[0][0]).toBe(3);
82
+ });
83
+
84
+ it('emits the correct values when pressing ArrowLeft', async () => {
85
+ const wrapper = shallowMount(MStarRating, {
86
+ props: { modelValue: 2, readonly: false },
87
+ });
88
+
89
+ const ratingInput = wrapper.find('.mc-star-rating__wrapper');
90
+ await ratingInput.trigger('keydown', { key: 'ArrowLeft' });
91
+ const emitted = wrapper.emitted('update:modelValue') || [];
92
+ expect(emitted.length).toBe(1);
93
+ expect(emitted[0][0]).toBe(1);
94
+ });
95
+
96
+ it('does not do anything if the wrong key is pressed', async () => {
97
+ const wrapper = shallowMount(MStarRating, {
98
+ props: { modelValue: 2, readonly: false },
99
+ });
100
+ const ratingInput = wrapper.find('.mc-star-rating__wrapper');
101
+ await ratingInput.trigger('keydown', { key: 'ArrowUp' });
102
+ const emitted = wrapper.emitted('update:modelValue') || [];
103
+ expect(emitted.length).toBe(0);
104
+ });
105
+
106
+ it('resets hover to null on mouseleave (aria-label falls back to modelValue)', async () => {
107
+ const wrapper = shallowMount(MStarRating, {
108
+ props: { modelValue: 2, size: 'm', readonly: false },
109
+ });
110
+
111
+ const stars = wrapper.findAll('.mc-star-rating__icon');
112
+ const first = stars[0];
113
+ mockRect(first.element, { left: 0, width: 100 });
114
+
115
+ await first.trigger('mousemove', { clientX: 10 });
116
+ await nextTick();
117
+
118
+ // aria-label should reflect hovered value (0.5) not modelValue (2)
119
+ const root = wrapper.find('[role="slider"]');
120
+ expect(root.attributes('aria-label')).toContain('1');
121
+
122
+ // trigger mouseleave on root, hover should be cleared and aria-label revert to modelValue
123
+ await root.trigger('mouseleave');
124
+ await nextTick();
125
+ expect(root.attributes('aria-label')).toContain('2');
126
+ });
127
+
128
+ it('resets hover to null on blur (aria-label falls back to modelValue)', async () => {
129
+ const wrapper = shallowMount(MStarRating, {
130
+ props: { modelValue: 3, size: 'm', readonly: false },
131
+ });
132
+
133
+ const stars = wrapper.findAll('.mc-star-rating__icon');
134
+ const secondStar = stars[1];
135
+ mockRect(secondStar.element, { left: 0, width: 100 });
136
+
137
+ const root = wrapper.find('[role="slider"]');
138
+
139
+ await root.trigger('focus');
140
+ await secondStar.trigger('mousemove', { clientX: 10 });
141
+ await nextTick();
142
+ expect(root.attributes('aria-label')).toContain('2');
143
+
144
+ // blur should clear hover and revert aria-label to modelValue (3)
145
+ await root.trigger('blur');
146
+ await nextTick();
147
+ expect(root.attributes('aria-label')).toContain('3');
148
+ });
149
+
150
+ it('renders information text when text prop is provided', () => {
151
+ const wrapper = shallowMount(MStarRating, {
152
+ props: { modelValue: 3, text: 'Note publique' },
153
+ });
154
+
155
+ const info = wrapper.find('.mc-star-rating__info');
156
+ expect(info.exists()).toBe(true);
157
+ expect(info.text()).toBe('Note publique');
158
+ });
159
+
160
+ it('renders href when href prop is provided and text is not', () => {
161
+ const wrapper = shallowMount(MStarRating, {
162
+ props: { modelValue: 2, href: '/voir' },
163
+ });
164
+
165
+ const info = wrapper.find('.mc-star-rating__info');
166
+ expect(info.exists()).toBe(true);
167
+ expect(info.text()).toBe('/voir');
168
+ });
169
+
170
+ it('does not render info span when neither text nor href is provided', () => {
171
+ const wrapper = shallowMount(MStarRating, {
172
+ props: { modelValue: 1 },
173
+ });
174
+
175
+ const info = wrapper.find('.mc-star-rating__info');
176
+ expect(info.exists()).toBe(false);
177
+ });
178
+
179
+ it('renders router-link when href and router are provided', () => {
180
+ const wrapper = mount(MStarRating, {
181
+ props: { modelValue: 3, href: '/path', router: true },
182
+ global: { stubs: ['router-link'] },
183
+ });
184
+
185
+ expect(wrapper.element.tagName.toLowerCase()).toBe('router-link-stub');
186
+ });
187
+
188
+ it('renders an anchor when href is provided and router is falsy', () => {
189
+ const wrapper = shallowMount(MStarRating, {
190
+ props: { modelValue: 2, href: '#', router: false },
191
+ });
192
+
193
+ expect(wrapper.element.tagName.toLowerCase()).toBe('a');
194
+ });
195
+
196
+ it('renders a div when no href is provided', () => {
197
+ const wrapper = shallowMount(MStarRating, {
198
+ props: { modelValue: 1 },
199
+ });
200
+
201
+ expect(wrapper.element.tagName.toLowerCase()).toBe('div');
202
+ });
203
+ });
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import MStarRating from './MStarRating.vue';
3
+
4
+ const meta: Meta<typeof MStarRating> = {
5
+ title: 'Indicators/Star rating',
6
+ component: MStarRating,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component:
11
+ 'A Star rating visually represents a score or evaluation and can be used to display a rating or allow users to rate an item, such as a product or service. It serves two main purposes: collecting user feedback by enabling individuals to express their experience and providing social proof by displaying ratings from other users to assist decision-making. Rating Stars are commonly found in e-commerce, review systems, and feedback interfaces, offering a quick and intuitive way to assess quality or satisfaction.',
12
+ },
13
+ },
14
+ },
15
+ args: {
16
+ modelValue: 3.5,
17
+ appearance: 'accent',
18
+ readonly: true,
19
+ },
20
+ render: (args) => ({
21
+ components: { MStarRating },
22
+ setup() {
23
+ return { args };
24
+ },
25
+ template: `
26
+ <MStarRating v-model="args.modelValue" v-bind="args"></MStarRating>
27
+ `,
28
+ }),
29
+ };
30
+ export default meta;
31
+
32
+ type Story = StoryObj<typeof MStarRating>;
33
+
34
+ export const Default: Story = {};
35
+
36
+ export const AsInput: Story = {
37
+ args: {
38
+ readonly: false,
39
+ modelValue: 0,
40
+ },
41
+ };
42
+
43
+ export const SizeM: Story = {
44
+ args: {
45
+ size: 'm',
46
+ },
47
+ };
48
+
49
+ export const SizeL: Story = {
50
+ args: {
51
+ size: 'l',
52
+ },
53
+ };
54
+
55
+ export const StandardAppearance: Story = {
56
+ args: {
57
+ appearance: 'standard',
58
+ },
59
+ };
60
+
61
+ export const WithText: Story = {
62
+ args: {
63
+ appearance: 'accent',
64
+ text: 'Additional text',
65
+ },
66
+ };
67
+
68
+ export const WithLink: Story = {
69
+ args: {
70
+ appearance: 'accent',
71
+ text: 'Additional text',
72
+ href: '#',
73
+ },
74
+ };
75
+
76
+ export const CompactModeWithText: Story = {
77
+ args: {
78
+ compact: true,
79
+ appearance: 'accent',
80
+ text: '(35)',
81
+ },
82
+ };