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

@@ -45,7 +45,7 @@
</Button>
<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="{
width: containerStyles.width
}"

View File

@@ -71,14 +71,14 @@ const hasSlotError = computed(() => {
const errorClassesDot = computed(() => {
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(() =>
hasSlotError.value
? 'text-error dark-theme:text-error font-medium'
: 'dark-theme:text-slate-200 text-stone-200'
? 'text-error font-medium'
: 'text-node-component-slot-text'
)
const renderError = ref<string | null>(null)

View File

@@ -8,12 +8,12 @@
:data-node-id="nodeData.id"
:class="
cn(
'bg-white dark-theme:bg-charcoal-800',
'bg-node-component-surface',
'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)
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',
borderClass,
outlineClass,
@@ -274,8 +274,7 @@ const hasCustomContent = computed(() => {
})
// Computed classes and conditions for better reusability
const separatorClasses =
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
const separatorClasses = 'bg-node-component-border h-px mx-0 w-full lod-toggle'
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
@@ -287,17 +286,17 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
const borderClass = computed(() => {
return (
(hasAnyError.value && 'border-error dark-theme:border-error') ||
(executing.value && 'border-blue-500')
(hasAnyError.value && 'border-error') ||
(executing.value && 'border-node-executing')
)
})
const outlineClass = computed(() => {
return (
return cn(
isSelected.value &&
((hasAnyError.value && 'outline-error dark-theme:outline-error') ||
(executing.value && 'outline-blue-500 dark-theme:outline-blue-500') ||
'outline-black dark-theme:outline-white')
((hasAnyError.value && 'outline-error ') ||
(executing.value && 'outline-node-executing') ||
'outline-node-component-outline')
)
})

View File

@@ -1,13 +1,11 @@
<template>
<div class="scale-75">
<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" />
<div
class="bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full mb-4"
/>
<div class="bg-node-component-border h-px mx-0 w-full mb-4" />
<div class="flex flex-col gap-4 pb-4">
<NodeSlots

View File

@@ -1,5 +1,5 @@
<template>
<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>
</template>

View File

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

View File

@@ -4,24 +4,37 @@
</div>
<div
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"
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between gap-2.5 relative">
<!-- Collapse/Expand Button -->
<button
class="bg-transparent border-transparent flex items-center lod-toggle"
data-testid="node-collapse-button"
@click.stop="handleCollapse"
@dblclick.stop
>
<i
: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>
</button>
<div class="flex items-center lod-toggle shrink-0 px-0.5">
<IconButton
size="fit-content"
type="transparent"
data-testid="node-collapse-button"
@click.stop="handleCollapse"
@dblclick.stop
>
<i
:class="
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 -->
<div
@@ -38,7 +51,7 @@
/>
<i-lucide:pin
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"
/>
</div>
@@ -48,12 +61,13 @@
v-tooltip.top="enterSubgraphTooltipConfig"
size="sm"
type="transparent"
class="text-stone-200 dark-theme:text-slate-300"
data-testid="subgraph-enter-button"
@click.stop="handleEnterSubgraph"
@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>
</div>
<LODFallback />
@@ -79,6 +93,7 @@ import {
getLocatorIdFromNodeData,
getNodeByLocatorId
} from '@/utils/graphTraversalUtil'
import { cn } from '@/utils/tailwindUtil'
import LODFallback from './LODFallback.vue'

View File

@@ -5,7 +5,7 @@
<!-- Slot Name -->
<span
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}` }}
</span>

View File

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

View File

@@ -163,14 +163,14 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
pt: {
text: {
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) => ({
class: cn(
context.top && 'border-t-sand-100 dark-theme:border-t-slate-300',
context.bottom && 'border-b-sand-100 dark-theme:border-b-slate-300',
context.left && 'border-l-sand-100 dark-theme:border-l-slate-300',
context.right && 'border-r-sand-100 dark-theme:border-r-slate-300'
context.top && 'border-t-node-component-tooltip-border',
context.bottom && 'border-b-node-component-tooltip-border',
context.left && 'border-l-node-component-tooltip-border ',
context.right && 'border-r-node-component-tooltip-border'
)
})
} as TooltipDirectivePassThroughOptions

View File

@@ -145,7 +145,7 @@
:style="{ borderColor: '#262729' }"
>
<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' }"
>
<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(() => [
'bg-transparent border-0 outline-none text-zinc-400',
{
'hover:bg-zinc-500/30 hover:text-black hover:dark-theme:text-white cursor-pointer':
const theButtonStyle = computed(() =>
cn('bg-transparent border-0 outline-none text-zinc-400', {
'hover:bg-node-component-widget-input-surface/30 cursor-pointer':
!props.disabled,
'cursor-not-allowed': props.disabled
}
])
})
)
</script>
<template>

View File

@@ -36,7 +36,7 @@ const searchQuery = defineModel<string>('searchQuery')
<template>
<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 -->
<FormDropdownMenuFilter
@@ -67,7 +67,7 @@ const searchQuery = defineModel<string>('searchQuery')
"
>
<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
v-if="items.length === 0"

View File

@@ -17,7 +17,7 @@ defineProps<{
<div class="relative h-6 flex items-center mr-4">
<p
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 }}
</p>

View File

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