refactor: restructure BaseModalLayout from flexbox to CSS Grid

This commit is contained in:
Alexander Brown
2026-01-17 15:35:36 -08:00
parent 44c8d801f2
commit f45af6c455

View File

@@ -3,101 +3,91 @@
class="base-widget-layout rounded-2xl overflow-hidden relative"
@keydown.esc.capture="handleEscape"
>
<div class="flex h-full w-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="flex-1 flex bg-base-background overflow-hidden">
<div class="flex h-full w-full flex-col">
<header
v-if="$slots.header"
class="w-full h-18 px-6 flex items-center justify-between gap-2"
>
<div class="flex flex-1 shrink-0 gap-2">
<Button v-if="!notMobile" size="icon" @click="toggleLeftPanel">
<i
:class="
cn(
showLeftPanel
? 'icon-[lucide--panel-left]'
: 'icon-[lucide--panel-left-close]'
)
"
/>
</Button>
<slot name="header"></slot>
</div>
<slot name="header-right-area"></slot>
<template v-if="!isRightPanelOpen">
<Button
v-if="showRightPanelButton"
size="icon"
@click="toggleRightPanel"
>
<i class="icon-[lucide--panel-right] text-sm" />
</Button>
<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">
<!-- Fallback title bar when no leftPanel is provided -->
<slot name="contentFilter"></slot>
<h2
v-if="!$slots.leftPanel"
class="text-xxl m-0 px-6 pt-2 pb-6 capitalize"
>
{{ contentTitle }}
</h2>
<div
class="min-h-0 flex-1 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
>
<slot name="content"></slot>
</div>
</main>
<div
class="grid h-full w-full transition-[grid-template-columns] duration-300 ease-out"
:style="gridStyle"
>
<nav v-if="hasLeftPanel" class="overflow-hidden">
<div class="min-w-40 max-w-56">
<slot name="leftPanel" />
</div>
<Transition name="slide-panel-right">
<aside
v-if="hasRightPanel && isRightPanelOpen"
key="right-panel"
class="flex w-72 shrink-0 bg-modal-panel-background flex-col"
>
<header
data-component-id="RightPanelHeader"
class="flex h-16 shrink-0 items-center gap-2 px-4"
</nav>
<div class="flex flex-col bg-base-background overflow-hidden">
<header
v-if="$slots.header"
class="w-full h-18 px-6 flex items-center justify-between gap-2"
>
<div class="flex flex-1 shrink-0 gap-2">
<Button v-if="!notMobile" size="icon" @click="toggleLeftPanel">
<i
:class="
cn(
showLeftPanel
? 'icon-[lucide--panel-left]'
: 'icon-[lucide--panel-left-close]'
)
"
/>
</Button>
<slot name="header" />
</div>
<slot name="header-right-area" />
<template v-if="!isRightPanelOpen">
<Button
v-if="showRightPanelButton"
size="icon"
@click="toggleRightPanel"
>
<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 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>
</Transition>
<i class="icon-[lucide--panel-right] text-sm" />
</Button>
<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">
<slot name="contentFilter" />
<h2
v-if="!hasLeftPanel"
class="text-xxl m-0 px-6 pt-2 pb-6 capitalize"
>
{{ contentTitle }}
</h2>
<div
class="min-h-0 flex-1 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
>
<slot name="content" />
</div>
</main>
</div>
<aside v-if="hasRightPanel" class="overflow-hidden">
<div class="w-72 flex flex-col bg-modal-panel-background h-full">
<header
data-component-id="RightPanelHeader"
class="flex h-16 shrink-0 items-center gap-2 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 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>
</div>
</aside>
</div>
</div>
</template>
@@ -120,6 +110,7 @@ const isRightPanelOpen = defineModel<boolean>('rightPanelOpen', {
})
const slots = useSlots()
const hasLeftPanel = computed(() => !!slots.leftPanel)
const hasRightPanel = computed(() => !!slots.rightPanel)
const hideRightPanelButton = defineModel<boolean>('hideRightPanelButton', {
@@ -131,11 +122,6 @@ const showRightPanelButton = computed(
)
const BREAKPOINTS = { md: 880 }
const PANEL_SIZES = {
width: 'w-1/3',
minWidth: 'min-w-40',
maxWidth: 'max-w-56'
}
const closeDialog = inject(OnCloseKey, () => {})
@@ -158,6 +144,10 @@ const showLeftPanel = computed(() => {
return shouldShow
})
const gridStyle = computed(() => ({
gridTemplateColumns: `${hasLeftPanel.value && showLeftPanel.value ? 'minmax(10rem,14rem)' : '0fr'} 1fr ${isRightPanelOpen.value ? '18rem' : '0fr'}`
}))
const toggleLeftPanel = () => {
if (notMobile.value) {
isLeftPanelOpen.value = !isLeftPanelOpen.value
@@ -197,30 +187,4 @@ function handleEscape(event: KeyboardEvent) {
max-width: 1724px;
}
}
/* 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);
}
.slide-panel-enter-from,
.slide-panel-leave-to {
transform: translateX(-100%);
}
/* Slide transition for right panel */
.slide-panel-right-enter-active,
.slide-panel-right-leave-active {
position: absolute;
right: 0;
top: 0;
height: 100%;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-panel-right-enter-from,
.slide-panel-right-leave-to {
transform: translateX(100%);
}
</style>