-
-
Save shricodev/3f82d6037608b5212df462ea993ba231 to your computer and use it in GitHub Desktop.
Google's Gemini 3 Pro 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 React, { useState, useEffect, useCallback } from 'react'; | |
| export default function Calculator() { | |
| const [display, setDisplay] = useState('0'); | |
| const [firstOperand, setFirstOperand] = useState<number | null>(null); | |
| const [operator, setOperator] = useState<string | null>(null); | |
| const [waitingForSecondOperand, setWaitingForSecondOperand] = useState(false); | |
| const inputDigit = useCallback((digit: string) => { | |
| if (waitingForSecondOperand) { | |
| setDisplay(digit); | |
| setWaitingForSecondOperand(false); | |
| } else { | |
| setDisplay(display === '0' ? digit : display + digit); | |
| } | |
| }, [waitingForSecondOperand, display]); | |
| const inputDot = useCallback(() => { | |
| if (!display.includes('.')) { | |
| setDisplay(display + '.'); | |
| setWaitingForSecondOperand(false); | |
| } | |
| }, [display]); | |
| const clear = useCallback(() => { | |
| setDisplay('0'); | |
| setFirstOperand(null); | |
| setOperator(null); | |
| setWaitingForSecondOperand(false); | |
| }, []); | |
| const backspace = useCallback(() => { | |
| if (waitingForSecondOperand) return; | |
| setDisplay(display.length > 1 ? display.slice(0, -1) : '0'); | |
| }, [display, waitingForSecondOperand]); | |
| const performCalculation = (op: string, first: number, second: number) => { | |
| switch (op) { | |
| case '+': return first + second; | |
| case '-': return first - second; | |
| case '*': return first * second; | |
| case '/': return first / second; | |
| case '=': return second; | |
| default: return second; | |
| } | |
| }; | |
| const handleOperator = useCallback((nextOperator: string) => { | |
| const inputValue = parseFloat(display); | |
| if (operator && waitingForSecondOperand) { | |
| setOperator(nextOperator); | |
| return; | |
| } | |
| if (firstOperand === null) { | |
| setFirstOperand(inputValue); | |
| } else if (operator) { | |
| const result = performCalculation(operator, firstOperand, inputValue); | |
| // Prevent overflow or long decimals | |
| const formattedResult = parseFloat(result.toFixed(8)); | |
| setDisplay(String(formattedResult)); | |
| setFirstOperand(formattedResult); | |
| } | |
| setWaitingForSecondOperand(true); | |
| setOperator(nextOperator); | |
| }, [display, operator, waitingForSecondOperand, firstOperand]); | |
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| const key = e.key; | |
| if (/[0-9]/.test(key)) { | |
| inputDigit(key); | |
| } else if (key === '.') { | |
| inputDot(); | |
| } else if (key === 'Enter' || key === '=') { | |
| e.preventDefault(); | |
| handleOperator('='); | |
| } else if (key === 'Backspace') { | |
| backspace(); | |
| } else if (key === 'Escape') { | |
| clear(); | |
| } else if (key === '+' || key === '-' || key === '*' || key === '/') { | |
| handleOperator(key); | |
| } | |
| }; | |
| window.addEventListener('keydown', handleKeyDown); | |
| return () => window.removeEventListener('keydown', handleKeyDown); | |
| }, [inputDigit, inputDot, handleOperator, clear, backspace]); | |
| const Button = ({ label, onClick, className = "" }: { label: string, onClick: () => void, className?: string }) => ( | |
| <button | |
| onClick={onClick} | |
| className={`flex-1 rounded-lg text-lg font-medium hover:bg-opacity-80 active:scale-95 active:bg-opacity-60 transition-all shadow-sm border border-black/5 dark:border-white/5 ${className}`} | |
| > | |
| {label} | |
| </button> | |
| ); | |
| return ( | |
| <div className="flex flex-col h-full bg-[#f3f3f3] dark:bg-[#202020] p-1"> | |
| <div className="h-16 flex items-end justify-end px-4 pb-2 text-4xl font-semibold text-black dark:text-white overflow-hidden select-all"> | |
| {display} | |
| </div> | |
| <div className="grid grid-cols-4 grid-rows-5 gap-1 p-1 flex-1"> | |
| <Button label="C" onClick={clear} className="bg-white/50 dark:bg-white/5 hover:bg-white/80 dark:hover:bg-white/10 text-black dark:text-white" /> | |
| <Button label="⌫" onClick={backspace} className="bg-white/50 dark:bg-white/5 hover:bg-white/80 dark:hover:bg-white/10 text-black dark:text-white" /> | |
| <Button label="%" onClick={() => {}} className="bg-white/50 dark:bg-white/5 hover:bg-white/80 dark:hover:bg-white/10 text-black dark:text-white" /> | |
| <Button label="÷" onClick={() => handleOperator('/')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" /> | |
| <Button label="7" onClick={() => inputDigit('7')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="8" onClick={() => inputDigit('8')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="9" onClick={() => inputDigit('9')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="×" onClick={() => handleOperator('*')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" /> | |
| <Button label="4" onClick={() => inputDigit('4')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="5" onClick={() => inputDigit('5')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="6" onClick={() => inputDigit('6')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="-" onClick={() => handleOperator('-')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" /> | |
| <Button label="1" onClick={() => inputDigit('1')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="2" onClick={() => inputDigit('2')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="3" onClick={() => inputDigit('3')} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="+" onClick={() => handleOperator('+')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" /> | |
| <Button label="0" onClick={() => inputDigit('0')} className="bg-white dark:bg-gray-700 text-black dark:text-white col-span-2" /> | |
| <Button label="." onClick={inputDot} className="bg-white dark:bg-gray-700 text-black dark:text-white" /> | |
| <Button label="=" onClick={() => handleOperator('=')} className="bg-blue-500 hover:bg-blue-600 text-white" /> | |
| </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 React from 'react'; | |
| import { useOS, APPS, AppId } from '@/app/context/OSContext'; | |
| import { getWallpaperUrl } from '@/app/utils/wallpapers'; | |
| import Taskbar from './Taskbar'; | |
| import StartMenu from './StartMenu'; | |
| import WindowFrame from './WindowFrame'; | |
| import Calculator from '../apps/Calculator'; | |
| import Notepad from '../apps/Notepad'; | |
| import Explorer from '../apps/Explorer'; | |
| import Settings from '../apps/Settings'; | |
| import { CalculatorIcon, NotepadIcon, FolderIcon, SettingsIcon, BrowserIcon, TrashIcon } from '../icons'; | |
| const DesktopIcon = ({ id, label, onClick }: { id: string, label: string, onClick: () => void }) => { | |
| const Icon = () => { | |
| switch(id) { | |
| case 'calculator': return <CalculatorIcon size={32} className="text-orange-500 drop-shadow-sm" />; | |
| case 'notepad': return <NotepadIcon size={32} className="text-blue-400 drop-shadow-sm" />; | |
| case 'explorer': return <FolderIcon size={32} className="text-yellow-400 drop-shadow-sm" />; | |
| case 'settings': return <SettingsIcon size={32} className="text-gray-500 drop-shadow-sm" />; | |
| case 'browser': return <BrowserIcon size={32} className="text-blue-600 drop-shadow-sm" />; | |
| case 'recycle-bin': return <TrashIcon size={32} className="text-gray-500 drop-shadow-sm" />; | |
| default: return <div className="w-8 h-8 bg-gray-400 rounded"></div>; | |
| } | |
| }; | |
| return ( | |
| <button | |
| className="flex flex-col items-center gap-1 p-2 w-24 hover:bg-white/10 hover:backdrop-blur-sm rounded border border-transparent hover:border-white/20 transition-all text-shadow group" | |
| onDoubleClick={onClick} | |
| onClick={onClick} // Allow single click for simplicity on web, usually double click on desktop | |
| > | |
| <Icon /> | |
| <span className="text-xs text-white font-medium text-center drop-shadow-md line-clamp-2 group-hover:text-white"> | |
| {label} | |
| </span> | |
| </button> | |
| ); | |
| }; | |
| const AppContainer = ({ appId }: { appId: AppId }) => { | |
| switch (appId) { | |
| case 'calculator': return <Calculator />; | |
| case 'notepad': return <Notepad />; | |
| case 'explorer': return <Explorer />; | |
| case 'settings': return <Settings />; | |
| case 'browser': return <div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900 text-gray-500">Browser Placeholder</div>; | |
| case 'recycle-bin': return <div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900 text-gray-500">Recycle Bin is empty</div>; | |
| default: return <div className="p-4">App not found</div>; | |
| } | |
| }; | |
| export default function Desktop() { | |
| const { wallpaper, windows, openWindow, closeStartMenu, theme } = useOS(); | |
| const bgUrl = getWallpaperUrl(wallpaper); | |
| const desktopIcons: AppId[] = ['recycle-bin', 'explorer', 'browser', 'notepad', 'calculator', 'settings']; | |
| return ( | |
| <div | |
| className={`relative h-screen w-screen overflow-hidden flex flex-col select-none ${theme === 'dark' ? 'dark' : ''}`} | |
| style={{ | |
| backgroundImage: `url("${bgUrl}")`, | |
| backgroundSize: 'cover', | |
| backgroundPosition: 'center', | |
| backgroundColor: '#1a1a1a' | |
| }} | |
| onClick={closeStartMenu} | |
| > | |
| {/* Desktop Icons Area */} | |
| <div className="flex-1 relative p-4 grid grid-flow-col grid-rows-[repeat(auto-fill,100px)] gap-4 content-start items-start w-min"> | |
| {desktopIcons.map(appId => ( | |
| <DesktopIcon | |
| key={appId} | |
| id={appId} | |
| label={APPS[appId].title} | |
| onClick={() => openWindow(appId)} | |
| /> | |
| ))} | |
| </div> | |
| {/* Windows Layer */} | |
| {windows.map(win => ( | |
| <WindowFrame key={win.id} windowState={win}> | |
| <AppContainer appId={win.appId} /> | |
| </WindowFrame> | |
| ))} | |
| {/* UI Layer */} | |
| <StartMenu /> | |
| <Taskbar /> | |
| </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 React from 'react'; | |
| import { FolderIcon, BrowserIcon, NotepadIcon } from '../icons'; | |
| export default function Explorer() { | |
| return ( | |
| <div className="flex h-full bg-[#f0f4f9] dark:bg-[#191919] text-sm text-black dark:text-white select-none"> | |
| {/* Sidebar */} | |
| <div className="w-48 flex flex-col gap-1 p-2 border-r border-gray-200 dark:border-gray-800 overflow-y-auto"> | |
| <div className="px-2 py-1.5 font-semibold text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">Quick Access</div> | |
| <div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default"> | |
| <FolderIcon className="text-blue-400" size={16} /> Desktop | |
| </div> | |
| <div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default"> | |
| <FolderIcon className="text-blue-400" size={16} /> Downloads | |
| </div> | |
| <div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default"> | |
| <FolderIcon className="text-blue-400" size={16} /> Documents | |
| </div> | |
| <div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default"> | |
| <FolderIcon className="text-blue-400" size={16} /> Pictures | |
| </div> | |
| <div className="mt-4 px-2 py-1.5 font-semibold text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">This PC</div> | |
| <div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default"> | |
| <div className="w-4 h-4 bg-gray-400 rounded-sm"></div> Local Disk (C:) | |
| </div> | |
| </div> | |
| {/* Main Content */} | |
| <div className="flex-1 flex flex-col bg-white dark:bg-[#202020]"> | |
| {/* Toolbar */} | |
| <div className="h-10 flex items-center gap-4 px-4 border-b border-gray-200 dark:border-gray-800"> | |
| <button className="px-3 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2"> | |
| <span className="text-blue-500 text-lg">+</span> New | |
| </button> | |
| <div className="h-4 w-[1px] bg-gray-300 dark:bg-gray-700"></div> | |
| <button className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Sort</button> | |
| <button className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">View</button> | |
| </div> | |
| {/* File Grid */} | |
| <div className="p-4 grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-4 overflow-y-auto"> | |
| <div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group"> | |
| <FolderIcon size={48} className="text-yellow-400 drop-shadow-sm" /> | |
| <span className="text-center truncate w-full group-hover:text-blue-600">Work</span> | |
| </div> | |
| <div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group"> | |
| <FolderIcon size={48} className="text-yellow-400 drop-shadow-sm" /> | |
| <span className="text-center truncate w-full group-hover:text-blue-600">Personal</span> | |
| </div> | |
| <div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group"> | |
| <NotepadIcon size={48} className="text-blue-500 drop-shadow-sm" /> | |
| <span className="text-center truncate w-full group-hover:text-blue-600">notes.txt</span> | |
| </div> | |
| <div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group"> | |
| <div className="w-12 h-12 bg-blue-500 rounded flex items-center justify-center text-white font-bold text-xs">IMG</div> | |
| <span className="text-center truncate w-full group-hover:text-blue-600">vacation.jpg</span> | |
| </div> | |
| </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
| import React from 'react'; | |
| export const IconProps = { | |
| className: '', | |
| size: 24, | |
| }; | |
| type IconComponent = React.FC<{ className?: string; size?: number }>; | |
| export const StartIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}> | |
| <path d="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z" /> | |
| </svg> | |
| ); | |
| export const SearchIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <circle cx="11" cy="11" r="8" /> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65" /> | |
| </svg> | |
| ); | |
| export const CalculatorIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <rect x="4" y="2" width="16" height="20" rx="2" ry="2" /> | |
| <line x1="8" y1="6" x2="16" y2="6" /> | |
| <line x1="16" y1="14" x2="16" y2="14" /> | |
| <line x1="16" y1="18" x2="16" y2="18" /> | |
| <line x1="12" y1="14" x2="12" y2="14" /> | |
| <line x1="12" y1="18" x2="12" y2="18" /> | |
| <line x1="8" y1="14" x2="8" y2="14" /> | |
| <line x1="8" y1="18" x2="8" y2="18" /> | |
| </svg> | |
| ); | |
| export const NotepadIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /> | |
| <polyline points="14 2 14 8 20 8" /> | |
| <line x1="16" y1="13" x2="8" y2="13" /> | |
| <line x1="16" y1="17" x2="8" y2="17" /> | |
| <polyline points="10 9 9 9 8 9" /> | |
| </svg> | |
| ); | |
| export const FolderIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" /> | |
| </svg> | |
| ); | |
| export const SettingsIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <circle cx="12" cy="12" r="3" /> | |
| <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" /> | |
| </svg> | |
| ); | |
| export const BrowserIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <circle cx="12" cy="12" r="10" /> | |
| <circle cx="12" cy="12" r="4" /> | |
| <line x1="21.17" y1="8" x2="12" y2="8" /> | |
| <line x1="3.95" y1="6.06" x2="8.54" y2="14" /> | |
| <line x1="10.88" y1="21.94" x2="15.46" y2="14" /> | |
| </svg> | |
| ); | |
| export const TrashIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <polyline points="3 6 5 6 21 6" /> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /> | |
| </svg> | |
| ); | |
| export const WifiIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <path d="M5 12.55a11 11 0 0 1 14.08 0" /> | |
| <path d="M1.42 9a16 16 0 0 1 21.16 0" /> | |
| <path d="M8.53 16.11a6 6 0 0 1 6.95 0" /> | |
| <line x1="12" y1="20" x2="12.01" y2="20" /> | |
| </svg> | |
| ); | |
| export const VolumeIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" /> | |
| <path d="M19.07 4.93a10 10 0 0 1 0 14.14" /> | |
| <path d="M15.54 8.46a5 5 0 0 1 0 7.07" /> | |
| </svg> | |
| ); | |
| export const BatteryIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <rect x="1" y="6" width="18" height="12" rx="2" ry="2" /> | |
| <line x1="23" y1="13" x2="23" y2="11" /> | |
| </svg> | |
| ); | |
| export const UserIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> | |
| <circle cx="12" cy="7" r="4" /> | |
| </svg> | |
| ); | |
| export const CloseIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <line x1="18" y1="6" x2="6" y2="18" /> | |
| <line x1="6" y1="6" x2="18" y2="18" /> | |
| </svg> | |
| ); | |
| export const MinusIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <line x1="5" y1="12" x2="19" y2="12" /> | |
| </svg> | |
| ); | |
| export const MaximizeIcon: IconComponent = ({ className, size = 24 }) => ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2" /> | |
| </svg> | |
| ); |
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 React, { useState, useEffect } from 'react'; | |
| import { useOS } from '@/app/context/OSContext'; | |
| import { UserIcon } from '../icons'; | |
| export default function LoginScreen() { | |
| const { login } = useOS(); | |
| const [username, setUsername] = useState('User'); | |
| const [password, setPassword] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [isLocked, setIsLocked] = useState(true); | |
| const [time, setTime] = useState<Date | null>(null); | |
| useEffect(() => { | |
| setTime(new Date()); | |
| const timer = setInterval(() => setTime(new Date()), 1000); | |
| return () => clearInterval(timer); | |
| }, []); | |
| const handleLogin = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (username.trim()) { | |
| setLoading(true); | |
| setTimeout(() => { | |
| login(username); | |
| setLoading(false); | |
| }, 1500); | |
| } | |
| }; | |
| const formatTime = (date: Date) => { | |
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); | |
| }; | |
| const formatDate = (date: Date) => { | |
| return date.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' }); | |
| }; | |
| // Lock Screen | |
| if (isLocked) { | |
| return ( | |
| <div | |
| className="relative h-screen w-screen overflow-hidden bg-cover bg-center flex flex-col items-center justify-center text-white select-none cursor-default" | |
| style={{ | |
| backgroundImage: 'url("https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1920&auto=format&fit=crop")', | |
| backgroundColor: '#1a1a1a' | |
| }} | |
| onClick={() => setIsLocked(false)} | |
| > | |
| <div className="flex flex-col items-center gap-4 transform -translate-y-32"> | |
| {time && ( | |
| <> | |
| <div className="text-9xl font-semibold tracking-tighter drop-shadow-md">{formatTime(time)}</div> | |
| <div className="text-3xl font-medium drop-shadow-md">{formatDate(time)}</div> | |
| </> | |
| )} | |
| </div> | |
| <div className="absolute bottom-12 text-sm font-medium animate-bounce opacity-80">Click to sign in</div> | |
| </div> | |
| ); | |
| } | |
| // Login Form | |
| return ( | |
| <div className="relative h-screen w-screen overflow-hidden bg-cover bg-center flex items-center justify-center animate-in fade-in zoom-in duration-300" | |
| style={{ | |
| backgroundImage: 'url("https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1920&auto=format&fit=crop")', | |
| backgroundColor: '#1a1a1a' | |
| }} | |
| > | |
| {/* Acrylic overlay */} | |
| <div className="absolute inset-0 bg-black/10 backdrop-blur-sm transition-all duration-500"></div> | |
| <div className="relative z-10 w-full max-w-sm flex flex-col items-center animate-in slide-in-from-bottom-8 duration-500"> | |
| <div className="w-48 h-48 bg-gray-200/20 rounded-full flex items-center justify-center mb-6 shadow-2xl"> | |
| <UserIcon size={96} className="text-white drop-shadow-lg" /> | |
| </div> | |
| <h1 className="text-2xl font-semibold text-white mb-8 drop-shadow-md">{username}</h1> | |
| {loading ? ( | |
| <div className="flex flex-col items-center gap-4"> | |
| <div className="w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin"></div> | |
| <span className="text-white font-medium">Welcome</span> | |
| </div> | |
| ) : ( | |
| <form onSubmit={handleLogin} className="w-full flex flex-col items-center gap-4"> | |
| <div className="w-full bg-black/30 backdrop-blur-md rounded-md border-b-2 border-white/50 hover:border-white focus-within:border-white focus-within:bg-white/90 group transition-colors"> | |
| <input | |
| type="password" | |
| placeholder="Password" | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| className="w-full p-2 bg-transparent text-white placeholder-gray-300 focus:text-black focus:outline-none text-center transition-colors group-focus-within:text-black group-focus-within:placeholder-gray-500" | |
| /> | |
| </div> | |
| <button | |
| type="submit" | |
| className="mt-2 px-8 py-2 bg-white/20 hover:bg-white/30 backdrop-blur-md text-white rounded-md border border-white/10 transition font-medium shadow-lg" | |
| > | |
| Sign in | |
| </button> | |
| <div onClick={() => setIsLocked(true)} className="mt-8 text-white/80 text-sm hover:text-white cursor-pointer flex items-center gap-2"> | |
| <span>← Lock Screen</span> | |
| </div> | |
| </form> | |
| )} | |
| </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 React, { useState } from 'react'; | |
| export default function Notepad() { | |
| const [text, setText] = useState(''); | |
| return ( | |
| <div className="flex flex-col h-full bg-white dark:bg-[#202020] text-black dark:text-white"> | |
| <div className="flex items-center gap-2 px-2 py-1 text-sm border-b border-gray-200 dark:border-gray-700"> | |
| <span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">File</span> | |
| <span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">Edit</span> | |
| <span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">Format</span> | |
| <span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">View</span> | |
| <span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">Help</span> | |
| </div> | |
| <textarea | |
| value={text} | |
| onChange={(e) => setText(e.target.value)} | |
| className="flex-1 w-full h-full p-4 resize-none outline-none bg-transparent font-mono text-sm" | |
| spellCheck={false} | |
| placeholder="Type something..." | |
| /> | |
| <div className="h-6 bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 flex items-center px-2 text-xs text-gray-500"> | |
| <span className="ml-auto">Ln 1, Col {text.length + 1}</span> | |
| <span className="ml-4">UTF-8</span> | |
| <span className="ml-4">Windows (CRLF)</span> | |
| </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 React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; | |
| export type AppId = 'calculator' | 'notepad' | 'explorer' | 'settings' | 'browser' | 'recycle-bin'; | |
| export interface AppConfig { | |
| id: AppId; | |
| title: string; | |
| icon: string; // We'll use string keys for icons | |
| defaultWidth: number; | |
| defaultHeight: number; | |
| } | |
| export const APPS: Record<AppId, AppConfig> = { | |
| calculator: { id: 'calculator', title: 'Calculator', icon: 'calculator', defaultWidth: 320, defaultHeight: 480 }, | |
| notepad: { id: 'notepad', title: 'Notepad', icon: 'notepad', defaultWidth: 600, defaultHeight: 400 }, | |
| explorer: { id: 'explorer', title: 'File Explorer', icon: 'folder', defaultWidth: 800, defaultHeight: 500 }, | |
| settings: { id: 'settings', title: 'Settings', icon: 'settings', defaultWidth: 700, defaultHeight: 500 }, | |
| browser: { id: 'browser', title: 'Edge', icon: 'browser', defaultWidth: 900, defaultHeight: 600 }, | |
| 'recycle-bin': { id: 'recycle-bin', title: 'Recycle Bin', icon: 'trash', defaultWidth: 600, defaultHeight: 400 }, | |
| }; | |
| export interface WindowState { | |
| id: string; // unique instance id | |
| appId: AppId; | |
| title: string; | |
| isMinimized: boolean; | |
| isMaximized: boolean; | |
| zIndex: number; | |
| position: { x: number; y: number }; | |
| size: { width: number; height: number }; | |
| } | |
| interface OSContextType { | |
| isLoggedIn: boolean; | |
| username: string; | |
| login: (username: string) => void; | |
| logout: () => void; | |
| windows: WindowState[]; | |
| activeWindowId: string | null; | |
| openWindow: (appId: AppId) => void; | |
| closeWindow: (id: string) => void; | |
| minimizeWindow: (id: string) => void; | |
| maximizeWindow: (id: string) => void; // Toggle | |
| focusWindow: (id: string) => void; | |
| updateWindowPosition: (id: string, position: { x: number; y: number }) => void; | |
| isStartMenuOpen: boolean; | |
| toggleStartMenu: () => void; | |
| closeStartMenu: () => void; | |
| theme: 'light' | 'dark'; | |
| setTheme: (theme: 'light' | 'dark') => void; | |
| wallpaper: string; | |
| setWallpaper: (url: string) => void; | |
| } | |
| const OSContext = createContext<OSContextType | undefined>(undefined); | |
| export const OSProvider = ({ children }: { children: ReactNode }) => { | |
| const [isLoggedIn, setIsLoggedIn] = useState(false); | |
| const [username, setUsername] = useState(''); | |
| const [windows, setWindows] = useState<WindowState[]>([]); | |
| const [activeWindowId, setActiveWindowId] = useState<string | null>(null); | |
| const [isStartMenuOpen, setIsStartMenuOpen] = useState(false); | |
| const [theme, setTheme] = useState<'light' | 'dark'>('light'); | |
| const [wallpaper, setWallpaper] = useState('default'); // 'default' or other keys | |
| const [nextZIndex, setNextZIndex] = useState(100); | |
| const login = (user: string) => { | |
| setUsername(user); | |
| setIsLoggedIn(true); | |
| // Optional: Save to local storage | |
| localStorage.setItem('os_username', user); | |
| }; | |
| const logout = () => { | |
| setIsLoggedIn(false); | |
| setUsername(''); | |
| setWindows([]); | |
| localStorage.removeItem('os_username'); | |
| }; | |
| useEffect(() => { | |
| const savedUser = localStorage.getItem('os_username'); | |
| if (savedUser) { | |
| login(savedUser); | |
| } | |
| }, []); | |
| const openWindow = (appId: AppId) => { | |
| const app = APPS[appId]; | |
| // Check if app allows multiple instances. For simplicity, let's say Calculator does, others maybe not? | |
| // Actually, standard windows behavior is multiple instances usually, but for a web sim, | |
| // single instance for some apps might be easier. Let's allow multiples for now. | |
| const id = `${appId}-${Date.now()}`; | |
| // Center the window roughly | |
| const startX = 100 + (windows.length * 20); | |
| const startY = 100 + (windows.length * 20); | |
| const newWindow: WindowState = { | |
| id, | |
| appId, | |
| title: app.title, | |
| isMinimized: false, | |
| isMaximized: false, | |
| zIndex: nextZIndex, | |
| position: { x: startX, y: startY }, | |
| size: { width: app.defaultWidth, height: app.defaultHeight }, | |
| }; | |
| setWindows([...windows, newWindow]); | |
| setActiveWindowId(id); | |
| setNextZIndex(prev => prev + 1); | |
| setIsStartMenuOpen(false); | |
| }; | |
| const closeWindow = (id: string) => { | |
| setWindows(prev => prev.filter(w => w.id !== id)); | |
| if (activeWindowId === id) { | |
| setActiveWindowId(null); | |
| } | |
| }; | |
| const minimizeWindow = (id: string) => { | |
| setWindows(prev => prev.map(w => w.id === id ? { ...w, isMinimized: true } : w)); | |
| if (activeWindowId === id) { | |
| setActiveWindowId(null); | |
| } | |
| }; | |
| const maximizeWindow = (id: string) => { | |
| setWindows(prev => prev.map(w => w.id === id ? { ...w, isMaximized: !w.isMaximized } : w)); | |
| focusWindow(id); | |
| }; | |
| const focusWindow = (id: string) => { | |
| setActiveWindowId(id); | |
| setWindows(prev => prev.map(w => w.id === id ? { ...w, isMinimized: false, zIndex: nextZIndex } : w)); | |
| setNextZIndex(prev => prev + 1); | |
| }; | |
| const updateWindowPosition = (id: string, position: { x: number; y: number }) => { | |
| setWindows(prev => prev.map(w => w.id === id ? { ...w, position } : w)); | |
| }; | |
| const toggleStartMenu = () => setIsStartMenuOpen(prev => !prev); | |
| const closeStartMenu = () => setIsStartMenuOpen(false); | |
| return ( | |
| <OSContext.Provider value={{ | |
| isLoggedIn, | |
| username, | |
| login, | |
| logout, | |
| windows, | |
| activeWindowId, | |
| openWindow, | |
| closeWindow, | |
| minimizeWindow, | |
| maximizeWindow, | |
| focusWindow, | |
| updateWindowPosition, | |
| isStartMenuOpen, | |
| toggleStartMenu, | |
| closeStartMenu, | |
| theme, | |
| setTheme, | |
| wallpaper, | |
| setWallpaper | |
| }}> | |
| {children} | |
| </OSContext.Provider> | |
| ); | |
| }; | |
| export const useOS = () => { | |
| const context = useContext(OSContext); | |
| if (context === undefined) { | |
| throw new Error('useOS must be used within an OSProvider'); | |
| } | |
| return context; | |
| }; |
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 { OSProvider, useOS } from './context/OSContext'; | |
| import LoginScreen from './components/os/LoginScreen'; | |
| import Desktop from './components/os/Desktop'; | |
| function AppContent() { | |
| const { isLoggedIn } = useOS(); | |
| return ( | |
| <> | |
| {isLoggedIn ? <Desktop /> : <LoginScreen />} | |
| </> | |
| ); | |
| } | |
| export default function Home() { | |
| return ( | |
| <OSProvider> | |
| <AppContent /> | |
| </OSProvider> | |
| ); | |
| } |
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 React from 'react'; | |
| import { useOS } from '@/app/context/OSContext'; | |
| export default function Settings() { | |
| const { theme, setTheme, wallpaper, setWallpaper } = useOS(); | |
| const wallpapers = [ | |
| { id: 'default', url: 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', name: 'Blue Gradient' }, | |
| { id: 'mountain', url: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', name: 'Mountains' }, | |
| { id: 'dark', url: 'https://images.unsplash.com/photo-1550684848-fac1c5b4e853?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', name: 'Dark Mode' }, | |
| ]; | |
| return ( | |
| <div className="flex h-full bg-[#f3f3f3] dark:bg-[#202020] text-black dark:text-white"> | |
| <div className="w-64 p-4 border-r border-gray-200 dark:border-gray-700 flex flex-col gap-2"> | |
| <div className="text-xl font-semibold mb-4 px-2">Settings</div> | |
| <div className="bg-white dark:bg-white/10 px-3 py-2 rounded-md font-medium shadow-sm">Personalization</div> | |
| <div className="px-3 py-2 rounded-md hover:bg-gray-200 dark:hover:bg-white/5 text-gray-600 dark:text-gray-400">System</div> | |
| <div className="px-3 py-2 rounded-md hover:bg-gray-200 dark:hover:bg-white/5 text-gray-600 dark:text-gray-400">Bluetooth & devices</div> | |
| <div className="px-3 py-2 rounded-md hover:bg-gray-200 dark:hover:bg-white/5 text-gray-600 dark:text-gray-400">Network & internet</div> | |
| </div> | |
| <div className="flex-1 p-8 overflow-y-auto"> | |
| <h2 className="text-2xl font-semibold mb-6">Personalization</h2> | |
| <div className="mb-8"> | |
| <div className="text-sm font-medium mb-3 text-gray-500">Select a theme</div> | |
| <div className="flex gap-4"> | |
| <button | |
| onClick={() => setTheme('light')} | |
| className={`flex flex-col items-center gap-2 p-2 rounded-lg border-2 ${theme === 'light' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-transparent hover:bg-gray-200 dark:hover:bg-white/5'}`} | |
| > | |
| <div className="w-32 h-20 bg-white rounded shadow-md border border-gray-200"></div> | |
| <span className="text-sm">Light</span> | |
| </button> | |
| <button | |
| onClick={() => setTheme('dark')} | |
| className={`flex flex-col items-center gap-2 p-2 rounded-lg border-2 ${theme === 'dark' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-transparent hover:bg-gray-200 dark:hover:bg-white/5'}`} | |
| > | |
| <div className="w-32 h-20 bg-[#202020] rounded shadow-md border border-gray-700"></div> | |
| <span className="text-sm">Dark</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-sm font-medium mb-3 text-gray-500">Background</div> | |
| <div className="grid grid-cols-3 gap-4"> | |
| {wallpapers.map((wp) => ( | |
| <button | |
| key={wp.id} | |
| onClick={() => setWallpaper(wp.id)} | |
| className={`group relative rounded-lg overflow-hidden aspect-video border-2 transition-all ${wallpaper === wp.id ? 'border-blue-500 ring-2 ring-blue-200' : 'border-transparent hover:border-gray-300'}`} | |
| > | |
| <img src={wp.url} alt={wp.name} className="w-full h-full object-cover" /> | |
| <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors"></div> | |
| <span className="absolute bottom-2 left-2 text-xs text-white font-medium shadow-black drop-shadow-md">{wp.name}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </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 React, { useState, useEffect } from 'react'; | |
| import { useOS, APPS, AppId } from '@/app/context/OSContext'; | |
| import { StartIcon, WifiIcon, VolumeIcon, BatteryIcon, CalculatorIcon, NotepadIcon, FolderIcon, SettingsIcon, BrowserIcon, TrashIcon } from '../icons'; | |
| const AppIcon = ({ id, size = 24 }: { id: string, size?: number }) => { | |
| switch(id) { | |
| case 'calculator': return <CalculatorIcon size={size} className="text-orange-500" />; | |
| case 'notepad': return <NotepadIcon size={size} className="text-blue-400" />; | |
| case 'explorer': return <FolderIcon size={size} className="text-yellow-400" />; | |
| case 'settings': return <SettingsIcon size={size} className="text-gray-500 dark:text-gray-400" />; | |
| case 'browser': return <BrowserIcon size={size} className="text-blue-600" />; | |
| case 'recycle-bin': return <TrashIcon size={size} className="text-gray-500" />; | |
| default: return <div className="w-6 h-6 bg-gray-400 rounded"></div>; | |
| } | |
| }; | |
| export default function Taskbar() { | |
| const { toggleStartMenu, isStartMenuOpen, openWindow, windows, activeWindowId, minimizeWindow, focusWindow } = useOS(); | |
| const [time, setTime] = useState<Date | null>(null); | |
| useEffect(() => { | |
| setTime(new Date()); | |
| const timer = setInterval(() => setTime(new Date()), 1000); | |
| return () => clearInterval(timer); | |
| }, []); | |
| const formatTime = (date: Date) => { | |
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| }; | |
| const formatDate = (date: Date) => { | |
| return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }); | |
| }; | |
| const pinnedApps: AppId[] = ['explorer', 'browser', 'notepad', 'calculator', 'settings']; | |
| const handleAppClick = (appId: AppId) => { | |
| // Check if window exists | |
| const existingWindow = windows.find(w => w.appId === appId && !w.isMinimized); | |
| const existingMinimized = windows.find(w => w.appId === appId && w.isMinimized); | |
| if (existingWindow) { | |
| if (activeWindowId === existingWindow.id) { | |
| minimizeWindow(existingWindow.id); | |
| } else { | |
| focusWindow(existingWindow.id); | |
| } | |
| } else if (existingMinimized) { | |
| focusWindow(existingMinimized.id); | |
| } else { | |
| openWindow(appId); | |
| } | |
| }; | |
| const isAppOpen = (appId: AppId) => windows.some(w => w.appId === appId); | |
| const isAppActive = (appId: AppId) => { | |
| const win = windows.find(w => w.id === activeWindowId); | |
| return win?.appId === appId; | |
| }; | |
| return ( | |
| <div className="h-12 bg-[#f3f3f3]/85 dark:bg-[#202020]/85 backdrop-blur-md border-t border-white/20 dark:border-gray-700 flex items-center justify-between px-4 relative z-50 select-none"> | |
| <div className="flex-1"></div> {/* Spacer */} | |
| {/* Center Dock */} | |
| <div className="flex items-center gap-1 h-full"> | |
| <button | |
| onClick={toggleStartMenu} | |
| className={`p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-all active:scale-95 ${isStartMenuOpen ? 'bg-white/50 dark:bg-white/10' : ''}`} | |
| title="Start" | |
| > | |
| <StartIcon className="text-blue-600 dark:text-blue-400" /> | |
| </button> | |
| {pinnedApps.map(appId => ( | |
| <button | |
| key={appId} | |
| onClick={() => handleAppClick(appId)} | |
| className={`relative group p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-all active:scale-95 flex flex-col items-center justify-center h-10 w-10 ${isAppActive(appId) ? 'bg-white/50 dark:bg-white/10' : ''}`} | |
| > | |
| <AppIcon id={appId} /> | |
| {isAppOpen(appId) && ( | |
| <div className={`absolute bottom-0 w-1.5 h-1 rounded-full transition-all ${isAppActive(appId) ? 'bg-blue-500 w-4' : 'bg-gray-400'}`}></div> | |
| )} | |
| <span className="absolute -top-10 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none"> | |
| {APPS[appId].title} | |
| </span> | |
| </button> | |
| ))} | |
| </div> | |
| {/* System Tray */} | |
| <div className="flex-1 flex items-center justify-end gap-2 h-full"> | |
| <div className="hidden sm:flex items-center gap-1 hover:bg-white/50 dark:hover:bg-white/10 px-2 py-1 rounded transition-colors cursor-default"> | |
| <span className="text-xs text-gray-500 dark:text-gray-400">ENG</span> | |
| </div> | |
| <div className="flex items-center gap-3 hover:bg-white/50 dark:hover:bg-white/10 px-2 py-1 rounded transition-colors cursor-default"> | |
| <WifiIcon size={16} /> | |
| <VolumeIcon size={16} /> | |
| <BatteryIcon size={16} /> | |
| </div> | |
| <div className="flex flex-col items-end justify-center px-2 py-1 hover:bg-white/50 dark:hover:bg-white/10 rounded transition-colors cursor-default leading-tight"> | |
| {time && ( | |
| <> | |
| <span className="text-xs font-medium text-gray-800 dark:text-gray-200">{formatTime(time)}</span> | |
| <span className="text-[10px] text-gray-600 dark:text-gray-400">{formatDate(time)}</span> | |
| </> | |
| )} | |
| </div> | |
| <div className="w-1 h-full border-l border-gray-300 dark:border-gray-600 ml-2"></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 const WALLPAPERS = [ | |
| { id: 'default', url: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1920&auto=format&fit=crop', name: 'Windows Light' }, | |
| { id: 'dark', url: 'https://images.unsplash.com/photo-1483728642387-6c3bdd6c93e5?q=80&w=1920&auto=format&fit=crop', name: 'Windows Dark' }, | |
| { id: 'mountain', url: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=1920&auto=format&fit=crop', name: 'Mountains' }, | |
| ]; | |
| export const getWallpaperUrl = (id: string) => { | |
| return WALLPAPERS.find(w => w.id === id)?.url || WALLPAPERS[0].url; | |
| }; |
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 React, { useState, useEffect, useRef } from 'react'; | |
| import { useOS, WindowState, APPS } from '@/app/context/OSContext'; | |
| import { CloseIcon, MinusIcon, MaximizeIcon } from '../icons'; | |
| interface WindowFrameProps { | |
| windowState: WindowState; | |
| children: React.ReactNode; | |
| } | |
| export default function WindowFrame({ windowState, children }: WindowFrameProps) { | |
| const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, updateWindowPosition } = useOS(); | |
| const [isDragging, setIsDragging] = useState(false); | |
| const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); | |
| const [currentPos, setCurrentPos] = useState(windowState.position); | |
| const [isMaximized, setIsMaximized] = useState(windowState.isMaximized); | |
| const appConfig = APPS[windowState.appId]; | |
| useEffect(() => { | |
| // Sync prop changes to local state if needed (e.g. if changed externally) | |
| setCurrentPos(windowState.position); | |
| setIsMaximized(windowState.isMaximized); | |
| }, [windowState.position, windowState.isMaximized]); | |
| const handleMouseDown = (e: React.MouseEvent) => { | |
| focusWindow(windowState.id); | |
| }; | |
| const handleDragStart = (e: React.PointerEvent) => { | |
| if (isMaximized) return; | |
| e.preventDefault(); | |
| setIsDragging(true); | |
| const rect = (e.target as HTMLElement).closest('.window-frame')?.getBoundingClientRect(); | |
| if (rect) { | |
| setDragOffset({ | |
| x: e.clientX - rect.left, | |
| y: e.clientY - rect.top | |
| }); | |
| } | |
| (e.target as HTMLElement).setPointerCapture(e.pointerId); | |
| }; | |
| const handleDrag = (e: React.PointerEvent) => { | |
| if (!isDragging) return; | |
| e.preventDefault(); | |
| const newX = e.clientX - dragOffset.x; | |
| const newY = e.clientY - dragOffset.y; | |
| setCurrentPos({ x: newX, y: newY }); | |
| }; | |
| const handleDragEnd = (e: React.PointerEvent) => { | |
| if (!isDragging) return; | |
| setIsDragging(false); | |
| (e.target as HTMLElement).releasePointerCapture(e.pointerId); | |
| updateWindowPosition(windowState.id, currentPos); | |
| }; | |
| if (windowState.isMinimized) return null; | |
| const style: React.CSSProperties = isMaximized ? { | |
| top: 0, | |
| left: 0, | |
| width: '100%', | |
| height: 'calc(100% - 48px)', // Subtract taskbar height | |
| transform: 'none', | |
| borderRadius: 0, | |
| } : { | |
| transform: `translate(${currentPos.x}px, ${currentPos.y}px)`, | |
| width: windowState.size.width, | |
| height: windowState.size.height, | |
| }; | |
| return ( | |
| <div | |
| className={`window-frame absolute flex flex-col bg-white dark:bg-gray-800 shadow-2xl rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 transition-shadow animate-in fade-in zoom-in-95 duration-200 ${isMaximized ? '' : 'rounded-lg'}`} | |
| style={{ | |
| ...style, | |
| zIndex: windowState.zIndex, | |
| }} | |
| onMouseDown={handleMouseDown} | |
| > | |
| {/* Title Bar */} | |
| <div | |
| className="h-10 bg-gray-100 dark:bg-gray-900 flex items-center justify-between px-2 select-none border-b border-gray-200 dark:border-gray-700" | |
| onPointerDown={handleDragStart} | |
| onPointerMove={handleDrag} | |
| onPointerUp={handleDragEnd} | |
| > | |
| <div className="flex items-center gap-2 pl-2"> | |
| {/* We could render the app icon here */} | |
| <span className="font-medium text-sm text-gray-700 dark:text-gray-300">{windowState.title}</span> | |
| </div> | |
| <div className="flex items-center h-full"> | |
| <button | |
| onClick={(e) => { e.stopPropagation(); minimizeWindow(windowState.id); }} | |
| className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500 transition-colors" | |
| > | |
| <MinusIcon size={14} /> | |
| </button> | |
| <button | |
| onClick={(e) => { e.stopPropagation(); maximizeWindow(windowState.id); }} | |
| className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500 transition-colors" | |
| > | |
| <MaximizeIcon size={12} /> | |
| </button> | |
| <button | |
| onClick={(e) => { e.stopPropagation(); closeWindow(windowState.id); }} | |
| className="w-10 h-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-gray-500 transition-colors" | |
| > | |
| <CloseIcon size={14} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="flex-1 overflow-auto relative"> | |
| {children} | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment