Files
ComfyUI_frontend/ComfyUI_vibe/src/components/v2/nodes/NodeHeader.vue
orkhanart 67af617868 feat(ui): Implement experimental UI layer with versioned architecture
- Reorganize components into v1/v2 versioned structure
- Add common components for shared UI elements
- Introduce composables for reusable logic
- Restructure views into v1/v2 directories
- Remove old component structure in favor of versioned approach
- Update router and UI store for new architecture
2025-11-28 18:23:26 -08:00

156 lines
4.1 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
import type { NodeState, NodeBadge } from '@/types/node'
interface Props {
title: string
collapsed?: boolean
pinned?: boolean
badges?: NodeBadge[]
state?: NodeState
}
const props = withDefaults(defineProps<Props>(), {
collapsed: false,
pinned: false,
badges: () => [],
state: 'idle',
})
const emit = defineEmits<{
collapse: []
'update:title': [title: string]
}>()
const isEditing = ref(false)
const editValue = ref('')
const statusBadge = computed((): NodeBadge | null => {
switch (props.state) {
case 'muted':
return { text: 'Muted', icon: 'pi-ban', variant: 'default' }
case 'bypassed':
return { text: 'Bypassed', icon: 'pi-redo', variant: 'warning' }
case 'error':
return { text: 'Error', icon: 'pi-exclamation-triangle', variant: 'error' }
case 'executing':
return { text: 'Running', icon: 'pi-spin pi-spinner', variant: 'default' }
default:
return null
}
})
function handleCollapseClick(event: MouseEvent): void {
event.stopPropagation()
emit('collapse')
}
function handleDoubleClick(): void {
if (!isEditing.value) {
isEditing.value = true
editValue.value = props.title
}
}
function handleTitleBlur(): void {
if (isEditing.value) {
const trimmed = editValue.value.trim()
if (trimmed && trimmed !== props.title) {
emit('update:title', trimmed)
}
isEditing.value = false
}
}
function handleTitleKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter') {
handleTitleBlur()
} else if (event.key === 'Escape') {
isEditing.value = false
}
}
function getBadgeClasses(variant?: string): string {
switch (variant) {
case 'success':
return 'bg-green-500/20 text-green-400'
case 'warning':
return 'bg-amber-500/20 text-amber-400'
case 'error':
return 'bg-red-500/20 text-red-400'
default:
return 'bg-zinc-700 text-zinc-300'
}
}
</script>
<template>
<div
:class="[
'node-header py-2 pl-2 pr-3 text-sm',
'bg-zinc-800 text-zinc-100',
collapsed ? 'rounded-2xl' : 'rounded-t-2xl',
]"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between gap-2 min-w-0">
<div class="flex items-center gap-2 min-w-0 flex-1">
<button
class="flex h-5 w-5 shrink-0 items-center justify-center rounded text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
@click="handleCollapseClick"
@dblclick.stop
>
<i
:class="[
'pi pi-chevron-down text-xs transition-transform duration-200',
collapsed && '-rotate-90',
]"
/>
</button>
<div class="flex min-w-0 flex-1 items-center">
<input
v-if="isEditing"
v-model="editValue"
type="text"
class="w-full min-w-0 truncate bg-transparent text-sm font-semibold text-zinc-100 outline-none ring-1 ring-blue-500 rounded px-1"
autofocus
@blur="handleTitleBlur"
@keydown="handleTitleKeydown"
/>
<span v-else class="truncate text-sm font-semibold">
{{ title }}
</span>
</div>
</div>
<div class="flex shrink-0 items-center gap-1.5">
<span
v-for="badge in badges"
:key="badge.text"
:class="[
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium',
getBadgeClasses(badge.variant),
]"
>
<i v-if="badge.icon" :class="['pi', badge.icon, 'text-[9px]']" />
{{ badge.text }}
</span>
<span
v-if="statusBadge"
:class="[
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium',
getBadgeClasses(statusBadge.variant),
]"
>
<i v-if="statusBadge.icon" :class="['pi', statusBadge.icon, 'text-[9px]']" />
{{ statusBadge.text }}
</span>
<i v-if="pinned" class="pi pi-thumbtack text-xs text-zinc-500" />
</div>
</div>
</div>
</template>