@teammodlr/ui-kit 3.0.0-beta.9 → 3.0.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 (87) hide show
  1. package/lib/components/checkbox/Checkbox.vue.d.ts +4 -4
  2. package/lib/components/clearbutton/ClearButton.vue.d.ts +7 -0
  3. package/lib/components/clearbutton/style/ClearButton.css.d.ts +2 -0
  4. package/lib/components/clearbutton/style/index.d.ts +1 -0
  5. package/lib/components/colorpicker/ColorPicker.vue.d.ts +0 -1
  6. package/lib/components/colorpicker/style/ColorPicker.css.d.ts +1 -0
  7. package/lib/components/dialog/style/Dialog.css.d.ts +2 -0
  8. package/lib/components/drawer/Drawer.vue.d.ts +142 -0
  9. package/lib/components/drawer/style/Drawer.css.d.ts +33 -0
  10. package/lib/components/drawer/style/index.d.ts +1 -0
  11. package/lib/components/hovercard/HoverCard.vue.d.ts +122 -0
  12. package/lib/components/hovercard/style/HoverCard.css.d.ts +3 -0
  13. package/lib/components/hovercard/style/index.d.ts +1 -0
  14. package/lib/components/index.d.ts +3 -0
  15. package/lib/components/input/Input.vue.d.ts +20 -5
  16. package/lib/components/input/style/Input.css.d.ts +4 -0
  17. package/lib/components/paneldialog/PanelDialog.vue.d.ts +167 -0
  18. package/lib/components/paneldialog/style/PanelDialog.css.d.ts +8 -0
  19. package/lib/components/paneldialog/style/index.d.ts +1 -0
  20. package/lib/components/popover/Popover.vue.d.ts +24 -0
  21. package/lib/components/radio/Radio.vue.d.ts +5 -5
  22. package/lib/components/radiobutton/RadioButton.vue.d.ts +4 -4
  23. package/lib/components/select/Select.vue.d.ts +72 -91
  24. package/lib/components/select/style/Select.css.d.ts +9 -1
  25. package/lib/components/switch/Switch.vue.d.ts +4 -4
  26. package/lib/components/textarea/Textarea.vue.d.ts +16 -5
  27. package/lib/components/textarea/style/Textarea.css.d.ts +2 -0
  28. package/lib/composables/useInput.d.ts +9 -2
  29. package/lib/index.cjs.js +3 -3
  30. package/lib/index.es.js +8491 -6431
  31. package/lib/style.css +1 -1
  32. package/lib/tsconfig.app.tsbuildinfo +1 -0
  33. package/package.json +7 -4
  34. package/src/components/cardcontextmenu/style/Dropdown.css.ts +7 -0
  35. package/src/components/clearbutton/ClearButton.vue +39 -0
  36. package/src/components/clearbutton/style/ClearButton.css.ts +37 -0
  37. package/src/components/clearbutton/style/index.ts +1 -0
  38. package/src/components/colorpicker/ColorPicker.vue +8 -11
  39. package/src/components/colorpicker/examples/basic.vue +8 -17
  40. package/src/components/colorpicker/style/ColorPicker.css.ts +6 -1
  41. package/src/components/dialog/Dialog.vue +2 -5
  42. package/src/components/dialog/style/Dialog.css.ts +30 -0
  43. package/src/components/drawer/Drawer.vue +276 -0
  44. package/src/components/drawer/examples/basic.vue +15 -0
  45. package/src/components/drawer/examples/custom-title.vue +21 -0
  46. package/src/components/drawer/examples/index.stories.ts +24 -0
  47. package/src/components/drawer/examples/index.ts +7 -0
  48. package/src/components/drawer/examples/no-mask.vue +15 -0
  49. package/src/components/drawer/examples/no-outside-close.vue +15 -0
  50. package/src/components/drawer/examples/placements.vue +28 -0
  51. package/src/components/drawer/examples/with-footer.vue +21 -0
  52. package/src/components/drawer/examples/with-trigger.vue +13 -0
  53. package/src/components/drawer/style/Drawer.css.ts +208 -0
  54. package/src/components/drawer/style/index.ts +1 -0
  55. package/src/components/dropdown/Dropdown.vue +1 -1
  56. package/src/components/hovercard/HoverCard.vue +163 -0
  57. package/src/components/hovercard/style/HoverCard.css.ts +175 -0
  58. package/src/components/hovercard/style/index.ts +1 -0
  59. package/src/components/index.ts +3 -0
  60. package/src/components/input/Input.vue +52 -4
  61. package/src/components/input/examples/clearable.vue +18 -0
  62. package/src/components/input/examples/index.stories.ts +3 -1
  63. package/src/components/input/examples/index.ts +1 -0
  64. package/src/components/input/style/Input.css.ts +19 -0
  65. package/src/components/messageprovider/MessageProvider.vue +33 -7
  66. package/src/components/paneldialog/PanelDialog.vue +500 -0
  67. package/src/components/paneldialog/examples/basic.vue +36 -0
  68. package/src/components/paneldialog/examples/index.stories.ts +22 -0
  69. package/src/components/paneldialog/examples/index.ts +2 -0
  70. package/src/components/paneldialog/examples/modal.vue +31 -0
  71. package/src/components/paneldialog/style/PanelDialog.css.ts +90 -0
  72. package/src/components/paneldialog/style/index.ts +1 -0
  73. package/src/components/popover/Popover.vue +6 -1
  74. package/src/components/select/Select.vue +122 -42
  75. package/src/components/select/examples/clearable.vue +18 -0
  76. package/src/components/select/examples/fill.vue +52 -0
  77. package/src/components/select/examples/index.stories.ts +3 -1
  78. package/src/components/select/examples/index.ts +2 -0
  79. package/src/components/select/style/Select.css.ts +140 -24
  80. package/src/components/tab/Tab.vue +1 -1
  81. package/src/components/tab/style/Tab.css.ts +6 -0
  82. package/src/components/textarea/Textarea.vue +40 -4
  83. package/src/components/textarea/examples/clearable.vue +16 -0
  84. package/src/components/textarea/examples/index.stories.ts +2 -1
  85. package/src/components/textarea/examples/index.ts +1 -0
  86. package/src/components/textarea/style/Textarea.css.ts +10 -0
  87. package/src/composables/useInput.ts +10 -2
