@mozaic-ds/vue 2.14.0 → 2.16.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 (52) hide show
  1. package/dist/mozaic-vue.css +1 -1
  2. package/dist/mozaic-vue.d.ts +1582 -500
  3. package/dist/mozaic-vue.js +8020 -3218
  4. package/dist/mozaic-vue.js.map +1 -1
  5. package/dist/mozaic-vue.umd.cjs +24 -5
  6. package/dist/mozaic-vue.umd.cjs.map +1 -1
  7. package/package.json +6 -4
  8. package/src/components/DarkMode.mdx +115 -0
  9. package/src/components/actionlistbox/MActionListbox.spec.ts +20 -10
  10. package/src/components/actionlistbox/MActionListbox.stories.ts +15 -8
  11. package/src/components/actionlistbox/MActionListbox.vue +15 -12
  12. package/src/components/actionlistbox/README.md +2 -1
  13. package/src/components/avatar/MAvatar.stories.ts +1 -1
  14. package/src/components/breadcrumb/MBreadcrumb.vue +2 -2
  15. package/src/components/button/README.md +2 -0
  16. package/src/components/combobox/MCombobox.spec.ts +246 -0
  17. package/src/components/combobox/MCombobox.stories.ts +190 -0
  18. package/src/components/combobox/MCombobox.vue +277 -0
  19. package/src/components/combobox/README.md +52 -0
  20. package/src/components/field/MField.stories.ts +105 -0
  21. package/src/components/optionListbox/MOptionListbox.spec.ts +527 -0
  22. package/src/components/optionListbox/MOptionListbox.vue +470 -0
  23. package/src/components/optionListbox/README.md +63 -0
  24. package/src/components/pageheader/MPageHeader.spec.ts +12 -12
  25. package/src/components/pageheader/MPageHeader.stories.ts +9 -1
  26. package/src/components/pageheader/MPageHeader.vue +3 -6
  27. package/src/components/segmentedcontrol/MSegmentedControl.spec.ts +57 -25
  28. package/src/components/segmentedcontrol/MSegmentedControl.stories.ts +6 -19
  29. package/src/components/segmentedcontrol/MSegmentedControl.vue +27 -13
  30. package/src/components/segmentedcontrol/README.md +6 -3
  31. package/src/components/select/MSelect.vue +4 -3
  32. package/src/components/sidebar/stories/DefaultCase.stories.vue +2 -2
  33. package/src/components/sidebar/stories/README.md +8 -0
  34. package/src/components/sidebar/stories/WithExpandOnly.stories.vue +1 -1
  35. package/src/components/sidebar/stories/WithProfileInfoOnly.stories.vue +2 -2
  36. package/src/components/sidebar/stories/WithSingleLevel.stories.vue +2 -2
  37. package/src/components/stepperinline/MStepperInline.spec.ts +63 -28
  38. package/src/components/stepperinline/MStepperInline.stories.ts +18 -10
  39. package/src/components/stepperinline/MStepperInline.vue +24 -10
  40. package/src/components/stepperinline/README.md +6 -2
  41. package/src/components/stepperstacked/MStepperStacked.spec.ts +162 -0
  42. package/src/components/stepperstacked/MStepperStacked.stories.ts +57 -0
  43. package/src/components/stepperstacked/MStepperStacked.vue +106 -0
  44. package/src/components/stepperstacked/README.md +15 -0
  45. package/src/components/tabs/MTabs.stories.ts +18 -0
  46. package/src/components/tabs/MTabs.vue +30 -14
  47. package/src/components/tabs/Mtabs.spec.ts +56 -10
  48. package/src/components/tabs/README.md +6 -3
  49. package/src/components/textinput/MTextInput.vue +13 -1
  50. package/src/components/textinput/README.md +15 -1
  51. package/src/components/tileclickable/README.md +1 -1
  52. package/src/main.ts +10 -2
