mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 07:00:23 +00:00
feat: v3 style of node body (#5169)
* feat: v3 style of node body * Update src/renderer/extensions/vueNodes/components/LGraphNode.vue * fix: review's issues * fix: review's issue
This commit is contained in:
committed by
Benjamin Lu
parent
1447b15f4c
commit
57db10f408
@@ -89,3 +89,6 @@ When referencing Comfy-Org repos:
|
||||
- NEVER use `--no-verify` flag when committing
|
||||
- NEVER delete or disable tests to make them pass
|
||||
- NEVER circumvent quality checks
|
||||
- NEVER use `dark:` prefix - always use `dark-theme:` for dark mode styles, for example: `dark-theme:text-white dark-theme:bg-black`
|
||||
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('bg-red-500', { 'bg-blue-500': condition })" />`
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
"algoliasearch": "^5.21.0",
|
||||
"axios": "^1.8.2",
|
||||
"chart.js": "^4.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
@@ -146,6 +147,7 @@
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.5",
|
||||
"semver": "^7.7.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"vue": "^3.5.13",
|
||||
|
||||
@@ -1,6 +1,61 @@
|
||||
@layer primevue, tailwind-utilities;
|
||||
|
||||
@layer tailwind-utilities {
|
||||
/* Set default values to prevent some styles from not working properly. */
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(66 153 225 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
}
|
||||
@@ -28,7 +83,7 @@
|
||||
--content-fg: #000;
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgba(0, 122, 255, 1);
|
||||
--code-bg-color: rgba(96, 165, 250, 0.2);
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<InputText
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
size="small"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -19,6 +16,7 @@ import InputText from 'primevue/inputtext'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
INPUT_EXCLUDED_PROPS,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<MultiSelect
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
size="small"
|
||||
:pt="{
|
||||
option: 'text-xs'
|
||||
}"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -22,6 +19,7 @@ import MultiSelect from 'primevue/multiselect'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-4" :style="{ height: widgetHeight + 'px' }">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<div class="flex items-center gap-2 flex-1 justify-end">
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<Slider
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="flex-grow text-xs"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
<InputText
|
||||
@@ -24,7 +21,7 @@
|
||||
@keydown="handleInputKeydown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -33,14 +30,13 @@ import Slider from 'primevue/slider'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import { COMFY_VUE_NODE_DIMENSIONS } from '../../../lib/litegraph/src/litegraph'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
modelValue: number
|
||||
@@ -58,9 +54,6 @@ const { localValue, onChange } = useNumberWidgetValue(
|
||||
emit
|
||||
)
|
||||
|
||||
// Get widget height from litegraph constants
|
||||
const widgetHeight = COMFY_VUE_NODE_DIMENSIONS.components.STANDARD_WIDGET_HEIGHT
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<ToggleSwitch
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -17,6 +14,7 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useBooleanWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<TreeSelect
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
size="small"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -19,6 +16,7 @@ import TreeSelect from 'primevue/treeselect'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<!-- Connection Dot -->
|
||||
<div class="w-5 h-5 flex items-center justify-center group/slot">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-white transition-all duration-150 group-hover/slot:w-2.5 group-hover/slot:h-2.5 group-hover/slot:border-2 group-hover/slot:border-white"
|
||||
class="w-2.5 h-2.5 rounded-full bg-white transition-all duration-150 group-hover/slot:w-3 group-hover/slot:h-3 group-hover/slot:border-2 group-hover/slot:border-white"
|
||||
:style="{
|
||||
backgroundColor: slotColor
|
||||
}"
|
||||
@@ -26,7 +26,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Slot Name -->
|
||||
<span v-if="!dotOnly" class="text-xs text-surface-700 whitespace-nowrap">
|
||||
<span
|
||||
v-if="!dotOnly"
|
||||
class="whitespace-nowrap text-sm font-normal dark-theme:text-[#9FA2BD] text-[#888682]"
|
||||
>
|
||||
{{ slotData.name || `Input ${index}` }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -6,23 +6,26 @@
|
||||
<div
|
||||
v-else
|
||||
:data-node-id="nodeData.id"
|
||||
:class="[
|
||||
'lg-node absolute border-2 rounded-lg',
|
||||
'contain-layout contain-style contain-paint',
|
||||
selected ? 'border-blue-500 ring-2 ring-blue-300' : 'border-gray-600',
|
||||
executing ? 'animate-pulse' : '',
|
||||
nodeData.mode === 4 ? 'opacity-50' : '', // bypassed
|
||||
error ? 'border-red-500 bg-red-50' : '',
|
||||
isDragging ? 'will-change-transform' : '',
|
||||
lodCssClass,
|
||||
'hover:border-green-500' // Debug: visual feedback on hover
|
||||
]"
|
||||
:class="
|
||||
cn(
|
||||
'bg-white dark-theme:bg-[#15161A]',
|
||||
'min-w-[445px]',
|
||||
'lg-node absolute border-2 border-solid rounded-2xl',
|
||||
{
|
||||
'border-blue-500 ring-2 ring-blue-300': selected,
|
||||
'border-[#e1ded5] dark-theme:border-[#292A30]': !selected,
|
||||
'animate-pulse': executing,
|
||||
'opacity-50': nodeData.mode === 4,
|
||||
'border-red-500 bg-red-50': error,
|
||||
'will-change-transform': isDragging
|
||||
},
|
||||
lodCssClass,
|
||||
'hover:border-green-500'
|
||||
)
|
||||
"
|
||||
:style="[
|
||||
{
|
||||
transform: `translate(${layoutPosition.x ?? position?.x ?? 0}px, ${(layoutPosition.y ?? position?.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
width: size ? `${size.width}px` : '200px',
|
||||
height: size ? `${size.height}px` : 'auto',
|
||||
backgroundColor: '#353535',
|
||||
pointerEvents: 'auto'
|
||||
},
|
||||
dragStyle
|
||||
@@ -31,51 +34,70 @@
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData.title, lodLevel, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleTitleUpdate"
|
||||
/>
|
||||
|
||||
<!-- Node Body - rendered based on LOD level and collapsed state -->
|
||||
<div
|
||||
v-if="!isMinimalLOD && !isCollapsed"
|
||||
class="flex flex-col gap-2"
|
||||
:data-testid="`node-body-${nodeData.id}`"
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-if="shouldRenderSlots"
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length, lodLevel]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
@slot-click="handleSlotClick"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="shouldRenderWidgets && nodeData.widgets?.length"
|
||||
v-memo="[nodeData.widgets?.length, lodLevel]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="shouldRenderContent && hasCustomContent"
|
||||
<div class="flex items-center">
|
||||
<template v-if="isCollapsed">
|
||||
<MultiSlotPoint class="absolute left-0 -translate-x-1/2" />
|
||||
<MultiSlotPoint class="absolute right-0 translate-x-1/2" />
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData.title, lodLevel, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleTitleUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="!isMinimalLOD && !isCollapsed">
|
||||
<div :class="cn(separatorClasses, 'mb-4')" />
|
||||
|
||||
<!-- 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-if="shouldRenderSlots"
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length, lodLevel]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
@slot-click="handleSlotClick"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="shouldRenderSlots && shouldShowWidgets"
|
||||
:class="separatorClasses"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="shouldShowWidgets"
|
||||
v-memo="[nodeData.widgets?.length, lodLevel]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="(shouldRenderSlots || shouldShowWidgets) && shouldShowContent"
|
||||
:class="separatorClasses"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="shouldShowContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Progress bar for executing state -->
|
||||
<div
|
||||
v-if="executing && progress !== undefined"
|
||||
@@ -94,7 +116,9 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import MultiSlotPoint from './MultiSlotPoint.vue'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
import NodeSlots from './NodeSlots.vue'
|
||||
@@ -184,6 +208,18 @@ const hasCustomContent = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
// Computed classes and conditions for better reusability
|
||||
const separatorClasses = 'bg-[#e1ded5] dark-theme:bg-[#292A30] h-[1px] mx-4'
|
||||
|
||||
// Common condition computations to avoid repetition
|
||||
const shouldShowWidgets = computed(
|
||||
() => shouldRenderWidgets.value && props.nodeData.widgets?.length
|
||||
)
|
||||
|
||||
const shouldShowContent = computed(
|
||||
() => shouldRenderContent.value && hasCustomContent.value
|
||||
)
|
||||
|
||||
// Event handlers
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!props.nodeData) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
color?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="{ backgroundColor: color }"
|
||||
class="bg-[#5B5E7D] w-[13.3px] h-[29.3px] rounded-full"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,16 +1,12 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div v-if="renderError" class="node-error p-2 text-red-500 text-sm">
|
||||
<div v-if="renderError" class="node-error p-4 text-red-500 text-sm">
|
||||
⚠️ Node Header Error
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="lg-node-header flex items-center justify-between p-2 rounded-t-lg cursor-move"
|
||||
class="lg-node-header flex items-center justify-between p-4 rounded-t-2xl cursor-move"
|
||||
:data-testid="`node-header-${nodeInfo?.id || ''}`"
|
||||
:style="{
|
||||
backgroundColor: headerColor,
|
||||
color: textColor
|
||||
}"
|
||||
@dblclick="handleDoubleClick"
|
||||
>
|
||||
<!-- Collapse/Expand Button -->
|
||||
@@ -23,12 +19,12 @@
|
||||
>
|
||||
<i
|
||||
:class="collapsed ? 'pi pi-chevron-right' : 'pi pi-chevron-down'"
|
||||
class="text-xs leading-none relative top-[1px]"
|
||||
class="text-xs leading-none relative top-[1px] text-[#888682] dark-theme:text-[#5B5E7D]"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<!-- Node Title -->
|
||||
<div class="text-sm font-medium truncate flex-1" data-testid="node-title">
|
||||
<div class="text-sm font-bold truncate flex-1" data-testid="node-title">
|
||||
<EditableText
|
||||
:model-value="displayTitle"
|
||||
:is-editing="isEditing"
|
||||
@@ -92,34 +88,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Compute header color based on node color property or type
|
||||
const headerColor = computed(() => {
|
||||
const info = nodeInfo.value
|
||||
if (!info) return '#353535'
|
||||
|
||||
if (info.mode === 4) return '#666' // Bypassed
|
||||
if (info.mode === 2) return '#444' // Muted
|
||||
return '#353535' // Default
|
||||
})
|
||||
|
||||
// Compute text color for contrast
|
||||
const textColor = computed(() => {
|
||||
const color = headerColor.value
|
||||
if (!color || color === '#353535' || color === '#444' || color === '#666') {
|
||||
return '#fff'
|
||||
}
|
||||
const colorStr = String(color)
|
||||
const rgb = parseInt(
|
||||
colorStr.startsWith('#') ? colorStr.slice(1) : colorStr,
|
||||
16
|
||||
)
|
||||
const r = (rgb >> 16) & 255
|
||||
const g = (rgb >> 8) & 255
|
||||
const b = rgb & 255
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000
|
||||
return brightness > 128 ? '#000' : '#fff'
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
const handleCollapse = () => {
|
||||
emit('collapse')
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
@pointerdown="handleClick"
|
||||
>
|
||||
<!-- Slot Name -->
|
||||
<span v-if="!dotOnly" class="text-xs text-surface-700 whitespace-nowrap">
|
||||
<span
|
||||
v-if="!dotOnly"
|
||||
class="whitespace-nowrap text-sm font-normal dark-theme:text-[#9FA2BD] text-[#888682]"
|
||||
>
|
||||
{{ slotData.name || `Output ${index}` }}
|
||||
</span>
|
||||
|
||||
<!-- Connection Dot -->
|
||||
<div class="w-5 h-5 flex items-center justify-center group/slot">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-white transition-all duration-150 group-hover/slot:w-2.5 group-hover/slot:h-2.5 group-hover/slot:border-2 group-hover/slot:border-white"
|
||||
class="w-2.5 h-2.5 rounded-full bg-white transition-all duration-150 group-hover/slot:w-3 group-hover/slot:h-3 group-hover/slot:border-2 group-hover/slot:border-white"
|
||||
:style="{
|
||||
backgroundColor: slotColor
|
||||
}"
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<InputText
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
size="small"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -25,6 +22,8 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
modelValue: string
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<MultiSelect
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
size="small"
|
||||
:pt="{
|
||||
option: 'text-xs'
|
||||
}"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -28,6 +25,8 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any[]>
|
||||
modelValue: any[]
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-4"
|
||||
:style="{ height: widgetHeight + 'px' }"
|
||||
>
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget>
|
||||
<Select
|
||||
v-model="localValue"
|
||||
:options="selectOptions"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs bg-[#F9F8F4] dark-theme:bg-[#0E0E12] border-[#E1DED5] dark-theme:border-[#15161C] !rounded-lg"
|
||||
size="small"
|
||||
:pt="{
|
||||
option: 'text-xs'
|
||||
}"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -26,13 +20,14 @@ import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { COMFY_VUE_NODE_DIMENSIONS } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string | number | undefined>
|
||||
modelValue: string | number | undefined
|
||||
@@ -51,9 +46,6 @@ const { localValue, onChange } = useWidgetValue({
|
||||
emit
|
||||
})
|
||||
|
||||
// Get widget height from litegraph constants
|
||||
const widgetHeight = COMFY_VUE_NODE_DIMENSIONS.components.STANDARD_WIDGET_HEIGHT
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, PANEL_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<SelectButton
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
:pt="{
|
||||
pcToggleButton: {
|
||||
label: 'text-xs'
|
||||
@@ -15,7 +12,7 @@
|
||||
}"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -29,6 +26,8 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any>
|
||||
modelValue: any
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-4" :style="{ height: widgetHeight + 'px' }">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<div class="flex items-center gap-2 flex-1 justify-end">
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<div
|
||||
class="flex items-center gap-2 w-full rounded-lg pl-4 pr-2 bg-[#F9F8F4] dark-theme:bg-[#0E0E12] border-[#E1DED5] dark-theme:border-[#15161C] border-solid border"
|
||||
>
|
||||
<Slider
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="flex-grow text-xs"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
<InputText
|
||||
@@ -18,13 +17,13 @@
|
||||
:min="widget.options?.min"
|
||||
:max="widget.options?.max"
|
||||
:step="stepValue"
|
||||
class="w-[4em] text-center text-xs px-0"
|
||||
class="w-[4em] text-center text-xs px-0 !border-none !shadow-none !bg-transparent"
|
||||
size="small"
|
||||
@blur="handleInputBlur"
|
||||
@keydown="handleInputKeydown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -33,13 +32,14 @@ import Slider from 'primevue/slider'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { COMFY_VUE_NODE_DIMENSIONS } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
modelValue: number
|
||||
@@ -57,9 +57,6 @@ const { localValue, onChange } = useNumberWidgetValue(
|
||||
emit
|
||||
)
|
||||
|
||||
// Get widget height from litegraph constants
|
||||
const widgetHeight = COMFY_VUE_NODE_DIMENSIONS.components.STANDARD_WIDGET_HEIGHT
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<ToggleSwitch
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -23,6 +20,8 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<boolean>
|
||||
modelValue: boolean
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label v-if="widget.name" class="text-xs opacity-80 min-w-[4em] truncate">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<TreeSelect
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
class="flex-grow min-w-[8em] max-w-[20em] text-xs"
|
||||
class="w-full text-xs"
|
||||
size="small"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -25,6 +22,8 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any>
|
||||
modelValue: any
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { COMFY_VUE_NODE_DIMENSIONS } from '@/lib/litegraph/src/litegraph'
|
||||
import { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
defineProps<{
|
||||
widget: Pick<SimplifiedWidget<string | number | undefined>, 'name'>
|
||||
}>()
|
||||
|
||||
// Get widget height from litegraph constants
|
||||
const widgetHeight = COMFY_VUE_NODE_DIMENSIONS.components.STANDARD_WIDGET_HEIGHT
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2"
|
||||
:style="{ height: widgetHeight + 'px' }"
|
||||
>
|
||||
<p
|
||||
v-if="widget.name"
|
||||
class="text-sm text-[#888682] dark-theme:text-[#9FA2BD] font-normal flex-1 truncate w-20"
|
||||
>
|
||||
{{ widget.name }}
|
||||
</p>
|
||||
<div class="w-75">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
6
src/utils/tailwindUtil.ts
Normal file
6
src/utils/tailwindUtil.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import clsx, { type ClassArray } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassArray) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export default {
|
||||
60: '15rem',
|
||||
64: '16rem',
|
||||
72: '18rem',
|
||||
75: '18.75rem',
|
||||
80: '20rem',
|
||||
84: '22rem',
|
||||
90: '24rem',
|
||||
|
||||
Reference in New Issue
Block a user