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>
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -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}`)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 81 KiB |
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
54
src/components/TopMenuSection.vue
Normal 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>
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|
||||||
|
|||||||
32
src/components/icons/ComfyLogoTransparent.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<SidebarIcon
|
<SidebarIcon
|
||||||
|
label="shortcuts.shortcuts"
|
||||||
:tooltip="tooltipText"
|
:tooltip="tooltipText"
|
||||||
:selected="isShortcutsPanelVisible"
|
:selected="isShortcutsPanelVisible"
|
||||||
@click="toggleShortcutsPanel"
|
@click="toggleShortcutsPanel"
|
||||||
|
|||||||
82
src/components/topbar/LoginButton.vue
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||