fix layout overflow issues on bigger screens

This commit is contained in:
Johnpaul
2025-08-28 01:00:00 +01:00
parent f6dc62ab8f
commit 5d88200aae
12 changed files with 141 additions and 64 deletions

View File

@@ -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>

View File

@@ -33,4 +33,4 @@ const wrapperStyle = computed(() => {
const iconColorStyle = computed(() => {
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
})
</script>
</script>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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(() => {

View File

@@ -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

View File

@@ -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()

View File

@@ -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' })