@@ -0,0 +1,470 @@
1
+ <template>
2
+ <div
3
+ ref="listboxEl"
4
+ class="mc-option-listbox mc-listbox__content mc-combobox__listbox"
5
+ >
6
+ <template v-if="search">
7
+ <div class="mc-option-listbox__search">
8
+ <MTextInput
9
+ ref="textInput"
10
+ v-model="searchText"
11
+ role="combobox"
12
+ :id="`search-${id}`"
13
+ size="s"
14
+ :placeholder="searchPlaceholder"
15
+ autocomplete="off"
16
+ :aria-expanded="open"
17
+ :aria-controls="`listbox-${id}`"
18
+ aria-autocomplete="list"
19
+ :aria-activedescendant="activeDescendantId"
20
+ @input="updateFilteredResults"
21
+ @keydown="handleKeydown"
22
+ >
23
+ <template #icon>
24
+ <Search24 />
25
+ </template>
26
+ </MTextInput>
27
+ </div>
28
+
29
+ <hr class="mc-option-listbox__separator" />
30
+ </template>
31
+
32
+ <template v-if="multiple && actions">
33
+ <div class="mc-option-listbox__actions">
34
+ <MButton appearance="accent" ghost size="s" @click="selectAll">
35
+ {{ selectLabel }}
36
+ </MButton>
37
+ <MButton appearance="standard" ghost size="s" @click="clearSelection">
38
+ {{ clearLabel }}
39
+ </MButton>
40
+ </div>
41
+ <hr class="mc-option-listbox__separator" />
42
+ </template>
43
+
44
+ <ul
45
+ class="mc-option-listbox__list"
46
+ role="listbox"
47
+ :id="`listbox-${id}`"
48
+ :tabindex="-1"
49
+ aria-label="Suggestions"
50
+ :aria-multiselectable="multiple"
51
+ >
52
+ <li
53
+ v-for="(item, index) in filteredResults"
54
+ :key="index"
55
+ :id="`option-${id}-${index}`"
56
+ :class="{
57
+ 'mc-option-listbox__item': true,
58
+ 'mc-option-listbox__item--section': item.type === 'section',
59
+ 'mc-option-listbox__item--readonly': readonly,
60
+ 'mc-option-listbox__item--disabled': item.disabled,
61
+ 'mc-option-listbox__item--selectable': isSelectable(item),
62
+ 'mc-option-listbox__item--active': activeIndex === index,
63
+ 'mc-option-listbox__item--selected':
64
+ item.type === 'section'
65
+ ? isSectionSelected(item) || isIndeterminate(item)
66
+ : isOptionSelected(item),
67
+ }"
68
+ v-bind="
69
+ item.type === 'section' && !checkableSections
70
+ ? {
71
+ role: 'presentation',
72
+ }
73
+ : {
74
+ role: 'option',
75
+ ['aria-disabled']: item.disabled,
76
+ ['aria-selected']: isOptionSelected(item),
77
+ onClick: () =>
78
+ item.type === 'section'
79
+ ? toggleSection(item)
80
+ : toggleValue(item),
81
+ }
82
+ "
83
+ >
84
+ <div class="mc-option-listbox__label">
85
+ <slot v-if="item.type !== 'section'" name="optionPrefix" />
86
+
87
+ <div class="mc-option-listbox__content">
88
+ <span
89
+ :class="
90
+ item.type === 'section'
91
+ ? 'mc-option-listbox__section-title'
92
+ : 'mc-option-listbox__text'
93
+ "
94
+ >
95
+ {{ item.label }}
96
+ </span>
97
+
98
+ <span v-if="item.content" class="mc-option-listbox__additional">
99
+ {{ item.content }}
100
+ </span>
101
+ </div>
102
+
103
+ <div class="mc-option-listbox__spacer"></div>
104
+
105
+ <template v-if="isSelectable(item)">
106
+ <span
107
+ v-if="item.type === 'section'"
108
+ class="mc-option-listbox__checkbox"
109
+ >
110
+ <component :is="isIndeterminate(item) ? Less20 : Check20" />
111
+ </span>
112
+
113
+ <template v-else>
114
+ <span v-if="multiple" class="mc-option-listbox__checkbox">
115
+ <Check20 />
116
+ </span>
117
+
118
+ <CheckCircleFilled24
119
+ v-else
120
+ class="mc-option-listbox__selection-icon"
121
+ />
122
+ </template>
123
+ </template>
124
+ </div>
125
+ </li>
126
+ </ul>
127
+ </div>
128
+ </template>
129
+
130
+ <script setup lang="ts">
131
+ import { computed, ref, useTemplateRef, watch, type VNode } from 'vue';
132
+ import MButton from '../button/MButton.vue';
133
+ import MTextInput from '../textinput/MTextInput.vue';
134
+ import {
135
+ CheckCircleFilled24,
136
+ Search24,
137
+ Less20,
138
+ Check20,
139
+ } from '@mozaic-ds/icons-vue';
140
+ import { debounce } from 'lodash';
141
+
142
+ /**
143
+ * An Option Listbox is a customizable, accessible listbox component designed to power dropdowns and comboboxes with advanced selection capabilities. It supports single or multiple selection, optional search, grouped options with section headers, and full keyboard navigation.
144
+ */
145
+ export type ListboxOption = {
146
+ label: string;
147
+ content?: string;
148
+ value?: string | number;
149
+ disabled?: boolean;
150
+ type?: 'option' | 'section';
151
+ };
152
+
153
+ const props = withDefaults(
154
+ defineProps<{
155
+ /**
156
+ * The current selected value(s) of the listbox.
157
+ */
158
+ modelValue: string | number | null | (string | number)[];
159
+ /**
160
+ * Unique identifier for the listbox.
161
+ */
162
+ id: string;
163
+ /**
164
+ * Whether the listbox is open.
165
+ */
166
+ open?: boolean;
167
+ /**
168
+ * Enable multiple selection.
169
+ */
170
+ multiple?: boolean;
171
+ /**
172
+ * Make the listbox read-only.
173
+ */
174
+ readonly?: boolean;
175
+ /**
176
+ * Show a search input above the options.
177
+ */
178
+ search?: boolean;
179
+ /**
180
+ * Show select all / clear buttons when multiple.
181
+ */
182
+ actions?: boolean;
183
+ /**
184
+ * Enable checkable section headers.
185
+ */
186
+ checkableSections?: boolean;
187
+ /**
188
+ * Placeholder text for the search input.
189
+ */
190
+ searchPlaceholder?: string;
191
+ /**
192
+ * Label for the "Select all" button.
193
+ */
194
+ selectLabel?: string;
195
+ /**
196
+ * Label for the "Clear selection" button.
197
+ */
198
+ clearLabel?: string;
199
+ /**
200
+ * Array of options and sections to display in the listbox.
201
+ */
202
+ options: Array<ListboxOption>;
203
+ }>(),
204
+ {
205
+ searchPlaceholder: 'Find an option...',
206
+ selectLabel: 'Select all',
207
+ clearLabel: 'Clear',
208
+ },
209
+ );
210
+
211
+ const emit = defineEmits<{
212
+ /**
213
+ * Emits when the selected value changes.
214
+ */
215
+ (
216
+ on: 'update:modelValue',
217
+ value: string | number | null | (string | number)[],
218
+ ): void;
219
+ /**
220
+ * Emits when the listbox should open.
221
+ */
222
+ (on: 'open'): void;
223
+ /**
224
+ * Emits when the listbox should close.
225
+ */
226
+ (on: 'close'): void;
227
+ }>();
228
+
229
+ defineSlots<{
230
+ /**
231
+ * Use this slot to add a prefix to options.
232
+ */
233
+ optionPrefix: VNode;
234
+ }>();
235
+
236
+ const listboxEl = useTemplateRef('listboxEl');
237
+ const textInput = useTemplateRef('textInput');
238
+
239
+ const activeIndex = ref<number>(-1);
240
+
241
+ const searchText = ref('');
242
+
243
+ const filteredResults = ref<ListboxOption[]>(props.options);
244
+
245
+ const selection = computed({
246
+ get() {
247
+ return props.modelValue;
248
+ },
249
+ set(value: string | number | null | (string | number)[]) {
250
+ emit('update:modelValue', value);
251
+ },
252
+ });
253
+
254
+ const activeDescendantId = computed(() => {
255
+ return activeIndex.value >= 0
256
+ ? `option-${props.id}-${activeIndex.value}`
257
+ : undefined;
258
+ });
259
+
260
+ const updateFilteredResults = debounce(() => {
261
+ const search = searchText.value.toLowerCase().trim();
262
+
263
+ if (!search) {
264
+ filteredResults.value = props.options;
265
+ return;
266
+ }
267
+
268
+ filteredResults.value = props.options.filter((option) =>
269
+ option.label.toLowerCase().includes(search),
270
+ );
271
+
272
+ activeIndex.value = filteredResults.value.length ? 0 : -1;
273
+ }, 200);
274
+
275
+ const sectionMap = computed(() => {
276
+ const map = new Map<string, ListboxOption[]>();
277
+ let currentSection: ListboxOption | null = null;
278
+
279
+ props.options.forEach((option) => {
280
+ if (option.type === 'section') {
281
+ currentSection = option;
282
+ map.set(currentSection?.value?.toString() || currentSection.label, []);
283
+ } else if (currentSection) {
284
+ map
285
+ .get(currentSection?.value?.toString() || currentSection.label)!
286
+ .push(option);
287
+ }
288
+ });
289
+
290
+ return map;
291
+ });
292
+
293
+ function toggleSection(item: ListboxOption) {
294
+ if (!props.checkableSections || !props.multiple) return;
295
+
296
+ const sectionItems =
297
+ sectionMap.value.get(item.value?.toString() || item.label) || [];
298
+ const selectedItems = selection.value as (string | number)[];
299
+
300
+ if (isSectionSelected(item)) {
301
+ selection.value = selectedItems.filter(
302
+ (opt) => !sectionItems.find((item) => item.value === opt),
303
+ );
304
+ } else {
305
+ selection.value = [
306
+ ...selectedItems,
307
+ ...sectionItems
308
+ .filter((opt) => !selectedItems.includes(opt.value!))
309
+ .map((item) => item.value!),
310
+ ];
311
+ }
312
+ }
313
+
314
+ function toggleValue(item?: ListboxOption) {
315
+ if (!item || !isSelectable(item)) return;
316
+
317
+ if (Array.isArray(selection.value)) {
318
+ if (isOptionSelected(item)) {
319
+ selection.value = (selection.value as (string | number)[]).filter(
320
+ (el) => el !== item.value,
321
+ );
322
+ } else {
323
+ selection.value = [...selection.value, item.value!];
324
+ }
325
+ } else {
326
+ selection.value = isOptionSelected(item) ? null : item.value!;
327
+ }
328
+ }
329
+
330
+ function selectAll() {
331
+ selection.value = [
332
+ ...props.options
333
+ .filter(
334
+ (option) =>
335
+ !!option.value && !option.disabled && option.type !== 'section',
336
+ )
337
+ .map((item) => item.value!),
338
+ ];
339
+ }
340
+
341
+ function clearSelection() {
342
+ if (Array.isArray(selection.value)) {
343
+ selection.value = [];
344
+ } else {
345
+ selection.value = null;
346
+ }
347
+ }
348
+
349
+ function isSelectable(item: ListboxOption) {
350
+ return (
351
+ (item.type !== 'section' && !item.disabled) ||
352
+ (item.type === 'section' && props.checkableSections && props.multiple)
353
+ );
354
+ }
355
+
356
+ function isSectionSelected(item: ListboxOption) {
357
+ if (!props.checkableSections || !props.multiple) return false;
358
+
359
+ const sectionItems =
360
+ sectionMap.value.get(item.value?.toString() || item.label) || [];
361
+ const selectedItems = selection.value as (string | number)[];
362
+
363
+ return sectionItems.every((opt) => selectedItems.includes(opt.value!));
364
+ }
365
+
366
+ function isOptionSelected(item: ListboxOption) {
367
+ if (!item.value) return false;
368
+
369
+ if (Array.isArray(selection.value)) {
370
+ return (selection.value as (string | number)[])?.includes(item.value!);
371
+ } else {
372
+ return item.value === selection.value;
373
+ }
374
+ }
375
+
376
+ function isIndeterminate(item: ListboxOption) {
377
+ const section = sectionMap.value.get(item.value?.toString() || item.label);
378
+ return (
379
+ section?.some((option) => isOptionSelected(option)) &&
380
+ !section?.every((option) => isOptionSelected(option))
381
+ );
382
+ }
383
+
384
+ function moveActive(delta: number) {
385
+ if (!props.open || filteredResults.value.length === 0) return;
386
+
387
+ let nextIndex = activeIndex.value + delta;
388
+
389
+ if (nextIndex < 0) nextIndex = filteredResults.value.length - 1;
390
+ if (nextIndex >= filteredResults.value.length) nextIndex = 0;
391
+
392
+ while (!isSelectable(filteredResults.value[nextIndex])) {
393
+ nextIndex += delta > 0 ? 1 : -1;
394
+ if (nextIndex < 0) nextIndex = filteredResults.value.length - 1;
395
+ if (nextIndex >= filteredResults.value.length) nextIndex = 0;
396
+ }
397
+
398
+ activeIndex.value = nextIndex;
399
+ }
400
+
401
+ function selectActive() {
402
+ const item = filteredResults.value[activeIndex.value];
403
+ if (!item || !isSelectable(item)) return;
404
+
405
+ if (item.type === 'section') {
406
+ toggleSection(item);
407
+ } else {
408
+ toggleValue(item);
409
+ }
410
+ }
411
+
412
+ function handleKeydown(event: KeyboardEvent) {
413
+ switch (event.key) {
414
+ case 'ArrowDown':
415
+ event.preventDefault();
416
+ if (!props.open) {
417
+ emit('open');
418
+ activeIndex.value = 0;
419
+ } else {
420
+ moveActive(1);
421
+ }
422
+ break;
423
+ case 'ArrowUp':
424
+ event.preventDefault();
425
+ if (!props.open) {
426
+ emit('open');
427
+ activeIndex.value = filteredResults.value.length - 1;
428
+ } else {
429
+ moveActive(-1);
430
+ }
431
+ break;
432
+ case 'Enter':
433
+ event.preventDefault();
434
+ if (!props.open) {
435
+ emit('open');
436
+ activeIndex.value = 0;
437
+ } else {
438
+ selectActive();
439
+ }
440
+ break;
441
+ case 'Escape':
442
+ event.preventDefault();
443
+ emit('close');
444
+ break;
445
+ }
446
+ }
447
+
448
+ watch(
449
+ () => props.open,
450
+ (v) => {
451
+ if (v) {
452
+ setTimeout(() => {
453
+ textInput.value?.focus();
454
+ }, 50);
455
+ }
456
+ },
457
+ );
458
+
459
+ defineExpose({
460
+ handleKeydown,
461
+ toggleValue,
462
+ clearSelection,
463
+ listboxEl,
464
+ activeIndex,
465
+ });
466
+ </script>
467
+
468
+ <style lang="scss">
469
+ @use '@mozaic-ds/styles/components/option-listbox';
470
+ </style>
@@ -0,0 +1,63 @@
1
+ # MOptionListbox
2
+
3
+ An Option Listbox is a customizable, accessible listbox component designed to power dropdowns and comboboxes with advanced selection capabilities. It supports single or multiple selection, optional search, grouped options with section headers, and full keyboard navigation.
4
+
5
+
6
+ ## Props
7
+
8
+ | Name | Description | Type | Default |
9
+ | --- | --- | --- | --- |
10
+ | `modelValue*` | The current selected value(s) of the listbox. | `string` `number` `(string` `number)[]` `null` | - |
11
+ | `id*` | Unique identifier for the listbox. | `string` | - |
12
+ | `open` | Whether the listbox is open. | `boolean` | - |
13
+ | `multiple` | Enable multiple selection. | `boolean` | - |
14
+ | `readonly` | Make the listbox read-only. | `boolean` | - |
15
+ | `search` | Show a search input above the options. | `boolean` | - |
16
+ | `actions` | Show select all / clear buttons when multiple. | `boolean` | - |
17
+ | `checkableSections` | Enable checkable section headers. | `boolean` | - |
18
+ | `searchPlaceholder` | Placeholder text for the search input. | `string` | `"Find an option..."` |
19
+ | `selectLabel` | Label for the "Select all" button. | `string` | `"Select all"` |
20
+ | `clearLabel` | Label for the "Clear selection" button. | `string` | `"Clear"` |
21
+ | `options*` | Array of options and sections to display in the listbox. | `ListboxOption[]` | - |
22
+
23
+ ## Slots
24
+
25
+ | Name | Description |
26
+ | --- | --- |
27
+ | `optionPrefix` | Use this slot to add a prefix to options. |
28
+
29
+ ## Events
30
+
31
+ | Name | Description | Type |
32
+ | --- | --- | --- |
33
+ | `update:modelValue` | - | `[value: string` `number` `(string` `number)[]` `null]` |
34
+ | `close` | Emits when the listbox should close. | [] |
35
+ | `open` | Emits when the selected value changes. / ( on: 'update:modelValue', value: string | number | null | (string | number)[], ): void; /** Emits when the listbox should open. | [] |
36
+
37
+ ## Dependencies
38
+
39
+ ### Depends on
40
+
41
+ - [MButton](../button)
42
+ - [MTextInput](../textinput)
43
+
44
+ ### Graph
45
+
46
+ ```mermaid
47
+ graph TD;
48
+ MOptionListbox --> MButton
49
+ MOptionListbox --> MTextInput
50
+ style MOptionListbox fill:#008240,stroke:#333,stroke-width:4px
51
+ ```
52
+
53
+ ### Used By
54
+
55
+ - [MCombobox](../combobox)
56
+
57
+ ### Graph
58
+
59
+ ```mermaid
60
+ graph TD;
61
+ MCombobox --> MOptionListbox
62
+ style MOptionListbox fill:#008240,stroke:#333,stroke-width:4px
63
+ ```
@@ -98,9 +98,9 @@ describe('MPageHeader', () => {
98
98
  },
99
99
  });
100
100
 
101
- expect(
102
- wrapper.findComponent({ name: 'MStatusBadge' }).exists(),
103
- ).toBe(true);
101
+ expect(wrapper.findComponent({ name: 'MStatusBadge' }).exists()).toBe(
102
+ true,
103
+ );
104
104
  });
105
105
 
106
106
  it('does not render status badge if statusLabel is missing', () => {
@@ -111,9 +111,9 @@ describe('MPageHeader', () => {
111
111
  },
112
112
  });
113
113
 
114
- expect(
115
- wrapper.findComponent({ name: 'MStatusBadge' }).exists(),
116
- ).toBe(false);
114
+ expect(wrapper.findComponent({ name: 'MStatusBadge' }).exists()).toBe(
115
+ false,
116
+ );
117
117
  });
118
118
 
119
119
  it('renders extra info when provided', () => {
@@ -124,9 +124,9 @@ describe('MPageHeader', () => {
124
124
  },
125
125
  });
126
126
 
127
- expect(
128
- wrapper.find('.mc-page-header__extra-info').text(),
129
- ).toBe('Details');
127
+ expect(wrapper.find('.mc-page-header__extra-info').text()).toBe(
128
+ 'Details',
129
+ );
130
130
  });
131
131
 
132
132
  it('renders info wrapper only when status or extraInfo exists', () => {
@@ -134,9 +134,9 @@ describe('MPageHeader', () => {
134
134
  props: { title: 'My Page' },
135
135
  });
136
136
 
137
- expect(
138
- wrapper.find('.mc-page-header__info-wrapper').exists(),
139
- ).toBe(false);
137
+ expect(wrapper.find('.mc-page-header__info-wrapper').exists()).toBe(
138
+ false,
139
+ );
140
140
  });
141
141
  });
142
142
  });
@@ -63,7 +63,15 @@ const meta: Meta<typeof MPageHeader> = {
63
63
  `,
64
64
  },
65
65
  render: (args) => ({
66
- components: { MPageHeader, MTabs, MIconButton, Search24, HelpCircle24, Notification24, MSelect },
66
+ components: {
67
+ MPageHeader,
68
+ MTabs,
69
+ MIconButton,
70
+ Search24,
71
+ HelpCircle24,
72
+ Notification24,
73
+ MSelect,
74
+ },
67
75
  setup() {
68
76
  const handleBackButtonClick = action('back');
69
77
  const handleMenuClick = action('toggle-menu');
@@ -23,10 +23,7 @@
23
23
  {{ title }}
24
24
  </span>
25
25
 
26
- <div
27
- v-if="status || extraInfo"
28
- class="mc-page-header__info-wrapper"
29
- >
26
+ <div v-if="status || extraInfo" class="mc-page-header__info-wrapper">
30
27
  <MStatusBadge
31
28
  v-if="statusLabel && status"
32
29
  :label="statusLabel"
@@ -55,13 +52,13 @@
55
52
  </MIconButton>
56
53
 
57
54
  <div class="mc-page-header__actions-content">
58
- <slot name="actions"/>
55
+ <slot name="actions" />
59
56
  </div>
60
57
  </div>
61
58
  </div>
62
59
 
63
60
  <div class="mc-page-header__tabs">
64
- <slot name="tabs"/>
61
+ <slot name="tabs" />
65
62
  </div>
66
63
  </div>
67
64
  </template>