mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-05 13:10:24 +00:00
* lint: turn on type import rules setting up for verbatimModuleSyntax * lint: --fix for type imports
192 lines
5.8 KiB
Vue
192 lines
5.8 KiB
Vue
<template>
|
|
<!--
|
|
Note: We explicitly pass options here (not just via $attrs) because:
|
|
1. Our custom value template needs options to look up labels from values
|
|
2. PrimeVue's value slot only provides 'value' and 'placeholder', not the selected item's label
|
|
3. We need to maintain the icon slot functionality in the value template
|
|
option-label="name" is required because our option template directly accesses option.name
|
|
-->
|
|
<Select
|
|
v-model="selectedItem"
|
|
v-bind="$attrs"
|
|
:options="options"
|
|
option-label="name"
|
|
option-value="value"
|
|
unstyled
|
|
:pt="pt"
|
|
:aria-label="label || t('g.singleSelectDropdown')"
|
|
role="combobox"
|
|
:aria-expanded="false"
|
|
aria-haspopup="listbox"
|
|
:tabindex="0"
|
|
>
|
|
<!-- Trigger value -->
|
|
<template #value="slotProps">
|
|
<div class="flex items-center gap-2 text-sm text-neutral-500">
|
|
<slot name="icon" />
|
|
<span
|
|
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
|
class="text-zinc-700 dark-theme:text-gray-200"
|
|
>
|
|
{{ getLabel(slotProps.value) }}
|
|
</span>
|
|
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
|
|
{{ label }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Trigger caret -->
|
|
<template #dropdownicon>
|
|
<i-lucide:chevron-down class="text-base text-neutral-500" />
|
|
</template>
|
|
|
|
<!-- Option row -->
|
|
<template #option="{ option, selected }">
|
|
<div
|
|
class="flex items-center justify-between gap-3 w-full"
|
|
:style="optionStyle"
|
|
>
|
|
<span class="truncate">{{ option.name }}</span>
|
|
<i-lucide:check
|
|
v-if="selected"
|
|
class="text-neutral-600 dark-theme:text-white"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Select>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { SelectPassThroughMethodOptions } from 'primevue/select'
|
|
import Select from 'primevue/select'
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
import type { SelectOption } from './types'
|
|
|
|
defineOptions({
|
|
inheritAttrs: false
|
|
})
|
|
|
|
const {
|
|
label,
|
|
options,
|
|
listMaxHeight = '28rem',
|
|
popoverMinWidth,
|
|
popoverMaxWidth
|
|
} = defineProps<{
|
|
label?: string
|
|
/**
|
|
* Required for displaying the selected item's label.
|
|
* Cannot rely on $attrs alone because we need to access options
|
|
* in getLabel() to map values to their display names.
|
|
*/
|
|
options?: SelectOption[]
|
|
/** Maximum height of the dropdown panel (default: 28rem) */
|
|
listMaxHeight?: string
|
|
/** Minimum width of the popover (default: auto) */
|
|
popoverMinWidth?: string
|
|
/** Maximum width of the popover (default: auto) */
|
|
popoverMaxWidth?: string
|
|
}>()
|
|
|
|
const selectedItem = defineModel<string | null>({ required: true })
|
|
|
|
const { t } = useI18n()
|
|
|
|
/**
|
|
* Maps a value to its display label.
|
|
* Necessary because PrimeVue's value slot doesn't provide the selected item's label,
|
|
* only the raw value. We need this to show the correct text when an item is selected.
|
|
*/
|
|
const getLabel = (val: string | null | undefined) => {
|
|
if (val == null) return label ?? ''
|
|
if (!options) return label ?? ''
|
|
const found = options.find((o) => o.value === val)
|
|
return found ? found.name : label ?? ''
|
|
}
|
|
|
|
// Extract complex style logic from template
|
|
const optionStyle = computed(() => {
|
|
if (!popoverMinWidth && !popoverMaxWidth) return undefined
|
|
|
|
const styles: string[] = []
|
|
if (popoverMinWidth) styles.push(`min-width: ${popoverMinWidth}`)
|
|
if (popoverMaxWidth) styles.push(`max-width: ${popoverMaxWidth}`)
|
|
|
|
return styles.join('; ')
|
|
})
|
|
|
|
/**
|
|
* Unstyled + PT API only
|
|
* - No background/border (same as page background)
|
|
* - Text/icon scale: compact size matching MultiSelect
|
|
*/
|
|
const pt = computed(() => ({
|
|
root: ({ props }: SelectPassThroughMethodOptions<SelectOption>) => ({
|
|
class: [
|
|
// container
|
|
'h-10 relative inline-flex cursor-pointer select-none items-center',
|
|
// trigger surface
|
|
'rounded-lg',
|
|
'bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
|
|
'border-[2.5px] border-solid border-transparent',
|
|
'transition-all duration-200 ease-in-out',
|
|
'focus-within:border-blue-400 dark-theme:focus-within:border-blue-500',
|
|
// disabled
|
|
{ 'opacity-60 cursor-default': props.disabled }
|
|
]
|
|
}),
|
|
label: {
|
|
class:
|
|
// Align with MultiSelect labelContainer spacing
|
|
'flex-1 flex items-center overflow-hidden whitespace-nowrap pl-4 py-2 outline-hidden'
|
|
},
|
|
dropdown: {
|
|
class:
|
|
// Right chevron touch area
|
|
'flex shrink-0 items-center justify-center px-3 py-2'
|
|
},
|
|
overlay: {
|
|
class: cn(
|
|
'mt-2 p-2 rounded-lg',
|
|
'bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
|
|
'border border-solid border-neutral-200 dark-theme:border-zinc-700'
|
|
)
|
|
},
|
|
listContainer: () => ({
|
|
style: `max-height: ${listMaxHeight}`,
|
|
class: 'overflow-y-auto scrollbar-hide'
|
|
}),
|
|
list: {
|
|
class:
|
|
// Same list tone/size as MultiSelect
|
|
'flex flex-col gap-0 p-0 m-0 list-none border-none text-sm'
|
|
},
|
|
option: ({ context }: SelectPassThroughMethodOptions<SelectOption>) => ({
|
|
class: [
|
|
// Row layout
|
|
'flex items-center justify-between gap-3 px-2 py-3 rounded',
|
|
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
|
// Selected state + check icon
|
|
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected },
|
|
// Add focus state for keyboard navigation
|
|
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.focused }
|
|
]
|
|
}),
|
|
optionLabel: {
|
|
class: 'truncate'
|
|
},
|
|
optionGroupLabel: {
|
|
class:
|
|
'px-3 py-2 text-xs uppercase tracking-wide text-zinc-500 dark-theme:text-zinc-400'
|
|
},
|
|
emptyMessage: {
|
|
class: 'px-3 py-2 text-sm text-zinc-500 dark-theme:text-zinc-400'
|
|
}
|
|
}))
|
|
</script>
|