mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Add bottom panel support (#1294)
* Add bottom panel * Bottom panel store * Extract ExtensionSlot component * Tab rendering * Add toggle button on top menu bar * nit * Add toggle button tooltip * Add command
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<Splitter class="splitter-overlay" :pt:gutter="gutterClass">
|
||||
<Splitter
|
||||
class="splitter-overlay-root splitter-overlay"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
>
|
||||
<SplitterPanel
|
||||
class="side-bar-panel"
|
||||
:minSize="10"
|
||||
@@ -9,9 +12,22 @@
|
||||
>
|
||||
<slot name="side-bar-panel"></slot>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel class="graph-canvas-panel relative" :size="100">
|
||||
<slot name="graph-canvas-panel"></slot>
|
||||
|
||||
<SplitterPanel :size="100">
|
||||
<Splitter
|
||||
class="splitter-overlay"
|
||||
layout="vertical"
|
||||
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel"></slot>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel class="bottom-panel" v-show="bottomPanelVisible">
|
||||
<slot name="bottom-panel"></slot>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
class="side-bar-panel"
|
||||
:minSize="10"
|
||||
@@ -26,7 +42,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed } from 'vue'
|
||||
@@ -37,42 +54,39 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
)
|
||||
|
||||
const sidebarPanelVisible = computed(
|
||||
() => useWorkspaceStore().sidebarTab.activeSidebarTab !== null
|
||||
() => useSidebarTabStore().activeSidebarTab !== null
|
||||
)
|
||||
const bottomPanelVisible = computed(
|
||||
() => useBottomPanelStore().bottomPanelVisible
|
||||
)
|
||||
const gutterClass = computed(() => {
|
||||
return sidebarPanelVisible.value ? '' : 'gutter-hidden'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.p-splitter-gutter {
|
||||
<style scoped>
|
||||
:deep(.p-splitter-gutter) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.gutter-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.side-bar-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.splitter-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
@apply bg-transparent pointer-events-none border-none;
|
||||
}
|
||||
|
||||
.splitter-overlay-root {
|
||||
@apply w-full h-full absolute top-0 left-0;
|
||||
|
||||
/* Set it the same as the ComfyUI menu */
|
||||
/* Note: Lite-graph DOM widgets have the same z-index as the node id, so
|
||||
999 should be sufficient to make sure splitter overlays on node's DOM
|
||||
widgets */
|
||||
z-index: 999;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
43
src/components/bottomPanel/BottomPanel.vue
Normal file
43
src/components/bottomPanel/BottomPanel.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
|
||||
<TabList pt:tabList="border-none">
|
||||
<div class="w-full flex justify-between">
|
||||
<div class="tabs-container">
|
||||
<Tab
|
||||
v-for="tab in bottomPanelStore.bottomPanelTabs"
|
||||
:key="tab.id"
|
||||
:value="tab.id"
|
||||
class="p-3 border-none"
|
||||
>
|
||||
<span class="font-bold">
|
||||
{{ tab.title.toUpperCase() }}
|
||||
</span>
|
||||
</Tab>
|
||||
</div>
|
||||
<Button
|
||||
class="justify-self-end"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
text
|
||||
@click="bottomPanelStore.bottomPanelVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<ExtensionSlot
|
||||
v-if="bottomPanelStore.activeBottomPanelTab"
|
||||
:extension="bottomPanelStore.activeBottomPanelTab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tab from 'primevue/tab'
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
</script>
|
||||
34
src/components/common/ExtensionSlot.vue
Normal file
34
src/components/common/ExtensionSlot.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<component v-if="extension.type === 'vue'" :is="extension.component" />
|
||||
<div
|
||||
v-else
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el)
|
||||
mountCustomExtension(
|
||||
props.extension as CustomExtension,
|
||||
el as HTMLElement
|
||||
)
|
||||
}
|
||||
"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CustomExtension, VueExtension } from '@/types/extensionTypes'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
extension: VueExtension | CustomExtension
|
||||
}>()
|
||||
|
||||
const mountCustomExtension = (extension: CustomExtension, el: HTMLElement) => {
|
||||
extension.render(el)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (props.extension.type === 'custom' && props.extension.destroy) {
|
||||
props.extension.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -4,6 +4,9 @@
|
||||
<template #side-bar-panel>
|
||||
<SideToolbar />
|
||||
</template>
|
||||
<template #bottom-panel>
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" />
|
||||
</template>
|
||||
@@ -19,6 +22,7 @@
|
||||
<script setup lang="ts">
|
||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
|
||||
@@ -21,19 +21,7 @@
|
||||
v-if="selectedTab"
|
||||
class="sidebar-content-container h-full overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<component v-if="selectedTab.type === 'vue'" :is="selectedTab.component" />
|
||||
<div
|
||||
v-else
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el)
|
||||
mountCustomTab(
|
||||
selectedTab as CustomSidebarTabExtension,
|
||||
el as HTMLElement
|
||||
)
|
||||
}
|
||||
"
|
||||
></div>
|
||||
<ExtensionSlot :extension="selectedTab" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,13 +29,11 @@
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
import SidebarThemeToggleIcon from './SidebarThemeToggleIcon.vue'
|
||||
import SidebarSettingsToggleIcon from './SidebarSettingsToggleIcon.vue'
|
||||
import { computed, onBeforeUnmount } from 'vue'
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import {
|
||||
CustomSidebarTabExtension,
|
||||
SidebarTabExtension
|
||||
} from '@/types/extensionTypes'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -65,20 +51,9 @@ const isSmall = computed(
|
||||
|
||||
const tabs = computed(() => workspaceStore.getSidebarTabs())
|
||||
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
const mountCustomTab = (tab: CustomSidebarTabExtension, el: HTMLElement) => {
|
||||
tab.render(el)
|
||||
}
|
||||
const onTabClick = (item: SidebarTabExtension) => {
|
||||
workspaceStore.sidebarTab.toggleSidebarTab(item.id)
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
tabs.value.forEach((tab) => {
|
||||
if (tab.type === 'custom' && tab.destroy) {
|
||||
tab.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
const keybinding = keybindingStore.getKeybindingByCommandId(
|
||||
|
||||
23
src/components/topbar/BottomPanelToggleButton.vue
Normal file
23
src/components/topbar/BottomPanelToggleButton.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="bottomPanelStore.bottomPanelTabs.length > 0"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="bottomPanelStore.toggleBottomPanel"
|
||||
v-tooltip="{ value: $t('menu.toggleBottomPanel'), showDelay: 300 }"
|
||||
>
|
||||
<template #icon>
|
||||
<i-material-symbols:dock-to-bottom
|
||||
v-if="bottomPanelStore.bottomPanelVisible"
|
||||
/>
|
||||
<i-material-symbols:dock-to-bottom-outline v-else />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
</script>
|
||||
@@ -14,6 +14,7 @@
|
||||
</div>
|
||||
<div class="comfyui-menu-right" ref="menuRight"></div>
|
||||
<Actionbar />
|
||||
<BottomPanelToggleButton />
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
@@ -23,6 +24,7 @@ import Divider from 'primevue/divider'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
|
||||
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import BottomPanelToggleButton from '@/components/topbar/BottomPanelToggleButton.vue'
|
||||
import { computed, onMounted, provide, ref } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
@@ -91,7 +91,8 @@ const messages = {
|
||||
refresh: 'Refresh node definitions',
|
||||
clipspace: 'Open Clipspace',
|
||||
resetView: 'Reset canvas view',
|
||||
clear: 'Clear workflow'
|
||||
clear: 'Clear workflow',
|
||||
toggleBottomPanel: 'Toggle Bottom Panel'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: 'Get Started with a Template',
|
||||
@@ -200,7 +201,8 @@ const messages = {
|
||||
refresh: '刷新节点',
|
||||
clipspace: '打开剪贴板',
|
||||
resetView: '重置画布视图',
|
||||
clear: '清空工作流'
|
||||
clear: '清空工作流',
|
||||
toggleBottomPanel: '底部面板'
|
||||
},
|
||||
templateWorkflows: {
|
||||
title: '从模板开始',
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useTitleEditorStore } from './graphStore'
|
||||
import { useErrorHandling } from '@/hooks/errorHooks'
|
||||
import { useWorkflowStore } from './workflowStore'
|
||||
import { type KeybindingImpl, useKeybindingStore } from './keybindingStore'
|
||||
import { useBottomPanelStore } from './workspace/bottomPanelStore'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
export interface ComfyCommand {
|
||||
@@ -458,6 +459,15 @@ export const useCommandStore = defineStore('command', () => {
|
||||
}
|
||||
}
|
||||
})()
|
||||
},
|
||||
{
|
||||
id: 'Workspace.ToggleBottomPanel',
|
||||
icon: 'pi pi-list',
|
||||
label: 'Toggle Bottom Panel',
|
||||
versionAdded: '1.3.22',
|
||||
function: () => {
|
||||
useBottomPanelStore().toggleBottomPanel()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
59
src/stores/workspace/bottomPanelStore.ts
Normal file
59
src/stores/workspace/bottomPanelStore.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
||||
const bottomPanelVisible = ref(false)
|
||||
const toggleBottomPanel = () => {
|
||||
// If there are no tabs, don't show the bottom panel
|
||||
if (bottomPanelTabs.value.length === 0) {
|
||||
return
|
||||
}
|
||||
bottomPanelVisible.value = !bottomPanelVisible.value
|
||||
}
|
||||
|
||||
const bottomPanelTabs = ref<BottomPanelExtension[]>([])
|
||||
const activeBottomPanelTabId = ref<string | null>(null)
|
||||
const activeBottomPanelTab = computed<BottomPanelExtension | null>(() => {
|
||||
return (
|
||||
bottomPanelTabs.value.find(
|
||||
(tab) => tab.id === activeBottomPanelTabId.value
|
||||
) ?? null
|
||||
)
|
||||
})
|
||||
const setActiveTab = (tabId: string) => {
|
||||
activeBottomPanelTabId.value = tabId
|
||||
}
|
||||
const toggleBottomPanelTab = (tabId: string) => {
|
||||
if (activeBottomPanelTabId.value === tabId) {
|
||||
bottomPanelVisible.value = false
|
||||
} else {
|
||||
activeBottomPanelTabId.value = tabId
|
||||
bottomPanelVisible.value = true
|
||||
}
|
||||
}
|
||||
const registerBottomPanelTab = (tab: BottomPanelExtension) => {
|
||||
bottomPanelTabs.value = [...bottomPanelTabs.value, tab]
|
||||
if (bottomPanelTabs.value.length === 1) {
|
||||
activeBottomPanelTabId.value = tab.id
|
||||
}
|
||||
useCommandStore().registerCommand({
|
||||
id: `Workspace.ToggleBottomPanelTab.${tab.id}`,
|
||||
icon: 'pi pi-list',
|
||||
label: tab.title,
|
||||
function: () => toggleBottomPanelTab(tab.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
bottomPanelVisible,
|
||||
toggleBottomPanel,
|
||||
bottomPanelTabs,
|
||||
activeBottomPanelTab,
|
||||
activeBottomPanelTabId,
|
||||
setActiveTab,
|
||||
toggleBottomPanelTab,
|
||||
registerBottomPanelTab
|
||||
}
|
||||
})
|
||||
@@ -6,25 +6,41 @@ export interface BaseSidebarTabExtension {
|
||||
title: string
|
||||
icon?: string
|
||||
iconBadge?: string | (() => string | null)
|
||||
order?: number
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export interface VueSidebarTabExtension extends BaseSidebarTabExtension {
|
||||
export interface BaseBottomPanelExtension {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface VueExtension {
|
||||
id: string
|
||||
type: 'vue'
|
||||
component: Component
|
||||
}
|
||||
|
||||
export interface CustomSidebarTabExtension extends BaseSidebarTabExtension {
|
||||
export interface CustomExtension {
|
||||
id: string
|
||||
type: 'custom'
|
||||
render: (container: HTMLElement) => void
|
||||
destroy?: () => void
|
||||
}
|
||||
|
||||
export type VueSidebarTabExtension = BaseSidebarTabExtension & VueExtension
|
||||
export type CustomSidebarTabExtension = BaseSidebarTabExtension &
|
||||
CustomExtension
|
||||
export type SidebarTabExtension =
|
||||
| VueSidebarTabExtension
|
||||
| CustomSidebarTabExtension
|
||||
|
||||
export type VueBottomPanelExtension = BaseBottomPanelExtension & VueExtension
|
||||
export type CustomBottomPanelExtension = BaseBottomPanelExtension &
|
||||
CustomExtension
|
||||
export type BottomPanelExtension =
|
||||
| VueBottomPanelExtension
|
||||
| CustomBottomPanelExtension
|
||||
|
||||
export type ToastManager = {
|
||||
add(message: ToastMessageOptions): void
|
||||
remove(message: ToastMessageOptions): void
|
||||
|
||||
Reference in New Issue
Block a user