change select ui

This commit is contained in:
Julian Freeman
2025-12-02 09:57:33 -04:00
parent 826459746c
commit ee4880a83b
2 changed files with 122 additions and 12 deletions

View File

@@ -0,0 +1,108 @@
// filepath: src/components/ui/AppSelect.vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ChevronDown, Check } from 'lucide-vue-next'
import { onClickOutside, useElementBounding } from '@vueuse/core'
interface Option {
label: string
value: string | number
}
const props = defineProps<{
modelValue: string | number
options: Option[]
disabled?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string | number): void
}>()
const isOpen = ref(false)
const target = ref<HTMLElement | null>(null) // Ref for the main div (trigger)
// Use useElementBounding to get position of the trigger element
const { x, y, width, height } = useElementBounding(target)
// Dropdown styles for fixed positioning
const dropdownStyles = computed(() => {
if (!isOpen.value) return {}
return {
top: `${y.value + height.value + 8}px`, // 8px for mt-2 from original
left: `${x.value}px`,
width: `${width.value}px`,
}
})
onClickOutside(target, () => isOpen.value = false)
const selectedLabel = computed(() => {
const option = props.options.find(o => o.value === props.modelValue)
return option ? option.label : props.modelValue
})
function select(value: string | number) {
emit('update:modelValue', value)
isOpen.value = false
}
function toggle() {
if (!props.disabled) {
isOpen.value = !isOpen.value
}
}
</script>
<template>
<div ref="target" class="relative">
<!-- Trigger -->
<div
@click="toggle"
class="flex items-center justify-between w-full px-4 py-3 bg-gray-50 dark:bg-zinc-800 rounded-xl cursor-pointer border border-transparent transition-all duration-200 outline-none"
:class="[
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100 dark:hover:bg-zinc-700',
isOpen ? 'ring-2 ring-blue-500 dark:ring-blue-500 border-transparent' : ''
]"
>
<span class="text-zinc-900 dark:text-white font-medium truncate">{{ selectedLabel }}</span>
<ChevronDown
class="w-5 h-5 text-gray-400 transition-transform duration-200"
:class="{ 'rotate-180': isOpen }"
/>
</div>
<!-- Dropdown Menu - Teleported to body to escape overflow:hidden parent -->
<Teleport to="body">
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<div
v-if="isOpen"
class="fixed bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-700 rounded-xl shadow-xl overflow-hidden z-[100] py-1"
:style="dropdownStyles"
>
<div
v-for="option in options"
:key="option.value"
@click="select(option.value)"
class="flex items-center justify-between px-4 py-2.5 cursor-pointer transition-colors text-sm"
:class="[
modelValue === option.value
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
: 'text-zinc-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-zinc-800'
]"
>
<span>{{ option.label }}</span>
<Check v-if="modelValue === option.value" class="w-4 h-4" />
</div>
</div>
</transition>
</Teleport>
</div>
</template>

View File

@@ -2,15 +2,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch } from 'vue' import { watch } from 'vue'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { Loader2, ChevronDown } from 'lucide-vue-next' import { Loader2 } from 'lucide-vue-next'
import { useQueueStore } from '../stores/queue' import { useQueueStore } from '../stores/queue'
import { useSettingsStore } from '../stores/settings' import { useSettingsStore } from '../stores/settings'
import { useAnalysisStore } from '../stores/analysis' import { useAnalysisStore } from '../stores/analysis'
import AppSelect from '../components/ui/AppSelect.vue'
const queueStore = useQueueStore() const queueStore = useQueueStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const analysisStore = useAnalysisStore() const analysisStore = useAnalysisStore()
const qualityOptions = [
{ label: 'Best Quality', value: 'best' },
{ label: '1080p', value: '1080' },
{ label: '720p', value: '720' },
{ label: '480p', value: '480' },
]
// Sync default download path if not set // Sync default download path if not set
watch(() => settingsStore.settings.download_path, (newPath) => { watch(() => settingsStore.settings.download_path, (newPath) => {
if (newPath && !analysisStore.options.output_path) { if (newPath && !analysisStore.options.output_path) {
@@ -130,17 +138,11 @@ async function startDownload() {
<!-- Quality Dropdown --> <!-- Quality Dropdown -->
<div class="relative"> <div class="relative">
<select <AppSelect
v-model="analysisStore.options.quality" v-model="analysisStore.options.quality"
class="w-full appearance-none bg-gray-50 dark:bg-zinc-800 border-none rounded-xl px-4 py-3 text-zinc-900 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none" :options="qualityOptions"
:disabled="analysisStore.options.is_audio_only" :disabled="analysisStore.options.is_audio_only"
> />
<option value="best">Best Quality</option>
<option value="1080">1080p</option>
<option value="720">720p</option>
<option value="480">480p</option>
</select>
<ChevronDown class="absolute right-4 top-3.5 w-5 h-5 text-gray-400 pointer-events-none" />
</div> </div>
</div> </div>
</div> </div>