Compare commits

...

16 Commits

Author SHA1 Message Date
Austin Mroz
4c79c2684a Remove remaining draggableList uses 2026-02-17 19:47:57 -08:00
Austin Mroz
21d47a5fb5 Ensure reactivity triggers 2026-02-17 19:45:41 -08:00
Austin Mroz
3ebda6cb65 Update displayed label on rename 2026-02-17 18:42:38 -08:00
Austin Mroz
149b01db03 Partial renaming support 2026-02-17 17:41:19 -08:00
Austin Mroz
fbf7a4c7dc AccordionItems for inputs/outputs 2026-02-17 16:29:40 -08:00
Austin Mroz
c8a9ac63cd litegraph overlay support 2026-02-17 09:53:32 -08:00
Austin Mroz
42dd2c29fb Add empty box for unchecked outptus 2026-02-16 22:24:58 -08:00
Austin Mroz
70318e1613 Highlight selectable outputs 2026-02-16 21:52:03 -08:00
Austin Mroz
53a3e23938 Minor styling tweaks 2026-02-16 20:23:02 -08:00
Austin Mroz
5dd1613c2c Move overlay to transform pane
This permanently solves the earlier sizing concerns, but introduces a
little bit of ick with a humble exclamation mark
2026-02-16 19:50:03 -08:00
Austin Mroz
72ccec85ae Use design colors 2026-02-16 18:06:10 -08:00
Austin Mroz
ac82555d31 Forwarding cleanup: move select to click 2026-02-16 17:29:03 -08:00
Austin Mroz
1cd6c8b739 Remove event capturing, use forward event 2026-02-16 17:17:18 -08:00
Austin Mroz
b2bc20a665 Use DraggableList for subgraphs 2026-02-16 16:36:58 -08:00
Austin Mroz
e0ebc5168b new DraggableList component, use for app creation 2026-02-16 15:58:06 -08:00
Austin Mroz
d6c6ad6da2 Early app setup handling 2026-02-14 20:56:35 -08:00
10 changed files with 521 additions and 265 deletions

View File

@@ -0,0 +1,59 @@
<script setup lang="ts" generic="T">
import { onBeforeUnmount, ref, useTemplateRef, watchEffect } from 'vue'
import { DraggableList } from '@/scripts/ui/draggableList'
const modelValue = defineModel<T[]>({ required: true })
const draggableList = ref<DraggableList>()
const draggableItems = useTemplateRef('draggableItems')
watchEffect(() => {
void modelValue.value.length
draggableList.value?.dispose()
if (!draggableItems.value?.children?.length) return
draggableList.value = new DraggableList(
draggableItems.value,
'.draggable-item'
)
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems = []
let oldPosition = -1
this.getAllItems().forEach((item, index) => {
if (item === this.draggableItem) {
oldPosition = index
return
}
if (!this.isItemToggled(item)) {
reorderedItems[index] = item
return
}
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
reorderedItems[newIndex] = item
})
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
reorderedItems[index] = this.draggableItem
}
}
const newPosition = reorderedItems.indexOf(this.draggableItem)
const itemList = modelValue.value
const [item] = itemList.splice(oldPosition, 1)
itemList.splice(newPosition, 0, item)
modelValue.value = [...itemList]
}
})
onBeforeUnmount(() => {
draggableList.value?.dispose()
})
</script>
<template>
<div ref="draggableItems" class="pb-2 px-2 space-y-0.5 mt-0.5">
<slot
drag-class="draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing"
/>
</div>
</template>

View File

