@teammodlr/ui-kit 3.0.0-beta.23 → 3.0.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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>
@@ -26,10 +26,10 @@
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>
@@ -71,24 +71,30 @@
71
71
  <input v-if="searchable" type="text" ref="searchRef" v-model="search" placeholder="Search"
72
72
  :class="style.searchInput" :data-input="`select-menu-search-${id}`" />
73
73
  </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>
74
+ <div v-if="computedOptions.length === 0" :class="style.noResults">No results</div>
75
+ <VList
76
+ v-else
77
+ ref="virtualListRef"
78
+ :data="computedOptions"
79
+ :item-size="optionItemHeight"
80
+ :style="{ height: optionsHeight + 'px', padding: '4px' }"
81
+ >
82
+ <template #default="{ item, index }">
83
+ <div :class="[optionSizeClass, {
84
+ [style.option]: true,
85
+ [style.optionSelected]: item.active,
86
+ [style.optionFocused]: selectedIndex === index
87
+ }]" @click.prevent="item.onClick" @mouseenter="selectedIndex = index"
88
+ :data-element="`select-menu-option-${id}-${item.value}`">
89
+ <span :class="style.optionIcon">
90
+ <svg v-if="item.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
91
+ <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>
92
+ </svg>
93
+ </span>
94
+ <span :class="style.optionLabel">{{ item.label }}</span>
95
+ </div>
90
96
  </template>
91
- </div>
97
+ </VList>
92
98
  </PopoverContent>
93
99
  </Transition>
94
100
  </PopoverPortal>
@@ -115,7 +121,12 @@ import {
115
121
  type PropType
116
122
  } from 'vue';
117
123
  import MTag from '../tag/Tag.vue';
124
+ import { VList } from 'virtua/vue';
118
125
  import * as style from './style';
126
+
127
+ interface VListHandle {
128
+ scrollToIndex(index: number, opts?: { align?: 'start' | 'center' | 'end' | 'nearest' }): void;
129
+ }
119
130
  import { fade } from '../../styles/transitions.css';
120
131
  import { useInteractive } from '../../composables/useInteractive';
121
132
  import type { OnUpdateValue, OnUpdateValueImpl } from './types';