@@ -0,0 +1,22 @@
1
+ import MPanelDialog from '../PanelDialog.vue';
2
+ import { MPanelDialogBasic, MPanelDialogModal } from './index';
3
+ import { createStory } from '../../../__storybook__';
4
+
5
+ export default {
6
+ component: MPanelDialog,
7
+ title: 'Components/PanelDialog',
8
+ argTypes: {
9
+ open: { control: 'boolean' },
10
+ modal: { control: 'boolean' },
11
+ draggable: { control: 'boolean' },
12
+ width: { control: 'text' },
13
+ height: { control: 'text' },
14
+ maxHeight: { control: 'text' },
15
+ handleSelector: { control: 'text' },
16
+ backdrop: { control: 'boolean' },
17
+ focusTrap: { control: 'boolean' }
18
+ }
19
+ };
20
+
21
+ export const Basic = createStory(MPanelDialogBasic);
22
+ export const Modal = createStory(MPanelDialogModal);
@@ -0,0 +1,2 @@
1
+ export { default as MPanelDialogBasic } from './basic.vue';
2
+ export { default as MPanelDialogModal } from './modal.vue';
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div>
3
+ <button @click="open = true">Open modal panel</button>
4
+ <MPanelDialog
5
+ v-model:open="open"
6
+ :modal="true"
7
+ width="520px"
8
+ :draggable="false"
9
+ :backdrop="true"
10
+ >
11
+ <div data-panel-handle style="padding: 12px 16px; border-bottom: 1px solid #e4e2ea; font-weight: 600;">
12
+ Modal panel (drag disabled)
13
+ </div>
14
+ <div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
15
+ <p style="margin: 0;">Modal variant blocks background interaction and shows an overlay.</p>
16
+ <MInput placeholder="Focus trap stays inside" />
17
+ <div style="display: flex; gap: 8px; justify-content: flex-end;">
18
+ <button @click="open = false">Close</button>
19
+ </div>
20
+ </div>
21
+ </MPanelDialog>
22
+ </div>
23
+ </template>
24
+
25
+ <script lang="ts" setup>
26
+ import { ref } from 'vue';
27
+ import MPanelDialog from '../PanelDialog.vue';
28
+ import MInput from '../../input/Input.vue';
29
+
30
+ const open = ref(false);
31
+ </script>
@@ -0,0 +1,90 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css';
2
+ import { themeVars } from '../../../styles/theme.css';
3
+
4
+ export const backdrop = style({
5
+ position: 'fixed',
6
+ inset: 0,
7
+ backgroundColor: 'rgba(0,0,0,.45)',
8
+ pointerEvents: 'none'
9
+ });
10
+
11
+ export const dialogContent = style({
12
+ position: 'fixed',
13
+ inset: 0,
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ outline: 'none',
18
+ selectors: {
19
+ '&:focus': {
20
+ outline: 'none'
21
+ },
22
+ '&:focus-visible': {
23
+ outline: 'none'
24
+ }
25
+ }
26
+ });
27
+
28
+ export const dialogContentModal = style({
29
+ pointerEvents: 'auto'
30
+ });
31
+
32
+ export const dialogContentNonModal = style({
33
+ pointerEvents: 'none'
34
+ });
35
+
36
+ export const focusScope = style({
37
+ display: 'flex',
38
+ alignItems: 'center',
39
+ justifyContent: 'center',
40
+ width: '100%',
41
+ height: '100%',
42
+ outline: 'none',
43
+ selectors: {
44
+ '&:focus': {
45
+ outline: 'none'
46
+ },
47
+ '&:focus-visible': {
48
+ outline: 'none'
49
+ }
50
+ }
51
+ });
52
+
53
+ export const focusScopeNonModal = style({
54
+ pointerEvents: 'none'
55
+ });
56
+
57
+ export const focusScopeModal = style({
58
+ pointerEvents: 'auto'
59
+ });
60
+
61
+ export const panel = style({
62
+ position: 'relative',
63
+ display: 'flex',
64
+ flexDirection: 'column',
65
+ backgroundColor: '#FFF',
66
+ backgroundClip: 'padding-box',
67
+ border: `1px solid ${themeVars.color.gray[300]}`,
68
+ borderRadius: themeVars.rounded.medium,
69
+ boxShadow: `2px 2px 8px rgba(128, 128, 128, 0.1), 0 9px 28px 8px rgba(0,0,0,.05)`,
70
+ overflow: 'hidden',
71
+ outline: 'none',
72
+ pointerEvents: 'auto',
73
+ maxWidth: 'calc(100vw - 32px)',
74
+ selectors: {
75
+ '&:focus': {
76
+ outline: 'none'
77
+ },
78
+ '&:focus-visible': {
79
+ outline: 'none'
80
+ }
81
+ }
82
+ });
83
+
84
+ globalStyle(`${panel} [data-panel-handle]`, {
85
+ cursor: 'grab'
86
+ });
87
+
88
+ globalStyle(`${panel} [data-panel-handle]:active`, {
89
+ cursor: 'grabbing'
90
+ });
@@ -0,0 +1 @@
1
+ export * from './PanelDialog.css';
@@ -9,6 +9,7 @@
9
9
  :side="resolvedSide"
