- UiModal : composant générique réutilisable (teleport, escape, backdrop, max-width configurable) - InventoryExportModal : 3 checkboxes pour les tranches d'âge, footer centré sans annuler - BovineRepository::findActiveForInventoryExport(?array $ageRanges) en DQL - Endpoint inventory-export accepte ageRanges (comma-separated) en query param - Aucune coche = export complet (comportement actuel intact) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
2.9 KiB
Vue
97 lines
2.9 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition
|
|
enter-active-class="transition duration-150 ease-out"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition duration-100 ease-in"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="modelValue"
|
|
class="fixed inset-0 z-40 flex items-center justify-center bg-black/50 px-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
@mousedown.self="closeOnBackdrop"
|
|
>
|
|
<div
|
|
class="w-full rounded-md bg-white shadow-2xl"
|
|
:class="maxWidth"
|
|
@mousedown.stop
|
|
>
|
|
<div class="flex items-center justify-between border-b border-slate-200 px-6 py-4">
|
|
<h2 class="text-xl font-bold uppercase text-primary-500">{{ title }}</h2>
|
|
<button
|
|
type="button"
|
|
class="text-slate-500 hover:text-primary-500 flex items-center"
|
|
aria-label="Fermer"
|
|
@click="close"
|
|
>
|
|
<Icon name="mdi:close" size="24" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="px-6 py-5">
|
|
<slot />
|
|
</div>
|
|
|
|
<div
|
|
v-if="$slots.footer"
|
|
class="border-t border-slate-200 px-6 py-4"
|
|
>
|
|
<slot name="footer" :close="close" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, onBeforeUnmount, watch } from 'vue'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
modelValue: boolean
|
|
title?: string
|
|
closeOnBackdropClick?: boolean
|
|
maxWidth?: string
|
|
}>(), {
|
|
title: '',
|
|
closeOnBackdropClick: true,
|
|
maxWidth: 'max-w-lg'
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: boolean): void
|
|
}>()
|
|
|
|
const close = () => emit('update:modelValue', false)
|
|
|
|
const closeOnBackdrop = () => {
|
|
if (props.closeOnBackdropClick) close()
|
|
}
|
|
|
|
const onKeydown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape' && props.modelValue) close()
|
|
}
|
|
|
|
watch(() => props.modelValue, (open) => {
|
|
if (typeof document === 'undefined') return
|
|
document.body.style.overflow = open ? 'hidden' : ''
|
|
})
|
|
|
|
onMounted(() => {
|
|
if (typeof document !== 'undefined') {
|
|
document.addEventListener('keydown', onKeydown)
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (typeof document !== 'undefined') {
|
|
document.removeEventListener('keydown', onKeydown)
|
|
document.body.style.overflow = ''
|
|
}
|
|
})
|
|
</script>
|