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

@@ -46,6 +46,10 @@ class ComfyMenu {
.nth(0) .nth(0)
} }
get buttons() {
return this.sideToolbar.locator('.side-bar-button')
}
get nodeLibraryTab() { get nodeLibraryTab() {
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page) this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab return this._nodeLibraryTab

View File

@@ -7,7 +7,7 @@ export class Topbar {
constructor(public readonly page: Page) { constructor(public readonly page: Page) {
this.menuLocator = page.locator('.comfy-command-menu') this.menuLocator = page.locator('.comfy-command-menu')
this.menuTrigger = page.locator('.comfyui-logo-wrapper') this.menuTrigger = page.locator('.comfy-menu-button-wrapper')
} }
async getTabNames(): Promise<string[]> { async getTabNames(): Promise<string[]> {
@@ -105,7 +105,7 @@ export class Topbar {
* Close the topbar menu by clicking outside * Close the topbar menu by clicking outside
*/ */
async closeTopbarMenu() { async closeTopbarMenu() {
await this.page.locator('body').click({ position: { x: 10, y: 10 } }) await this.page.locator('body').click({ position: { x: 300, y: 10 } })
await expect(this.menuLocator).not.toBeVisible() await expect(this.menuLocator).not.toBeVisible()
} }

View File

@@ -116,9 +116,10 @@ test.describe('Actionbar', () => {
test('Can dock actionbar into top menu', async ({ comfyPage }) => { test('Can dock actionbar into top menu', async ({ comfyPage }) => {
await comfyPage.page.dragAndDrop( await comfyPage.page.dragAndDrop(
'.actionbar .drag-handle', '.actionbar .drag-handle',
'.comfyui-menu', '.actionbar-container',
{ {
targetPosition: { x: 0, y: 0 } targetPosition: { x: 50, y: 20 },
force: true
} }
) )
expect(await comfyPage.actionbar.isDocked()).toBe(true) expect(await comfyPage.actionbar.isDocked()).toBe(true)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -233,6 +233,7 @@ test.describe('Group Node', () => {
} }
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => { const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
await comfyPage.menu.nodeLibraryTab.open()
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
.getFolder(GROUP_NODE_CATEGORY) .getFolder(GROUP_NODE_CATEGORY)
.count() .count()
@@ -253,8 +254,6 @@ test.describe('Group Node', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.loadWorkflow(WORKFLOW_NAME) await comfyPage.loadWorkflow(WORKFLOW_NAME)
await comfyPage.menu.nodeLibraryTab.open()
groupNode = await comfyPage.getFirstNodeRef() groupNode = await comfyPage.getFirstNodeRef()
if (!groupNode) if (!groupNode)
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`) throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)

View File

@@ -792,10 +792,19 @@ test.describe('Viewport settings', () => {
await comfyPage.menu.topbar.saveWorkflow('Workflow A') await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame() await comfyPage.nextFrame()
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
// Save workflow as a new file, then zoom out before screen shot // Save workflow as a new file, then zoom out before screen shot
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B') await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
await comfyPage.nextFrame()
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
await changeTab(tabA)
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
await changeTab(tabB)
await comfyMouse.move(comfyPage.emptySpace) await comfyMouse.move(comfyPage.emptySpace)
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
await comfyMouse.wheel(0, 60) await comfyMouse.wheel(0, 60)
@@ -807,9 +816,6 @@ test.describe('Viewport settings', () => {
// Ensure that the screenshots are different due to zoom level // Ensure that the screenshots are different due to zoom level
expect(screenshotB).not.toBe(screenshotA) expect(screenshotB).not.toBe(screenshotA)
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
// Go back to Workflow A // Go back to Workflow A
await changeTab(tabA) await changeTab(tabA)
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe( expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(

View File

@@ -8,9 +8,7 @@ test.describe('Menu', () => {
}) })
test('Can register sidebar tab', async ({ comfyPage }) => { test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate( const initialChildrenCount = await comfyPage.menu.buttons.count()
(el) => el.children.length
)
await comfyPage.page.evaluate(async () => { await comfyPage.page.evaluate(async () => {
window['app'].extensionManager.registerSidebarTab({ window['app'].extensionManager.registerSidebarTab({
@@ -26,9 +24,7 @@ test.describe('Menu', () => {
}) })
await comfyPage.nextFrame() await comfyPage.nextFrame()
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate( const newChildrenCount = await comfyPage.menu.buttons.count()
(el) => el.children.length
)
expect(newChildrenCount).toBe(initialChildrenCount + 1) expect(newChildrenCount).toBe(initialChildrenCount + 1)
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -53,7 +53,7 @@
"comfy_base": { "comfy_base": {
"fg-color": "#222", "fg-color": "#222",
"bg-color": "#DDD", "bg-color": "#DDD",
"comfy-menu-bg": "#F5F5F5", "comfy-menu-bg": "#FFFFFF",
"comfy-menu-hover-bg": "#ccc", "comfy-menu-hover-bg": "#ccc",
"comfy-menu-secondary-bg": "#EEE", "comfy-menu-secondary-bg": "#EEE",
"comfy-input-bg": "#C9C9C9", "comfy-input-bg": "#C9C9C9",

View File

@@ -1,48 +1,88 @@
<template> <template>
<Splitter <div class="splitter-overlay-root pointer-events-none flex flex-col">
:key="sidebarStateKey" <slot name="workflow-tabs" />
class="splitter-overlay-root splitter-overlay"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'" <div
:state-key="sidebarStateKey" class="pointer-events-none flex flex-1 overflow-hidden"
state-storage="local" :class="{
> 'flex-row': sidebarLocation === 'left',
<SplitterPanel 'flex-row-reverse': sidebarLocation === 'right'
v-show="sidebarPanelVisible" }"
v-if="sidebarLocation === 'left'" >
class="side-bar-panel" <div class="side-toolbar-container pointer-events-auto">
:min-size="10" <slot name="side-toolbar" />
:size="20" </div>
>
<slot name="side-bar-panel" />
</SplitterPanel>
<SplitterPanel :size="100">
<Splitter <Splitter
class="splitter-overlay max-w-full" key="main-splitter-stable"
layout="vertical" class="splitter-overlay flex-1 overflow-hidden"
:pt:gutter="bottomPanelVisible ? '' : 'hidden'" :pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
state-key="bottom-panel-splitter" :state-key="sidebarStateKey || 'main-splitter'"
state-storage="local" state-storage="local"
> >
<SplitterPanel class="graph-canvas-panel relative"> <SplitterPanel
<slot name="graph-canvas-panel" /> 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>
<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> </SplitterPanel>
</Splitter> </Splitter>
</SplitterPanel> </div>
</div>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'right'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel" />
</SplitterPanel>
</Splitter>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -74,7 +114,11 @@ const activeSidebarTabId = computed(
) )
const sidebarStateKey = 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> </script>
@@ -93,12 +137,17 @@ const sidebarStateKey = computed(() => {
.side-bar-panel { .side-bar-panel {
background-color: var(--bg-color); background-color: var(--bg-color);
pointer-events: auto;
} }
.bottom-panel { .bottom-panel {
background-color: var(--bg-color); background-color: var(--comfy-menu-bg);
pointer-events: auto; 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 { .splitter-overlay {

View File

@@ -1,8 +1,7 @@
<template> <template>
<div <div
v-show="workspaceState.focusMode" v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag" class="comfy-menu-hamburger no-drag top-0 right-0"
:style="positionCSS"
> >
<Button <Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }" v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
@@ -15,14 +14,13 @@
@click="exitFocusMode" @click="exitFocusMode"
@contextmenu="showNativeSystemMenu" @contextmenu="showNativeSystemMenu"
/> />
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" /> <div class="window-actions-spacer" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import type { CSSProperties } from 'vue' import { watchEffect } from 'vue'
import { computed, watchEffect } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
@@ -45,15 +43,6 @@ watchEffect(() => {
app.ui.menuContainer.style.display = 'block' 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> </script>
<style scoped> <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> <template>
<Panel <div class="flex h-full items-center">
class="actionbar w-fit" <div
:style="style" v-if="isDragging && !isDocked"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }" class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
> :class="{
<div ref="panelRef" class="actionbar-content flex items-center select-none"> 'drop-zone-active': isMouseOverDropZone
<span }"
ref="dragHandleRef" @mouseenter="onMouseEnterDropZone"
:class=" @mouseleave="onMouseLeaveDropZone"
cn( >
'drag-handle cursor-grab w-3 h-max mr-2', {{ t('actionbar.dockToTop') }}
isDragging && 'cursor-grabbing'
)
"
/>
<ComfyQueueButton />
</div> </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> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import {
useDraggable, useDraggable,
useElementBounding,
useEventBus,
useEventListener, useEventListener,
useLocalStorage, useLocalStorage,
watchDebounced watchDebounced
} from '@vueuse/core' } from '@vueuse/core'
import { clamp } from 'es-toolkit/compat' import { clamp } from 'es-toolkit/compat'
import Panel from 'primevue/panel' import Panel from 'primevue/panel'
import type { Ref } from 'vue' import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
@@ -41,10 +60,9 @@ import ComfyQueueButton from './ComfyQueueButton.vue'
const settingsStore = useSettingStore() const settingsStore = useSettingStore()
const position = computed(() => settingsStore.get('Comfy.UseNewMenu')) const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
const visible = computed(() => position.value !== 'Disabled') 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 panelRef = ref<HTMLElement | null>(null)
const dragHandleRef = ref<HTMLElement | null>(null) const dragHandleRef = ref<HTMLElement | null>(null)
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true) const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
@@ -63,11 +81,9 @@ const {
containerElement: document.body, containerElement: document.body,
onMove: (event) => { onMove: (event) => {
// Prevent dragging the menu over the top of the tabs // Prevent dragging the menu over the top of the tabs
if (position.value === 'Top') { const minY = tabContainer?.getBoundingClientRect().bottom ?? 40
const minY = topMenuRef?.value?.getBoundingClientRect().top ?? 40 if (event.y < minY) {
if (event.y < minY) { event.y = minY
event.y = minY
}
} }
} }
}) })
@@ -202,39 +218,38 @@ const adjustMenuPosition = () => {
useEventListener(window, 'resize', adjustMenuPosition) useEventListener(window, 'resize', adjustMenuPosition)
const topMenuBounds = useElementBounding(topMenuRef) // Drop zone state
const overlapThreshold = 20 // pixels const isMouseOverDropZone = ref(false)
const isOverlappingWithTopMenu = computed(() => {
if (!panelRef.value) { // Mouse event handlers for self-contained drop zone
return false 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 = const onMouseLeaveDropZone = () => {
Math.min(actionbarBottom, topMenuBottom) - if (isDragging.value) {
Math.max(y.value, topMenuBounds.top.value) isMouseOverDropZone.value = false
return overlapPixels > overlapThreshold }
}) }
watch(isDragging, (newIsDragging) => { // Handle drag state changes
if (!newIsDragging) { watch(isDragging, (dragging) => {
// Stop dragging if (dragging) {
isDocked.value = isOverlappingWithTopMenu.value // Starting to drag - undock if docked
if (isDocked.value) {
isDocked.value = false
}
} else { } else {
// Start dragging // Stopped dragging - dock if mouse is over drop zone
isDocked.value = false 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> </script>
<style scoped> <style scoped>
@@ -242,17 +257,27 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
.actionbar { .actionbar {
pointer-events: all; pointer-events: all;
position: fixed;
z-index: 1000; z-index: 1000;
} }
.actionbar.is-docked { .actionbar-drop-zone {
position: static; width: 265px;
@apply bg-transparent border-none p-0; 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 { .actionbar.is-dragging {
user-select: none; user-select: none;
pointer-events: none;
} }
:deep(.p-panel-content) { :deep(.p-panel-content) {

View File

@@ -3,17 +3,42 @@
<Tabs <Tabs
:key="$i18n.locale" :key="$i18n.locale"
v-model:value="bottomPanelStore.activeBottomPanelTabId" 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="flex w-full justify-between">
<div class="tabs-container"> <div class="tabs-container">
<Tab <Tab
v-for="tab in bottomPanelStore.bottomPanelTabs" v-for="tab in bottomPanelStore.bottomPanelTabs"
:key="tab.id" :key="tab.id"
:value="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) }} {{ getTabDisplayTitle(tab) }}
</span> </span>
</Tab> </Tab>
@@ -56,6 +81,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import Tab from 'primevue/tab' import Tab from 'primevue/tab'
import type { TabPassThroughMethodOptions } from 'primevue/tab'
import TabList from 'primevue/tablist' import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs' import Tabs from 'primevue/tabs'
import { computed } from 'vue' import { computed } from 'vue'
@@ -95,3 +121,9 @@ const closeBottomPanel = () => {
bottomPanelStore.activePanel = null bottomPanelStore.activePanel = null
} }
</script> </script>
<style scoped>
:deep(.p-tablist-active-bar) {
display: none;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,28 +2,46 @@
<!-- Load splitter overlay only after comfyApp is ready. --> <!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly <!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). --> synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled"> <LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
<template v-if="!workspaceStore.focusMode" #side-bar-panel> <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 /> <SideToolbar />
</template> </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 /> <BottomPanel />
</template> </template>
<template #graph-canvas-panel> <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" /> <GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
<MiniMap <MiniMap
v-if="comfyAppReady && minimapEnabled" v-if="comfyAppReady && minimapEnabled && showUI"
class="pointer-events-auto" class="pointer-events-auto"
/> />
</template> </template>
</LiteGraphCanvasSplitterOverlay> </LiteGraphCanvasSplitterOverlay>
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas <canvas
id="graph-canvas" id="graph-canvas"
ref="canvasRef" ref="canvasRef"
@@ -81,7 +99,9 @@ import {
} from 'vue' } from 'vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue' import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import TopMenuSection from '@/components/TopMenuSection.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue' import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import DomWidgets from '@/components/graph/DomWidgets.vue' import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue' import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.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 NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue' import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.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 { useChainCallback } from '@/composables/functional/useChainCallback'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useViewportCulling } from '@/composables/graph/useViewportCulling' import { useViewportCulling } from '@/composables/graph/useViewportCulling'
@@ -129,6 +150,7 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore' import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
const emit = defineEmits<{ const emit = defineEmits<{
ready: [] ready: []
@@ -160,6 +182,12 @@ const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
const selectionToolboxEnabled = computed(() => const selectionToolboxEnabled = computed(() =>
settingStore.get('Comfy.Canvas.SelectionToolbox') 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')) 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> <template>
<div <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="{ :class="{
'comfyui-logo-menu-visible': menuRef?.visible 'comfy-menu-button-active': menuRef?.visible
}"
:style="{
minWidth: isLargeSidebar ? '4rem' : 'auto'
}" }"
@click="menuRef?.toggle($event)" @click="menuRef?.toggle($event)"
> >
<img <ComfyLogoTransparent
src="/assets/images/comfy-logo-mono.svg"
alt="ComfyUI Logo" alt="ComfyUI Logo"
class="comfyui-logo h-7" class="comfyui-logo h-[18px] w-[18px]"
@contextmenu="showNativeSystemMenu"
/> />
<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> </div>
<TieredMenu <TieredMenu
ref="menuRef" ref="menuRef"
:model="translatedItems" :model="translatedItems"
:popup="true" :popup="true"
class="comfy-command-menu" class="comfy-command-menu"
:class="{
'comfy-command-menu-top': isTopMenu
}"
@show="onMenuShow" @show="onMenuShow"
> >
<template #item="{ item, props }"> <template #item="{ item, props }">
@@ -48,7 +46,7 @@
v-else-if=" v-else-if="
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow' item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
" "
class="p-menubar-item-icon" class="p-menubar-item-icon text-sm"
:class="item.icon" :class="item.icon"
/> />
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span> <span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
@@ -67,8 +65,6 @@
</a> </a>
</template> </template>
</TieredMenu> </TieredMenu>
<SubgraphBreadcrumb />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -78,38 +74,34 @@ import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
import { computed, nextTick, ref } from 'vue' import { computed, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.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 SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useColorPaletteService } from '@/services/colorPaletteService' import { useColorPaletteService } from '@/services/colorPaletteService'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore' import { useDialogStore } from '@/stores/dialogStore'
import { useMenuItemStore } from '@/stores/menuItemStore' import { useMenuItemStore } from '@/stores/menuItemStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil' import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil' import { whileMouseDown } from '@/utils/mouseDownUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState' import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes' import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const { t } = useI18n()
const commandStore = useCommandStore()
const menuItemStore = useMenuItemStore()
const colorPaletteStore = useColorPaletteStore() const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService() const colorPaletteService = useColorPaletteService()
const menuItemsStore = useMenuItemStore()
const commandStore = useCommandStore()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const settingStore = useSettingStore()
const { t } = useI18n()
const managerState = useManagerState() const managerState = useManagerState()
const { isSmall = false } = defineProps<{
isSmall?: boolean
}>()
const menuRef = ref< const menuRef = ref<
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null ({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
>(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 translateMenuItem = (item: MenuItem): MenuItem => {
const label = typeof item.label === 'function' ? item.label() : item.label const label = typeof item.label === 'function' ? item.label() : item.label
@@ -185,7 +177,7 @@ const extraMenuItems = computed(() => [
]) ])
const translatedItems = 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 helpIndex = items.findIndex((item) => item.key === 'Help')
let helpItem: MenuItem | undefined let helpItem: MenuItem | undefined
@@ -272,16 +264,24 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
return ( return (
item.parentPath && item.parentPath &&
(item.parentPath === 'theme' || (item.parentPath === 'theme' ||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]) menuItemStore.menuItemHasActiveStateChildren[item.parentPath])
) )
} }
</script> </script>
<style scoped> <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) { .comfy-menu-button-wrapper:hover {
@apply top-auto bottom-full flex-col-reverse; 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 { .keybinding-tag {
@@ -289,11 +289,6 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
border-color: var(--p-content-border-color); border-color: var(--p-content-border-color);
border-style: solid; border-style: solid;
} }
.comfyui-logo-menu-visible,
.comfyui-logo-wrapper:hover {
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
}
</style> </style>
<style> <style>
@@ -309,22 +304,8 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
transparent transparent
); );
} }
.comfy-command-menu ul { .comfy-command-menu ul {
background-color: var(--comfy-menu-secondary-bg) !important; background-color: var(--comfy-menu-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;
}
} }
</style> </style>

View File

@@ -1,48 +1,69 @@
<template> <template>
<teleport :to="teleportTarget"> <nav
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }"> ref="sideToolbarRef"
<SidebarIcon class="side-tool-bar-container flex h-full flex-col items-center bg-transparent [.floating-sidebar]:-mr-2"
v-for="tab in tabs" :class="{
:key="tab.id" 'small-sidebar': isSmall,
:icon="tab.icon" 'connected-sidebar': isConnected,
:icon-badge="tab.iconBadge" 'floating-sidebar': !isConnected,
:tooltip="tab.tooltip" 'overflowing-sidebar': isOverflowing
: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"
> >
<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> </template>
<script setup lang="ts"> <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 SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue' import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/stores/keybindingStore' import { useKeybindingStore } from '@/stores/keybindingStore'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes' import type { SidebarTabExtension } from '@/types/extensionTypes'
import { cn } from '@/utils/tailwindUtil'
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue' import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
import SidebarIcon from './SidebarIcon.vue' import SidebarIcon from './SidebarIcon.vue'
@@ -53,16 +74,25 @@ const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const userStore = useUserStore() const userStore = useUserStore()
const commandStore = useCommandStore() const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const teleportTarget = computed(() => const sideToolbarRef = ref<HTMLElement>()
settingStore.get('Comfy.Sidebar.Location') === 'left' const contentMeasureRef = ref<HTMLElement>()
? '.comfyui-body-left' const topToolbarRef = ref<HTMLElement>()
: '.comfyui-body-right' const bottomToolbarRef = ref<HTMLElement>()
)
const isSmall = computed( const isSmall = computed(
() => settingStore.get('Comfy.Sidebar.Size') === 'small' () => 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 tabs = computed(() => workspaceStore.getSidebarTabs())
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab) const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
@@ -79,6 +109,68 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
) )
return keybinding ? ` (${keybinding.combo.toString()})` : '' 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> </script>
<style> <style>
@@ -88,36 +180,64 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
* but need to reference sidebar dimensions for proper positioning. * but need to reference sidebar dimensions for proper positioning.
*/ */
:root { :root {
--sidebar-width: 4rem; --sidebar-padding: 8px;
--sidebar-icon-size: 1rem; --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) { :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>
<style scoped> <style scoped>
.side-tool-bar-container { @reference "tailwindcss";
display: flex;
flex-direction: column;
align-items: center;
width: var(--sidebar-width); .floating-sidebar {
height: 100%; padding: var(--sidebar-padding);
background-color: var(--comfy-menu-secondary-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
} }
.side-tool-bar-container.small-sidebar { .floating-sidebar .sidebar-item-group {
--sidebar-width: 2.5rem; border-color: var(--p-panel-border-color);
--sidebar-icon-size: 1rem;
} }
.side-tool-bar-end { .connected-sidebar {
align-self: flex-end; padding: var(--sidebar-padding) 0;
margin-top: auto; 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> </style>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,10 @@
<template> <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> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,5 +1,6 @@
<template> <template>
<SidebarIcon <SidebarIcon
label="shortcuts.shortcuts"
:tooltip="tooltipText" :tooltip="tooltipText"
:selected="isShortcutsPanelVisible" :selected="isShortcutsPanelVisible"
@click="toggleShortcutsPanel" @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> <template>
<div <div
ref="positionRef" ref="positionRef"
class="absolute left-1/2 -translate-x-1/2" class="absolute bottom-0 left-1/2 -translate-x-1/2"
:class="positions.positioner"
></div> ></div>
<Popover <Popover
ref="popoverRef" ref="popoverRef"
append-to="body" append-to="body"
:pt="{ :pt="{
root: { root: {
class: 'workflow-popover-fade fit-content ' + positions.root, class: 'workflow-popover-fade fit-content',
'data-popover-id': id, 'data-popover-id': id
style: {
transform: positions.active
}
} }
}" }"
@mouseenter="cancelHidePopover" @mouseenter="cancelHidePopover"
@@ -39,9 +35,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Popover from 'primevue/popover' import Popover from 'primevue/popover'
import { computed, nextTick, ref, toRefs, useId } from 'vue' import { nextTick, ref, toRefs, useId } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
const POPOVER_WIDTH = 250 const POPOVER_WIDTH = 250
@@ -53,29 +47,6 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const { thumbnailUrl, isActiveTab } = toRefs(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 popoverRef = ref<InstanceType<typeof Popover> | null>(null)
const positionRef = ref<HTMLElement | null>(null) const positionRef = ref<HTMLElement | null>(null)
let hideTimeout: ReturnType<typeof setTimeout> | null = null let hideTimeout: ReturnType<typeof setTimeout> | null = null
@@ -174,7 +145,7 @@ defineExpose({
.workflow-preview-content { .workflow-preview-content {
@apply flex flex-col rounded-xl overflow-hidden; @apply flex flex-col rounded-xl overflow-hidden;
max-width: var(--popover-width); max-width: var(--popover-width);
background-color: var(--comfy-menu-secondary-bg); background-color: var(--comfy-menu-bg);
color: var(--fg-color); color: var(--fg-color);
} }
@@ -184,11 +155,7 @@ defineExpose({
.workflow-preview-thumbnail img { .workflow-preview-thumbnail img {
@apply shadow-md; @apply shadow-md;
background-color: color-mix( background-color: color-mix(in srgb, var(--comfy-menu-bg) 70%, black);
in srgb,
var(--comfy-menu-secondary-bg) 70%,
black
);
} }
.dark-theme .workflow-preview-thumbnail img { .dark-theme .workflow-preview-thumbnail img {

View File

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

View File

@@ -312,6 +312,14 @@ export class LGraphCanvas
} }
} }
/**
* The location of the fps info widget. Leaving an element unset will use the default position for that element.
*/
fpsInfoLocation:
| [x: number | null | undefined, y: number | null | undefined]
| null
| undefined
/** Dispatches a custom event on the canvas. */ /** Dispatches a custom event on the canvas. */
dispatch<T extends keyof NeverNever<LGraphCanvasEventMap>>( dispatch<T extends keyof NeverNever<LGraphCanvasEventMap>>(
type: T, type: T,
@@ -4698,7 +4706,8 @@ export class LGraphCanvas
// info widget // info widget
if (this.show_info) { if (this.show_info) {
this.renderInfo(ctx, area ? area[0] : 0, area ? area[1] : 0) const pos = this.fpsInfoLocation ?? area
this.renderInfo(ctx, pos?.[0] ?? 0, pos?.[1] ?? 0)
} }
if (graph) { if (graph) {

View File

@@ -593,7 +593,9 @@
"nodes": "Nodes", "nodes": "Nodes",
"models": "Models", "models": "Models",
"workflows": "Workflows", "workflows": "Workflows",
"templates": "Templates" "templates": "Templates",
"console": "Console",
"menu": "Menu"
}, },
"browseTemplates": "Browse example templates", "browseTemplates": "Browse example templates",
"openWorkflow": "Open workflow in local file system", "openWorkflow": "Open workflow in local file system",
@@ -1860,6 +1862,10 @@
"cancel": "Cancel", "cancel": "Cancel",
"success": "Account Deleted", "success": "Account Deleted",
"successDetail": "Your account has been successfully deleted." "successDetail": "Your account has been successfully deleted."
},
"loginButton": {
"tooltipHelp": "Login to be able to use \"API Nodes\"",
"tooltipLearnMore": "Learn more..."
} }
}, },
"validation": { "validation": {
@@ -1963,6 +1969,7 @@
"enterNewName": "Enter new name" "enterNewName": "Enter new name"
}, },
"shortcuts": { "shortcuts": {
"shortcuts": "Shortcuts",
"essentials": "Essential", "essentials": "Essential",
"viewControls": "View Controls", "viewControls": "View Controls",
"manageShortcuts": "Manage Shortcuts", "manageShortcuts": "Manage Shortcuts",
@@ -2004,6 +2011,9 @@
"sortRecent": "Recent", "sortRecent": "Recent",
"sortPopular": "Popular" "sortPopular": "Popular"
}, },
"actionbar": {
"dockToTop": "Dock to top"
},
"desktopDialogs": { "desktopDialogs": {
"": { "": {
"title": "Invalid Dialog", "title": "Invalid Dialog",
@@ -2013,4 +2023,4 @@
} }
} }
} }
} }

View File

@@ -115,6 +115,14 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: true, defaultValue: true,
versionAdded: '1.18.1' versionAdded: '1.18.1'
}, },
{
id: 'Comfy.Sidebar.Style',
category: ['Appearance', 'Sidebar', 'Style'],
name: 'Sidebar style',
type: 'combo',
options: ['floating', 'connected'],
defaultValue: 'floating'
},
{ {
id: 'Comfy.TextareaWidget.FontSize', id: 'Comfy.TextareaWidget.FontSize',
category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'], category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'],
@@ -549,13 +557,15 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: 'Top', defaultValue: 'Top',
name: 'Use new menu', name: 'Use new menu',
type: 'combo', type: 'combo',
options: ['Disabled', 'Top', 'Bottom'], options: ['Disabled', 'Top'],
tooltip: tooltip:
'Menu bar position. On mobile devices, the menu is always shown at the top.', 'Menu bar position. On mobile devices, the menu is always shown at the top.',
migrateDeprecatedValue: (value: string) => { migrateDeprecatedValue: (value: string) => {
// Floating is now supported by dragging the docked actionbar off. // Floating is now supported by dragging the docked actionbar off.
if (value === 'Floating') { if (value === 'Floating') {
return 'Top' return 'Top'
} else if (value === 'Bottom') {
return 'Top'
} }
return value return value
} }
@@ -564,10 +574,14 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.Workflow.WorkflowTabsPosition', id: 'Comfy.Workflow.WorkflowTabsPosition',
name: 'Opened workflows position', name: 'Opened workflows position',
type: 'combo', type: 'combo',
options: ['Sidebar', 'Topbar', 'Topbar (2nd-row)'], options: ['Sidebar', 'Topbar'],
// Default to topbar (2nd-row) if the window is less than 1536px(2xl) wide. defaultValue: 'Topbar',
defaultValue: () => migrateDeprecatedValue: (value: string) => {
window.innerWidth < 1536 ? 'Topbar (2nd-row)' : 'Topbar' if (value === 'Topbar (2nd-row)') {
return 'Topbar'
}
return value
}
}, },
{ {
id: 'Comfy.Graph.CanvasMenu', id: 'Comfy.Graph.CanvasMenu',

View File

@@ -415,19 +415,16 @@ const zSettings = z.object({
'Comfy.Sidebar.Location': z.enum(['left', 'right']), 'Comfy.Sidebar.Location': z.enum(['left', 'right']),
'Comfy.Sidebar.Size': z.enum(['small', 'normal']), 'Comfy.Sidebar.Size': z.enum(['small', 'normal']),
'Comfy.Sidebar.UnifiedWidth': z.boolean(), 'Comfy.Sidebar.UnifiedWidth': z.boolean(),
'Comfy.Sidebar.Style': z.enum(['floating', 'connected']),
'Comfy.SnapToGrid.GridSize': z.number(), 'Comfy.SnapToGrid.GridSize': z.number(),
'Comfy.TextareaWidget.FontSize': z.number(), 'Comfy.TextareaWidget.FontSize': z.number(),
'Comfy.TextareaWidget.Spellcheck': z.boolean(), 'Comfy.TextareaWidget.Spellcheck': z.boolean(),
'Comfy.UseNewMenu': z.enum(['Disabled', 'Top', 'Bottom']), 'Comfy.UseNewMenu': z.enum(['Disabled', 'Top']),
'Comfy.TreeExplorer.ItemPadding': z.number(), 'Comfy.TreeExplorer.ItemPadding': z.number(),
'Comfy.Validation.Workflows': z.boolean(), 'Comfy.Validation.Workflows': z.boolean(),
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(), 'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']), 'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
'Comfy.Workflow.WorkflowTabsPosition': z.enum([ 'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
'Sidebar',
'Topbar',
'Topbar (2nd-row)'
]),
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(), 'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
'Comfy.WidgetControlMode': z.enum(['before', 'after']), 'Comfy.WidgetControlMode': z.enum(['before', 'after']),
'Comfy.Window.UnloadConfirmation': z.boolean(), 'Comfy.Window.UnloadConfirmation': z.boolean(),

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="comfyui-body grid h-full w-full overflow-hidden"> <div class="comfyui-body grid h-full w-full overflow-hidden">
<div id="comfyui-body-top" class="comfyui-body-top"> <div id="comfyui-body-top" class="comfyui-body-top" />
<TopMenubar v-if="showTopMenu" /> <div id="comfyui-body-bottom" class="comfyui-body-bottom" />
</div>
<div id="comfyui-body-bottom" class="comfyui-body-bottom">
<TopMenubar v-if="showBottomMenu" />
</div>
<div id="comfyui-body-left" class="comfyui-body-left" /> <div id="comfyui-body-left" class="comfyui-body-left" />
<div id="comfyui-body-right" class="comfyui-body-right" /> <div id="comfyui-body-right" class="comfyui-body-right" />
<div id="graph-canvas-container" class="graph-canvas-container"> <div
id="graph-canvas-container"
ref="graphCanvasContainerRef"
class="graph-canvas-container"
>
<GraphCanvas @ready="onGraphReady" /> <GraphCanvas @ready="onGraphReady" />
</div> </div>
</div> </div>
@@ -20,7 +20,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useBreakpoints, useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import type { ToastMessageOptions } from 'primevue/toast' import type { ToastMessageOptions } from 'primevue/toast'
import { useToast } from 'primevue/usetoast' import { useToast } from 'primevue/usetoast'
import { import {
@@ -28,6 +28,7 @@ import {
nextTick, nextTick,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
ref,
watch, watch,
watchEffect watchEffect
} from 'vue' } from 'vue'
@@ -39,7 +40,6 @@ import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDi
import GraphCanvas from '@/components/graph/GraphCanvas.vue' import GraphCanvas from '@/components/graph/GraphCanvas.vue'
import GlobalToast from '@/components/toast/GlobalToast.vue' import GlobalToast from '@/components/toast/GlobalToast.vue'
import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue' import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue'
import TopMenubar from '@/components/topbar/TopMenubar.vue'
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle' import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
import { useCoreCommands } from '@/composables/useCoreCommands' import { useCoreCommands } from '@/composables/useCoreCommands'
import { useErrorHandling } from '@/composables/useErrorHandling' import { useErrorHandling } from '@/composables/useErrorHandling'
@@ -81,13 +81,7 @@ const executionStore = useExecutionStore()
const colorPaletteStore = useColorPaletteStore() const colorPaletteStore = useColorPaletteStore()
const queueStore = useQueueStore() const queueStore = useQueueStore()
const versionCompatibilityStore = useVersionCompatibilityStore() const versionCompatibilityStore = useVersionCompatibilityStore()
const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)
const breakpoints = useBreakpoints({ md: 961 })
const isMobile = breakpoints.smaller('md')
const showTopMenu = computed(() => isMobile.value || useNewMenu.value === 'Top')
const showBottomMenu = computed(
() => !isMobile.value && useNewMenu.value === 'Bottom'
)
watch( watch(
() => colorPaletteStore.completedActivePalette, () => colorPaletteStore.completedActivePalette,
@@ -226,6 +220,8 @@ onMounted(() => {
try { try {
init() init()
// Relocate the legacy menu container to the graph canvas container so it is below other elements
graphCanvasContainerRef.value?.prepend(app.ui.menuContainer)
} catch (e) { } catch (e) {
console.error('Failed to init ComfyUI frontend', e) console.error('Failed to init ComfyUI frontend', e)
} }