Compare commits

...

3 Commits

Author SHA1 Message Date
pythongosssss
9ac33a34c1 - Update widgets to inherit their nodes color
- Small visibility fixes
2026-01-15 15:26:29 -08:00
Terry Jia
aff7f2a296 fix: prevent Record Audio waveform from overflowing node bounds (#8070)
## Summary

Add min-w-0 to flex containers to allow shrinking, reduce gaps and
padding, and use shrink-0 on fixed-size elements to ensure the waveform
area clips properly when the node is at minimum width.

## Screenshots (if applicable)
before

https://github.com/user-attachments/assets/215455aa-555b-4ade-9b36-9e89ac7c14aa


after

https://github.com/user-attachments/assets/bf56028b-ae02-4388-be83-460c7b1f14e1

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8070-fix-prevent-Record-Audio-waveform-from-overflowing-node-bounds-2e96d73d3650816b9660e5532ffa0bc3)
by [Unito](https://www.unito.io)
2026-01-15 15:49:32 -05:00
Alexander Brown
23694f37bf feat(ui): add TagsInput component with click-to-edit behavior (#8066)
## Summary

Add TagsInput component based on shadcn-vue/Reka UI primitives with a
click-to-edit UX pattern.

## Features

- **Click-to-edit behavior**: Starts in read-only state; clicking
enables editing and focuses input; clicking outside exits edit mode
- **Disabled state**: When `disabled=true`, component is completely
inert
- **Empty state placeholder**: Shows input placeholder when tag list is
empty
- **Animated transitions**: Delete button animates when toggling edit
mode

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-14 21:17:04 -08:00
17 changed files with 613 additions and 17 deletions

View File

@@ -200,7 +200,7 @@
--node-component-disabled: var(--color-alpha-ash-500-20);
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-ash-800);
--node-component-header-icon: var(--node-component-surface-highlight);
--node-component-header-surface: var(--color-smoke-400);
--node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%);
@@ -258,6 +258,7 @@
--component-node-border: var(--color-border-default);
--component-node-foreground: var(--base-foreground);
--component-node-foreground-secondary: var(--color-muted-foreground);
--component-node-shadow: 1px 1px 8px 0 rgba(0, 0, 0, 0.2);
--component-node-widget-background: var(--secondary-background);
--component-node-widget-background-hovered: var(--secondary-background-hover);
--component-node-widget-background-selected: var(--secondary-background-selected);
@@ -282,7 +283,7 @@
--modal-card-border-highlighted: var(--secondary-background-selected);
--modal-card-button-surface: var(--color-smoke-300);
--modal-card-placeholder-background: var(--color-smoke-600);
--modal-card-tag-background: var(--color-smoke-400);
--modal-card-tag-background: var(--color-smoke-200);
--modal-card-tag-foreground: var(--base-foreground);
--modal-panel-background: var(--color-white);
}
@@ -323,7 +324,7 @@
--node-component-border-error: var(--color-danger-100);
--node-component-border-executing: var(--color-blue-500);
--node-component-border-selected: var(--color-charcoal-200);
--node-component-header-icon: var(--color-slate-300);
--node-component-header-icon: var(--node-component-surface-highlight);
--node-component-header-surface: var(--color-charcoal-800);
--node-component-outline: var(--color-white);
--node-component-ring: rgb(var(--color-smoke-500) / 20%);
@@ -384,6 +385,7 @@
--component-node-border: var(--color-charcoal-100);
--component-node-foreground: var(--base-foreground);
--component-node-foreground-secondary: var(--color-muted-foreground);
--component-node-shadow: 1px 1px 8px 0 rgba(0, 0, 0, 0.4);
--component-node-widget-background: var(--secondary-background-hover);
--component-node-widget-background-hovered: var(--secondary-background-selected);
--component-node-widget-background-selected: var(--color-charcoal-100);
@@ -1324,6 +1326,29 @@ audio.comfy-audio.empty-audio-widget {
.lg-node {
/* Disable text selection on all nodes */
user-select: none;
--component-node-widget-background: color-mix(
in srgb,
var(--component-node-background) 92%,
var(--contrast-mix-color, #000)
);
--component-node-widget-background-hovered: color-mix(
in srgb,
var(--component-node-background) 80%,
var(--contrast-mix-color, #000)
);
}
.dark-theme .lg-node {
--component-node-widget-background: color-mix(
in srgb,
var(--component-node-background) 85%,
var(--contrast-mix-color, #000)
);
--component-node-widget-background-hovered: color-mix(
in srgb,
var(--component-node-background) 70%,
var(--contrast-mix-color, #000)
);
}
.lg-node .lg-slot,

View File

@@ -0,0 +1,182 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { ComponentExposed } from 'vue-component-type-helpers'
import { ref } from 'vue'
import TagsInput from './TagsInput.vue'
import TagsInputInput from './TagsInputInput.vue'
import TagsInputItem from './TagsInputItem.vue'
import TagsInputItemDelete from './TagsInputItemDelete.vue'
import TagsInputItemText from './TagsInputItemText.vue'
interface GenericMeta<C> extends Omit<Meta<C>, 'component'> {
component: ComponentExposed<C>
}
const meta: GenericMeta<typeof TagsInput> = {
title: 'Components/TagsInput',
component: TagsInput,
tags: ['autodocs'],
argTypes: {
modelValue: {
control: 'object',
description: 'Array of tag values'
},
disabled: {
control: 'boolean',
description:
'When true, completely disables the component. When false (default), shows read-only state with edit icon until clicked.'
},
'onUpdate:modelValue': { action: 'update:modelValue' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: (args) => ({
components: {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
},
setup() {
const tags = ref(args.modelValue || ['tag1', 'tag2'])
return { tags, args }
},
template: `
<TagsInput v-model="tags" :disabled="args.disabled" class="w-80" v-slot="{ isEmpty }">
<TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput :is-empty="isEmpty" placeholder="Add tag..." />
</TagsInput>
<div class="mt-4 text-sm text-muted-foreground">
Tags: {{ tags.join(', ') }}
</div>
`
}),
args: {
modelValue: ['Vue', 'TypeScript'],
disabled: false
}
}
export const Empty: Story = {
args: {
disabled: false
},
render: (args) => ({
components: {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
},
setup() {
const tags = ref<string[]>([])
return { tags, args }
},
template: `
<TagsInput v-model="tags" :disabled="args.disabled" class="w-80" v-slot="{ isEmpty }">
<TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput :is-empty="isEmpty" placeholder="Start typing to add tags..." />
</TagsInput>
`
})
}
export const ManyTags: Story = {
render: (args) => ({
components: {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
},
setup() {
const tags = ref([
'JavaScript',
'TypeScript',
'Vue',
'React',
'Svelte',
'Node.js',
'Python',
'Rust'
])
return { tags, args }
},
template: `
<TagsInput v-model="tags" :disabled="args.disabled" class="w-96" v-slot="{ isEmpty }">
<TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput :is-empty="isEmpty" placeholder="Add technology..." />
</TagsInput>
`
})
}
export const Disabled: Story = {
args: {
disabled: true
},
render: (args) => ({
components: {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
},
setup() {
const tags = ref(['Read', 'Only', 'Tags'])
return { tags, args }
},
template: `
<TagsInput v-model="tags" :disabled="args.disabled" class="w-80" v-slot="{ isEmpty }">
<TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput :is-empty="isEmpty" placeholder="Cannot add tags..." />
</TagsInput>
`
})
}
export const CustomWidth: Story = {
render: (args) => ({
components: {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
},
setup() {
const tags = ref(['Full', 'Width'])
return { tags, args }
},
template: `
<TagsInput v-model="tags" :disabled="args.disabled" class="w-full" v-slot="{ isEmpty }">
<TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput :is-empty="isEmpty" placeholder="Add tag..." />
</TagsInput>
`
})
}

View File

@@ -0,0 +1,161 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { h, nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import TagsInput from './TagsInput.vue'
import TagsInputInput from './TagsInputInput.vue'
import TagsInputItem from './TagsInputItem.vue'
import TagsInputItemDelete from './TagsInputItemDelete.vue'
import TagsInputItemText from './TagsInputItemText.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: { g: { removeTag: 'Remove tag' } } }
})
describe('TagsInput', () => {
function mountTagsInput(props = {}, slots = {}) {
return mount(TagsInput, {
props: {
modelValue: [],
...props
},
slots
})
}
it('renders slot content', () => {
const wrapper = mountTagsInput({}, { default: '<span>Slot Content</span>' })
expect(wrapper.text()).toContain('Slot Content')
})
})
describe('TagsInput with child components', () => {
function mountFullTagsInput(tags: string[] = ['tag1', 'tag2']) {
return mount(TagsInput, {
global: { plugins: [i18n] },
props: {
modelValue: tags
},
slots: {
default: () => [
...tags.map((tag) =>
h(TagsInputItem, { key: tag, value: tag }, () => [
h(TagsInputItemText),
h(TagsInputItemDelete)
])
),
h(TagsInputInput, { placeholder: 'Add tag...' })
]
}
})
}
it('renders tags structure and content', () => {
const tags = ['tag1', 'tag2']
const wrapper = mountFullTagsInput(tags)
const items = wrapper.findAllComponents(TagsInputItem)
const textElements = wrapper.findAllComponents(TagsInputItemText)
const deleteButtons = wrapper.findAllComponents(TagsInputItemDelete)
expect(items).toHaveLength(tags.length)
expect(textElements).toHaveLength(tags.length)
expect(deleteButtons).toHaveLength(tags.length)
textElements.forEach((el, i) => {
expect(el.text()).toBe(tags[i])
})
expect(wrapper.findComponent(TagsInputInput).exists()).toBe(true)
})
it('updates model value when adding a tag', async () => {
let currentTags = ['existing']
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
props: {
modelValue: currentTags,
'onUpdate:modelValue': (payload) => {
currentTags = payload
}
},
slots: {
default: () => h(TagsInputInput, { placeholder: 'Add tag...' })
}
})
await wrapper.trigger('click')
await nextTick()
const input = wrapper.find('input')
await input.setValue('newTag')
await input.trigger('keydown', { key: 'Enter' })
await nextTick()
expect(currentTags).toContain('newTag')
})
it('does not enter edit mode when disabled', async () => {
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
props: {
modelValue: ['tag1'],
disabled: true
},
slots: {
default: () => h(TagsInputInput, { placeholder: 'Add tag...' })
}
})
expect(wrapper.find('input').exists()).toBe(false)
await wrapper.trigger('click')
await nextTick()
expect(wrapper.find('input').exists()).toBe(false)
})
it('exits edit mode when clicking outside', async () => {
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
props: {
modelValue: ['tag1']
},
slots: {
default: () => h(TagsInputInput, { placeholder: 'Add tag...' })
},
attachTo: document.body
})
await wrapper.trigger('click')
await nextTick()
expect(wrapper.find('input').exists()).toBe(true)
document.body.click()
await nextTick()
expect(wrapper.find('input').exists()).toBe(false)
wrapper.unmount()
})
it('shows placeholder when modelValue is empty', async () => {
const wrapper = mount<typeof TagsInput<string>>(TagsInput, {
props: {
modelValue: []
},
slots: {
default: ({ isEmpty }: { isEmpty: boolean }) =>
h(TagsInputInput, { placeholder: 'Add tag...', isEmpty })
}
})
await nextTick()
const input = wrapper.find('input')
expect(input.exists()).toBe(true)
expect(input.attributes('placeholder')).toBe('Add tag...')
})
})