@@ -20,6 +20,7 @@ import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
import { cn } from '@/utils/tailwindUtil'
import TabApp from './TabApp.vue'
import TabError from './TabError.vue'
import TabInfo from './info/TabInfo.vue'
import TabGlobalParameters from './parameters/TabGlobalParameters.vue'
@@ -109,6 +110,10 @@ const tabs = computed<RightSidePanelTabList>(() => {
icon: 'icon-[lucide--octagon-alert] bg-node-stroke-error ml-1'
})
}
list.push({
label: () => 'app',
value: 'app'
})
list.push({
label: () =>
@@ -298,6 +303,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
<!-- Panel Content -->
<div class="scrollbar-thin flex-1 overflow-y-auto">
<template v-if="!hasSelection">
<TabApp v-if="activeTab === 'app'" />
<TabGlobalParameters v-if="activeTab === 'parameters'" />
<TabNodes v-else-if="activeTab === 'nodes'" />
<TabGlobalSettings v-else-if="activeTab === 'settings'" />

View File

@@ -0,0 +1,311 @@
<script setup lang="ts">
import { remove } from 'es-toolkit'
import { useElementBounding } from '@vueuse/core'
import { computed, reactive, toValue } from 'vue'
import type { MaybeRef } from 'vue'
import { useI18n } from 'vue-i18n'
import DraggableList from '@/components/common/DraggableList.vue'
import IoItem from '@/components/rightSidePanel/app/IoItem.vue'
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
import Button from '@/components/ui/button/Button.vue'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import { useDialogService } from '@/services/dialogService'
import { useHoveredStore } from '@/stores/hoveredStore'
import { cn } from '@/utils/tailwindUtil'
type BoundStyle = { top: string; left: string; width: string; height: string }
const canvasInteractions = useCanvasInteractions()
const canvasStore = useCanvasStore()
const hoveredStore = useHoveredStore()
const settingStore = useSettingStore()
const { t } = useI18n()
const canvas: LGraphCanvas = canvasStore.getCanvas()
const selectedInputs = reactive<
[string, string, string, (() => void) | undefined, MaybeRef<BoundStyle>][]
>([])
const selectedOutputs = reactive<[NodeId, string][]>([])
async function renameWidget(
widget: IBaseWidget,
input: INodeInputSlot,
key: string
) {
const newLabel = await useDialogService().prompt({
title: t('g.rename'),
message: t('g.enterNewNamePrompt'),
defaultValue: widget.label,
placeholder: widget.name
})
if (newLabel === null) return
widget.label = newLabel || undefined
input.label = newLabel || undefined
widget.callback?.(widget.value)
useCanvasStore().canvas?.setDirty(true)
const inputTuple = selectedInputs.find(([k]) => k === key)
if (!inputTuple) return
inputTuple[1] = newLabel
}
function getHovered(
e?: MouseEvent
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
const { graph } = canvas
if (!canvas || !graph) return
if (settingStore.get('Comfy.VueNodes.Enabled')) {
const node = graph.getNodeById(hoveredStore.hoveredNodeId)
if (!node) return
const widget = node.widgets?.find(
(w) => w.name === hoveredStore.hoveredWidgetName
)
return [node, widget]
}
if (!e) return
canvas.adjustMouseEvent(e)
const node = graph.getNodeOnPos(e.canvasX, e.canvasY)
if (!node) return
return [node, node.getWidgetOnPos(e.canvasX, e.canvasY, false)]
}
function elementPosition(e: HTMLElement) {
const bounding = useElementBounding(e)
return computed(() => ({
width: `${bounding.width.value / canvas.ds.scale}px`,
height: `${bounding.height.value / canvas.ds.scale}px`,
left: `${bounding.left.value / canvas.ds.scale - canvas.ds.offset[0]}px`,
top: `${bounding.top.value / canvas.ds.scale - canvas.ds.offset[1]}px`
}))
}
function getBounding(nodeId: NodeId, widgetName?: string) {
if (settingStore.get('Comfy.VueNodes.Enabled')) {
const element = document.querySelector(
widgetName
? `[data-node-id="${nodeId}"] [data-widget-name="${widgetName}"`
: `[data-node-id="${nodeId}"]`
)
return element instanceof HTMLElement ? elementPosition(element) : undefined
}
const node = canvas.graph?.getNodeById(nodeId)
if (!node) return
if (!widgetName)
return {
width: `${node.size[0]}px`,
height: `${node.size[1] + 30}px`,
left: `${node.pos[0]}px`,
top: `${node.pos[1] - 30}px`
}
const widget = node.widgets?.find((w) => w.name === widgetName)
if (!widget) return
return {
width: `${node.size[0] - 30}px`,
height: `${(widget.computedHeight ?? 24) - 4}px`,
left: `${node.pos[0] + 15}px`,
top: `${node.pos[1] + widget.y}px`
}
}
function handleDown(e: MouseEvent) {
const [node] = getHovered(e) ?? []
if (!node || e.button > 0) canvasInteractions.forwardEventToCanvas(e)
}
function handleClick(e: MouseEvent) {
const [node, widget] = getHovered(e) ?? []
if (!node) return canvasInteractions.forwardEventToCanvas(e)
if (!widget) {
if (!node.constructor.nodeData?.output_node)
return canvasInteractions.forwardEventToCanvas(e)
const index = selectedOutputs.findIndex(([id]) => id === node.id)
if (index === -1) selectedOutputs.push([node.id, node.title])
else selectedOutputs.splice(index, 1)
return
}
const key = `${node.id}: ${widget.name}`
const bounding = getBounding(node.id, widget.name)
const keyIndex = selectedInputs.findIndex(([k]) => k === key)
const input = node.inputs.find((i) => i.widget?.name === widget.name)
const rename = input && (() => renameWidget(widget, input, key))
if (keyIndex === -1 && bounding)
selectedInputs.push([
key,
widget.label ?? widget.name,
node.title,
rename,
bounding
])
else selectedInputs.splice(keyIndex, 1)
}
function nodeToDisplayTuple(
n: LGraphNode
): [NodeId, MaybeRef<BoundStyle> | undefined, boolean] {
return [n.id, getBounding(n.id), selectedOutputs.some(([id]) => n.id === id)]
}
const outputNodes = computed(() =>
canvas
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
.map(nodeToDisplayTuple)
)
</script>
<template>
<div class="flex font-bold p-2 border-border-subtle border-b">
{{ t('[ph]App builder mode') }}
<Button class="ml-auto">
{{ t('[ph]Exit builder') }}
</Button>
</div>
<PropertiesAccordionItem
:label="t('nodeHelpPage.inputs')"
enable-empty-state
:disabled="!selectedInputs.length"
class="border-border-subtle border-b"
>
<template #empty>
<div class="w-full p-4 text-muted-foreground gap-2 flex flex-col">
<div
v-text="t('[ph]Click on node parameters to add them here as inputs')"
/>
<div
class="text-base-foreground"
v-text="t('[ph]No inputs added yet')"
/>
<div
v-text="
t(
'[ph]Users will interact and adjust these to generate their outputs.'
)
"
/>
<div v-text="t('[ph]Examples: “Load image”, “Text prompt”, “Steps”')" />
</div>
</template>
<div
class="w-full p-4 pt-2 text-muted-foreground"
v-text="t('[ph]Click on node parameters to add them here as inputs')"
/>
<DraggableList v-slot="{ dragClass }" v-model="selectedInputs">
<IoItem
v-for="[key, title, subTitle, rename] in selectedInputs"
:key
:class="cn(dragClass, 'bg-primary-background/30 p-2 my-2 rounded-lg')"
:title
:sub-title
:rename
:remove="() => remove(selectedInputs, ([k]) => k === key)"
/>
</DraggableList>
</PropertiesAccordionItem>
<PropertiesAccordionItem
:label="t('nodeHelpPage.outputs')"
enable-empty-state
:disabled="!selectedOutputs.length"
>
<template #empty>
<div class="w-full p-4 text-muted-foreground gap-2 flex flex-col">
<div
v-text="
t(
'[ph]Click on output nodes to add them here. These will be the generated results.'
)
"
/>
<div
class="text-base-foreground"
v-text="t('[ph]No outputs nodes added yet')"
/>
<div
v-text="
t(
'[ph]Connect at least one output node so users can see results after running.'
)
"
/>
<div v-text="t('[ph]Examples: “Save Image” or “Save Video”')" />
</div>
</template>
<div
class="w-full p-4 pt-2 text-muted-foreground"
v-text="t('[ph]Click on node parameters to add them here as inputs')"
/>
<DraggableList v-slot="{ dragClass }" v-model="selectedOutputs">
<IoItem
v-for="([key, title], index) in selectedOutputs"
:key
:class="
cn(
dragClass,
'bg-warning-background/40 p-2 my-2 rounded-lg',
index === 0 && 'ring-warning-background ring-2'
)
"
:title
:sub-title="String(key)"
:remove="() => remove(selectedOutputs, ([k]) => k === key)"
/>
</DraggableList>
</PropertiesAccordionItem>
<Teleport to="body">
<TransformPane
:class="
cn(
'absolute w-full h-full pointer-events-auto!',
getHovered() ? 'cursor-pointer' : 'cursor-grab'
)
"
:canvas="canvasStore.getCanvas()"
@pointerdown="handleDown"
@click="handleClick"
@wheel="canvasInteractions.forwardEventToCanvas"
>
<div
v-for="[key, , , , style] in selectedInputs"
:key
:style="toValue(style)"
class="fixed bg-primary-background/30 rounded-lg"
/>
<div
v-for="[key, style, isSelected] in outputNodes"
:key
:style="toValue(style)"
:class="
cn(
'fixed ring-warning-background ring-5 rounded-2xl',
!isSelected && 'ring-warning-background/50'
)
"
>
<div class="absolute top-0 right-0 size-8">
<div
v-if="isSelected"
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg"
>
<i class="icon-[lucide--check] bg-text-foreground size-full" />
</div>
<div
v-else
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg"
/>
</div>
</div>
</TransformPane>
</Teleport>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
const { t } = useI18n()
const { rename, remove } = defineProps<{
title: string
subTitle?: string
rename?: () => void
remove?: () => void
}>()
const entries = computed(() => {
const items = []
if (rename)
items.push({
label: t('g.rename'),
action: rename,
icon: 'icon-[lucide--pencil]'
})
if (remove)
items.push({
label: t('g.delete'),
action: remove,
icon: 'icon-[lucide--trash-2]'
})
return [items]
})
</script>
<template>
<div class="p-2 my-2 rounded-lg flex items-center-safe">
<span class="mr-auto" v-text="title" />
<span class="text-muted-foreground mr-2" v-text="subTitle" />
<Popover :entries>
<template #button>
<Button variant="muted-textonly">
<i class="icon-[lucide--ellipsis]" />
</Button>
</template>
</Popover>
</div>
</template>

View File

@@ -1,18 +1,10 @@
<script setup lang="ts">
import { useMounted, watchDebounced } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
shallowRef
} from 'vue'
import { computed, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import DraggableList from '@/components/common/DraggableList.vue'
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
import { DraggableList } from '@/scripts/ui/draggableList'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import type { ValidFavoritedWidget } from '@/stores/workspace/favoritedWidgetsStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
@@ -25,8 +17,6 @@ const rightSidePanelStore = useRightSidePanelStore()
const { searchQuery } = storeToRefs(rightSidePanelStore)
const { t } = useI18n()
const draggableList = ref<DraggableList | undefined>(undefined)
const sectionWidgetsRef = ref<{ widgetsContainer: HTMLElement }>()
const isSearching = ref(false)
const favoritedWidgets = computed(
@@ -47,67 +37,6 @@ async function searcher(query: string) {
isSearching.value = query.trim().length > 0
searchedFavoritedWidgets.value = searchWidgets(favoritedWidgets.value, query)
}
const isMounted = useMounted()
function setDraggableState() {
if (!isMounted.value) return
draggableList.value?.dispose()
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems: HTMLElement[] = []
let oldPosition = -1
this.getAllItems().forEach((item, index) => {
if (item === this.draggableItem) {
oldPosition = index
return
}
if (!this.isItemToggled(item)) {
reorderedItems[index] = item
return
}
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
reorderedItems[newIndex] = item
})
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
reorderedItems[index] = this.draggableItem as HTMLElement
}
}
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
}
}
watchDebounced(
searchedFavoritedWidgets,
() => {
setDraggableState()
},
{ debounce: 100 }
)
onMounted(() => {
setDraggableState()
})
onBeforeUnmount(() => {
draggableList.value?.dispose()
})
</script>
<template>
@@ -118,42 +47,45 @@ onBeforeUnmount(() => {
:update-key="favoritedWidgets"
/>
</div>
<SectionWidgets
ref="sectionWidgetsRef"
:label
:widgets="searchedFavoritedWidgets"
:is-draggable="!isSearching"
hidden-favorite-indicator
show-node-name
enable-empty-state
class="border-b border-interface-stroke"
@update:collapse="nextTick(setDraggableState)"
<DraggableList
v-model="searchedFavoritedWidgets"
@update:model-value="favoritedWidgetsStore.reorderFavorites"
>
<template #empty>
<div class="text-sm text-muted-foreground px-4 text-center py-10">
<p>
{{
isSearching
? t('rightSidePanel.noneSearchDesc')
: t('rightSidePanel.favoritesNoneDesc')
}}
</p>
<i18n-t
v-if="!isSearching"
keypath="rightSidePanel.favoritesNoneHint"
tag="p"
class="mt-2 text-xs"
>
<template #moreIcon>
<span
aria-hidden="true"
class="inline-flex size-5 items-center justify-center rounded-md bg-secondary-background-hover text-secondary-foreground align-middle"
>
<i class="icon-[lucide--more-vertical] text-sm" />
</span>
</template>
</i18n-t>
</div>
</template>
</SectionWidgets>
<SectionWidgets
:label
:widgets="searchedFavoritedWidgets"
:is-draggable="!isSearching"
hidden-favorite-indicator
show-node-name
enable-empty-state
class="border-b border-interface-stroke"
>
<template #empty>
<div class="text-sm text-muted-foreground px-4 text-center py-10">
<p>
{{
isSearching
? t('rightSidePanel.noneSearchDesc')
: t('rightSidePanel.favoritesNoneDesc')
}}
</p>
<i18n-t
v-if="!isSearching"
keypath="rightSidePanel.favoritesNoneHint"
tag="p"
class="mt-2 text-xs"
>
<template #moreIcon>
<span
aria-hidden="true"
class="inline-flex size-5 items-center justify-center rounded-md bg-secondary-background-hover text-secondary-foreground align-middle"
>
<i class="icon-[lucide--more-vertical] text-sm" />
</span>
</template>
</i18n-t>
</div>
</template>
</SectionWidgets>
</DraggableList>
</template>

View File

@@ -1,27 +1,22 @@
<script setup lang="ts">
import { useMounted, watchDebounced } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import {
computed,
customRef,
nextTick,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
triggerRef,
useTemplateRef,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import DraggableList from '@/components/common/DraggableList.vue'
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
import type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
import { DraggableList } from '@/scripts/ui/draggableList'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { searchWidgets } from '../shared'
@@ -37,13 +32,10 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const canvasStore = useCanvasStore()
const rightSidePanelStore = useRightSidePanelStore()
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
const advancedInputsCollapsed = ref(true)
const draggableList = ref<DraggableList | undefined>(undefined)
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
// Use customRef to track proxyWidgets changes
@@ -137,66 +129,6 @@ async function searcher(query: string) {
searchedWidgetsList.value = searchWidgets(widgetsList.value, query)
}
const isMounted = useMounted()
function setDraggableState() {
if (!isMounted.value) return
draggableList.value?.dispose()
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems: HTMLElement[] = []
let oldPosition = -1
this.getAllItems().forEach((item, index) => {
if (item === this.draggableItem) {
oldPosition = index
return
}
if (!this.isItemToggled(item)) {
reorderedItems[index] = item
return
}
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
reorderedItems[newIndex] = item
})
if (oldPosition === -1) {
console.error('[TabSubgraphInputs] draggableItem not found in items')
return
}
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
reorderedItems[index] = this.draggableItem as HTMLElement
}
}
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
// Update proxyWidgets order
const pw = proxyWidgets.value
const [w] = pw.splice(oldPosition, 1)
pw.splice(newPosition, 0, w)
proxyWidgets.value = pw
canvasStore.canvas?.setDirty(true, true)
triggerRef(proxyWidgets)
}
}
watchDebounced(searchedWidgetsList, () => setDraggableState(), {
debounce: 100
})
onMounted(() => setDraggableState())
onBeforeUnmount(() => draggableList.value?.dispose())
const label = computed(() => {
return searchedWidgetsList.value.length !== 0
? t('rightSidePanel.inputs')
@@ -212,28 +144,28 @@ const label = computed(() => {
:update-key="widgetsList"
/>
</div>
<SectionWidgets
ref="sectionWidgetsRef"
:node
:label
:parents
:widgets="searchedWidgetsList"
:is-draggable="!isSearching"
:enable-empty-state="isSearching"
:tooltip="
isSearching || searchedWidgetsList.length
? ''
: t('rightSidePanel.inputsNoneTooltip')
"
class="border-b border-interface-stroke"
@update:collapse="nextTick(setDraggableState)"
>
<template #empty>
<div class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15">
{{ t('rightSidePanel.noneSearchDesc') }}
</div>
</template>
</SectionWidgets>
<DraggableList v-model="proxyWidgets">
<SectionWidgets
:node
:label
:parents
:widgets="searchedWidgetsList"
:is-draggable="!isSearching"
:enable-empty-state="isSearching"
:tooltip="
isSearching || searchedWidgetsList.length
? ''
: t('rightSidePanel.inputsNoneTooltip')
"
class="border-b border-interface-stroke"
>
<template #empty>
<div class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15">
{{ t('rightSidePanel.noneSearchDesc') }}
</div>
</template>
</SectionWidgets>
</DraggableList>
<SectionWidgets
v-if="advancedInputsWidgets.length > 0"
ref="advancedInputsSectionRef"

View File

@@ -1,15 +1,8 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import {
computed,
customRef,
onBeforeUnmount,
onMounted,
ref,
triggerRef,
watch
} from 'vue'
import { computed, customRef, onMounted, triggerRef } from 'vue'
import DraggableList from '@/components/common/DraggableList.vue'
import Button from '@/components/ui/button/Button.vue'
import {
demoteWidget,
@@ -28,9 +21,9 @@ import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
import { DraggableList } from '@/scripts/ui/draggableList'
import { useLitegraphService } from '@/services/litegraphService'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { cn } from '@/utils/tailwindUtil'
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
@@ -38,8 +31,6 @@ const canvasStore = useCanvasStore()
const rightSidePanelStore = useRightSidePanelStore()
const { searchQuery } = storeToRefs(rightSidePanelStore)
const draggableList = ref<DraggableList | undefined>(undefined)
const draggableItems = ref()
const proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({
get() {
track()
@@ -188,54 +179,9 @@ function showRecommended() {
proxyWidgets.value = widgets
}
function setDraggableState() {
draggableList.value?.dispose()
if (searchQuery.value || !draggableItems.value?.children?.length) return
draggableList.value = new DraggableList(
draggableItems.value,
'.draggable-item'
)
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems = []
let oldPosition = -1
this.getAllItems().forEach((item, index) => {
if (item === this.draggableItem) {
oldPosition = index
return
}
if (!this.isItemToggled(item)) {
reorderedItems[index] = item
return
}
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
reorderedItems[newIndex] = item
})
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
reorderedItems[index] = this.draggableItem
}
}
const newPosition = reorderedItems.indexOf(this.draggableItem)
const aw = activeWidgets.value
const [w] = aw.splice(oldPosition, 1)
aw.splice(newPosition, 0, w)
activeWidgets.value = aw
}
}
watch(filteredActive, () => {
setDraggableState()
})
onMounted(() => {
setDraggableState()
if (activeNode.value) pruneDisconnected(activeNode.value)
})
onBeforeUnmount(() => {
draggableList.value?.dispose()
})
</script>
<template>
@@ -273,19 +219,18 @@ onBeforeUnmount(() => {
{{ $t('subgraphStore.hideAll') }}</a
>
</div>
<div ref="draggableItems" class="pb-2 px-2 space-y-0.5 mt-0.5">
<DraggableList v-slot="{ dragClass }" v-model="activeWidgets">
<SubgraphNodeWidget
v-for="[node, widget] in filteredActive"
:key="toKey([node, widget])"
class="bg-comfy-menu-bg"
:class="cn(!searchQuery && dragClass, 'bg-comfy-menu-bg')"
:node-title="node.title"
:widget-name="widget.name"
:is-shown="true"
:is-draggable="!searchQuery"
:is-physical="node.id === -1"
:is-draggable="!searchQuery"
@toggle-visibility="demote([node, widget])"
/>
</div>
</DraggableList>
</div>
<div

View File

@@ -29,8 +29,7 @@ function getIcon() {
cn(
'flex py-1 px-2 break-all rounded items-center gap-1',
'bg-node-component-surface',
props.isDraggable &&
'draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing hover:ring-1 ring-accent-background',
props.isDraggable && 'hover:ring-1 ring-accent-background',
props.class
)
"

View File

@@ -0,0 +1,25 @@
import { useElementByPoint, useMouse } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const useHoveredStore = defineStore('hovered', () => {
const { x, y } = useMouse({ type: 'client' })
const { element } = useElementByPoint({ x, y, multiple: true })
const hoveredWidgetName = computed(() => {
const widgetEl = element.value?.find((e) => e.matches('.lg-node-widget'))
if (!(widgetEl instanceof HTMLElement)) return
return widgetEl.dataset.widgetName
})
const hoveredNodeId = computed(() => {
const nodeEl = element.value?.find((e) => e.matches('.lg-node'))
if (!(nodeEl instanceof HTMLElement)) return
return nodeEl.dataset.nodeId
})
return {
hoveredNodeId,
hoveredWidgetName
}
})

View File

@@ -10,6 +10,7 @@ export type RightSidePanelTab =
| 'settings'
| 'info'
| 'subgraph'
| 'app'
type RightSidePanelSection = 'advanced-inputs' | string