mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
feat: vue node previews with refactored lgnode.vue
This commit is contained in:
@@ -2,137 +2,39 @@
|
||||
<div v-if="renderError" class="node-error p-2 text-red-500 text-sm">
|
||||
{{ $t('Node Render Error') }}
|
||||
</div>
|
||||
<div
|
||||
<NodeBaseTemplate
|
||||
v-else
|
||||
ref="nodeContainerRef"
|
||||
:data-node-id="nodeData.id"
|
||||
:class="
|
||||
cn(
|
||||
'bg-white dark-theme:bg-charcoal-800',
|
||||
'lg-node absolute rounded-2xl',
|
||||
'border border-solid border-sand-100 dark-theme:border-charcoal-600',
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents &&
|
||||
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
||||
'outline-transparent -outline-offset-2 outline-2',
|
||||
borderClass,
|
||||
outlineClass,
|
||||
{
|
||||
'animate-pulse': executing,
|
||||
'opacity-50 before:rounded-2xl before:pointer-events-none before:absolute before:bg-bypass/60 before:inset-0':
|
||||
bypassed,
|
||||
'will-change-transform': isDragging
|
||||
},
|
||||
|
||||
shouldHandleNodePointerEvents
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none'
|
||||
)
|
||||
"
|
||||
:style="[
|
||||
{
|
||||
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex
|
||||
},
|
||||
dragStyle
|
||||
]"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<template v-if="isCollapsed">
|
||||
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData.title, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleHeaderTitleUpdate"
|
||||
@enter-subgraph="handleEnterSubgraph"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isCollapsed && executing && progress !== undefined"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',
|
||||
progressClasses
|
||||
)
|
||||
"
|
||||
:style="{ width: `${Math.min(progress * 100, 100)}%` }"
|
||||
/>
|
||||
|
||||
<template v-if="!isCollapsed">
|
||||
<div class="mb-4 relative">
|
||||
<div :class="separatorClasses" />
|
||||
<!-- Progress bar for executing state -->
|
||||
<div
|
||||
v-if="executing && progress !== undefined"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-0 top-1/2 -translate-y-1/2',
|
||||
!!(progress < 1) && 'rounded-r-full',
|
||||
progressClasses
|
||||
)
|
||||
"
|
||||
:style="{ width: `${Math.min(progress * 100, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Node Body - rendered based on LOD level and collapsed state -->
|
||||
<div
|
||||
class="flex flex-col gap-4 pb-4"
|
||||
:data-testid="`node-body-${nodeData.id}`"
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="nodeData.widgets?.length"
|
||||
v-memo="[nodeData.widgets?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="hasCustomContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:image-urls="nodeImageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
<div
|
||||
v-if="shouldShowPreviewImg"
|
||||
v-memo="[latestPreviewUrl]"
|
||||
class="px-4"
|
||||
>
|
||||
<img
|
||||
:src="latestPreviewUrl"
|
||||
alt="preview"
|
||||
class="w-full max-h-64 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:container-classes="presentation.containerBaseClasses.value"
|
||||
:container-style="containerStyle"
|
||||
:is-collapsed="presentation.isCollapsed.value"
|
||||
:separator-classes="presentation.separatorClasses"
|
||||
:progress-classes="presentation.progressClasses"
|
||||
:progress-bar-classes="presentation.progressBarClasses.value"
|
||||
:show-progress="presentation.showProgress.value"
|
||||
:progress-value="progress"
|
||||
:progress-style="presentation.progressStyle.value"
|
||||
:progress-bar-style="presentation.progressBarStyle.value"
|
||||
:has-custom-content="hasCustomContent"
|
||||
:image-urls="nodeImageUrls"
|
||||
:show-preview-image="shouldShowPreviewImg"
|
||||
:preview-image-url="latestPreviewUrl"
|
||||
:event-handlers="{
|
||||
onPointerdown: handlePointerDown,
|
||||
onPointermove: handlePointerMove,
|
||||
onPointerup: handlePointerUp,
|
||||
onWheel: handleWheel
|
||||
}"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleHeaderTitleUpdate"
|
||||
@enter-subgraph="handleEnterSubgraph"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, inject, onErrorCaptured, onMounted, provide, ref } from 'vue'
|
||||
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
@@ -142,6 +44,7 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||
import { useNodePresentation } from '@/renderer/extensions/vueNodes/composables/useNodePresentation'
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
@@ -153,13 +56,8 @@ import {
|
||||
getLocatorIdFromNodeData,
|
||||
getNodeByLocatorId
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
import NodeSlots from './NodeSlots.vue'
|
||||
import NodeWidgets from './NodeWidgets.vue'
|
||||
import SlotConnectionDot from './SlotConnectionDot.vue'
|
||||
import NodeBaseTemplate from './NodeBaseTemplate.vue'
|
||||
|
||||
// Extended props for main node component
|
||||
interface LGraphNodeProps {
|
||||
@@ -240,8 +138,28 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Track collapsed state
|
||||
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
|
||||
// Use the new presentation composable
|
||||
const presentation = useNodePresentation(() => nodeData, {
|
||||
readonly,
|
||||
isPreview: false,
|
||||
isSelected,
|
||||
executing,
|
||||
progress,
|
||||
hasExecutionError,
|
||||
hasAnyError,
|
||||
bypassed,
|
||||
isDragging,
|
||||
shouldHandleNodePointerEvents
|
||||
})
|
||||
|
||||
// Container style combining position and drag styles
|
||||
const containerStyle = computed(() => [
|
||||
{
|
||||
transform: `translate(${position.value.x ?? 0}px, ${(position.value.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex.value
|
||||
},
|
||||
dragStyle.value
|
||||
])
|
||||
|
||||
// Check if node has custom content (like image outputs)
|
||||
const hasCustomContent = computed(() => {
|
||||
@@ -249,44 +167,16 @@ const hasCustomContent = computed(() => {
|
||||
return nodeImageUrls.value.length > 0
|
||||
})
|
||||
|
||||
// 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 progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
() => nodeData.id,
|
||||
{
|
||||
isCollapsed
|
||||
isCollapsed: presentation.isCollapsed
|
||||
}
|
||||
)
|
||||
|
||||
const borderClass = computed(() => {
|
||||
if (hasAnyError.value) {
|
||||
return 'border-error'
|
||||
}
|
||||
if (executing.value) {
|
||||
return 'border-blue-500'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const outlineClass = computed(() => {
|
||||
if (!isSelected.value) {
|
||||
return undefined
|
||||
}
|
||||
if (hasAnyError.value) {
|
||||
return 'outline-error'
|
||||
}
|
||||
if (executing.value) {
|
||||
return 'outline-blue-500'
|
||||
}
|
||||
return 'outline-black dark-theme:outline-white'
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
const handleCollapse = () => {
|
||||
handleNodeCollapse(nodeData.id, !isCollapsed.value)
|
||||
handleNodeCollapse(nodeData.id, !presentation.isCollapsed.value)
|
||||
}
|
||||
|
||||
const handleHeaderTitleUpdate = (newTitle: string) => {
|
||||
@@ -344,7 +234,4 @@ const nodeImageUrls = computed(() => {
|
||||
// Clear URLs if no outputs or no images
|
||||
return []
|
||||
})
|
||||
|
||||
const nodeContainerRef = ref()
|
||||
provide('tooltipContainer', nodeContainerRef)
|
||||
</script>
|
||||
|
||||
@@ -1,46 +1,14 @@
|
||||
<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"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<NodeHeader :node-data="nodeData" :readonly="readonly" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4 relative">
|
||||
<div class="bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 pb-4">
|
||||
<NodeSlots
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<NodeWidgets
|
||||
v-if="nodeData.widgets?.length"
|
||||
v-memo="[nodeData.widgets?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<NodeContent
|
||||
v-if="hasCustomContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:image-urls="nodeImageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
<!-- <div v-if="shouldShowPreviewImg" class="px-4">
|
||||
<img
|
||||
:src="latestPreviewUrl"
|
||||
alt="preview"
|
||||
class="w-full max-h-64 object-contain"
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<NodeBaseTemplate
|
||||
:node-data="nodeData"
|
||||
:readonly="true"
|
||||
:container-classes="presentation.containerBaseClasses.value"
|
||||
:is-collapsed="false"
|
||||
:separator-classes="presentation.separatorClasses"
|
||||
:has-custom-content="false"
|
||||
:image-urls="[]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,13 +16,12 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import NodeContent from '@/renderer/extensions/vueNodes/components/NodeContent.vue'
|
||||
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
|
||||
import NodeSlots from '@/renderer/extensions/vueNodes/components/NodeSlots.vue'
|
||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||
import { useNodePresentation } from '@/renderer/extensions/vueNodes/composables/useNodePresentation'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
|
||||
import NodeBaseTemplate from './NodeBaseTemplate.vue'
|
||||
|
||||
const { nodeDef } = defineProps<{
|
||||
nodeDef: ComfyNodeDefV2
|
||||
}>()
|
||||
@@ -110,13 +77,9 @@ const nodeData = computed<VueNodeData>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const readonly = true
|
||||
|
||||
const hasCustomContent = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
const nodeImageUrls = computed(() => {
|
||||
return []
|
||||
// Use the presentation composable with preview mode
|
||||
const presentation = useNodePresentation(() => nodeData.value, {
|
||||
readonly: true,
|
||||
isPreview: true
|
||||
})
|
||||
</script>
|
||||
|
||||
158
src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue
Normal file
158
src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div
|
||||
ref="nodeContainerRef"
|
||||
:data-node-id="nodeData?.id"
|
||||
:class="containerClasses"
|
||||
:style="containerStyle"
|
||||
v-bind="eventHandlers"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<template v-if="isCollapsed">
|
||||
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData?.title, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="emit('collapse')"
|
||||
@update:title="emit('update:title', $event)"
|
||||
@enter-subgraph="emit('enter-subgraph')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isCollapsed && showProgress"
|
||||
:class="progressBarClasses"
|
||||
:style="progressBarStyle"
|
||||
/>
|
||||
|
||||
<template v-if="!isCollapsed">
|
||||
<div class="mb-4 relative">
|
||||
<div :class="separatorClasses" />
|
||||
<!-- Progress bar for executing state -->
|
||||
<div
|
||||
v-if="showProgress"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-0 top-1/2 -translate-y-1/2',
|
||||
!!(progressValue && progressValue < 1) && 'rounded-r-full',
|
||||
progressClasses
|
||||
)
|
||||
"
|
||||
:style="progressStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Node Body - rendered based on LOD level and collapsed state -->
|
||||
<div
|
||||
class="flex flex-col gap-4 pb-4"
|
||||
:data-testid="`node-body-${nodeData?.id}`"
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-memo="[nodeData?.inputs?.length, nodeData?.outputs?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="nodeData?.widgets?.length"
|
||||
v-memo="[nodeData?.widgets?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="hasCustomContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:image-urls="imageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
<div
|
||||
v-if="showPreviewImage && previewImageUrl"
|
||||
v-memo="[previewImageUrl]"
|
||||
class="px-4"
|
||||
>
|
||||
<img
|
||||
:src="previewImageUrl"
|
||||
alt="preview"
|
||||
class="w-full max-h-64 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
import NodeSlots from './NodeSlots.vue'
|
||||
import NodeWidgets from './NodeWidgets.vue'
|
||||
import SlotConnectionDot from './SlotConnectionDot.vue'
|
||||
|
||||
interface NodeBaseTemplateProps {
|
||||
nodeData?: VueNodeData
|
||||
readonly?: boolean
|
||||
// Presentation state
|
||||
containerClasses?: string
|
||||
containerStyle?: any
|
||||
isCollapsed?: boolean
|
||||
separatorClasses?: string
|
||||
progressClasses?: string
|
||||
progressBarClasses?: string
|
||||
// Progress state
|
||||
showProgress?: boolean
|
||||
progressValue?: number
|
||||
progressStyle?: any
|
||||
progressBarStyle?: any
|
||||
// Content state
|
||||
hasCustomContent?: boolean
|
||||
imageUrls?: string[]
|
||||
showPreviewImage?: boolean
|
||||
previewImageUrl?: string
|
||||
// Event handlers object for v-bind
|
||||
eventHandlers?: Record<string, any>
|
||||
}
|
||||
|
||||
const {
|
||||
nodeData,
|
||||
readonly = false,
|
||||
containerClasses = '',
|
||||
containerStyle = {},
|
||||
isCollapsed = false,
|
||||
separatorClasses = '',
|
||||
progressClasses = '',
|
||||
progressBarClasses = '',
|
||||
showProgress = false,
|
||||
progressValue,
|
||||
progressStyle,
|
||||
progressBarStyle,
|
||||
hasCustomContent = false,
|
||||
imageUrls = [],
|
||||
showPreviewImage = false,
|
||||
previewImageUrl,
|
||||
eventHandlers = {}
|
||||
} = defineProps<NodeBaseTemplateProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
collapse: []
|
||||
'update:title': [newTitle: string]
|
||||
'enter-subgraph': []
|
||||
}>()
|
||||
|
||||
// Provide tooltip container for child components
|
||||
const nodeContainerRef = ref<HTMLElement>()
|
||||
provide('tooltipContainer', nodeContainerRef)
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
import { type ComputedRef, type Ref, computed, unref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
export interface NodePresentationOptions {
|
||||
readonly?: boolean
|
||||
isPreview?: boolean
|
||||
scale?: number
|
||||
// Interactive node state
|
||||
isSelected?: ComputedRef<boolean>
|
||||
executing?: ComputedRef<boolean>
|
||||
progress?: ComputedRef<number | undefined>
|
||||
hasExecutionError?: ComputedRef<boolean>
|
||||
hasAnyError?: ComputedRef<boolean>
|
||||
bypassed?: ComputedRef<boolean>
|
||||
isDragging?: ComputedRef<boolean> | Ref<boolean>
|
||||
shouldHandleNodePointerEvents?: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export interface NodePresentationState {
|
||||
// Classes
|
||||
containerBaseClasses: ComputedRef<string>
|
||||
separatorClasses: string
|
||||
progressClasses: string
|
||||
// Computed states
|
||||
isCollapsed: ComputedRef<boolean>
|
||||
showProgress: ComputedRef<boolean>
|
||||
progressStyle: ComputedRef<{ width: string } | undefined>
|
||||
progressBarStyle: ComputedRef<{ width: string } | undefined>
|
||||
progressBarClasses: ComputedRef<string>
|
||||
borderClass: ComputedRef<string | undefined>
|
||||
outlineClass: ComputedRef<string | undefined>
|
||||
}
|
||||
|
||||
export function useNodePresentation(
|
||||
nodeData: () => VueNodeData | undefined,
|
||||
options: NodePresentationOptions = {}
|
||||
): NodePresentationState {
|
||||
const {
|
||||
isPreview = false,
|
||||
isSelected,
|
||||
executing,
|
||||
progress,
|
||||
hasAnyError,
|
||||
bypassed,
|
||||
isDragging,
|
||||
shouldHandleNodePointerEvents
|
||||
} = options
|
||||
|
||||
// Collapsed state
|
||||
const isCollapsed = computed(() => nodeData()?.flags?.collapsed ?? false)
|
||||
|
||||
// Show progress when executing with defined progress
|
||||
const showProgress = computed(() => {
|
||||
if (isPreview) return false
|
||||
return !!(executing?.value && progress?.value !== undefined)
|
||||
})
|
||||
|
||||
// Progress styles
|
||||
const progressStyle = computed(() => {
|
||||
if (!showProgress.value || !progress?.value) return undefined
|
||||
return { width: `${Math.min(progress.value * 100, 100)}%` }
|
||||
})
|
||||
|
||||
const progressBarStyle = progressStyle
|
||||
|
||||
// Border class based on state
|
||||
const borderClass = computed(() => {
|
||||
if (isPreview) return undefined
|
||||
if (hasAnyError?.value) {
|
||||
return 'border-error'
|
||||
}
|
||||
if (executing?.value) {
|
||||
return 'border-blue-500'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Outline class based on selection and state
|
||||
const outlineClass = computed(() => {
|
||||
if (isPreview) return undefined
|
||||
if (!isSelected?.value) {
|
||||
return undefined
|
||||
}
|
||||
if (hasAnyError?.value) {
|
||||
return 'outline-error'
|
||||
}
|
||||
if (executing?.value) {
|
||||
return 'outline-blue-500'
|
||||
}
|
||||
return 'outline-black dark-theme:outline-white'
|
||||
})
|
||||
|
||||
// Container base classes (without dynamic state classes)
|
||||
const containerBaseClasses = computed(() => {
|
||||
if (isPreview) {
|
||||
return cn(
|
||||
'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'
|
||||
)
|
||||
}
|
||||
|
||||
return cn(
|
||||
'bg-white dark-theme:bg-charcoal-800',
|
||||
'lg-node absolute rounded-2xl',
|
||||
'border border-solid border-sand-100 dark-theme:border-charcoal-600',
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents?.value &&
|
||||
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
||||
'outline-transparent -outline-offset-2 outline-2',
|
||||
borderClass.value,
|
||||
outlineClass.value,
|
||||
{
|
||||
'animate-pulse': executing?.value,
|
||||
'opacity-50 before:rounded-2xl before:pointer-events-none before:absolute before:bg-bypass/60 before:inset-0':
|
||||
bypassed?.value,
|
||||
'will-change-transform': unref(isDragging)
|
||||
},
|
||||
shouldHandleNodePointerEvents?.value
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none'
|
||||
)
|
||||
})
|
||||
|
||||
const progressBarClasses = computed(() => {
|
||||
return cn(
|
||||
'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',
|
||||
progressClasses
|
||||
)
|
||||
})
|
||||
|
||||
// Static classes
|
||||
const separatorClasses =
|
||||
'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'
|
||||
|
||||
return {
|
||||
containerBaseClasses,
|
||||
separatorClasses,
|
||||
progressClasses,
|
||||
isCollapsed,
|
||||
showProgress,
|
||||
progressStyle,
|
||||
progressBarStyle,
|
||||
progressBarClasses,
|
||||
borderClass,
|
||||
outlineClass
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user