Add light mode toggle

This commit is contained in:
Jack Kingsman
2026-03-10 19:32:22 -07:00
parent 42ca242ee1
commit d3a7b7ce07
3 changed files with 58 additions and 3 deletions

View File

@@ -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<string>).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 (
<header className="flex items-center gap-3 px-4 py-2.5 bg-card border-b border-border text-xs">
{/* Mobile menu button - only visible on small screens */}
@@ -128,6 +148,18 @@ export function StatusBar({
>
{settingsMode ? 'Back to Chat' : 'Settings'}
</button>
<button
onClick={handleThemeToggle}
className="p-0.5 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
title={currentTheme === 'light' ? 'Switch to classic theme' : 'Switch to light theme'}
aria-label={currentTheme === 'light' ? 'Switch to classic theme' : 'Switch to light theme'}
>
{currentTheme === 'light' ? (
<Sun className="h-4 w-4" aria-hidden="true" />
) : (
<Moon className="h-4 w-4" aria-hidden="true" />
)}
</button>
</header>
);
}

View File

@@ -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(<StatusBar health={baseHealth} config={null} onSettingsClick={vi.fn()} />);
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();
});
});

View File

@@ -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 }));
}
}