diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
index 08ff30d3b..f7f919259 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
@@ -2,137 +2,39 @@
{{ $t('Node Render Error') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
![preview]()
-
-
-
-
+ :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"
+ />
diff --git a/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue b/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue
index ce59aabf1..dd060de61 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNodePreview.vue
@@ -1,46 +1,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -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(() => {
}
})
-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
})
diff --git a/src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue b/src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue
new file mode 100644
index 000000000..e365d5850
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![preview]()
+
+
+
+
+
+
+
diff --git a/src/renderer/extensions/vueNodes/composables/useNodePresentation.ts b/src/renderer/extensions/vueNodes/composables/useNodePresentation.ts
new file mode 100644
index 000000000..89440ca8e
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/composables/useNodePresentation.ts
@@ -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
+ executing?: ComputedRef
+ progress?: ComputedRef
+ hasExecutionError?: ComputedRef
+ hasAnyError?: ComputedRef
+ bypassed?: ComputedRef
+ isDragging?: ComputedRef | Ref
+ shouldHandleNodePointerEvents?: ComputedRef
+}
+
+export interface NodePresentationState {
+ // Classes
+ containerBaseClasses: ComputedRef
+ separatorClasses: string
+ progressClasses: string
+ // Computed states
+ isCollapsed: ComputedRef
+ showProgress: ComputedRef
+ progressStyle: ComputedRef<{ width: string } | undefined>
+ progressBarStyle: ComputedRef<{ width: string } | undefined>
+ progressBarClasses: ComputedRef
+ borderClass: ComputedRef
+ outlineClass: ComputedRef
+}
+
+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
+ }
+}