@mozaic-ds/vue 1.0.0-beta.8 → 1.0.0-beta.9

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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/mozaic-vue.css +1 -1
  3. package/dist/mozaic-vue.d.ts +410 -198
  4. package/dist/mozaic-vue.js +1100 -777
  5. package/dist/mozaic-vue.js.map +1 -1
  6. package/dist/mozaic-vue.umd.cjs +1 -1
  7. package/dist/mozaic-vue.umd.cjs.map +1 -1
  8. package/package.json +8 -11
  9. package/src/components/Contributing.mdx +1 -1
  10. package/src/components/GettingStarted.mdx +2 -7
  11. package/src/components/Introduction.mdx +41 -21
  12. package/src/components/Support.mdx +1 -1
  13. package/src/components/breadcrumb/MBreadcrumb.stories.ts +11 -13
  14. package/src/components/breadcrumb/MBreadcrumb.vue +1 -1
  15. package/src/components/button/MButton.stories.ts +1 -8
  16. package/src/components/checkbox/MCheckbox.stories.ts +2 -2
  17. package/src/components/checkboxgroup/MCheckboxGroup.stories.ts +2 -2
  18. package/src/components/divider/MDivider.stories.ts +2 -2
  19. package/src/components/divider/MDivider.vue +2 -2
  20. package/src/components/drawer/MDrawer.spec.ts +100 -0
  21. package/src/components/drawer/MDrawer.stories.ts +128 -0
  22. package/src/components/drawer/MDrawer.vue +140 -0
  23. package/src/components/field/MField.stories.ts +2 -9
  24. package/src/components/fieldgroup/MFieldGroup.stories.ts +2 -9
  25. package/src/components/iconbutton/MIconButton.stories.ts +12 -4
  26. package/src/components/link/MLink.stories.ts +3 -12
  27. package/src/components/loader/MLoader.stories.ts +3 -5
  28. package/src/components/loader/MLoader.vue +1 -0
  29. package/src/components/loadingoverlay/MLoadingOverlay.spec.ts +37 -0
  30. package/src/components/loadingoverlay/MLoadingOverlay.stories.ts +40 -0
  31. package/src/components/loadingoverlay/MLoadingOverlay.vue +28 -0
  32. package/src/components/modal/MModal.spec.ts +103 -0
  33. package/src/components/modal/MModal.stories.ts +127 -0
  34. package/src/components/modal/MModal.vue +131 -0
  35. package/src/components/numberbadge/MNumberBadge.stories.ts +3 -5
  36. package/src/components/overlay/MOverlay.stories.ts +3 -8
  37. package/src/components/pagination/MPagination.stories.ts +3 -3
  38. package/src/components/pagination/MPagination.vue +5 -3
  39. package/src/components/passwordinput/MPasswordInput.stories.ts +2 -2
  40. package/src/components/passwordinput/MPasswordInput.vue +2 -5
  41. package/src/components/pincode/MPincode.spec.ts +126 -0
  42. package/src/components/pincode/MPincode.stories.ts +68 -0
  43. package/src/components/pincode/MPincode.vue +139 -0
  44. package/src/components/quantityselector/MQuantitySelector.stories.ts +2 -2
  45. package/src/components/radio/MRadio.stories.ts +2 -2
  46. package/src/components/radiogroup/MRadioGroup.stories.ts +2 -2
  47. package/src/components/select/MSelect.stories.ts +2 -2
  48. package/src/components/statusbadge/MStatusBadge.stories.ts +1 -1
  49. package/src/components/statusdot/MStatusDot.stories.ts +1 -1
  50. package/src/components/statusnotification/MStatusNotification.spec.ts +12 -8
  51. package/src/components/statusnotification/MStatusNotification.stories.ts +2 -9
  52. package/src/components/statusnotification/MStatusNotification.vue +8 -8
  53. package/src/components/tabs/MTabs.stories.ts +4 -4
  54. package/src/components/tabs/MTabs.vue +2 -2
  55. package/src/components/tabs/Mtabs.spec.ts +56 -61
  56. package/src/components/tag/MTag.stories.ts +2 -2
  57. package/src/components/tag/MTag.vue +1 -4
  58. package/src/components/textarea/MTextArea.stories.ts +2 -2
  59. package/src/components/textinput/MTextInput.stories.ts +2 -9
  60. package/src/components/toggle/MToggle.stories.ts +2 -2
  61. package/src/components/togglegroup/MToggleGroup.stories.ts +2 -2
  62. package/src/components/usingIcons.mdx +5 -13
  63. package/src/components/usingPresets.mdx +12 -9
  64. package/src/main.ts +4 -0
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <MOverlay :is-visible="open" dialogLabel="drawerTitle">
3
+ <section
4
+ class="mc-drawer"
5
+ :class="classObject"
6
+ role="dialog"
7
+ aria-labelledby="drawerTitle"
8
+ :aria-modal="open ? 'true' : 'false'"
9
+ tabindex="-1"
10
+ :aria-hidden="!open"
11
+ v-bind="$attrs"
12
+ @keydown.esc="onClose"
13
+ >
14
+ <div class="mc-drawer__dialog" role="document">
15
+ <div class="mc-drawer__header">
16
+ <MIconButton
17
+ v-if="back"
18
+ class="mc-drawer__back"
19
+ aria-label="Back"
20
+ ghost
21
+ @click="emit('back')"
22
+ >
23
+ <template #icon>
24
+ <ArrowBack24 aria-hidden="true" />
25
+ </template>
26
+ </MIconButton>
27
+ <h2 class="mc-drawer__title" id="drawerTitle">{{ title }}</h2>
28
+ <MIconButton
29
+ class="mc-drawer__close"
30
+ aria-label="Close"
31
+ ghost
32
+ @click="onClose"
33
+ >
34
+ <template #icon>
35
+ <Cross24 aria-hidden="true" />
36
+ </template>
37
+ </MIconButton>
38
+ </div>
39
+ <div class="mc-drawer__body">
40
+ <div class="mc-drawer__content" tabindex="0">
41
+ <h2 v-if="contentTitle" class="mc-drawer__content__title">
42
+ {{ contentTitle }}
43
+ </h2>
44
+ <slot />
45
+ </div>
46
+ </div>
47
+ <div v-if="$slots.footer" class="mc-drawer__footer">
48
+ <slot name="footer" />
49
+ </div>
50
+ </div>
51
+ </section>
52
+ </MOverlay>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { computed, watch, type VNode } from 'vue';
57
+ import ArrowBack24 from '@mozaic-ds/icons-vue/src/components/ArrowBack24/ArrowBack24.vue';
58
+ import Cross24 from '@mozaic-ds/icons-vue/src/components/Cross24/Cross24.vue';
59
+ import MIconButton from '../iconbutton/MIconButton.vue';
60
+ import MOverlay from '../overlay/MOverlay.vue';
61
+ /**
62
+ * A drawer is a sliding panel that appears from the side of the screen, providing additional content, settings, or actions without disrupting the main view. It is often used for filtering options, or contextual details. It enhances usability by keeping interfaces clean while offering expandable functionality.
63
+ */
64
+ const props = defineProps<{
65
+ /**
66
+ * If `true`, display the drawer.
67
+ */
68
+ open?: boolean;
69
+ /**
70
+ * Position of the drawer
71
+ */
72
+ position?: 'left' | 'right';
73
+ /**
74
+ * If `true`, the drawer have a bigger width.
75
+ */
76
+ extended?: boolean;
77
+ /**
78
+ * If `true`, display the back button.
79
+ */
80
+ back?: boolean;
81
+ /**
82
+ * Title of the drawer
83
+ */
84
+ title: string;
85
+ /**
86
+ * Title of the content of the drawer
87
+ */
88
+ contentTitle?: string;
89
+ }>();
90
+
91
+ defineSlots<{
92
+ /**
93
+ * Use this slot to insert the content of the drawer
94
+ */
95
+ default?: VNode;
96
+ /**
97
+ * Use this slot to insert buttons in the footer
98
+ */
99
+ footer?: VNode;
100
+ }>();
101
+
102
+ const classObject = computed(() => {
103
+ return {
104
+ 'is-open': props.open,
105
+ 'mc-drawer--extend': props.extended,
106
+ [`mc-drawer--${props.position}`]:
107
+ props.position && props.position != 'right',
108
+ };
109
+ });
110
+
111
+ watch(
112
+ () => props.open,
113
+ (newValue) => {
114
+ emit('update:open', newValue);
115
+ },
116
+ );
117
+
118
+ const onClose = () => {
119
+ emit('update:open', false);
120
+ };
121
+
122
+ const emit = defineEmits<{
123
+ /**
124
+ * Emits when the drawer open state changes, updating the modelValue prop.
125
+ */
126
+ (on: 'update:open', value: boolean): void;
127
+ /**
128
+ * Emits when click back button of the drawer.
129
+ */
130
+ (on: 'back'): void;
131
+ }>();
132
+ </script>
133
+
134
+ <style lang="scss" scoped>
135
+ @use '@mozaic-ds/styles/components/drawer';
136
+
137
+ .mc-overlay {
138
+ filter: none;
139
+ }
140
+ </style>
@@ -1,5 +1,5 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3';
2
- import { action } from '@storybook/addon-actions';
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { action } from 'storybook/actions';
3
3
 