View File

@@ -0,0 +1,77 @@
<script setup lang="ts" generic="T extends AcceptableInputValue = string">
import { onClickOutside, useCurrentElement } from '@vueuse/core'
import type {
AcceptableInputValue,
TagsInputRootEmits,
TagsInputRootProps
} from 'reka-ui'
import { TagsInputRoot, useForwardPropsEmits } from 'reka-ui'
import { computed, nextTick, provide, ref } from 'vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import { tagsInputFocusKey, tagsInputIsEditingKey } from './tagsInputContext'
import type { FocusCallback } from './tagsInputContext'
const {
disabled = false,
class: className,
...restProps
} = defineProps<TagsInputRootProps<T> & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<TagsInputRootEmits<T>>()
const isEditing = ref(false)
const rootEl = useCurrentElement<HTMLElement>()
const focusInput = ref<FocusCallback>()
provide(tagsInputFocusKey, (callback: FocusCallback) => {
focusInput.value = callback
})
provide(tagsInputIsEditingKey, isEditing)
const internalDisabled = computed(() => disabled || !isEditing.value)
const delegatedProps = computed(() => ({
...restProps,
disabled: internalDisabled.value
}))
const forwarded = useForwardPropsEmits(delegatedProps, emits)
async function enableEditing() {
if (!disabled && !isEditing.value) {
isEditing.value = true
await nextTick()
focusInput.value?.()
}
}
onClickOutside(rootEl, () => {
isEditing.value = false
})
</script>
<template>
<TagsInputRoot
v-slot="{ modelValue }"
v-bind="forwarded"
:class="
cn(
'group relative flex flex-wrap items-center gap-2 rounded-lg bg-transparent p-2 text-xs text-base-foreground',
!internalDisabled &&
'hover:bg-modal-card-background-hovered focus-within:bg-modal-card-background-hovered',
!disabled && !isEditing && 'cursor-pointer',
className
)
"
@click="enableEditing"
>
<slot :is-empty="modelValue.length === 0" />
<i
v-if="!disabled && !isEditing"
aria-hidden="true"
class="icon-[lucide--square-pen] absolute bottom-2 right-2 size-4 text-muted-foreground"
/>
</TagsInputRoot>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import type { TagsInputInputProps } from 'reka-ui'
import { TagsInputInput, useForwardExpose, useForwardProps } from 'reka-ui'
import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import { tagsInputFocusKey, tagsInputIsEditingKey } from './tagsInputContext'
const {
isEmpty = false,
class: className,
...restProps
} = defineProps<
TagsInputInputProps & { class?: HTMLAttributes['class']; isEmpty?: boolean }
>()
const forwardedProps = useForwardProps(restProps)
const isEditing = inject(tagsInputIsEditingKey, ref(true))
const showInput = computed(() => isEditing.value || isEmpty)
const { forwardRef, currentElement } = useForwardExpose()
const registerFocus = inject(tagsInputFocusKey, undefined)
onMounted(() => {
registerFocus?.(() => currentElement.value?.focus())
})
onUnmounted(() => {
registerFocus?.(undefined)
})
</script>
<template>
<TagsInputInput
v-if="showInput"
:ref="forwardRef"
v-bind="forwardedProps"
:class="
cn(
'min-h-6 flex-1 bg-transparent text-xs text-muted-foreground placeholder:text-muted-foreground focus:outline-none appearance-none border-none',
!isEditing && 'pointer-events-none',
className
)
"
/>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { TagsInputItemProps } from 'reka-ui'
import { TagsInputItem, useForwardProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const { class: className, ...restProps } = defineProps<
TagsInputItemProps & { class?: HTMLAttributes['class'] }
>()
const forwardedProps = useForwardProps(restProps)
</script>
<template>
<TagsInputItem
v-bind="forwardedProps"
:class="
cn(
'flex h-6 items-center gap-1 rounded-sm bg-modal-card-tag-background py-1 pl-2 pr-1 text-modal-card-tag-foreground backdrop-blur-sm ring-offset-base-background data-[state=active]:ring-2 data-[state=active]:ring-base-foreground data-[state=active]:ring-offset-1',
className
)
"
>
<slot />
</TagsInputItem>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { TagsInputItemDeleteProps } from 'reka-ui'
import { TagsInputItemDelete, useForwardProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
const { class: className, ...restProps } = defineProps<
TagsInputItemDeleteProps & { class?: HTMLAttributes['class'] }
>()
const forwardedProps = useForwardProps(restProps)
const { t } = useI18n()
</script>
<template>
<TagsInputItemDelete
v-bind="forwardedProps"
:as="Button"
variant="textonly"
size="icon-sm"
:aria-label="t('g.removeTag')"
:class="
cn(
'opacity-60 hover:bg-transparent hover:opacity-100 transition-[opacity,width] duration-150 w-4 data-[disabled]:w-0 data-[disabled]:opacity-0 data-[disabled]:pointer-events-none overflow-hidden',
className
)
"
>
<slot>
<i class="icon-[lucide--x] size-4" />
</slot>
</TagsInputItemDelete>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { TagsInputItemTextProps } from 'reka-ui'
import { TagsInputItemText, useForwardProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const { class: className, ...restProps } = defineProps<
TagsInputItemTextProps & { class?: HTMLAttributes['class'] }
>()
const forwardedProps = useForwardProps(restProps)
</script>
<template>
<TagsInputItemText
v-bind="forwardedProps"
:class="cn('bg-transparent text-xs', className)"
/>
</template>

View File

@@ -0,0 +1,10 @@
import type { InjectionKey, Ref } from 'vue'
export type FocusCallback = (() => void) | undefined
export const tagsInputFocusKey: InjectionKey<
(callback: FocusCallback) => void
> = Symbol('tagsInputFocus')
export const tagsInputIsEditingKey: InjectionKey<Ref<boolean>> =
Symbol('tagsInputIsEditing')

View File

@@ -16,6 +16,7 @@
"increment": "Increment",
"removeImage": "Remove image",
"removeVideo": "Remove video",
"removeTag": "Remove tag",
"chart": "Chart",
"chartLowercase": "chart",
"file": "file",

View File

@@ -13,7 +13,7 @@
'contain-style contain-layout min-w-[225px] min-h-(--node-height) w-(--node-width)',
shapeClass,
'touch-none flex flex-col',
'border-1 border-solid border-component-node-border',
'shadow-[var(--component-node-shadow)]',
// hover (only when node should handle events)
shouldHandleNodePointerEvents &&
'hover:ring-7 ring-node-component-ring',

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-1">
<Button
class="text-base-foreground w-full border-0 bg-component-node-widget-background p-2"
class="text-base-foreground w-full border-0 bg-component-node-widget-background hover:bg-component-node-widget-background-hovered p-2"
:aria-label="widget.label"
size="sm"
variant="textonly"

View File

@@ -64,7 +64,8 @@ function updateValue(e: UIEvent) {
textEdit.value = false
}
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
const sharedButtonClass =
'w-8 bg-transparent border-0 text-sm text-node-component-surface-highlight'
const canDecrement = computed(
() =>
modelValue.value > filteredProps.value.min &&

View File

@@ -12,11 +12,11 @@
</div>
<div
v-if="isRecording || isPlaying || recordedURL"
class="flex h-14 w-full items-center gap-4 rounded-lg px-4 bg-node-component-surface text-text-secondary"
class="flex h-14 w-full min-w-0 items-center gap-2 rounded-lg px-3 bg-node-component-surface text-text-secondary"
>
<!-- Recording Status -->
<div class="flex min-w-30 items-center gap-2">
<span class="min-w-20 text-xs">
<div class="flex shrink-0 items-center gap-1">
<span class="text-xs">
{{
isRecording
? t('g.listening', 'Listening...')
@@ -27,11 +27,11 @@
: ''
}}
</span>
<span class="min-w-10 text-sm">{{ formatTime(timer) }}</span>
<span class="text-sm">{{ formatTime(timer) }}</span>
</div>
<!-- Waveform Visualization -->
<div class="flex h-8 flex-1 items-center gap-2 overflow-x-clip">
<div class="flex h-8 min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div
v-for="(bar, index) in waveformBars"
:key="index"
@@ -45,7 +45,7 @@
<button
v-if="isRecording"
:title="t('g.stopRecording', 'Stop Recording')"
class="flex size-8 animate-pulse items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
class="flex shrink-0 size-8 animate-pulse items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
@click="handleStopRecording"
>
<div class="size-2.5 rounded-sm bg-danger-100" />
@@ -54,7 +54,7 @@
<button
v-else-if="!isRecording && recordedURL && !isPlaying"
:title="t('g.playRecording') || 'Play Recording'"
class="flex size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
class="flex shrink-0 size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
@click="handlePlayRecording"
>
<i class="text-text-secondary icon-[lucide--play] size-4" />
@@ -63,7 +63,7 @@
<button
v-else-if="isPlaying"
:title="t('g.stopPlayback') || 'Stop Playback'"
class="flex size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
class="flex shrink-0 size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
@click="handleStopPlayback"
>
<i class="text-text-secondary icon-[lucide--square] size-4" />

View File

@@ -14,7 +14,7 @@
option: 'text-xs',
dropdown: 'w-8',
label: cn('truncate min-w-[4ch]', $slots.default && 'mr-5'),
overlay: 'w-fit min-w-full'
overlay: 'w-fit min-w-full bg-component-node-widget-background'
}"
data-capture-wheel="true"
/>

View File

@@ -18,6 +18,16 @@
v-model="modelValue"
v-bind="filteredProps"
:aria-label="widget.name"
:pt="{
slider: {
class: 'bg-component-node-widget-background'
},
handle: {
class: modelValue
? 'bg-component-node-foreground'
: 'bg-node-component-surface-highlight'
}
}"
/>
</div>
</WidgetLayoutField>

View File

@@ -16,10 +16,8 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const THEME_PROPERTY_MAP = {
NODE_BOX_OUTLINE_COLOR: 'component-node-border',
NODE_DEFAULT_BGCOLOR: 'component-node-background',
NODE_DEFAULT_BOXCOLOR: 'node-component-header-icon',
NODE_DEFAULT_COLOR: 'node-component-header-surface',
NODE_TITLE_COLOR: 'node-component-header',
WIDGET_BGCOLOR: 'component-node-widget-background',
WIDGET_TEXT_COLOR: 'component-node-foreground'
} as const satisfies Partial<Record<keyof Colors['litegraph_base'], string>>