Floating Menus - UI rework (#5980)

## Summary

Enhancing and further modernizing the UI, giving users more usable area
whilst keeping farmiliar positioning and feel of elements.

## Changes

- **What**: Significant restructure of the UI elements, changing
elements from large blocks to floating elements, updating:
- Side toolbar menu (floating style, supports small/normal mode,
combines to scroll on height overflow)
- Bottom tabs panel (floating style, tabs redesigned)
- Action bar (support for docking/undocking menu)
    - Added login/user menu button to top right
- Restyled breadcrumbs (still collapse when overflows)
- Add litegraph support for fps info position (so it isn't covered by
the sidebar)

- **Breaking**: 
- Removed various elements and added new ones, I have tested custom
sidebars, custom actions, etc but if scripts are inserting elements into
"other" elements they may have been (re)moved.
- Remove support for bottom menu
- Remove support for 2nd-row tabs

## Screenshots 
<img width="1116" height="907" alt="ui"
src="https://github.com/user-attachments/assets/b040a215-67d3-4c88-8c4d-f402a16a34f6"
/>


https://github.com/user-attachments/assets/571dbda5-01ec-47e8-b235-ee1b88c93dd0

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5980-Floating-Menus-UI-rework-2866d73d3650810aac60cc1afe979b60)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
pythongosssss
2025-10-17 02:12:09 +01:00
committed by GitHub
parent 8cc5b52c64
commit 984ebef416
59 changed files with 793 additions and 533 deletions

View File

