mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 00:10:40 +00:00
fix: Add dropdown size control to Select components and improve UI (#5290)
* 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>
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<!--
|
||||
<!--
|
||||
Note: Unlike SingleSelect, we don't need an explicit options prop because:
|
||||
1. Our value template only shows a static label (not dynamic based on selection)
|
||||
2. We display a count badge instead of actual selected labels
|
||||
3. All PrimeVue props (including options) are passed via v-bind="$attrs"
|
||||
|
||||
option-label="name" is required because our option template directly accesses option.name
|
||||
max-selected-labels="0" is required to show count badge instead of selected item labels
|
||||
-->
|
||||
@@ -20,12 +19,13 @@
|
||||
v-if="showSearchBox || showSelectedCount || showClearButton"
|
||||
#header
|
||||
>
|
||||
<div class="p-2 flex flex-col pb-0">
|
||||
<div class="pt-2 pb-0 px-2 flex flex-col">
|
||||
<SearchBox
|
||||
v-if="showSearchBox"
|
||||
v-model="searchQuery"
|
||||
:class="showSelectedCount || showClearButton ? 'mb-2' : ''"
|
||||
:show-order="true"
|
||||
:show-border="true"
|
||||
:place-holder="searchPlaceholder"
|
||||
/>
|
||||
<div
|
||||
@@ -47,11 +47,11 @@
|
||||
:label="$t('g.clearAll')"
|
||||
type="transparent"
|
||||
size="fit-content"
|
||||
class="text-sm text-blue-500! dark-theme:text-blue-600!"
|
||||
class="text-sm text-blue-500 dark-theme:text-blue-600"
|
||||
@click.stop="selectedItems = []"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
|
||||
<div class="my-4 h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,13 +75,13 @@
|
||||
|
||||
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
||||
<template #option="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2" :style="popoverStyle">
|
||||
<div
|
||||
class="flex h-4 w-4 p-0.5 shrink-0 items-center justify-center rounded transition-all duration-200"
|
||||
:class="
|
||||
slotProps.selected
|
||||
? 'border-[3px] border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
|
||||
: 'border-[1px] border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
|
||||
? 'bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
|
||||
: 'bg-neutral-100 dark-theme:bg-zinc-700'
|
||||
"
|
||||
>
|
||||
<i-lucide:check
|
||||
@@ -89,9 +89,11 @@
|
||||
class="text-xs text-bold text-white"
|
||||
/>
|
||||
</div>
|
||||
<Button class="border-none outline-none bg-transparent" unstyled>{{
|
||||
slotProps.option.name
|
||||
}}</Button>
|
||||
<Button
|
||||
class="border-none outline-none bg-transparent text-left"
|
||||
unstyled
|
||||
>{{ slotProps.option.name }}</Button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
@@ -105,6 +107,8 @@ import MultiSelect, {
|
||||
import { computed } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import { usePopoverSizing } from '@/composables/usePopoverSizing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TextButton from '../button/TextButton.vue'
|
||||
|
||||
@@ -125,6 +129,12 @@ interface Props {
|
||||
showClearButton?: boolean
|
||||
/** Placeholder for the search input */
|
||||
searchPlaceholder?: 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
|
||||
// Note: options prop is intentionally omitted.
|
||||
// It's passed via $attrs to maximize PrimeVue API compatibility
|
||||
}
|
||||
@@ -133,7 +143,10 @@ const {
|
||||
showSearchBox = false,
|
||||
showSelectedCount = false,
|
||||
showClearButton = false,
|
||||
searchPlaceholder = 'Search...'
|
||||
searchPlaceholder = 'Search...',
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth
|
||||
} = defineProps<Props>()
|
||||
|
||||
const selectedItems = defineModel<Option[]>({
|
||||
@@ -142,10 +155,15 @@ const selectedItems = defineModel<Option[]>({
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
const selectedCount = computed(() => selectedItems.value.length)
|
||||
|
||||
const popoverStyle = usePopoverSizing({
|
||||
minWidth: popoverMinWidth,
|
||||
maxWidth: popoverMaxWidth
|
||||
})
|
||||
|
||||
const pt = computed(() => ({
|
||||
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
||||
class: [
|
||||
'relative inline-flex cursor-pointer select-none',
|
||||
'h-10 relative inline-flex cursor-pointer select-none',
|
||||
'rounded-lg bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'border-[2.5px] border-solid',
|
||||
@@ -170,16 +188,26 @@ const pt = computed(() => ({
|
||||
showSearchBox || showSelectedCount || showClearButton ? 'block' : 'hidden'
|
||||
}),
|
||||
// Overlay & list visuals unchanged
|
||||
overlay:
|
||||
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100 dark-theme:border-zinc-700',
|
||||
overlay: {
|
||||
class: cn(
|
||||
'mt-2 rounded-lg py-2 px-2',
|
||||
'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: { maxHeight: listMaxHeight },
|
||||
class: 'overflow-y-auto scrollbar-hide'
|
||||
}),
|
||||
list: {
|
||||
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
|
||||
class: 'flex flex-col gap-0 p-0 m-0 list-none border-none text-sm'
|
||||
},
|
||||
// Option row hover and focus tone
|
||||
option: ({ context }: MultiSelectPassThroughMethodOptions) => ({
|
||||
class: [
|
||||
'flex gap-1 items-center p-2',
|
||||
'hover:bg-neutral-100/50 hover:dark-theme:bg-zinc-700/50',
|
||||
'flex gap-2 items-center h-10 px-2 rounded-lg',
|
||||
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
||||
// Add focus/highlight state for keyboard navigation
|
||||
{
|
||||
'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context?.focused
|
||||
@@ -189,11 +217,11 @@ const pt = computed(() => ({
|
||||
// Hide built-in checkboxes entirely via PT (no :deep)
|
||||
pcHeaderCheckbox: {
|
||||
root: { class: 'hidden' },
|
||||
style: 'display: none !important'
|
||||
style: { display: 'none' }
|
||||
},
|
||||
pcOptionCheckbox: {
|
||||
root: { class: 'hidden' },
|
||||
style: 'display: none !important'
|
||||
style: { display: 'none' }
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user