4
4
  import MField from './MField.vue';
5
5
  import MTextInput from '../textinput/MTextInput.vue';
@@ -27,13 +27,6 @@ const meta: Meta<typeof MField> = {
27
27
  </div>
28
28
  `,
29
29
  },
30
- argTypes: {
31
- $slots: {
32
- table: {
33
- disable: true,
34
- },
35
- },
36
- },
37
30
  render: (args) => ({
38
31
  components: { MField, MTextInput },
39
32
  setup() {
@@ -1,5 +1,5 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3';
2
- import { action } from '@storybook/addon-actions';
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { action } from 'storybook/actions';
3
3
 
4
4
  import MFieldGroup from './MFieldGroup.vue';
5
5
  import MCheckboxGroup from '../checkboxgroup/MCheckboxGroup.vue';
@@ -53,13 +53,6 @@ const meta: Meta<typeof MFieldGroup> = {
53
53
  />
54
54
  `,
55
55
  },
56
- argTypes: {
57
- $slots: {
58
- table: {
59
- disable: true,
60
- },
61
- },
62
- },
63
56
  render: (args) => ({
64
57
  components: { MFieldGroup, MCheckboxGroup, MRadioGroup, MToggleGroup },
65
58
  setup() {
@@ -1,7 +1,9 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3';
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
2
 
3
3
  import MIconButton from './MIconButton.vue';
4
+ import ChevronRight20 from '@mozaic-ds/icons-vue/src/components/ChevronRight20/ChevronRight20.vue';
4
5
  import ChevronRight24 from '@mozaic-ds/icons-vue/src/components/ChevronRight24/ChevronRight24.vue';
6
+ import ChevronRight32 from '@mozaic-ds/icons-vue/src/components/ChevronRight32/ChevronRight32.vue';
5
7
 
6
8
  const meta: Meta<typeof MIconButton> = {
7
9
  title: 'Action/Icon Button',
@@ -31,7 +33,7 @@ const meta: Meta<typeof MIconButton> = {
31
33
  },
32
34
  },
33
35
  render: (args) => ({
34
- components: { MIconButton, ChevronRight24 },
36
+ components: { MIconButton, ChevronRight20, ChevronRight24, ChevronRight32 },
35
37
  setup() {
36
38
  return { args };
37
39
  },
@@ -58,9 +60,15 @@ export const Ghost: Story = {
58
60
  };
59
61
 
60
62
  export const SizeS: Story = {
61
- args: { size: 's' },
63
+ args: {
64
+ size: 's',
65
+ icon: '<ChevronRight20/>',
66
+ },
62
67
  };
63
68
 
64
69
  export const SizeL: Story = {
65
- args: { size: 'l' },
70
+ args: {
71
+ size: 'l',
72
+ icon: '<ChevronRight32/>',
73
+ },
66
74
  };
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3';
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
2
  import MLink from './MLink.vue';
3
3
  import ChevronLeft24 from '@mozaic-ds/icons-vue/src/components/ChevronLeft24/ChevronLeft24.vue';
4
4
  import ChevronRight24 from '@mozaic-ds/icons-vue/src/components/ChevronRight24/ChevronRight24.vue';
@@ -18,13 +18,6 @@ const meta: Meta<typeof MLink> = {
18
18
  default: 'Stand-alone link',
19
19
  href: '#',
20
20
  },
21
- argTypes: {
22
- $slots: {
23
- table: {
24
- disable: true,
25
- },
26
- },
27
- },
28
21
  render: (args) => ({
29
22
  components: { MLink, ChevronRight24, ChevronLeft24 },
30
23
  setup() {
@@ -67,10 +60,8 @@ export const Accent: Story = {
67
60
  };
68
61
 
69
62
  export const Inverse: Story = {
70
- parameters: {
71
- backgrounds: {
72
- default: 'Inverse',
73
- },
63
+ globals: {
64
+ backgrounds: { value: 'inverse' },
74
65
  },
75
66
  args: { appearance: 'inverse' },
76
67
  };
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/vue3';
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
2
  import MLoader from './MLoader.vue';
3
3
 
4
4
  const meta: Meta<typeof MLoader> = {
@@ -32,10 +32,8 @@ export const Accent: Story = {
32
32
  };
33
33
 
34
34
  export const Inverse: Story = {
35
- parameters: {
36
- backgrounds: {
37
- default: 'Inverse',
38
- },
35
+ globals: {
36
+ backgrounds: { value: 'inverse' },
39
37
  },
40
38
  args: { appearance: 'inverse' },
41
39
  };
@@ -52,6 +52,7 @@ const classObject = computed(() => {
52
52
  [`mc-loader--${props.size}`]: props.size && props.size !== 'm',
53
53
  [`mc-loader--${props.appearance}`]:
54
54
  props.appearance && props.appearance !== 'standard',
55
+ 'mc-loader--text-visible': props.text,
55
56
  };
56
57
  });
57
58
 
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MLoadingOverlay from './MLoadingOverlay.vue';
4
+
5
+ describe('MLoadingOverlay component', () => {
6
+ it('renders and applies isVisible class based on prop', () => {
7
+ const wrapper = mount(MLoadingOverlay, {
8
+ props: { isVisible: true, text: 'Loading data...' },
9
+ });
10
+
11
+ expect(wrapper.classes()).toContain('is-visible');
12
+
13
+ const dialog = wrapper.find('[role="dialog"]');
14
+ expect(dialog.attributes('aria-label')).toBe('Loading data...');
15
+ });
16
+
17
+ it('does not have is-visible class when isVisible is false or undefined', () => {
18
+ const wrapper = mount(MLoadingOverlay, {
19
+ props: { isVisible: false },
20
+ });
21
+ expect(wrapper.classes()).not.toContain('is-visible');
22
+
23
+ const wrapperNoProp = mount(MLoadingOverlay);
24
+ expect(wrapperNoProp.classes()).not.toContain('is-visible');
25
+ });
26
+
27
+ it('passes down attributes via $attrs', () => {
28
+ const wrapper = mount(MLoadingOverlay, {
29
+ props: { isVisible: true, text: 'Loading...' },
30
+ attrs: { id: 'custom-id', 'data-test': 'loading-overlay' },
31
+ });
32
+
33
+ const dialog = wrapper.find('[role="dialog"]');
34
+ expect(dialog.attributes('id')).toBe('custom-id');
35
+ expect(dialog.attributes('data-test')).toBe('loading-overlay');
36
+ });
37
+ });
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import MLoadingOverlay from './MLoadingOverlay.vue';
3
+
4
+ const meta: Meta<typeof MLoadingOverlay> = {
5
+ title: 'Overlay/Loading Overlay',
6
+ component: MLoadingOverlay,
7
+ parameters: {
8
+ layout: 'fullscreen',
9
+ docs: {
10
+ story: { height: '400px' },
11
+ description: {
12
+ component:
13
+ 'An overlay component is a UI element that appears above the main content to display additional information or interactions, often blocking or dimming the background.',
14
+ },
15
+ },
16
+ },
17
+ args: {
18
+ isVisible: true,
19
+ ariaLabel: 'Page loading',
20
+ },
21
+ render: (args) => ({
22
+ components: { MLoadingOverlay },
23
+ setup() {
24
+ return { args };
25
+ },
26
+ template: `
27
+ <MLoadingOverlay v-bind="args"/>
28
+ `,
29
+ }),
30
+ };
31
+ export default meta;
32
+ type Story = StoryObj<typeof MLoadingOverlay>;
33
+
34
+ export const Default: Story = {};
35
+
36
+ export const text: Story = {
37
+ args: {
38
+ text: 'Insert your text here',
39
+ },
40
+ };
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <div class="mc-loading-loader" :class="{ 'is-visible': isVisible }">
3
+ <div role="dialog" tabindex="-1" :aria-label="text" v-bind="$attrs">
4
+ <MLoader size="l" appearance="inverse" :text="text" />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import MLoader from '../loader/MLoader.vue';
11
+ /**
12
+ * A loading overlay is a full-screen or container-level layer that indicates a process is in progress, preventing user interaction until the task is completed. It includes a progress indicator, and a message to inform users about the loading state. Loading Overlays are commonly used in data-heavy applications, form submissions, and page transitions to enhance user experience by managing wait times effectively.
13
+ */
14
+ defineProps<{
15
+ /**
16
+ * Controls the visibility of the loading overlay.
17
+ */
18
+ isVisible?: boolean;
19
+ /**
20
+ * Text of the loading overlay.
21
+ */
22
+ text?: string;
23
+ }>();
24
+ </script>
25
+
26
+ <style lang="scss" scoped>
27
+ @use '@mozaic-ds/styles/components/loading-overlay';
28
+ </style>
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MModal from './MModal.vue';
4
+
5
+ const stubs = {
6
+ Cross24: { template: '<svg />' },
7
+ MIconButton: {
8
+ template: `<button @click="$emit('click')"><slot name="icon"/></button>`,
9
+ },
10
+ MOverlay: {
11
+ template: `<div class="overlay"><slot/></div>`,
12
+ },
13
+ };
14
+
15
+ describe('MModal component', () => {
16
+ it('renders title and description', () => {
17
+ const wrapper = mount(MModal, {
18
+ props: {
19
+ open: true,
20
+ title: 'Test Modal',
21
+ description: 'This is a description',
22
+ },
23
+ global: { stubs },
24
+ });
25
+
26
+ expect(wrapper.text()).toContain('Test Modal');
27
+ expect(wrapper.text()).toContain('This is a description');
28
+ });
29
+
30
+ it('renders close button if closable is true', () => {
31
+ const wrapper = mount(MModal, {
32
+ props: { open: true, title: 'Title', closable: true },
33
+ global: { stubs },
34
+ });
35
+
36
+ expect(wrapper.find('button.mc-modal__close').exists()).toBe(true);
37
+ });
38
+
39
+ it('does not render close button if closable is false', () => {
40
+ const wrapper = mount(MModal, {
41
+ props: { open: true, title: 'Title', closable: false },
42
+ global: { stubs },
43
+ });
44
+
45
+ expect(wrapper.find('button.mc-modal__close').exists()).toBe(false);
46
+ });
47
+
48
+ it('emits update:open with false when close button clicked', async () => {
49
+ const wrapper = mount(MModal, {
50
+ props: { open: true, title: 'Title', closable: true },
51
+ global: { stubs },
52
+ });
53
+
54
+ await wrapper.find('button.mc-modal__close').trigger('click');
55
+
56
+ expect(wrapper.emitted('update:open')).toBeTruthy();
57
+ expect(wrapper.emitted('update:open')![0]).toEqual([false]);
58
+ });
59
+
60
+ it('renders slots content', () => {
61
+ const wrapper = mount(MModal, {
62
+ props: { open: true, title: 'Title' },
63
+ slots: {
64
+ default: '<div class="default-slot">Default Content</div>',
65
+ icon: '<span class="icon-slot">ICON</span>',
66
+ footer: '<div class="footer-slot">Footer Content</div>',
67
+ link: '<a href="#" class="link-slot">Link Content</a>',
68
+ },
69
+ global: { stubs },
70
+ });
71
+
72
+ expect(wrapper.find('.icon-slot').exists()).toBe(true);
73
+ expect(wrapper.find('.default-slot').exists()).toBe(true);
74
+ expect(wrapper.find('.footer-slot').exists()).toBe(true);
75
+ expect(wrapper.find('.link-slot').exists()).toBe(true);
76
+ });
77
+
78
+ it('has aria-hidden set correctly based on open prop', async () => {
79
+ const wrapper = mount(MModal, {
80
+ props: { open: false, title: 'Title' },
81
+ global: { stubs },
82
+ });
83
+
84
+ expect(wrapper.find('.mc-modal').attributes('aria-hidden')).toBe('true');
85
+
86
+ await wrapper.setProps({ open: true });
87
+
88
+ expect(wrapper.find('.mc-modal').attributes('aria-hidden')).toBe('false');
89
+ });
90
+
91
+ it('adds "is-open" class when open is true', async () => {
92
+ const wrapper = mount(MModal, {
93
+ props: { open: false, title: 'Title' },
94
+ global: { stubs },
95
+ });
96
+
97
+ expect(wrapper.find('.mc-modal').classes()).not.toContain('is-open');
98
+
99
+ await wrapper.setProps({ open: true });
100
+
101
+ expect(wrapper.find('.mc-modal').classes()).toContain('is-open');
102
+ });
103
+ });
@@ -0,0 +1,127 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref, watch } from 'vue';
3
+ import { action } from 'storybook/actions';
4
+ import MModal from './MModal.vue';
5
+ import MButton from '../button/MButton.vue';
6
+ import MLink from '../link/MLink.vue';
7
+ import InfoCircle32 from '@mozaic-ds/icons-vue/src/components/InfoCircle32/InfoCircle32.vue';
8
+ import ExternalLink24 from '@mozaic-ds/icons-vue/src/components/ExternalLink24/ExternalLink24.vue';
9
+
10
+ const meta: Meta<typeof MModal> = {
11
+ title: 'Overlay/Modal',
12
+ component: MModal,
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ docs: {
16
+ story: { height: '400px' },
17
+ description: {
18
+ component:
19
+ 'A modal is a dialog window that appears on top of the main content, requiring user interaction before returning to the main interface. It is used to focus attention on a specific task, provide important information, or request confirmation for an action. Modals typically include a title, description, and primary/secondary actions and should be used for single, focused tasks to avoid disrupting the user experience.',
20
+ },
21
+ },
22
+ },
23
+ args: {
24
+ open: true,
25
+ title: 'Modal title',
26
+ description: `A modal is a dialog window allowing you to focus the user's attention on a specific task, a piece of information or a mandatory action. It must be used for single action only and must have a call to action button in the bottom.`,
27
+ footer: `
28
+ <MButton ghost>Cancel</MButton>
29
+ <MButton>Primary action</MButton>
30
+ `,
31
+ },
32
+ render: (args) => ({
33
+ components: { MModal, MButton, MLink, InfoCircle32, ExternalLink24 },
34
+ setup() {
35
+ const handleUpdate = action('update:open');
36
+ const openState = ref(args.open);
37
+
38
+ watch(
39
+ () => args.open,
40
+ (val) => {
41
+ openState.value = val;
42
+ },
43
+ );
44
+
45
+ const onUpdateOpen = (val: boolean) => {
46
+ openState.value = val;
47
+ handleUpdate(val);
48
+ args.open = val;
49
+ };
50
+
51
+ return { args, openState, onUpdateOpen };
52
+ },
53
+ template: `
54
+ <MModal
55
+ v-bind="args"
56
+ @update:open="onUpdateOpen"
57
+ v-model:open="openState"
58
+ >
59
+ <template v-if="${'icon' in args}" v-slot:icon>${args.icon}</template>
60
+ <template v-if="${'default' in args}" v-slot>${args.default}</template>
61
+ <template v-if="${'link' in args}" v-slot:link>${args.link}</template>
62
+ <template v-if="${'footer' in args}" v-slot:footer>${args.footer}</template>
63
+ </MModal>
64
+ `,
65
+ }),
66
+ };
67
+ export default meta;
68
+ type Story = StoryObj<typeof MModal>;
69
+
70
+ export const Default: Story = {};
71
+
72
+ export const Icon = {
73
+ args: {
74
+ icon: '<InfoCircle32/>',
75
+ },
76
+ };
77
+
78
+ export const ValidationOnly = {
79
+ args: {
80
+ footer: `
81
+ <MButton>Primary action</MButton>
82
+ `,
83
+ },
84
+ };
85
+
86
+ export const TwoOptions = {
87
+ args: {
88
+ footer: `
89
+ <MButton outlined>Secondary action</MButton>
90
+ <MButton>Primary action</MButton>
91
+ `,
92
+ },
93
+ };
94
+
95
+ export const Cancel = {
96
+ args: {
97
+ footer: `
98
+ <MButton ghost>Cancel</MButton>
99
+ <MButton outlined>Secondary action</MButton>
100
+ <MButton>Primary action</MButton>
101
+ `,
102
+ },
103
+ };
104
+
105
+ export const Link = {
106
+ args: {
107
+ link: `
108
+ <MLink class="mc-modal__link" href="#" size="m" icon-position="right">
109
+ Learn more
110
+ <template #icon><ExternalLink24 /></template>
111
+ </MLink>
112
+ `,
113
+ footer: `
114
+ <MButton ghost>Cancel</MButton>
115
+ <MButton>Primary action</MButton>
116
+ `,
117
+ },
118
+ };
119
+
120
+ export const Danger = {
121
+ args: {
122
+ footer: `
123
+ <MButton ghost>Cancel</MButton>
124
+ <MButton appearance="danger">Primary action</MButton>
125
+ `,
126
+ },
127
+ };