refactor: improve modal layout and add class prop to PropertiesAccordionItem

This commit is contained in:
Alexander Brown
2026-01-15 18:05:45 -08:00
parent e3f74b2df4
commit 5de166abdf
5 changed files with 149 additions and 149 deletions

View File

@@ -5,25 +5,32 @@ import { cn } from '@/utils/tailwindUtil'
import TransitionCollapse from './TransitionCollapse.vue'
const props = defineProps<{
const {
disabled,
label,
enableEmptyState,
tooltip,
class: className
} = defineProps<{
disabled?: boolean
label?: string
enableEmptyState?: boolean
tooltip?: string
class?: string
}>()
const isCollapse = defineModel<boolean>('collapse', { default: false })
const isExpanded = computed(() => !isCollapse.value && !props.disabled)
const isExpanded = computed(() => !isCollapse.value && !disabled)
const tooltipConfig = computed(() => {
if (!props.tooltip) return undefined
return { value: props.tooltip, showDelay: 1000 }
if (!tooltip) return undefined
return { value: tooltip, showDelay: 1000 }
})
</script>
<template>
<div class="flex flex-col bg-comfy-menu-bg">
<div :class="cn('flex flex-col bg-comfy-menu-bg', className)">
<div
class="sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl bg-inherit"
>

View File

@@ -1,25 +1,5 @@
<template>
<div class="base-widget-layout rounded-2xl overflow-hidden relative">
<Button
v-show="!isRightPanelOpen && showRightPanelButton"
size="icon"
:class="
cn('absolute top-4 right-18 z-10', 'transition-opacity duration-200', {
'opacity-0 pointer-events-none':
isRightPanelOpen || !showRightPanelButton
})
"
@click="toggleRightPanel"
>
<i class="icon-[lucide--panel-right] text-sm" />
</Button>
<Button
size="lg"
class="absolute top-4 right-6 z-10 transition-opacity duration-200 w-10"
@click="closeDialog"
>
<i class="pi pi-times" />
</Button>
<div class="flex h-full w-full">
<Transition name="slide-panel">
<nav
@@ -55,24 +35,18 @@
<slot name="header"></slot>
</div>
<slot name="header-right-area"></slot>
<div
:class="
cn(
'flex justify-end gap-2 w-0',
showRightPanelButton && !isRightPanelOpen
? 'min-w-22'
: 'min-w-10'
)
"
>
<template v-if="!isRightPanelOpen">
<Button
v-if="isRightPanelOpen && showRightPanelButton"
v-if="showRightPanelButton"
size="icon"
@click="toggleRightPanel"
>
<i class="icon-[lucide--panel-right-close]" />
<i class="icon-[lucide--panel-right] text-sm" />
</Button>
</div>
<Button size="lg" class="w-10" @click="closeDialog">
<i class="pi pi-times" />
</Button>
</template>
</header>
<main class="flex min-h-0 flex-1 flex-col">
@@ -93,9 +67,33 @@
</div>
<aside
v-if="hasRightPanel && isRightPanelOpen"
class="w-1/4 min-w-40 max-w-80"
class="flex w-72 shrink-0 bg-modal-panel-background flex-col border-l border-border-default"
>
<slot name="rightPanel" />
<header
data-component-id="RightPanelHeader"
class="flex h-16 shrink-0 items-center gap-2 border-b border-border-default px-4"
>
<h2 v-if="rightPanelTitle" class="flex-1 text-lg font-semibold">
{{ rightPanelTitle }}
</h2>
<div v-else class="flex-1">
<slot name="rightPanelHeaderTitle" />
</div>
<slot name="rightPanelHeaderActions" />
<Button
v-if="showRightPanelButton"
size="icon"
@click="toggleRightPanel"
>
<i class="icon-[lucide--panel-right-close] text-sm" />
</Button>
<Button size="icon" @click="closeDialog">
<i class="pi pi-times" />
</Button>
</header>
<div class="min-h-0 flex-1 overflow-y-auto">
<slot name="rightPanel" />
</div>
</aside>
</div>
</div>
@@ -110,8 +108,9 @@ import Button from '@/components/ui/button/Button.vue'
import { OnCloseKey } from '@/types/widgetTypes'
import { cn } from '@/utils/tailwindUtil'
const { contentTitle } = defineProps<{
const { contentTitle, rightPanelTitle } = defineProps<{
contentTitle: string
rightPanelTitle?: string
}>()
const isRightPanelOpen = defineModel<boolean>('rightPanelOpen', {

View File

@@ -5,6 +5,7 @@
data-component-id="AssetBrowserModal"
class="size-full max-h-full max-w-full min-w-0"
:content-title="displayTitle"
:right-panel-title="$t('assetBrowser.modelInfo.title')"
@close="handleClose"
>
<template v-if="shouldShowLeftPanel" #leftPanel>

View File

@@ -115,7 +115,7 @@
<script setup lang="ts">
import { onClickOutside, useImage } from '@vueuse/core'
import { computed, ref, toValue, useId, useTemplateRef, watch } from 'vue'
import { computed, ref, toValue, useId, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import IconGroup from '@/components/button/IconGroup.vue'
@@ -160,21 +160,19 @@ const dropdownMenuButton = useTemplateRef<InstanceType<typeof MoreButton>>(
'dropdown-menu-button'
)
const stopClickOutside = ref<(() => void) | null>(null)
watch(
() => focused,
(isFocused) => {
stopClickOutside.value?.()
stopClickOutside.value = null
if (isFocused && cardRef.value) {
stopClickOutside.value = onClickOutside(cardRef, () => {
emit('blur')
})
onClickOutside(
cardRef,
() => {
if (focused) {
emit('blur')
}
},
{ immediate: true }
{
ignore: [
'[data-component-id="ModelInfoPanel"]',
'[data-component-id="RightPanelHeader"]'
]
}
)
const titleId = useId()

View File

@@ -1,103 +1,98 @@
<template>
<div class="flex h-full flex-col bg-comfy-menu-bg">
<div class="flex h-18 items-center border-b border-divider px-4">
<h2 class="text-lg font-semibold">
{{ $t('assetBrowser.modelInfo.title') }}
</h2>
</div>
<div class="flex-1 overflow-y-auto scrollbar-custom">
<PropertiesAccordionItem>
<template #label>
<span class="text-xs uppercase">
{{ $t('assetBrowser.modelInfo.basicInfo') }}
</span>
</template>
<ModelInfoField :label="$t('assetBrowser.modelInfo.displayName')">
<span class="text-sm">{{ displayName }}</span>
</ModelInfoField>
<ModelInfoField :label="$t('assetBrowser.modelInfo.fileName')">
<span class="text-sm">{{ asset.name }}</span>
</ModelInfoField>
<ModelInfoField
v-if="sourceUrl"
:label="$t('assetBrowser.modelInfo.source')"
<div
data-component-id="ModelInfoPanel"
class="flex h-full flex-col scrollbar-custom"
>
<PropertiesAccordionItem class="bg-transparent">
<template #label>
<span class="text-xs uppercase font-inter">
{{ $t('assetBrowser.modelInfo.basicInfo') }}
</span>
</template>
<ModelInfoField :label="$t('assetBrowser.modelInfo.displayName')">
<span class="text-sm break-all">{{ displayName }}</span>
</ModelInfoField>
<ModelInfoField :label="$t('assetBrowser.modelInfo.fileName')">
<span class="text-sm break-all">{{ asset.name }}</span>
</ModelInfoField>
<ModelInfoField
v-if="sourceUrl"
:label="$t('assetBrowser.modelInfo.source')"
>
<a
:href="sourceUrl"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-link hover:underline"
>
<a
:href="sourceUrl"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-link hover:underline"
{{
$t('assetBrowser.modelInfo.viewOnSource', { source: sourceName })
}}
</a>
</ModelInfoField>
</PropertiesAccordionItem>
<PropertiesAccordionItem class="bg-transparent">
<template #label>
<span class="text-xs uppercase font-inter">
{{ $t('assetBrowser.modelInfo.modelTagging') }}
</span>
</template>
<ModelInfoField
v-if="modelType"
:label="$t('assetBrowser.modelInfo.modelType')"
>
<span class="text-sm">{{ modelType }}</span>
</ModelInfoField>
<ModelInfoField
v-if="baseModel"
:label="$t('assetBrowser.modelInfo.compatibleBaseModels')"
>
<span class="text-sm">{{ baseModel }}</span>
</ModelInfoField>
<ModelInfoField
v-if="additionalTags.length > 0"
:label="$t('assetBrowser.modelInfo.additionalTags')"
>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in additionalTags"
:key="tag"
class="rounded px-2 py-0.5 text-xs"
>
{{
$t('assetBrowser.modelInfo.viewOnSource', { source: sourceName })
}}
</a>
</ModelInfoField>
</PropertiesAccordionItem>
<PropertiesAccordionItem>
<template #label>
<span class="text-xs uppercase">
{{ $t('assetBrowser.modelInfo.modelTagging') }}
{{ tag }}
</span>
</template>
<ModelInfoField
v-if="modelType"
:label="$t('assetBrowser.modelInfo.modelType')"
>
<span class="text-sm">{{ modelType }}</span>
</ModelInfoField>
<ModelInfoField
v-if="baseModel"
:label="$t('assetBrowser.modelInfo.compatibleBaseModels')"
>
<span class="text-sm">{{ baseModel }}</span>
</ModelInfoField>
<ModelInfoField
v-if="additionalTags.length > 0"
:label="$t('assetBrowser.modelInfo.additionalTags')"
>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in additionalTags"
:key="tag"
class="rounded bg-surface-container px-2 py-0.5 text-xs"
>
{{ tag }}
</span>
</div>
</ModelInfoField>
</PropertiesAccordionItem>
</div>
</ModelInfoField>
</PropertiesAccordionItem>
<PropertiesAccordionItem>
<template #label>
<span class="text-xs uppercase">
{{ $t('assetBrowser.modelInfo.modelDescription') }}
<PropertiesAccordionItem class="bg-transparent">
<template #label>
<span class="text-xs uppercase font-inter">
{{ $t('assetBrowser.modelInfo.modelDescription') }}
</span>
</template>
<ModelInfoField
v-if="triggerPhrases.length > 0"
:label="$t('assetBrowser.modelInfo.triggerPhrases')"
>
<div class="flex flex-wrap gap-1">
<span
v-for="phrase in triggerPhrases"
:key="phrase"
class="rounded px-2 py-0.5 text-xs"
>
{{ phrase }}
</span>
</template>
<ModelInfoField
v-if="triggerPhrases.length > 0"
:label="$t('assetBrowser.modelInfo.triggerPhrases')"
>
<div class="flex flex-wrap gap-1">
<span
v-for="phrase in triggerPhrases"
:key="phrase"
class="rounded bg-surface-container px-2 py-0.5 text-xs"
>
{{ phrase }}
</span>
</div>
</ModelInfoField>
<ModelInfoField
v-if="description"
:label="$t('assetBrowser.modelInfo.description')"
>
<p class="text-sm whitespace-pre-wrap">{{ description }}</p>
</ModelInfoField>
</PropertiesAccordionItem>
</div>
</div>
</ModelInfoField>
<ModelInfoField
v-if="description"
:label="$t('assetBrowser.modelInfo.description')"
>
<p class="text-sm whitespace-pre-wrap">{{ description }}</p>
</ModelInfoField>
</PropertiesAccordionItem>
</div>
</template>