mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
Floating Menus - UI rework (#5980)
## Summary
Enhancing and further modernizing the UI, giving users more usable area
whilst keeping farmiliar positioning and feel of elements.
## Changes
- **What**: Significant restructure of the UI elements, changing
elements from large blocks to floating elements, updating:
- Side toolbar menu (floating style, supports small/normal mode,
combines to scroll on height overflow)
- Bottom tabs panel (floating style, tabs redesigned)
- Action bar (support for docking/undocking menu)
- Added login/user menu button to top right
- Restyled breadcrumbs (still collapse when overflows)
- Add litegraph support for fps info position (so it isn't covered by
the sidebar)
- **Breaking**:
- Removed various elements and added new ones, I have tested custom
sidebars, custom actions, etc but if scripts are inserting elements into
"other" elements they may have been (re)moved.
- Remove support for bottom menu
- Remove support for 2nd-row tabs
## Screenshots
<img width="1116" height="907" alt="ui"
src="https://github.com/user-attachments/assets/b040a215-67d3-4c88-8c4d-f402a16a34f6"
/>
https://github.com/user-attachments/assets/571dbda5-01ec-47e8-b235-ee1b88c93dd0
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5980-Floating-Menus-UI-rework-2866d73d3650810aac60cc1afe979b60)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -1,48 +1,88 @@
|
||||
<template>
|
||||
<Splitter
|
||||
:key="sidebarStateKey"
|
||||
class="splitter-overlay-root splitter-overlay"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
<div class="splitter-overlay-root pointer-events-none flex flex-col">
|
||||
<slot name="workflow-tabs" />
|
||||
|
||||
<div
|
||||
class="pointer-events-none flex flex-1 overflow-hidden"
|
||||
:class="{
|
||||
'flex-row': sidebarLocation === 'left',
|
||||
'flex-row-reverse': sidebarLocation === 'right'
|
||||
}"
|
||||
>
|
||||
<div class="side-toolbar-container pointer-events-auto">
|
||||
<slot name="side-toolbar" />
|
||||
</div>
|
||||
|
||||
<SplitterPanel :size="100">
|
||||
<Splitter
|
||||
class="splitter-overlay max-w-full"
|
||||
layout="vertical"
|
||||
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
||||
state-key="bottom-panel-splitter"
|
||||
key="main-splitter-stable"
|
||||
class="splitter-overlay flex-1 overflow-hidden"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey || 'main-splitter'"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'left'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
|
||||
<slot name="bottom-panel" />
|
||||
|
||||
<SplitterPanel :size="80" class="flex flex-col">
|
||||
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
|
||||
|
||||
<Splitter
|
||||
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
|
||||
layout="vertical"
|
||||
:pt:gutter="
|
||||
'rounded-tl-lg rounded-tr-lg ' +
|
||||
(bottomPanelVisible ? '' : 'hidden')
|
||||
"
|
||||
state-key="bottom-panel-splitter"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
v-show="bottomPanelVisible"
|
||||
class="bottom-panel pointer-events-auto rounded-lg"
|
||||
>
|
||||
<slot name="bottom-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'right'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -74,7 +114,11 @@ const activeSidebarTabId = computed(
|
||||
)
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
|
||||
if (unifiedWidth.value) {
|
||||
return 'unified-sidebar'
|
||||
}
|
||||
// When no tab is active, use a default key to maintain state
|
||||
return activeSidebarTabId.value ?? 'default-sidebar'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -93,12 +137,17 @@ const sidebarStateKey = computed(() => {
|
||||
|
||||
.side-bar-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.splitter-overlay-bottom :deep(.p-splitter-gutter) {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
.splitter-overlay {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="workspaceState.focusMode"
|
||||
class="comfy-menu-hamburger no-drag"
|
||||
:style="positionCSS"
|
||||
class="comfy-menu-hamburger no-drag top-0 right-0"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
@@ -15,14 +14,13 @@
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
<div class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -45,15 +43,6 @@ watchEffect(() => {
|
||||
app.ui.menuContainer.style.display = 'block'
|
||||
}
|
||||
})
|
||||
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const positionCSS = computed<CSSProperties>(() =>
|
||||
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
|
||||
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
|
||||
menuSetting.value === 'Bottom'
|
||||
? { bottom: '0px', right: '0px' }
|
||||
: { top: '0px', right: '0px' }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
54
src/components/TopMenuSection.vue
Normal file
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>
|
||||
<Panel
|
||||
class="actionbar w-fit"
|
||||
:style="style"
|
||||
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
||||
>
|
||||
<div ref="panelRef" class="actionbar-content flex items-center select-none">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ComfyQueueButton />
|
||||
<div class="flex h-full items-center">
|
||||
<div
|
||||
v-if="isDragging && !isDocked"
|
||||
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
|
||||
:class="{
|
||||
'drop-zone-active': isMouseOverDropZone
|
||||
}"
|
||||
@mouseenter="onMouseEnterDropZone"
|
||||
@mouseleave="onMouseLeaveDropZone"
|
||||
>
|
||||
{{ t('actionbar.dockToTop') }}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
class="actionbar"
|
||||
:style="style"
|
||||
:class="{
|
||||
fixed: !isDocked,
|
||||
'is-dragging': isDragging,
|
||||
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="panelRef"
|
||||
class="actionbar-content flex items-center select-none"
|
||||
>
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ComfyQueueButton />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
useDraggable,
|
||||
useElementBounding,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -41,10 +60,9 @@ import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
const settingsStore = useSettingStore()
|
||||
|
||||
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
|
||||
const visible = computed(() => position.value !== 'Disabled')
|
||||
|
||||
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
|
||||
const tabContainer = document.querySelector('.workflow-tabs-container')
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
||||
@@ -63,11 +81,9 @@ const {
|
||||
containerElement: document.body,
|
||||
onMove: (event) => {
|
||||
// Prevent dragging the menu over the top of the tabs
|
||||
if (position.value === 'Top') {
|
||||
const minY = topMenuRef?.value?.getBoundingClientRect().top ?? 40
|
||||
if (event.y < minY) {
|
||||
event.y = minY
|
||||
}
|
||||
const minY = tabContainer?.getBoundingClientRect().bottom ?? 40
|
||||
if (event.y < minY) {
|
||||
event.y = minY
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -202,39 +218,38 @@ const adjustMenuPosition = () => {
|
||||
|
||||
useEventListener(window, 'resize', adjustMenuPosition)
|
||||
|
||||
const topMenuBounds = useElementBounding(topMenuRef)
|
||||
const overlapThreshold = 20 // pixels
|
||||
const isOverlappingWithTopMenu = computed(() => {
|
||||
if (!panelRef.value) {
|
||||
return false
|
||||
// Drop zone state
|
||||
const isMouseOverDropZone = ref(false)
|
||||
|
||||
// Mouse event handlers for self-contained drop zone
|
||||
const onMouseEnterDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = true
|
||||
}
|
||||
const { height } = panelRef.value.getBoundingClientRect()
|
||||
const actionbarBottom = y.value + height
|
||||
const topMenuBottom = topMenuBounds.bottom.value
|
||||
}
|
||||
|
||||
const overlapPixels =
|
||||
Math.min(actionbarBottom, topMenuBottom) -
|
||||
Math.max(y.value, topMenuBounds.top.value)
|
||||
return overlapPixels > overlapThreshold
|
||||
})
|
||||
const onMouseLeaveDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDragging, (newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
isDocked.value = isOverlappingWithTopMenu.value
|
||||
// Handle drag state changes
|
||||
watch(isDragging, (dragging) => {
|
||||
if (dragging) {
|
||||
// Starting to drag - undock if docked
|
||||
if (isDocked.value) {
|
||||
isDocked.value = false
|
||||
}
|
||||
} else {
|
||||
// Start dragging
|
||||
isDocked.value = false
|
||||
// Stopped dragging - dock if mouse is over drop zone
|
||||
if (isMouseOverDropZone.value) {
|
||||
isDocked.value = true
|
||||
}
|
||||
// Reset drop zone state
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const eventBus = useEventBus<string>('topMenu')
|
||||
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
eventBus.emit('updateHighlight', {
|
||||
isDragging: dragging,
|
||||
isOverlapping: overlapping
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -242,17 +257,27 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
|
||||
.actionbar {
|
||||
pointer-events: all;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.actionbar.is-docked {
|
||||
position: static;
|
||||
@apply bg-transparent border-none p-0;
|
||||
.actionbar-drop-zone {
|
||||
width: 265px;
|
||||
border: 2px dashed var(--p-primary-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.actionbar-drop-zone.drop-zone-active {
|
||||
background: var(--p-highlight-background-focus);
|
||||
border-color: var(--p-primary-color);
|
||||
border-width: 3px;
|
||||
box-shadow: 0 0 20px var(--p-primary-color);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.actionbar.is-dragging {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
|
||||
@@ -3,17 +3,42 @@
|
||||
<Tabs
|
||||
:key="$i18n.locale"
|
||||
v-model:value="bottomPanelStore.activeBottomPanelTabId"
|
||||
style="--p-tabs-tablist-background: var(--comfy-menu-bg)"
|
||||
>
|
||||
<TabList pt:tab-list="border-none">
|
||||
<TabList
|
||||
pt:tab-list="border-none h-full flex items-center py-2 border-b-1 border-solid"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class="tabs-container">
|
||||
<Tab
|
||||
v-for="tab in bottomPanelStore.bottomPanelTabs"
|
||||
:key="tab.id"
|
||||
:value="tab.id"
|
||||
class="border-none p-3"
|
||||
class="m-1 mx-2 border-none"
|
||||
:class="{
|
||||
'tab-list-single-item':
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
}"
|
||||
:pt:root="
|
||||
(x: TabPassThroughMethodOptions) => ({
|
||||
class: {
|
||||
'p-3 rounded-lg': true,
|
||||
'pointer-events-none':
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
},
|
||||
style: {
|
||||
color: 'var(--fg-color)',
|
||||
backgroundColor:
|
||||
!x.context.active ||
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
? ''
|
||||
: 'var(--bg-color)'
|
||||
}
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="font-bold">
|
||||
<span class="font-normal">
|
||||
{{ getTabDisplayTitle(tab) }}
|
||||
</span>
|
||||
</Tab>
|
||||
@@ -56,6 +81,7 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tab from 'primevue/tab'
|
||||
import type { TabPassThroughMethodOptions } from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed } from 'vue'
|
||||
@@ -95,3 +121,9 @@ const closeBottomPanel = () => {
|
||||
bottomPanelStore.activePanel = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-tablist-active-bar) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,7 +64,6 @@ const terminalCreated = (
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full w-full bg-black">
|
||||
<div class="h-full w-full bg-transparent">
|
||||
<p v-if="errorMessage" class="p-4 text-center">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
@@ -94,7 +94,6 @@ const terminalCreated = (
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="subgraph-breadcrumb w-auto"
|
||||
class="subgraph-breadcrumb w-auto drop-shadow-md"
|
||||
:class="{
|
||||
'subgraph-breadcrumb-collapse': collapseTabs,
|
||||
'subgraph-breadcrumb-overflow': overflowingTabs
|
||||
}"
|
||||
:style="{
|
||||
'--p-breadcrumb-gap': `${ITEM_GAP}px`,
|
||||
'--p-breadcrumb-gap': `0px`,
|
||||
'--p-breadcrumb-item-margin': `${ITEM_GAP / 2}px`,
|
||||
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
|
||||
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
|
||||
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
|
||||
@@ -14,7 +15,7 @@
|
||||
>
|
||||
<Breadcrumb
|
||||
ref="breadcrumbRef"
|
||||
class="bg-transparent p-0"
|
||||
class="w-fit rounded-lg p-0"
|
||||
:model="items"
|
||||
aria-label="Graph navigation"
|
||||
>
|
||||
@@ -174,30 +175,65 @@ onUpdated(() => {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb) {
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item) {
|
||||
@apply flex items-center rounded-lg overflow-hidden;
|
||||
@apply flex items-center overflow-hidden;
|
||||
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);
|
||||
/* Collapse middle items first */
|
||||
flex-shrink: 10000;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-separator) {
|
||||
display: flex;
|
||||
padding: 0 var(--p-breadcrumb-item-margin);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item-link) {
|
||||
padding: 0
|
||||
calc(var(--p-breadcrumb-item-margin) + var(--p-breadcrumb-item-padding));
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-separator),
|
||||
:deep(.p-breadcrumb-item) {
|
||||
@apply h-12;
|
||||
border-top: 1px solid var(--p-panel-border-color);
|
||||
border-bottom: 1px solid var(--p-panel-border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-icon-visible)) {
|
||||
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem + 20px);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:first-child) {
|
||||
@apply rounded-l-lg;
|
||||
/* Then collapse the root workflow */
|
||||
flex-shrink: 5000;
|
||||
border-left: 1px solid var(--p-panel-border-color);
|
||||
|
||||
.p-breadcrumb-item-link {
|
||||
padding-left: var(--p-breadcrumb-item-padding);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:last-child) {
|
||||
@apply rounded-r-lg;
|
||||
/* Then collapse the active item */
|
||||
flex-shrink: 1;
|
||||
border-right: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:hover),
|
||||
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-menu-visible)) {
|
||||
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
|
||||
:deep(.p-breadcrumb-item-link:hover),
|
||||
:deep(.p-breadcrumb-item-link-menu-visible) {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--fg-color) 10%,
|
||||
var(--comfy-menu-bg)
|
||||
) !important;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -214,7 +250,7 @@ onUpdated(() => {
|
||||
.p-breadcrumb-item:nth-last-child(3),
|
||||
.p-breadcrumb-separator:nth-last-child(2),
|
||||
.p-breadcrumb-item:nth-last-child(1) {
|
||||
@apply block;
|
||||
@apply flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
showDelay: 512
|
||||
}"
|
||||
href="#"
|
||||
class="p-breadcrumb-item-link cursor-pointer"
|
||||
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
|
||||
:class="{
|
||||
'flex items-center gap-1': isActive,
|
||||
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
|
||||
@@ -15,7 +15,7 @@
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
|
||||
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
|
||||
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
</a>
|
||||
@@ -26,7 +26,7 @@
|
||||
:popup="true"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'background-color: var(--comfy-menu-secondary-bg)'
|
||||
style: 'background-color: var(--comfy-menu-bg)'
|
||||
},
|
||||
itemLink: {
|
||||
class: 'py-2'
|
||||
@@ -240,7 +240,6 @@ const inputBlur = async (doRename: boolean) => {
|
||||
|
||||
.p-breadcrumb-item-link {
|
||||
@apply overflow-hidden;
|
||||
padding: var(--p-breadcrumb-item-padding);
|
||||
}
|
||||
|
||||
.p-breadcrumb-item-label {
|
||||
|
||||
@@ -2,28 +2,46 @@
|
||||
<!-- Load splitter overlay only after comfyApp is ready. -->
|
||||
<!-- If load immediately, the top-level splitter stateKey won't be correctly
|
||||
synced with the stateStorage (localStorage). -->
|
||||
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
|
||||
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
|
||||
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
|
||||
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
|
||||
<div
|
||||
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
|
||||
>
|
||||
<!-- Native drag area for Electron -->
|
||||
<div
|
||||
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
|
||||
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
|
||||
/>
|
||||
<div class="flex">
|
||||
<WorkflowTabs />
|
||||
<TopbarBadges />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showUI" #side-toolbar>
|
||||
<SideToolbar />
|
||||
</template>
|
||||
<template v-if="!workspaceStore.focusMode" #bottom-panel>
|
||||
<template v-if="showUI" #side-bar-panel>
|
||||
<div
|
||||
class="sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showUI" #topmenu>
|
||||
<TopMenuSection />
|
||||
</template>
|
||||
<template v-if="showUI" #bottom-panel>
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<div class="pointer-events-auto absolute top-0 left-0 w-auto max-w-full">
|
||||
<SecondRowWorkflowTabs
|
||||
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
|
||||
/>
|
||||
</div>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||
|
||||
<MiniMap
|
||||
v-if="comfyAppReady && minimapEnabled"
|
||||
v-if="comfyAppReady && minimapEnabled && showUI"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
||||
<canvas
|
||||
id="graph-canvas"
|
||||
ref="canvasRef"
|
||||
@@ -81,7 +99,9 @@ import {
|
||||
} from 'vue'
|
||||
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import TopMenuSection from '@/components/TopMenuSection.vue'
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
@@ -90,7 +110,8 @@ import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
||||
@@ -129,6 +150,7 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isNativeWindow } from '@/utils/envUtil'
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: []
|
||||
@@ -160,6 +182,12 @@ const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
const selectionToolboxEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
||||
)
|
||||
const activeSidebarTab = computed(() => {
|
||||
return workspaceStore.sidebarTab.activeSidebarTab
|
||||
})
|
||||
const showUI = computed(
|
||||
() => !workspaceStore.focusMode && betaMenuEnabled.value
|
||||
)
|
||||
|
||||
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
|
||||
|
||||
32
src/components/icons/ComfyLogoTransparent.vue
Normal file
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>
|
||||
<div
|
||||
class="comfyui-logo-wrapper mr-2 flex cursor-pointer items-center justify-center rounded-md p-1"
|
||||
class="comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center rounded-t-md p-2 transition-colors"
|
||||
:class="{
|
||||
'comfyui-logo-menu-visible': menuRef?.visible
|
||||
}"
|
||||
:style="{
|
||||
minWidth: isLargeSidebar ? '4rem' : 'auto'
|
||||
'comfy-menu-button-active': menuRef?.visible
|
||||
}"
|
||||
@click="menuRef?.toggle($event)"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/comfy-logo-mono.svg"
|
||||
<ComfyLogoTransparent
|
||||
alt="ComfyUI Logo"
|
||||
class="comfyui-logo h-7"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
class="comfyui-logo h-[18px] w-[18px]"
|
||||
/>
|
||||
<i class="pi pi-angle-down ml-1 text-[10px]" />
|
||||
|
||||
<span
|
||||
v-if="!isSmall"
|
||||
class="side-bar-button-label mt-1 text-center text-[10px]"
|
||||
>{{ t('sideToolbar.labels.menu') }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<TieredMenu
|
||||
ref="menuRef"
|
||||
:model="translatedItems"
|
||||
:popup="true"
|
||||
class="comfy-command-menu"
|
||||
:class="{
|
||||
'comfy-command-menu-top': isTopMenu
|
||||
}"
|
||||
@show="onMenuShow"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
@@ -48,7 +46,7 @@
|
||||
v-else-if="
|
||||
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
|
||||
"
|
||||
class="p-menubar-item-icon"
|
||||
class="p-menubar-item-icon text-sm"
|
||||
:class="item.icon"
|
||||
/>
|
||||
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
|
||||
@@ -67,8 +65,6 @@
|
||||
</a>
|
||||
</template>
|
||||
</TieredMenu>
|
||||
|
||||
<SubgraphBreadcrumb />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -78,38 +74,34 @@ import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import ComfyLogoTransparent from '@/components/icons/ComfyLogoTransparent.vue'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const menuItemStore = useMenuItemStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const managerState = useManagerState()
|
||||
|
||||
const { isSmall = false } = defineProps<{
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
|
||||
>(null)
|
||||
const isLargeSidebar = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') !== 'small'
|
||||
)
|
||||
const isTopMenu = computed(() => settingStore.get('Comfy.UseNewMenu') === 'Top')
|
||||
|
||||
const translateMenuItem = (item: MenuItem): MenuItem => {
|
||||
const label = typeof item.label === 'function' ? item.label() : item.label
|
||||
@@ -185,7 +177,7 @@ const extraMenuItems = computed(() => [
|
||||
])
|
||||
|
||||
const translatedItems = computed(() => {
|
||||
const items = menuItemsStore.menuItems.map(translateMenuItem)
|
||||
const items = menuItemStore.menuItems.map(translateMenuItem)
|
||||
let helpIndex = items.findIndex((item) => item.key === 'Help')
|
||||
let helpItem: MenuItem | undefined
|
||||
|
||||
@@ -272,16 +264,24 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
return (
|
||||
item.parentPath &&
|
||||
(item.parentPath === 'theme' ||
|
||||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath])
|
||||
menuItemStore.menuItemHasActiveStateChildren[item.parentPath])
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
.comfy-menu-button-wrapper {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-item-height);
|
||||
}
|
||||
|
||||
:deep(.p-menubar-submenu.dropdown-direction-up) {
|
||||
@apply top-auto bottom-full flex-col-reverse;
|
||||
.comfy-menu-button-wrapper:hover {
|
||||
background: var(--p-button-text-secondary-hover-background);
|
||||
}
|
||||
|
||||
.comfy-menu-button-active,
|
||||
.comfy-menu-button-active:hover {
|
||||
background-color: var(--content-hover-bg);
|
||||
}
|
||||
|
||||
.keybinding-tag {
|
||||
@@ -289,11 +289,6 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
border-color: var(--p-content-border-color);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.comfyui-logo-menu-visible,
|
||||
.comfyui-logo-wrapper:hover {
|
||||
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@@ -309,22 +304,8 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.comfy-command-menu ul {
|
||||
background-color: var(--comfy-menu-secondary-bg) !important;
|
||||
}
|
||||
.comfy-command-menu-top .p-tieredmenu-submenu {
|
||||
left: calc(100% + 15px) !important;
|
||||
top: -4px !important;
|
||||
}
|
||||
@media (max-height: 700px) {
|
||||
.comfy-command-menu .p-tieredmenu-submenu {
|
||||
@apply absolute max-h-[90vh] overflow-y-auto;
|
||||
}
|
||||
/* Help (last) submenu upward offset in compact mode */
|
||||
.p-tieredmenu-root-list
|
||||
> .p-tieredmenu-item:last-of-type
|
||||
.p-tieredmenu-submenu {
|
||||
top: -188px !important;
|
||||
}
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +1,69 @@
|
||||
<template>
|
||||
<teleport :to="teleportTarget">
|
||||
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:icon-badge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip-suffix="getTabTooltipSuffix(tab)"
|
||||
:label="tab.label || tab.title"
|
||||
:is-small="isSmall"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<SidebarTemplatesButton />
|
||||
<div class="side-tool-bar-end">
|
||||
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
||||
<SidebarHelpCenterIcon />
|
||||
<SidebarBottomPanelToggleButton />
|
||||
<SidebarShortcutsToggleButton />
|
||||
</div>
|
||||
</nav>
|
||||
</teleport>
|
||||
<div
|
||||
v-if="selectedTab"
|
||||
class="sidebar-content-container h-full overflow-x-hidden overflow-y-auto"
|
||||
<nav
|
||||
ref="sideToolbarRef"
|
||||
class="side-tool-bar-container flex h-full flex-col items-center bg-transparent [.floating-sidebar]:-mr-2"
|
||||
:class="{
|
||||
'small-sidebar': isSmall,
|
||||
'connected-sidebar': isConnected,
|
||||
'floating-sidebar': !isConnected,
|
||||
'overflowing-sidebar': isOverflowing
|
||||
}"
|
||||
>
|
||||
<ExtensionSlot :extension="selectedTab" />
|
||||
</div>
|
||||
<div
|
||||
ref="contentMeasureRef"
|
||||
:class="
|
||||
isOverflowing
|
||||
? 'side-tool-bar-container overflow-y-auto'
|
||||
: 'flex flex-col h-full'
|
||||
"
|
||||
>
|
||||
<div ref="topToolbarRef" :class="groupClasses">
|
||||
<ComfyMenuButton :is-small="isSmall" />
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:icon-badge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip-suffix="getTabTooltipSuffix(tab)"
|
||||
:label="tab.label || tab.title"
|
||||
:is-small="isSmall"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<SidebarTemplatesButton />
|
||||
</div>
|
||||
|
||||
<div ref="bottomToolbarRef" class="mt-auto" :class="groupClasses">
|
||||
<SidebarLogoutIcon
|
||||
v-if="userStore.isMultiUserServer"
|
||||
:is-small="isSmall"
|
||||
/>
|
||||
<SidebarHelpCenterIcon :is-small="isSmall" />
|
||||
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
||||
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
|
||||
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
||||
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
@@ -53,16 +74,25 @@ const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const teleportTarget = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
||||
? '.comfyui-body-left'
|
||||
: '.comfyui-body-right'
|
||||
)
|
||||
const canvasStore = useCanvasStore()
|
||||
const sideToolbarRef = ref<HTMLElement>()
|
||||
const contentMeasureRef = ref<HTMLElement>()
|
||||
const topToolbarRef = ref<HTMLElement>()
|
||||
const bottomToolbarRef = ref<HTMLElement>()
|
||||
|
||||
const isSmall = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
|
||||
)
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
|
||||
const isConnected = computed(
|
||||
() =>
|
||||
selectedTab.value ||
|
||||
isOverflowing.value ||
|
||||
sidebarStyle.value === 'connected'
|
||||
)
|
||||
|
||||
const tabs = computed(() => workspaceStore.getSidebarTabs())
|
||||
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
@@ -79,6 +109,68 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
)
|
||||
return keybinding ? ` (${keybinding.combo.toString()})` : ''
|
||||
}
|
||||
|
||||
const isOverflowing = ref(false)
|
||||
const groupClasses = computed(() =>
|
||||
cn(
|
||||
'sidebar-item-group flex flex-col items-center overflow-hidden flex-shrink-0' +
|
||||
(isConnected.value ? '' : ' rounded-lg shadow-md')
|
||||
)
|
||||
)
|
||||
|
||||
const ENTER_OVERFLOW_MARGIN = 20
|
||||
const EXIT_OVERFLOW_MARGIN = 50
|
||||
|
||||
const checkOverflow = debounce(() => {
|
||||
if (!sideToolbarRef.value || !topToolbarRef.value || !bottomToolbarRef.value)
|
||||
return
|
||||
|
||||
const containerHeight = sideToolbarRef.value.clientHeight
|
||||
const topHeight = topToolbarRef.value.scrollHeight
|
||||
const bottomHeight = bottomToolbarRef.value.scrollHeight
|
||||
const contentHeight = topHeight + bottomHeight
|
||||
|
||||
if (isOverflowing.value) {
|
||||
isOverflowing.value = containerHeight < contentHeight + EXIT_OVERFLOW_MARGIN
|
||||
} else {
|
||||
isOverflowing.value =
|
||||
containerHeight < contentHeight + ENTER_OVERFLOW_MARGIN
|
||||
}
|
||||
}, 16)
|
||||
|
||||
onMounted(() => {
|
||||
if (!sideToolbarRef.value) return
|
||||
|
||||
const overflowObserver = useResizeObserver(
|
||||
sideToolbarRef.value,
|
||||
checkOverflow
|
||||
)
|
||||
|
||||
checkOverflow()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
overflowObserver.stop()
|
||||
})
|
||||
|
||||
watch(
|
||||
[isSmall, sidebarLocation],
|
||||
async () => {
|
||||
if (canvasStore.canvas) {
|
||||
if (sidebarLocation.value === 'left') {
|
||||
await nextTick()
|
||||
canvasStore.canvas.fpsInfoLocation = [
|
||||
sideToolbarRef.value?.getBoundingClientRect()?.right,
|
||||
null
|
||||
]
|
||||
} else {
|
||||
canvasStore.canvas.fpsInfoLocation = null
|
||||
}
|
||||
canvasStore.canvas.setDirty(false, true)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -88,36 +180,64 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
* but need to reference sidebar dimensions for proper positioning.
|
||||
*/
|
||||
:root {
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-padding: 8px;
|
||||
--sidebar-icon-size: 1rem;
|
||||
|
||||
--sidebar-default-floating-width: 56px;
|
||||
--sidebar-default-connected-width: calc(
|
||||
var(--sidebar-default-floating-width) + var(--sidebar-padding) * 2
|
||||
);
|
||||
--sidebar-default-item-height: 56px;
|
||||
|
||||
--sidebar-small-floating-width: 48px;
|
||||
--sidebar-small-connected-width: calc(
|
||||
var(--sidebar-small-floating-width) + var(--sidebar-padding) * 2
|
||||
);
|
||||
--sidebar-small-item-height: 48px;
|
||||
|
||||
--sidebar-width: var(--sidebar-default-floating-width);
|
||||
--sidebar-item-height: var(--sidebar-default-item-height);
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.small-sidebar) {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-width: var(--sidebar-small-floating-width);
|
||||
--sidebar-item-height: var(--sidebar-small-item-height);
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.connected-sidebar) {
|
||||
--sidebar-width: var(--sidebar-default-connected-width);
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.small-sidebar.connected-sidebar) {
|
||||
--sidebar-width: var(--sidebar-small-connected-width);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.side-tool-bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@reference "tailwindcss";
|
||||
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
color: var(--fg-color);
|
||||
box-shadow: var(--bar-shadow);
|
||||
.floating-sidebar {
|
||||
padding: var(--sidebar-padding);
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
.floating-sidebar .sidebar-item-group {
|
||||
border-color: var(--p-panel-border-color);
|
||||
}
|
||||
|
||||
.side-tool-bar-end {
|
||||
align-self: flex-end;
|
||||
margin-top: auto;
|
||||
.connected-sidebar {
|
||||
padding: var(--sidebar-padding) 0;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.sidebar-item-group {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.overflowing-sidebar :deep(.comfy-menu-button-wrapper) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
label="sideToolbar.labels.console"
|
||||
:tooltip="$t('menu.toggleBottomPanel')"
|
||||
:selected="bottomPanelStore.activePanel == 'terminal'"
|
||||
@click="bottomPanelStore.toggleBottomPanel"
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<SidebarIcon
|
||||
icon="pi pi-question-circle"
|
||||
class="comfy-help-center-btn"
|
||||
label="menu.help"
|
||||
:tooltip="$t('sideToolbar.helpCenter')"
|
||||
:icon-badge="shouldShowRedDot ? '•' : ''"
|
||||
:is-small="isSmall"
|
||||
@click="toggleHelpCenter"
|
||||
/>
|
||||
|
||||
@@ -16,7 +18,7 @@
|
||||
:class="{
|
||||
'sidebar-left': sidebarLocation === 'left',
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
'small-sidebar': isSmall
|
||||
}"
|
||||
>
|
||||
<HelpCenterMenuContent @close="closeHelpCenter" />
|
||||
@@ -29,7 +31,7 @@
|
||||
:class="{
|
||||
'sidebar-left': sidebarLocation === 'left',
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
'small-sidebar': isSmall
|
||||
}"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -40,7 +42,7 @@
|
||||
:class="{
|
||||
'sidebar-left': sidebarLocation === 'left',
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
'small-sidebar': isSmall
|
||||
}"
|
||||
@whats-new-dismissed="handleWhatsNewDismissed"
|
||||
/>
|
||||
@@ -59,7 +61,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, toRefs } from 'vue'
|
||||
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -87,6 +89,11 @@ const { showNodeConflictDialog } = useDialogService()
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const props = defineProps<{
|
||||
isSmall: boolean
|
||||
}>()
|
||||
const { isSmall } = toRefs(props)
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed((): boolean => {
|
||||
const releaseRedDot = showReleaseRedDot.value
|
||||
@@ -97,8 +104,6 @@ const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
|
||||
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
|
||||
|
||||
const toggleHelpCenter = () => {
|
||||
helpCenterStore.toggle()
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
text
|
||||
:pt="{
|
||||
root: {
|
||||
class: `side-bar-button ${
|
||||
selected
|
||||
? 'p-button-primary side-bar-button-selected'
|
||||
: 'p-button-secondary'
|
||||
class: `side-bar-button p-button-secondary ${
|
||||
selected ? 'side-bar-button-selected' : ''
|
||||
}`,
|
||||
'aria-label': computedTooltip
|
||||
}
|
||||
@@ -87,9 +85,13 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
font-size: var(--sidebar-icon-size) !important;
|
||||
}
|
||||
|
||||
.side-bar-button-selected {
|
||||
background-color: var(--content-hover-bg);
|
||||
color: var(--content-hover-fg);
|
||||
}
|
||||
|
||||
.side-bar-button-selected .side-bar-button-icon {
|
||||
font-size: var(--sidebar-icon-size) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,8 +100,9 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
|
||||
.side-bar-button {
|
||||
width: var(--sidebar-width);
|
||||
height: calc(var(--sidebar-width) + 0.5rem);
|
||||
height: var(--sidebar-item-height);
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.side-tool-bar-end .side-bar-button {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<SidebarIcon icon="pi pi-sign-out" :tooltip="tooltip" @click="logout" />
|
||||
<SidebarIcon
|
||||
icon="pi pi-sign-out"
|
||||
:tooltip="tooltip"
|
||||
label="sideToolbar.logout"
|
||||
@click="logout"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
label="shortcuts.shortcuts"
|
||||
:tooltip="tooltipText"
|
||||
:selected="isShortcutsPanelVisible"
|
||||
@click="toggleShortcutsPanel"
|
||||
|
||||
82
src/components/topbar/LoginButton.vue
Normal file
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>
|
||||
<div
|
||||
ref="positionRef"
|
||||
class="absolute left-1/2 -translate-x-1/2"
|
||||
:class="positions.positioner"
|
||||
class="absolute bottom-0 left-1/2 -translate-x-1/2"
|
||||
></div>
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
append-to="body"
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'workflow-popover-fade fit-content ' + positions.root,
|
||||
'data-popover-id': id,
|
||||
style: {
|
||||
transform: positions.active
|
||||
}
|
||||
class: 'workflow-popover-fade fit-content',
|
||||
'data-popover-id': id
|
||||
}
|
||||
}"
|
||||
@mouseenter="cancelHidePopover"
|
||||
@@ -39,9 +35,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, nextTick, ref, toRefs, useId } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { nextTick, ref, toRefs, useId } from 'vue'
|
||||
|
||||
const POPOVER_WIDTH = 250
|
||||
|
||||
@@ -53,29 +47,6 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { thumbnailUrl, isActiveTab } = toRefs(props)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const positions = computed<{
|
||||
positioner: string
|
||||
root?: string
|
||||
active?: string
|
||||
}>(() => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') === 'Topbar' &&
|
||||
settingStore.get('Comfy.UseNewMenu') === 'Bottom'
|
||||
) {
|
||||
return {
|
||||
positioner: 'top-0',
|
||||
root: 'p-popover-flipped',
|
||||
active: isActiveTab.value ? 'translateY(-100%)' : undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positioner: 'bottom-0'
|
||||
}
|
||||
})
|
||||
|
||||
const popoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
||||
const positionRef = ref<HTMLElement | null>(null)
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -174,7 +145,7 @@ defineExpose({
|
||||
.workflow-preview-content {
|
||||
@apply flex flex-col rounded-xl overflow-hidden;
|
||||
max-width: var(--popover-width);
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
@@ -184,11 +155,7 @@ defineExpose({
|
||||
|
||||
.workflow-preview-thumbnail img {
|
||||
@apply shadow-md;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--comfy-menu-secondary-bg) 70%,
|
||||
black
|
||||
);
|
||||
background-color: color-mix(in srgb, var(--comfy-menu-bg) 70%, black);
|
||||
}
|
||||
|
||||
.dark-theme .workflow-preview-thumbnail img {
|
||||
|
||||
@@ -63,10 +63,7 @@
|
||||
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
|
||||
/>
|
||||
<ContextMenu ref="menu" :model="contextMenuItems" />
|
||||
<div
|
||||
v-if="menuSetting !== 'Bottom' && isDesktop"
|
||||
class="window-actions-spacer app-drag shrink-0"
|
||||
/>
|
||||
<div v-if="isDesktop" class="window-actions-spacer app-drag shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,7 +78,6 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import {
|
||||
@@ -108,7 +104,6 @@ const { t } = useI18n()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||
const settingStore = useSettingStore()
|
||||
const workflowService = useWorkflowService()
|
||||
|
||||
const rightClickedTab = ref<WorkflowOption | undefined>()
|
||||
@@ -119,7 +114,6 @@ const leftArrowEnabled = ref(false)
|
||||
const rightArrowEnabled = ref(false)
|
||||
|
||||
const isDesktop = isElectron()
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
|
||||
const workflowToOption = (workflow: ComfyWorkflow): WorkflowOption => ({
|
||||
value: workflow.path,
|
||||
@@ -308,7 +302,7 @@ onUpdated(() => {
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-tabs-container {
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) {
|
||||
|
||||
Reference in New Issue
Block a user