@@ -1,48 +1,88 @@
<template>
<Splitter
:key="sidebarStateKey"
class="splitter-overlay-root splitter-overlay"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:state-key="sidebarStateKey"
state-storage="local"
>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'left'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel" />
</SplitterPanel>
<div class="splitter-overlay-root pointer-events-none flex flex-col">
<slot name="workflow-tabs" />
<div
class="pointer-events-none flex flex-1 overflow-hidden"
:class="{
'flex-row': sidebarLocation === 'left',
'flex-row-reverse': sidebarLocation === 'right'
}"
>
<div class="side-toolbar-container pointer-events-auto">
<slot name="side-toolbar" />
</div>
<SplitterPanel :size="100">
<Splitter
class="splitter-overlay max-w-full"
layout="vertical"
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
state-key="bottom-panel-splitter"
key="main-splitter-stable"
class="splitter-overlay flex-1 overflow-hidden"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:state-key="sidebarStateKey || 'main-splitter'"
state-storage="local"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel" />
<SplitterPanel
v-if="sidebarLocation === 'left'"
class="side-bar-panel pointer-events-auto"
:min-size="10"
:size="20"
:style="{
display:
sidebarPanelVisible && sidebarLocation === 'left'
? 'flex'
: 'none'
}"
>
<slot
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
name="side-bar-panel"
/>
</SplitterPanel>
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
<slot name="bottom-panel" />
<SplitterPanel :size="80" class="flex flex-col">
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
<Splitter
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
layout="vertical"
:pt:gutter="
'rounded-tl-lg rounded-tr-lg ' +
(bottomPanelVisible ? '' : 'hidden')
"
state-key="bottom-panel-splitter"
state-storage="local"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel" />
</SplitterPanel>
<SplitterPanel
v-show="bottomPanelVisible"
class="bottom-panel pointer-events-auto rounded-lg"
>
<slot name="bottom-panel" />
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
v-if="sidebarLocation === 'right'"
class="side-bar-panel pointer-events-auto"
:min-size="10"
:size="20"
:style="{
display:
sidebarPanelVisible && sidebarLocation === 'right'
? 'flex'
: 'none'
}"
>
<slot
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
name="side-bar-panel"
/>
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'right'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel" />
</SplitterPanel>
</Splitter>
</div>
</div>
</template>
<script setup lang="ts">
@@ -74,7 +114,11 @@ const activeSidebarTabId = computed(
)
const sidebarStateKey = computed(() => {
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
if (unifiedWidth.value) {
return 'unified-sidebar'
}
// When no tab is active, use a default key to maintain state
return activeSidebarTabId.value ?? 'default-sidebar'
})
</script>
@@ -93,12 +137,17 @@ const sidebarStateKey = computed(() => {
.side-bar-panel {
background-color: var(--bg-color);
pointer-events: auto;
}
.bottom-panel {
background-color: var(--bg-color);
pointer-events: auto;
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
max-width: 100%;
overflow-x: auto;
}
.splitter-overlay-bottom :deep(.p-splitter-gutter) {
transform: translateY(5px);
}
.splitter-overlay {

View File

@@ -1,8 +1,7 @@
<template>
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag"
:style="positionCSS"
class="comfy-menu-hamburger no-drag top-0 right-0"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
@@ -15,14 +14,13 @@
@click="exitFocusMode"
@contextmenu="showNativeSystemMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
<div class="window-actions-spacer" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import type { CSSProperties } from 'vue'
import { computed, watchEffect } from 'vue'
import { watchEffect } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
@@ -45,15 +43,6 @@ watchEffect(() => {
app.ui.menuContainer.style.display = 'block'
}
})
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
const positionCSS = computed<CSSProperties>(() =>
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
menuSetting.value === 'Bottom'
? { bottom: '0px', right: '0px' }
: { top: '0px', right: '0px' }
)
</script>
<style scoped>

View File

@@ -0,0 +1,54 @@
<template>
<div
v-if="!workspaceStore.focusMode"
class="pointer-events-none ml-2 flex pt-2"
>
<div class="pointer-events-auto min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div
class="actionbar-container pointer-events-auto mx-2 flex h-12 items-center rounded-lg px-2 shadow-md"
>
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<LoginButton v-if="!isLoggedIn" />
<CurrentUserButton v-else class="shrink-0" />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
const workspaceStore = useWorkspaceStore()
const { isLoggedIn } = useCurrentUser()
// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()
onMounted(() => {
if (legacyCommandsContainerRef.value) {
app.menu.element.style.width = 'fit-content'
legacyCommandsContainerRef.value.appendChild(app.menu.element)
}
})
</script>
<style scoped>
.actionbar-container {
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
}
</style>

View File

@@ -1,38 +1,57 @@
<template>
<Panel
class="actionbar w-fit"
:style="style"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
>
<div ref="panelRef" class="actionbar-content flex items-center select-none">
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max mr-2',
isDragging && 'cursor-grabbing'
)
"
/>
<ComfyQueueButton />
<div class="flex h-full items-center">
<div
v-if="isDragging && !isDocked"
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
:class="{
'drop-zone-active': isMouseOverDropZone
}"
@mouseenter="onMouseEnterDropZone"
@mouseleave="onMouseLeaveDropZone"
>
{{ t('actionbar.dockToTop') }}
</div>
</Panel>
<Panel
class="actionbar"
:style="style"
:class="{
fixed: !isDocked,
'is-dragging': isDragging,
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
}"
>
<div
ref="panelRef"
class="actionbar-content flex items-center select-none"
>
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max mr-2',
isDragging && 'cursor-grabbing'
)
"
/>
<ComfyQueueButton />
</div>
</Panel>
</div>
</template>
<script lang="ts" setup>
import {
useDraggable,
useElementBounding,
useEventBus,
useEventListener,
useLocalStorage,
watchDebounced
} from '@vueuse/core'
import { clamp } from 'es-toolkit/compat'
import Panel from 'primevue/panel'
import type { Ref } from 'vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@/utils/tailwindUtil'
@@ -41,10 +60,9 @@ import ComfyQueueButton from './ComfyQueueButton.vue'
const settingsStore = useSettingStore()
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
const visible = computed(() => position.value !== 'Disabled')
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
const tabContainer = document.querySelector('.workflow-tabs-container')
const panelRef = ref<HTMLElement | null>(null)
const dragHandleRef = ref<HTMLElement | null>(null)
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
@@ -63,11 +81,9 @@ const {
containerElement: document.body,
onMove: (event) => {
// Prevent dragging the menu over the top of the tabs
if (position.value === 'Top') {
const minY = topMenuRef?.value?.getBoundingClientRect().top ?? 40
if (event.y < minY) {
event.y = minY
}
const minY = tabContainer?.getBoundingClientRect().bottom ?? 40
if (event.y < minY) {
event.y = minY
}
}
})
@@ -202,39 +218,38 @@ const adjustMenuPosition = () => {
useEventListener(window, 'resize', adjustMenuPosition)
const topMenuBounds = useElementBounding(topMenuRef)
const overlapThreshold = 20 // pixels
const isOverlappingWithTopMenu = computed(() => {
if (!panelRef.value) {
return false
// Drop zone state
const isMouseOverDropZone = ref(false)
// Mouse event handlers for self-contained drop zone
const onMouseEnterDropZone = () => {
if (isDragging.value) {
isMouseOverDropZone.value = true
}
const { height } = panelRef.value.getBoundingClientRect()
const actionbarBottom = y.value + height
const topMenuBottom = topMenuBounds.bottom.value
}
const overlapPixels =
Math.min(actionbarBottom, topMenuBottom) -
Math.max(y.value, topMenuBounds.top.value)
return overlapPixels > overlapThreshold
})
const onMouseLeaveDropZone = () => {
if (isDragging.value) {
isMouseOverDropZone.value = false
}
}
watch(isDragging, (newIsDragging) => {
if (!newIsDragging) {
// Stop dragging
isDocked.value = isOverlappingWithTopMenu.value
// Handle drag state changes
watch(isDragging, (dragging) => {
if (dragging) {
// Starting to drag - undock if docked
if (isDocked.value) {
isDocked.value = false
}
} else {
// Start dragging
isDocked.value = false
// Stopped dragging - dock if mouse is over drop zone
if (isMouseOverDropZone.value) {
isDocked.value = true
}
// Reset drop zone state
isMouseOverDropZone.value = false
}
})
const eventBus = useEventBus<string>('topMenu')
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
eventBus.emit('updateHighlight', {
isDragging: dragging,
isOverlapping: overlapping
})
})
</script>
<style scoped>
@@ -242,17 +257,27 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
.actionbar {
pointer-events: all;
position: fixed;
z-index: 1000;
}
.actionbar.is-docked {
position: static;
@apply bg-transparent border-none p-0;
.actionbar-drop-zone {
width: 265px;
border: 2px dashed var(--p-primary-color);
opacity: 0.8;
}
.actionbar-drop-zone.drop-zone-active {
background: var(--p-highlight-background-focus);
border-color: var(--p-primary-color);
border-width: 3px;
box-shadow: 0 0 20px var(--p-primary-color);
opacity: 1;
transform: scale(1.05);
}
.actionbar.is-dragging {
user-select: none;
pointer-events: none;
}
:deep(.p-panel-content) {

View File

@@ -3,17 +3,42 @@
<Tabs
:key="$i18n.locale"
v-model:value="bottomPanelStore.activeBottomPanelTabId"
style="--p-tabs-tablist-background: var(--comfy-menu-bg)"
>
<TabList pt:tab-list="border-none">
<TabList
pt:tab-list="border-none h-full flex items-center py-2 border-b-1 border-solid"
class="bg-transparent"
>
<div class="flex w-full justify-between">
<div class="tabs-container">
<Tab
v-for="tab in bottomPanelStore.bottomPanelTabs"
:key="tab.id"
:value="tab.id"
class="border-none p-3"
class="m-1 mx-2 border-none"
:class="{
'tab-list-single-item':
bottomPanelStore.bottomPanelTabs.length === 1
}"
:pt:root="
(x: TabPassThroughMethodOptions) => ({
class: {
'p-3 rounded-lg': true,
'pointer-events-none':
bottomPanelStore.bottomPanelTabs.length === 1
},
style: {
color: 'var(--fg-color)',
backgroundColor:
!x.context.active ||
bottomPanelStore.bottomPanelTabs.length === 1
? ''
: 'var(--bg-color)'
}
})
"
>
<span class="font-bold">
<span class="font-normal">
{{ getTabDisplayTitle(tab) }}
</span>
</Tab>
@@ -56,6 +81,7 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Tab from 'primevue/tab'
import type { TabPassThroughMethodOptions } from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
import { computed } from 'vue'
@@ -95,3 +121,9 @@ const closeBottomPanel = () => {
bottomPanelStore.activePanel = null
}
</script>
<style scoped>
:deep(.p-tablist-active-bar) {
display: none;
}
</style>

View File

@@ -64,7 +64,6 @@ const terminalCreated = (
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="h-full w-full bg-black">
<div class="h-full w-full bg-transparent">
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>
@@ -94,7 +94,6 @@ const terminalCreated = (
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,12 +1,13 @@
<template>
<div
class="subgraph-breadcrumb w-auto"
class="subgraph-breadcrumb w-auto drop-shadow-md"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
}"
:style="{
'--p-breadcrumb-gap': `${ITEM_GAP}px`,
'--p-breadcrumb-gap': `0px`,
'--p-breadcrumb-item-margin': `${ITEM_GAP / 2}px`,
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
@@ -14,7 +15,7 @@
>
<Breadcrumb
ref="breadcrumbRef"
class="bg-transparent p-0"
class="w-fit rounded-lg p-0"
:model="items"
aria-label="Graph navigation"
>
@@ -174,30 +175,65 @@ onUpdated(() => {
@apply overflow-hidden;
}
:deep(.p-breadcrumb) {
width: 100%;
background-color: transparent;
}
:deep(.p-breadcrumb-item) {
@apply flex items-center rounded-lg overflow-hidden;
@apply flex items-center overflow-hidden;
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);
/* Collapse middle items first */
flex-shrink: 10000;
}
:deep(.p-breadcrumb-separator) {
display: flex;
padding: 0 var(--p-breadcrumb-item-margin);
}
:deep(.p-breadcrumb-item-link) {
padding: 0
calc(var(--p-breadcrumb-item-margin) + var(--p-breadcrumb-item-padding));
}
:deep(.p-breadcrumb-separator),
:deep(.p-breadcrumb-item) {
@apply h-12;
border-top: 1px solid var(--p-panel-border-color);
border-bottom: 1px solid var(--p-panel-border-color);
background-color: var(--comfy-menu-bg);
}
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-icon-visible)) {
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem + 20px);
}
:deep(.p-breadcrumb-item:first-child) {
@apply rounded-l-lg;
/* Then collapse the root workflow */
flex-shrink: 5000;
border-left: 1px solid var(--p-panel-border-color);
.p-breadcrumb-item-link {
padding-left: var(--p-breadcrumb-item-padding);
}
}
:deep(.p-breadcrumb-item:last-child) {
@apply rounded-r-lg;
/* Then collapse the active item */
flex-shrink: 1;
border-right: 1px solid var(--p-panel-border-color);
}
:deep(.p-breadcrumb-item:hover),
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-menu-visible)) {
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
:deep(.p-breadcrumb-item-link:hover),
:deep(.p-breadcrumb-item-link-menu-visible) {
background-color: color-mix(
in srgb,
var(--fg-color) 10%,
var(--comfy-menu-bg)
) !important;
color: var(--fg-color);
}
</style>
@@ -214,7 +250,7 @@ onUpdated(() => {
.p-breadcrumb-item:nth-last-child(3),
.p-breadcrumb-separator:nth-last-child(2),
.p-breadcrumb-item:nth-last-child(1) {
@apply block;
@apply flex;
}
}
</style>

View File

@@ -6,7 +6,7 @@
showDelay: 512
}"
href="#"
class="p-breadcrumb-item-link cursor-pointer"
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
:class="{
'flex items-center gap-1': isActive,
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
@@ -15,7 +15,7 @@
}"
@click="handleClick"
>
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
</a>
@@ -26,7 +26,7 @@
:popup="true"
:pt="{
root: {
style: 'background-color: var(--comfy-menu-secondary-bg)'
style: 'background-color: var(--comfy-menu-bg)'
},
itemLink: {
class: 'py-2'
@@ -240,7 +240,6 @@ const inputBlur = async (doRename: boolean) => {
.p-breadcrumb-item-link {
@apply overflow-hidden;
padding: var(--p-breadcrumb-item-padding);
}
.p-breadcrumb-item-label {

View File

@@ -2,28 +2,46 @@
<!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
<div
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
>
<!-- Native drag area for Electron -->
<div
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div class="flex">
<WorkflowTabs />
<TopbarBadges />
</div>
</div>
</template>
<template v-if="showUI" #side-toolbar>
<SideToolbar />
</template>
<template v-if="!workspaceStore.focusMode" #bottom-panel>
<template v-if="showUI" #side-bar-panel>
<div
class="sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto"
>
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
</div>
</template>
<template v-if="showUI" #topmenu>
<TopMenuSection />
</template>
<template v-if="showUI" #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>
<div class="pointer-events-auto absolute top-0 left-0 w-auto max-w-full">
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
</div>
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
<MiniMap
v-if="comfyAppReady && minimapEnabled"
v-if="comfyAppReady && minimapEnabled && showUI"
class="pointer-events-auto"
/>
</template>
</LiteGraphCanvasSplitterOverlay>
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
id="graph-canvas"
ref="canvasRef"
@@ -81,7 +99,9 @@ import {
} from 'vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import TopMenuSection from '@/components/TopMenuSection.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
@@ -90,7 +110,8 @@ import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
@@ -129,6 +150,7 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
const emit = defineEmits<{
ready: []
@@ -160,6 +182,12 @@ const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
const selectionToolboxEnabled = computed(() =>
settingStore.get('Comfy.Canvas.SelectionToolbox')
)
const activeSidebarTab = computed(() => {
return workspaceStore.sidebarTab.activeSidebarTab
})
const showUI = computed(
() => !workspaceStore.focusMode && betaMenuEnabled.value
)
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))

View File

@@ -0,0 +1,32 @@
<template>
<svg
:class="iconClass"
:width="size"
:height="size"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.8193 0.600586C15.1248 0.600586 15.3296 0.70893 15.459 0.881836C15.5914 1.05888 15.6471 1.33774 15.5527 1.66895L14.8037 4.30176C14.7063 4.64386 14.4729 4.97024 14.1641 5.21191C13.8544 5.45415 13.496 5.58984 13.1699 5.58984H13.1689L9.5791 5.59668H7.90625C7.52654 5.59668 7.19496 5.84986 7.09082 6.21289L5.69434 11.0889C5.63007 11.3133 5.66134 11.5534 5.77734 11.7529L5.83203 11.8359C5.99177 12.0491 6.24252 12.1758 6.50977 12.1758H6.51074L8.88281 12.1709H11.4971C11.7643 12.171 11.9541 12.254 12.084 12.3906L12.1357 12.4521C12.2685 12.6295 12.3249 12.9089 12.2305 13.2402L11.4805 15.8721C11.383 16.2144 11.1498 16.5415 10.8408 16.7832C10.5314 17.0252 10.1736 17.161 9.84766 17.1611H9.84668L6.25684 17.168H3.64258C3.33762 17.1679 3.13349 17.0588 3.00391 16.8857C2.87135 16.7087 2.81482 16.43 2.90918 16.0986L3.39551 14.3887C3.46841 14.1327 3.41794 13.8576 3.25879 13.6445V13.6436C3.09901 13.4303 2.84745 13.3037 2.58008 13.3037H1.18066C0.875088 13.3037 0.670398 13.1953 0.541016 13.0225C0.408483 12.8451 0.351891 12.5655 0.446289 12.2344L2.11914 6.38965L2.30371 5.74707V5.74609C2.40139 5.40341 2.63456 5.07671 2.94336 4.83496C3.25302 4.59258 3.61143 4.45705 3.9375 4.45703H5.6123C5.94484 4.45703 6.24083 4.26316 6.37891 3.9707L6.42773 3.83984L6.98145 1.89551C7.07894 1.55317 7.31212 1.22614 7.62109 0.984375C7.93074 0.742127 8.2892 0.606445 8.61523 0.606445H8.61621L12.1982 0.600586H14.8193Z"
:stroke="color"
stroke-width="1"
/>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
size?: number | string
color?: string
class?: string
}
const {
size = 16,
color = 'currentColor',
class: className
} = defineProps<Props>()
const iconClass = computed(() => className || '')
</script>

View File

@@ -1,30 +1,28 @@
<template>
<div
class="comfyui-logo-wrapper mr-2 flex cursor-pointer items-center justify-center rounded-md p-1"
class="comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center rounded-t-md p-2 transition-colors"
:class="{
'comfyui-logo-menu-visible': menuRef?.visible
}"
:style="{
minWidth: isLargeSidebar ? '4rem' : 'auto'
'comfy-menu-button-active': menuRef?.visible
}"
@click="menuRef?.toggle($event)"
>
<img
src="/assets/images/comfy-logo-mono.svg"
<ComfyLogoTransparent
alt="ComfyUI Logo"
class="comfyui-logo h-7"
@contextmenu="showNativeSystemMenu"
class="comfyui-logo h-[18px] w-[18px]"
/>
<i class="pi pi-angle-down ml-1 text-[10px]" />
<span
v-if="!isSmall"
class="side-bar-button-label mt-1 text-center text-[10px]"
>{{ t('sideToolbar.labels.menu') }}</span
>
</div>
<TieredMenu
ref="menuRef"
:model="translatedItems"
:popup="true"
class="comfy-command-menu"
:class="{
'comfy-command-menu-top': isTopMenu
}"
@show="onMenuShow"
>
<template #item="{ item, props }">
@@ -48,7 +46,7 @@
v-else-if="
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
"
class="p-menubar-item-icon"
class="p-menubar-item-icon text-sm"
:class="item.icon"
/>
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
@@ -67,8 +65,6 @@
</a>
</template>
</TieredMenu>
<SubgraphBreadcrumb />
</template>
<script setup lang="ts">
@@ -78,38 +74,34 @@ import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
import { computed, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import ComfyLogoTransparent from '@/components/icons/ComfyLogoTransparent.vue'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const { t } = useI18n()
const commandStore = useCommandStore()
const menuItemStore = useMenuItemStore()
const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService()
const menuItemsStore = useMenuItemStore()
const commandStore = useCommandStore()
const dialogStore = useDialogStore()
const settingStore = useSettingStore()
const { t } = useI18n()
const managerState = useManagerState()
const { isSmall = false } = defineProps<{
isSmall?: boolean
}>()
const menuRef = ref<
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
>(null)
const isLargeSidebar = computed(
() => settingStore.get('Comfy.Sidebar.Size') !== 'small'
)
const isTopMenu = computed(() => settingStore.get('Comfy.UseNewMenu') === 'Top')
const translateMenuItem = (item: MenuItem): MenuItem => {
const label = typeof item.label === 'function' ? item.label() : item.label
@@ -185,7 +177,7 @@ const extraMenuItems = computed(() => [
])
const translatedItems = computed(() => {
const items = menuItemsStore.menuItems.map(translateMenuItem)
const items = menuItemStore.menuItems.map(translateMenuItem)
let helpIndex = items.findIndex((item) => item.key === 'Help')
let helpItem: MenuItem | undefined
@@ -272,16 +264,24 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
return (
item.parentPath &&
(item.parentPath === 'theme' ||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath])
menuItemStore.menuItemHasActiveStateChildren[item.parentPath])
)
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
.comfy-menu-button-wrapper {
width: var(--sidebar-width);
height: var(--sidebar-item-height);
}
:deep(.p-menubar-submenu.dropdown-direction-up) {
@apply top-auto bottom-full flex-col-reverse;
.comfy-menu-button-wrapper:hover {
background: var(--p-button-text-secondary-hover-background);
}
.comfy-menu-button-active,
.comfy-menu-button-active:hover {
background-color: var(--content-hover-bg);
}
.keybinding-tag {
@@ -289,11 +289,6 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
border-color: var(--p-content-border-color);
border-style: solid;
}
.comfyui-logo-menu-visible,
.comfyui-logo-wrapper:hover {
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
}
</style>
<style>
@@ -309,22 +304,8 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
transparent
);
}
.comfy-command-menu ul {
background-color: var(--comfy-menu-secondary-bg) !important;
}
.comfy-command-menu-top .p-tieredmenu-submenu {
left: calc(100% + 15px) !important;
top: -4px !important;
}
@media (max-height: 700px) {
.comfy-command-menu .p-tieredmenu-submenu {
@apply absolute max-h-[90vh] overflow-y-auto;
}
/* Help (last) submenu upward offset in compact mode */
.p-tieredmenu-root-list
> .p-tieredmenu-item:last-of-type
.p-tieredmenu-submenu {
top: -188px !important;
}
background-color: var(--comfy-menu-bg) !important;
}
</style>

View File

@@ -1,48 +1,69 @@
<template>
<teleport :to="teleportTarget">
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
<SidebarIcon
v-for="tab in tabs"
:key="tab.id"
:icon="tab.icon"
:icon-badge="tab.iconBadge"
:tooltip="tab.tooltip"
:tooltip-suffix="getTabTooltipSuffix(tab)"
:label="tab.label || tab.title"
:is-small="isSmall"
:selected="tab.id === selectedTab?.id"
:class="tab.id + '-tab-button'"
@click="onTabClick(tab)"
/>
<SidebarTemplatesButton />
<div class="side-tool-bar-end">
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
<SidebarHelpCenterIcon />
<SidebarBottomPanelToggleButton />
<SidebarShortcutsToggleButton />
</div>
</nav>
</teleport>
<div
v-if="selectedTab"
class="sidebar-content-container h-full overflow-x-hidden overflow-y-auto"
<nav
ref="sideToolbarRef"
class="side-tool-bar-container flex h-full flex-col items-center bg-transparent [.floating-sidebar]:-mr-2"
:class="{
'small-sidebar': isSmall,
'connected-sidebar': isConnected,
'floating-sidebar': !isConnected,
'overflowing-sidebar': isOverflowing
}"
>
<ExtensionSlot :extension="selectedTab" />
</div>
<div
ref="contentMeasureRef"
:class="
isOverflowing
? 'side-tool-bar-container overflow-y-auto'
: 'flex flex-col h-full'
"
>
<div ref="topToolbarRef" :class="groupClasses">
<ComfyMenuButton :is-small="isSmall" />
<SidebarIcon
v-for="tab in tabs"
:key="tab.id"
:icon="tab.icon"
:icon-badge="tab.iconBadge"
:tooltip="tab.tooltip"
:tooltip-suffix="getTabTooltipSuffix(tab)"
:label="tab.label || tab.title"
:is-small="isSmall"
:selected="tab.id === selectedTab?.id"
:class="tab.id + '-tab-button'"
@click="onTabClick(tab)"
/>
<SidebarTemplatesButton />
</div>
<div ref="bottomToolbarRef" class="mt-auto" :class="groupClasses">
<SidebarLogoutIcon
v-if="userStore.isMultiUserServer"
:is-small="isSmall"
/>
<SidebarHelpCenterIcon :is-small="isSmall" />
<SidebarBottomPanelToggleButton :is-small="isSmall" />
<SidebarShortcutsToggleButton :is-small="isSmall" />
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'es-toolkit/compat'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { cn } from '@/utils/tailwindUtil'
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
import SidebarIcon from './SidebarIcon.vue'
@@ -53,16 +74,25 @@ const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
const commandStore = useCommandStore()
const teleportTarget = computed(() =>
settingStore.get('Comfy.Sidebar.Location') === 'left'
? '.comfyui-body-left'
: '.comfyui-body-right'
)
const canvasStore = useCanvasStore()
const sideToolbarRef = ref<HTMLElement>()
const contentMeasureRef = ref<HTMLElement>()
const topToolbarRef = ref<HTMLElement>()
const bottomToolbarRef = ref<HTMLElement>()
const isSmall = computed(
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
)
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
const isConnected = computed(
() =>
selectedTab.value ||
isOverflowing.value ||
sidebarStyle.value === 'connected'
)
const tabs = computed(() => workspaceStore.getSidebarTabs())
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
@@ -79,6 +109,68 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
)
return keybinding ? ` (${keybinding.combo.toString()})` : ''
}
const isOverflowing = ref(false)
const groupClasses = computed(() =>
cn(
'sidebar-item-group flex flex-col items-center overflow-hidden flex-shrink-0' +
(isConnected.value ? '' : ' rounded-lg shadow-md')
)
)
const ENTER_OVERFLOW_MARGIN = 20
const EXIT_OVERFLOW_MARGIN = 50
const checkOverflow = debounce(() => {
if (!sideToolbarRef.value || !topToolbarRef.value || !bottomToolbarRef.value)
return
const containerHeight = sideToolbarRef.value.clientHeight
const topHeight = topToolbarRef.value.scrollHeight
const bottomHeight = bottomToolbarRef.value.scrollHeight
const contentHeight = topHeight + bottomHeight
if (isOverflowing.value) {
isOverflowing.value = containerHeight < contentHeight + EXIT_OVERFLOW_MARGIN
} else {
isOverflowing.value =
containerHeight < contentHeight + ENTER_OVERFLOW_MARGIN
}
}, 16)
onMounted(() => {
if (!sideToolbarRef.value) return
const overflowObserver = useResizeObserver(
sideToolbarRef.value,
checkOverflow
)
checkOverflow()
onBeforeUnmount(() => {
overflowObserver.stop()
})
watch(
[isSmall, sidebarLocation],
async () => {
if (canvasStore.canvas) {
if (sidebarLocation.value === 'left') {
await nextTick()
canvasStore.canvas.fpsInfoLocation = [
sideToolbarRef.value?.getBoundingClientRect()?.right,
null
]
} else {
canvasStore.canvas.fpsInfoLocation = null
}
canvasStore.canvas.setDirty(false, true)
}
},
{ immediate: true }
)
})
</script>
<style>
@@ -88,36 +180,64 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
* but need to reference sidebar dimensions for proper positioning.
*/
:root {
--sidebar-width: 4rem;
--sidebar-padding: 8px;
--sidebar-icon-size: 1rem;
--sidebar-default-floating-width: 56px;
--sidebar-default-connected-width: calc(
var(--sidebar-default-floating-width) + var(--sidebar-padding) * 2
);
--sidebar-default-item-height: 56px;
--sidebar-small-floating-width: 48px;
--sidebar-small-connected-width: calc(
var(--sidebar-small-floating-width) + var(--sidebar-padding) * 2
);
--sidebar-small-item-height: 48px;
--sidebar-width: var(--sidebar-default-floating-width);
--sidebar-item-height: var(--sidebar-default-item-height);
}
:root:has(.side-tool-bar-container.small-sidebar) {
--sidebar-width: 2.5rem;
--sidebar-width: var(--sidebar-small-floating-width);
--sidebar-item-height: var(--sidebar-small-item-height);
}
:root:has(.side-tool-bar-container.connected-sidebar) {
--sidebar-width: var(--sidebar-default-connected-width);
}
:root:has(.side-tool-bar-container.small-sidebar.connected-sidebar) {
--sidebar-width: var(--sidebar-small-connected-width);
}
</style>
<style scoped>
.side-tool-bar-container {
display: flex;
flex-direction: column;
align-items: center;
@reference "tailwindcss";
width: var(--sidebar-width);
height: 100%;
background-color: var(--comfy-menu-secondary-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
.floating-sidebar {
padding: var(--sidebar-padding);
}
.side-tool-bar-container.small-sidebar {
--sidebar-width: 2.5rem;
--sidebar-icon-size: 1rem;
.floating-sidebar .sidebar-item-group {
border-color: var(--p-panel-border-color);
}
.side-tool-bar-end {
align-self: flex-end;
margin-top: auto;
.connected-sidebar {
padding: var(--sidebar-padding) 0;
background-color: var(--comfy-menu-bg);
}
.sidebar-item-group {
background-color: var(--comfy-menu-bg);
border: 1px solid transparent;
}
.overflowing-sidebar :deep(.comfy-menu-button-wrapper) {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--comfy-menu-bg);
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<SidebarIcon
label="sideToolbar.labels.console"
:tooltip="$t('menu.toggleBottomPanel')"
:selected="bottomPanelStore.activePanel == 'terminal'"
@click="bottomPanelStore.toggleBottomPanel"

View File

@@ -3,8 +3,10 @@
<SidebarIcon
icon="pi pi-question-circle"
class="comfy-help-center-btn"
label="menu.help"
:tooltip="$t('sideToolbar.helpCenter')"
:icon-badge="shouldShowRedDot ? '' : ''"
:is-small="isSmall"
@click="toggleHelpCenter"
/>
@@ -16,7 +18,7 @@
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
'small-sidebar': isSmall
}"
>
<HelpCenterMenuContent @close="closeHelpCenter" />
@@ -29,7 +31,7 @@
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
'small-sidebar': isSmall
}"
/>
</Teleport>
@@ -40,7 +42,7 @@
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
'small-sidebar': isSmall
}"
@whats-new-dismissed="handleWhatsNewDismissed"
/>
@@ -59,7 +61,7 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, onMounted } from 'vue'
import { computed, onMounted, toRefs } from 'vue'
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -87,6 +89,11 @@ const { showNodeConflictDialog } = useDialogService()
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
useConflictAcknowledgment()
const props = defineProps<{
isSmall: boolean
}>()
const { isSmall } = toRefs(props)
// Use either release red dot or conflict red dot
const shouldShowRedDot = computed((): boolean => {
const releaseRedDot = showReleaseRedDot.value
@@ -97,8 +104,6 @@ const sidebarLocation = computed(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
const toggleHelpCenter = () => {
helpCenterStore.toggle()
}

View File

@@ -8,10 +8,8 @@
text
:pt="{
root: {
class: `side-bar-button ${
selected
? 'p-button-primary side-bar-button-selected'
: 'p-button-secondary'
class: `side-bar-button p-button-secondary ${
selected ? 'side-bar-button-selected' : ''
}`,
'aria-label': computedTooltip
}
@@ -87,9 +85,13 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
font-size: var(--sidebar-icon-size) !important;
}
.side-bar-button-selected {
background-color: var(--content-hover-bg);
color: var(--content-hover-fg);
}
.side-bar-button-selected .side-bar-button-icon {
font-size: var(--sidebar-icon-size) !important;
font-weight: 700;
}
</style>
@@ -98,8 +100,9 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
.side-bar-button {
width: var(--sidebar-width);
height: calc(var(--sidebar-width) + 0.5rem);
height: var(--sidebar-item-height);
border-radius: 0;
flex-shrink: 0;
}
.side-tool-bar-end .side-bar-button {

View File

@@ -1,5 +1,10 @@
<template>
<SidebarIcon icon="pi pi-sign-out" :tooltip="tooltip" @click="logout" />
<SidebarIcon
icon="pi pi-sign-out"
:tooltip="tooltip"
label="sideToolbar.logout"
@click="logout"
/>
</template>
<script setup lang="ts">

View File

@@ -1,5 +1,6 @@
<template>
<SidebarIcon
label="shortcuts.shortcuts"
:tooltip="tooltipText"
:selected="isShortcutsPanelVisible"
@click="toggleShortcutsPanel"

View File

@@ -0,0 +1,82 @@
<template>
<Button
v-if="!isLoggedIn"
:label="t('auth.login.loginButton')"
outlined
severity="secondary"
class="text-neutral border-black/50 px-4 capitalize dark-theme:border-white/50 dark-theme:text-white"
@click="handleSignIn()"
@mouseenter="showPopover"
@mouseleave="hidePopover"
/>
<Popover
ref="popoverRef"
class="p-2"
@mouseout="hidePopover"
@mouseover="cancelHidePopover"
>
<div>
<div class="mb-1">{{ t('auth.loginButton.tooltipHelp') }}</div>
<a
href="https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes"
target="_blank"
class="text-neutral-500 hover:text-primary"
>{{ t('auth.loginButton.tooltipLearnMore') }}</a
>
</div>
</Popover>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { ref } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { t } from '@/i18n'
const { isLoggedIn, handleSignIn } = useCurrentUser()
const popoverRef = ref<InstanceType<typeof Popover> | null>(null)
let hideTimeout: ReturnType<typeof setTimeout> | null = null
let showTimeout: ReturnType<typeof setTimeout> | null = null
const showPopover = (event: Event) => {
// Clear any existing timeouts
if (hideTimeout) {
clearTimeout(hideTimeout)
hideTimeout = null
}
if (showTimeout) {
clearTimeout(showTimeout)
showTimeout = null
}
showTimeout = setTimeout(() => {
if (popoverRef.value) {
popoverRef.value.show(event, event.target as HTMLElement)
}
}, 200)
}
const cancelHidePopover = () => {
if (hideTimeout) {
clearTimeout(hideTimeout)
hideTimeout = null
}
}
const hidePopover = () => {
// Clear show timeout if mouse leaves before popover appears
if (showTimeout) {
clearTimeout(showTimeout)
showTimeout = null
}
hideTimeout = setTimeout(() => {
if (popoverRef.value) {
popoverRef.value.hide()
}
}, 150) // Minimal delay to allow moving to popover
}
</script>

View File

@@ -1,15 +0,0 @@
<template>
<div class="w-auto max-w-full">
<WorkflowTabs />
</div>
</template>
<script setup lang="ts">
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
</script>
<style scoped>
:deep(.workflow-tabs) {
background-color: var(--comfy-menu-bg);
}
</style>

View File

@@ -1,158 +0,0 @@
<template>
<div>
<div
v-show="showTopMenu && workflowTabsPosition === 'Topbar'"
class="z-1001 flex h-9.5 w-full content-end"
style="background: var(--border-color)"
>
<WorkflowTabs />
<TopbarBadges />
</div>
<div
v-show="showTopMenu"
ref="topMenuRef"
class="comfyui-menu flex items-center bg-gray-100"
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
>
<CommandMenubar />
<div class="app-drag h-full min-w-0 grow"></div>
<div
ref="menuRight"
class="comfyui-menu-right flex-shrink-1 overflow-auto"
/>
<Actionbar />
<CurrentUserButton class="shrink-0" />
</div>
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow() && !showTopMenu"
class="app-drag fixed top-0 left-0 h-(--comfy-topbar-height) w-full"
/>
</div>
</template>
<script setup lang="ts">
import { useEventBus } from '@vueuse/core'
import { computed, onMounted, provide, ref } from 'vue'
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron, isNativeWindow } from '@/utils/envUtil'
import TopbarBadges from './TopbarBadges.vue'
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
const betaMenuEnabled = computed(() => menuSetting.value !== 'Disabled')
const showTopMenu = computed(
() => betaMenuEnabled.value && !workspaceState.focusMode
)
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)
const menuRight = ref<HTMLDivElement | null>(null)
// Menu-right holds legacy topbar elements attached by custom scripts
onMounted(() => {
if (menuRight.value) {
app.menu.element.style.width = 'fit-content'
menuRight.value.appendChild(app.menu.element)
}
})
const topMenuRef = ref<HTMLDivElement | null>(null)
provide('topMenuRef', topMenuRef)
const eventBus = useEventBus<string>('topMenu')
const isDropZone = ref(false)
const isDroppable = ref(false)
eventBus.on((event: string, payload: any) => {
if (event === 'updateHighlight') {
isDropZone.value = payload.isDragging
isDroppable.value = payload.isOverlapping && payload.isDragging
}
})
onMounted(() => {
if (isElectron()) {
electronAPI().changeTheme({
height: topMenuRef.value?.getBoundingClientRect().height ?? 0
})
}
})
</script>
<style scoped>
.comfyui-menu {
width: 100vw;
height: var(--comfy-topbar-height);
background: var(--comfy-menu-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
box-sizing: border-box;
z-index: 1000;
order: 0;
grid-column: 1/-1;
}
.comfyui-menu.dropzone {
background: var(--p-highlight-background);
}
.comfyui-menu.dropzone-active {
background: var(--p-highlight-background-focus);
}
:deep(.p-menubar-item-label) {
line-height: revert;
}
.comfyui-logo {
user-select: none;
cursor: default;
filter: invert(0);
}
.dark-theme .comfyui-logo {
filter: invert(1);
}
.comfyui-menu-button-hide {
background-color: var(--comfy-menu-secondary-bg);
border-left: 1px solid var(--border-color);
}
</style>
<style>
.comfyui-menu-right::-webkit-scrollbar {
max-height: 5px;
}
.comfyui-menu-right:hover::-webkit-scrollbar {
cursor: grab;
}
.comfyui-menu-right::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--border-color) 60%, transparent);
}
.comfyui-menu-right:hover::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--border-color) 80%, transparent);
}
.comfyui-menu-right::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--fg-color) 30%, transparent);
}
.comfyui-menu-right::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--fg-color) 80%, transparent);
}
</style>

View File

@@ -1,19 +1,15 @@
<template>
<div
ref="positionRef"
class="absolute left-1/2 -translate-x-1/2"
:class="positions.positioner"
class="absolute bottom-0 left-1/2 -translate-x-1/2"
></div>
<Popover
ref="popoverRef"
append-to="body"
:pt="{
root: {
class: 'workflow-popover-fade fit-content ' + positions.root,
'data-popover-id': id,
style: {
transform: positions.active
}
class: 'workflow-popover-fade fit-content',
'data-popover-id': id
}
}"
@mouseenter="cancelHidePopover"
@@ -39,9 +35,7 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import { computed, nextTick, ref, toRefs, useId } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { nextTick, ref, toRefs, useId } from 'vue'
const POPOVER_WIDTH = 250
@@ -53,29 +47,6 @@ interface Props {
const props = defineProps<Props>()
const { thumbnailUrl, isActiveTab } = toRefs(props)
const settingStore = useSettingStore()
const positions = computed<{
positioner: string
root?: string
active?: string
}>(() => {
if (
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') === 'Topbar' &&
settingStore.get('Comfy.UseNewMenu') === 'Bottom'
) {
return {
positioner: 'top-0',
root: 'p-popover-flipped',
active: isActiveTab.value ? 'translateY(-100%)' : undefined
}
}
return {
positioner: 'bottom-0'
}
})
const popoverRef = ref<InstanceType<typeof Popover> | null>(null)
const positionRef = ref<HTMLElement | null>(null)
let hideTimeout: ReturnType<typeof setTimeout> | null = null
@@ -174,7 +145,7 @@ defineExpose({
.workflow-preview-content {
@apply flex flex-col rounded-xl overflow-hidden;
max-width: var(--popover-width);
background-color: var(--comfy-menu-secondary-bg);
background-color: var(--comfy-menu-bg);
color: var(--fg-color);
}
@@ -184,11 +155,7 @@ defineExpose({
.workflow-preview-thumbnail img {
@apply shadow-md;
background-color: color-mix(
in srgb,
var(--comfy-menu-secondary-bg) 70%,
black
);
background-color: color-mix(in srgb, var(--comfy-menu-bg) 70%, black);
}
.dark-theme .workflow-preview-thumbnail img {

View File

@@ -63,10 +63,7 @@
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
/>
<ContextMenu ref="menu" :model="contextMenuItems" />
<div
v-if="menuSetting !== 'Bottom' && isDesktop"
class="window-actions-spacer app-drag shrink-0"
/>
<div v-if="isDesktop" class="window-actions-spacer app-drag shrink-0" />
</div>
</template>
@@ -81,7 +78,6 @@ import { useI18n } from 'vue-i18n'
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import {
@@ -108,7 +104,6 @@ const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const workflowStore = useWorkflowStore()
const workflowBookmarkStore = useWorkflowBookmarkStore()
const settingStore = useSettingStore()
const workflowService = useWorkflowService()
const rightClickedTab = ref<WorkflowOption | undefined>()
@@ -119,7 +114,6 @@ const leftArrowEnabled = ref(false)
const rightArrowEnabled = ref(false)
const isDesktop = isElectron()
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
const workflowToOption = (workflow: ComfyWorkflow): WorkflowOption => ({
value: workflow.path,
@@ -308,7 +302,7 @@ onUpdated(() => {
@reference '../../assets/css/style.css';
.workflow-tabs-container {
background-color: var(--comfy-menu-secondary-bg);
background-color: var(--comfy-menu-bg);
}
:deep(.p-togglebutton) {