Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/design-system/src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
},
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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 () => {
Expand All @@ -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', () => {
Expand Down
136 changes: 76 additions & 60 deletions src/components/common/SearchBox.vue
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion src/components/input/MultiSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Loading