Spaces:
Sleeping
Sleeping
File size: 7,452 Bytes
aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c 080f0c0 aaa634c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | import React, { useState, useRef, useEffect } from 'react';
interface WindowShellProps {
id: string;
title: string;
isOpen: boolean;
isMinimized: boolean;
onClose: () => void;
onMinimize: () => void;
activeWindow: string;
setActiveWindow: (id: string) => void;
children: React.ReactNode;
icon?: string;
defaultWidth?: string;
defaultHeight?: string;
defaultX?: number;
defaultY?: number;
titleBarColor?: string;
isMobile?: boolean;
}
export default function WindowShell({
id,
title,
isOpen,
isMinimized,
onClose,
onMinimize,
activeWindow,
setActiveWindow,
children,
icon = 'terminal',
defaultWidth = '700px',
defaultHeight = '600px',
defaultX,
defaultY,
titleBarColor = '#0ea5e9',
isMobile = false,
}: WindowShellProps) {
// Helper to parse default dimensions (e.g. '700px') into integer values
const parseDimension = (val: string): number => {
const num = parseInt(val, 10);
return isNaN(num) ? 500 : num;
};
const [position, setPosition] = useState({ x: defaultX ?? 0, y: defaultY ?? 0 });
const [dimensions, setDimensions] = useState({
width: parseDimension(defaultWidth),
height: parseDimension(defaultHeight)
});
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const dragStart = useRef({ x: 0, y: 0 });
const resizeStart = useRef({ width: 0, height: 0, x: 0, y: 0 });
const windowRef = useRef<HTMLDivElement>(null);
const isActive = activeWindow === id;
useEffect(() => {
// If not set, randomize offset slightly to feel "tossed on the desk"
if (defaultX === undefined && defaultY === undefined) {
const randomX = Math.floor(Math.random() * 40) - 20;
const randomY = Math.floor(Math.random() * 40) - 20;
setPosition({ x: randomX, y: randomY });
}
}, [defaultX, defaultY]);
const handleMouseDown = (e: React.MouseEvent) => {
if (isMobile) return;
// Only drag on left click and not on control buttons or resize handles
if (e.button !== 0) return;
const target = e.target as HTMLElement;
if (target.closest('.window-control-btn') || target.closest('.window-resize-handle')) return;
setActiveWindow(id);
setIsDragging(true);
dragStart.current = {
x: e.clientX - position.x,
y: e.clientY - position.y
};
e.preventDefault();
};
const handleResizeMouseDown = (e: React.MouseEvent) => {
if (isMobile) return;
if (e.button !== 0) return;
e.stopPropagation();
e.preventDefault();
setActiveWindow(id);
setIsResizing(true);
resizeStart.current = {
width: dimensions.width,
height: dimensions.height,
x: e.clientX,
y: e.clientY
};
};
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
const newX = e.clientX - dragStart.current.x;
const newY = e.clientY - dragStart.current.y;
setPosition({ x: newX, y: newY });
} else if (isResizing) {
const deltaX = e.clientX - resizeStart.current.x;
const deltaY = e.clientY - resizeStart.current.y;
// Enforce minimum dimensions (350x250 px)
const newWidth = Math.max(350, resizeStart.current.width + deltaX);
const newHeight = Math.max(250, resizeStart.current.height + deltaY);
setDimensions({
width: newWidth,
height: newHeight
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
setIsResizing(false);
};
if (isDragging || isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, isResizing, isMobile]);
if (!isOpen || isMinimized) return null;
return (
<div
ref={windowRef}
onMouseDown={() => setActiveWindow(id)}
style={isMobile ? {
zIndex: isActive ? 40 : 30,
} : {
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px))`,
zIndex: isActive ? 40 : 30,
}}
className={`absolute bg-paper-white border-[4px] flex flex-col overflow-hidden window-shadow transition-all ${
isMobile
? 'top-0 left-0 w-full h-[calc(100vh-56px)] max-w-none max-h-none border-t-0 border-x-0'
: 'top-1/2 left-1/2 max-w-[98vw] max-h-[90vh] border-ink-black'
} ${
isActive ? 'border-ink-black shadow-[8px_8px_0px_0px_rgba(30,41,59,1)]' : 'border-ink-black opacity-95 shadow-[4px_4px_0px_0px_rgba(30,41,59,1)]'
}`}
>
{/* Title Bar */}
<div
onMouseDown={handleMouseDown}
style={{ backgroundColor: titleBarColor }}
className="border-b-[4px] border-ink-black px-4 py-2 flex justify-between items-center text-white shrink-0 cursor-move select-none"
>
<div className="flex items-center gap-2">
{icon && (
<span className="material-symbols-outlined select-none" style={{ fontVariationSettings: "'FILL' 1" }}>
{icon}
</span>
)}
<span className="font-window-title text-sm md:text-base font-bold tracking-wide select-none">{title}</span>
</div>
<div className="flex gap-1.5">
{!isMobile && (
<button
onClick={(e) => {
e.stopPropagation();
onMinimize();
}}
className="window-control-btn w-7 h-7 bg-white text-ink-black border-[3px] border-ink-black flex items-center justify-center hover:bg-yellow-300 active:translate-y-0.5 active:translate-x-0.5 shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] active:shadow-none transition-all cursor-pointer"
>
<span className="material-symbols-outlined text-[14px] font-bold">minimize</span>
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="window-control-btn w-7 h-7 bg-highlight-pink text-white border-[3px] border-ink-black flex items-center justify-center hover:bg-rose-600 active:translate-y-0.5 active:translate-x-0.5 shadow-[2px_2px_0px_0px_rgba(30,41,59,1)] active:shadow-none transition-all cursor-pointer"
>
<span className="material-symbols-outlined text-[14px] font-bold">close</span>
</button>
</div>
</div>
{/* Window Content */}
<div className="flex-1 overflow-hidden relative bg-paper-white flex flex-col">
{children}
</div>
{/* Resize Handle */}
{!isMobile && (
<div
onMouseDown={handleResizeMouseDown}
className="window-resize-handle absolute bottom-0 right-0 w-4 h-4 cursor-se-resize flex items-end justify-end p-0.5 select-none z-[50]"
>
<svg width="10" height="10" viewBox="0 0 10 10" className="text-ink-black opacity-60">
<line x1="8" y1="2" x2="2" y2="8" stroke="currentColor" strokeWidth="1.5" />
<line x1="8" y1="5" x2="5" y2="8" stroke="currentColor" strokeWidth="1.5" />
</svg>
</div>
)}
</div>
);
}
|