@@ -202,6 +213,14 @@ export default defineComponent({
202
213
  type: Boolean,
203
214
  default: false
204
215
  },
216
+ fill: {
217
+ type: Boolean,
218
+ default: false
219
+ },
220
+ noBorder: {
221
+ type: Boolean,
222
+ default: false
223
+ },
205
224
  zIndex: {
206
225
  type: Number
207
226
  },
@@ -214,6 +233,7 @@ export default defineComponent({
214
233
  },
215
234
  components: {
216
235
  MTag,
236
+ VList,
217
237
  PopoverContent,
218
238
  PopoverPortal,
219
239
  PopoverRoot,
@@ -221,7 +241,7 @@ export default defineComponent({
221
241
  },
222
242
  setup(props, { emit, expose }) {
223
243
  const container = ref<HTMLElement | null>(null);
224
- const optionsRef = ref<HTMLElement[]>([]);
244
+ const virtualListRef = ref<VListHandle | null>(null);
225
245
  const searchRef = ref<HTMLInputElement>();
226
246
  const selectRef = ref<HTMLElement>();
227
247
  const popper = ref<HTMLElement>();
@@ -741,20 +761,35 @@ export default defineComponent({
741
761
  ]
742
762
  })
743
763
 
764
+ const optionItemHeightMap: Record<string, number> = { xs: 30, sm: 32, md: 38, lg: 42, xl: 46 };
765
+ const optionItemHeight = computed(() => optionItemHeightMap[props.size] ?? 38);
766
+ const optionsHeight = computed(() => {
767
+ const total = computedOptions.value.length * optionItemHeight.value + 8; // 8px = paddingTop+paddingBottom
768
+ return Math.min(total, 222);
769
+ });
770
+ const optionSizeClass = computed(() => style.optionSizes[props.size]);
771
+ const fillRootClass = computed(() => props.fill ? style.fillRoot : undefined);
772
+ const fillLabelClass = computed(() => props.fill ? style.fillLabel : undefined);
773
+
744
774
  const computedWrapperClasses = computed(() => {
745
- return [style.wrapper];
775
+ return [
776
+ style.wrapper,
777
+ { [style.fill]: props.fill }
778
+ ];
746
779
  });
747
780
 
748
781
  const selectComputedClasses = computed(() => {
749
782
  return [
750
783
  style.base,
784
+ style.sizes[props.size],
751
785
  {
752
786
  [style.selectedTags]:
753
787
  props.multiple &&
754
788
  Array.isArray(selected.value) &&
755
789
  selected.value.length > 0,
756
- [style.focused]: open.value || focused.value,
757
- // [`m-select--${props.size}`]: props.size,
790
+ [style.focused]: !props.noBorder && (open.value || focused.value),
791
+ [style.fillBase]: props.fill,
792
+ [style.noBorder]: props.noBorder,
758
793
  'm-select--disabled': disabled.value || loading.value,
759
794
  'm-select--focused': open.value || focused.value,
760
795
  'm-select--multiple': props.multiple,
@@ -784,11 +819,8 @@ export default defineComponent({
784
819
  if (!keyboardNavigation.value) {
785
820
  return;
786
821
  }
787
- if (typeof index !== 'undefined' && optionsRef.value) {
788
- optionsRef.value[index].scrollIntoView({
789
- block: 'nearest',
790
- inline: 'nearest'
791
- });
822
+ if (typeof index !== 'undefined') {
823
+ virtualListRef.value?.scrollToIndex(index, { align: 'nearest' });
792
824
  }
793
825
  });
794
826
 
@@ -816,7 +848,7 @@ export default defineComponent({
816
848
  });
817
849
  }
818
850
 
819
- if (value && optionsRef.value) {
851
+ if (value) {
820
852
  const selectedValue = Array.isArray(selected.value)
821
853
  ? selected.value[0]
822
854
  : selected.value;
@@ -824,9 +856,7 @@ export default defineComponent({
824
856
  i => i.value === selectedValue
825
857
  );
826
858
  if (index !== -1) {
827
- optionsRef.value[index].scrollIntoView({
828
- block: 'nearest'
829
- });
859
+ nextTick(() => virtualListRef.value?.scrollToIndex(index, { align: 'nearest' }));
830
860
  selectedIndex.value = index;
831
861
  } else if (computedOptions.value.length > 0) {
832
862
  selectedIndex.value = 0;
@@ -894,7 +924,7 @@ export default defineComponent({
894
924
 
895
925
  return {
896
926
  container,
897
- optionsRef,
927
+ virtualListRef,
898
928
  searchRef,
899
929
  selectRef,
900
930
  popper,
@@ -928,7 +958,12 @@ export default defineComponent({
928
958
  style,
929
959
  focused,
930
960
  fade,
931
- toggleAllItems
961
+ toggleAllItems,
962
+ optionItemHeight,
963
+ optionsHeight,
964
+ optionSizeClass,
965
+ fillRootClass,
966
+ fillLabelClass
932
967
  };
933
968
  }
934
969
  });
@@ -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 } from './index';
3
3
 
4
4
  import { createStory, sizeArgType } from '../../../__storybook__';
5
5
 
@@ -14,3 +14,4 @@ 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);
@@ -1,3 +1,4 @@
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';
@@ -6,7 +6,7 @@ import {
6
6
  } from '@vanilla-extract/css';
7
7
  import { themeVars } from '../../../styles/theme.css';
8
8
 
9
- const sizes = {
9
+ const heights = {
10
10
  xs: '26px',
11
11
  sm: '30px',
12
12
  md: '38px',
@@ -42,7 +42,7 @@ export const wrapper = style({
42
42
 
43
43
  export const base = style({
44
44
  vars: {
45
- [vars.height]: sizes.md,
45
+ [vars.height]: heights.md,
46
46
  [vars.radius]: borderRadiuses.md,
47
47
  [vars.paddingX]: themeVars.space[2],
48
48
  [vars.paddingY]: themeVars.space[0],
@@ -189,8 +189,7 @@ export const menu = style({
189
189
  borderRadius: '4px',
190
190
  border: `1px solid ${themeVars.color.gray[200]}`,
191
191
  boxShadow: themeVars.shadow.md,
192
- maxHeight: '230px',
193
- overflowY: 'auto'
192
+ overflow: 'hidden'
194
193
  });
195
194
 
196
195
  export const placeholder = style({
@@ -199,11 +198,7 @@ export const placeholder = style({
199
198
  });
200
199
 
201
200
  export const search = style({
202
- // padding: `${themeVars.space[1]} ${themeVars.space[1]} 0`,
203
201
  backgroundColor: '#FFFFFF',
204
- position: 'sticky',
205
- top: 0,
206
- zIndex: 1,
207
202
  borderBottom: `1px solid ${themeVars.color.gray[200]}`,
208
203
  display: 'flex'
209
204
  });
@@ -219,10 +214,7 @@ export const searchInput = style({
219
214
  });
220
215
 
221
216
  export const options = style({
222
- padding: themeVars.space[1],
223
- display: 'flex',
224
- flexDirection: 'column',
225
- gap: themeVars.space[0.5]
217
+ overflow: 'hidden'
226
218
  });
227
219
 
228
220
  export const option = style({
@@ -234,10 +226,7 @@ export const option = style({
234
226
  display: 'flex',
235
227
  alignItems: 'center',
236
228
  cursor: 'pointer',
237
- minHeight: '36px',
238
- fontSize: themeVars.fontSize.base,
239
- paddingBlock: themeVars.space['1.5'],
240
- paddingInline: themeVars.space['3'],
229
+ marginBottom: '2px',
241
230
  gap: themeVars.space[2],
242
231
  color: vars.optionColor,
243
232
  transition: 'background-color 0.05s ease-in-out',
@@ -280,6 +269,39 @@ export const optionSelected = style({
280
269
 
281
270
  export const optionLabel = style({});
282
271
 
272
+ export const optionSizes = styleVariants({
273
+ xs: {
274
+ minHeight: '28px',
275
+ fontSize: themeVars.fontSize.small,
276
+ paddingBlock: themeVars.space['1'],
277
+ paddingInline: themeVars.space['2']
278
+ },
279
+ sm: {
280
+ minHeight: '30px',
281
+ fontSize: themeVars.fontSize.base,
282
+ paddingBlock: themeVars.space['1'],
283
+ paddingInline: themeVars.space['2.5']
284
+ },
285
+ md: {
286
+ minHeight: '36px',
287
+ fontSize: themeVars.fontSize.base,
288
+ paddingBlock: themeVars.space['1.5'],
289
+ paddingInline: themeVars.space['3']
290
+ },
291
+ lg: {
292
+ minHeight: '40px',
293
+ fontSize: themeVars.fontSize.large,
294
+ paddingBlock: themeVars.space['2'],
295
+ paddingInline: themeVars.space['3']
296
+ },
297
+ xl: {
298
+ minHeight: '44px',
299
+ fontSize: themeVars.fontSize.large,
300
+ paddingBlock: themeVars.space['2.5'],
301
+ paddingInline: themeVars.space['3']
302
+ }
303
+ });
304
+
283
305
  export const checkAll = style({
284
306
  display: 'flex',
285
307
  alignItems: 'center',
@@ -371,12 +393,104 @@ export const arrow = style({
371
393
  height: themeVars.space[4],
372
394
  position: 'absolute',
373
395
  right: themeVars.space[3],
374
- top: themeVars.space[3],
396
+ top: '50%',
397
+ transform: 'translateY(-50%)',
375
398
  display: 'flex',
376
399
  color: themeVars.color.gray[500],
377
400
  transition: 'transform 0.3s ease',
378
401
  });
379
402
 
380
403
  export const arrowOpen = style({
381
- transform: 'rotate(180deg)',
404
+ transform: 'translateY(-50%) rotate(180deg)',
382
405
  })
406
+
407
+ export const fillRoot = style({
408
+ display: 'flex',
409
+ flexDirection: 'column',
410
+ height: '100%',
411
+ width: '100%'
412
+ });
413
+
414
+ export const fill = style({
415
+ flexGrow: 1,
416
+ width: '100%',
417
+ minHeight: 0
418
+ });
419
+
420
+ export const sizes = styleVariants({
421
+ xs: {
422
+ vars: {
423
+ [vars.height]: heights.xs,
424
+ [vars.radius]: borderRadiuses.xs,
425
+ [vars.paddingX]: themeVars.space[1]
426
+ },
427
+ fontSize: themeVars.fontSize.small
428
+ },
429
+ sm: {
430
+ vars: {
431
+ [vars.height]: heights.sm,
432
+ [vars.radius]: borderRadiuses.sm,
433
+ [vars.paddingX]: themeVars.space[1.5]
434
+ },
435
+ fontSize: themeVars.fontSize.small
436
+ },
437
+ md: {
438
+ vars: {
439
+ [vars.height]: heights.md,
440
+ [vars.radius]: borderRadiuses.md
441
+ }
442
+ },
443
+ lg: {
444
+ vars: {
445
+ [vars.height]: heights.lg,
446
+ [vars.radius]: borderRadiuses.lg,
447
+ [vars.paddingX]: themeVars.space[2.5]
448
+ },
449
+ fontSize: themeVars.fontSize.large
450
+ },
451
+ xl: {
452
+ vars: {
453
+ [vars.height]: heights.xl,
454
+ [vars.radius]: borderRadiuses.xl,
455
+ [vars.paddingX]: themeVars.space[3]
456
+ },
457
+ fontSize: themeVars.fontSize.large
458
+ }
459
+ });
460
+
461
+ export const fillBase = style({
462
+ minHeight: 'unset',
463
+ height: '100%',
464
+ lineHeight: 'normal',
465
+ display: 'flex',
466
+ alignItems: 'center',
467
+ overflow: 'hidden',
468
+ paddingRight: '2rem'
469
+ });
470
+
471
+ export const fillLabel = style({
472
+ overflow: 'hidden',
473
+ whiteSpace: 'nowrap',
474
+ textOverflow: 'ellipsis',
475
+ flex: 1,
476
+ minWidth: 0
477
+ });
478
+
479
+ export const noBorder = style({
480
+ vars: {
481
+ [vars.borderColor]: 'transparent'
482
+ },
483
+ borderRadius: 0,
484
+ boxShadow: 'none',
485
+ ':hover': {
486
+ vars: {
487
+ [vars.borderColor]: 'transparent'
488
+ }
489
+ },
490
+ ':focus': {
491
+ vars: {
492
+ [vars.borderColor]: 'transparent'
493
+ },
494
+ boxShadow: 'none'
495
+ }
496
+ });