mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-02 20:22:08 +00:00
* feature: size adjust * feature: design adjust * fix: popover width, height added * fix: li style override * refactor: improve component readability and maintainability per PR feedback - Replace CardGridList component with createGridStyle utility function - Add runtime validation for grid column values - Remove !important usage in MultiSelect, use cn() function instead - Extract popover sizing logic into usePopoverSizing composable - Improve class string readability by splitting into logical groups - Use Tailwind size utilities (size-8, size-10) instead of separate width/height - Remove magic numbers in SearchBox, align with button sizes - Rename BaseWidgetLayout to BaseModalLayout for clarity - Enhance SearchBox click area to cover entire component - Refactor long class strings using cn() utility across components * fix: BaseWidgetLayout => BaseModalLayout * fix: CardGrid deleted * fix: unused exported types * Update test expectations [skip ci] * chore: code review * Update test expectations [skip ci] * chore: restore screenshot --------- Co-authored-by: github-actions <github-actions@github.com>
186 lines
5.4 KiB
Vue
186 lines
5.4 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"
|
|
>
|
|
<!-- 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 Select, { SelectPassThroughMethodOptions } from 'primevue/select'
|
|
import { computed } from 'vue'
|
|
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
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?: {
|
|
name: string
|
|
value: string
|
|
}[]
|
|
/** 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 })
|
|
|
|
/**
|
|
* 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<{ name: string; value: string }>) => ({
|
|
class: [
|
|
// container
|
|
'h-10 relative inline-flex cursor-pointer select-none items-center',
|
|
// trigger surface
|
|
'rounded-md',
|
|
'bg-transparent text-neutral dark-theme:text-white',
|
|
'border-0',
|
|
// 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<{ name: string; value: string }>) => ({
|
|
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>
|