mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 07:00:23 +00:00
## Summary Unify the current sidebar tabs, structurally and aesthetically. ## Changes - Removes the Assets only Layout - Standardizes the title styling and spacing across the tabs. ## Review Focus <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7215-WIP-Sidebar-Tabs-component-and-style-alignment-2c26d73d3650817193bfd752e0d0bbde) by [Unito](https://www.unito.io)
220 lines
6.3 KiB
Vue
220 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
import { storeToRefs } from 'pinia'
|
|
import { computed, ref, toValue, watchEffect } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import IconButton from '@/components/button/IconButton.vue'
|
|
import EditableText from '@/components/common/EditableText.vue'
|
|
import Tab from '@/components/tab/Tab.vue'
|
|
import TabList from '@/components/tab/TabList.vue'
|
|
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
|
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
|
|
import { isLGraphNode } from '@/utils/litegraphUtil'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
import TabInfo from './info/TabInfo.vue'
|
|
import TabParameters from './parameters/TabParameters.vue'
|
|
import TabSettings from './settings/TabSettings.vue'
|
|
import SubgraphEditor from './subgraph/SubgraphEditor.vue'
|
|
|
|
const canvasStore = useCanvasStore()
|
|
const rightSidePanelStore = useRightSidePanelStore()
|
|
const { t } = useI18n()
|
|
|
|
const { selectedItems } = storeToRefs(canvasStore)
|
|
const { activeTab, isEditingSubgraph } = storeToRefs(rightSidePanelStore)
|
|
|
|
const hasSelection = computed(() => selectedItems.value.length > 0)
|
|
|
|
const selectedNodes = computed((): LGraphNode[] => {
|
|
return selectedItems.value.filter(isLGraphNode)
|
|
})
|
|
|
|
const isSubgraphNode = computed(() => {
|
|
return selectedNode.value instanceof SubgraphNode
|
|
})
|
|
|
|
const isSingleNodeSelected = computed(() => selectedNodes.value.length === 1)
|
|
|
|
const selectedNode = computed(() => {
|
|
return isSingleNodeSelected.value ? selectedNodes.value[0] : null
|
|
})
|
|
|
|
const selectionCount = computed(() => selectedItems.value.length)
|
|
|
|
const panelTitle = computed(() => {
|
|
if (isSingleNodeSelected.value && selectedNode.value) {
|
|
return selectedNode.value.title || selectedNode.value.type || 'Node'
|
|
}
|
|
return t('rightSidePanel.title', { count: selectionCount.value })
|
|
})
|
|
|
|
function closePanel() {
|
|
rightSidePanelStore.closePanel()
|
|
}
|
|
|
|
type RightSidePanelTabList = Array<{
|
|
label: () => string
|
|
value: RightSidePanelTab
|
|
}>
|
|
|
|
const tabs = computed<RightSidePanelTabList>(() => {
|
|
const list: RightSidePanelTabList = [
|
|
{
|
|
label: () => t('rightSidePanel.parameters'),
|
|
value: 'parameters'
|
|
},
|
|
{
|
|
label: () => t('g.settings'),
|
|
value: 'settings'
|
|
}
|
|
]
|
|
if (
|
|
!hasSelection.value ||
|
|
(isSingleNodeSelected.value && !isSubgraphNode.value)
|
|
) {
|
|
list.push({
|
|
label: () => t('rightSidePanel.info'),
|
|
value: 'info'
|
|
})
|
|
}
|
|
return list
|
|
})
|
|
|
|
// Use global state for activeTab and ensure it's valid
|
|
watchEffect(() => {
|
|
if (
|
|
!tabs.value.some((tab) => tab.value === activeTab.value) &&
|
|
!(activeTab.value === 'subgraph' && isSubgraphNode.value)
|
|
) {
|
|
rightSidePanelStore.openPanel(tabs.value[0].value)
|
|
}
|
|
})
|
|
|
|
const isEditing = ref(false)
|
|
|
|
function handleTitleEdit(newTitle: string) {
|
|
isEditing.value = false
|
|
|
|
const trimmedTitle = newTitle.trim()
|
|
if (!trimmedTitle) return
|
|
|
|
const node = toValue(selectedNode)
|
|
if (!node) return
|
|
|
|
if (trimmedTitle === node.title) return
|
|
|
|
node.title = trimmedTitle
|
|
canvasStore.canvas?.setDirty(true, false)
|
|
}
|
|
|
|
function handleTitleCancel() {
|
|
isEditing.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
data-testid="properties-panel"
|
|
class="flex size-full flex-col bg-interface-panel-surface"
|
|
>
|
|
<!-- Panel Header -->
|
|
<section class="pt-1">
|
|
<div class="flex items-center justify-between pl-4 pr-3">
|
|
<h3 class="my-3.5 text-sm font-semibold line-clamp-2">
|
|
<EditableText
|
|
v-if="isSingleNodeSelected"
|
|
:model-value="panelTitle"
|
|
:is-editing="isEditing"
|
|
:input-attrs="{ 'data-testid': 'node-title-input' }"
|
|
@edit="handleTitleEdit"
|
|
@cancel="handleTitleCancel"
|
|
@dblclick="isEditing = true"
|
|
/>
|
|
<template v-else>
|
|
{{ panelTitle }}
|
|
</template>
|
|
</h3>
|
|
|
|
<div class="flex gap-2">
|
|
<IconButton
|
|
v-if="isSubgraphNode"
|
|
type="transparent"
|
|
size="sm"
|
|
:class="
|
|
cn(
|
|
'bg-secondary-background hover:bg-secondary-background-hover text-base-foreground',
|
|
isEditingSubgraph && 'bg-secondary-background-selected'
|
|
)
|
|
"
|
|
@click="
|
|
rightSidePanelStore.openPanel(
|
|
isEditingSubgraph ? 'parameters' : 'subgraph'
|
|
)
|
|
"
|
|
>
|
|
<i class="icon-[lucide--settings-2]" />
|
|
</IconButton>
|
|
<IconButton
|
|
type="transparent"
|
|
size="sm"
|
|
class="bg-secondary-background hover:bg-secondary-background-hover text-base-foreground"
|
|
:aria-pressed="rightSidePanelStore.isOpen"
|
|
:aria-label="t('rightSidePanel.togglePanel')"
|
|
@click="closePanel"
|
|
>
|
|
<i class="icon-[lucide--panel-right] size-4" />
|
|
</IconButton>
|
|
</div>
|
|
</div>
|
|
<nav v-if="hasSelection" class="px-4 pb-2 pt-1">
|
|
<TabList
|
|
:model-value="activeTab"
|
|
@update:model-value="
|
|
(newTab: RightSidePanelTab) => {
|
|
rightSidePanelStore.openPanel(newTab)
|
|
}
|
|
"
|
|
>
|
|
<Tab
|
|
v-for="tab in tabs"
|
|
:key="tab.value"
|
|
class="text-sm py-1 px-2 font-inter"
|
|
:value="tab.value"
|
|
>
|
|
{{ tab.label() }}
|
|
</Tab>
|
|
</TabList>
|
|
</nav>
|
|
</section>
|
|
|
|
<!-- Panel Content -->
|
|
<div class="scrollbar-thin flex-1 overflow-y-auto">
|
|
<div
|
|
v-if="!hasSelection"
|
|
class="flex size-full p-4 items-start justify-start text-sm text-muted-foreground"
|
|
>
|
|
{{ $t('rightSidePanel.noSelection') }}
|
|
</div>
|
|
<SubgraphEditor
|
|
v-else-if="isSubgraphNode && isEditingSubgraph"
|
|
:node="selectedNode"
|
|
/>
|
|
<template v-else>
|
|
<TabParameters
|
|
v-if="activeTab === 'parameters'"
|
|
:nodes="selectedNodes"
|
|
/>
|
|
<TabInfo v-else-if="activeTab === 'info'" :nodes="selectedNodes" />
|
|
<TabSettings
|
|
v-else-if="activeTab === 'settings'"
|
|
:nodes="selectedNodes"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|