From 9f46c0697c286cd5e1363f403fce5f5924d63f2b Mon Sep 17 00:00:00 2001 From: M1ngdaXie <156019134+M1ngdaXie@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:21:07 -0700 Subject: [PATCH] feat: add window component with traffic lights and framer-motion --- src/components/Window/Window.css | 86 ++++++++++++++++++++++++++++++++ src/components/Window/Window.tsx | 75 ++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/components/Window/Window.css create mode 100644 src/components/Window/Window.tsx diff --git a/src/components/Window/Window.css b/src/components/Window/Window.css new file mode 100644 index 0000000..5ad5da7 --- /dev/null +++ b/src/components/Window/Window.css @@ -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; +} diff --git a/src/components/Window/Window.tsx b/src/components/Window/Window.tsx new file mode 100644 index 0000000..b2a6b6a --- /dev/null +++ b/src/components/Window/Window.tsx @@ -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 ( + + {isVisible && ( + + 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)} + > +
+
+
+
+ {app.title} +
+
+ +
+
+
+
+ )} +
+ ); +}