-
-
Save shricodev/e8ec4a2072f15aa14fef9cfde65ec439 to your computer and use it in GitHub Desktop.
OpenAI ChatGPT 5.1 UI Test (Windows 11) - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { | |
| ReactNode, | |
| useCallback, | |
| useEffect, | |
| useRef, | |
| useState, | |
| MouseEvent as ReactMouseEvent, | |
| CSSProperties, | |
| } from "react"; | |
| interface AppWindowProps { | |
| appId: string; | |
| title: string; | |
| iconGlyph: string; | |
| x: number; | |
| y: number; | |
| width: number; | |
| height: number; | |
| isActive: boolean; | |
| isMaximized: boolean; | |
| zIndex: number; | |
| onClose: () => void; | |
| onMinimize: () => void; | |
| onToggleMaximize: () => void; | |
| onFocus: () => void; | |
| onMove: (x: number, y: number) => void; | |
| children: ReactNode; | |
| } | |
| export const AppWindow: React.FC<AppWindowProps> = ({ | |
| title, | |
| iconGlyph, | |
| x, | |
| y, | |
| width, | |
| height, | |
| isActive, | |
| isMaximized, | |
| zIndex, | |
| onClose, | |
| onMinimize, | |
| onToggleMaximize, | |
| onFocus, | |
| onMove, | |
| children, | |
| }) => { | |
| const [dragging, setDragging] = useState(false); | |
| const dragOffset = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); | |
| const handleMouseDownTitle = (event: ReactMouseEvent) => { | |
| if (isMaximized) return; | |
| event.stopPropagation(); | |
| onFocus(); | |
| setDragging(true); | |
| dragOffset.current = { | |
| x: event.clientX - x, | |
| y: event.clientY - y, | |
| }; | |
| }; | |
| const handleMouseDownWindow = (event: ReactMouseEvent) => { | |
| event.stopPropagation(); | |
| onFocus(); | |
| }; | |
| const handleMouseUp = useCallback(() => { | |
| setDragging(false); | |
| }, []); | |
| const handleMouseMove = useCallback( | |
| (event: MouseEvent) => { | |
| if (!dragging || isMaximized) return; | |
| const viewportWidth = window.innerWidth; | |
| const viewportHeight = window.innerHeight; | |
| const taskbarHeight = 56; | |
| const nextX = event.clientX - dragOffset.current.x; | |
| const nextY = event.clientY - dragOffset.current.y; | |
| const clampedX = Math.min( | |
| Math.max(nextX, 8), | |
| viewportWidth - width - 8 | |
| ); | |
| const clampedY = Math.min( | |
| Math.max(nextY, 8), | |
| viewportHeight - taskbarHeight - 40 | |
| ); | |
| onMove(clampedX, clampedY); | |
| }, | |
| [dragging, isMaximized, onMove, width] | |
| ); | |
| useEffect(() => { | |
| if (!dragging) return; | |
| window.addEventListener("mousemove", handleMouseMove); | |
| window.addEventListener("mouseup", handleMouseUp); | |
| return () => { | |
| window.removeEventListener("mousemove", handleMouseMove); | |
| window.removeEventListener("mouseup", handleMouseUp); | |
| }; | |
| }, [dragging, handleMouseMove, handleMouseUp]); | |
| const style: CSSProperties = isMaximized | |
| ? { | |
| left: 8, | |
| top: 8, | |
| right: 8, | |
| bottom: 64, | |
| width: "auto", | |
| height: "auto", | |
| } | |
| : { | |
| left: x, | |
| top: y, | |
| width, | |
| height, | |
| }; | |
| return ( | |
| <div | |
| className={`pointer-events-auto absolute overflow-hidden rounded-2xl border ${ | |
| isActive | |
| ? "border-sky-400/80 shadow-[0_18px_55px_rgba(15,23,42,0.9)]" | |
| : "border-slate-600/60 shadow-[0_16px_45px_rgba(15,23,42,0.7)]" | |
| } bg-slate-900/80 backdrop-blur-2xl`} | |
| style={{ ...style, zIndex }} | |
| onMouseDown={handleMouseDownWindow} | |
| > | |
| <div | |
| className={`flex items-center justify-between gap-2 border-b border-slate-700/70 px-3 py-1.5 ${ | |
| isActive ? "bg-slate-900/80" : "bg-slate-900/60" | |
| }`} | |
| onMouseDown={handleMouseDownTitle} | |
| > | |
| <div className="flex items-center gap-2 text-xs text-slate-100"> | |
| <span aria-hidden>{iconGlyph}</span> | |
| <span className="truncate text-[11px]">{title}</span> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <button | |
| type="button" | |
| onClick={(event) => { | |
| event.stopPropagation(); | |
| onMinimize(); | |
| }} | |
| className="flex h-6 w-6 items-center justify-center rounded-full text-xs text-slate-200 hover:bg-slate-50/10" | |
| > | |
| <span aria-hidden>─</span> | |
| <span className="sr-only">Minimize</span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={(event) => { | |
| event.stopPropagation(); | |
| onToggleMaximize(); | |
| }} | |
| className="flex h-6 w-6 items-center justify-center rounded-full text-[10px] text-slate-200 hover:bg-slate-50/10" | |
| > | |
| <span aria-hidden>{isMaximized ? "▢" : "□"}</span> | |
| <span className="sr-only"> | |
| {isMaximized ? "Restore" : "Maximize"} | |
| </span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={(event) => { | |
| event.stopPropagation(); | |
| onClose(); | |
| }} | |
| className="flex h-6 w-6 items-center justify-center rounded-full text-xs text-slate-100 hover:bg-rose-500/90 hover:text-white" | |
| > | |
| <span aria-hidden>×</span> | |
| <span className="sr-only">Close</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex h-[calc(100%-2.25rem)] flex-col overflow-hidden"> | |
| {children} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState } from "react"; | |
| type Operator = "+" | "-" | "*" | "/" | null; | |
| export const CalculatorApp: React.FC = () => { | |
| const [display, setDisplay] = useState("0"); | |
| const [accumulator, setAccumulator] = useState<number | null>(null); | |
| const [pendingOp, setPendingOp] = useState<Operator>(null); | |
| const [overwrite, setOverwrite] = useState(false); | |
| const inputDigit = (digit: string) => { | |
| setDisplay((current) => { | |
| if (overwrite || current === "0") { | |
| setOverwrite(false); | |
| return digit; | |
| } | |
| if (current.length >= 12) return current; | |
| return current + digit; | |
| }); | |
| }; | |
| const inputDot = () => { | |
| setDisplay((current) => { | |
| if (overwrite) { | |
| setOverwrite(false); | |
| return "0."; | |
| } | |
| if (current.includes(".")) return current; | |
| return `${current}.`; | |
| }); | |
| }; | |
| const clearAll = () => { | |
| setDisplay("0"); | |
| setAccumulator(null); | |
| setPendingOp(null); | |
| setOverwrite(false); | |
| }; | |
| const applyOperator = (a: number, b: number, op: Operator): number => { | |
| switch (op) { | |
| case "+": | |
| return a + b; | |
| case "-": | |
| return a - b; | |
| case "*": | |
| return a * b; | |
| case "/": | |
| return b === 0 ? 0 : a / b; | |
| default: | |
| return b; | |
| } | |
| }; | |
| const formatNumber = (value: number): string => { | |
| if (!Number.isFinite(value)) return "0"; | |
| const text = value.toString(); | |
| if (text.length <= 12) return text; | |
| return value.toExponential(6); | |
| }; | |
| const performOperation = (nextOp: Operator) => { | |
| const currentValue = parseFloat(display || "0"); | |
| if (accumulator == null) { | |
| setAccumulator(currentValue); | |
| } else if (pendingOp) { | |
| const result = applyOperator(accumulator, currentValue, pendingOp); | |
| setAccumulator(result); | |
| setDisplay(formatNumber(result)); | |
| } | |
| setPendingOp(nextOp); | |
| setOverwrite(true); | |
| }; | |
| const handleEquals = () => { | |
| const currentValue = parseFloat(display || "0"); | |
| if (pendingOp && accumulator != null) { | |
| const result = applyOperator(accumulator, currentValue, pendingOp); | |
| setDisplay(formatNumber(result)); | |
| setAccumulator(null); | |
| setPendingOp(null); | |
| setOverwrite(true); | |
| } | |
| }; | |
| const buttonClasses = | |
| "flex items-center justify-center rounded-2xl bg-slate-900/70 text-sm font-medium text-slate-50 shadow-sm shadow-slate-900/70 hover:bg-slate-800/80 active:bg-slate-700/80 transition select-none"; | |
| return ( | |
| <div className="flex h-full flex-col bg-slate-900/70 p-3 text-slate-50"> | |
| <div className="mb-2 flex-1 rounded-3xl bg-slate-900/10 p-4 text-right shadow-inner shadow-slate-900/80"> | |
| <div className="text-xs text-slate-300"> | |
| {accumulator != null && pendingOp ? `${accumulator} ${pendingOp}` : ""} | |
| </div> | |
| <div className="mt-2 text-3xl font-light tracking-tight"> | |
| {display} | |
| </div> | |
| </div> | |
| <div className="grid flex-[2] grid-cols-4 gap-2 text-sm"> | |
| <button | |
| type="button" | |
| className={`${buttonClasses} bg-slate-800/90 text-sky-300`} | |
| onClick={clearAll} | |
| > | |
| C | |
| </button> | |
| <button | |
| type="button" | |
| className={`${buttonClasses} bg-slate-800/90 text-sky-300`} | |
| onClick={() => performOperation("/")} | |
| > | |
| ÷ | |
| </button> | |
| <button | |
| type="button" | |
| className={`${buttonClasses} bg-slate-800/90 text-sky-300`} | |
| onClick={() => performOperation("*")} | |
| > | |
| × | |
| </button> | |
| <button | |
| type="button" | |
| className={`${buttonClasses} bg-slate-800/90 text-sky-300`} | |
| onClick={() => performOperation("-")} | |
| > | |
| − | |
| </button> | |
| {[7, 8, 9].map((d) => ( | |
| <button | |
| key={d} | |
| type="button" | |
| className={buttonClasses} | |
| onClick={() => inputDigit(String(d))} | |
| > | |
| {d} | |
| </button> | |
| ))} | |
| <button | |
| type="button" | |
| className={`${buttonClasses} bg-slate-800/90 text-sky-300`} | |
| onClick={() => performOperation("+")} | |
| > | |
| + | |
| </button> | |
| {[4, 5, 6].map((d) => ( | |
| <button | |
| key={d} | |
| type="button" | |
| className={buttonClasses} | |
| onClick={() => inputDigit(String(d))} | |
| > | |
| {d} | |
| </button> | |
| ))} | |
| <button | |
| type="button" | |
| className={`${buttonClasses} row-span-2 bg-sky-500/90 text-base shadow-md shadow-sky-900/70 hover:bg-sky-400/90`} | |
| onClick={handleEquals} | |
| > | |
| = | |
| </button> | |
| {[1, 2, 3].map((d) => ( | |
| <button | |
| key={d} | |
| type="button" | |
| className={buttonClasses} | |
| onClick={() => inputDigit(String(d))} | |
| > | |
| {d} | |
| </button> | |
| ))} | |
| <button | |
| type="button" | |
| className={`${buttonClasses} col-span-2`} | |
| onClick={() => inputDigit("0")} | |
| > | |
| 0 | |
| </button> | |
| <button | |
| type="button" | |
| className={buttonClasses} | |
| onClick={inputDot} | |
| > | |
| . | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { | |
| ReactNode, | |
| createContext, | |
| useCallback, | |
| useContext, | |
| useMemo, | |
| useState, | |
| } from "react"; | |
| import { AppId, DesktopTheme, DesktopWindow, WallpaperId } from "./types"; | |
| interface DesktopContextValue { | |
| windows: DesktopWindow[]; | |
| activeAppId: AppId | null; | |
| isStartMenuOpen: boolean; | |
| theme: DesktopTheme; | |
| wallpaper: WallpaperId; | |
| openApp: (appId: AppId) => void; | |
| closeApp: (appId: AppId) => void; | |
| minimizeApp: (appId: AppId) => void; | |
| toggleMaximizeApp: (appId: AppId) => void; | |
| focusApp: (appId: AppId) => void; | |
| moveWindow: (appId: AppId, x: number, y: number) => void; | |
| setStartMenuOpen: (open: boolean) => void; | |
| toggleStartMenu: () => void; | |
| setTheme: (theme: DesktopTheme) => void; | |
| setWallpaper: (wallpaper: WallpaperId) => void; | |
| } | |
| const DesktopContext = createContext<DesktopContextValue | null>(null); | |
| const initialWindowPosition: Record<AppId, { x: number; y: number }> = { | |
| "recycle-bin": { x: 24, y: 24 }, | |
| calculator: { x: 260, y: 80 }, | |
| notepad: { x: 360, y: 120 }, | |
| explorer: { x: 180, y: 140 }, | |
| browser: { x: 320, y: 200 }, | |
| settings: { x: 420, y: 160 }, | |
| }; | |
| const initialWindowSize: Record<AppId, { width: number; height: number }> = { | |
| "recycle-bin": { width: 360, height: 280 }, | |
| calculator: { width: 280, height: 360 }, | |
| notepad: { width: 520, height: 360 }, | |
| explorer: { width: 640, height: 400 }, | |
| browser: { width: 640, height: 400 }, | |
| settings: { width: 480, height: 340 }, | |
| }; | |
| const appTitles: Record<AppId, string> = { | |
| "recycle-bin": "Recycle Bin", | |
| calculator: "Calculator", | |
| notepad: "Notepad", | |
| explorer: "File Explorer", | |
| browser: "Browser", | |
| settings: "Settings", | |
| }; | |
| interface DesktopProviderProps { | |
| children: ReactNode; | |
| } | |
| export const DesktopProvider: React.FC<DesktopProviderProps> = ({ | |
| children, | |
| }) => { | |
| const [windows, setWindows] = useState<DesktopWindow[]>([]); | |
| const [activeAppId, setActiveAppId] = useState<AppId | null>(null); | |
| const [isStartMenuOpen, setIsStartMenuOpen] = useState(false); | |
| const [theme, setTheme] = useState<DesktopTheme>("light"); | |
| const [wallpaper, setWallpaper] = useState<WallpaperId>("default"); | |
| const openApp = useCallback((appId: AppId) => { | |
| setIsStartMenuOpen(false); | |
| setWindows((current) => { | |
| const existing = current.find((w) => w.appId === appId); | |
| if (existing) { | |
| setActiveAppId(appId); | |
| if (existing.isMinimized) { | |
| return current.map((w) => | |
| w.appId === appId ? { ...w, isMinimized: false } : w | |
| ); | |
| } | |
| return current; | |
| } | |
| const basePos = initialWindowPosition[appId] ?? { x: 180, y: 120 }; | |
| const baseSize = initialWindowSize[appId] ?? { | |
| width: 480, | |
| height: 320, | |
| }; | |
| const maxZ = current.reduce( | |
| (max, w) => (w.zIndex > max ? w.zIndex : max), | |
| 10 | |
| ); | |
| const nextZ = maxZ + 1; | |
| setActiveAppId(appId); | |
| const newWindow: DesktopWindow = { | |
| appId, | |
| title: appTitles[appId], | |
| x: basePos.x, | |
| y: basePos.y, | |
| width: baseSize.width, | |
| height: baseSize.height, | |
| zIndex: nextZ, | |
| isMinimized: false, | |
| isMaximized: false, | |
| }; | |
| return [...current, newWindow]; | |
| }); | |
| }, []); | |
| const closeApp = useCallback((appId: AppId) => { | |
| setWindows((current) => current.filter((w) => w.appId !== appId)); | |
| setActiveAppId((currentActive) => | |
| currentActive === appId ? null : currentActive | |
| ); | |
| }, []); | |
| const minimizeApp = useCallback((appId: AppId) => { | |
| setWindows((current) => | |
| current.map((w) => | |
| w.appId === appId ? { ...w, isMinimized: true } : w | |
| ) | |
| ); | |
| setActiveAppId((currentActive) => | |
| currentActive === appId ? null : currentActive | |
| ); | |
| }, []); | |
| const toggleMaximizeApp = useCallback((appId: AppId) => { | |
| setWindows((current) => | |
| current.map((w) => | |
| w.appId === appId ? { ...w, isMaximized: !w.isMaximized } : w | |
| ) | |
| ); | |
| setActiveAppId(appId); | |
| }, []); | |
| const focusApp = useCallback((appId: AppId) => { | |
| setWindows((current) => { | |
| const maxZ = current.reduce( | |
| (max, w) => (w.zIndex > max ? w.zIndex : max), | |
| 10 | |
| ); | |
| const nextZ = maxZ + 1; | |
| setActiveAppId(appId); | |
| return current.map((win) => | |
| win.appId === appId ? { ...win, zIndex: nextZ } : win | |
| ); | |
| }); | |
| }, []); | |
| const moveWindow = useCallback( | |
| (appId: AppId, x: number, y: number) => { | |
| setWindows((current) => | |
| current.map((w) => (w.appId === appId ? { ...w, x, y } : w)) | |
| ); | |
| }, | |
| [] | |
| ); | |
| const toggleStartMenu = useCallback(() => { | |
| setIsStartMenuOpen((open) => !open); | |
| }, []); | |
| const value: DesktopContextValue = useMemo( | |
| () => ({ | |
| windows, | |
| activeAppId, | |
| isStartMenuOpen, | |
| theme, | |
| wallpaper, | |
| openApp, | |
| closeApp, | |
| minimizeApp, | |
| toggleMaximizeApp, | |
| focusApp, | |
| moveWindow, | |
| setStartMenuOpen: setIsStartMenuOpen, | |
| toggleStartMenu, | |
| setTheme, | |
| setWallpaper, | |
| }), | |
| [ | |
| windows, | |
| activeAppId, | |
| isStartMenuOpen, | |
| theme, | |
| wallpaper, | |
| openApp, | |
| closeApp, | |
| minimizeApp, | |
| toggleMaximizeApp, | |
| focusApp, | |
| moveWindow, | |
| toggleStartMenu, | |
| ] | |
| ); | |
| return ( | |
| <DesktopContext.Provider value={value}>{children}</DesktopContext.Provider> | |
| ); | |
| }; | |
| export const useDesktop = (): DesktopContextValue => { | |
| const ctx = useContext(DesktopContext); | |
| if (!ctx) { | |
| throw new Error("useDesktop must be used within DesktopProvider"); | |
| } | |
| return ctx; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useDesktop } from "./DesktopContext"; | |
| import { AppId } from "./types"; | |
| interface DesktopIconConfig { | |
| id: AppId; | |
| label: string; | |
| glyph: string; | |
| } | |
| const ICONS: DesktopIconConfig[] = [ | |
| { id: "recycle-bin", label: "Bin", glyph: "🗑️" }, | |
| { id: "explorer", label: "Files", glyph: "📁" }, | |
| { id: "notepad", label: "Notes?", glyph: "✏️" }, | |
| { id: "calculator", label: "Calc", glyph: "➗" }, | |
| { id: "settings", label: "Prefs", glyph: "🔧" }, | |
| ]; | |
| export const DesktopIcons: React.FC = () => { | |
| const { openApp } = useDesktop(); | |
| return ( | |
| <div className="pointer-events-none absolute inset-0 p-3 sm:p-5"> | |
| <div className="grid w-28 grid-cols-1 gap-3"> | |
| {ICONS.map((icon) => ( | |
| <button | |
| key={icon.id} | |
| type="button" | |
| className="pointer-events-auto flex flex-col items-center gap-1 rounded-xl p-2 text-xs text-slate-50/90 transition hover:bg-sky-400/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/80" | |
| onClick={(event) => { | |
| event.stopPropagation(); | |
| openApp(icon.id); | |
| }} | |
| > | |
| <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900/40 shadow-lg shadow-slate-900/60 backdrop-blur-md"> | |
| <span className="text-xl" aria-hidden> | |
| {icon.glyph} | |
| </span> | |
| </div> | |
| <span className="w-full truncate text-center drop-shadow-sm"> | |
| {icon.label} | |
| </span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { DesktopProvider } from "./DesktopContext"; | |
| import { DesktopShell } from "./DesktopShell"; | |
| interface DesktopRootProps { | |
| username: string; | |
| onLogout: () => void; | |
| } | |
| export const DesktopRoot: React.FC<DesktopRootProps> = ({ | |
| username, | |
| onLogout, | |
| }) => { | |
| return ( | |
| <DesktopProvider> | |
| <DesktopShell username={username} onLogout={onLogout} /> | |
| </DesktopProvider> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useDesktop } from "./DesktopContext"; | |
| import { DesktopIcons } from "./DesktopIcons"; | |
| import { Taskbar } from "./Taskbar"; | |
| import { WindowManager } from "./WindowManager"; | |
| import { StartMenu } from "./StartMenu"; | |
| interface DesktopShellProps { | |
| username: string; | |
| onLogout: () => void; | |
| } | |
| export const DesktopShell: React.FC<DesktopShellProps> = ({ | |
| username, | |
| onLogout, | |
| }) => { | |
| const { theme, wallpaper, isStartMenuOpen, setStartMenuOpen } = useDesktop(); | |
| const wallpaperClass = (() => { | |
| switch (wallpaper) { | |
| case "blue": | |
| return "bg-gradient-to-br from-sky-500 via-sky-700 to-indigo-800"; | |
| case "purple": | |
| return "bg-gradient-to-br from-purple-500 via-indigo-600 to-slate-900"; | |
| case "sunset": | |
| return "bg-gradient-to-br from-amber-400 via-rose-500 to-slate-900"; | |
| default: | |
| return "bg-gradient-to-br from-sky-600 via-sky-900 to-slate-950"; | |
| } | |
| })(); | |
| const themeClass = | |
| theme === "dark" | |
| ? "text-slate-50" | |
| : "text-slate-900"; | |
| return ( | |
| <div | |
| className={`relative flex h-screen w-screen flex-col overflow-hidden ${themeClass}`} | |
| onClick={() => { | |
| if (isStartMenuOpen) { | |
| setStartMenuOpen(false); | |
| } | |
| }} | |
| > | |
| <div | |
| className={`absolute inset-0 ${wallpaperClass} bg-fixed`} | |
| style={{ | |
| backgroundImage: | |
| "radial-gradient(circle at 10% 20%, rgba(255,255,255,0.18), transparent 55%), radial-gradient(circle at 80% 0%, rgba(56, 189, 248, 0.22), transparent 55%)", | |
| }} | |
| /> | |
| <div className="pointer-events-none absolute inset-0 bg-sky-900/10 mix-blend-soft-light" /> | |
| <div className="relative z-10 flex h-full flex-col"> | |
| <div className="relative flex-1 overflow-hidden"> | |
| <DesktopIcons /> | |
| <WindowManager /> | |
| <StartMenu username={username} onLogout={onLogout} /> | |
| </div> | |
| <Taskbar /> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| export const FileExplorerApp: React.FC = () => { | |
| const locations = ["Desktop", "Documents", "Downloads", "Pictures", "Music"]; | |
| const items = [ | |
| { name: "Projects", type: "Folder" }, | |
| { name: "Screenshots", type: "Folder" }, | |
| { name: "notes.txt", type: "Text Document" }, | |
| { name: "Designs", type: "Folder" }, | |
| ]; | |
| return ( | |
| <div className="flex h-full bg-slate-900/70 text-slate-100"> | |
| <aside className="flex w-40 flex-col border-r border-slate-700/70 bg-slate-950/50 p-2 text-[11px]"> | |
| <div className="mb-2 px-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400"> | |
| Quick access | |
| </div> | |
| {locations.map((location) => ( | |
| <button | |
| key={location} | |
| type="button" | |
| className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-left text-slate-200 hover:bg-slate-800/60" | |
| > | |
| <span aria-hidden>📁</span> | |
| <span>{location}</span> | |
| </button> | |
| ))} | |
| </aside> | |
| <main className="flex-1 p-3 text-[11px]"> | |
| <div className="flex items-center justify-between border-b border-slate-700/70 pb-1"> | |
| <h2 className="text-xs font-medium text-slate-50">Desktop</h2> | |
| <div className="flex items-center gap-2 text-[10px] text-slate-300"> | |
| <span>View</span> | |
| <span>⋮</span> | |
| </div> | |
| </div> | |
| <div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-3"> | |
| {items.map((item) => ( | |
| <div | |
| key={item.name} | |
| className="flex flex-col items-start gap-1 rounded-xl bg-slate-900/70 p-2 text-slate-100 shadow-sm shadow-slate-900/70" | |
| > | |
| <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/20 text-lg"> | |
| <span aria-hidden>{item.type === "Folder" ? "📁" : "📄"}</span> | |
| </div> | |
| <div className="text-[11px]"> | |
| <div className="truncate font-medium">{item.name}</div> | |
| <div className="text-[10px] text-slate-400">{item.type}</div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { Metadata } from "next"; | |
| import { Geist, Geist_Mono } from "next/font/google"; | |
| import "./globals.css"; | |
| const geistSans = Geist({ | |
| variable: "--font-geist-sans", | |
| subsets: ["latin"], | |
| }); | |
| const geistMono = Geist_Mono({ | |
| variable: "--font-geist-mono", | |
| subsets: ["latin"], | |
| }); | |
| export const metadata: Metadata = { | |
| title: "Create Next App", | |
| description: "Generated by create next app", | |
| }; | |
| export default function RootLayout({ | |
| children, | |
| }: Readonly<{ | |
| children: React.ReactNode; | |
| }>) { | |
| return ( | |
| <html lang="en"> | |
| <body | |
| className={`${geistSans.variable} ${geistMono.variable} antialiased`} | |
| > | |
| {children} | |
| </body> | |
| </html> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { FormEvent, useState } from "react"; | |
| interface LoginScreenProps { | |
| onLogin: (username: string) => void; | |
| } | |
| export const LoginScreen: React.FC<LoginScreenProps> = ({ onLogin }) => { | |
| const [username, setUsername] = useState(""); | |
| const [password, setPassword] = useState(""); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const handleSubmit = (event: FormEvent) => { | |
| event.preventDefault(); | |
| if (!username.trim() || !password.trim()) { | |
| setError("Please enter a username and password."); | |
| return; | |
| } | |
| setError(null); | |
| setIsLoading(true); | |
| setTimeout(() => { | |
| setIsLoading(false); | |
| onLogin(username.trim()); | |
| }, 900); | |
| }; | |
| return ( | |
| <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-sky-700 via-sky-900 to-indigo-900"> | |
| <div className="relative flex h-full w-full items-center justify-center px-4 py-8"> | |
| <div className="absolute inset-0 bg-[radial-gradient(circle_at_10%_20%,rgba(255,255,255,0.18),transparent_55%),radial-gradient(circle_at_80%_0%,rgba(56,189,248,0.22),transparent_55%)]" /> | |
| <div className="relative z-10 w-full max-w-sm rounded-3xl bg-slate-900/60 p-8 shadow-2xl shadow-slate-900/60 backdrop-blur-xl border border-white/10"> | |
| <div className="flex flex-col items-center gap-4"> | |
| <div className="flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-sky-400 to-indigo-500 shadow-lg shadow-sky-900/60 text-3xl font-semibold text-white"> | |
| {username ? username[0]?.toUpperCase() : "U"} | |
| </div> | |
| <h1 className="text-xl font-semibold text-slate-50">Sign in</h1> | |
| <p className="text-xs text-slate-300/80"> | |
| Welcome to your web desktop | |
| </p> | |
| </div> | |
| <form onSubmit={handleSubmit} className="mt-6 space-y-4"> | |
| <div className="space-y-1.5"> | |
| <label className="block text-xs font-medium text-slate-200"> | |
| Username | |
| </label> | |
| <input | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| className="w-full rounded-xl border border-white/10 bg-slate-900/60 px-3 py-2 text-sm text-slate-50 outline-none ring-0 transition focus:border-sky-400/70 focus:bg-slate-900/70 focus:ring-2 focus:ring-sky-500/60 placeholder:text-slate-400" | |
| placeholder="Your name" | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <label className="block text-xs font-medium text-slate-200"> | |
| Password | |
| </label> | |
| <input | |
| type="password" | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| className="w-full rounded-xl border border-white/10 bg-slate-900/60 px-3 py-2 text-sm text-slate-50 outline-none ring-0 transition focus:border-sky-400/70 focus:bg-slate-900/70 focus:ring-2 focus:ring-sky-500/60 placeholder:text-slate-400" | |
| placeholder="Enter anything" | |
| /> | |
| </div> | |
| {error && ( | |
| <p className="text-xs text-rose-300/90" role="alert"> | |
| {error} | |
| </p> | |
| )} | |
| <button | |
| type="submit" | |
| disabled={isLoading} | |
| className="mt-2 flex w-full items-center justify-center rounded-2xl bg-sky-500 px-4 py-2.5 text-sm font-medium text-white shadow-lg shadow-sky-900/60 transition hover:bg-sky-400 disabled:cursor-not-allowed disabled:bg-sky-500/70" | |
| > | |
| {isLoading ? ( | |
| <span className="flex items-center gap-2"> | |
| <span className="h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-transparent" /> | |
| Signing in... | |
| </span> | |
| ) : ( | |
| "Sign in" | |
| )} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState } from "react"; | |
| export const NotepadApp: React.FC = () => { | |
| const [text, setText] = useState(""); | |
| return ( | |
| <div className="flex h-full flex-col bg-slate-900/60 text-slate-100"> | |
| <div className="flex items-center gap-3 border-b border-slate-700/70 bg-slate-900/80 px-3 py-1.5 text-[11px] text-slate-300"> | |
| <span className="text-xs" aria-hidden> | |
| 📝 | |
| </span> | |
| <span>Untitled note</span> | |
| </div> | |
| <textarea | |
| value={text} | |
| onChange={(event) => setText(event.target.value)} | |
| className="flex-1 resize-none bg-transparent px-3 py-2 text-xs text-slate-100 outline-none placeholder:text-slate-500" | |
| placeholder="Start typing..." | |
| /> | |
| </div> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { LoginScreen } from "./features/login/LoginScreen"; | |
| import { DesktopRoot } from "./features/desktop/DesktopRoot"; | |
| export default function Home() { | |
| const [isHydrated, setIsHydrated] = useState(false); | |
| const [isLoggedIn, setIsLoggedIn] = useState(false); | |
| const [username, setUsername] = useState<string | null>(null); | |
| useEffect(() => { | |
| setIsHydrated(true); | |
| if (typeof window === "undefined") return; | |
| const storedUser = window.localStorage.getItem("desktop_username"); | |
| const storedLoggedIn = window.localStorage.getItem("desktop_logged_in"); | |
| if (storedUser && storedLoggedIn === "true") { | |
| setUsername(storedUser); | |
| setIsLoggedIn(true); | |
| } | |
| }, []); | |
| const handleLogin = (name: string) => { | |
| setUsername(name); | |
| setIsLoggedIn(true); | |
| if (typeof window !== "undefined") { | |
| window.localStorage.setItem("desktop_username", name); | |
| window.localStorage.setItem("desktop_logged_in", "true"); | |
| } | |
| }; | |
| const handleLogout = () => { | |
| setIsLoggedIn(false); | |
| setUsername(null); | |
| if (typeof window !== "undefined") { | |
| window.localStorage.removeItem("desktop_username"); | |
| window.localStorage.removeItem("desktop_logged_in"); | |
| } | |
| }; | |
| if (!isHydrated) { | |
| return ( | |
| <div className="flex min-h-screen items-center justify-center bg-slate-900 text-slate-100"> | |
| <div className="flex flex-col items-center gap-3"> | |
| <div className="h-10 w-10 animate-spin rounded-full border-2 border-slate-500 border-t-transparent" /> | |
| <p className="text-sm text-slate-300">Loading desktop...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!isLoggedIn || !username) { | |
| return <LoginScreen onLogin={handleLogin} />; | |
| } | |
| return <DesktopRoot username={username} onLogout={handleLogout} />; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useDesktop } from "../DesktopContext"; | |
| export const SettingsApp: React.FC = () => { | |
| const { theme, setTheme, wallpaper, setWallpaper } = useDesktop(); | |
| return ( | |
| <div className="flex h-full bg-slate-900/70 text-slate-100"> | |
| <aside className="flex w-40 flex-col border-r border-slate-700/70 bg-slate-950/50 p-3 text-[11px]"> | |
| <div className="mb-3 text-[10px] font-semibold uppercase tracking-wide text-slate-400"> | |
| Settings | |
| </div> | |
| <button | |
| type="button" | |
| className="flex items-center gap-2 rounded-lg bg-slate-800/70 px-2 py-1.5 text-left text-slate-50" | |
| > | |
| <span aria-hidden>🎨</span> | |
| <span>Personalization</span> | |
| </button> | |
| </aside> | |
| <main className="flex-1 space-y-4 p-4 text-[11px]"> | |
| <section> | |
| <h2 className="mb-2 text-xs font-semibold text-slate-50"> | |
| Theme | |
| </h2> | |
| <div className="flex gap-3"> | |
| <button | |
| type="button" | |
| onClick={() => setTheme("light")} | |
| className={`flex flex-1 flex-col items-start gap-2 rounded-2xl border px-3 py-2 text-left ${ | |
| theme === "light" | |
| ? "border-sky-400 bg-slate-900/60" | |
| : "border-slate-700/70 bg-slate-900/40" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <span aria-hidden>🌤️</span> | |
| <span className="text-xs font-medium">Light</span> | |
| </div> | |
| <p className="text-[10px] text-slate-300"> | |
| Brighter colors and higher contrast. | |
| </p> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setTheme("dark")} | |
| className={`flex flex-1 flex-col items-start gap-2 rounded-2xl border px-3 py-2 text-left ${ | |
| theme === "dark" | |
| ? "border-sky-400 bg-slate-900/60" | |
| : "border-slate-700/70 bg-slate-900/40" | |
| }`} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <span aria-hidden>🌙</span> | |
| <span className="text-xs font-medium">Dark</span> | |
| </div> | |
| <p className="text-[10px] text-slate-300"> | |
| Softer contrast designed for low light. | |
| </p> | |
| </button> | |
| </div> | |
| </section> | |
| <section> | |
| <h2 className="mb-2 text-xs font-semibold text-slate-50"> | |
| Wallpaper | |
| </h2> | |
| <div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> | |
| <WallpaperOption | |
| id="default" | |
| label="Deep blue" | |
| active={wallpaper === "default"} | |
| gradient="from-sky-600 via-sky-900 to-slate-950" | |
| onSelect={setWallpaper} | |
| /> | |
| <WallpaperOption | |
| id="blue" | |
| label="Sky waves" | |
| active={wallpaper === "blue"} | |
| gradient="from-sky-400 via-sky-600 to-indigo-700" | |
| onSelect={setWallpaper} | |
| /> | |
| <WallpaperOption | |
| id="purple" | |
| label="Violet glow" | |
| active={wallpaper === "purple"} | |
| gradient="from-purple-500 via-indigo-600 to-slate-900" | |
| onSelect={setWallpaper} | |
| /> | |
| <WallpaperOption | |
| id="sunset" | |
| label="Sunset" | |
| active={wallpaper === "sunset"} | |
| gradient="from-amber-400 via-rose-500 to-slate-900" | |
| onSelect={setWallpaper} | |
| /> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| interface WallpaperOptionProps { | |
| id: "default" | "blue" | "purple" | "sunset"; | |
| label: string; | |
| gradient: string; | |
| active: boolean; | |
| onSelect: (id: "default" | "blue" | "purple" | "sunset") => void; | |
| } | |
| const WallpaperOption: React.FC<WallpaperOptionProps> = ({ | |
| id, | |
| label, | |
| gradient, | |
| active, | |
| onSelect, | |
| }) => { | |
| return ( | |
| <button | |
| type="button" | |
| onClick={() => onSelect(id)} | |
| className={`flex flex-col gap-1 rounded-2xl border p-1.5 ${ | |
| active | |
| ? "border-sky-400 bg-slate-900/60" | |
| : "border-slate-700/70 bg-slate-900/40 hover:border-slate-400/80" | |
| }`} | |
| > | |
| <div | |
| className={`h-14 w-full rounded-xl bg-gradient-to-br ${gradient}`} | |
| /> | |
| <span className="truncate text-[10px] text-slate-200">{label}</span> | |
| </button> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| const formatTime = (date: Date) => | |
| date.toLocaleTimeString([], { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| }); | |
| const formatDate = (date: Date) => | |
| date.toLocaleDateString([], { | |
| month: "short", | |
| day: "2-digit", | |
| }); | |
| export const SystemClock: React.FC = () => { | |
| const [now, setNow] = useState(new Date()); | |
| useEffect(() => { | |
| const tick = () => setNow(new Date()); | |
| const interval = window.setInterval(tick, 1000 * 30); | |
| return () => window.clearInterval(interval); | |
| }, []); | |
| return ( | |
| <> | |
| <span>{formatTime(now)}</span> | |
| <span className="text-[9px] text-slate-200/80">{formatDate(now)}</span> | |
| </> | |
| ); | |
| }; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useDesktop } from "./DesktopContext"; | |
| import { AppId } from "./types"; | |
| import { SystemClock } from "./system/SystemClock"; | |
| interface PinnedApp { | |
| id: AppId; | |
| glyph: string; | |
| label: string; | |
| } | |
| const PINNED_APPS: PinnedApp[] = [ | |
| { id: "explorer", glyph: "📁", label: "Files" }, | |
| { id: "browser", glyph: "🛰️", label: "Net" }, | |
| { id: "notepad", glyph: "✏️", label: "Notes" }, | |
| { id: "calculator", glyph: "➗", label: "Calc" }, | |
| { id: "settings", glyph: "🛠️", label: "Prefs" }, | |
| ]; | |
| export const Taskbar: React.FC = () => { | |
| const { | |
| openApp, | |
| windows, | |
| activeAppId, | |
| toggleStartMenu, | |
| theme, | |
| } = useDesktop(); | |
| const isAppRunning = (appId: AppId) => | |
| windows.some((w) => w.appId === appId && !w.isMinimized); | |
| const isAppOpen = (appId: AppId) => | |
| windows.some((w) => w.appId === appId); | |
| const isDark = theme === "dark"; | |
| return ( | |
| <div className="relative z-20 flex h-14 w-full items-center justify-between border-t border-white/15 bg-slate-900/60 px-3 text-xs text-slate-50/90 shadow-[0_-10px_40px_rgba(15,23,42,0.8)] backdrop-blur-xl"> | |
| <div className="flex flex-1 items-center justify-start gap-3"> | |
| <button | |
| type="button" | |
| className="flex h-9 w-9 items-center justify-center rounded-2xl bg-slate-50/10 text-[0px] shadow-md shadow-slate-900/70 transition hover:bg-slate-50/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-300 translate-y-[1px]" | |
| onClick={(event) => { | |
| event.stopPropagation(); | |
| toggleStartMenu(); | |
| }} | |
| > | |
| <span className="relative flex h-4 w-4 flex-wrap content-between items-center justify-between"> | |
| <span className="h-[7px] w-[7px] rounded-[3px] bg-sky-400" /> | |
| <span className="h-[7px] w-[7px] rounded-[3px] bg-sky-300" /> | |
| <span className="h-[7px] w-[7px] rounded-[3px] bg-sky-300" /> | |
| <span className="h-[7px] w-[7px] rounded-[3px] bg-sky-200" /> | |
| </span> | |
| <span className="sr-only">Open start menu</span> | |
| </button> | |
| <div className="flex items-center gap-1 pl-1"> | |
| {PINNED_APPS.map((app) => { | |
| const running = isAppRunning(app.id); | |
| const open = isAppOpen(app.id); | |
| const active = activeAppId === app.id; | |
| return ( | |
| <button | |
| key={app.id} | |
| type="button" | |
| className={`group relative flex h-9 w-9 items-center justify-center rounded-2xl px-1 text-lg transition | |
| ${active ? "bg-slate-50/20" : "hover:bg-slate-50/10"} | |
| `} | |
| onClick={(event) => { | |
| event.stopPropagation(); | |
| openApp(app.id); | |
| }} | |
| > | |
| <span aria-hidden>{app.glyph}</span> | |
| <span className="sr-only">{app.label}</span> | |
| {open && ( | |
| <span | |
| className={`absolute inset-x-2 bottom-1 h-1 rounded-full ${ | |
| running | |
| ? "bg-sky-400 shadow-[0_0_8px_rgba(56,189,248,0.9)]" | |
| : "bg-slate-300/80" | |
| }`} | |
| /> | |
| )} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 pl-3 pr-1"> | |
| <div className="flex items-center gap-2 text-[10px] text-slate-100/80"> | |
| <button | |
| type="button" | |
| className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50/5 text-xs hover:bg-slate-50/15" | |
| > | |
| <span aria-hidden>📶</span> | |
| <span className="sr-only">Network status</span> | |
| </button> | |
| <button | |
| type="button" | |
| className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50/5 text-xs hover:bg-slate-50/15" | |
| > | |
| <span aria-hidden>{isDark ? "🔈" : "🔊"}</span> | |
| <span className="sr-only">Volume</span> | |
| </button> | |
| <button | |
| type="button" | |
| className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50/5 text-xs hover:bg-slate-50/15" | |
| > | |
| <span aria-hidden>🔋</span> | |
| <span className="sr-only">Battery status</span> | |
| </button> | |
| </div> | |
| <div className="flex h-10 flex-col items-end justify-center rounded-lg px-2 text-[10px] leading-tight text-slate-100/90 hover:bg-slate-50/5"> | |
| <SystemClock /> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export type AppId = | |
| | "recycle-bin" | |
| | "calculator" | |
| | "notepad" | |
| | "explorer" | |
| | "browser" | |
| | "settings"; | |
| export type DesktopTheme = "light" | "dark"; | |
| export type WallpaperId = "default" | "blue" | "purple" | "sunset"; | |
| export interface DesktopWindow { | |
| appId: AppId; | |
| title: string; | |
| x: number; | |
| y: number; | |
| width: number; | |
| height: number; | |
| zIndex: number; | |
| isMinimized: boolean; | |
| isMaximized: boolean; | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useDesktop } from "./DesktopContext"; | |
| import { AppWindow } from "./windows/AppWindow"; | |
| import { CalculatorApp } from "./windows/CalculatorApp"; | |
| import { NotepadApp } from "./windows/NotepadApp"; | |
| import { FileExplorerApp } from "./windows/FileExplorerApp"; | |
| import { SettingsApp } from "./windows/SettingsApp"; | |
| import { AppId } from "./types"; | |
| const renderWindowContent = (appId: AppId) => { | |
| switch (appId) { | |
| case "calculator": | |
| return <CalculatorApp />; | |
| case "notepad": | |
| return <NotepadApp />; | |
| case "explorer": | |
| return <FileExplorerApp />; | |
| case "browser": | |
| return ( | |
| <div className="flex h-full flex-col bg-slate-900/60 text-xs text-slate-100"> | |
| <div className="flex items-center gap-2 border-b border-slate-700/70 bg-slate-900/80 px-3 py-1.5"> | |
| <span className="text-[11px] text-slate-300">Address</span> | |
| <div className="flex-1 rounded-lg bg-slate-950/60 px-2 py-1 text-[11px] text-slate-300"> | |
| https://web-desktop.local | |
| </div> | |
| </div> | |
| <div className="flex flex-1 items-center justify-center p-6 text-center text-[11px] text-slate-200/90"> | |
| <p> | |
| This is a placeholder browser window. Imagine your favorite site | |
| here. | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| case "recycle-bin": | |
| return ( | |
| <div className="flex h-full flex-col bg-slate-900/60 text-xs text-slate-100"> | |
| <div className="border-b border-slate-700/70 bg-slate-900/80 px-3 py-1.5 text-[11px] text-slate-300"> | |
| Items (0) | |
| </div> | |
| <div className="flex flex-1 items-center justify-center p-4 text-[11px] text-slate-300"> | |
| <p>Your recycle bin is empty.</p> | |
| </div> | |
| </div> | |
| ); | |
| case "settings": | |
| return <SettingsApp />; | |
| default: | |
| return null; | |
| } | |
| }; | |
| const windowIconForApp = (appId: AppId): string => { | |
| switch (appId) { | |
| case "recycle-bin": | |
| return "🗑️"; | |
| case "calculator": | |
| return "➗"; | |
| case "notepad": | |
| return "✏️"; | |
| case "explorer": | |
| return "📁"; | |
| case "browser": | |
| return "🛰️"; | |
| case "settings": | |
| return "🛠️"; | |
| default: | |
| return ""; | |
| } | |
| }; | |
| export const WindowManager: React.FC = () => { | |
| const { | |
| windows, | |
| activeAppId, | |
| closeApp, | |
| minimizeApp, | |
| toggleMaximizeApp, | |
| focusApp, | |
| moveWindow, | |
| } = useDesktop(); | |
| const visibleWindows = [...windows] | |
| .filter((w) => !w.isMinimized) | |
| .sort((a, b) => a.zIndex - b.zIndex); | |
| return ( | |
| <div className="pointer-events-none absolute inset-0"> | |
| {visibleWindows.map((win) => ( | |
| <AppWindow | |
| key={win.appId} | |
| appId={win.appId} | |
| title={win.title} | |
| iconGlyph={windowIconForApp(win.appId)} | |
| x={win.x} | |
| y={win.y} | |
| width={win.width} | |
| height={win.height} | |
| isActive={activeAppId === win.appId} | |
| isMaximized={win.isMaximized} | |
| zIndex={win.zIndex} | |
| onClose={() => closeApp(win.appId)} | |
| onMinimize={() => minimizeApp(win.appId)} | |
| onToggleMaximize={() => toggleMaximizeApp(win.appId)} | |
| onFocus={() => focusApp(win.appId)} | |
| onMove={(x, y) => moveWindow(win.appId, x, y)} | |
| > | |
| {renderWindowContent(win.appId)} | |
| </AppWindow> | |
| ))} | |
| </div> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment