From d3a7b7ce07d2b2cdfcbe315d5e1cf6e4846e62c2 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Tue, 10 Mar 2026 19:32:22 -0700 Subject: [PATCH] Add light mode toggle --- frontend/src/components/StatusBar.tsx | 36 +++++++++++++++++++++++++-- frontend/src/test/statusBar.test.tsx | 19 +++++++++++++- frontend/src/utils/theme.ts | 6 +++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/StatusBar.tsx b/frontend/src/components/StatusBar.tsx index 1b5c97e..76660b4 100644 --- a/frontend/src/components/StatusBar.tsx +++ b/frontend/src/components/StatusBar.tsx @@ -1,9 +1,10 @@ -import { useState } from 'react'; -import { Menu } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Menu, Moon, Sun } from 'lucide-react'; import type { HealthStatus, RadioConfig } from '../types'; import { api } from '../api'; import { toast } from './ui/sonner'; import { handleKeyboardActivate } from '../utils/a11y'; +import { applyTheme, getSavedTheme, THEME_CHANGE_EVENT } from '../utils/theme'; import { cn } from '@/lib/utils'; interface StatusBarProps { @@ -29,6 +30,19 @@ export function StatusBar({ ? 'Radio OK' : 'Radio Disconnected'; const [reconnecting, setReconnecting] = useState(false); + const [currentTheme, setCurrentTheme] = useState(getSavedTheme); + + useEffect(() => { + const handleThemeChange = (event: Event) => { + const themeId = (event as CustomEvent).detail; + setCurrentTheme(typeof themeId === 'string' && themeId ? themeId : getSavedTheme()); + }; + + window.addEventListener(THEME_CHANGE_EVENT, handleThemeChange as EventListener); + return () => { + window.removeEventListener(THEME_CHANGE_EVENT, handleThemeChange as EventListener); + }; + }, []); const handleReconnect = async () => { setReconnecting(true); @@ -46,6 +60,12 @@ export function StatusBar({ } }; + const handleThemeToggle = () => { + const nextTheme = currentTheme === 'light' ? 'original' : 'light'; + applyTheme(nextTheme); + setCurrentTheme(nextTheme); + }; + return (
{/* Mobile menu button - only visible on small screens */} @@ -128,6 +148,18 @@ export function StatusBar({ > {settingsMode ? 'Back to Chat' : 'Settings'} +
); } diff --git a/frontend/src/test/statusBar.test.tsx b/frontend/src/test/statusBar.test.tsx index 0ee8446..c664af6 100644 --- a/frontend/src/test/statusBar.test.tsx +++ b/frontend/src/test/statusBar.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { StatusBar } from '../components/StatusBar'; @@ -47,4 +47,21 @@ describe('StatusBar', () => { expect(screen.getByRole('status', { name: 'Radio Disconnected' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Reconnect' })).toBeInTheDocument(); }); + + it('toggles between classic and light themes from the shortcut button', () => { + localStorage.setItem('remoteterm-theme', 'cyberpunk'); + + render(); + + const themeToggle = screen.getByRole('button', { name: 'Switch to light theme' }); + fireEvent.click(themeToggle); + + expect(localStorage.getItem('remoteterm-theme')).toBe('light'); + expect(document.documentElement.dataset.theme).toBe('light'); + + fireEvent.click(screen.getByRole('button', { name: 'Switch to classic theme' })); + + expect(localStorage.getItem('remoteterm-theme')).toBe('original'); + expect(document.documentElement.dataset.theme).toBeUndefined(); + }); }); diff --git a/frontend/src/utils/theme.ts b/frontend/src/utils/theme.ts index 9324d2b..f240b68 100644 --- a/frontend/src/utils/theme.ts +++ b/frontend/src/utils/theme.ts @@ -7,6 +7,8 @@ export interface Theme { metaThemeColor: string; } +export const THEME_CHANGE_EVENT = 'remoteterm-theme-change'; + export const THEMES: Theme[] = [ { id: 'original', @@ -77,4 +79,8 @@ export function applyTheme(themeId: string): void { meta.setAttribute('content', theme.metaThemeColor); } } + + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, { detail: themeId })); + } }