mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-11 11:13:04 +02:00
Add light mode toggle
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Menu } from 'lucide-react';
|
import { Menu, Moon, Sun } from 'lucide-react';
|
||||||
import type { HealthStatus, RadioConfig } from '../types';
|
import type { HealthStatus, RadioConfig } from '../types';
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { toast } from './ui/sonner';
|
import { toast } from './ui/sonner';
|
||||||
import { handleKeyboardActivate } from '../utils/a11y';
|
import { handleKeyboardActivate } from '../utils/a11y';
|
||||||
|
import { applyTheme, getSavedTheme, THEME_CHANGE_EVENT } from '../utils/theme';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface StatusBarProps {
|
interface StatusBarProps {
|
||||||
@@ -29,6 +30,19 @@ export function StatusBar({
|
|||||||
? 'Radio OK'
|
? 'Radio OK'
|
||||||
: 'Radio Disconnected';
|
: 'Radio Disconnected';
|
||||||
const [reconnecting, setReconnecting] = useState(false);
|
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 () => {
|
const handleReconnect = async () => {
|
||||||
setReconnecting(true);
|
setReconnecting(true);
|
||||||
@@ -46,6 +60,12 @@ export function StatusBar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleThemeToggle = () => {
|
||||||
|
const nextTheme = currentTheme === 'light' ? 'original' : 'light';
|
||||||
|
applyTheme(nextTheme);
|
||||||
|
setCurrentTheme(nextTheme);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex items-center gap-3 px-4 py-2.5 bg-card border-b border-border text-xs">
|
<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 */}
|
{/* Mobile menu button - only visible on small screens */}
|
||||||
@@ -128,6 +148,18 @@ export function StatusBar({
|
|||||||
>
|
>
|
||||||
{settingsMode ? 'Back to Chat' : 'Settings'}
|
{settingsMode ? 'Back to Chat' : 'Settings'}
|
||||||
</button>
|
</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>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { StatusBar } from '../components/StatusBar';
|
import { StatusBar } from '../components/StatusBar';
|
||||||
@@ -47,4 +47,21 @@ describe('StatusBar', () => {
|
|||||||
expect(screen.getByRole('status', { name: 'Radio Disconnected' })).toBeInTheDocument();
|
expect(screen.getByRole('status', { name: 'Radio Disconnected' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Reconnect' })).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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export interface Theme {
|
|||||||
metaThemeColor: string;
|
metaThemeColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const THEME_CHANGE_EVENT = 'remoteterm-theme-change';
|
||||||
|
|
||||||
export const THEMES: Theme[] = [
|
export const THEMES: Theme[] = [
|
||||||
{
|
{
|
||||||
id: 'original',
|
id: 'original',
|
||||||
@@ -77,4 +79,8 @@ export function applyTheme(themeId: string): void {
|
|||||||
meta.setAttribute('content', theme.metaThemeColor);
|
meta.setAttribute('content', theme.metaThemeColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, { detail: themeId }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user