({
isOpen,
onClose,
fileName,
children,
className,
}: MobileFileViewerProps)
| 21 | * - Content area is scrollable with pinch-to-zoom enabled on images |
| 22 | */ |
| 23 | export function MobileFileViewer({ |
| 24 | isOpen, |
| 25 | onClose, |
| 26 | fileName, |
| 27 | children, |
| 28 | className, |
| 29 | }: MobileFileViewerProps) { |
| 30 | const swipeHandlers = useTouchGesture({ onSwipeDown: onClose, threshold: 80 }); |
| 31 | |
| 32 | // Lock body scroll and close on Escape |
| 33 | useEffect(() => { |
| 34 | if (isOpen) { |
| 35 | document.body.style.overflow = "hidden"; |
| 36 | } else { |
| 37 | document.body.style.overflow = ""; |
| 38 | } |
| 39 | return () => { |
| 40 | document.body.style.overflow = ""; |
| 41 | }; |
| 42 | }, [isOpen]); |
| 43 | |
| 44 | useEffect(() => { |
| 45 | if (!isOpen) return; |
| 46 | const onKey = (e: KeyboardEvent) => { |
| 47 | if (e.key === "Escape") onClose(); |
| 48 | }; |
| 49 | document.addEventListener("keydown", onKey); |
| 50 | return () => document.removeEventListener("keydown", onKey); |
| 51 | }, [isOpen, onClose]); |
| 52 | |
| 53 | return ( |
| 54 | <div |
| 55 | className={cn( |
| 56 | "fixed inset-0 z-50 bg-surface-950 flex flex-col", |
| 57 | "transition-transform duration-300 ease-out", |
| 58 | isOpen ? "translate-y-0" : "translate-y-full", |
| 59 | className |
| 60 | )} |
| 61 | role="dialog" |
| 62 | aria-modal="true" |
| 63 | aria-label={fileName ?? "File viewer"} |
| 64 | > |
| 65 | {/* Header — swipe-down handle zone */} |
| 66 | <div |
| 67 | className="flex items-center gap-2 px-2 border-b border-surface-800 bg-surface-900/80 backdrop-blur-sm h-[52px] flex-shrink-0" |
| 68 | {...swipeHandlers} |
| 69 | > |
| 70 | {/* Drag handle */} |
| 71 | <div className="absolute left-1/2 -translate-x-1/2 top-2 w-10 h-1 bg-surface-600 rounded-full" /> |
| 72 | |
| 73 | <button |
| 74 | onClick={onClose} |
| 75 | className="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-md text-surface-400 hover:text-surface-100 active:bg-surface-800 transition-colors" |
| 76 | aria-label="Close file viewer" |
| 77 | > |
| 78 | <X className="w-5 h-5" /> |
| 79 | </button> |
| 80 |
nothing calls this directly
no test coverage detected