anydrop/web/src/components/DropZone.tsx
ordinarthur c18d995c3f feat(web): Paper & Envelope design system
Replace generic slate/indigo dark theme with a custom editorial
direction: warm paper neutrals, oxblood signal, Fraunces serif
display, Inter body, JetBrains Mono for codes. SVG paper-texture
noise overlay and thin rules across the app.

Refactored: Home, Settings, JoinRoom, Pair, Share, plus every
modal and panel (DropZone, DevicePairingPanel, PublicRoomPanel,
ProfileSetup, TextShareModal, ReceiveDialog, TransferProgress,
PeerList, PeerAvatar).

Also drops three pre-existing Uint8Array/BlobPart strictness
errors so the production build is green again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 10:49:15 +02:00

104 lines
2.7 KiB
TypeScript

import { useState, useRef, useCallback } from "react";
interface DropZoneProps {
onFilesSelected: (files: File[]) => void;
disabled?: boolean;
}
export default function DropZone({ onFilesSelected, disabled }: DropZoneProps) {
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const dragCounter = useRef(0);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
dragCounter.current++;
if (e.dataTransfer.items.length > 0) {
setIsDragging(true);
}
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragging(false);
}
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
dragCounter.current = 0;
if (disabled) return;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
onFilesSelected(files);
}
},
[onFilesSelected, disabled],
);
const handleClick = () => {
if (!disabled) {
inputRef.current?.click();
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) {
onFilesSelected(files);
}
e.target.value = "";
};
return (
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleClick}
className={`
border border-dashed rounded-sm px-6 py-8
flex flex-col items-center justify-center gap-2
transition-colors duration-fast ease-crisp cursor-pointer
${isDragging
? "border-signal bg-signal-quiet"
: disabled
? "border-paper-edge bg-paper-deep/40 cursor-not-allowed opacity-60"
: "border-paper-edge bg-paper hover:border-ink hover:bg-paper-deep/40"
}
`}
>
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-ink-muted">
{isDragging ? "Release" : "Drop"}
</span>
<p className="font-display text-xl text-ink">
{disabled
? "Select a device first"
: isDragging
? "Release to send"
: "Drop files here"}
</p>
<p className="text-xs text-ink-muted">
or click to choose
</p>
<input
ref={inputRef}
type="file"
multiple
className="hidden"
onChange={handleFileChange}
/>
</div>
);
}