UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

162 lines (161 loc) 7.43 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { FileIcon, UpdateIcon, UploadIcon } from "@radix-ui/react-icons"; import { useCallback, useState } from "react"; import { HEIGHTS } from "../../lib/layout-constants.js"; import { cn } from "../../lib/utils.js"; /** * DropZone - Standardized drag & drop component with consistent styling * * Features: * - Beautiful dashed border styling with CSS variables * - Radix icons for consistent theming * - Drag states with visual feedback * - File validation and error handling * - Fully accessible with keyboard support * - Customizable content and styling * * @example * ```tsx * // Basic file upload * <DropZone * accept=".json,.txt" * onFileSelect={(file) => processFile(file)} * title="Import Backup" * description="Drag & drop your backup file here" * /> * * // Custom data drop zone * <DropZone * icon={<TokensIcon />} * title="Add Transaction Data" * description="Drop your transaction files" * height={200} * /> * ``` */ export function DropZone({ onDrop, onFileSelect, accept, multiple = false, disabled = false, loading = false, className = "", height = HEIGHTS.CARD_MIN, icon, title = "Upload Files", description = "Drag & drop or click to select", loadingText = "Processing...", dragActiveText = "Drop files here", dragActiveDescription = "Release to upload", maxSize, onError, size = "default", }) { const [dragActive, setDragActive] = useState(false); const validateFile = useCallback((file) => { if (maxSize && file.size > maxSize) { return `File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB`; } if (accept) { const acceptedTypes = accept.split(",").map((type) => type.trim()); const isAccepted = acceptedTypes.some((type) => { if (type.startsWith(".")) { return file.name.toLowerCase().endsWith(type.toLowerCase()); } return file.type === type; }); if (!isAccepted) { return `Invalid file type. Accepted types: ${accept}`; } } return null; }, [accept, maxSize]); const handleFiles = useCallback((fileList) => { if (disabled || loading) return; const filesArray = Array.from(fileList); // Validate files for (const file of filesArray) { const error = validateFile(file); if (error) { onError?.(error); return; } } // Handle files if (onDrop) { onDrop(fileList); } else if (onFileSelect && filesArray.length > 0) { const firstFile = filesArray[0]; if (firstFile) { onFileSelect(firstFile); } } }, [disabled, loading, validateFile, onDrop, onFileSelect, onError]); const handleFileSelect = useCallback((e) => { const files = e.target.files; if (files) { handleFiles(files); } // Reset input value so same file can be selected again e.target.value = ""; }, [handleFiles]); const handleDrop = useCallback((e) => { e.preventDefault(); setDragActive(false); if (disabled || loading) return; const files = e.dataTransfer.files; if (files) { handleFiles(files); } }, [disabled, loading, handleFiles]); const handleDragOver = useCallback((e) => { e.preventDefault(); }, []); const handleDragEnter = useCallback((e) => { e.preventDefault(); if (!disabled && !loading) { setDragActive(true); } }, [disabled, loading]); const handleDragLeave = useCallback((e) => { e.preventDefault(); // Only hide drag state if leaving the entire drop zone if (!e.currentTarget.contains(e.relatedTarget)) { setDragActive(false); } }, []); const handleKeyDown = useCallback((e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); // Trigger file input click const input = e.currentTarget.querySelector('input[type="file"]'); if (input && !disabled && !loading) { input.click(); } } }, [disabled, loading]); const isDisabled = disabled || loading; const currentIcon = loading ? (_jsx(UpdateIcon, { className: "animate-spin" })) : (icon || _jsx(UploadIcon, {})); const currentTitle = loading ? loadingText : dragActive ? dragActiveText : title; const currentDescription = loading ? "" : dragActive ? dragActiveDescription : description; const iconSizeClasses = { sm: "h-8 w-8", default: "h-12 w-12", lg: "h-16 w-16", }; const textSizeClasses = { sm: "text-sm", default: "text-base", lg: "text-lg", }; const descriptionSizeClasses = { sm: "text-xs", default: "text-sm", lg: "text-base", }; return (_jsxs("div", { className: cn("relative grid place-items-center rounded-lg border-2 border-dashed transition-all duration-200", dragActive ? "border-primary bg-primary/10 shadow-lg shadow-primary/25" : "border-muted-foreground/25 bg-muted/50 hover:border-muted-foreground/50", isDisabled && "cursor-not-allowed opacity-50", !isDisabled && "cursor-pointer", className), style: { height: typeof height === "number" ? `${height}px` : height, }, tabIndex: isDisabled ? -1 : 0, // biome-ignore lint/a11y/useSemanticElements: Drop zone requires complex drag/drop handling that native buttons don't support role: "button", "aria-label": `${title}. ${description}`, onDrop: handleDrop, onDragOver: handleDragOver, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onKeyDown: handleKeyDown, children: [_jsx("input", { type: "file", accept: accept, multiple: multiple, onChange: handleFileSelect, disabled: isDisabled, className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed" }), _jsxs("div", { className: "pointer-events-none flex flex-col items-center gap-3", children: [_jsx("div", { className: cn(iconSizeClasses[size], dragActive || loading ? "text-primary" : "text-muted-foreground"), children: currentIcon }), _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx("p", { className: cn("font-medium", textSizeClasses[size], dragActive || loading ? "text-primary" : "text-foreground"), children: currentTitle }), currentDescription && (_jsx("p", { className: cn(descriptionSizeClasses[size], "text-muted-foreground"), children: currentDescription }))] }), accept && !loading && (_jsx("p", { className: "text-xs text-muted-foreground", children: accept }))] })] })); } // Preset variants for common use cases export const FileDropZone = (props) => (_jsx(DropZone, { icon: _jsx(FileIcon, {}), title: "Select Files", description: "Drag & drop or click to browse", ...props })); export const BackupDropZone = (props) => (_jsx(DropZone, { icon: _jsx(UploadIcon, {}), title: "Import Backup", description: "Drag & drop your backup file", accept: ".json,.txt", ...props }));