Style: Make components themeable (#5908)

## Summary

Replace color/dark-color pairs in components with design tokens to allow
for easy overriding.
<!-- Also standardizes the icon pattern to simplify the tailwind config.
-->

## Changes

- **What**: Token based colors, for now, mostly.
- **Breaking**: Got approval from Design to collapse some very similar
pairs of colors that seem to have diverged in implementations over time.
Some of the colors might be a little different, but we can tweak them
later.

## Review Focus

Still have quite a few places from which to remove `dark-theme`, but
this at least gets the theming much closer.
Need to decide if I want to keep going in here or cut this and do the
rest in a subsequent PR.

## Screenshots (if applicable)

<!-- Add screenshots or video recording to help explain your changes -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5908-WIP-Make-components-themeable-2816d73d365081ffbc05d189fe71084b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Alexander Brown
2025-10-06 16:27:08 -07:00
committed by GitHub
parent 51f0f111ea
commit e7745eb2be
50 changed files with 235 additions and 105 deletions

View File

@@ -24,9 +24,7 @@ export class VueNodeHelpers {
* Get locator for selected Vue node components (using visual selection indicators) * Get locator for selected Vue node components (using visual selection indicators)
*/ */
get selectedNodes(): Locator { get selectedNodes(): Locator {
return this.page.locator( return this.page.locator('[data-node-id].outline-node-component-outline')
'[data-node-id].outline-black, [data-node-id].outline-white'
)
} }
/** /**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -50,17 +50,17 @@ test.describe('Vue Node Collapse', () => {
// Check initial expanded state icon // Check initial expanded state icon
let iconClass = await vueNode.getCollapseIconClass() let iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down') expect(iconClass).not.toContain('-rotate-90')
// Collapse and check icon // Collapse and check icon
await vueNode.toggleCollapse() await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass() iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-right') expect(iconClass).toContain('-rotate-90')
// Expand and check icon // Expand and check icon
await vueNode.toggleCollapse() await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass() iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down') expect(iconClass).not.toContain('-rotate-90')
}) })
test('should preserve title when collapsing/expanding', async ({ test('should preserve title when collapsing/expanding', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -128,12 +128,90 @@
--color-dark-elevation-2: rgba(from white r g b / 0.03); --color-dark-elevation-2: rgba(from white r g b / 0.03);
} }
:root {
--backdrop: var(--color-white);
--dialog-surface: var(--color-neutral-200);
--node-component-border: var(--color-gray-400);
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-stone-200);
--node-component-header-surface: var(--color-white);
--node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-gray-500) r g b / 50%);
--node-component-slot-dot-outline-opacity-mult: 1;
--node-component-slot-dot-outline-opacity: 5%;
--node-component-slot-dot-outline: var(--color-black);
--node-component-slot-text: var(--color-stone-200);
--node-component-surface-highlight: var(--color-stone-100);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-white);
--node-component-tooltip: var(--color-charcoal-700);
--node-component-tooltip-border: var(--color-sand-100);
--node-component-tooltip-surface: var(--color-white);
--node-component-widget-input: var(--fg-color);
--node-component-widget-input-surface: rgb(from var(--color-zinc-500) r g b / 10%);
--node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-stroke: var(--color-stone-100);
}
.dark-theme {
--backdrop: var(--color-neutral-900);
--dialog-surface: var(--color-neutral-700);
--node-component-border: var(--color-stone-200);
--node-component-header-icon: var(--color-slate-300);
--node-component-header-surface: var(--color-charcoal-800);
--node-component-outline: var(--color-white);
--node-component-ring: rgb(var(--color-gray-500) / 20%);
--node-component-slot-dot-outline-opacity: 10%;
--node-component-slot-dot-outline: var(--color-white);
--node-component-slot-text: var(--color-slate-200);
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-charcoal-800);
--node-component-tooltip: var(--color-white);
--node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800);
--node-component-widget-skeleton-surface: var(--color-zinc-800);
--node-stroke: var(--color-slate-100);
}
@theme inline { @theme inline {
--color-node-component-surface: var(--color-charcoal-600); --color-backdrop: var(--backdrop);
--color-node-component-surface-highlight: var(--color-slate-100); --color-dialog-surface: var(--dialog-surface);
--color-node-component-surface-hovered: var(--color-charcoal-400); --color-node-component-border: var(--node-component-border);
--color-node-component-surface-selected: var(--color-charcoal-200); --color-node-component-executing: var(--node-component-executing);
--color-node-stroke: var(--color-stone-100); --color-node-component-header: var(--node-component-header);
--color-node-component-header-icon: var(--node-component-header-icon);
--color-node-component-header-surface: var(--node-component-header-surface);
--color-node-component-outline: var(--node-component-outline);
--color-node-component-ring: var(--node-component-ring);
--color-node-component-slot-dot-outline: rgb(
from var(--node-component-slot-dot-outline) r g b /
calc(
var(--node-component-slot-dot-outline-opacity) *
var(--node-component-slot-dot-outline-opacity-mult)
)
);
--color-node-component-slot-text: var(--node-component-slot-text);
--color-node-component-surface-highlight: var(
--node-component-surface-highlight
);
--color-node-component-surface-hovered: var(--node-component-surface-hovered);
--color-node-component-surface-selected: var(--component-surface-selected);
--color-node-component-surface: var(--node-component-surface);
--color-node-component-tooltip: var(--node-component-tooltip);
--color-node-component-tooltip-border: var(--node-component-tooltip-border);
--color-node-component-tooltip-surface: var(--node-component-tooltip-surface);
--color-node-component-widget-input: var(--node-component-widget-input);
--color-node-component-widget-input-surface: var(
--node-component-widget-input-surface
);
--color-node-component-widget-skeleton-surface: var(
--node-component-widget-skeleton-surface
);
--color-node-stroke: var(--node-stroke);
} }
@custom-variant dark-theme { @custom-variant dark-theme {

View File

@@ -128,7 +128,7 @@
<!-- Title --> <!-- Title -->
<span <span
v-if="isLoading" v-if="isLoading"
class="inline-block h-8 w-48 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse" class="inline-block h-8 w-48 bg-dialog-surface rounded animate-pulse"
></span> ></span>
<!-- Template Cards Grid --> <!-- Template Cards Grid -->
@@ -148,7 +148,7 @@
<CardTop ratio="landscape"> <CardTop ratio="landscape">
<template #default> <template #default>
<div <div
class="w-full h-full bg-neutral-200 dark-theme:bg-neutral-700 animate-pulse" class="w-full h-full bg-dialog-surface animate-pulse"
></div> ></div>
</template> </template>
</CardTop> </CardTop>
@@ -157,10 +157,10 @@
<CardBottom> <CardBottom>
<div class="px-4 py-3"> <div class="px-4 py-3">
<div <div
class="h-6 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse mb-2" class="h-6 bg-dialog-surface rounded animate-pulse mb-2"
></div> ></div>
<div <div
class="h-4 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse" class="h-4 bg-dialog-surface rounded animate-pulse"
></div> ></div>
</div> </div>
</CardBottom> </CardBottom>
@@ -323,7 +323,7 @@
<CardTop ratio="square"> <CardTop ratio="square">
<template #default> <template #default>
<div <div
class="w-full h-full bg-neutral-200 dark-theme:bg-neutral-700 animate-pulse" class="w-full h-full bg-dialog-surface animate-pulse"
></div> ></div>
</template> </template>
</CardTop> </CardTop>
@@ -332,10 +332,10 @@
<CardBottom> <CardBottom>
<div class="px-4 py-3"> <div class="px-4 py-3">
<div <div
class="h-6 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse mb-2" class="h-6 bg-dialog-surface rounded animate-pulse mb-2"
></div> ></div>
<div <div
class="h-4 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse" class="h-4 bg-dialog-surface rounded animate-pulse"
></div> ></div>
</div> </div>
</CardBottom> </CardBottom>

View File

@@ -13,7 +13,7 @@
<div class="relative"> <div class="relative">
<span <span
v-if="shouldShowStatusIndicator" v-if="shouldShowStatusIndicator"
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-secondary-bg) w-4" class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-bg) w-4"
></span ></span
> >
<Button <Button

View File

@@ -246,7 +246,7 @@ onBeforeUnmount(() => {
/> />
<div <div
v-if="filteredActive.length" v-if="filteredActive.length"
class="pt-1 pb-4 border-b-1 border-sand-100 dark-theme:border-charcoal-600" class="pt-1 pb-4 border-b-1 border-node-component-border"
> >
<div class="flex py-0 px-4 justify-between"> <div class="flex py-0 px-4 justify-between">
<div class="text-slate-100 text-[9px] font-semibold uppercase"> <div class="text-slate-100 text-[9px] font-semibold uppercase">
@@ -302,7 +302,7 @@ onBeforeUnmount(() => {
</div> </div>
<div <div
v-if="recommendedWidgets.length" v-if="recommendedWidgets.length"
class="justify-center flex py-4 border-t-1 border-sand-100 dark-theme:border-charcoal-600" class="justify-center flex py-4 border-t-1 border-node-component-border"
> >
<Button <Button
size="small" size="small"

View File

@@ -16,7 +16,7 @@ defineEmits<{
function classes() { function classes() {
return cn( return cn(
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1', 'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1',
'bg-pure-white dark-theme:bg-charcoal-800', 'bg-node-component-surface',
props.isDraggable props.isDraggable
? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing' ? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing'
: '' : ''

View File

@@ -10,7 +10,7 @@ export function showSubgraphNodeDialog() {
position: 'topright', position: 'topright',
pt: { pt: {
root: { root: {
class: 'bg-pure-white dark-theme:bg-charcoal-800 mt-22' class: 'bg-node-component-surface mt-22'
}, },
header: { header: {
class: 'h-8 text-xs ml-3' class: 'h-8 text-xs ml-3'

View File

@@ -45,7 +45,7 @@
</Button> </Button>
<hr <hr
class="absolute top-5 bg-[#E1DED5] dark-theme:bg-[#262729] h-[1px] border-0" class="absolute top-5 bg-node-component-border h-px border-0"
:style="{ :style="{
width: containerStyles.width width: containerStyles.width
}" }"

View File

@@ -71,14 +71,14 @@ const hasSlotError = computed(() => {
const errorClassesDot = computed(() => { const errorClassesDot = computed(() => {
return hasSlotError.value return hasSlotError.value
? 'ring-2 ring-error dark-theme:ring-error ring-offset-0 rounded-full' ? 'ring-2 ring-error ring-offset-0 rounded-full'
: '' : ''
}) })
const labelClasses = computed(() => const labelClasses = computed(() =>
hasSlotError.value hasSlotError.value
? 'text-error dark-theme:text-error font-medium' ? 'text-error font-medium'
: 'dark-theme:text-slate-200 text-stone-200' : 'text-node-component-slot-text'
) )
const renderError = ref<string | null>(null) const renderError = ref<string | null>(null)

View File

@@ -8,12 +8,12 @@
:data-node-id="nodeData.id" :data-node-id="nodeData.id"
:class=" :class="
cn( cn(
'bg-white dark-theme:bg-charcoal-800', 'bg-node-component-surface',
'lg-node absolute rounded-2xl touch-none', 'lg-node absolute rounded-2xl touch-none',
'border-1 border-solid border-gray-400 dark-theme:border-stone-200', 'border-1 border-solid border-node-component-border',
// hover (only when node should handle events) // hover (only when node should handle events)
shouldHandleNodePointerEvents && shouldHandleNodePointerEvents &&
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20', 'hover:ring-7 ring-node-component-ring',
'outline-transparent -outline-offset-2 outline-2', 'outline-transparent -outline-offset-2 outline-2',
borderClass, borderClass,
outlineClass, outlineClass,
@@ -274,8 +274,7 @@ const hasCustomContent = computed(() => {
}) })
// Computed classes and conditions for better reusability // Computed classes and conditions for better reusability
const separatorClasses = const separatorClasses = 'bg-node-component-border h-px mx-0 w-full lod-toggle'
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300' const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState( const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
@@ -287,17 +286,17 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
const borderClass = computed(() => { const borderClass = computed(() => {
return ( return (
(hasAnyError.value && 'border-error dark-theme:border-error') || (hasAnyError.value && 'border-error') ||
(executing.value && 'border-blue-500') (executing.value && 'border-node-executing')
) )
}) })
const outlineClass = computed(() => { const outlineClass = computed(() => {
return ( return cn(
isSelected.value && isSelected.value &&
((hasAnyError.value && 'outline-error dark-theme:outline-error') || ((hasAnyError.value && 'outline-error ') ||
(executing.value && 'outline-blue-500 dark-theme:outline-blue-500') || (executing.value && 'outline-node-executing') ||
'outline-black dark-theme:outline-white') 'outline-node-component-outline')
) )
}) })

View File

@@ -1,13 +1,11 @@
<template> <template>
<div class="scale-75"> <div class="scale-75">
<div <div
class="bg-white dark-theme:bg-charcoal-800 lg-node absolute rounded-2xl border border-solid border-sand-100 dark-theme:border-charcoal-600 outline-transparent -outline-offset-2 outline-2 pointer-events-none" class="bg-node-component-surface lg-node absolute rounded-2xl border border-solid border-node-component-border outline-transparent -outline-offset-2 outline-2 pointer-events-none"
> >
<NodeHeader :node-data="nodeData" :readonly="readonly" /> <NodeHeader :node-data="nodeData" :readonly="readonly" />
<div <div class="bg-node-component-border h-px mx-0 w-full mb-4" />
class="bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full mb-4"
/>
<div class="flex flex-col gap-4 pb-4"> <div class="flex flex-col gap-4 pb-4">
<NodeSlots <NodeSlots

View File

@@ -1,5 +1,5 @@
<template> <template>
<div <div
class="lod-fallback absolute inset-0 w-full h-full bg-zinc-300 dark-theme:bg-zinc-800" class="lod-fallback absolute inset-0 w-full h-full bg-node-component-widget-skeleton-surface"
></div> ></div>
</template> </template>

View File

@@ -181,11 +181,11 @@ describe('NodeHeader.vue', () => {
it('renders correct chevron icon based on collapsed prop', async () => { it('renders correct chevron icon based on collapsed prop', async () => {
const wrapper = mountHeader({ collapsed: false }) const wrapper = mountHeader({ collapsed: false })
const expandedIcon = wrapper.get('i') const expandedIcon = wrapper.get('i')
expect(expandedIcon.classes()).toContain('pi-chevron-down') expect(expandedIcon.classes()).not.toContain('-rotate-90')
await wrapper.setProps({ collapsed: true }) await wrapper.setProps({ collapsed: true })
const collapsedIcon = wrapper.get('i') const collapsedIcon = wrapper.get('i')
expect(collapsedIcon.classes()).toContain('pi-chevron-right') expect(collapsedIcon.classes()).toContain('-rotate-90')
}) })
describe('Tooltips', () => { describe('Tooltips', () => {

View File

@@ -4,24 +4,37 @@
</div> </div>
<div <div
v-else v-else
class="lg-node-header p-4 rounded-t-2xl cursor-move w-full" :class="
cn(
'lg-node-header p-4 rounded-t-2xl cursor-move w-full bg-node-component-header-surface text-node-component-header',
collapsed && 'rounded-2xl'
)
"
:style="headerStyle" :style="headerStyle"
:data-testid="`node-header-${nodeData?.id || ''}`" :data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick" @dblclick="handleDoubleClick"
> >
<div class="flex items-center justify-between gap-2.5 relative"> <div class="flex items-center justify-between gap-2.5 relative">
<!-- Collapse/Expand Button --> <!-- Collapse/Expand Button -->
<button <div class="flex items-center lod-toggle shrink-0 px-0.5">
class="bg-transparent border-transparent flex items-center lod-toggle" <IconButton
data-testid="node-collapse-button" size="fit-content"
@click.stop="handleCollapse" type="transparent"
@dblclick.stop data-testid="node-collapse-button"
> @click.stop="handleCollapse"
<i @dblclick.stop
:class="collapsed ? 'pi pi-chevron-right' : 'pi pi-chevron-down'" >
class="text-xs leading-none relative top-px text-stone-200 dark-theme:text-slate-300" <i
></i> :class="
</button> cn(
'icon-[lucide--chevron-down] size-5 transition-transform',
collapsed && '-rotate-90'
)
"
class="text-xs leading-none relative top-px text-node-component-header-icon"
></i>
</IconButton>
</div>
<!-- Node Title --> <!-- Node Title -->
<div <div
@@ -38,7 +51,7 @@
/> />
<i-lucide:pin <i-lucide:pin
v-if="isPinned" v-if="isPinned"
class="w-5 h-5 text-stone-200 dark-theme:text-slate-300" class="size-5 text-node-component-header-icon"
data-testid="node-pin-indicator" data-testid="node-pin-indicator"
/> />
</div> </div>
@@ -48,12 +61,13 @@
v-tooltip.top="enterSubgraphTooltipConfig" v-tooltip.top="enterSubgraphTooltipConfig"
size="sm" size="sm"
type="transparent" type="transparent"
class="text-stone-200 dark-theme:text-slate-300"
data-testid="subgraph-enter-button" data-testid="subgraph-enter-button"
@click.stop="handleEnterSubgraph" @click.stop="handleEnterSubgraph"
@dblclick.stop @dblclick.stop
> >
<i class="pi pi-external-link"></i> <i
class="icon-[lucide--picture-in-picture] size-5 text-node-component-header-icon"
></i>
</IconButton> </IconButton>
</div> </div>
<LODFallback /> <LODFallback />
@@ -79,6 +93,7 @@ import {
getLocatorIdFromNodeData, getLocatorIdFromNodeData,
getNodeByLocatorId getNodeByLocatorId
} from '@/utils/graphTraversalUtil' } from '@/utils/graphTraversalUtil'
import { cn } from '@/utils/tailwindUtil'
import LODFallback from './LODFallback.vue' import LODFallback from './LODFallback.vue'

View File

@@ -5,7 +5,7 @@
<!-- Slot Name --> <!-- Slot Name -->
<span <span
v-if="!dotOnly" v-if="!dotOnly"
class="whitespace-nowrap text-sm font-normal dark-theme:text-slate-200 text-stone-200 lod-toggle" class="whitespace-nowrap text-sm font-normal text-node-component-slot-text lod-toggle"
> >
{{ slotData.localized_name || slotData.name || `Output ${index}` }} {{ slotData.localized_name || slotData.name || `Output ${index}` }}
</span> </span>

View File

@@ -28,11 +28,11 @@ defineExpose({
:style="{ backgroundColor: color }" :style="{ backgroundColor: color }"
:class=" :class="
cn( cn(
'bg-[#5B5E7D] rounded-full', 'bg-slate-300 rounded-full',
'transition-all duration-150', 'transition-all duration-150',
'cursor-crosshair', 'cursor-crosshair',
'border border-solid border-black/5 dark-theme:border-white/10', 'border border-solid border-node-component-slot-dot-outline',
'group-hover/slot:border-black/20 dark-theme:group-hover/slot:border-white/50 group-hover/slot:scale-125', 'group-hover/slot:[--node-component-slot-dot-outline-opacity-mult:5] group-hover/slot:scale-125',
multi ? 'w-3 h-6' : 'size-3' multi ? 'w-3 h-6' : 'size-3'
) )
" "

View File

@@ -163,14 +163,14 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
pt: { pt: {
text: { text: {
class: class:
'border-sand-100 bg-pure-white dark-theme:bg-charcoal-800 border dark-theme:border-slate-300 rounded-md px-4 py-2 text-charcoal-700 dark-theme:text-pure-white text-sm font-normal leading-tight max-w-75 shadow-none' 'border-node-component-tooltip-border bg-node-component-tooltip-surface border rounded-md px-4 py-2 text-node-component-tooltip text-sm font-normal leading-tight max-w-75 shadow-none'
}, },
arrow: ({ context }: TooltipPassThroughMethodOptions) => ({ arrow: ({ context }: TooltipPassThroughMethodOptions) => ({
class: cn( class: cn(
context.top && 'border-t-sand-100 dark-theme:border-t-slate-300', context.top && 'border-t-node-component-tooltip-border',
context.bottom && 'border-b-sand-100 dark-theme:border-b-slate-300', context.bottom && 'border-b-node-component-tooltip-border',
context.left && 'border-l-sand-100 dark-theme:border-l-slate-300', context.left && 'border-l-node-component-tooltip-border ',
context.right && 'border-r-sand-100 dark-theme:border-r-slate-300' context.right && 'border-r-node-component-tooltip-border'
) )
}) })
} as TooltipDirectivePassThroughOptions } as TooltipDirectivePassThroughOptions

View File

@@ -145,7 +145,7 @@
:style="{ borderColor: '#262729' }" :style="{ borderColor: '#262729' }"
> >
<div <div
class="border border-dashed p-1 rounded-md transition-colors duration-200 hover:border-[#5B5E7D]" class="border border-dashed p-1 rounded-md transition-colors duration-200 hover:border-slate-300"
:style="{ borderColor: '#262729' }" :style="{ borderColor: '#262729' }"
> >
<div class="flex flex-col items-center gap-2 w-full py-4"> <div class="flex flex-col items-center gap-2 w-full py-4">

View File

@@ -38,14 +38,13 @@ const chevronClass = computed(() =>
}) })
) )
const theButtonStyle = computed(() => [ const theButtonStyle = computed(() =>
'bg-transparent border-0 outline-none text-zinc-400', cn('bg-transparent border-0 outline-none text-zinc-400', {
{ 'hover:bg-node-component-widget-input-surface/30 cursor-pointer':
'hover:bg-zinc-500/30 hover:text-black hover:dark-theme:text-white cursor-pointer':
!props.disabled, !props.disabled,
'cursor-not-allowed': props.disabled 'cursor-not-allowed': props.disabled
} })
]) )
</script> </script>
<template> <template>

View File

@@ -36,7 +36,7 @@ const searchQuery = defineModel<string>('searchQuery')
<template> <template>
<div <div
class="w-103 h-[640px] pt-4 bg-white dark-theme:bg-charcoal-800 rounded-lg outline outline-offset-[-1px] outline-sand-100 dark-theme:outline-zinc-800 flex flex-col" class="w-103 max-h-[640px] pt-4 bg-node-component-surface rounded-lg outline outline-offset-[-1px] outline-node-component-border flex flex-col"
> >
<!-- Filter --> <!-- Filter -->
<FormDropdownMenuFilter <FormDropdownMenuFilter
@@ -67,7 +67,7 @@ const searchQuery = defineModel<string>('searchQuery')
" "
> >
<div <div
class="absolute top-0 inset-x-3 h-5 bg-gradient-to-b from-white dark-theme:from-neutral-900 to-transparent pointer-events-none z-10" class="absolute top-0 inset-x-3 h-5 bg-gradient-to-b from-backdrop to-transparent pointer-events-none z-10"
/> />
<div <div
v-if="items.length === 0" v-if="items.length === 0"

View File

@@ -17,7 +17,7 @@ defineProps<{
<div class="relative h-6 flex items-center mr-4"> <div class="relative h-6 flex items-center mr-4">
<p <p
v-if="widget.name" v-if="widget.name"
class="text-sm text-stone-200 dark-theme:text-slate-200 font-normal flex-1 truncate w-20 lod-toggle" class="text-sm text-node-component-slot-text font-normal flex-1 truncate w-20 lod-toggle"
> >
{{ widget.label || widget.name }} {{ widget.label || widget.name }}
</p> </p>

View File

@@ -2,15 +2,13 @@ import { cn } from '@/utils/tailwindUtil'
export const WidgetInputBaseClass = cn([ export const WidgetInputBaseClass = cn([
// Background // Background
'bg-zinc-500/10', 'bg-node-component-widget-input-surface',
'text-node-component-widget-input',
// Outline // Outline
'border-none', 'border-none',
'outline', 'outline outline-offset-[-1px] outline-zinc-300/10',
'outline-1',
'outline-offset-[-1px]',
'outline-zinc-300/10',
// Rounded // Rounded
'!rounded-lg', 'rounded-lg',
// Hover // Hover
'hover:outline-blue-500/80' 'hover:outline-blue-500/80'
]) ])

View File

@@ -12,6 +12,16 @@ import { downloadBlob, uploadFile } from '@/scripts/utils'
import { useNodeDefStore } from '@/stores/nodeDefStore' import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const THEME_PROPERTY_MAP = {
NODE_BOX_OUTLINE_COLOR: 'node-component-border',
NODE_DEFAULT_BGCOLOR: 'node-component-surface',
NODE_DEFAULT_BOXCOLOR: 'node-component-header-icon',
NODE_DEFAULT_COLOR: 'node-component-header-surface',
NODE_TITLE_COLOR: 'node-component-header',
WIDGET_BGCOLOR: 'node-component-widget-input-surface',
WIDGET_TEXT_COLOR: 'node-component-widget-input'
} as const satisfies Partial<Record<keyof Colors['litegraph_base'], string>>
export const useColorPaletteService = () => { export const useColorPaletteService = () => {
const colorPaletteStore = useColorPaletteStore() const colorPaletteStore = useColorPaletteStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
@@ -78,6 +88,40 @@ export const useColorPaletteService = () => {
Object.assign(LGraphCanvas.link_type_colors, types, linkColorPalette) Object.assign(LGraphCanvas.link_type_colors, types, linkColorPalette)
} }
function validThemeProp(
propertyMaybe: unknown
): propertyMaybe is keyof typeof THEME_PROPERTY_MAP {
return (
(propertyMaybe as keyof typeof THEME_PROPERTY_MAP) in THEME_PROPERTY_MAP
)
}
function loadLitegraphForVueNodes(
palette: Colors['litegraph_base'],
colorPaletteId: string
) {
if (!palette) return
const rootStyle = document.getElementById('vue-app')?.style
if (!rootStyle) return
for (const themeVar of Object.keys(THEME_PROPERTY_MAP)) {
if (!validThemeProp(themeVar)) {
continue
}
const cssVar = THEME_PROPERTY_MAP[themeVar]
if (colorPaletteId === 'dark' || colorPaletteId === 'light') {
rootStyle.removeProperty(`--${cssVar}`)
continue
}
const valueMaybe = palette[themeVar]
if (valueMaybe) {
rootStyle.setProperty(`--${cssVar}`, valueMaybe)
} else {
rootStyle.removeProperty(`--${cssVar}`)
}
}
}
/** /**
* Loads the LiteGraph color palette. * Loads the LiteGraph color palette.
* *
@@ -120,20 +164,19 @@ export const useColorPaletteService = () => {
* @param comfyColorPalette - The palette to set. * @param comfyColorPalette - The palette to set.
*/ */
const loadComfyColorPalette = (comfyColorPalette: Colors['comfy_base']) => { const loadComfyColorPalette = (comfyColorPalette: Colors['comfy_base']) => {
if (comfyColorPalette) { if (!comfyColorPalette) return
const rootStyle = document.documentElement.style const rootStyle = document.documentElement.style
for (const [key, value] of Object.entries(comfyColorPalette)) { for (const [key, value] of Object.entries(comfyColorPalette)) {
rootStyle.setProperty('--' + key, value) rootStyle.setProperty('--' + key, value)
} }
const backgroundImage = settingStore.get('Comfy.Canvas.BackgroundImage') const backgroundImage = settingStore.get('Comfy.Canvas.BackgroundImage')
if (backgroundImage) { if (backgroundImage) {
rootStyle.setProperty( rootStyle.setProperty(
'--bg-img', '--bg-img',
`url('${backgroundImage}') no-repeat center /cover` `url('${backgroundImage}') no-repeat center /cover`
) )
} else { } else {
rootStyle.removeProperty('--bg-img') rootStyle.removeProperty('--bg-img')
}
} }
} }
@@ -151,6 +194,10 @@ export const useColorPaletteService = () => {
const completedPalette = colorPaletteStore.completePalette(colorPalette) const completedPalette = colorPaletteStore.completePalette(colorPalette)
loadLinkColorPalette(completedPalette.colors.node_slot) loadLinkColorPalette(completedPalette.colors.node_slot)
loadLiteGraphColorPalette(completedPalette.colors.litegraph_base) loadLiteGraphColorPalette(completedPalette.colors.litegraph_base)
loadLitegraphForVueNodes(
completedPalette.colors.litegraph_base,
colorPaletteId
)
loadComfyColorPalette(completedPalette.colors.comfy_base) loadComfyColorPalette(completedPalette.colors.comfy_base)
app.canvas.setDirty(true, true) app.canvas.setDirty(true, true)

View File

@@ -121,8 +121,7 @@ export const useDialogService = () => {
pt: { pt: {
pcCloseButton: { pcCloseButton: {
root: { root: {
class: class: 'bg-dialog-surface w-9 h-9 p-1.5 rounded-full text-white'
'bg-gray-500 dark-theme:bg-neutral-700 w-9 h-9 p-1.5 rounded-full text-white'
} }
}, },
header: { class: 'py-0! px-6 m-0! h-[68px]' }, header: { class: 'py-0! px-6 m-0! h-[68px]' },
@@ -469,7 +468,7 @@ export const useDialogService = () => {
pcCloseButton: { pcCloseButton: {
root: { root: {
class: class:
'!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5 bg-gray-500 dark-theme:bg-neutral-700 text-white' '!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5 bg-dialog-surface text-white'
} }
} }
}, },

View File

@@ -6,7 +6,7 @@
" "
class="inline-flex items-center gap-1 rounded-2xl text-xs py-1" class="inline-flex items-center gap-1 rounded-2xl text-xs py-1"
:class="{ :class="{
'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill, 'bg-dialog-surface px-1.5': fill,
'cursor-pointer': !isDisabled, 'cursor-pointer': !isDisabled,
'cursor-not-allowed opacity-60': isDisabled 'cursor-not-allowed opacity-60': isDisabled
}" }"

View File

@@ -171,8 +171,7 @@ describe('LGraphNode', () => {
mockData.mockNodeIds = new Set(['test-node-123']) mockData.mockNodeIds = new Set(['test-node-123'])
const wrapper = mountLGraphNode({ nodeData: mockNodeData }) const wrapper = mountLGraphNode({ nodeData: mockNodeData })
expect(wrapper.classes()).toContain('outline-2') expect(wrapper.classes()).toContain('outline-2')
expect(wrapper.classes()).toContain('outline-black') expect(wrapper.classes()).toContain('outline-node-component-outline')
expect(wrapper.classes()).toContain('dark-theme:outline-white')
}) })
it('should apply executing animation when executing prop is true', () => { it('should apply executing animation when executing prop is true', () => {