first commit

This commit is contained in:
root
2025-05-05 08:52:16 +02:00
commit e3205d500d
41 changed files with 7772 additions and 0 deletions

View File

@@ -0,0 +1,223 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
ref="modalRef"
:style="{
position: isFullScreen ? 'fixed' : 'absolute',
left: isFullScreen ? '0' : `${position.x}px`,
top: isFullScreen ? '0' : `${position.y}px`,
width: isFullScreen ? '100%' : `${size.width}px`,
height: isFullScreen ? '100%' : `${size.height}px`,
}"
class="bg-white dark:bg-gray-800 rounded-2xl border-2 border-gray-300 dark:border-gray-700 shadow-xl flex flex-col fixed z-50 transition-colors duration-300"
:class="{ 'rounded-none border-0': isFullScreen }"
>
<!-- Header with drag handle -->
<div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700" :class="{ 'cursor-move': !isFullScreen }" @mousedown="startDrag" @touchstart.prevent="handleTouchStart">
<h3 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ title }}</h3>
<div class="flex items-center space-x-2">
<button @click="toggleFullScreen" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-umami-event="modal-fullscreen">
<img src="@/assets/images/fullscreen-icon.svg" class="w-4 h-4 dark:invert" alt="Fullscreen" :class="{ 'rotate-180': isFullScreen }" />
</button>
<button @click="close" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-umami-event="modal-close">
<img src="@/assets/images/close-icon.svg" class="w-5 h-5 dark:invert" alt="Close" />
</button>
</div>
</div>
<!-- Body -->
<div class="p-6 flex-1 overflow-auto">
<slot></slot>
</div>
<!-- Resize handle -->
<div v-if="!isFullScreen" class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize" @mousedown="startResize" @touchstart="startResize"></div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
const props = defineProps<{
isOpen: boolean;
title: string;
initialWidth?: number;
initialHeight?: number;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const modalRef = ref<HTMLElement | null>(null);
const position = ref({ x: 0, y: 0 });
const size = ref({
width: props.initialWidth || 800,
height: props.initialHeight || 600,
});
const isDragging = ref(false);
const isResizing = ref(false);
const startPos = ref({ x: 0, y: 0 });
const startSize = ref({ width: 0, height: 0 });
// Add isFullScreen ref
const isFullScreen = ref(false);
// Add previous state storage for restoring from full screen
const previousState = ref({
position: { x: 0, y: 0 },
size: { width: 0, height: 0 },
});
// Add toggleFullScreen function
const toggleFullScreen = () => {
if (!isFullScreen.value) {
// Store current state before going full screen
previousState.value = {
position: { ...position.value },
size: { ...size.value },
};
} else {
// Restore previous state
position.value = { ...previousState.value.position };
size.value = { ...previousState.value.size };
}
isFullScreen.value = !isFullScreen.value;
};
// Unified start function for both drag and resize
const startAction = (event: MouseEvent | TouchEvent, action: 'drag' | 'resize') => {
if (isFullScreen.value) return;
// Extract the correct coordinates based on event type
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
if (action === 'drag') {
isDragging.value = true;
startPos.value = {
x: clientX - position.value.x,
y: clientY - position.value.y,
};
} else {
isResizing.value = true;
startPos.value = { x: clientX, y: clientY };
startSize.value = { ...size.value };
}
document.addEventListener('mousemove', handleMove);
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('mouseup', stopAction);
document.addEventListener('touchend', stopAction);
};
const startDrag = (event: MouseEvent | TouchEvent) => startAction(event, 'drag');
const startResize = (event: MouseEvent | TouchEvent) => startAction(event, 'resize');
const handleMove = (event: MouseEvent | TouchEvent) => {
if (!isDragging.value && !isResizing.value) return;
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
if (isDragging.value) {
const newX = clientX - startPos.value.x;
const newY = clientY - startPos.value.y;
position.value = constrainPosition(newX, newY);
} else if (isResizing.value) {
const deltaX = clientX - startPos.value.x;
const deltaY = clientY - startPos.value.y;
size.value = constrainSize(startSize.value.width + deltaX, startSize.value.height + deltaY);
}
};
const stopAction = () => {
isDragging.value = false;
isResizing.value = false;
document.removeEventListener('mousemove', handleMove);
document.removeEventListener('touchmove', handleMove);
document.removeEventListener('mouseup', stopAction);
document.removeEventListener('touchend', stopAction);
};
const constrainPosition = (x: number, y: number) => {
if (!modalRef.value) return { x, y };
const modalRect = modalRef.value.getBoundingClientRect();
return {
x: Math.max(0, Math.min(x, window.innerWidth - modalRect.width)),
y: Math.max(0, Math.min(y, window.innerHeight - modalRect.height)),
};
};
const constrainSize = (width: number, height: number) => {
return {
width: Math.max(400, Math.min(width, window.innerWidth - position.value.x)),
height: Math.max(300, Math.min(height, window.innerHeight - position.value.y)),
};
};
const centerModal = () => {
if (!modalRef.value) return;
position.value = {
x: (window.innerWidth - size.value.width) / 2,
y: (window.innerHeight - size.value.height) / 2,
};
};
const close = () => {
emit('close');
position.value = { x: 0, y: 0 };
};
// Event handlers
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.isOpen) close();
};
const handleResize = () => {
if (!isDragging.value && !isResizing.value) {
position.value = constrainPosition(position.value.x, position.value.y);
size.value = constrainSize(size.value.width, size.value.height);
}
};
// Add these new touch handling functions
const handleTouchStart = (event: TouchEvent) => {
if (isFullScreen.value) return;
if (event.touches.length === 1) {
startAction(event, 'drag');
}
};
const handleTouchMove = (event: TouchEvent) => {
if (!isDragging.value && !isResizing.value) return;
event.preventDefault();
if (event.touches.length === 1) {
handleMove(event);
}
};
// Lifecycle
watch(
() => props.isOpen,
newValue => {
if (newValue) nextTick(centerModal);
}
);
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
window.addEventListener('resize', handleResize);
if (props.isOpen) centerModal();
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('resize', handleResize);
stopAction();
});
</script>