mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Tighten node menu color picker layout
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -13,7 +14,7 @@ vi.mock('@/composables/graph/useNodeCustomization', () => ({
|
||||
}))
|
||||
|
||||
describe('ColorPickerMenu', () => {
|
||||
it('renders a PrimeVue picker for custom color submenu entries', async () => {
|
||||
it('renders a compact PrimeVue picker panel for custom color submenu entries', async () => {
|
||||
const onColorPick = vi.fn()
|
||||
const option: MenuOption = {
|
||||
label: 'Color',
|
||||
@@ -41,13 +42,58 @@ describe('ColorPickerMenu', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const picker = wrapper.findComponent({ name: 'ColorPicker' })
|
||||
const picker = wrapper.findComponent(ColorPicker)
|
||||
expect(picker.exists()).toBe(true)
|
||||
expect(picker.props('modelValue')).toBe('112233')
|
||||
expect(picker.props('inline')).toBe(true)
|
||||
expect(wrapper.text()).toContain('#112233')
|
||||
|
||||
picker.vm.$emit('update:model-value', 'fedcba')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(onColorPick).toHaveBeenCalledWith('#fedcba')
|
||||
})
|
||||
|
||||
it('shows preset swatches in a compact grid when color presets are available', () => {
|
||||
const option: MenuOption = {
|
||||
label: 'Color',
|
||||
hasSubmenu: true,
|
||||
action: () => {},
|
||||
submenu: [
|
||||
{
|
||||
label: 'Custom',
|
||||
action: () => {},
|
||||
pickerValue: '112233',
|
||||
onColorPick: vi.fn()
|
||||
},
|
||||
{
|
||||
label: 'Red',
|
||||
action: () => {},
|
||||
color: '#ff0000'
|
||||
},
|
||||
{
|
||||
label: 'Green',
|
||||
action: () => {},
|
||||
color: '#00ff00'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const wrapper = mount(ColorPickerMenu, {
|
||||
props: { option },
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
Popover: {
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('button[title]').map((node) => node.attributes('title'))).toEqual([
|
||||
'Red',
|
||||
'Green'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,91 @@
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="isCompactColorPanel"
|
||||
class="w-[15.5rem] rounded-2xl border border-border-default bg-interface-panel-surface p-2.5 shadow-lg"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
||||
{{ option.label }}
|
||||
</p>
|
||||
<p class="mt-0.5 truncate text-sm font-medium text-base-foreground">
|
||||
{{ pickerOption?.label ?? 'Custom' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-md border border-border-default bg-secondary-background px-2 py-1 font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
{{ selectedPickerColor }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorPicker
|
||||
v-if="pickerOption"
|
||||
data-testid="color-picker-inline"
|
||||
:model-value="pickerOption.pickerValue"
|
||||
inline
|
||||
format="hex"
|
||||
:aria-label="pickerOption.label"
|
||||
class="w-full"
|
||||
:pt="{
|
||||
root: { class: '!w-full' },
|
||||
content: {
|
||||
class: '!border-none !bg-transparent !p-0 !shadow-none'
|
||||
},
|
||||
colorSelector: {
|
||||
class: '!h-32 !w-full overflow-hidden !rounded-xl'
|
||||
},
|
||||
colorBackground: {
|
||||
class: '!rounded-xl'
|
||||
},
|
||||
colorHandle: {
|
||||
class:
|
||||
'!h-3.5 !w-3.5 !rounded-full !border-2 !border-black/70 !shadow-sm'
|
||||
},
|
||||
hue: {
|
||||
class:
|
||||
'!mt-2 !h-3 !overflow-hidden !rounded-full !border !border-border-default'
|
||||
},
|
||||
hueHandle: {
|
||||
class:
|
||||
'!h-3.5 !w-3.5 !-translate-x-1/2 !rounded-full !border-2 !border-white !shadow-sm'
|
||||
}
|
||||
}"
|
||||
@update:model-value="handleColorPickerUpdate(pickerOption, $event)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="swatchOptions.length"
|
||||
class="mt-2 rounded-xl border border-border-default bg-secondary-background p-2"
|
||||
>
|
||||
<div class="-mx-0.5 flex gap-1.5 overflow-x-auto px-0.5 pb-0.5">
|
||||
<button
|
||||
v-for="subOption in swatchOptions"
|
||||
:key="subOption.label"
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-xl border border-transparent transition-transform hover:scale-[1.04] hover:border-border-default hover:bg-secondary-background-hover"
|
||||
:title="subOption.label"
|
||||
@click="handleSubmenuClick(subOption)"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'size-5 rounded-full border transition-shadow',
|
||||
isSelectedSwatch(subOption)
|
||||
? 'border-white shadow-[0_0_0_2px_rgba(255,255,255,0.18)]'
|
||||
: 'border-border-default'
|
||||
)
|
||||
"
|
||||
:style="{ backgroundColor: subOption.color }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
isColorSubmenu
|
||||
? 'flex flex-col gap-1 p-2'
|
||||
@@ -28,25 +113,6 @@
|
||||
>
|
||||
<template v-for="subOption in option.submenu" :key="subOption.label">
|
||||
<div
|
||||
v-if="isPickerOption(subOption)"
|
||||
class="flex items-center gap-2 rounded-sm px-3 py-1.5 text-sm"
|
||||
>
|
||||
<span class="flex-1">{{ subOption.label }}</span>
|
||||
<ColorPicker
|
||||
:model-value="subOption.pickerValue"
|
||||
format="hex"
|
||||
:aria-label="subOption.label"
|
||||
class="h-8 w-8 overflow-hidden rounded-md border border-border-default bg-secondary-background"
|
||||
:pt="{
|
||||
preview: {
|
||||
class: '!h-full !w-full !rounded-md !border-none'
|
||||
}
|
||||
}"
|
||||
@update:model-value="handleColorPickerUpdate(subOption, $event)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer rounded-sm hover:bg-secondary-background-hover',
|
||||
@@ -126,6 +192,24 @@ const isPickerOption = (subOption: SubMenuOption): boolean =>
|
||||
typeof subOption.pickerValue === 'string' &&
|
||||
typeof subOption.onColorPick === 'function'
|
||||
|
||||
const pickerOption = computed(
|
||||
() => props.option.submenu?.find(isPickerOption) ?? null
|
||||
)
|
||||
|
||||
const swatchOptions = computed(() =>
|
||||
(props.option.submenu ?? []).filter(
|
||||
(subOption) => Boolean(subOption.color) && !isPickerOption(subOption)
|
||||
)
|
||||
)
|
||||
|
||||
const selectedPickerColor = computed(() =>
|
||||
pickerOption.value?.pickerValue
|
||||
? `#${pickerOption.value.pickerValue.toUpperCase()}`
|
||||
: '#000000'
|
||||
)
|
||||
|
||||
const isCompactColorPanel = computed(() => Boolean(pickerOption.value))
|
||||
|
||||
async function handleColorPickerUpdate(
|
||||
subOption: SubMenuOption,
|
||||
value: string
|
||||
@@ -133,9 +217,12 @@ async function handleColorPickerUpdate(
|
||||
if (!isPickerOption(subOption) || !value) return
|
||||
|
||||
await subOption.onColorPick?.(`#${value}`)
|
||||
if (typeof popoverRef.value?.hide === 'function') {
|
||||
popoverRef.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectedSwatch(subOption: SubMenuOption): boolean {
|
||||
return (
|
||||
subOption.color?.toLowerCase() === selectedPickerColor.value.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
const isShapeSelected = (subOption: SubMenuOption): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user