fix: arbitrary styles, min size <= content, ensure layout calc, trunc… (#6731)

## Summary

### Problem:
After [vue node compacting
PR](https://github.com/Comfy-Org/ComfyUI_frontend/pull/6687) the white
space within the node has been greatly reduced, lowering the min
intrinsic size, thus allowing us to reduce the amount we need to scale
up via ensureCorrectLayoutScale(), therefore increasing readability of
nodes. Great!

However, a side effect of reducing the scale factor means nodes with
larger min content will not be scaled up enough causing nodes to be too
large in many cases.

For example, if the min intrinsic width is very long due to input
length:
<img width="807" height="519" alt="image"
src="https://github.com/user-attachments/assets/a6ea3852-bed5-49b2-b10e-c2e65c6450b2"
/>

### Solution:
Allow for nodes to be resized less than their intrinsic min width. And
truncate widget inputs like many other node UIs do.

IMPORTANT: when a node is added via search or other, it will still get a
min size based on its intrinsic content it just wont be the min width!
So best of both worlds.

<img width="670" height="551" alt="image"
src="https://github.com/user-attachments/assets/f4f5ec8c-037e-472f-a5a1-d8a59a87c0b0"
/>


this means we choose a default min width and clamp resize to it. This
also means we have to remove the arbitrary min width values that were
sprinkled around the vue node widgets. They are not needed because
instead of min width, they can take up full width and inherit the sizing
from the node min width! This makes nodes like little browser windows
and widgets are just responsive elements with in. Much more natural imo.

### Bonus
- Set ensureCorrectLayouScale() to scale factor of 1.2 which means vue
nodes are now only being set 20% bigger than LG. That covers for the
height difference we cant change!
- Fix ensureCorrectLayouScale() to offset y position for groups / better
alignment
- Get rid of arbitrary inflexible min width like min-[417px] which
shouldnt have been used the first place
- Make Select and Input overlay portals width set to their content


## Changes

**What**: 
- Node resizing behavior
- Node widget min width
- Widget input and slot truncation
- Misc arbitrary styling that should have been fluid

## Screenshots (if applicable)


https://github.com/user-attachments/assets/3ea4b8fe-565a-47f7-b3ab-6cef56cecde5


https://github.com/user-attachments/assets/2fe1e1a0-a9dc-4000-b865-ce2d8c7f3606


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6731-fix-arbitrary-styles-min-size-content-ensure-layout-calc-trunc-2af6d73d365081eab507c2f1638a4194)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Simula_r
2025-11-18 12:52:23 -08:00
committed by GitHub
parent 7a0302ba7a
commit 0cff8eb357
43 changed files with 86 additions and 58 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -11,12 +11,12 @@
>
<!-- Video Wrapper -->
<div
class="relative h-88 w-full grow overflow-hidden rounded-[5px] bg-node-component-surface"
class="relative h-full w-full grow overflow-hidden rounded-[5px] bg-node-component-surface"
>
<!-- Error State -->
<div
v-if="videoError"
class="flex size-full flex-col items-center justify-center bg-smoke-800/50 text-center text-white"
class="flex size-full flex-col items-center justify-center bg-smoke-800/50 text-center text-white py-8"
>
<i class="mb-2 icon-[lucide--video-off] h-12 w-12 text-smoke-400" />
<p class="text-sm text-smoke-300">{{ $t('g.videoFailedToLoad') }}</p>

View File

@@ -11,12 +11,12 @@
>
<!-- Image Wrapper -->
<div
class="min-h-88 w-full overflow-hidden rounded-[5px] bg-node-component-surface"
class="h-full w-full overflow-hidden rounded-[5px] bg-node-component-surface"
>
<!-- Error State -->
<div
v-if="imageError"
class="flex size-full flex-col items-center justify-center bg-smoke-800/50 text-center text-white"
class="flex size-full flex-col items-center justify-center bg-smoke-800/50 text-center text-white py-8"
>
<i class="mb-2 icon-[lucide--image-off] h-12 w-12 text-smoke-400" />
<p class="text-sm text-smoke-300">{{ $t('g.imageFailedToLoad') }}</p>

View File

@@ -10,12 +10,10 @@
/>
<!-- Slot Name -->
<div class="relative h-full flex items-center">
<div class="relative h-full flex items-center min-w-0">
<span
v-if="!dotOnly"
:class="
cn('whitespace-nowrap text-xs font-normal lod-toggle', labelClasses)
"
:class="cn('truncate text-xs font-normal lod-toggle', labelClasses)"
>
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
</span>

View File

@@ -9,7 +9,8 @@
:class="
cn(
'bg-component-node-background lg-node absolute',
'h-min w-min contain-style contain-layout min-h-(--node-height) min-w-(--node-width)',
'contain-style contain-layout min-w-[225px] min-h-(--node-height) w-(--node-width)',
'rounded-2xl touch-none flex flex-col',
'border-1 border-solid border-component-node-border',
// hover (only when node should handle events)
@@ -100,7 +101,7 @@
<!-- Node Body - rendered based on LOD level and collapsed state -->
<div
class="flex min-h-min min-w-min flex-1 flex-col gap-1 pb-2"
class="flex flex-1 flex-col gap-1 pb-2"
:data-testid="`node-body-${nodeData.id}`"
>
<!-- Slots only rendered at full detail -->
@@ -343,12 +344,17 @@ const cornerResizeHandles: CornerResizeHandle[] = [
}
]
const MIN_NODE_WIDTH = 225
const { startResize } = useNodeResize(
(result, element) => {
if (isCollapsed.value) return
// Clamp width to minimum to avoid conflicts with CSS min-width
const clampedWidth = Math.max(result.size.width, MIN_NODE_WIDTH)
// Apply size directly to DOM element - ResizeObserver will pick this up
element.style.setProperty('--node-width', `${result.size.width}px`)
element.style.setProperty('--node-width', `${clampedWidth}px`)
element.style.setProperty('--node-height', `${result.size.height}px`)
const currentPosition = position.value

View File

@@ -6,7 +6,7 @@
v-else
:class="
cn(
'lg-node-header py-2 pl-2 pr-3 text-sm rounded-t-2xl w-full min-w-50',
'lg-node-header py-2 pl-2 pr-3 text-sm rounded-t-2xl w-full min-w-0',
'text-node-component-header bg-node-component-header-surface',
collapsed && 'rounded-2xl'
)
@@ -15,9 +15,9 @@
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between gap-2.5">
<div class="flex items-center justify-between gap-2.5 min-w-0">
<!-- Collapse/Expand Button -->
<div class="relative grow-1 flex items-center gap-2.5">
<div class="relative grow-1 flex items-center gap-2.5 min-w-0 flex-1">
<div class="lod-toggle flex shrink-0 items-center px-0.5">
<IconButton
size="fit-content"
@@ -44,16 +44,18 @@
<!-- Node Title -->
<div
v-tooltip.top="tooltipConfig"
class="lod-toggle grow-1 items-center gap-2 truncate text-sm font-bold w-15"
class="lod-toggle flex min-w-0 flex-1 items-center gap-2 text-sm font-bold"
data-testid="node-title"
>
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
:input-attrs="{ 'data-testid': 'node-title-input' }"
@edit="handleTitleEdit"
@cancel="handleTitleCancel"
/>
<div class="truncate min-w-0 flex-1">
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
:input-attrs="{ 'data-testid': 'node-title-input' }"
@edit="handleTitleEdit"
@cancel="handleTitleCancel"
/>
</div>
</div>
<LODFallback />
</div>

View File

@@ -2,10 +2,10 @@
<div v-if="renderError" class="node-error p-2 text-sm text-red-500">
{{ st('nodeErrors.slots', 'Node Slots Error') }}
</div>
<div v-else :class="cn('flex justify-between', unifiedWrapperClass)">
<div v-else :class="cn('flex justify-between min-w-0', unifiedWrapperClass)">
<div
v-if="filteredInputs.length"
:class="cn('flex flex-col', unifiedDotsClass)"
:class="cn('flex flex-col min-w-0', unifiedDotsClass)"
>
<InputSlot
v-for="(input, index) in filteredInputs"
@@ -19,7 +19,7 @@
<div
v-if="nodeData?.outputs?.length"
:class="cn('ml-auto flex flex-col', unifiedDotsClass)"
:class="cn('ml-auto flex flex-col min-w-0', unifiedDotsClass)"
>
<OutputSlot
v-for="(output, index) in nodeData.outputs"

View File

@@ -1,11 +1,11 @@
<template>
<div v-if="renderError" class="node-error p-1 text-xs text-red-500"></div>
<div v-else v-tooltip.right="tooltipConfig" :class="slotWrapperClass">
<div class="relative h-full flex items-center">
<div class="relative h-full flex items-center min-w-0">
<!-- Slot Name -->
<span
v-if="!dotOnly"
class="lod-toggle text-xs font-normal whitespace-nowrap text-node-component-slot-text"
class="lod-toggle text-xs font-normal truncate text-node-component-slot-text"
>
{{ slotData.localized_name || slotData.name || `Output ${index}` }}
</span>

View File

@@ -10,7 +10,7 @@ import { app as comfyApp } from '@/scripts/app'
import type { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode'
import type { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOutputNode'
const SCALE_FACTOR = 1.75
const SCALE_FACTOR = 1.2
export function ensureCorrectLayoutScale(
renderer?: rendererType,
@@ -72,23 +72,32 @@ export function ensureCorrectLayoutScale(
? 1 / SCALE_FACTOR
: 1
//TODO: once we remove the need for LiteGraph.NODE_TITLE_HEIGHT in vue nodes we nned to remove everything here.
for (const node of graph.nodes) {
const lgNode = lgNodesById.get(node.id)
if (!lgNode) continue
const lgBodyY = lgNode.pos[1]
const adjustedY = needsDownscale
? lgBodyY - LiteGraph.NODE_TITLE_HEIGHT / 2
: lgBodyY
const relativeX = lgNode.pos[0] - originX
const relativeY = lgBodyY - originY
const relativeY = adjustedY - originY
const newX = originX + relativeX * scaleFactor
const newY = originY + relativeY * scaleFactor
const scaledY = originY + relativeY * scaleFactor
const newWidth = lgNode.width * scaleFactor
const newHeight = lgNode.height * scaleFactor
const finalY = needsUpscale
? scaledY + LiteGraph.NODE_TITLE_HEIGHT / 2
: scaledY
// Directly update LiteGraph node to ensure immediate consistency
// Dont need to reference vue directly because the pos and dims are already in yjs
lgNode.pos[0] = newX
lgNode.pos[1] = newY
lgNode.pos[1] = finalY
lgNode.size[0] = newWidth
lgNode.size[1] =
newHeight - (needsDownscale ? LiteGraph.NODE_TITLE_HEIGHT : 0)
@@ -99,7 +108,7 @@ export function ensureCorrectLayoutScale(
nodeId: String(lgNode.id),
bounds: {
x: newX,
y: newY,
y: finalY,
width: newWidth,
height: newHeight - (needsDownscale ? LiteGraph.NODE_TITLE_HEIGHT : 0)
}

View File

@@ -16,9 +16,11 @@
}"
@update:model-value="onPickerUpdate"
/>
<span class="text-xs" data-testid="widget-color-text">{{
toHexFromFormat(localValue, format)
}}</span>
<span
class="text-xs truncate min-w-[4ch]"
data-testid="widget-color-text"
>{{ toHexFromFormat(localValue, format) }}</span
>
</label>
</WidgetLayoutField>
</template>

View File

@@ -26,7 +26,8 @@
size="small"
:pt="{
option: 'text-xs',
dropdownIcon: 'text-component-node-foreground-secondary'
dropdownIcon: 'text-component-node-foreground-secondary',
overlay: 'w-fit min-w-full'
}"
/>
<Button
@@ -97,7 +98,8 @@
size="small"
:pt="{
option: 'text-xs',
dropdownIcon: 'text-component-node-foreground-secondary'
dropdownIcon: 'text-component-node-foreground-secondary',
overlay: 'w-fit min-w-full'
}"
/>
<Button

View File

@@ -97,7 +97,8 @@ const buttonTooltip = computed(() => {
:show-buttons="!buttonsDisabled"
:pt="{
root: {
class: '[&>input]:bg-transparent [&>input]:border-0'
class:
'[&>input]:bg-transparent [&>input]:border-0 [&>input]:truncate [&>input]:min-w-[4ch]'
},
decrementButton: {
class: 'w-8 border-0'

View File

@@ -18,7 +18,7 @@
:max-fraction-digits="precision"
:aria-label="widget.name"
size="small"
pt:pc-input-text:root="min-w-full bg-transparent border-none text-center"
pt:pc-input-text:root="min-w-[4ch] bg-transparent border-none text-center truncate"
class="w-16"
:pt="sliderNumberPt"
@update:model-value="handleNumberInputUpdate"

View File

@@ -6,6 +6,7 @@
:class="cn(WidgetInputBaseClass, 'w-full text-xs py-2 px-4')"
:aria-label="widget.name"
size="small"
:pt="{ root: 'truncate min-w-[4ch]' }"
@update:model-value="onChange"
/>
</WidgetLayoutField>

View File

@@ -10,7 +10,8 @@
display="chip"
:pt="{
option: 'text-xs',
dropdownIcon: 'text-component-node-foreground-secondary'
dropdownIcon: 'text-component-node-foreground-secondary',
overlay: 'w-fit min-w-full'
}"
@update:model-value="onChange"
/>

View File

@@ -2,7 +2,7 @@
<div class="relative">
<div class="mb-4">
<Button
class="text-text-secondary w-[413px] border-0 bg-secondary-background hover:bg-secondary-background-hover"
class="text-text-secondary w-full border-0 bg-secondary-background hover:bg-secondary-background-hover"
:disabled="isRecording || readonly"
@click="handleStartRecording"
>
@@ -12,7 +12,7 @@
</div>
<div
v-if="isRecording || isPlaying || recordedURL"
class="flex h-14 w-[413px] items-center gap-4 rounded-lg px-4 bg-node-component-surface text-text-secondary"
class="flex h-14 w-full items-center gap-4 rounded-lg px-4 bg-node-component-surface text-text-secondary"
>
<!-- Recording Status -->
<div class="flex min-w-30 items-center gap-2">

View File

@@ -10,7 +10,9 @@
size="small"
:pt="{
option: 'text-xs',
dropdown: 'w-8'
dropdown: 'w-8',
label: 'truncate min-w-[4ch]',
overlay: 'w-fit min-w-full'
}"
data-capture-wheel="true"
@update:model-value="onChange"

View File

@@ -7,7 +7,8 @@
:aria-label="widget.name"
size="small"
:pt="{
dropdownIcon: 'text-component-node-foreground-secondary'
dropdownIcon: 'text-component-node-foreground-secondary',
overlay: 'w-fit min-w-full'
}"
@update:model-value="onChange"
/>

View File

@@ -12,7 +12,7 @@
:key="getOptionValue(option, index)"
:class="
cn(
'flex-1 h-6 px-5 py-[5px] rounded flex justify-center items-center gap-1 transition-all duration-150 ease-in-out',
'flex-1 h-6 px-5 py-[5px] rounded flex justify-center items-center gap-1 transition-all duration-150 ease-in-out truncate min-w-[4ch]',
'bg-transparent border-none',
'text-center text-xs font-normal',
{

View File

@@ -65,18 +65,22 @@ const theButtonStyle = computed(() =>
<!-- Dropdown -->
<button
:class="
cn(theButtonStyle, 'flex justify-between items-center flex-1 h-8', {
'rounded-l-lg': uploadable,
'rounded-lg': !uploadable
})
cn(
theButtonStyle,
'flex justify-between items-center flex-1 min-w-0 h-8',
{
'rounded-l-lg': uploadable,
'rounded-lg': !uploadable
}
)
"
@click="emit('select-click', $event)"
>
<span class="min-w-0 px-4 py-2 text-left">
<span v-if="!selectedItems.length" class="min-w-0">
<span class="min-w-0 flex-1 px-1 py-2 text-left truncate">
<span v-if="!selectedItems.length">
{{ props.placeholder }}
</span>
<span v-else class="line-clamp-1 min-w-0 break-all">
<span v-else>
{{ selectedItems.map((item) => item.label ?? item.name).join(', ') }}
</span>
</span>

View File

@@ -11,10 +11,8 @@ defineProps<{
</script>
<template>
<div class="flex h-[30px] min-w-78 items-center justify-between gap-1">
<div
class="relative flex h-full basis-content min-w-20 flex-1 items-center"
>
<div class="flex h-[30px] min-w-0 items-center justify-between gap-1">
<div class="relative flex h-full min-w-0 w-20 items-center">
<p
v-if="widget.name"
class="lod-toggle flex-1 truncate text-xs font-normal text-node-component-slot-text"
@@ -23,9 +21,10 @@ defineProps<{
</p>
<LODFallback />
</div>
<div class="relative min-w-56 basis-full grow">
<!-- basis-full grow -->
<div class="relative min-w-0 flex-1">
<div
class="lod-toggle cursor-default"
class="lod-toggle cursor-default min-w-0"
@pointerdown.stop="noop"
@pointermove.stop="noop"
@pointerup.stop="noop"