feat: add window component with traffic lights and framer-motion

This commit is contained in:
M1ngdaXie
2026-03-26 14:21:07 -07:00
parent 007331e83e
commit 9f46c0697c
2 changed files with 161 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
.window {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
border-radius: var(--radius-window);
box-shadow: var(--glass-shadow);
display: flex;
flex-direction: column;
overflow: hidden;
transition: opacity 0.15s;
}
.window--unfocused {
opacity: 0.72;
}
.window-titlebar {
height: var(--titlebar-height);
display: flex;
align-items: center;
padding: 0 12px;
cursor: move;
border-bottom: 1px solid var(--glass-border);
flex-shrink: 0;
gap: 8px;
position: relative;
}
.traffic-lights {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.traffic-light {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
cursor: pointer;
position: relative;
transition: filter 0.1s;
}
.window--unfocused .traffic-light {
background: #666 !important;
}
.traffic-light--close { background: var(--accent-red); }
.traffic-light--minimize { background: var(--accent-yellow); }
.traffic-light--maximize { background: var(--accent-green); }
.traffic-lights:hover .traffic-light--close::after { content: '✕'; }
.traffic-lights:hover .traffic-light--minimize::after { content: ''; }
.traffic-lights:hover .traffic-light--maximize::after { content: '+'; }
.traffic-light::after {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 700;
color: rgba(0,0,0,0.6);
line-height: 1;
}
.window-title {
flex: 1;
text-align: center;
font-size: 13px;
font-weight: 500;
color: rgba(255,255,255,0.85);
pointer-events: none;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.window-content {
flex: 1;
overflow: auto;
color: #fff;
}

View File

@@ -0,0 +1,75 @@
import { Rnd } from 'react-rnd';
import { motion, AnimatePresence } from 'framer-motion';
import { useOS } from '../../context/WindowContext';
import type { AppConfig } from '../../types';
import './Window.css';
interface Props {
app: AppConfig;
}
export default function Window({ app }: Props) {
const { state, closeWindow, minimizeWindow, maximizeWindow, focusWindow, moveWindow, resizeWindow } = useOS();
const win = state.windows[app.id];
const isFocused = win.zIndex === state.topZ;
const isVisible = win.isOpen && !win.isMinimized;
return (
<AnimatePresence>
{isVisible && (
<motion.div
key={app.id}
style={{ position: 'fixed', zIndex: win.zIndex, top: 0, left: 0, width: 0, height: 0 }}
initial={{ opacity: 0, scale: 0.92 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.92 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
>
<Rnd
position={win.position}
size={win.size}
onDragStop={(_, d) => moveWindow(app.id, d.x, d.y)}
onResizeStop={(_, __, ref, ___, pos) => {
resizeWindow(app.id, ref.offsetWidth, ref.offsetHeight);
moveWindow(app.id, pos.x, pos.y);
}}
dragHandleClassName="window-titlebar"
minWidth={280}
minHeight={200}
bounds="window"
onMouseDown={() => focusWindow(app.id)}
>
<div
className={`window ${isFocused ? 'window--focused' : 'window--unfocused'}`}
style={{ width: '100%', height: '100%' }}
>
<div className="window-titlebar">
<div className="traffic-lights">
<button
className="traffic-light traffic-light--close"
onClick={() => closeWindow(app.id)}
title="Close"
/>
<button
className="traffic-light traffic-light--minimize"
onClick={() => minimizeWindow(app.id)}
title="Minimize"
/>
<button
className="traffic-light traffic-light--maximize"
onClick={() => maximizeWindow(app.id, window.innerWidth, window.innerHeight)}
title="Maximize"
/>
</div>
<span className="window-title">{app.title}</span>
</div>
<div className="window-content">
<app.component />
</div>
</div>
</Rnd>
</motion.div>
)}
</AnimatePresence>
);
}