mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 00:50:05 +00:00
fix layout overflow issues on bigger screens
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<BaseWidgetLayout
|
||||
:content-title="$t('templateWorkflows.title', 'Workflow Templates')"
|
||||
class="workflow-template-selector-root"
|
||||
>
|
||||
<template #leftPanel>
|
||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="navItems">
|
||||
@@ -145,7 +146,6 @@
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</BaseWidgetLayout>
|
||||
</template>
|
||||
|
||||
@@ -160,7 +160,6 @@ import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
|
||||
import BaseWidgetLayout from '@/components/widget/layout/BaseWidgetLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'
|
||||
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
|
||||
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
|
||||
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
|
||||
@@ -324,11 +323,17 @@ const sortOptions = computed(() => [
|
||||
},
|
||||
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
|
||||
{
|
||||
name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Utilization (Low to High)'),
|
||||
name: t(
|
||||
'templateWorkflows.sort.vramLowToHigh',
|
||||
'VRAM Utilization (Low to High)'
|
||||
),
|
||||
value: 'vram-low-to-high'
|
||||
},
|
||||
{
|
||||
name: t('templateWorkflows.sort.modelSizeLowToHigh', 'Model Size (Low to High)'),
|
||||
name: t(
|
||||
'templateWorkflows.sort.modelSizeLowToHigh',
|
||||
'Model Size (Low to High)'
|
||||
),
|
||||
value: 'model-size-low-to-high'
|
||||
},
|
||||
{
|
||||
@@ -375,3 +380,18 @@ onMounted(async () => {
|
||||
isLoading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Ensure the workflow template selector fits within provided dialog without horizontal overflow */
|
||||
.workflow-template-selector-root.base-widget-layout {
|
||||
width: 100% !important;
|
||||
max-width: 1400px; /* matches dialog max-width */
|
||||
height: 100% !important;
|
||||
aspect-ratio: auto !important;
|
||||
}
|
||||
@media (min-width: 1600px) {
|
||||
.workflow-template-selector-root.base-widget-layout {
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,4 +33,4 @@ const wrapperStyle = computed(() => {
|
||||
const iconColorStyle = computed(() => {
|
||||
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<Card
|
||||
ref="cardRef"
|
||||
:data-testid="`template-workflow-${template.name}`"
|
||||
class="w-64 template-card rounded-2xl overflow-hidden cursor-pointer shadow-elevation-2 dark-theme:bg-dark-elevation-1.5 h-full"
|
||||
class="w-full template-card rounded-2xl overflow-hidden cursor-pointer shadow-elevation-2 dark-theme:bg-dark-elevation-1.5 h-full"
|
||||
:pt="{
|
||||
body: { class: 'p-0 h-full flex flex-col' }
|
||||
}"
|
||||
@click="$emit('loadWorkflow', template.name)"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative overflow-hidden rounded-t-lg">
|
||||
<div class="w-full">
|
||||
<div class="relative w-full overflow-hidden rounded-t-lg">
|
||||
<template v-if="template.mediaType === 'audio'">
|
||||
<AudioThumbnail :src="baseThumbnailSrc" />
|
||||
</template>
|
||||
@@ -62,15 +62,27 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex items-center px-4 py-3">
|
||||
<div class="flex-1 flex flex-col">
|
||||
<h3 class="line-clamp-2 text-lg font-normal mb-0" :title="title">
|
||||
<div class="flex flex-col px-4 py-3 flex-1">
|
||||
<div class="flex-1">
|
||||
<h3 class="line-clamp-2 text-lg font-normal mb-1" :title="title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="line-clamp-2 text-sm text-muted grow" :title="description">
|
||||
<p class="line-clamp-2 text-sm text-muted mb-3" :title="description">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="template.tags && template.tags.length > 0"
|
||||
class="flex flex-wrap gap-1"
|
||||
>
|
||||
<span
|
||||
v-for="tag in template.tags"
|
||||
:key="tag"
|
||||
class="px-2 py-1 text-xs bg-surface-100 dark-theme:bg-surface-800 text-surface-700 dark-theme:text-surface-300 rounded-full"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<Card
|
||||
class="w-64 template-card rounded-2xl overflow-hidden shadow-elevation-2 dark-theme:bg-dark-elevation-1.5 h-full"
|
||||
class="w-full template-card rounded-2xl overflow-hidden shadow-elevation-2 dark-theme:bg-dark-elevation-1.5 h-full"
|
||||
:pt="{
|
||||
body: { class: 'p-0 h-full flex flex-col' }
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative overflow-hidden rounded-t-lg">
|
||||
<Skeleton width="16rem" height="12rem" />
|
||||
<div class="w-full">
|
||||
<div class="relative w-full aspect-square overflow-hidden rounded-t-lg">
|
||||
<Skeleton class="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -57,6 +57,7 @@ import { useLocalStorage } from '@vueuse/core'
|
||||
import DataView from 'primevue/dataview'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
|
||||
import TemplateWorkflowCardSkeleton from '@/components/templates/TemplateWorkflowCardSkeleton.vue'
|
||||
import TemplateWorkflowList from '@/components/templates/TemplateWorkflowList.vue'
|
||||
@@ -67,7 +68,7 @@ import type { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { title, sourceModule, categoryTitle, loading, templates } = defineProps<{
|
||||
const { sourceModule, categoryTitle, loading, templates } = defineProps<{
|
||||
title: string
|
||||
sourceModule: string
|
||||
categoryTitle: string
|
||||
@@ -85,8 +86,7 @@ const loadTrigger = ref<HTMLElement | null>(null)
|
||||
|
||||
const templatesRef = computed(() => templates || [])
|
||||
|
||||
const { searchQuery, filteredTemplates, filteredCount } =
|
||||
useTemplateFiltering(templatesRef)
|
||||
const { searchQuery, filteredTemplates } = useTemplateFiltering(templatesRef)
|
||||
|
||||
// When searching, show all results immediately without pagination
|
||||
// When not searching, use lazy pagination
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col h-[83vh] w-[90vw] relative pb-6"
|
||||
class="flex flex-col h-full w-full relative pb-6 overflow-x-hidden"
|
||||
data-testid="template-workflows-content"
|
||||
>
|
||||
<Button
|
||||
@@ -38,7 +38,7 @@
|
||||
>
|
||||
<TemplateWorkflowView
|
||||
v-if="isReady && selectedTemplate"
|
||||
class="px-12 py-4"
|
||||
class="h-full"
|
||||
:title="selectedTemplate.title"
|
||||
:source-module="selectedTemplate.moduleName"
|
||||
:templates="selectedTemplate.templates"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="relative w-64 h-64 rounded-t-lg overflow-hidden select-none">
|
||||
<div
|
||||
class="relative w-full aspect-square rounded-t-lg overflow-hidden select-none"
|
||||
>
|
||||
<div
|
||||
v-if="!error"
|
||||
ref="contentRef"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-50 dark-theme:bg-zinc-800"
|
||||
:class="{ 'fit-parent': fitParent }"
|
||||
>
|
||||
<IconButton
|
||||
v-show="!isRightPanelOpen && hasRightPanel"
|
||||
@@ -32,8 +33,10 @@
|
||||
</nav>
|
||||
</Transition>
|
||||
|
||||
<div class="flex-1 flex bg-zinc-100 dark-theme:bg-neutral-900">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div
|
||||
class="flex-1 flex bg-zinc-100 dark-theme:bg-neutral-900 min-w-0 overflow-hidden"
|
||||
>
|
||||
<div class="w-full h-full flex flex-col min-w-0">
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="w-full h-16 px-6 py-4 flex justify-between gap-2"
|
||||
@@ -61,13 +64,15 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex flex-col flex-1 min-h-0">
|
||||
<main class="flex flex-col flex-1 min-h-0 min-w-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">
|
||||
{{ contentTitle }}
|
||||
</h2>
|
||||
<div class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto scrollbar-hide">
|
||||
<div
|
||||
class="min-h-0 px-3 pt-0 pb-6 overflow-x-hidden overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</main>
|
||||
@@ -90,8 +95,10 @@ import { computed, inject, ref, useSlots, watch } from 'vue'
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
|
||||
const { contentTitle } = defineProps<{
|
||||
const { contentTitle, fitParent } = defineProps<{
|
||||
contentTitle: string
|
||||
/** When true, the layout will size itself to its parent instead of using viewport-based widths. */
|
||||
fitParent?: boolean
|
||||
}>()
|
||||
|
||||
const BREAKPOINTS = { md: 880 }
|
||||
@@ -146,12 +153,32 @@ const toggleRightPanel = () => {
|
||||
aspect-ratio: 20/13;
|
||||
}
|
||||
|
||||
/* Modifier class to force the widget to respect parent dialog/container sizing (prevents overflow) */
|
||||
.base-widget-layout.fit-parent {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 1450px) {
|
||||
.base-widget-layout {
|
||||
max-width: 1724px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.base-widget-layout {
|
||||
max-width: 1920px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2400px) {
|
||||
.base-widget-layout {
|
||||
max-width: 2200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade transition for buttons */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
|
||||
@@ -18,24 +18,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
// Import only the icons used in getCategoryIcon
|
||||
import ILucideList from '~icons/lucide/list'
|
||||
import ILucideGraduationCap from '~icons/lucide/graduation-cap'
|
||||
import ILucideImage from '~icons/lucide/image'
|
||||
import ILucideFilm from '~icons/lucide/film'
|
||||
import ILucideBox from '~icons/lucide/box'
|
||||
import ILucideVolume2 from '~icons/lucide/volume-2'
|
||||
import ILucideHandCoins from '~icons/lucide/hand-coins'
|
||||
import ILucideMessageSquareText from '~icons/lucide/message-square-text'
|
||||
import ILucideZap from '~icons/lucide/zap'
|
||||
import ILucideCommand from '~icons/lucide/command'
|
||||
import ILucideDumbbell from '~icons/lucide/dumbbell'
|
||||
import ILucidePuzzle from '~icons/lucide/puzzle'
|
||||
import ILucideWrench from '~icons/lucide/wrench'
|
||||
import ILucideMaximize2 from '~icons/lucide/maximize-2'
|
||||
import ILucideSlidersHorizontal from '~icons/lucide/sliders-horizontal'
|
||||
import ILucideLayoutGrid from '~icons/lucide/layout-grid'
|
||||
import ILucideFilm from '~icons/lucide/film'
|
||||
import ILucideFolder from '~icons/lucide/folder'
|
||||
import ILucideGraduationCap from '~icons/lucide/graduation-cap'
|
||||
import ILucideHandCoins from '~icons/lucide/hand-coins'
|
||||
import ILucideImage from '~icons/lucide/image'
|
||||
import ILucideLayoutGrid from '~icons/lucide/layout-grid'
|
||||
// Import only the icons used in getCategoryIcon
|
||||
import ILucideList from '~icons/lucide/list'
|
||||
import ILucideMaximize2 from '~icons/lucide/maximize-2'
|
||||
import ILucideMessageSquareText from '~icons/lucide/message-square-text'
|
||||
import ILucidePuzzle from '~icons/lucide/puzzle'
|
||||
import ILucideSlidersHorizontal from '~icons/lucide/sliders-horizontal'
|
||||
import ILucideVolume2 from '~icons/lucide/volume-2'
|
||||
import ILucideWrench from '~icons/lucide/wrench'
|
||||
import ILucideZap from '~icons/lucide/zap'
|
||||
|
||||
const { icon, active, onClick } = defineProps<{
|
||||
icon?: string
|
||||
@@ -46,37 +46,37 @@ const { icon, active, onClick } = defineProps<{
|
||||
// Icon map matching getCategoryIcon function exactly
|
||||
const iconMap = {
|
||||
// Main categories
|
||||
'list': ILucideList,
|
||||
list: ILucideList,
|
||||
'graduation-cap': ILucideGraduationCap,
|
||||
|
||||
|
||||
// Generation types
|
||||
'image': ILucideImage,
|
||||
'film': ILucideFilm,
|
||||
'box': ILucideBox,
|
||||
image: ILucideImage,
|
||||
film: ILucideFilm,
|
||||
box: ILucideBox,
|
||||
'volume-2': ILucideVolume2,
|
||||
|
||||
|
||||
// API and models
|
||||
'hand-coins': ILucideHandCoins,
|
||||
|
||||
|
||||
// LLMs and AI
|
||||
'message-square-text': ILucideMessageSquareText,
|
||||
|
||||
|
||||
// Performance and hardware
|
||||
'zap': ILucideZap,
|
||||
'command': ILucideCommand,
|
||||
|
||||
zap: ILucideZap,
|
||||
command: ILucideCommand,
|
||||
|
||||
// Training
|
||||
'dumbbell': ILucideDumbbell,
|
||||
|
||||
dumbbell: ILucideDumbbell,
|
||||
|
||||
// Extensions and tools
|
||||
'puzzle': ILucidePuzzle,
|
||||
'wrench': ILucideWrench,
|
||||
|
||||
puzzle: ILucidePuzzle,
|
||||
wrench: ILucideWrench,
|
||||
|
||||
// Fallbacks for common patterns
|
||||
'maximize-2': ILucideMaximize2,
|
||||
'sliders-horizontal': ILucideSlidersHorizontal,
|
||||
'layout-grid': ILucideLayoutGrid,
|
||||
'folder': ILucideFolder
|
||||
folder: ILucideFolder
|
||||
}
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
|
||||
@@ -8,7 +8,12 @@ export interface TemplateFilterOptions {
|
||||
selectedModels?: string[]
|
||||
selectedUseCases?: string[] // Now represents selected tags
|
||||
selectedLicenses?: string[]
|
||||
sortBy?: 'default' | 'alphabetical' | 'newest' | 'vram-low-to-high' | 'model-size-low-to-high'
|
||||
sortBy?:
|
||||
| 'default'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
| 'model-size-low-to-high'
|
||||
}
|
||||
|
||||
export function useTemplateFiltering(
|
||||
@@ -18,7 +23,13 @@ export function useTemplateFiltering(
|
||||
const selectedModels = ref<string[]>([])
|
||||
const selectedUseCases = ref<string[]>([])
|
||||
const selectedLicenses = ref<string[]>([])
|
||||
const sortBy = ref<'default' | 'alphabetical' | 'newest' | 'vram-low-to-high' | 'model-size-low-to-high'>('default')
|
||||
const sortBy = ref<
|
||||
| 'default'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
| 'model-size-low-to-high'
|
||||
>('default')
|
||||
|
||||
const templatesArray = computed(() => {
|
||||
const templateData = 'value' in templates ? templates.value : templates
|
||||
|
||||
@@ -2,7 +2,7 @@ import WorkflowTemplateSelector from '@/components/custom/widget/WorkflowTemplat
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const DIALOG_KEY = 'global-workflow-template-selector';
|
||||
const DIALOG_KEY = 'global-workflow-template-selector'
|
||||
|
||||
export const useWorkflowTemplateSelectorDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
|
||||
@@ -118,7 +118,8 @@ export const useDialogService = () => {
|
||||
headerComponent: TemplateWorkflowsDialogHeader,
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
content: { class: '!px-0 overflow-y-hidden' }
|
||||
root: { style: 'width: 90vw; height: 85vh; max-width: 1600px;' },
|
||||
content: { class: '!px-0 overflow-x-hidden overflow-y-hidden' }
|
||||
}
|
||||
},
|
||||
props
|
||||
@@ -131,14 +132,18 @@ export const useDialogService = () => {
|
||||
modal: true,
|
||||
closable: false,
|
||||
pt: {
|
||||
content: { class: '!px-0' },
|
||||
root: { style: 'width: 90vw; max-width: 1400px; height: 85vh;' }
|
||||
content: { class: '!px-0 overflow-hidden' },
|
||||
// Let internal layout manage its own max-width; prevent child from exceeding and causing scrollWidth > clientWidth
|
||||
root: {
|
||||
style: 'width: 90vw; height: 85vh; max-width: 1400px; display: flex;'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showLayoutDialog({
|
||||
key: 'global-workflow-template-selector',
|
||||
component: WorkflowTemplateSelector,
|
||||
// Pass through sizing hint so inner layout adapts to parent rather than viewport
|
||||
props: {
|
||||
onClose: () =>
|
||||
dialogStore.closeDialog({ key: 'global-workflow-template-selector' })
|
||||
|
||||
Reference in New Issue
Block a user