88 lines
2.5 KiB
Vue
88 lines
2.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
|
import { X } from '@lucide/vue'
|
|
|
|
import { useBodyScrollLock } from '../../composables/useBodyScrollLock'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
open: boolean
|
|
title?: string
|
|
subtitle?: string
|
|
size?: 'md' | 'lg' | 'xl'
|
|
}>(), {
|
|
size: 'md',
|
|
})
|
|
|
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
|
|
const panelSizeClass = computed(() => ({
|
|
md: 'max-w-lg',
|
|
lg: 'max-w-3xl',
|
|
xl: 'max-w-6xl',
|
|
}[props.size]))
|
|
|
|
function onKey(event: KeyboardEvent) {
|
|
if (event.key === 'Escape' && props.open) emit('close')
|
|
}
|
|
|
|
useBodyScrollLock(() => props.open)
|
|
|
|
onMounted(() => window.addEventListener('keydown', onKey))
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('keydown', onKey)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="open"
|
|
class="modal-overlay fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
@click.self="emit('close')"
|
|
>
|
|
<div class="absolute inset-0 bg-violet-950/30 backdrop-blur-sm" @click="emit('close')" />
|
|
<div class="modal-panel relative z-10 w-full overflow-hidden rounded-[28px] border border-violet-200/70 bg-white shadow-[0_40px_90px_rgba(76,40,160,0.28)]" :class="panelSizeClass">
|
|
<div class="flex items-start justify-between gap-4 border-b border-violet-100 bg-[linear-gradient(135deg,#f3edff,#fff4e6)] px-6 py-5">
|
|
<div>
|
|
<h3 v-if="title" class="font-[Cormorant_Garamond] text-3xl text-violet-800">{{ title }}</h3>
|
|
<p v-if="subtitle" class="mt-1 text-sm text-slate-500">{{ subtitle }}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="grid h-9 w-9 shrink-0 place-items-center rounded-full text-slate-400 transition hover:bg-white/70 hover:text-violet-700"
|
|
@click="emit('close')"
|
|
>
|
|
<X class="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
<div class="max-h-[70vh] overflow-y-auto px-6 py-6">
|
|
<slot />
|
|
</div>
|
|
<div v-if="$slots.footer" class="flex justify-end gap-3 border-t border-violet-100 bg-violet-50/40 px-6 py-4">
|
|
<slot name="footer" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.modal-overlay {
|
|
animation: modal-fade 0.16s ease;
|
|
}
|
|
.modal-panel {
|
|
animation: modal-pop 0.18s ease;
|
|
}
|
|
@keyframes modal-fade {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
@keyframes modal-pop {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(8px) scale(0.98);
|
|
}
|
|
}
|
|
</style>
|