refactor(BaseModalLayout): convert right panel state to defineModel

This commit is contained in:
Alexander Brown
2026-01-15 17:29:37 -08:00
parent a466414079
commit e3f74b2df4
4 changed files with 73 additions and 23 deletions

View File

@@ -1,11 +1,12 @@
<template>
<div class="base-widget-layout rounded-2xl overflow-hidden relative">
<Button
v-show="!isRightPanelOpen && hasRightPanel"
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 || !hasRightPanel
'opacity-0 pointer-events-none':
isRightPanelOpen || !showRightPanelButton
})
"
@click="toggleRightPanel"
@@ -58,12 +59,14 @@
:class="
cn(
'flex justify-end gap-2 w-0',
hasRightPanel && !isRightPanelOpen ? 'min-w-22' : 'min-w-10'
showRightPanelButton && !isRightPanelOpen
? 'min-w-22'
: 'min-w-10'
)
"
>
<Button
v-if="isRightPanelOpen && hasRightPanel"
v-if="isRightPanelOpen && showRightPanelButton"
size="icon"
@click="toggleRightPanel"
>
@@ -92,7 +95,7 @@
v-if="hasRightPanel && isRightPanelOpen"
class="w-1/4 min-w-40 max-w-80"
>
<slot name="rightPanel"></slot>
<slot name="rightPanel" />
</aside>
</div>
</div>
@@ -111,6 +114,21 @@ const { contentTitle } = defineProps<{
contentTitle: string
}>()
const isRightPanelOpen = defineModel<boolean>('rightPanelOpen', {
default: false
})
const slots = useSlots()
const hasRightPanel = computed(() => !!slots.rightPanel)
const hideRightPanelButton = defineModel<boolean>('hideRightPanelButton', {
default: false
})
const showRightPanelButton = computed(
() => hasRightPanel.value && !hideRightPanelButton.value
)
const BREAKPOINTS = { md: 880 }
const PANEL_SIZES = {
width: 'w-1/3',
@@ -118,18 +136,14 @@ const PANEL_SIZES = {
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

View File

@@ -1,5 +1,7 @@
<template>
<BaseModalLayout
:hide-right-panel-button="true"
:right-panel-open="!!focusedAsset"
data-component-id="AssetBrowserModal"
class="size-full max-h-full max-w-full min-w-0"
:content-title="displayTitle"
@@ -56,13 +58,16 @@
<AssetGrid
:assets="filteredAssets"
:loading="isLoading"
:focused-asset-id="focusedAsset?.id"
@asset-focus="handleAssetFocus"
@asset-blur="focusedAsset = null"
@asset-select="handleAssetSelectAndEmit"
@asset-deleted="refreshAssets"
/>
</template>
<template v-if="selectedAsset" #rightPanel>
<ModelInfoPanel :asset="selectedAsset" />
<template #rightPanel>
<ModelInfoPanel v-if="focusedAsset" :asset="focusedAsset" />
</template>
</BaseModalLayout>
</template>
@@ -155,7 +160,7 @@ const {
updateFilters
} = useAssetBrowser(fetchedAssets)
const selectedAsset = ref<AssetDisplayItem | null>(null)
const focusedAsset = ref<AssetDisplayItem | null>(null)
const primaryCategoryTag = computed(() => {
const assets = fetchedAssets.value ?? []
@@ -198,11 +203,12 @@ function handleClose() {
emit('close')
}
function handleAssetFocus(asset: AssetDisplayItem) {
focusedAsset.value = asset
}
function handleAssetSelectAndEmit(asset: AssetDisplayItem) {
selectedAsset.value = asset
emit('asset-select', asset)
// onSelect callback is provided by dialog composable layer
// It handles the appropriate transformation (filename extraction or full asset)
props.onSelect?.(asset)
}
</script>

View File

@@ -1,5 +1,6 @@
<template>
<div
ref="card"
data-component-id="AssetCard"
:data-asset-id="asset.id"
:aria-labelledby="titleId"
@@ -12,22 +13,20 @@
'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4'
)
"
@click.stop="interactive && $emit('focus', asset)"
@dblclick="interactive && $emit('select', asset)"
@keydown.enter.self="interactive && $emit('select', asset)"
>
<div class="relative aspect-square w-full overflow-hidden rounded-xl">
<div
v-if="isLoading || error"
class="flex size-full cursor-pointer items-center justify-center bg-gradient-to-br from-smoke-400 via-smoke-800 to-charcoal-400"
role="button"
@click.self="interactive && $emit('select', asset)"
/>
<img
v-else
:src="asset.preview_url"
:alt="displayName"
class="size-full object-cover cursor-pointer"
role="button"
@click.self="interactive && $emit('select', asset)"
/>
<AssetBadgeGroup :badges="asset.badges" />
@@ -115,8 +114,8 @@
</template>
<script setup lang="ts">
import { useImage } from '@vueuse/core'
import { computed, ref, toValue, useId, useTemplateRef } from 'vue'
import { onClickOutside, useImage } from '@vueuse/core'
import { computed, ref, toValue, useId, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconGroup from '@/components/button/IconGroup.vue'
@@ -133,12 +132,19 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import { useDialogStore } from '@/stores/dialogStore'
import { cn } from '@/utils/tailwindUtil'
const { asset, interactive } = defineProps<{
const {
asset,
interactive,
focused = false
} = defineProps<{
asset: AssetDisplayItem
interactive?: boolean
focused?: boolean
}>()
const emit = defineEmits<{
focus: [asset: AssetDisplayItem]
blur: []
select: [asset: AssetDisplayItem]
deleted: [asset: AssetDisplayItem]
}>()
@@ -149,10 +155,28 @@ const { closeDialog } = useDialogStore()
const { flags } = useFeatureFlags()
const toastStore = useToastStore()
const cardRef = useTemplateRef<HTMLDivElement>('card')
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')
})
}
},
{ immediate: true }
)
const titleId = useId()
const descId = useId()

View File

@@ -34,6 +34,9 @@
<AssetCard
:asset="item"
:interactive="true"
:focused="item.id === focusedAssetId"
@focus="$emit('assetFocus', $event)"
@blur="$emit('assetBlur')"
@select="$emit('assetSelect', $event)"
@deleted="$emit('assetDeleted', $event)"
/>
@@ -50,12 +53,15 @@ import VirtualGrid from '@/components/common/VirtualGrid.vue'
import AssetCard from '@/platform/assets/components/AssetCard.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
const { assets } = defineProps<{
const { assets, focusedAssetId } = defineProps<{
assets: AssetDisplayItem[]
loading?: boolean
focusedAssetId?: string | null
}>()
defineEmits<{
assetFocus: [asset: AssetDisplayItem]
assetBlur: []
assetSelect: [asset: AssetDisplayItem]
assetDeleted: [asset: AssetDisplayItem]
}>()