/ app / components / Ui / Select.vue
Select.vue
  1  <template>
  2    <div
  3      class="select-container"
  4      @click="toggleDropdown"
  5      @blur="closeDropdown"
  6      tabindex="0">
  7      <div class="dropdown-menu" v-show="isOpen">
  8        <div
  9          v-for="(item, index) in items"
 10          :key="index"
 11          class="dropdown-item"
 12          :class="{ selected: selectedItem && selectedItem.value === item.value }"
 13          @click.stop="selectItem(item)">
 14          {{ item.label }}
 15          <div class="key-container" v-if="item.key">
 16            <template v-if="item.key.includes('+')">
 17              <span v-for="(part, index) in item.key.split('+')" :key="index">
 18                <UiKey :keyName="part" />
 19              </span>
 20            </template>
 21            <template v-else>
 22              <UiKey :keyName="item.key" />
 23            </template>
 24          </div>
 25        </div>
 26      </div>
 27      <div class="selected-item">
 28        {{ selectedItem ? selectedItem.label : placeholder }}
 29        <LucideChevronsDownUp :size="16" v-if="isOpen" />
 30        <LucideChevronsUpDown :size="16" v-else />
 31      </div>
 32    </div>
 33  </template>
 34  
 35  <script setup lang="ts" generic="T">
 36  import { ref, computed } from "vue";
 37  import { LucideChevronsDownUp, LucideChevronsUpDown } from "lucide-vue-next";
 38  
 39  interface SelectItem<V> {
 40    label: string;
 41    value: V;
 42    key?: string;
 43  }
 44  
 45  const props = defineProps<{
 46    items: SelectItem<T>[];
 47    modelValue?: T;
 48    placeholder?: string;
 49  }>();
 50  
 51  const emit = defineEmits<{
 52    "update:modelValue": [value: T];
 53  }>();
 54  
 55  const isOpen = ref(false);
 56  
 57  const selectedItem = computed<SelectItem<T> | null>(() => {
 58    if (props.modelValue !== undefined) {
 59      return props.items.find((item) => item.value === props.modelValue) ?? null;
 60    }
 61    return null;
 62  });
 63  
 64  const toggleDropdown = () => {
 65    isOpen.value = !isOpen.value;
 66  };
 67  
 68  const closeDropdown = () => {
 69    isOpen.value = false;
 70  };
 71  
 72  const selectItem = (item: SelectItem<T>) => {
 73    emit("update:modelValue", item.value);
 74    isOpen.value = false;
 75  };
 76  </script>
 77  
 78  <style scoped>
 79  .select-container {
 80    position: relative;
 81    cursor: pointer;
 82    user-select: none;
 83    outline: none;
 84    display: flex;
 85    flex-direction: column;
 86    gap: 8px;
 87  }
 88  
 89  .selected-item {
 90    display: flex;
 91    align-items: center;
 92    gap: 8px;
 93  }
 94  
 95  .dropdown-menu {
 96    position: absolute;
 97    bottom: 100%;
 98    right: 0;
 99    background-color: var(--background);
100    z-index: 10;
101    display: flex;
102    flex-direction: column;
103    margin-bottom: 8px;
104    margin-right: -8px;
105    border: 1px solid var(--border);
106  }
107  
108  .dropdown-item {
109    display: flex;
110    justify-content: space-between;
111    align-items: center;
112    width: 146px;
113    padding: 8px;
114  }
115  
116  .dropdown-item:hover {
117    background-color: var(--element);
118  }
119  
120  .key-container {
121    display: flex;
122    align-items: center;
123    gap: 2px;
124  }
125  </style>