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