mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-28 17:05:14 +00:00
Compare commits
2 Commits
main
...
rizumu/fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
452729aa7d | ||
|
|
260fe4db34 |
@@ -29,4 +29,16 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.width-collapse-enter-active,
|
||||
.width-collapse-leave-active {
|
||||
transition: max-width 150ms ease;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.width-collapse-enter-from,
|
||||
.width-collapse-leave-to {
|
||||
max-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,11 @@ async function selectTopSearchResult() {
|
||||
function handleSearchEnter() {
|
||||
void selectTopSearchResult()
|
||||
}
|
||||
|
||||
function showPicker() {
|
||||
triggerRef.value!.showPicker()
|
||||
closeDropdown()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -302,6 +307,7 @@ function handleSearchEnter() {
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:ownership-selected="ownershipSelected"
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:uploadable
|
||||
:filter-options
|
||||
:sort-options
|
||||
:show-ownership-filter
|
||||
@@ -317,6 +323,7 @@ function handleSearchEnter() {
|
||||
@close="closeDropdown"
|
||||
@search-enter="handleSearchEnter"
|
||||
@item-click="handleSelection"
|
||||
@show-picker="showPicker"
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
@@ -42,12 +42,29 @@ const theButtonStyle = computed(() =>
|
||||
)
|
||||
|
||||
const buttonRef = ref<HTMLButtonElement>()
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
|
||||
function focus() {
|
||||
buttonRef.value?.focus()
|
||||
}
|
||||
|
||||
defineExpose({ focus })
|
||||
/**
|
||||
* Open the native file picker without a user click on the input itself.
|
||||
* Must be invoked synchronously from a user-initiated event handler so the
|
||||
* browser's transient activation requirement is satisfied. Falls back to
|
||||
* `click()` on browsers that predate showPicker (Chrome <99, Firefox <101,
|
||||
* Safari <16).
|
||||
*/
|
||||
function showPicker() {
|
||||
const input = fileInputRef.value!
|
||||
if (typeof input.showPicker === 'function') {
|
||||
input.showPicker()
|
||||
} else {
|
||||
input.click()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ focus, showPicker })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -102,6 +119,7 @@ defineExpose({ focus })
|
||||
>
|
||||
<i class="icon-[lucide--folder-search] size-4" aria-hidden="true" />
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
class="absolute inset-0 -z-1 opacity-0"
|
||||
:aria-label="t('g.upload')"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import FormDropdownMenu from './FormDropdownMenu.vue'
|
||||
@@ -24,6 +25,7 @@ describe('FormDropdownMenu', () => {
|
||||
const defaultProps = {
|
||||
items: [createItem('1', 'Item 1'), createItem('2', 'Item 2')],
|
||||
isSelected: () => false,
|
||||
uploadable: false,
|
||||
filterOptions: [],
|
||||
sortOptions: []
|
||||
}
|
||||
@@ -131,6 +133,50 @@ describe('FormDropdownMenu', () => {
|
||||
expect(event.defaultPrevented).toBe(true)
|
||||
})
|
||||
|
||||
/** Stub that surfaces `uploadable` as a data attribute and exposes a button
|
||||
* that emits `show-picker`, so the parent's prop-forwarding and event
|
||||
* re-emission can be asserted from the DOM. */
|
||||
const FormDropdownMenuActionsStub = {
|
||||
name: 'FormDropdownMenuActions',
|
||||
props: ['uploadable'],
|
||||
emits: ['show-picker', 'search-enter'],
|
||||
template:
|
||||
'<button data-testid="actions-stub" :data-uploadable="String(uploadable)" @click="$emit(\'show-picker\')" />'
|
||||
}
|
||||
|
||||
it('forwards uploadable prop to FormDropdownMenuActions', () => {
|
||||
render(FormDropdownMenu, {
|
||||
props: { ...defaultProps, uploadable: true },
|
||||
global: {
|
||||
stubs: {
|
||||
FormDropdownMenuFilter: true,
|
||||
FormDropdownMenuActions: FormDropdownMenuActionsStub,
|
||||
VirtualGrid: VirtualGridStub
|
||||
},
|
||||
mocks: { $t: (key: string) => key }
|
||||
}
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('actions-stub').dataset.uploadable).toBe('true')
|
||||
})
|
||||
|
||||
it('re-emits show-picker when FormDropdownMenuActions emits it', async () => {
|
||||
const { emitted } = render(FormDropdownMenu, {
|
||||
props: { ...defaultProps, uploadable: true },
|
||||
global: {
|
||||
stubs: {
|
||||
FormDropdownMenuFilter: true,
|
||||
FormDropdownMenuActions: FormDropdownMenuActionsStub,
|
||||
VirtualGrid: VirtualGridStub
|
||||
},
|
||||
mocks: { $t: (key: string) => key }
|
||||
}
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByTestId('actions-stub'))
|
||||
expect(emitted('show-picker')).toHaveLength(1)
|
||||
})
|
||||
|
||||
/** Vertical scrolling must remain native so the dropdown's own scroll
|
||||
* container can scroll its content. */
|
||||
it('does not suppress vertical scroll', () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||
interface Props {
|
||||
items: FormDropdownItem[]
|
||||
isSelected: (item: FormDropdownItem, index: number) => boolean
|
||||
uploadable: boolean
|
||||
filterOptions: FilterOption[]
|
||||
sortOptions: SortOption[]
|
||||
showOwnershipFilter?: boolean
|
||||
@@ -32,6 +33,7 @@ interface Props {
|
||||
const {
|
||||
items,
|
||||
isSelected,
|
||||
uploadable,
|
||||
filterOptions,
|
||||
sortOptions,
|
||||
showOwnershipFilter,
|
||||
@@ -44,6 +46,7 @@ const {
|
||||
const emit = defineEmits<{
|
||||
(e: 'item-click', item: FormDropdownItem, index: number): void
|
||||
(e: 'search-enter'): void
|
||||
(e: 'show-picker'): void
|
||||
}>()
|
||||
|
||||
const filterSelected = defineModel<string>('filterSelected')
|
||||
@@ -136,7 +139,9 @@ const onWheel = (event: WheelEvent) => {
|
||||
:show-base-model-filter
|
||||
:base-model-options
|
||||
:candidate-label
|
||||
:uploadable
|
||||
@search-enter="emit('search-enter')"
|
||||
@show-picker="emit('show-picker')"
|
||||
/>
|
||||
<div
|
||||
v-if="items.length === 0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -16,16 +16,18 @@ import type { LayoutMode, SortOption } from './types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
const { uploadable } = defineProps<{
|
||||
sortOptions: SortOption[]
|
||||
showOwnershipFilter?: boolean
|
||||
ownershipOptions?: OwnershipFilterOption[]
|
||||
showBaseModelFilter?: boolean
|
||||
baseModelOptions?: FilterOption[]
|
||||
candidateLabel?: string
|
||||
uploadable: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-enter'): void
|
||||
(e: 'show-picker'): void
|
||||
}>()
|
||||
|
||||
const layoutMode = defineModel<LayoutMode>('layoutMode')
|
||||
@@ -104,264 +106,296 @@ function handleSearchEnter(event: KeyboardEvent) {
|
||||
event.preventDefault()
|
||||
emit('search-enter')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide file upload button during search. This allows more space for the search box.
|
||||
*/
|
||||
const showUploadButtonArea = computed(() => {
|
||||
return (
|
||||
uploadable && (!searchQuery.value || searchQuery.value.trim().length === 0)
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-secondary flex gap-2 px-4">
|
||||
<AsyncSearchInput
|
||||
v-model="searchQuery"
|
||||
autofocus
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'hover:outline-component-node-widget-background-highlighted/80',
|
||||
'focus-within:ring-0 focus-within:outline-component-node-widget-background-highlighted/80'
|
||||
)
|
||||
"
|
||||
@enter="handleSearchEnter"
|
||||
/>
|
||||
<span
|
||||
v-if="candidateLabel"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
class="sr-only"
|
||||
>
|
||||
{{ t('widgets.uploadSelect.topResult', { result: candidateLabel }) }}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
ref="sortTriggerRef"
|
||||
:aria-label="t('assetBrowser.sortBy')"
|
||||
:title="t('assetBrowser.sortBy')"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'relative w-8 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="toggleSortPopover"
|
||||
>
|
||||
<div
|
||||
v-if="sortSelected !== 'default'"
|
||||
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
|
||||
/>
|
||||
<i class="icon-[lucide--arrow-up-down] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
ref="sortPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isSortPopoverOpen = false"
|
||||
>
|
||||
<div
|
||||
<Transition name="width-collapse">
|
||||
<div v-if="showUploadButtonArea" class="text-secondary flex gap-2">
|
||||
<Button
|
||||
:aria-label="t('g.upload')"
|
||||
:title="t('g.upload')"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'relative flex items-center justify-center gap-2 p-2 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="emit('show-picker')"
|
||||
>
|
||||
<i class="icon-[lucide--folder-search] size-4" />
|
||||
<span>{{ $t('g.upload') }}</span>
|
||||
</Button>
|
||||
<div class="h-6 w-px self-center bg-node-component-border" />
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="text-secondary flex flex-1 gap-2">
|
||||
<AsyncSearchInput
|
||||
v-model="searchQuery"
|
||||
autofocus
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-32 flex-col gap-2 p-2',
|
||||
'bg-component-node-background',
|
||||
'rounded-lg outline -outline-offset-1 outline-component-node-border'
|
||||
actionButtonStyle,
|
||||
'hover:outline-component-node-widget-background-highlighted/80',
|
||||
'focus-within:ring-0 focus-within:outline-component-node-widget-background-highlighted/80'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-for="item of sortOptions"
|
||||
:key="item.name"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="handleSortSelected(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<i
|
||||
v-if="sortSelected === item.id"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
v-if="showOwnershipFilter && ownershipOptions?.length"
|
||||
ref="ownershipTriggerRef"
|
||||
:aria-label="t('assetBrowser.ownership')"
|
||||
:title="t('assetBrowser.ownership')"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'relative w-8 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="toggleOwnershipPopover"
|
||||
>
|
||||
<div
|
||||
v-if="ownershipSelected !== 'all'"
|
||||
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
|
||||
@enter="handleSearchEnter"
|
||||
/>
|
||||
<i class="icon-[lucide--user] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
ref="ownershipPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isOwnershipPopoverOpen = false"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-32 flex-col gap-2 p-2',
|
||||
'bg-component-node-background',
|
||||
'rounded-lg outline -outline-offset-1 outline-component-node-border'
|
||||
)
|
||||
"
|
||||
<span
|
||||
v-if="candidateLabel"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
class="sr-only"
|
||||
>
|
||||
<Button
|
||||
v-for="item of ownershipOptions"
|
||||
:key="item.value"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="handleOwnershipSelected(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<i
|
||||
v-if="ownershipSelected === item.value"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
{{ t('widgets.uploadSelect.topResult', { result: candidateLabel }) }}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
v-if="showBaseModelFilter && baseModelOptions?.length"
|
||||
ref="baseModelTriggerRef"
|
||||
:aria-label="t('assetBrowser.baseModel')"
|
||||
:title="t('assetBrowser.baseModel')"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'relative w-8 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="toggleBaseModelPopover"
|
||||
>
|
||||
<div
|
||||
v-if="baseModelSelected.size > 0"
|
||||
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
|
||||
/>
|
||||
<i class="icon-[comfy--ai-model] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
ref="baseModelPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isBaseModelPopoverOpen = false"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-32 flex-col gap-2 p-2',
|
||||
'bg-component-node-background',
|
||||
'rounded-lg outline -outline-offset-1 outline-component-node-border'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-for="item of baseModelOptions"
|
||||
:key="item.value"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="toggleBaseModelSelection(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<i
|
||||
v-if="baseModelSelected.has(item.value)"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<span class="h-0 w-full border-b border-border-default" />
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="baseModelSelected = new Set()"
|
||||
>
|
||||
{{ t('g.clearFilters') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'flex items-center justify-center gap-1 p-1 hover:outline-component-node-widget-background-highlighted'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:aria-label="t('assetBrowser.listView')"
|
||||
:title="t('assetBrowser.listView')"
|
||||
ref="sortTriggerRef"
|
||||
:aria-label="t('assetBrowser.sortBy')"
|
||||
:title="t('assetBrowser.sortBy')"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
layoutSwitchItemStyle,
|
||||
layoutMode === 'list' && 'bg-neutral-500/50 text-base-foreground'
|
||||
actionButtonStyle,
|
||||
'relative w-8 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="layoutMode = 'list'"
|
||||
@click="toggleSortPopover"
|
||||
>
|
||||
<i class="icon-[lucide--list] size-4" />
|
||||
<div
|
||||
v-if="sortSelected !== 'default'"
|
||||
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
|
||||
/>
|
||||
<i class="icon-[lucide--arrow-up-down] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
ref="sortPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isSortPopoverOpen = false"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-32 flex-col gap-2 p-2',
|
||||
'bg-component-node-background',
|
||||
'rounded-lg outline -outline-offset-1 outline-component-node-border'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-for="item of sortOptions"
|
||||
:key="item.name"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="handleSortSelected(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<i
|
||||
v-if="sortSelected === item.id"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
:aria-label="t('assetBrowser.gridView')"
|
||||
:title="t('assetBrowser.gridView')"
|
||||
v-if="showOwnershipFilter && ownershipOptions?.length"
|
||||
ref="ownershipTriggerRef"
|
||||
:aria-label="t('assetBrowser.ownership')"
|
||||
:title="t('assetBrowser.ownership')"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
layoutSwitchItemStyle,
|
||||
layoutMode === 'grid' && 'bg-neutral-500/50 text-base-foreground'
|
||||
actionButtonStyle,
|
||||
'relative w-8 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="layoutMode = 'grid'"
|
||||
@click="toggleOwnershipPopover"
|
||||
>
|
||||
<i class="icon-[lucide--layout-grid] size-4" />
|
||||
<div
|
||||
v-if="ownershipSelected !== 'all'"
|
||||
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
|
||||
/>
|
||||
<i class="icon-[lucide--user] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
ref="ownershipPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isOwnershipPopoverOpen = false"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-32 flex-col gap-2 p-2',
|
||||
'bg-component-node-background',
|
||||
'rounded-lg outline -outline-offset-1 outline-component-node-border'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-for="item of ownershipOptions"
|
||||
:key="item.value"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="handleOwnershipSelected(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<i
|
||||
v-if="ownershipSelected === item.value"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
v-if="showBaseModelFilter && baseModelOptions?.length"
|
||||
ref="baseModelTriggerRef"
|
||||
:aria-label="t('assetBrowser.baseModel')"
|
||||
:title="t('assetBrowser.baseModel')"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'relative w-8 hover:outline-component-node-widget-background-highlighted active:scale-95'
|
||||
)
|
||||
"
|
||||
@click="toggleBaseModelPopover"
|
||||
>
|
||||
<div
|
||||
v-if="baseModelSelected.size > 0"
|
||||
class="absolute top-[-2px] left-[-2px] size-2 rounded-full bg-component-node-widget-background-highlighted"
|
||||
/>
|
||||
<i class="icon-[comfy--ai-model] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
ref="baseModelPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isBaseModelPopoverOpen = false"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-32 flex-col gap-2 p-2',
|
||||
'bg-component-node-background',
|
||||
'rounded-lg outline -outline-offset-1 outline-component-node-border'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-for="item of baseModelOptions"
|
||||
:key="item.value"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="toggleBaseModelSelection(item)"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<i
|
||||
v-if="baseModelSelected.has(item.value)"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<span class="h-0 w-full border-b border-border-default" />
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="cn('flex h-6 items-center justify-between text-left')"
|
||||
@click="baseModelSelected = new Set()"
|
||||
>
|
||||
{{ t('g.clearFilters') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'flex items-center justify-center gap-1 p-1 hover:outline-component-node-widget-background-highlighted'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:aria-label="t('assetBrowser.listView')"
|
||||
:title="t('assetBrowser.listView')"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="
|
||||
cn(
|
||||
layoutSwitchItemStyle,
|
||||
layoutMode === 'list' && 'bg-neutral-500/50 text-base-foreground'
|
||||
)
|
||||
"
|
||||
@click="layoutMode = 'list'"
|
||||
>
|
||||
<i class="icon-[lucide--list] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
:aria-label="t('assetBrowser.gridView')"
|
||||
:title="t('assetBrowser.gridView')"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:class="
|
||||
cn(
|
||||
layoutSwitchItemStyle,
|
||||
layoutMode === 'grid' && 'bg-neutral-500/50 text-base-foreground'
|
||||
)
|
||||
"
|
||||
@click="layoutMode = 'grid'"
|
||||
>
|
||||
<i class="icon-[lucide--layout-grid] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user