feat(story): new select component with floating-vue (#151)
This commit is contained in:
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>
|
||||
@@ -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>
|
||||
Generated
+36
@@ -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
|
||||
|
||||
Reference in new issue
Block a user