Style: Larger Node Text, More Sidebar Alignment (#7223)
## Summary See what it looks like. How it feels. What do you think? - Also was able to unify down to a single SearchBox component. ## Changes - Bigger widget / slot labels - Smaller header text - Unified Searchboxes across sidebar tabs ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7223-Style-prototype-with-larger-node-text-2c36d73d365081f8a371c86f178fa1ff) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
@@ -98,7 +98,6 @@
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
|
||||
--color-interface-panel-job-progress-primary: var(--color-azure-300);
|
||||
--color-interface-panel-job-progress-secondary: var(--color-alpha-azure-600-30);
|
||||
@@ -438,7 +437,11 @@
|
||||
--color-interface-button-hover-surface: var(
|
||||
--interface-button-hover-surface
|
||||
);
|
||||
--color-comfy-input: var(--comfy-input-bg);
|
||||
--color-comfy-input-foreground: var(--input-text);
|
||||
--color-comfy-menu-bg: var(--comfy-menu-bg);
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
|
||||
--color-interface-stroke: var(--interface-stroke);
|
||||
--color-nav-background: var(--nav-background);
|
||||
--color-node-border: var(--node-border);
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import SearchBox from './SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
interface GenericMeta<C> extends Omit<Meta<C>, 'component'> {
|
||||
component: Omit<ComponentExposed<C>, 'focus'>
|
||||
}
|
||||
|
||||
const meta: Meta<typeof SearchBox> = {
|
||||
const meta: GenericMeta<typeof SearchBox> = {
|
||||
title: 'Components/Input/SearchBox',
|
||||
component: SearchBox,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
modelValue: {
|
||||
control: 'text'
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text'
|
||||
},
|
||||
@@ -19,9 +26,12 @@ const meta: Meta<typeof SearchBox> = {
|
||||
control: 'select',
|
||||
options: ['md', 'lg'],
|
||||
description: 'Size variant of the search box'
|
||||
}
|
||||
},
|
||||
'onUpdate:modelValue': { action: 'update:modelValue' },
|
||||
onSearch: { action: 'search' }
|
||||
},
|
||||
args: {
|
||||
modelValue: '',
|
||||
placeholder: 'Search...',
|
||||
showBorder: false,
|
||||
size: 'md'
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from './SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -50,15 +50,15 @@ describe('SearchBox', () => {
|
||||
await input.setValue('test')
|
||||
|
||||
// Model should not update immediately
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Advance timers by 299ms (just before debounce delay)
|
||||
vi.advanceTimersByTime(299)
|
||||
await vi.advanceTimersByTimeAsync(299)
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Advance timers by 1ms more (reaching 300ms)
|
||||
vi.advanceTimersByTime(1)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await nextTick()
|
||||
|
||||
// Model should now be updated
|
||||
@@ -82,19 +82,19 @@ describe('SearchBox', () => {
|
||||
|
||||
// Type third character (should reset timer again)
|
||||
await input.setValue('tes')
|
||||
vi.advanceTimersByTime(200)
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await nextTick()
|
||||
|
||||
// Should not have emitted yet (only 200ms passed since last keystroke)
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Advance final 100ms to reach 300ms
|
||||
vi.advanceTimersByTime(100)
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
await nextTick()
|
||||
|
||||
// Should now emit with final value
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['tes'])
|
||||
expect(wrapper.emitted('search')).toBeTruthy()
|
||||
expect(wrapper.emitted('search')?.[0]).toEqual(['tes', []])
|
||||
})
|
||||
|
||||
it('should only emit final value after rapid typing', async () => {
|
||||
@@ -105,19 +105,20 @@ describe('SearchBox', () => {
|
||||
const searchTerms = ['s', 'se', 'sea', 'sear', 'searc', 'search']
|
||||
for (const term of searchTerms) {
|
||||
await input.setValue(term)
|
||||
vi.advanceTimersByTime(50) // Less than debounce delay
|
||||
await vi.advanceTimersByTimeAsync(50) // Less than debounce delay
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
// Should not have emitted yet
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Complete the debounce delay
|
||||
vi.advanceTimersByTime(300)
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
await nextTick()
|
||||
|
||||
// Should emit only once with final value
|
||||
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['search'])
|
||||
expect(wrapper.emitted('search')).toHaveLength(1)
|
||||
expect(wrapper.emitted('search')?.[0]).toEqual(['search', []])
|
||||
})
|
||||
|
||||
describe('bidirectional model sync', () => {
|
||||
@@ -1,84 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
<IconField>
|
||||
<Button
|
||||
v-if="filterIcon"
|
||||
class="p-inputicon filter-button"
|
||||
:icon="filterIcon"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="$emit('showFilter', $event)"
|
||||
/>
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
class="search-box-input w-full"
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:autofocus="autofocus"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
</IconField>
|
||||
<div
|
||||
v-if="filters?.length"
|
||||
class="search-filters flex flex-wrap gap-2 pt-2"
|
||||
>
|
||||
<SearchFilterChip
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:text="filter.text"
|
||||
:badge="filter.badge"
|
||||
:badge-class="filter.badgeClass"
|
||||
@remove="$emit('removeFilter', filter)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full items-center gap-2 bg-comfy-input cursor-text text-comfy-input-foreground',
|
||||
customClass,
|
||||
wrapperStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
v-model="modelValue"
|
||||
:placeholder
|
||||
:autofocus
|
||||
unstyled
|
||||
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm"
|
||||
:aria-label="placeholder"
|
||||
/>
|
||||
<IconButton
|
||||
v-if="filterIcon"
|
||||
class="p-inputicon filter-button absolute right-0 inset-y-0 h-full m-0 p-0"
|
||||
:icon="filterIcon"
|
||||
severity="contrast"
|
||||
@click="$emit('showFilter', $event)"
|
||||
/>
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="modelValue = ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="filters?.length" class="search-filters flex flex-wrap gap-2 pt-2">
|
||||
<SearchFilterChip
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:text="filter.text"
|
||||
:badge="filter.badge"
|
||||
:badge-class="filter.badgeClass"
|
||||
@remove="$emit('removeFilter', filter)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import IconButton from '../button/IconButton.vue'
|
||||
import type { SearchFilter } from './SearchFilterChip.vue'
|
||||
import SearchFilterChip from './SearchFilterChip.vue'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
placeholder = 'Search...',
|
||||
icon = 'pi pi-search',
|
||||
debounceTime = 300,
|
||||
filterIcon,
|
||||
filters = [],
|
||||
autofocus = false
|
||||
autofocus = false,
|
||||
showBorder = false,
|
||||
size = 'md',
|
||||
class: customClass
|
||||
} = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
filterIcon?: string
|
||||
filters?: TFilter[]
|
||||
autofocus?: boolean
|
||||
showBorder?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'search', value: string, filters: TFilter[]): void
|
||||
(e: 'showFilter', event: Event): void
|
||||
(e: 'removeFilter', filter: TFilter): void
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
|
||||
const inputRef = ref()
|
||||
|
||||
defineExpose({
|
||||
@@ -87,20 +96,27 @@ defineExpose({
|
||||
}
|
||||
})
|
||||
|
||||
const emitSearch = debounce((value: string) => {
|
||||
emit('search', value, filters)
|
||||
}, debounceTime)
|
||||
watchDebounced(
|
||||
modelValue,
|
||||
(value: string) => {
|
||||
emit('search', value, filters)
|
||||
},
|
||||
{ debounce: debounceTime }
|
||||
)
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
emitSearch(target.value)
|
||||
}
|
||||
const wrapperStyle = computed(() => {
|
||||
if (showBorder) {
|
||||
return cn('rounded p-2 border border-solid border-border-default')
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
emit('update:modelValue', '')
|
||||
emitSearch('')
|
||||
}
|
||||
// Size-specific classes matching button sizes for consistency
|
||||
const sizeClasses = {
|
||||
md: 'h-8 px-2 py-1.5', // Matches button sm size
|
||||
lg: 'h-10 px-4 py-2' // Matches button md size
|
||||
}[size]
|
||||
|
||||
return cn('rounded-lg', sizeClasses)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -388,8 +388,8 @@ import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue'
|
||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||
|
||||
@@ -178,7 +178,7 @@ import MultiSelect from 'primevue/multiselect'
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { usePopoverSizing } from '@/composables/usePopoverSizing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div :class="wrapperStyle" @click="focusInput">
|
||||
<i class="icon-[lucide--search] text-muted-foreground" />
|
||||
<InputText
|
||||
ref="input"
|
||||
v-model="internalSearchQuery"
|
||||
:aria-label="
|
||||
placeholder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
|
||||
"
|
||||
:placeholder="
|
||||
placeholder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
|
||||
"
|
||||
type="text"
|
||||
unstyled
|
||||
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const SEARCH_DEBOUNCE_DELAY_MS = 300
|
||||
|
||||
const {
|
||||
autofocus = false,
|
||||
placeholder,
|
||||
showBorder = false,
|
||||
size = 'md'
|
||||
} = defineProps<{
|
||||
autofocus?: boolean
|
||||
placeholder?: string
|
||||
showBorder?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
}>()
|
||||
|
||||
// defineModel without arguments uses 'modelValue' as the prop name
|
||||
const searchQuery = defineModel<string>()
|
||||
|
||||
// Internal search query state for immediate UI updates
|
||||
const internalSearchQuery = ref<string>(searchQuery.value ?? '')
|
||||
|
||||
// Create debounced function to update the parent model
|
||||
const updateSearchQuery = useDebounceFn((value: string) => {
|
||||
searchQuery.value = value
|
||||
}, SEARCH_DEBOUNCE_DELAY_MS)
|
||||
|
||||
// Watch internal query changes and trigger debounced update
|
||||
watch(internalSearchQuery, (newValue) => {
|
||||
void updateSearchQuery(newValue)
|
||||
})
|
||||
|
||||
// Sync external changes back to internal state
|
||||
watch(searchQuery, (newValue) => {
|
||||
if (newValue !== internalSearchQuery.value) {
|
||||
internalSearchQuery.value = newValue || ''
|
||||
}
|
||||
})
|
||||
|
||||
const input = ref<{ $el: HTMLElement } | null>()
|
||||
const focusInput = () => {
|
||||
if (input.value && input.value.$el) {
|
||||
input.value.$el.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => autofocus && focusInput())
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const baseClasses =
|
||||
'relative flex w-full items-center gap-2 bg-secondary-background cursor-text'
|
||||
|
||||
if (showBorder) {
|
||||
return cn(
|
||||
baseClasses,
|
||||
'rounded p-2 border border-solid border-border-default'
|
||||
)
|
||||
}
|
||||
|
||||
// Size-specific classes matching button sizes for consistency
|
||||
const sizeClasses = {
|
||||
md: 'h-8 px-2 py-1.5', // Matches button sm size
|
||||
lg: 'h-10 px-4 py-2' // Matches button md size
|
||||
}[size]
|
||||
|
||||
return cn(baseClasses, 'rounded-lg', sizeClasses)
|
||||
})
|
||||
</script>
|
||||
@@ -23,6 +23,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #tool-buttons>
|
||||
<!-- Normal Tab View -->
|
||||
<TabList v-if="!isInFolderView" v-model="activeTab">
|
||||
<Tab class="font-inter" value="output">{{
|
||||
$t('sideToolbar.labels.generated')
|
||||
}}</Tab>
|
||||
<Tab class="font-inter" value="input">{{
|
||||
$t('sideToolbar.labels.imported')
|
||||
}}</Tab>
|
||||
</TabList>
|
||||
</template>
|
||||
<template #header>
|
||||
<!-- Job Detail View Header -->
|
||||
<div v-if="isInFolderView" class="px-2 2xl:px-4">
|
||||
@@ -36,15 +47,7 @@
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
<!-- Normal Tab View -->
|
||||
<TabList v-else v-model="activeTab" class="font-inter px-2 2xl:px-4">
|
||||
<Tab class="font-inter" value="output">{{
|
||||
$t('sideToolbar.labels.generated')
|
||||
}}</Tab>
|
||||
<Tab class="font-inter" value="input">{{
|
||||
$t('sideToolbar.labels.imported')
|
||||
}}</Tab>
|
||||
</TabList>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<MediaAssetFilterBar
|
||||
v-model:search-query="searchQuery"
|
||||
@@ -55,6 +58,7 @@
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<Divider type="dashed" class="m-2" />
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading">
|
||||
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
|
||||
@@ -174,6 +178,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||
import { Divider } from 'primevue'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
@@ -17,17 +17,19 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="model-lib-search-box p-2 2xl:p-4"
|
||||
:placeholder="$t('g.searchModels') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<div class="px-2 2xl:px-4">
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
:placeholder="$t('g.searchModels') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<ElectronDownloadItems v-if="isElectron()" />
|
||||
|
||||
<Divider type="dashed" class="m-2" />
|
||||
<TreeExplorer
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
class="model-lib-tree-explorer"
|
||||
@@ -43,6 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Divider } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, nextTick, onMounted, ref, toRef, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
</Popover>
|
||||
</template>
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="px-2 2xl:px-4">
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="node-lib-search-box p-2 2xl:p-4"
|
||||
class="node-lib-search-box"
|
||||
:placeholder="$t('g.searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header flex flex-col gap-2">
|
||||
<Toolbar
|
||||
class="min-h-15.5 bg-transparent rounded-none border-x-0 border-t-0 px-2 2xl:px-4"
|
||||
class="min-h-16 bg-transparent rounded-none border-x-0 border-t-0 px-2 2xl:px-4"
|
||||
>
|
||||
<template #start>
|
||||
<span class="truncate font-bold" :title="props.title">
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="workflows-search-box p-2 2xl:p-4"
|
||||
:placeholder="$t('g.searchWorkflows') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<div class="px-2 2xl:px-4">
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="workflows-search-box"
|
||||
:placeholder="$t('g.searchWorkflows') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="!isSearching" class="comfyui-workflows-panel">
|
||||
|
||||
@@ -140,8 +140,8 @@ import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
|
||||
@@ -9,7 +9,7 @@ import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
|
||||
@@ -662,7 +662,7 @@
|
||||
"filter3D": "3D"
|
||||
},
|
||||
"backToAssets": "Back to all assets",
|
||||
"searchAssets": "Search assets...",
|
||||
"searchAssets": "Search Assets",
|
||||
"labels": {
|
||||
"queue": "Queue",
|
||||
"nodes": "Nodes",
|
||||
|
||||
@@ -69,7 +69,7 @@ import { computed, provide, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<div class="flex gap-3">
|
||||
<SearchBox
|
||||
:model-value="searchQuery"
|
||||
:placeholder="$t('sideToolbar.searchAssets')"
|
||||
size="lg"
|
||||
:placeholder="$t('sideToolbar.searchAssets') + '...'"
|
||||
@update:model-value="handleSearchChange"
|
||||
/>
|
||||
<MediaAssetFilterButton
|
||||
@@ -37,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
import MediaAssetFilterButton from './MediaAssetFilterButton.vue'
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
<template>
|
||||
<div v-if="renderError" class="node-error p-1 text-xs text-red-500">⚠️</div>
|
||||
<div v-else v-tooltip.left="tooltipConfig" :class="slotWrapperClass">
|
||||
<div
|
||||
v-else
|
||||
v-tooltip.left="tooltipConfig"
|
||||
:class="
|
||||
cn(
|
||||
'lg-slot lg-slot--input flex items-center group rounded-r-lg m-0',
|
||||
'cursor-crosshair',
|
||||
props.dotOnly ? 'lg-slot--dot-only' : 'pr-6',
|
||||
{
|
||||
'lg-slot--connected': props.connected,
|
||||
'lg-slot--compatible': props.compatible,
|
||||
'opacity-40': shouldDim
|
||||
},
|
||||
props.socketless && 'pointer-events-none invisible'
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- Connection Dot -->
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
:color="slotColor"
|
||||
:class="cn('-translate-x-1/2 w-3', errorClassesDot)"
|
||||
:class="
|
||||
cn(
|
||||
'-translate-x-1/2 w-3',
|
||||
hasSlotError && 'ring-2 ring-error ring-offset-0 rounded-full'
|
||||
)
|
||||
"
|
||||
@click="onClick"
|
||||
@dblclick="onDoubleClick"
|
||||
@pointerdown="onPointerDown"
|
||||
@@ -15,7 +36,12 @@
|
||||
<div class="h-full flex items-center min-w-0">
|
||||
<span
|
||||
v-if="!dotOnly"
|
||||
:class="cn('truncate text-xs font-normal', labelClasses)"
|
||||
:class="
|
||||
cn(
|
||||
'truncate text-node-component-slot-text',
|
||||
hasSlotError && 'text-error font-medium'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
|
||||
</span>
|
||||
@@ -65,18 +91,6 @@ const hasSlotError = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const errorClassesDot = computed(() => {
|
||||
return hasSlotError.value
|
||||
? 'ring-2 ring-error ring-offset-0 rounded-full'
|
||||
: ''
|
||||
})
|
||||
|
||||
const labelClasses = computed(() =>
|
||||
hasSlotError.value
|
||||
? 'text-error font-medium'
|
||||
: 'text-node-component-slot-text'
|
||||
)
|
||||
|
||||
const renderError = ref<string | null>(null)
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
@@ -113,20 +127,6 @@ const shouldDim = computed(() => {
|
||||
return !dragState.compatible.get(slotKey.value)
|
||||
})
|
||||
|
||||
const slotWrapperClass = computed(() =>
|
||||
cn(
|
||||
'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6',
|
||||
'cursor-crosshair',
|
||||
props.dotOnly ? 'lg-slot--dot-only' : 'pr-6',
|
||||
{
|
||||
'lg-slot--connected': props.connected,
|
||||
'lg-slot--compatible': props.compatible,
|
||||
'opacity-40': shouldDim.value
|
||||
},
|
||||
props.socketless && 'pointer-events-none invisible'
|
||||
)
|
||||
)
|
||||
|
||||
const connectionDotRef = ref<ComponentPublicInstance<{
|
||||
slotElRef: HTMLElement | undefined
|
||||
}> | null>(null)
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
:data-node-id="nodeData.id"
|
||||
:class="
|
||||
cn(
|
||||
'bg-component-node-background lg-node absolute pb-1',
|
||||
|
||||
'bg-component-node-background lg-node absolute pb-1 text-sm',
|
||||
'contain-style contain-layout min-w-[225px] min-h-(--node-height) w-(--node-width)',
|
||||
shapeClass,
|
||||
'touch-none flex flex-col',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div v-if="renderError" class="node-error p-4 text-sm text-red-500">
|
||||
<div v-if="renderError" class="node-error p-4 text-red-500">
|
||||
{{ st('nodeErrors.header', 'Node Header Error') }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
'lg-node-header py-2 pl-2 pr-3 text-sm w-full min-w-0',
|
||||
'lg-node-header text-sm py-2 pl-2 pr-3 w-full min-w-0',
|
||||
'text-node-component-header bg-node-component-header-surface',
|
||||
headerShapeClass
|
||||
)
|
||||
@@ -34,7 +34,7 @@
|
||||
)
|
||||
"
|
||||
class="relative top-px text-xs leading-none text-node-component-header-icon"
|
||||
></i>
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<!-- Node Title -->
|
||||
<div
|
||||
v-tooltip.top="tooltipConfig"
|
||||
class="flex min-w-0 flex-1 items-center gap-2 text-sm font-bold"
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
data-testid="node-title"
|
||||
>
|
||||
<div class="truncate min-w-0 flex-1">
|
||||
@@ -92,7 +92,7 @@
|
||||
class="min-w-max rounded-sm bg-node-component-surface px-1 py-0.5 text-xs flex items-center gap-1"
|
||||
>
|
||||
{{ $t('g.edit') }}
|
||||
<i class="icon-[lucide--scaling] size-5"></i>
|
||||
<i class="icon-[lucide--scaling] size-5" />
|
||||
</div>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'z-10 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100 flex items-center',
|
||||
'z-10 w-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100 flex items-stretch',
|
||||
widget.slotMetadata?.linked && 'opacity-100'
|
||||
)
|
||||
"
|
||||
@@ -58,7 +58,7 @@
|
||||
:model-value="widget.value"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:node-type="nodeType"
|
||||
class="flex-1 col-span-2"
|
||||
class="col-span-2"
|
||||
@update:model-value="widget.updateHandler"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
<div v-else v-tooltip.right="tooltipConfig" :class="slotWrapperClass">
|
||||
<div class="relative h-full flex items-center min-w-0">
|
||||
<!-- Slot Name -->
|
||||
<span
|
||||
v-if="!dotOnly"
|
||||
class="text-xs font-normal truncate text-node-component-slot-text"
|
||||
>
|
||||
<span v-if="!dotOnly" class="truncate text-node-component-slot-text">
|
||||
{{ slotData.localized_name || slotData.name || `Output ${index}` }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { noop } from 'es-toolkit'
|
||||
import { inject } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
@@ -17,18 +16,12 @@ const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-subgrid min-w-0 items-center justify-between gap-1"
|
||||
class="grid grid-cols-subgrid min-w-0 justify-between gap-1 text-node-component-slot-text"
|
||||
>
|
||||
<div
|
||||
v-if="!hideLayoutField"
|
||||
class="relative flex h-full min-w-0 items-center"
|
||||
>
|
||||
<p
|
||||
v-if="widget.name"
|
||||
class="flex-1 truncate text-xs font-normal text-node-component-slot-text my-0"
|
||||
>
|
||||
<div v-if="!hideLayoutField" class="truncate content-center-safe">
|
||||
<template v-if="widget.name">
|
||||
{{ widget.label || widget.name }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<!-- basis-full grow -->
|
||||
<div class="relative min-w-0 flex-1">
|
||||
@@ -39,9 +32,9 @@ const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
||||
widget.borderStyle
|
||||
)
|
||||
"
|
||||
@pointerdown.stop="noop"
|
||||
@pointermove.stop="noop"
|
||||
@pointerup.stop="noop"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/components/input/SearchBox.vue', () => ({
|
||||
vi.mock('@/components/common/SearchBox.vue', () => ({
|
||||
default: {
|
||||
name: 'SearchBox',
|
||||
props: ['modelValue', 'size', 'placeholder', 'class'],
|
||||
|
||||