Files
claudia-docs-web/src/components/common/Modal.vue
Motoko c9cb07dbfc Phase 1 MVP - Complete implementation
- Login with JWT and refresh token rotation
- Dashboard with projects cards
- ProjectView with TreeView navigation
- DocumentView with markdown editor and auto-save
- Tag management (create, assign, remove)
- Dark mode CSS variables
- Security fixes applied (logout to backend, createDocument endpoint)
2026-03-30 15:17:29 +00:00

155 lines
2.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { watch, onUnmounted } from 'vue'
interface Props {
show: boolean
title?: string
size?: 'sm' | 'md' | 'lg'
}
withDefaults(defineProps<Props>(), {
size: 'md'
})
const emit = defineEmits<{
close: []
}>()
function handleBackdropClick(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains('modal')) {
emit('close')
}
}
function handleEscape(e: KeyboardEvent) {
if (e.key === 'Escape') {
emit('close')
}
}
watch(() => emit, () => {}, { immediate: true })
onUnmounted(() => {
document.removeEventListener('keydown', handleEscape)
})
</script>
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="show" class="modal" @click="handleBackdropClick">
<div :class="['modal__content', `modal__content--${size}`]">
<div v-if="title" class="modal__header">
<h2 class="modal__title">{{ title }}</h2>
<button class="modal__close" @click="emit('close')">×</button>
</div>
<div class="modal__body">
<slot />
</div>
<div v-if="$slots.footer" class="modal__footer">
<slot name="footer" />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.modal__content {
background: var(--bg-primary);
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
max-height: 90vh;
overflow: auto;
}
.modal__content--sm {
width: 90%;
max-width: 400px;
}
.modal__content--md {
width: 90%;
max-width: 560px;
}
.modal__content--lg {
width: 90%;
max-width: 800px;
}
.modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.modal__title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
}
.modal__close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0;
line-height: 1;
}
.modal__close:hover {
color: var(--text-primary);
}
.modal__body {
padding: 1.5rem;
}
.modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
}
/* Transitions */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.2s ease;
}
.modal-enter-active .modal__content,
.modal-leave-active .modal__content {
transition: transform 0.2s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal__content,
.modal-leave-to .modal__content {
transform: scale(0.95);
}
</style>