Spaces:
Running
Running
| 'use client'; | |
| import React, { ReactNode, useState, useEffect } from 'react'; | |
| import { Rnd } from 'react-rnd'; | |
| interface WindowProps { | |
| id: string; | |
| title: string; | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onMinimize?: () => void; | |
| onMaximize?: () => void; | |
| onFocus?: () => void; | |
| children: ReactNode; | |
| width?: number | string; | |
| height?: number | string; | |
| x?: number; | |
| y?: number; | |
| zIndex?: number; | |
| resizable?: boolean; | |
| className?: string; | |
| headerClassName?: string; | |
| contentClassName?: string; | |
| darkMode?: boolean; | |
| } | |
| const Window: React.FC<WindowProps> = ({ | |
| id, | |
| title, | |
| isOpen, | |
| onClose, | |
| onMinimize, | |
| onMaximize, | |
| onFocus, | |
| children, | |
| width = 800, | |
| height = 600, | |
| x = 100, | |
| y = 100, | |
| zIndex = 1000, | |
| resizable = true, | |
| className = '', | |
| headerClassName = '', | |
| contentClassName = '', | |
| darkMode = false, | |
| }) => { | |
| const [isMaximized, setIsMaximized] = React.useState(false); | |
| const [previousSize, setPreviousSize] = React.useState({ width, height, x, y: Math.max(y, 32) }); | |
| const [isMobile, setIsMobile] = React.useState(false); | |
| const [isDraggingOrResizing, setIsDraggingOrResizing] = React.useState(false); | |
| // Use refs to track current position/size without causing re-renders | |
| const currentPositionRef = React.useRef({ x, y: Math.max(y, 32) }); | |
| const currentSizeRef = React.useRef({ width, height }); | |
| // Detect mobile device - update threshold to better match small screens | |
| useEffect(() => { | |
| const checkMobile = () => { | |
| setIsMobile(window.innerWidth <= 768); | |
| }; | |
| checkMobile(); | |
| window.addEventListener('resize', checkMobile); | |
| return () => window.removeEventListener('resize', checkMobile); | |
| }, []); | |
| if (!isOpen) return null; | |
| // Define window classes | |
| const windowClass = darkMode ? 'bg-gray-900 border-gray-700' : 'bg-[#f5f5f5] border-gray-300/50'; | |
| const headerClass = darkMode ? 'bg-gray-800 border-gray-700' : 'macos-window-header'; | |
| // Bring window to front function - define it early | |
| const bringToFront = () => { | |
| if (onFocus) { | |
| onFocus(); | |
| } | |
| }; | |
| // On mobile, render with reduced size instead of full screen | |
| if (isMobile) { | |
| return ( | |
| <div | |
| className={`fixed flex flex-col overflow-hidden ${windowClass} ${className} rounded-lg shadow-2xl`} | |
| style={{ | |
| top: '60px', | |
| left: '10px', | |
| right: '10px', | |
| bottom: '80px', | |
| zIndex: zIndex || 1000, | |
| maxHeight: 'calc(100vh - 140px)' | |
| }} | |
| onMouseDown={bringToFront} | |
| onTouchStart={bringToFront} | |
| > | |
| <div | |
| className={`h-12 flex items-center px-3 space-x-2 border-b ${headerClass} ${headerClassName}`} | |
| > | |
| <div className="flex space-x-2 group relative z-10"> | |
| <button | |
| className="traffic-light traffic-close w-6 h-6 sm:w-4 sm:h-4" | |
| onClick={onClose} | |
| aria-label="Close" | |
| /> | |
| <button | |
| className="traffic-light traffic-min w-6 h-6 sm:w-4 sm:h-4" | |
| onClick={onMinimize} | |
| aria-label="Minimize" | |
| /> | |
| <button | |
| className="traffic-light traffic-max w-6 h-6 sm:w-4 sm:h-4" | |
| onClick={onMaximize} | |
| aria-label="Maximize" | |
| /> | |
| </div> | |
| <span className="font-semibold text-gray-700 flex-1 text-center text-sm truncate px-2">{title}</span> | |
| </div> | |
| <div className="flex-1 overflow-auto">{children}</div> | |
| </div> | |
| ); | |
| } | |
| const handleMaximize = () => { | |
| if (!isMaximized) { | |
| setPreviousSize({ | |
| width: currentSizeRef.current.width, | |
| height: currentSizeRef.current.height, | |
| x: currentPositionRef.current.x, | |
| y: currentPositionRef.current.y | |
| }); | |
| setIsMaximized(true); | |
| } else { | |
| setIsMaximized(false); | |
| } | |
| if (onMaximize) onMaximize(); | |
| }; | |
| // Calculate Rnd props based on state | |
| const rndProps = isMaximized ? { | |
| position: { x: 0, y: 4 }, // 32px for TopBar offset (h-8 = 32px) | |
| size: { | |
| width: typeof window !== 'undefined' ? window.innerWidth : '100vw', | |
| height: typeof window !== 'undefined' ? window.innerHeight - 120 : 'calc(100vh - 120px)' | |
| }, | |
| disableDragging: true, | |
| enableResizing: false | |
| } : { | |
| // Use default for uncontrolled mode - only sets initial position | |
| default: { | |
| x: currentPositionRef.current.x, | |
| y: currentPositionRef.current.y, | |
| width: currentSizeRef.current.width, | |
| height: currentSizeRef.current.height | |
| }, | |
| disableDragging: false, | |
| enableResizing: resizable | |
| }; | |
| return ( | |
| <Rnd | |
| {...rndProps} | |
| minWidth={400} | |
| minHeight={300} | |
| dragHandleClassName="window-drag-handle" | |
| enableResizing={!isMaximized && resizable} | |
| disableDragging={isMaximized} | |
| onMouseDown={bringToFront} | |
| onDragStart={() => { | |
| setIsDraggingOrResizing(true); | |
| bringToFront(); | |
| }} | |
| onDrag={(e, d) => { | |
| // Constrain dragging to not go above TopBar | |
| if (d.y < 4) { | |
| // Update position ref to boundary | |
| currentPositionRef.current = { x: d.x, y: 4 }; | |
| return false; | |
| } | |
| // Update position ref during drag | |
| currentPositionRef.current = { x: d.x, y: d.y }; | |
| }} | |
| onDragStop={(e, d) => { | |
| setIsDraggingOrResizing(false); | |
| if (!isMaximized) { | |
| // Ensure window doesn't go above TopBar and update position ref | |
| const constrainedY = Math.max(d.y, 4); | |
| currentPositionRef.current = { x: d.x, y: constrainedY }; | |
| } | |
| }} | |
| onResizeStart={() => { | |
| setIsDraggingOrResizing(true); | |
| bringToFront(); | |
| }} | |
| onResizeStop={(e, direction, ref, delta, position) => { | |
| setIsDraggingOrResizing(false); | |
| if (!isMaximized) { | |
| // Update size and position refs | |
| currentSizeRef.current = { | |
| width: ref.offsetWidth, | |
| height: ref.offsetHeight, | |
| }; | |
| currentPositionRef.current = position; | |
| } | |
| }} | |
| className="absolute" | |
| style={{ zIndex: zIndex || 1000 }} | |
| > | |
| <div | |
| className={`h-full macos-window flex flex-col ${className} ${windowClass}`} | |
| onMouseDown={bringToFront} | |
| > | |
| <div | |
| className={`window-drag-handle h-10 flex items-center px-4 space-x-4 border-b cursor-move ${headerClass} ${headerClassName}`} | |
| onDoubleClick={handleMaximize} | |
| > | |
| <div className="flex space-x-2 group"> | |
| <div className="traffic-light traffic-close" onClick={onClose} /> | |
| <div className="traffic-light traffic-min" onClick={onMinimize} /> | |
| <div className="traffic-light traffic-max" onClick={handleMaximize} /> | |
| </div> | |
| <span className="font-semibold text-gray-700 flex-1 text-center pr-16 text-sm select-none">{title}</span> | |
| </div> | |
| <div className={`flex-1 overflow-auto ${darkMode ? 'bg-gray-900/95 text-white' : 'bg-white/90'} backdrop-blur-sm ${contentClassName || ''}`} style={{ height: 'calc(100% - 2.5rem)' }}> | |
| {children} | |
| </div> | |
| </div> | |
| </Rnd> | |
| ); | |
| }; | |
| export default Window; |