- text-[9px]/text-[10px] in required-fields bar raised to text-xs (12px), meeting the project minimum for the 60+ audience (WCAG 1.4.4) - Upload animation now uses motion-safe: prefix so it stops for users with prefers-reduced-motion set (WCAG 2.1 SC 2.3.3) - Strengthened UploadZone tests: onCancel uses [role=status] button selector instead of first-button heuristic; added positive file selection test (valid PDF calls onFile), file-too-large test, and MIME rejection now also asserts the error message is visible Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.0 KiB
Svelte
103 lines
3.0 KiB
Svelte
<script lang="ts">
|
|
const ALLOWED_TYPES = new Set(['application/pdf', 'image/jpeg', 'image/png', 'image/tiff']);
|
|
const MAX_SIZE_BYTES = 50 * 1024 * 1024;
|
|
|
|
let {
|
|
filename,
|
|
isUploading,
|
|
isDragging = $bindable(false),
|
|
error,
|
|
onFile,
|
|
onCancel
|
|
}: {
|
|
filename: string;
|
|
isUploading: boolean;
|
|
isDragging?: boolean;
|
|
error: string | null;
|
|
onFile?: (file: File) => void;
|
|
onCancel?: () => void;
|
|
} = $props();
|
|
|
|
let validationError = $state<string | null>(null);
|
|
const displayError = $derived(error ?? validationError);
|
|
|
|
function handleFile(file: File | undefined) {
|
|
if (!file) return;
|
|
validationError = null;
|
|
if (!ALLOWED_TYPES.has(file.type)) {
|
|
validationError = 'Dieser Dateityp wird nicht unterstützt (PDF, JPG, PNG, TIFF).';
|
|
return;
|
|
}
|
|
if (file.size > MAX_SIZE_BYTES) {
|
|
validationError = 'Die Datei ist zu groß (max. 50 MB).';
|
|
return;
|
|
}
|
|
onFile?.(file);
|
|
}
|
|
|
|
function handleChange(e: Event) {
|
|
const input = e.currentTarget as HTMLInputElement;
|
|
handleFile(input.files?.[0]);
|
|
}
|
|
|
|
function handleDrop(e: DragEvent) {
|
|
e.preventDefault();
|
|
isDragging = false;
|
|
handleFile(e.dataTransfer?.files[0]);
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="flex flex-1 items-center justify-center bg-pdf-bg"
|
|
aria-live="polite"
|
|
aria-label={isUploading ? 'Datei wird hochgeladen' : 'Dateiupload-Bereich'}
|
|
>
|
|
{#if isUploading}
|
|
<div role="status" class="flex flex-col items-center gap-3 text-center">
|
|
<div class="h-0.5 w-48 overflow-hidden rounded-full bg-white/10">
|
|
<div
|
|
class="h-full bg-brand-mint/70 motion-safe:animate-[slide_1.4s_ease-in-out_infinite]"
|
|
></div>
|
|
</div>
|
|
<p class="max-w-[200px] truncate text-xs font-medium text-brand-mint/70">{filename}</p>
|
|
<p class="text-xs text-white/40">Wird hochgeladen …</p>
|
|
<button
|
|
class="min-h-[44px] px-3 text-xs text-white/40 transition-colors hover:text-white/60"
|
|
onclick={onCancel}
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<div
|
|
role="region"
|
|
aria-label="Datei ablegen"
|
|
class="flex flex-col items-center gap-3 rounded-sm border border-dashed p-8 text-center transition-colors
|
|
{isDragging ? 'border-brand-mint bg-brand-mint/5' : 'border-white/20'}"
|
|
ondragover={(e) => {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
}}
|
|
ondragleave={() => (isDragging = false)}
|
|
ondrop={handleDrop}
|
|
>
|
|
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 text-white/40">
|
|
↑
|
|
</div>
|
|
<p class="max-w-[200px] truncate text-xs font-medium text-white/50">{filename}</p>
|
|
<p class="text-xs text-white/30">Noch keine Datei hochgeladen</p>
|
|
{#if displayError}
|
|
<p class="text-xs text-red-400">{displayError}</p>
|
|
{/if}
|
|
<label
|
|
class="flex min-h-[44px] cursor-pointer items-center rounded-sm bg-brand-navy px-4 py-1.5 text-xs font-bold tracking-widest text-white/90 uppercase"
|
|
aria-label="Datei auswählen"
|
|
>
|
|
Datei auswählen
|
|
<input type="file" class="sr-only" onchange={handleChange} />
|
|
</label>
|
|
<p class="text-xs text-white/20">oder Datei hier ablegen</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|