From 2c7e6d830880f05d0a4c833cf84928ff856f1451 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Wed, 17 Dec 2025 01:43:33 +0000 Subject: [PATCH] Add customizable theme and layout options to the operating system Introduces `ThemeSettings` and `DesktopLayout` interfaces, initializes theme and layout state from localStorage, fetches weather data, and updates the `SettingsApp` component to manage theme preferences (mode, accent color, transparency) and save/load desktop layouts. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 8a42c24d-c4b5-4944-b3d7-21cfa32831c0 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/wSOtJcj Replit-Helium-Checkpoint-Created: true --- client/src/pages/os.tsx | 533 ++++++++++++++++++++++++++++++++++------ 1 file changed, 459 insertions(+), 74 deletions(-) diff --git a/client/src/pages/os.tsx b/client/src/pages/os.tsx index 8ebae67..d67863c 100644 --- a/client/src/pages/os.tsx +++ b/client/src/pages/os.tsx @@ -37,6 +37,18 @@ interface Toast { type: 'info' | 'success' | 'warning' | 'error'; } +interface ThemeSettings { + mode: 'dark' | 'light' | 'system'; + accentColor: string; + transparency: number; +} + +interface DesktopLayout { + name: string; + windows: Array<{ appId: string; x: number; y: number; width: number; height: number }>; + desktop: number; +} + const ACCENT_COLORS = [ { id: 'cyan', name: 'Cyan', color: '#06b6d4', ring: 'ring-cyan-400/50', shadow: 'shadow-cyan-500/20', bg: 'bg-cyan-500' }, { id: 'purple', name: 'Purple', color: '#a855f7', ring: 'ring-purple-400/50', shadow: 'shadow-purple-500/20', bg: 'bg-purple-500' }, @@ -98,18 +110,44 @@ export default function AeThexOS() { const [showOnboarding, setShowOnboarding] = useState(false); const [onboardingStep, setOnboardingStep] = useState(0); const [desktopIcons, setDesktopIcons] = useState([]); + const [theme, setTheme] = useState(() => { + const saved = localStorage.getItem('aethex-theme'); + return saved ? JSON.parse(saved) : { mode: 'dark', accentColor: 'cyan', transparency: 80 }; + }); + const [savedLayouts, setSavedLayouts] = useState(() => { + const saved = localStorage.getItem('aethex-layouts'); + return saved ? JSON.parse(saved) : []; + }); const desktopRef = useRef(null); const idleTimer = useRef(null); const spotlightRef = useRef(null); const { user, isAuthenticated, logout } = useAuth(); const [, setLocation] = useLocation(); + const { data: weatherData } = useQuery({ + queryKey: ['weather'], + queryFn: async () => { + const res = await fetch('https://api.open-meteo.com/v1/forecast?latitude=40.7128&longitude=-74.0060¤t_weather=true&temperature_unit=fahrenheit'); + return res.json(); + }, + refetchInterval: 600000, + staleTime: 300000, + }); + const addToast = useCallback((message: string, type: Toast['type'] = 'info') => { const id = Date.now().toString(); setToasts(prev => [...prev, { id, message, type }]); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 4000); }, []); + useEffect(() => { + localStorage.setItem('aethex-theme', JSON.stringify(theme)); + }, [theme]); + + useEffect(() => { + localStorage.setItem('aethex-layouts', JSON.stringify(savedLayouts)); + }, [savedLayouts]); + useEffect(() => { const bootSequence = async () => { const steps = [ @@ -414,7 +452,54 @@ export default function AeThexOS() { case 'chat': return ; case 'music': return ; case 'pitch': return setLocation('/pitch')} />; - case 'settings': return ; + case 'settings': return { + const layout: DesktopLayout = { + name, + windows: windows.map(w => ({ appId: w.component, x: w.x, y: w.y, width: w.width, height: w.height })), + desktop: currentDesktop, + }; + setSavedLayouts(prev => [...prev.filter(l => l.name !== name), layout]); + addToast(`Layout "${name}" saved`, 'success'); + }} + onLoadLayout={(layout) => { + setWindows([]); + setTimeout(() => { + layout.windows.forEach((w, i) => { + const app = apps.find(a => a.component === w.appId); + if (app) { + setWindows(prev => [...prev, { + id: `${app.id}-${Date.now()}-${i}`, + title: app.title, + icon: app.icon, + component: app.component, + x: w.x, + y: w.y, + width: w.width, + height: w.height, + minimized: false, + maximized: false, + zIndex: i + 1, + }]); + } + }); + setCurrentDesktop(layout.desktop); + addToast(`Layout "${layout.name}" loaded`, 'success'); + }, 100); + }} + onDeleteLayout={(name) => { + setSavedLayouts(prev => prev.filter(l => l.name !== name)); + addToast(`Layout "${name}" deleted`, 'info'); + }} + />; default: return null; } }; @@ -487,7 +572,7 @@ export default function AeThexOS() { >
- +
{apps.slice(0, 9).map((app) => ( @@ -594,7 +679,11 @@ export default function AeThexOS() { ); } -function DesktopWidgets({ time }: { time: Date }) { +function DesktopWidgets({ time, weather, notifications }: { + time: Date; + weather?: { current_weather?: { temperature: number; windspeed: number; weathercode: number } }; + notifications?: string[]; +}) { const { data: metrics } = useQuery({ queryKey: ['os-metrics'], queryFn: async () => { @@ -604,19 +693,57 @@ function DesktopWidgets({ time }: { time: Date }) { refetchInterval: 30000, }); + const getWeatherIcon = (code: number) => { + if (code === 0) return '☀️'; + if (code <= 3) return '⛅'; + if (code <= 48) return '🌫️'; + if (code <= 67) return '🌧️'; + if (code <= 77) return '🌨️'; + if (code <= 82) return '🌧️'; + if (code <= 86) return '🌨️'; + return '⛈️'; + }; + return ( -
-
+
+
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
-
+ + + {weather?.current_weather && ( + +
Weather
+
+ {getWeatherIcon(weather.current_weather.weathercode)} +
+
{Math.round(weather.current_weather.temperature)}°F
+
Wind: {weather.current_weather.windspeed} mph
+
+
+
+ )} {metrics && ( -
+
System Status
@@ -632,7 +759,23 @@ function DesktopWidgets({ time }: { time: Date }) { {metrics.onlineUsers || 0}
-
+ + )} + + {notifications && notifications.length > 0 && ( + +
Notifications
+
+ {notifications.slice(0, 3).map((n, i) => ( +
{n}
+ ))} +
+
)}
); @@ -2034,71 +2177,193 @@ function PitchApp({ onNavigate }: { onNavigate: () => void }) { ); } -function SettingsApp({ wallpaper, setWallpaper, soundEnabled, setSoundEnabled, secretsUnlocked }: { +function SettingsApp({ wallpaper, setWallpaper, soundEnabled, setSoundEnabled, secretsUnlocked, theme, setTheme, savedLayouts, onSaveLayout, onLoadLayout, onDeleteLayout }: { wallpaper: typeof WALLPAPERS[0]; setWallpaper: (w: typeof WALLPAPERS[0]) => void; soundEnabled: boolean; setSoundEnabled: (v: boolean) => void; secretsUnlocked: boolean; + theme: ThemeSettings; + setTheme: (t: ThemeSettings) => void; + savedLayouts: DesktopLayout[]; + onSaveLayout: (name: string) => void; + onLoadLayout: (layout: DesktopLayout) => void; + onDeleteLayout: (name: string) => void; }) { + const [layoutName, setLayoutName] = useState(''); + const [activeTab, setActiveTab] = useState<'appearance' | 'layouts' | 'system'>('appearance'); const visibleWallpapers = WALLPAPERS.filter(wp => !wp.secret || secretsUnlocked); return ( -
-

System Settings

- -
-
-
- Appearance {secretsUnlocked && ✨ SECRETS UNLOCKED} +
+
+ {(['appearance', 'layouts', 'system'] as const).map(tab => ( + + ))} +
+ +
+ {activeTab === 'appearance' && ( +
+
+
+ Accent Color +
+
+ {ACCENT_COLORS.map(color => ( +
+
+ +
+
+ Theme Mode +
+
+ {(['dark', 'light', 'system'] as const).map(mode => ( + + ))} +
+
Note: Light mode is preview only
+
+ +
+
+ Wallpaper {secretsUnlocked && ✨ UNLOCKED} +
+
+ {visibleWallpapers.map(wp => ( + + ))} +
+
+ +
+
+ Transparency +
+ setTheme({ ...theme, transparency: parseInt(e.target.value) })} + className="w-full accent-cyan-500" + /> +
+ More glass + {theme.transparency}% + More solid +
+
-
- {visibleWallpapers.map(wp => ( - +
+
+ +
+
Saved Layouts
+ {savedLayouts.length === 0 ? ( +
+ No saved layouts yet. Arrange your windows and save a layout above. +
+ ) : ( +
+ {savedLayouts.map(layout => ( +
+
+
{layout.name}
+
{layout.windows.length} windows • Desktop {layout.desktop + 1}
+
+
+ + +
+
+ ))} +
+ )} +
+
+ )} + + {activeTab === 'system' && ( +
+
+
+
Sound Effects
+
UI interaction feedback
+
+ - ))} -
-
- -
-
System
- -
-
-
Sound Effects
-
UI interaction feedback
- -
- -
-
-
Dark Mode
-
Always enabled
+ +
+
AeThex OS v3.0.0
+
Build 2025.12.17
-
-
-
-
-
- -
-
AeThex OS v3.0.0
-
Build 2025.12.16
-
- {!secretsUnlocked && ( -
-
🔒 Hidden features available...
-
Try the Konami Code or find secrets in Terminal
+ {!secretsUnlocked && ( +
+
🔒 Hidden features available...
+
Try the Konami Code or find secrets in Terminal
+
+ )}
)}
@@ -2281,7 +2546,7 @@ function MetricsDashboardApp() { } function CodeEditorApp() { - const sampleCode = `// AeThex Smart Contract + const defaultCode = `// AeThex Smart Contract import { Aegis } from '@aethex/core'; interface Architect { @@ -2317,33 +2582,153 @@ class MetaverseRegistry { } }`; + const [code, setCode] = useState(defaultCode); + const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 }); + const [showAutocomplete, setShowAutocomplete] = useState(false); + const [autocompleteItems, setAutocompleteItems] = useState([]); + const textareaRef = useRef(null); + + const keywords = ['const', 'let', 'var', 'function', 'class', 'interface', 'type', 'async', 'await', + 'return', 'import', 'export', 'from', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', + 'new', 'this', 'super', 'extends', 'implements', 'private', 'public', 'protected', 'static']; + + const snippets = ['console.log()', 'Aegis.verify()', 'Aegis.initialize()', 'generateId()', + 'Promise<>', 'Map<>', 'Array<>', 'string', 'number', 'boolean']; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + e.preventDefault(); + const start = textareaRef.current?.selectionStart || 0; + const end = textareaRef.current?.selectionEnd || 0; + setCode(code.substring(0, start) + ' ' + code.substring(end)); + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.selectionStart = textareaRef.current.selectionEnd = start + 2; + } + }, 0); + } else if (e.ctrlKey && e.key === ' ') { + e.preventDefault(); + const cursorIndex = textareaRef.current?.selectionStart || 0; + const textBefore = code.substring(0, cursorIndex); + const lastWord = textBefore.split(/[\s\n\(\)\{\}\[\];:,]/).pop() || ''; + const matches = [...keywords, ...snippets].filter(k => k.toLowerCase().startsWith(lastWord.toLowerCase())); + setAutocompleteItems(matches.slice(0, 8)); + setShowAutocomplete(matches.length > 0); + } else if (e.key === 'Escape') { + setShowAutocomplete(false); + } + }; + + const insertAutocomplete = (item: string) => { + const cursorIndex = textareaRef.current?.selectionStart || 0; + const textBefore = code.substring(0, cursorIndex); + const lastWordMatch = textBefore.match(/[\w]+$/); + const lastWordStart = lastWordMatch ? cursorIndex - lastWordMatch[0].length : cursorIndex; + setCode(code.substring(0, lastWordStart) + item + code.substring(cursorIndex)); + setShowAutocomplete(false); + textareaRef.current?.focus(); + }; + + const updateCursorPos = () => { + if (!textareaRef.current) return; + const pos = textareaRef.current.selectionStart; + const lines = code.substring(0, pos).split('\n'); + setCursorPos({ line: lines.length, col: (lines[lines.length - 1]?.length || 0) + 1 }); + }; + + const highlightLine = (line: string) => { + const parts: { text: string; color: string }[] = []; + let remaining = line; + + const patterns = [ + { regex: /^(\/\/.*)$/, color: 'text-green-500' }, + { regex: /^(\s*)(import|export|from|as)(\s)/, color: 'text-purple-400', capture: 2 }, + { regex: /(interface|class|type|enum)(\s+)(\w+)/, colors: ['text-purple-400', '', 'text-yellow-300'] }, + { regex: /(const|let|var|function|async|await|return|if|else|for|while|new|this|private|public|static)/, color: 'text-purple-400' }, + { regex: /('[^']*'|"[^"]*"|`[^`]*`)/, color: 'text-orange-400' }, + { regex: /(\d+)/, color: 'text-cyan-300' }, + { regex: /(@\w+)/, color: 'text-yellow-400' }, + ]; + + if (line.trim().startsWith('//')) { + return [{ text: line, color: 'text-green-500' }]; + } + + let result = line; + result = result.replace(/(import|export|from|as|interface|class|type|const|let|var|function|async|await|return|if|else|for|while|new|this|private|public|static|extends|implements)\b/g, + '$1'); + result = result.replace(/('[^']*'|"[^"]*"|`[^`]*`)/g, '$1'); + result = result.replace(/\b(\d+)\b/g, '$1'); + result = result.replace(/(@\w+)/g, '$1'); + result = result.replace(/\b(string|number|boolean|void|any|never|unknown|null|undefined|true|false)\b/g, + '$1'); + result = result.replace(/\b([A-Z]\w*)\b(?!$1'); + + return result; + }; + return (
registry.ts + ~ +
+
+ +
-
-
-          {sampleCode.split('\n').map((line, i) => (
-            
- {i + 1} - {line || ' '} + +
+
+
+ {code.split('\n').map((_, i) => ( +
{i + 1}
+ ))} +
+
+
+ {code.split('\n').map((line, i) => ( +
+ ))}
- ))} -
+