diff --git a/src/components/FileDropzone.jsx b/src/components/FileDropzone.jsx
new file mode 100644
index 0000000..b6e06e3
--- /dev/null
+++ b/src/components/FileDropzone.jsx
@@ -0,0 +1,108 @@
+import { useState, useRef } from 'react';
+import Box from '@mui/material/Box';
+
+/**
+ * An otherwise invisible dropzone wrapper.
+ * Renders seamlessly around children without altering layout or style,
+ * and highlights with an outline only while a file is dragged over it.
+ */
+export function FileDropzone({
+ accept = 'application/pdf,.pdf',
+ disabled = false,
+ onFileDrop,
+ children,
+ sx,
+ ...props
+}) {
+ const [isDragging, setIsDragging] = useState(false);
+ const dragCounter = useRef(0);
+
+ const handleDragEnter = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (disabled) return;
+ dragCounter.current += 1;
+ if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+ setIsDragging(true);
+ }
+ };
+
+ const handleDragLeave = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dragCounter.current -= 1;
+ if (dragCounter.current <= 0) {
+ dragCounter.current = 0;
+ setIsDragging(false);
+ }
+ };
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (disabled) {
+ e.dataTransfer.dropEffect = 'none';
+ return;
+ }
+ e.dataTransfer.dropEffect = 'copy';
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dragCounter.current = 0;
+ setIsDragging(false);
+ if (disabled) return;
+
+ const file = e.dataTransfer.files?.[0];
+ if (!file) return;
+
+ if (accept) {
+ const allowed = accept
+ .split(',')
+ .map((s) => s.trim().toLowerCase())
+ .filter(Boolean);
+ const name = (file.name || '').toLowerCase();
+ const dotIndex = name.lastIndexOf('.');
+ const ext = dotIndex !== -1 ? name.slice(dotIndex) : '';
+ const mimeType = (file.type || '').toLowerCase();
+
+ const matches = allowed.some((item) => {
+ if (item.startsWith('.')) {
+ return ext === item;
+ }
+ return mimeType === item;
+ });
+
+ if (!matches) return;
+ }
+
+ onFileDrop?.(file);
+ };
+
+ return (
+
+ {typeof children === 'function' ? children({ isDragging }) : children}
+
+ );
+}
+
+export default FileDropzone;
diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx
index d9349b4..01c3e4a 100644
--- a/src/components/manifest/ManifestHeader.jsx
+++ b/src/components/manifest/ManifestHeader.jsx
@@ -7,6 +7,7 @@ import TextField from '@mui/material/TextField';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile } from 'dashgui/panels';
+import FileDropzone from '../FileDropzone';
import { manifestConfig, partyDisplayName } from './manifestConfig';
class ManifestHeader extends PureComponent {
@@ -122,16 +123,42 @@ class ManifestHeader extends PureComponent {
/>
) : null}
{onPdfUpload ? (
- : }
+
- {pdfUploading ? 'Wird analysiert…' : 'PDF hochladen'}
-
+ {({ isDragging }) => (
+ : }
+ disabled={saving || pdfUploading}
+ onClick={this.handleUploadClick}
+ sx={{
+ whiteSpace: 'nowrap',
+ height: 40,
+ width: '100%',
+ px: 2,
+ boxSizing: 'border-box',
+ bgcolor: isDragging ? 'action.hover' : 'transparent',
+ borderColor: isDragging ? 'primary.main' : undefined,
+ transition: 'background-color 0.15s, border-color 0.15s',
+ }}
+ >
+ {pdfUploading
+ ? 'Wird analysiert…'
+ : isDragging
+ ? 'PDF hier ablegen'
+ : 'PDF hochladen'}
+
+ )}
+
) : null}
{onPdfUpload ? (