88 lines
2.7 KiB
Vue
88 lines
2.7 KiB
Vue
<script setup lang="ts">
|
|
const props = withDefaults(defineProps<{
|
|
modelValue: boolean
|
|
title?: string
|
|
/** Largeur max du panneau */
|
|
width?: 'sm' | 'md' | 'lg' | 'xl'
|
|
}>(), {
|
|
title: '',
|
|
width: 'md',
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
}>()
|
|
|
|
const WIDTH_CLASS: Record<NonNullable<typeof props.width>, string> = {
|
|
sm: 'max-w-md',
|
|
md: 'max-w-lg',
|
|
lg: 'max-w-2xl',
|
|
xl: 'max-w-4xl',
|
|
}
|
|
|
|
function close(): void {
|
|
emit('update:modelValue', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport v-if="modelValue" to="body">
|
|
<Transition name="app-modal" appear>
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" @click="close" />
|
|
|
|
<div
|
|
class="relative z-10 flex max-h-[90vh] w-full flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5"
|
|
:class="WIDTH_CLASS[width]"
|
|
>
|
|
<!-- Header (fixe) -->
|
|
<div class="flex shrink-0 items-center justify-between border-b border-neutral-100 bg-neutral-50/80 px-6 py-4">
|
|
<h2 class="text-base font-bold text-neutral-900">
|
|
<slot name="title">{{ title }}</slot>
|
|
</h2>
|
|
<MalioButtonIcon
|
|
icon="mdi:close"
|
|
aria-label="Fermer"
|
|
variant="ghost"
|
|
icon-size="20"
|
|
@click="close"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Body (scrollable) -->
|
|
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-5">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- Footer (sticky) -->
|
|
<div
|
|
v-if="$slots.footer"
|
|
class="flex shrink-0 justify-end gap-3 border-t border-neutral-100 bg-white px-6 py-4"
|
|
>
|
|
<slot name="footer" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-modal-enter-active,
|
|
.app-modal-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.app-modal-enter-active > div:last-child,
|
|
.app-modal-leave-active > div:last-child {
|
|
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
|
|
}
|
|
.app-modal-enter-from,
|
|
.app-modal-leave-to {
|
|
opacity: 0;
|
|
}
|
|
.app-modal-enter-from > div:last-child {
|
|
transform: scale(0.95) translateY(8px);
|
|
opacity: 0;
|
|
}
|
|
</style>
|