10
10
  :align="resolvedAlign"
11
11
  :side-offset="sideOffset"
12
+ :collision-padding="collisionPadding"
12
13
  :style="{ zIndex: zIndex }"
13
14
  >
14
15
  <slot name="content" />
@@ -68,12 +69,16 @@ export default defineComponent({
68
69
  },
69
70
  transfer: {
70
71
  type: Boolean,
71
- default: false
72
+ default: true
72
73
  },
73
74
  sideOffset: {
74
75
  type: Number,
75
76
  default: 5
76
77
  },
78
+ collisionPadding: {
79
+ type: [Number, Object] as PropType<number | { top?: number; right?: number; bottom?: number; left?: number }>,
80
+ default: 10
81
+ },
77
82
  arrow: {
78
83
  type: Boolean,
79
84
  default: false
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <PopoverRoot :open="isOpen" @update:open="handleOpenChange">
3
- <div ref="selectRef" tabindex="-1">
3
+ <div ref="selectRef" tabindex="-1" :class="fillRootClass">
4
4
  <label v-if="label" :for="id" :class="style.label" :id="`${id}-label`">
5
5
  {{ label }}
6
6
  </label>
@@ -16,7 +16,7 @@
16
16
  <div :class="selectComputedClasses">
17
17
  <template v-if="multiple && Array.isArray(selected) && selected.length > 0">
18
18
  <MTag v-for="value in selectedMax" :key="value" :closeable="true"
19
- @close="event => removeSelected(event, value)" :size="size">
19
+ @close="() => removeSelected(value)" :size="size">
20
20
  {{ getSelectedLabel(value) }}
21
21
  </MTag>
22
22
  <template v-if="selectedCountNotShown > 0">
@@ -26,15 +26,16 @@
26
26
  </template>
27
27
  </template>
28
28
  <template v-else-if="!multiple && selected">
29
- {{ getSelectedLabel(selected) }}
29
+ <span :class="fillLabelClass">{{ getSelectedLabel(selected) }}</span>
30
30
  </template>
31
31
  <template v-else>
32
- <div :class="style.placeholder">{{ placeholder }}</div>
32
+ <div :class="[style.placeholder, fillLabelClass]">{{ placeholder }}</div>
33
33
  </template>
34
34
  <span :class="selectComputedArrowClasses">
35
35
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
36
36
  </span>
37
37
  </div>
38
+ <MClearButton v-if="showClear" :class="style.clear" @clear="clearSelected" />
38
39
  </div>
39
40
  </PopoverTrigger>
40
41
 
@@ -71,24 +72,30 @@
71
72
  <input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
72
73
  :class="style.searchInput" :data-input="`select-menu-search-${id}`" />
73
74
  </div>
74
- <div :class="style.options">
75
- <div v-for="(option, index) in computedOptions" :key="index" ref="optionsRef" :class="{
76
- [style.option]: true,
77
- [style.optionSelected]: option.active,
78
- [style.optionFocused]: selectedIndex === index
79
- }" @click.prevent="option.onClick" @mouseenter="selectedIndex = index"
80
- :data-element="`select-menu-option-${id}-${option.value}`">
81
- <span :class="style.optionIcon">
82
- <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
83
- <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
84
- </svg>
85
- </span>
86
- <span :class="style.optionLabel">{{ option.label }}</span>
87
- </div>
88
- <template v-if="computedOptions.length === 0">
89
- <div :class="style.noResults">No results</div>
75
+ <div v-if="computedOptions.length === 0" :class="style.noResults">No results</div>
76
+ <VList
77
+ v-else
78
+ ref="virtualListRef"
79
+ :data="computedOptions"
80
+ :item-size="optionItemHeight"
81
+ :style="{ height: optionsHeight + 'px', padding: '4px' }"
82
+ >
83
+ <template #default="{ item, index }">
84
+ <div :class="[optionSizeClass, {
85
+ [style.option]: true,
86
+ [style.optionSelected]: item.active,
87
+ [style.optionFocused]: selectedIndex === index
88
+ }]" @click.prevent="item.onClick" @mouseenter="selectedIndex = index"
89
+ :data-element="`select-menu-option-${id}-${item.value}`">
90
+ <span :class="style.optionIcon">
91
+ <svg v-if="item.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
92
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
93
+ </svg>
94
+ </span>
95
+ <span :class="style.optionLabel">{{ item.label }}</span>
96
+ </div>
90
97
  </template>
