feat(story): new select component with floating-vue (#151)

This commit is contained in:
RainbowBird authored and GitHub committed 2025-04-30 17:08:56 +08:00
1 parent 0a72441039
commit 99bc4ca7a6
4 files changed
+172 -43

No files matched your search

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { Select } from '@proj-airi/ui'
import { ref } from 'vue'
const singleValue = ref('option1')
const disabledValue = ref('option2')
const options = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
{ label: 'Option 4', value: 'option4' },
]
</script>
<template>
<Story
title="Select"
group="form"
:layout="{ type: 'grid', width: 600 }"
>
<template #controls>
<ThemeColorsHueControl />
</template>
<Variant
id="basic"
title="Basic Select"
>
<div>
<Select
v-model="singleValue"
:options="options"
placeholder="Choose an option..."
/>
</div>
</Variant>
<Variant
id="disabled"
title="Disabled Select"
>
<div>
<Select
v-model="disabledValue"
:options="options"
placeholder="Disabled select"
disabled
/>
</div>
</Variant>
<Variant
id="custom-width"
title="Custom Width"
>
<div class="w-[300px]">
<Select
v-model="singleValue"
:options="options"
placeholder="Choose an option..."
/>
</div>
</Variant>
</Story>
</template>
+1
View File
@@ -36,6 +36,7 @@
},
"dependencies": {
"@vueuse/core": "^13.1.0",
"floating-vue": "^5.2.2",
"reka-ui": "^2.2.0",
"vue": "^3.5.13"
},
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { computed, ref } from 'vue'
import { Dropdown as VDropdown } from 'floating-vue'
import { computed } from 'vue'
const props = defineProps<{
options: { label: string, value: string | number }[]
@@ -11,66 +11,92 @@ const props = defineProps<{
const modelValue = defineModel<string | number>({ required: true })
const isOpen = ref(false)
const selectedLabel = computed(() => {
const selected = props.options.find(opt => opt.value === modelValue.value)
return selected ? selected.label : props.placeholder
})
function toggleDropdown() {
if (!props.disabled) {
isOpen.value = !isOpen.value
}
}
function selectOption(value: string | number) {
modelValue.value = value
isOpen.value = false
}
// Close dropdown when clicking outside
const dropdownRef = ref<HTMLElement | null>(null)
onClickOutside(dropdownRef, () => {
isOpen.value = false
})
</script>
<template>
<div
ref="dropdownRef"
class="relative w-full font-sans"
<VDropdown
auto-size
auto-boundary-max-size
>
<button
type="button"
class="min-w-[160px] flex items-center justify-between gap-2 border rounded-lg bg-white p-2.5 text-xs text-neutral-700 shadow-sm outline-none transition-colors dark:border-neutral-800 dark:bg-neutral-900 hover:bg-neutral-50 dark:text-neutral-200 focus:shadow-[0_0_0_2px] focus:shadow-black dark:hover:bg-neutral-800"
:class="{ 'cursor-not-allowed bg-neutral-100 text-neutral-400 dark:bg-neutral-800 dark:text-neutral-600': props.disabled }"
@click="toggleDropdown"
>
<span>{{ selectedLabel }}</span>
<div i-solar:alt-arrow-down-bold-duotone class="h-3.5 w-3.5 text-neutral-500 dark:text-neutral-400" />
</button>
<div
v-if="isOpen"
class="absolute left-0 right-0 top-[calc(100%+4px)] z-10 min-w-[160px] border rounded-lg bg-white shadow-lg transition-all dark:border-neutral-800 dark:bg-neutral-900"
class="min-w-[160px] flex cursor-pointer items-center justify-between gap-2 border rounded-lg bg-white p-2.5 text-xs text-neutral-700 shadow-sm outline-none transition-colors disabled:cursor-not-allowed dark:border-neutral-800 dark:bg-neutral-900 disabled:bg-neutral-100 hover:bg-neutral-50 dark:text-neutral-200 disabled:text-neutral-400 focus:ring-2 focus:ring-black/10 dark:disabled:bg-neutral-800 dark:hover:bg-neutral-800 dark:disabled:text-neutral-600"
:class="{ 'pointer-events-none': props.disabled }"
>
<div
v-if="props.title"
class="border-b p-2 text-xs text-neutral-500 font-semibold dark:border-neutral-800"
>
{{ props.title }}
<div class="flex-1 truncate">
<slot :label="selectedLabel">
{{ selectedLabel }}
</slot>
</div>
<div class="max-h-[300px] overflow-y-auto p-1">
<div i-solar:alt-arrow-down-bold-duotone class="h-3.5 w-3.5 text-neutral-500 dark:text-neutral-400" />
</div>
<template #popper="{ hide }">
<div class="min-w-[160px] flex flex-col gap-0.5 border border-neutral-200 rounded-lg bg-white p-1 shadow-lg dark:border-neutral-800 dark:bg-neutral-900">
<div
v-for="option in props.options"
v-for="option of props.options"
v-bind="{ ...$attrs, class: null, style: null }"
:key="option.value"
class="flex cursor-pointer select-none items-center rounded p-2 text-xs text-neutral-700 transition-colors hover:bg-neutral-50 dark:text-neutral-200 dark:hover:bg-neutral-800"
:class="{ 'bg-neutral-100 dark:bg-neutral-700': modelValue === option.value }"
@click="selectOption(option.value)"
class="cursor-pointer rounded px-2 py-1.5 text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-200 dark:hover:bg-neutral-800"
:class="{
'bg-neutral-100 dark:bg-neutral-800': modelValue === option.value,
}"
@click="selectOption(option.value); hide()"
>
{{ option.label }}
</div>
</div>
</div>
</div>
</template>
</VDropdown>
</template>
<style>
.resize-observer[data-v-b329ee4c] {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
border: none;
background-color: transparent;
pointer-events: none;
display: block;
overflow: hidden;
opacity: 0;
}
.resize-observer[data-v-b329ee4c] object {
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
pointer-events: none;
z-index: -1;
}
.v-popper__popper {
z-index: 10000;
top: 0;
left: 0;
outline: none;
}
.v-popper__arrow-container {
display: none;
}
.v-popper__inner {
border: none !important;
}
</style>
+36
View File
@@ -1014,6 +1014,9 @@ importers:
'@vueuse/core':
specifier: ^13.1.0
version: 13.1.0(vue@3.5.13(typescript@5.8.3))
floating-vue:
specifier: ^5.2.2
version: 5.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.40.0))(vue@3.5.13(typescript@5.8.3))
reka-ui:
specifier: ^2.2.0
version: 2.2.0(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
@@ -2692,6 +2695,9 @@ packages:
'@floating-ui/core@1.6.8':
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
'@floating-ui/dom@1.1.1':
resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==}
'@floating-ui/dom@1.6.13':
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
@@ -7173,6 +7179,15 @@ packages:
resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
engines: {node: '>=8'}
floating-vue@5.2.2:
resolution: {integrity: sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==}
peerDependencies:
'@nuxt/kit': ^3.2.0
vue: ^3.2.0
peerDependenciesMeta:
'@nuxt/kit':
optional: true
fluent-ffmpeg@2.1.3:
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
engines: {node: '>=18'}
@@ -11330,6 +11345,11 @@ packages:
peerDependencies:
vue: ^3.0.0
vue-resize@2.0.0-alpha.1:
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
peerDependencies:
vue: ^3.0.0
vue-router@4.5.1:
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
peerDependencies:
@@ -13279,6 +13299,10 @@ snapshots:
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.1.1':
dependencies:
'@floating-ui/core': 1.6.8
'@floating-ui/dom@1.6.13':
dependencies:
'@floating-ui/core': 1.6.8
@@ -18628,6 +18652,14 @@ snapshots:
flattie@1.1.1: {}
floating-vue@5.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.40.0))(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@floating-ui/dom': 1.1.1
vue: 3.5.13(typescript@5.8.3)
vue-resize: 2.0.0-alpha.1(vue@3.5.13(typescript@5.8.3))
optionalDependencies:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.40.0)
fluent-ffmpeg@2.1.3:
dependencies:
async: 0.2.10
@@ -24102,6 +24134,10 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.13(typescript@5.8.3)
vue-resize@2.0.0-alpha.1(vue@3.5.13(typescript@5.8.3)):
dependencies:
vue: 3.5.13(typescript@5.8.3)
vue-router@4.5.1(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@vue/devtools-api': 6.6.4