[feat] Add tooltip support for Vue nodes (#5577)

## Summary

Added tooltip support for Vue node components using PrimeVue's v-tooltip
directive with proper data integration and container scoping.


https://github.com/user-attachments/assets/d1af31e6-ef6a-4df8-8de4-5098aa4490a1

## Changes

- **What**: Implemented tooltip functionality for Vue node headers,
input/output slots, and widgets using [PrimeVue
v-tooltip](https://primevue.org/tooltip/) directive
- **Dependencies**: Leverages existing PrimeVue tooltip system, no new
dependencies

## Review Focus

Container scoping implementation via provide/inject pattern for tooltip
positioning, proper TypeScript interfaces eliminating `as any` casts,
and integration with existing settings store for tooltip delays and
enable/disable functionality.

```mermaid
graph TD
    A[LGraphNode Container] --> B[provide tooltipContainer]
    B --> C[NodeHeader inject]
    B --> D[InputSlot inject]
    B --> E[OutputSlot inject]
    B --> F[NodeWidgets inject]

    G[useNodeTooltips composable] --> H[NodeDefStore lookup]
    G --> I[Settings integration]
    G --> J[i18n fallback]

    C --> G
    D --> G
    E --> G
    F --> G

    style A fill:#f9f9f9,stroke:#333,color:#000
    style G fill:#e8f4fd,stroke:#0066cc,color:#000
```

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2025-09-19 01:07:50 -07:00
committed by GitHub
parent a17c74fa0c
commit a975e50f1b
8 changed files with 380 additions and 54 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="renderError" class="node-error p-1 text-red-500 text-xs"></div>
<div v-else :class="slotWrapperClass">
<div v-else v-tooltip.left="tooltipConfig" :class="slotWrapperClass">
<!-- Connection Dot -->
<SlotConnectionDot
ref="connectionDotRef"
@@ -22,7 +22,9 @@
<script setup lang="ts">
import {
type ComponentPublicInstance,
type Ref,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -30,7 +32,8 @@ import {
import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { cn } from '@/utils/tailwindUtil'
@@ -38,7 +41,7 @@ import { cn } from '@/utils/tailwindUtil'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface InputSlotProps {
node?: LGraphNode
nodeType?: string
nodeId?: string
slotData: INodeSlot
index: number
@@ -54,6 +57,20 @@ const props = defineProps<InputSlotProps>()
const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getInputSlotTooltip, createTooltipConfig } = useNodeTooltips(
props.nodeType || '',
tooltipContainer
)
const tooltipConfig = computed(() => {
const slotName = props.slotData.localized_name || props.slotData.name || ''
const tooltipText = getInputSlotTooltip(slotName)
const fallbackText = tooltipText || `Input: ${slotName}`
return createTooltipConfig(fallbackText)
})
onErrorCaptured((error) => {
renderError.value = error.message
toastErrorHandler(error)

View File

@@ -4,6 +4,7 @@
</div>
<div
v-else
ref="nodeContainerRef"
:data-node-id="nodeData.id"
:class="
cn(
@@ -493,6 +494,10 @@ watch(
{ deep: true }
)
// Provide nodeImageUrls to child components
// Template ref for tooltip positioning
const nodeContainerRef = ref<HTMLElement>()
// Provide nodeImageUrls and tooltip container to child components
provide('nodeImageUrls', nodeImageUrls)
provide('tooltipContainer', nodeContainerRef)
</script>

View File

@@ -1,12 +1,16 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createPinia, setActivePinia } from 'pinia'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import enMessages from '@/locales/en/main.json'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { Settings } from '@/schemas/apiSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import NodeHeader from './NodeHeader.vue'
@@ -24,19 +28,94 @@ const makeNodeData = (overrides: Partial<VueNodeData> = {}): VueNodeData => ({
...overrides
})
const mountHeader = (
props?: Partial<InstanceType<typeof NodeHeader>['$props']>
) => {
const setupMockStores = () => {
const pinia = createPinia()
setActivePinia(pinia)
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
// Mock tooltip delay setting
vi.spyOn(settingStore, 'get').mockImplementation(
<K extends keyof Settings>(key: K): Settings[K] => {
switch (key) {
case 'Comfy.EnableTooltips':
return true as Settings[K]
case 'LiteGraph.Node.TooltipDelay':
return 500 as Settings[K]
default:
return undefined as Settings[K]
}
}
)
// Mock node definition store
const baseMockNodeDef: ComfyNodeDef = {
name: 'KSampler',
display_name: 'KSampler',
category: 'sampling',
python_module: 'test_module',
description: 'Advanced sampling node for diffusion models',
input: {
required: {
model: ['MODEL', {}],
positive: ['CONDITIONING', {}],
negative: ['CONDITIONING', {}]
},
optional: {},
hidden: {}
},
output: ['LATENT'],
output_is_list: [false],
output_name: ['samples'],
output_node: false,
deprecated: false,
experimental: false
}
const mockNodeDef = new ComfyNodeDefImpl(baseMockNodeDef)
vi.spyOn(nodeDefStore, 'nodeDefsByName', 'get').mockReturnValue({
KSampler: mockNodeDef
})
return { settingStore, nodeDefStore, pinia }
}
const createMountConfig = () => {
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: enMessages }
})
return mount(NodeHeader, {
const { pinia } = setupMockStores()
return {
global: {
plugins: [PrimeVue, i18n, createPinia()],
components: { InputText }
},
plugins: [PrimeVue, i18n, pinia],
components: { InputText },
directives: {
tooltip: {
mounted: vi.fn(),
updated: vi.fn(),
unmounted: vi.fn()
}
},
provide: {
tooltipContainer: { value: document.createElement('div') }
}
}
}
}
const mountHeader = (
props?: Partial<InstanceType<typeof NodeHeader>['$props']>
) => {
const config = createMountConfig()
return mount(NodeHeader, {
...config,
props: {
nodeData: makeNodeData(),
readonly: false,
@@ -126,4 +205,68 @@ describe('NodeHeader.vue', () => {
const collapsedIcon = wrapper.get('i')
expect(collapsedIcon.classes()).toContain('pi-chevron-right')
})
describe('Tooltips', () => {
it('applies tooltip directive to node title with correct configuration', () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
// Check that v-tooltip directive was applied
const directive = wrapper.vm.$el.querySelector(
'[data-testid="node-title"]'
)
expect(directive).toBeTruthy()
})
it('disables tooltip when in readonly mode', () => {
const wrapper = mountHeader({
readonly: true,
nodeData: makeNodeData({ type: 'KSampler' })
})
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
})
it('disables tooltip when editing is active', async () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
// Enter edit mode
await wrapper.get('[data-testid="node-header-1"]').trigger('dblclick')
// Tooltip should be disabled during editing
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
})
it('creates tooltip configuration when component mounts', () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
// Verify tooltip directive is applied to the title element
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
// The tooltip composable should be initialized
expect(wrapper.vm).toBeDefined()
})
it('uses tooltip container from provide/inject', () => {
const wrapper = mountHeader({
nodeData: makeNodeData({ type: 'KSampler' })
})
expect(wrapper.exists()).toBe(true)
// Container should be provided through inject
const titleElement = wrapper.find('[data-testid="node-title"]')
expect(titleElement.exists()).toBe(true)
})
})
})

View File

@@ -5,7 +5,7 @@
<div
v-else
class="lg-node-header flex items-center justify-between p-4 rounded-t-2xl cursor-move"
:data-testid="`node-header-${nodeInfo?.id || ''}`"
:data-testid="`node-header-${nodeData?.id || ''}`"
@dblclick="handleDoubleClick"
>
<!-- Collapse/Expand Button -->
@@ -23,7 +23,11 @@
</button>
<!-- Node Title -->
<div class="text-sm font-bold truncate flex-1" data-testid="node-title">
<div
v-tooltip.top="tooltipConfig"
class="text-sm font-bold truncate flex-1"
data-testid="node-title"
>
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
@@ -36,23 +40,22 @@
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref, watch } from 'vue'
import { type Ref, computed, inject, onErrorCaptured, ref, watch } from 'vue'
import EditableText from '@/components/common/EditableText.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
interface NodeHeaderProps {
node?: LGraphNode // For backwards compatibility
nodeData?: VueNodeData // New clean data structure
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
collapsed?: boolean
}
const props = defineProps<NodeHeaderProps>()
const { nodeData, readonly, collapsed } = defineProps<NodeHeaderProps>()
const emit = defineEmits<{
collapse: []
@@ -72,9 +75,22 @@ onErrorCaptured((error) => {
// Editing state
const isEditing = ref(false)
const nodeInfo = computed(() => props.nodeData || props.node)
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getNodeDescription, createTooltipConfig } = useNodeTooltips(
nodeData?.type || '',
tooltipContainer
)
const resolveTitle = (info: LGraphNode | VueNodeData | undefined) => {
const tooltipConfig = computed(() => {
if (readonly || isEditing.value) {
return { value: '', disabled: true }
}
const description = getNodeDescription.value
return createTooltipConfig(description)
})
const resolveTitle = (info: VueNodeData | undefined) => {
const title = (info?.title ?? '').trim()
if (title.length > 0) return title
const type = (info?.type ?? '').trim()
@@ -82,13 +98,13 @@ const resolveTitle = (info: LGraphNode | VueNodeData | undefined) => {
}
// Local state for title to provide immediate feedback
const displayTitle = ref(resolveTitle(nodeInfo.value))
const displayTitle = ref(resolveTitle(nodeData))
// Watch for external changes to the node title or type
watch(
() => [nodeInfo.value?.title, nodeInfo.value?.type] as const,
() => [nodeData?.title, nodeData?.type] as const,
() => {
const next = resolveTitle(nodeInfo.value)
const next = resolveTitle(nodeData)
if (next !== displayTitle.value) {
displayTitle.value = next
}
@@ -101,7 +117,7 @@ const handleCollapse = () => {
}
const handleDoubleClick = () => {
if (!props.readonly) {
if (!readonly) {
isEditing.value = true
}
}

View File

@@ -8,7 +8,8 @@
v-for="(input, index) in filteredInputs"
:key="`input-${index}`"
:slot-data="input"
:node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''"
:node-type="nodeData?.type || ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="getActualInputIndex(input, index)"
:readonly="readonly"
/>
@@ -19,7 +20,8 @@
v-for="(output, index) in filteredOutputs"
:key="`output-${index}`"
:slot-data="output"
:node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''"
:node-type="nodeData?.type || ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="index"
:readonly="readonly"
/>
@@ -32,7 +34,7 @@ import { computed, onErrorCaptured, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { isSlotObject } from '@/utils/typeGuardUtil'
@@ -40,21 +42,18 @@ import InputSlot from './InputSlot.vue'
import OutputSlot from './OutputSlot.vue'
interface NodeSlotsProps {
node?: LGraphNode // For backwards compatibility
nodeData?: VueNodeData // New clean data structure
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
}
const props = defineProps<NodeSlotsProps>()
const nodeInfo = computed(() => props.nodeData || props.node || null)
const { nodeData = null, readonly } = defineProps<NodeSlotsProps>()
// Filter out input slots that have corresponding widgets
const filteredInputs = computed(() => {
if (!nodeInfo.value?.inputs) return []
if (!nodeData?.inputs) return []
return nodeInfo.value.inputs
return nodeData.inputs
.filter((input) => {
// Check if this slot has a widget property (indicating it has a corresponding widget)
if (isSlotObject(input) && 'widget' in input && input.widget) {
@@ -76,7 +75,7 @@ const filteredInputs = computed(() => {
// Outputs don't have widgets, so we don't need to filter them
const filteredOutputs = computed(() => {
const outputs = nodeInfo.value?.outputs || []
const outputs = nodeData?.outputs || []
return outputs.map((output) =>
isSlotObject(output)
? output
@@ -94,10 +93,10 @@ const getActualInputIndex = (
input: INodeSlot,
filteredIndex: number
): number => {
if (!nodeInfo.value?.inputs) return filteredIndex
if (!nodeData?.inputs) return filteredIndex
// Find the actual index in the unfiltered inputs array
const actualIndex = nodeInfo.value.inputs.findIndex((i) => i === input)
const actualIndex = nodeData.inputs.findIndex((i) => i === input)
return actualIndex !== -1 ? actualIndex : filteredIndex
}

View File

@@ -31,7 +31,7 @@
type: widget.type,
boundingRect: [0, 0, 0, 0]
}"
:node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="getWidgetInputIndex(widget)"
:readonly="readonly"
:dot-only="true"
@@ -40,6 +40,7 @@
<!-- Widget Component -->
<component
:is="widget.vueComponent"
v-tooltip.left="widget.tooltipConfig"
:widget="widget.simplified"
:model-value="widget.value"
:readonly="readonly"
@@ -51,15 +52,15 @@
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref } from 'vue'
import { type Ref, computed, inject, onErrorCaptured, ref } from 'vue'
import type {
SafeWidgetData,
VueNodeData
} from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
// Import widget components directly
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
@@ -74,13 +75,12 @@ import { cn } from '@/utils/tailwindUtil'
import InputSlot from './InputSlot.vue'
interface NodeWidgetsProps {
node?: LGraphNode
nodeData?: VueNodeData
readonly?: boolean
lodLevel?: LODLevel
}
const props = defineProps<NodeWidgetsProps>()
const { nodeData, readonly, lodLevel } = defineProps<NodeWidgetsProps>()
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
useCanvasInteractions()
@@ -101,7 +101,13 @@ onErrorCaptured((error) => {
return false
})
const nodeInfo = computed(() => props.nodeData || props.node)
const nodeType = computed(() => nodeData?.type || '')
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
nodeType.value,
tooltipContainer
)
interface ProcessedWidget {
name: string
@@ -110,14 +116,13 @@ interface ProcessedWidget {
simplified: SimplifiedWidget
value: WidgetValue
updateHandler: (value: unknown) => void
tooltipConfig: any
}
const processedWidgets = computed((): ProcessedWidget[] => {
const info = nodeInfo.value
if (!info?.widgets) return []
if (!nodeData?.widgets) return []
const widgets = info.widgets as SafeWidgetData[]
const lodLevel = props.lodLevel
const widgets = nodeData.widgets as SafeWidgetData[]
const result: ProcessedWidget[] = []
if (lodLevel === LODLevel.MINIMAL) {
@@ -148,13 +153,17 @@ const processedWidgets = computed((): ProcessedWidget[] => {
}
}
const tooltipText = getWidgetTooltip(widget)
const tooltipConfig = createTooltipConfig(tooltipText)
result.push({
name: widget.name,
type: widget.type,
vueComponent,
simplified,
value: widget.value,
updateHandler
updateHandler,
tooltipConfig
})
}
@@ -165,7 +174,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
// or restructuring data model to unify widgets and inputs
// Map a widget to its corresponding input slot index
const getWidgetInputIndex = (widget: ProcessedWidget): number => {
const inputs = nodeInfo.value?.inputs
const inputs = nodeData?.inputs
if (!inputs) return 0
const idx = inputs.findIndex((input: any) => {

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="renderError" class="node-error p-1 text-red-500 text-xs"></div>
<div v-else :class="slotWrapperClass">
<div v-else v-tooltip.right="tooltipConfig" :class="slotWrapperClass">
<!-- Slot Name -->
<span
v-if="!dotOnly"
@@ -22,7 +22,9 @@
<script setup lang="ts">
import {
type ComponentPublicInstance,
type Ref,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -30,7 +32,8 @@ import {
import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { cn } from '@/utils/tailwindUtil'
@@ -38,7 +41,7 @@ import { cn } from '@/utils/tailwindUtil'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface OutputSlotProps {
node?: LGraphNode
nodeType?: string
nodeId?: string
slotData: INodeSlot
index: number
@@ -55,6 +58,20 @@ const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getOutputSlotTooltip, createTooltipConfig } = useNodeTooltips(
props.nodeType || '',
tooltipContainer
)
const tooltipConfig = computed(() => {
const slotName = props.slotData.name || ''
const tooltipText = getOutputSlotTooltip(props.index)
const fallbackText = tooltipText || `Output: ${slotName}`
return createTooltipConfig(fallbackText)
})
onErrorCaptured((error) => {
renderError.value = error.message
toastErrorHandler(error)

View File

@@ -0,0 +1,120 @@
import { type MaybeRef, type Ref, computed, unref } from 'vue'
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
import { st } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
/**
* Composable for managing Vue node tooltips
* Provides tooltip text for node headers, slots, and widgets
*/
export function useNodeTooltips(
nodeType: MaybeRef<string>,
containerRef?: Ref<HTMLElement | undefined>
) {
const nodeDefStore = useNodeDefStore()
const settingsStore = useSettingStore()
// Check if tooltips are globally enabled
const tooltipsEnabled = computed(() =>
settingsStore.get('Comfy.EnableTooltips')
)
// Get node definition for tooltip data
const nodeDef = computed(() => nodeDefStore.nodeDefsByName[unref(nodeType)])
/**
* Get tooltip text for node description (header hover)
*/
const getNodeDescription = computed(() => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.description`
return st(key, nodeDef.value.description || '')
})
/**
* Get tooltip text for input slots
*/
const getInputSlotTooltip = (slotName: string) => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(slotName)}.tooltip`
const inputTooltip = nodeDef.value.inputs?.[slotName]?.tooltip ?? ''
return st(key, inputTooltip)
}
/**
* Get tooltip text for output slots
*/
const getOutputSlotTooltip = (slotIndex: number) => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.outputs.${slotIndex}.tooltip`
const outputTooltip = nodeDef.value.outputs?.[slotIndex]?.tooltip ?? ''
return st(key, outputTooltip)
}
/**
* Get tooltip text for widgets
*/
const getWidgetTooltip = (widget: SafeWidgetData) => {
if (!tooltipsEnabled.value || !nodeDef.value) return ''
// First try widget-specific tooltip
const widgetTooltip = (widget as { tooltip?: string }).tooltip
if (widgetTooltip) return widgetTooltip
// Then try input-based tooltip lookup
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(widget.name)}.tooltip`
const inputTooltip = nodeDef.value.inputs?.[widget.name]?.tooltip ?? ''
return st(key, inputTooltip)
}
/**
* Create tooltip configuration object for v-tooltip directive
*/
const createTooltipConfig = (text: string) => {
const tooltipDelay = settingsStore.get('LiteGraph.Node.TooltipDelay')
const tooltipText = text || ''
const config: {
value: string
showDelay: number
disabled: boolean
appendTo?: HTMLElement
pt?: any
} = {
value: tooltipText,
showDelay: tooltipDelay as number,
disabled: !tooltipsEnabled.value || !tooltipText,
pt: {
text: {
class:
'bg-charcoal-100 border border-slate-300 rounded-md px-4 py-2 text-white text-sm font-normal leading-tight max-w-75 shadow-none'
},
arrow: {
class: 'before:border-charcoal-100'
}
}
}
// If we have a container reference, append tooltips to it
if (containerRef?.value) {
config.appendTo = containerRef.value
}
return config
}
return {
tooltipsEnabled,
getNodeDescription,
getInputSlotTooltip,
getOutputSlotTooltip,
getWidgetTooltip,
createTooltipConfig
}
}