Initial commit

This commit is contained in:
2025-04-21 14:12:36 +02:00
commit 3fe2969b39
57 changed files with 17976 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
<script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { fade, scale } from 'svelte/transition';
const dispatch = createEventDispatcher();
// Props
export let title = '';
export let closable = true;
export let component = null;
export let componentProps = {};
// State
let isOpen = false;
let modalContent;
let componentInstance = null;
// Event callbacks
let onSubmitCallback = null;
let onCancelCallback = null;
// Handle component dispatch events
function handleComponentEvent(event) {
if (event.type === 'save') {
if (onSubmitCallback) {
onSubmitCallback(event.detail);
}
close();
} else if (event.type === 'cancel') {
if (onCancelCallback) {
onCancelCallback();
}
close();
}
}
// Open the modal
export function open() {
console.log('Opening modal');
isOpen = true;
}
// Close the modal
export function close() {
console.log('Closing modal');
isOpen = false;
// Schedule component cleanup
setTimeout(() => {
if (!isOpen) {
cleanupComponent();
}
}, 300); // Wait for transitions to complete
}
// Set properties and callbacks
export function setProps(props) {
if (props.title !== undefined) title = props.title;
if (props.closable !== undefined) closable = props.closable;
if (props.component !== undefined) component = props.component;
if (props.componentProps !== undefined) componentProps = props.componentProps;
if (props.onSubmit) onSubmitCallback = props.onSubmit;
if (props.onCancel) onCancelCallback = props.onCancel;
}
// Clean up any previous component instance
function cleanupComponent() {
if (componentInstance) {
try {
componentInstance.$destroy();
} catch (error) {
console.error('Error destroying component instance:', error);
}
componentInstance = null;
}
}
// Only create component when modal is open and modalContent is available
$: if (isOpen && component && modalContent) {
console.log('Creating component in modal:', component.name || 'Unknown component');
// Clean up previous component first to prevent conflicts
cleanupComponent();
try {
// Create the component instance
componentInstance = new component({
target: modalContent,
props: componentProps
});
console.log('Component created successfully');
// Listen for events from the component
for (const event of ['save', 'cancel']) {
componentInstance.$on(event, (e) => handleComponentEvent({ type: event, detail: e.detail }));
}
} catch (error) {
console.error('Error creating component in modal:', error);
}
}
// Close on ESC key
function handleKeydown(event) {
if (event.key === 'Escape' && closable && isOpen) {
close();
if (onCancelCallback) onCancelCallback();
}
}
// Cleanup on destroy
onDestroy(() => {
cleanupComponent();
window.removeEventListener('keydown', handleKeydown);
});
// Listen for ESC key
onMount(() => {
window.addEventListener('keydown', handleKeydown);
return () => {
window.removeEventListener('keydown', handleKeydown);
};
});
// Prevent clicks inside the modal from bubbling up
function handleModalClick(event) {
event.stopPropagation();
}
// Handle backdrop click
function handleBackdropClick() {
if (closable) {
close();
if (onCancelCallback) onCancelCallback();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<div class="modal-backdrop" on:click={handleBackdropClick} transition:fade={{ duration: 150 }}>
<div class="modal-content" on:click={handleModalClick} transition:scale={{ start: 0.95, duration: 200 }}>
<div class="modal-header">
<h2 class="modal-title">{title}</h2>
{#if closable}
<button type="button" class="modal-close-button" on:click={close} aria-label="Close modal">
&times;
</button>
{/if}
</div>
<div class="modal-body" bind:this={modalContent}></div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.modal-title {
margin: 0;
font-size: 1.25rem;
color: #333;
}
.modal-close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
}
.modal-close-button:hover {
color: #333;
background-color: #f0f0f0;
}
.modal-body {
max-height: calc(90vh - 70px);
overflow-y: auto;
}
/* Dark mode support */
:global(body.dark) .modal-content {
background-color: #2d3748;
color: #f8f9fa;
}
:global(body.dark) .modal-header {
border-bottom-color: #4a5568;
}
:global(body.dark) .modal-title {
color: #f8f9fa;
}
:global(body.dark) .modal-close-button {
color: #cbd5e0;
}
:global(body.dark) .modal-close-button:hover {
color: #f8f9fa;
background-color: #4a5568;
}
</style>