|
| 1 | +import React, { useEffect, useState } from 'react'; |
| 2 | +import { motion, AnimatePresence } from 'framer-motion'; |
| 3 | +import { XIcon, InfoIcon } from '@phosphor-icons/react'; |
| 4 | +import { useSidePanel } from '../context/SidePanelContext'; |
| 5 | +import GenerativeArt from './GenerativeArt'; |
| 6 | + |
| 7 | +const BrutalistSidePanel = () => { |
| 8 | + const { |
| 9 | + isOpen, |
| 10 | + closeSidePanel, |
| 11 | + panelTitle, |
| 12 | + panelContent, |
| 13 | + panelWidth, |
| 14 | + setPanelWidth, |
| 15 | + } = useSidePanel(); |
| 16 | + const [isResizing, setIsResizing] = useState(false); |
| 17 | + |
| 18 | + // Close on escape key |
| 19 | + useEffect(() => { |
| 20 | + const handleKeyDown = (event) => { |
| 21 | + if (event.key === 'Escape' && isOpen) { |
| 22 | + closeSidePanel(); |
| 23 | + } |
| 24 | + }; |
| 25 | + window.addEventListener('keydown', handleKeyDown); |
| 26 | + return () => window.removeEventListener('keydown', handleKeyDown); |
| 27 | + }, [isOpen, closeSidePanel]); |
| 28 | + |
| 29 | + // Handle resizing |
| 30 | + useEffect(() => { |
| 31 | + const handleMouseMove = (e) => { |
| 32 | + if (!isResizing) return; |
| 33 | + const newWidth = window.innerWidth - e.clientX; |
| 34 | + // Constrain width: min 300px, max 90% of screen |
| 35 | + if (newWidth > 300 && newWidth < window.innerWidth * 0.9) { |
| 36 | + setPanelWidth(newWidth); |
| 37 | + } |
| 38 | + }; |
| 39 | + |
| 40 | + const handleMouseUp = () => { |
| 41 | + setIsResizing(false); |
| 42 | + }; |
| 43 | + |
| 44 | + if (isResizing) { |
| 45 | + window.addEventListener('mousemove', handleMouseMove); |
| 46 | + window.addEventListener('mouseup', handleMouseUp); |
| 47 | + document.body.style.cursor = 'ew-resize'; |
| 48 | + document.body.style.userSelect = 'none'; |
| 49 | + } else { |
| 50 | + document.body.style.cursor = 'default'; |
| 51 | + document.body.style.userSelect = 'auto'; |
| 52 | + } |
| 53 | + |
| 54 | + return () => { |
| 55 | + window.removeEventListener('mousemove', handleMouseMove); |
| 56 | + window.removeEventListener('mouseup', handleMouseUp); |
| 57 | + document.body.style.cursor = 'default'; |
| 58 | + document.body.style.userSelect = 'auto'; |
| 59 | + }; |
| 60 | + }, [isResizing, setPanelWidth]); |
| 61 | + |
| 62 | + const variants = { |
| 63 | + open: { x: 0, opacity: 1 }, |
| 64 | + closed: { x: '100%', opacity: 1 }, |
| 65 | + }; |
| 66 | + |
| 67 | + return ( |
| 68 | + <AnimatePresence> |
| 69 | + {isOpen && ( |
| 70 | + <> |
| 71 | + {/* Backdrop with blur */} |
| 72 | + <motion.div |
| 73 | + initial={{ opacity: 0 }} |
| 74 | + animate={{ opacity: 1 }} |
| 75 | + exit={{ opacity: 0 }} |
| 76 | + onClick={closeSidePanel} |
| 77 | + className="fixed inset-0 bg-black/80 backdrop-blur-md z-[100]" |
| 78 | + /> |
| 79 | + |
| 80 | + <motion.div |
| 81 | + initial="closed" |
| 82 | + animate="open" |
| 83 | + exit="closed" |
| 84 | + variants={variants} |
| 85 | + transition={{ type: 'spring', stiffness: 300, damping: 30 }} |
| 86 | + style={{ width: panelWidth }} |
| 87 | + className="fixed top-0 right-0 h-full bg-[#050505] border-l border-white/10 shadow-2xl z-[110] flex flex-col overflow-hidden" |
| 88 | + > |
| 89 | + {/* Background Art */} |
| 90 | + <div className="absolute inset-0 opacity-[0.03] pointer-events-none grayscale"> |
| 91 | + <GenerativeArt seed={panelTitle} className="w-full h-full" /> |
| 92 | + </div> |
| 93 | + |
| 94 | + {/* Resize Handle */} |
| 95 | + <div |
| 96 | + onMouseDown={(e) => { |
| 97 | + setIsResizing(true); |
| 98 | + e.preventDefault(); |
| 99 | + }} |
| 100 | + className="absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize hover:bg-emerald-500/50 transition-colors z-[120]" |
| 101 | + title="DRAG_TO_RESIZE" |
| 102 | + /> |
| 103 | + |
| 104 | + {/* Header */} |
| 105 | + <div className="relative z-10 flex items-center justify-between p-8 border-b border-white/10 bg-black/40"> |
| 106 | + <div className="flex flex-col gap-1 min-w-0 pr-4"> |
| 107 | + <span className="font-mono text-[9px] text-emerald-500 uppercase tracking-[0.3em] font-bold"> |
| 108 | + System_Panel_Node |
| 109 | + </span> |
| 110 | + <h2 className="text-2xl font-black font-mono tracking-tighter text-white uppercase italic leading-none pr-2"> |
| 111 | + {panelTitle} |
| 112 | + </h2> |
| 113 | + </div> |
| 114 | + <button |
| 115 | + onClick={closeSidePanel} |
| 116 | + className="p-2 text-gray-500 hover:text-white transition-colors flex-shrink-0" |
| 117 | + > |
| 118 | + <XIcon size={24} weight="bold" /> |
| 119 | + </button> |
| 120 | + </div> |
| 121 | + |
| 122 | + {/* Content Area */} |
| 123 | + <div className="relative z-10 flex-1 overflow-y-auto p-8 custom-scrollbar text-white"> |
| 124 | + <div className="space-y-8"> |
| 125 | + {panelContent} |
| 126 | + </div> |
| 127 | + </div> |
| 128 | + |
| 129 | + {/* Panel Footer */} |
| 130 | + <div className="relative z-10 p-6 border-t border-white/10 bg-black/40 flex items-center justify-between"> |
| 131 | + <div className="flex items-center gap-3 text-gray-600 font-mono text-[9px] uppercase tracking-[0.2em]"> |
| 132 | + <InfoIcon weight="fill" size={14} className="text-emerald-500/50" /> |
| 133 | + <span>Buffer_Active // {Math.round(panelWidth)}PX</span> |
| 134 | + </div> |
| 135 | + <div className="flex gap-2"> |
| 136 | + <div className="w-1.5 h-1.5 bg-emerald-500 animate-pulse rounded-full" /> |
| 137 | + <div className="w-1.5 h-1.5 bg-white/10 rounded-full" /> |
| 138 | + </div> |
| 139 | + </div> |
| 140 | + </motion.div> |
| 141 | + </> |
| 142 | + )} |
| 143 | + </AnimatePresence> |
| 144 | + ); |
| 145 | +}; |
| 146 | + |
| 147 | +export default BrutalistSidePanel; |
0 commit comments