feat(ui) : composant AppModal réutilisable (header fixe / body scrollable / footer sticky)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
frontend/components/ui/AppModal.vue
Normal file
87
frontend/components/ui/AppModal.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user