adds ci/cd

This commit is contained in:
2026-07-22 21:28:50 +02:00
parent 0f7bb4b6eb
commit 44ca534cfa
13 changed files with 2040 additions and 185 deletions
+286
View File
@@ -0,0 +1,286 @@
<script lang="ts">
import AttachmentPreview from '$lib/components/AttachmentPreview.svelte';
import type { AttachmentLocation, SubDocument, ZipAttachment } from '$lib/zip-processing';
import { cn } from '$lib/utils';
type Props = {
subDocument: SubDocument;
blockIndex: number;
thumbnailWidth: number;
isBlockDragSource?: boolean;
isBlockDropTarget?: boolean;
onSubDocumentChange?: (next: SubDocument) => void;
onRemove?: () => void;
onAttachmentMove?: (source: AttachmentLocation, target: AttachmentLocation) => void;
onBlockDragStart?: (blockIndex: number, event: DragEvent) => void;
onBlockDragOver?: (blockIndex: number, event: DragEvent) => void;
onBlockDrop?: (blockIndex: number, event: DragEvent) => void;
onBlockDragEnd?: () => void;
};
let {
subDocument,
blockIndex,
thumbnailWidth = 200,
isBlockDragSource = false,
isBlockDropTarget = false,
onSubDocumentChange = () => {},
onRemove = () => {},
onAttachmentMove = () => {},
onBlockDragStart = () => {},
onBlockDragOver = () => {},
onBlockDrop = () => {},
onBlockDragEnd = () => {}
}: Props = $props();
let subDocumentName = $state('');
let dragIndex = $state<number | null>(null);
let dropIndex = $state<number | null>(null);
$effect(() => {
subDocumentName = subDocument.name;
});
const emitChange = (overrides: Partial<SubDocument> = {}) => {
onSubDocumentChange({
...subDocument,
name: subDocumentName,
attachments: subDocument.attachments.map((attachment) => ({ ...attachment })),
...overrides
});
};
const ATTACHMENT_MIME = 'application/x-juri-merger-attachment';
const BLOCK_MIME = 'application/x-juri-merger-block';
const parseAttachmentPayload = (event: DragEvent): AttachmentLocation | null => {
const raw = event.dataTransfer?.getData(ATTACHMENT_MIME);
if (!raw) {
return null;
}
try {
const parsed = JSON.parse(raw) as AttachmentLocation;
if (parsed.kind === 'loose' || parsed.kind === 'subDocument') {
return parsed;
}
return null;
} catch {
return null;
}
};
const handleCardDragStart = (index: number, event: DragEvent) => {
dragIndex = index;
dropIndex = index;
const source: AttachmentLocation = {
kind: 'subDocument',
id: subDocument.id,
index
};
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(ATTACHMENT_MIME, JSON.stringify(source));
// keep a text/plain fallback so the drag is not silently cancelled
event.dataTransfer.setData('text/plain', String(index));
}
};
const handleCardDragOver = (index: number, event: DragEvent) => {
if (event.dataTransfer?.types.includes(ATTACHMENT_MIME)) {
event.preventDefault();
dropIndex = index;
}
};
const handleCardDrop = (index: number, event: DragEvent) => {
const source = parseAttachmentPayload(event);
if (!source) {
return;
}
event.preventDefault();
const target: AttachmentLocation = {
kind: 'subDocument',
id: subDocument.id,
index
};
onAttachmentMove(source, target);
dragIndex = null;
dropIndex = null;
};
const handleCardDragEnd = () => {
dragIndex = null;
dropIndex = null;
};
const handleNameInput = (event: Event) => {
subDocumentName = (event.currentTarget as HTMLInputElement).value;
emitChange();
};
const handleHeaderDragStart = (event: DragEvent) => {
// do not start a block drag when the user grabbed the remove button or name input
if (event.defaultPrevented) {
return;
}
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(BLOCK_MIME, String(blockIndex));
event.dataTransfer.setData('text/plain', String(blockIndex));
}
onBlockDragStart(blockIndex, event);
};
const handleHeaderDragOver = (event: DragEvent) => {
if (event.dataTransfer?.types.includes(BLOCK_MIME)) {
event.preventDefault();
onBlockDragOver(blockIndex, event);
}
};
const handleHeaderDrop = (event: DragEvent) => {
const raw = event.dataTransfer?.getData(BLOCK_MIME);
if (raw === undefined || raw === '') {
return;
}
event.preventDefault();
onBlockDrop(blockIndex, event);
};
const attachments: ZipAttachment[] = $derived(subDocument.attachments);
</script>
<section
class={cn(
'flex flex-col gap-3 rounded-xl border bg-primary-50/40 px-3 py-3 transition',
isBlockDropTarget ? 'border-primary-400 ring-1 ring-primary-200' : 'border-primary-100',
isBlockDragSource ? 'opacity-60' : ''
)}
>
<header class="flex flex-wrap items-center gap-2 text-primary">
<button
class="w-fit cursor-grab rounded-md border border-primary-200 bg-white px-2 py-1 text-sm font-semibold leading-none text-primary-700 active:cursor-grabbing"
draggable="true"
type="button"
aria-label="Teildokument verschieben"
ondragstart={handleHeaderDragStart}
ondragover={handleHeaderDragOver}
ondrop={handleHeaderDrop}
ondragend={onBlockDragEnd}
>
::
</button>
<label class="block min-w-0 flex-1">
<span class="sr-only">Teildokumentname</span>
<input
class="w-full max-w-md rounded-md border border-primary-200 bg-white px-3 py-1.5 text-[15px] font-semibold text-primary-900 outline-none transition focus:border-primary-400 focus:ring-2 focus:ring-primary-200"
type="text"
value={subDocumentName}
oninput={handleNameInput}
draggable="false"
/>
</label>
<span class="text-[13px] text-primary-700">
{attachments.length} Datei{attachments.length === 1 ? '' : 'en'}
</span>
<button
class="rounded-md border border-primary-200 bg-white px-2 py-1 text-[13px] font-medium text-primary-700 transition hover:border-red-300 hover:bg-red-50 hover:text-red-700"
type="button"
onclick={onRemove}
draggable="false"
ondragstart={(event) => event.preventDefault()}
>
Auflösen
</button>
</header>
{#if attachments.length === 0}
<ul
class="flex items-center justify-center rounded-md border border-dashed border-primary-200 bg-white/60 px-4 py-6 text-[13px] text-primary-600"
ondragover={(event) => {
if (event.dataTransfer?.types.includes(ATTACHMENT_MIME)) {
event.preventDefault();
}
}}
ondrop={(event) => {
const source = parseAttachmentPayload(event);
if (!source) {
return;
}
event.preventDefault();
const target: AttachmentLocation = {
kind: 'subDocument',
id: subDocument.id,
index: 0
};
onAttachmentMove(source, target);
dragIndex = null;
dropIndex = null;
}}
>
<li class="w-full text-center">
Dieses Teildokument ist leer. Ziehe Dateien hierher, um sie hinzuzufügen.
</li>
</ul>
{:else}
<ul class="flex gap-3 overflow-x-auto pb-1">
{#each attachments as attachment, index (attachment.path)}
<li
class={cn(
'flex flex-col shrink-0 items-stretch gap-2 rounded-xl border bg-accent p-2 text-primary',
dropIndex === index
? 'border-primary-400 ring-1 ring-primary-200'
: 'border-primary-100',
dragIndex === index ? 'opacity-60' : ''
)}
style={`width: ${thumbnailWidth}px;`}
draggable="true"
ondragstart={(event) => handleCardDragStart(index, event)}
ondragover={(event) => handleCardDragOver(index, event)}
ondrop={(event) => handleCardDrop(index, event)}
ondragend={handleCardDragEnd}
>
<div class="flex min-w-0 flex-1 flex-row items-baseline gap-2 py-1">
<button
class="w-fit cursor-grab rounded-md border border-primary-200 bg-white px-2 py-1 text-sm font-semibold leading-none text-primary-700 active:cursor-grabbing"
draggable="false"
type="button"
aria-label="Datei verschieben"
>
::
</button>
<div
class="truncate text-[13px] font-medium leading-tight text-primary-900"
title={attachment.name}
>
{attachment.name}
</div>
</div>
<AttachmentPreview {attachment} />
</li>
{/each}
</ul>
{/if}
</section>