feat: add visual indicator for list output slots (#8766)

## Summary

Add rounded square dot shape and "(Iterative)" tooltip for list-type
output slots in Vue nodes, matching litegraph's visual indicator.

## Changes

- **What**: `SlotConnectionDot.vue` renders `rounded-[1px]` instead of
`rounded-full` when slot shape is `RenderShape.GRID`. `OutputSlot.vue`
appends "(Iterative)" to the tooltip for these slots.

<img width="807" height="542" alt="Screenshot 2026-02-10 at 03 38 42"
src="https://github.com/user-attachments/assets/137b60c5-ac3b-457f-a52d-58f5f28a59ea"
/>


## Review Focus
- i18n key added for the iterative suffix

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8766-feat-add-visual-indicator-for-list-output-slots-3036d73d3650813aad85ce094d29c42b)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2026-02-11 01:49:58 +01:00
committed by GitHub
parent da56c9e554
commit 7f30d6b6a5
4 changed files with 59 additions and 3 deletions

View File

@@ -2834,6 +2834,9 @@
}
}
},
"vueNodesSlot": {
"iterative": "(Iterative)"
},
"vueNodesBanner": {
"title": "Introducing Nodes 2.0",
"desc": " More flexible workflows, powerful new widgets, built for extensibility",

View File

@@ -23,9 +23,11 @@
<script setup lang="ts">
import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { RenderShape } from '@/lib/litegraph/src/types/globalEnums'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
@@ -47,6 +49,8 @@ interface OutputSlotProps {
const props = defineProps<OutputSlotProps>()
const { t } = useI18n()
const hasNoLabel = computed(
() => !props.slotData.localized_name && props.slotData.name === ''
)
@@ -65,7 +69,11 @@ const tooltipConfig = computed(() => {
const slotName = props.slotData.name || ''
const tooltipText = getOutputSlotTooltip(props.index)
const fallbackText = tooltipText || `Output: ${slotName}`
return createTooltipConfig(fallbackText)
const iterativeSuffix =
props.slotData.shape === RenderShape.GRID
? ` ${t('vueNodesSlot.iterative')}`
: ''
return createTooltipConfig(fallbackText + iterativeSuffix)
})
onErrorCaptured((error) => {

View File

@@ -0,0 +1,41 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { RenderShape } from '@/lib/litegraph/src/types/globalEnums'
import SlotConnectionDot from './SlotConnectionDot.vue'
const defaultSlot: INodeSlot = {
name: 'output',
type: 'IMAGE',
boundingRect: [0, 0, 0, 0]
}
function mountDot(slotData?: INodeSlot) {
return mount(SlotConnectionDot, {
props: { slotData }
})
}
describe('SlotConnectionDot', () => {
it('renders circle shape by default', () => {
const wrapper = mountDot(defaultSlot)
const dot = wrapper.find('.slot-dot')
expect(dot.classes()).toContain('rounded-full')
expect(dot.element.tagName).toBe('DIV')
})
it('renders rounded square for GRID shape', () => {
const wrapper = mountDot({
...defaultSlot,
shape: RenderShape.GRID
})
const dot = wrapper.find('.slot-dot')
expect(dot.classes()).toContain('rounded-[1px]')
expect(dot.classes()).not.toContain('rounded-full')
expect(dot.element.tagName).toBe('DIV')
})
})

View File

@@ -3,6 +3,7 @@ import { computed, useTemplateRef } from 'vue'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { RenderShape } from '@/lib/litegraph/src/types/globalEnums'
import { cn } from '@/utils/tailwindUtil'
import type { ClassValue } from '@/utils/tailwindUtil'
@@ -41,9 +42,12 @@ defineExpose({
slotElRef
})
const isListShape = computed(() => props.slotData?.shape === RenderShape.GRID)
const slotClass = computed(() =>
cn(
'bg-slate-300 rounded-full slot-dot',
'bg-slate-300 slot-dot',
isListShape.value ? 'rounded-[1px]' : 'rounded-full',
'transition-all duration-150',
'border border-solid border-node-component-slot-dot-outline',
props.multi
@@ -63,7 +67,7 @@ const slotClass = computed(() =>
"
>
<div
v-if="types.length === 1 && slotData?.shape == undefined"
v-if="types.length === 1 && (slotData?.shape == undefined || isListShape)"
ref="slot-el"
:style="{ backgroundColor: types.length === 1 ? types[0] : undefined }"
:class="slotClass"