Files
BetiX/resources/js/components/ui/select.vue
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

130 lines
4.8 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import { ChevronDown, Check } from 'lucide-vue-next';
const props = defineProps<{
modelValue?: string | number;
options?: { label: string; value: string | number; icon?: string }[];
placeholder?: string;
id?: string;
required?: boolean;
disabled?: boolean;
}>();
const emit = defineEmits(['update:modelValue']);
const isOpen = ref(false);
const containerRef = ref<HTMLElement | null>(null);
const toggle = () => {
if (!props.disabled) {
isOpen.value = !isOpen.value;
}
};
const select = (value: string | number) => {
emit('update:modelValue', value);
isOpen.value = false;
};
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
isOpen.value = false;
}
};
// Re-init icons when dropdown opens
watch(isOpen, (val) => {
if (val) {
nextTick(() => {
if ((window as any).lucide) (window as any).lucide.createIcons();
});
}
});
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside);
});
// Helper to get label for current value
const currentLabel = () => {
if (!props.options) return props.modelValue;
const opt = props.options.find(o => o.value === props.modelValue);
return opt ? opt.label : props.placeholder || 'Select...';
};
// Helper to get icon for current value (optional, to show icon in trigger)
const currentIcon = () => {
if (!props.options) return null;
const opt = props.options.find(o => o.value === props.modelValue);
return opt ? opt.icon : null;
};
// Watch modelValue to update trigger icon
watch(() => props.modelValue, () => {
nextTick(() => {
if ((window as any).lucide) (window as any).lucide.createIcons();
});
});
</script>
<template>
<div class="relative w-full" ref="containerRef">
<!-- Trigger -->
<div
class="flex h-10 w-full items-center justify-between rounded-md border border-[#151515] bg-[#0a0a0a] px-3 py-2 text-sm text-white shadow-sm cursor-pointer transition-all hover:border-[#333]"
:class="{ 'ring-1 ring-[#00f2ff] border-[#00f2ff]': isOpen, 'opacity-50 cursor-not-allowed': disabled }"
@click="toggle"
>
<span class="flex items-center gap-2" :class="{ 'text-[#666]': !modelValue }">
<i v-if="currentIcon()" :data-lucide="currentIcon()" class="w-4 h-4"></i>
{{ currentLabel() }}
</span>
<ChevronDown class="h-4 w-4 text-[#666] transition-transform duration-200" :class="{ 'rotate-180': isOpen }" />
</div>
<!-- Dropdown -->
<transition name="fade-scale">
<div v-if="isOpen" class="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-[#222] bg-[#0a0a0a] py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none custom-scrollbar">
<div v-if="!options || options.length === 0" class="px-4 py-2 text-sm text-[#666]">No options</div>
<div
v-for="opt in options"
:key="opt.value"
class="relative flex cursor-pointer select-none items-center py-2 pl-3 pr-9 text-sm text-[#ccc] hover:bg-[#151515] hover:text-white transition-colors"
:class="{ 'bg-[#111] text-white': modelValue === opt.value }"
@click="select(opt.value)"
>
<span class="flex items-center gap-2 truncate">
<i v-if="opt.icon" :data-lucide="opt.icon" class="w-4 h-4"></i>
{{ opt.label }}
</span>
<span v-if="modelValue === opt.value" class="absolute inset-y-0 right-0 flex items-center pr-4 text-[#00f2ff]">
<Check class="h-4 w-4" />
</span>
</div>
</div>
</transition>
<!-- Hidden Native Select for Form Submission/Validation if needed -->
<select :id="id" :value="modelValue" class="sr-only" :required="required" :disabled="disabled">
<option v-for="opt in options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
</div>
</template>
<style scoped>
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-track { background: #0a0a0a; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #222; border-radius: 3px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #333; }
.fade-scale-enter-active, .fade-scale-leave-active { transition: all 0.15s ease-out; }
.fade-scale-enter-from, .fade-scale-leave-to { opacity: 0; transform: scale(0.95) translateY(-5px); }
</style>