feat: add window component with traffic lights and framer-motion
This commit is contained in:
86
src/components/Window/Window.css
Normal file
86
src/components/Window/Window.css
Normal 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;
|
||||
}
|
||||
75
src/components/Window/Window.tsx
Normal file
75
src/components/Window/Window.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user