91
- </div>
98
+ </VList>
92
99
  </PopoverContent>
93
100
  </Transition>
94
101
  </PopoverPortal>
@@ -101,7 +108,7 @@
101
108
  </template>
102
109
 
103
110
  <script lang="ts">
104
- import { useInput } from '../../composables/useInput';
111
+ import { useInput, type InputEmitsMap } from '../../composables/useInput';
105
112
  import type { MaybeArray, Size } from '../../types/props';
106
113
  import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui';
107
114
  import { useEventListener } from '@vueuse/core';
@@ -115,7 +122,13 @@ import {
115
122
  type PropType
116
123
  } from 'vue';
117
124
  import MTag from '../tag/Tag.vue';
125
+ import MClearButton from '../clearbutton/ClearButton.vue';
126
+ import { VList } from 'virtua/vue';
118
127
  import * as style from './style';
128
+
129
+ interface VListHandle {
130
+ scrollToIndex(index: number, opts?: { align?: 'start' | 'center' | 'end' | 'nearest' }): void;
131
+ }
119
132
  import { fade } from '../../styles/transitions.css';
120
133
  import { useInteractive } from '../../composables/useInteractive';
121
134
  import type { OnUpdateValue, OnUpdateValueImpl } from './types';
@@ -127,7 +140,10 @@ let shouldOpenNextFocusedSelect = false;
127
140
  export default defineComponent({
128
141
  name: 'MSelect',
129
142
  emits: {
130
- ...useInput.emits()
143
+ ...(useInput.emits() as unknown as InputEmitsMap),
144
+ 'on-open': () => true,
145
+ 'on-close': () => true,
146
+ clear: () => true
131
147
  },
132
148
  props: {
133
149
  ...useInput.props(),
@@ -184,7 +200,7 @@ export default defineComponent({
184
200
  default: false
185
201
  },
186
202
  error: {
187
- type: String
203
+ type: String as PropType<string | null>
188
204
  },
189
205
  allowBlank: {
190
206
  type: Boolean,
@@ -202,9 +218,26 @@ export default defineComponent({
202
218
  type: Boolean,
203
219
  default: false
204
220
  },
221
+ fill: {
222
+ type: Boolean,
223
+ default: false
224
+ },
225
+ noBorder: {
226
+ type: Boolean,
227
+ default: false
228
+ },
205
229
  zIndex: {
206
230
  type: Number
207
231
  },
232
+ clearable: {
233
+ type: Boolean,
234
+ default: false
235
+ },
236
+ // Value a single select resets to when cleared (e.g. a "Don't restrict" option)
237
+ clearValue: {
238
+ type: [String, Number, Boolean, Object] as PropType<string | number | boolean | object | null>,
239
+ default: undefined
240
+ },
208
241
  showDataOptions: {
209
242
  type: Boolean,
210
243
  default: true
@@ -214,6 +247,8 @@ export default defineComponent({
214
247
  },
215
248
  components: {
216
249
  MTag,
250
+ MClearButton,
251
+ VList,
217
252
  PopoverContent,
218
253
  PopoverPortal,
219
254
  PopoverRoot,
@@ -221,7 +256,7 @@ export default defineComponent({
221
256
  },
222
257
  setup(props, { emit, expose }) {
223
258
  const container = ref<HTMLElement | null>(null);
224
- const optionsRef = ref<HTMLElement[]>([]);
259
+ const virtualListRef = ref<VListHandle | null>(null);
225
260
  const searchRef = ref<HTMLInputElement>();
226
261
  const selectRef = ref<HTMLElement>();
227
262
  const popper = ref<HTMLElement>();
@@ -593,9 +628,34 @@ export default defineComponent({
593
628
  }
594
629
  }
595
630
 
596
- function removeSelected(event: MouseEvent, value: string | number) {
597
- event.preventDefault();
598
- event.stopPropagation();
631
+ const hasValue = computed(() => {
632
+ if (props.multiple) {
633
+ return Array.isArray(selected.value) && selected.value.length > 0;
634
+ }
635
+
636
+ if (props.clearValue !== undefined && selected.value === props.clearValue) {
637
+ return false;
638
+ }
639
+
640
+ return !!selected.value;
641
+ });
642
+
643
+ const showClear = computed(() => {
644
+ return props.clearable
645
+ && hasValue.value
646
+ && !disabled.value
647
+ && !loading.value;
648
+ });
649
+
650
+ function clearSelected() {
651
+ selected.value = props.multiple
652
+ ? []
653
+ : (props.clearValue !== undefined ? props.clearValue : '');
654
+ emit('clear');
655
+ container.value?.focus();
656
+ }
657
+
658
+ function removeSelected(value: string | number) {
599
659
  if (!props.multiple) {
600
660
  return;
601
661
  }
@@ -741,20 +801,36 @@ export default defineComponent({
741
801
  ]
742
802
  })
743
803
 
804
+ const optionItemHeightMap: Record<string, number> = { xs: 30, sm: 32, md: 38, lg: 42, xl: 46 };
805
+ const optionItemHeight = computed(() => optionItemHeightMap[props.size] ?? 38);
806
+ const optionsHeight = computed(() => {
807
+ const total = computedOptions.value.length * optionItemHeight.value + 8; // 8px = paddingTop+paddingBottom
808
+ return Math.min(total, 222);
809
+ });
810
+ const optionSizeClass = computed(() => style.optionSizes[props.size]);
811
+ const fillRootClass = computed(() => props.fill ? style.fillRoot : undefined);
812
+ const fillLabelClass = computed(() => props.fill ? style.fillLabel : undefined);
813
+
744
814
  const computedWrapperClasses = computed(() => {
745
- return [style.wrapper];
815
+ return [
816
+ style.wrapper,
817
+ { [style.fill]: props.fill }
818
+ ];
746
819
  });
747
820
 
748
821
  const selectComputedClasses = computed(() => {
749
822
  return [
750
823
  style.base,
824
+ style.sizes[props.size],
751
825
  {
826
+ [style.hasClear]: showClear.value,
752
827
  [style.selectedTags]:
753
828
  props.multiple &&
754
829
  Array.isArray(selected.value) &&
755
830
  selected.value.length > 0,
756
- [style.focused]: open.value || focused.value,
757
- // [`m-select--${props.size}`]: props.size,
831
+ [style.focused]: !props.noBorder && (open.value || focused.value),
832
+ [style.fillBase]: props.fill,
833
+ [style.noBorder]: props.noBorder,
758
834
  'm-select--disabled': disabled.value || loading.value,
759
835
  'm-select--focused': open.value || focused.value,
760
836
  'm-select--multiple': props.multiple,
@@ -784,21 +860,20 @@ export default defineComponent({
784
860
  if (!keyboardNavigation.value) {
785
861
  return;
786
862
  }
787
- if (typeof index !== 'undefined' && optionsRef.value) {
788
- optionsRef.value[index].scrollIntoView({
789
- block: 'nearest',
790
- inline: 'nearest'
791
- });
863
+ if (typeof index !== 'undefined') {
864
+ virtualListRef.value?.scrollToIndex(index, { align: 'nearest' });
792
865
  }
793
866
  });
794
867
 
795
868
  watch(open, (value: boolean) => {
796
869
  nextTick(() => {
797
870
  if (!value) {
871
+ emit('on-close');
798
872
  search.value = '';
799
873
  }
800
874
 
801
875
  if (value) {
876
+ emit('on-open');
802
877
  zIndex.value = nextZIndex();
803
878
  }
804
879
 
@@ -816,7 +891,7 @@ export default defineComponent({
816
891
  });
817
892
  }
818
893
 
819
- if (value && optionsRef.value) {
894
+ if (value) {
820
895
  const selectedValue = Array.isArray(selected.value)
821
896
  ? selected.value[0]
822
897
  : selected.value;
@@ -824,9 +899,7 @@ export default defineComponent({
824
899
  i => i.value === selectedValue
825
900
  );
826
901
  if (index !== -1) {
827
- optionsRef.value[index].scrollIntoView({
828
- block: 'nearest'
829
- });
902
+ nextTick(() => virtualListRef.value?.scrollToIndex(index, { align: 'nearest' }));
830
903
  selectedIndex.value = index;
831
904
  } else if (computedOptions.value.length > 0) {
832
905
  selectedIndex.value = 0;
@@ -894,7 +967,7 @@ export default defineComponent({
894
967
 
895
968
  return {
896
969
  container,
897
- optionsRef,
970
+ virtualListRef,
898
971
  searchRef,
899
972
  selectRef,
900
973
  popper,
@@ -922,13 +995,20 @@ export default defineComponent({
922
995
  getSelectedLabel,
923
996
  openSelect,
924
997
  removeSelected,
998
+ showClear,
999
+ clearSelected,
925
1000
  inputError,
926
1001
  transitionName,
927
1002
  inputListeners,
928
1003
  style,
929
1004
  focused,
930
1005
  fade,
931
- toggleAllItems
1006
+ toggleAllItems,
1007
+ optionItemHeight,
1008
+ optionsHeight,
1009
+ optionSizeClass,
1010
+ fillRootClass,
1011
+ fillLabelClass
932
1012
  };
933
1013
  }
934
1014
  });
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div style="display: flex; flex-direction: column; gap: 12px; max-width: 320px;">
3
+ <MSelect v-model="single" :options="options" clearable />
4
+ <MSelect v-model="multiple" :options="options" multiple clearable />
5
+ </div>
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import { ref } from 'vue';
10
+
11
+ const options = Array.from({ length: 10 }, (_, i) => ({
12
+ value: i + 1,
13
+ label: `Option ${i + 1}`
14
+ }));
15
+
16
+ const single = ref(2);
17
+ const multiple = ref([1, 2]);
18
+ </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <div style="display: grid; grid-template-columns: 120px 1fr 1fr; border: 1px solid #e5e7eb;">
3
+ <div style="padding: 6px 8px; font-size: 13px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb; background: #f9fafb; font-weight: 500;">Row 1</div>
4
+ <div style="height: 36px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;">
5
+ <MSelect size="xs" v-model="row1Status" :options="statusOptions" fill no-border />
6
+ </div>
7
+ <div style="height: 36px; border-bottom: 1px solid #e5e7eb;">
8
+ <MSelect size="xs" v-model="row1Priority" :options="priorityOptions" fill no-border />
9
+ </div>
10
+
11
+ <div style="padding: 6px 8px; font-size: 13px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb; background: #f9fafb; font-weight: 500;">Row 2</div>
12
+ <div style="height: 36px; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;">
13
+ <MSelect size="xs" v-model="row2Status" :options="statusOptions" fill no-border />
14
+ </div>
15
+ <div style="height: 36px; border-bottom: 1px solid #e5e7eb;">
16
+ <MSelect size="xs" v-model="row2Priority" :options="priorityOptions" fill no-border />
17
+ </div>
18
+
19
+ <div style="padding: 6px 8px; font-size: 13px; border-right: 1px solid #e5e7eb; background: #f9fafb; font-weight: 500;">Row 3</div>
20
+ <div style="height: 36px; border-right: 1px solid #e5e7eb;">
21
+ <MSelect size="xs" v-model="row3Status" :options="statusOptions" fill no-border />
22
+ </div>
23
+ <div style="height: 36px;">
24
+ <MSelect size="xs" v-model="row3Priority" :options="priorityOptions" fill no-border />
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script lang="ts" setup>
30
+ import { ref } from 'vue';
31
+
32
+ const statusOptions = [
33
+ { value: 'todo', label: 'To Do' },
34
+ { value: 'in_progress', label: 'In Progress' },
35
+ { value: 'in_review', label: 'In Review' },
36
+ { value: 'done', label: 'Done' }
37
+ ];
38
+
39
+ const priorityOptions = [
40
+ { value: 'low', label: 'Low' },
41
+ { value: 'medium', label: 'Medium' },
42
+ { value: 'high', label: 'High' },
43
+ { value: 'critical', label: 'Critical' }
44
+ ];
45
+
46
+ const row1Status = ref('in_progress');
47
+ const row1Priority = ref('high');
48
+ const row2Status = ref('todo');
49
+ const row2Priority = ref('medium');
50
+ const row3Status = ref('done');
51
+ const row3Priority = ref('low');
52
+ </script>
@@ -1,5 +1,5 @@
1
1
  import MSelect from '../Select.vue';
2
- import { MSelectBasic, MSelectSearchable, MSelectMultiple } from './index';
2
+ import { MSelectBasic, MSelectSearchable, MSelectMultiple, MSelectFill, MSelectClearable } from './index';
3
3
 
4
4
  import { createStory, sizeArgType } from '../../../__storybook__';
5
5
 
@@ -14,3 +14,5 @@ export default {
14
14
  export const Basic = createStory(MSelectBasic);
15
15
  export const Searchable = createStory(MSelectSearchable);
16
16
  export const Multiple = createStory(MSelectMultiple);
17
+ export const Fill = createStory(MSelectFill);
18
+ export const Clearable = createStory(MSelectClearable);
@@ -1,3 +1,5 @@
1
1
  export { default as MSelectBasic } from './basic.vue';
2
2
  export { default as MSelectSearchable } from './searchable.vue';
3
3
  export { default as MSelectMultiple } from './multiple.vue';
4
+ export { default as MSelectFill } from './fill.vue';
5
+ export { default as MSelectClearable } from './clearable.vue';