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>
104 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|