Files
vtuber-awards/frontend/src/components/ui/Modal.vue
T
AzuTear 9393d433fa
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 56s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m5s
Harden audit privacy and admin modals
2026-06-26 20:47:07 +02:00

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>