mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-24 00:34:09 +00:00
## Summary Merges latest changes from `main` as of 10-06-2025. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5965-Merge-main-as-of-10-06-2025-into-rh-test-2856d73d3650812cb95fd8917278a770) by [Unito](https://www.unito.io) --------- Signed-off-by: Marcel Petrick <mail@marcelpetrick.it> Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: snomiao <snomiao@gmail.com> Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com> Co-authored-by: Jake Schroeder <jake.schroeder@isophex.com> Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com> Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Co-authored-by: Marcel Petrick <mail@marcelpetrick.it> Co-authored-by: Alexander Brown <DrJKL0424@gmail.com> Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com> Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe> Co-authored-by: JakeSchroeder <jake@axiom.co> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: DrJKL <DrJKL@users.noreply.github.com> Co-authored-by: ComfyUI Wiki <contact@comfyui-wiki.com>
211 lines
5.3 KiB
Vue
211 lines
5.3 KiB
Vue
<template>
|
|
<div :class="layoutClasses">
|
|
<IconButton
|
|
v-show="!isRightPanelOpen && hasRightPanel"
|
|
:class="rightPanelButtonClasses"
|
|
@click="toggleRightPanel"
|
|
>
|
|
<i-lucide:panel-right class="text-sm" />
|
|
</IconButton>
|
|
<IconButton :class="closeButtonClasses" @click="closeDialog">
|
|
<i class="pi pi-times text-sm"></i>
|
|
</IconButton>
|
|
<div class="flex w-full h-full">
|
|
<Transition name="slide-panel">
|
|
<nav
|
|
v-if="$slots.leftPanel && showLeftPanel"
|
|
:class="[
|
|
PANEL_SIZES.width,
|
|
PANEL_SIZES.minWidth,
|
|
PANEL_SIZES.maxWidth
|
|
]"
|
|
>
|
|
<slot name="leftPanel"></slot>
|
|
</nav>
|
|
</Transition>
|
|
|
|
<div :class="mainContainerClasses">
|
|
<div class="w-full h-full flex flex-col">
|
|
<header v-if="$slots.header" :class="headerClasses">
|
|
<div class="flex-1 flex gap-2 shrink-0">
|
|
<IconButton v-if="!notMobile" @click="toggleLeftPanel">
|
|
<i-lucide:panel-left v-if="!showLeftPanel" class="text-sm" />
|
|
<i-lucide:panel-left-close v-else class="text-sm" />
|
|
</IconButton>
|
|
<slot name="header"></slot>
|
|
</div>
|
|
<slot name="header-right-area"></slot>
|
|
<div :class="rightAreaClasses">
|
|
<IconButton
|
|
v-if="isRightPanelOpen && hasRightPanel"
|
|
@click="toggleRightPanel"
|
|
>
|
|
<i-lucide:panel-right-close class="text-sm" />
|
|
</IconButton>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="flex flex-col flex-1 min-h-0">
|
|
<!-- Fallback title bar when no leftPanel is provided -->
|
|
<slot name="contentFilter"></slot>
|
|
<h2
|
|
v-if="!$slots.leftPanel"
|
|
class="text-xxl px-6 pt-2 pb-6 m-0 capitalize"
|
|
>
|
|
{{ contentTitle }}
|
|
</h2>
|
|
<div :class="contentContainerClasses">
|
|
<slot name="content"></slot>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<aside
|
|
v-if="hasRightPanel && isRightPanelOpen"
|
|
:class="rightPanelClasses"
|
|
>
|
|
<slot name="rightPanel"></slot>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useBreakpoints } from '@vueuse/core'
|
|
import { computed, inject, ref, useSlots, watch } from 'vue'
|
|
|
|
import IconButton from '@/components/button/IconButton.vue'
|
|
import { OnCloseKey } from '@/types/widgetTypes'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
const { contentTitle } = defineProps<{
|
|
contentTitle: string
|
|
}>()
|
|
|
|
const BREAKPOINTS = { md: 880 }
|
|
const PANEL_SIZES = {
|
|
width: 'w-1/3',
|
|
minWidth: 'min-w-40',
|
|
maxWidth: 'max-w-56'
|
|
}
|
|
|
|
const slots = useSlots()
|
|
const closeDialog = inject(OnCloseKey, () => {})
|
|
|
|
const breakpoints = useBreakpoints(BREAKPOINTS)
|
|
const notMobile = breakpoints.greater('md')
|
|
|
|
const isLeftPanelOpen = ref<boolean>(true)
|
|
const isRightPanelOpen = ref<boolean>(false)
|
|
const mobileMenuOpen = ref<boolean>(false)
|
|
|
|
const hasRightPanel = computed(() => !!slots.rightPanel)
|
|
|
|
watch(notMobile, (isDesktop) => {
|
|
if (!isDesktop) {
|
|
mobileMenuOpen.value = false
|
|
}
|
|
})
|
|
|
|
const showLeftPanel = computed(() => {
|
|
const shouldShow = notMobile.value
|
|
? isLeftPanelOpen.value
|
|
: mobileMenuOpen.value
|
|
return shouldShow
|
|
})
|
|
|
|
const toggleLeftPanel = () => {
|
|
if (notMobile.value) {
|
|
isLeftPanelOpen.value = !isLeftPanelOpen.value
|
|
} else {
|
|
mobileMenuOpen.value = !mobileMenuOpen.value
|
|
}
|
|
}
|
|
|
|
const toggleRightPanel = () => {
|
|
isRightPanelOpen.value = !isRightPanelOpen.value
|
|
}
|
|
|
|
// Computed classes for better readability
|
|
const layoutClasses = cn(
|
|
'base-widget-layout',
|
|
'rounded-2xl overflow-hidden relative',
|
|
'bg-gray-50 dark-theme:bg-gray-800'
|
|
)
|
|
|
|
const rightPanelButtonClasses = computed(() => {
|
|
return cn('absolute top-4 right-18 z-10', 'transition-opacity duration-200', {
|
|
'opacity-0 pointer-events-none':
|
|
isRightPanelOpen.value || !hasRightPanel.value
|
|
})
|
|
})
|
|
|
|
const closeButtonClasses = cn(
|
|
'absolute top-4 right-6 z-10',
|
|
'transition-opacity duration-200'
|
|
)
|
|
|
|
const mainContainerClasses = cn(
|
|
'flex-1 flex',
|
|
'bg-gray-100 dark-theme:bg-neutral-900'
|
|
)
|
|
|
|
const headerClasses = cn(
|
|
'w-full h-18 px-6',
|
|
'flex items-center justify-between gap-2'
|
|
)
|
|
|
|
const rightAreaClasses = computed(() => {
|
|
return cn(
|
|
'flex justify-end gap-2 w-0',
|
|
hasRightPanel.value && !isRightPanelOpen.value ? 'min-w-22' : 'min-w-10'
|
|
)
|
|
})
|
|
|
|
const contentContainerClasses = computed(() => {
|
|
return cn('min-h-0 px-6 pt-0 pb-10', 'overflow-y-auto scrollbar-hide')
|
|
})
|
|
|
|
const rightPanelClasses = computed(() => {
|
|
return cn('w-1/4 min-w-40 max-w-80')
|
|
})
|
|
</script>
|
|
<style scoped>
|
|
.base-widget-layout {
|
|
height: 80vh;
|
|
width: 90vw;
|
|
max-width: 1280px;
|
|
aspect-ratio: 20/13;
|
|
}
|
|
|
|
@media (min-width: 1450px) {
|
|
.base-widget-layout {
|
|
max-width: 1724px;
|
|
}
|
|
}
|
|
|
|
/* Fade transition for buttons */
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.fade-enter-from,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* Slide transition for left panel */
|
|
.slide-panel-enter-active,
|
|
.slide-panel-leave-active {
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
will-change: transform;
|
|
backface-visibility: hidden;
|
|
}
|
|
|
|
.slide-panel-enter-from,
|
|
.slide-panel-leave-to {
|
|
transform: translateX(-100%);
|
|
}
|
|
</style>
|