79 lines
2.2 KiB
React
79 lines
2.2 KiB
React
import React, { createContext, useState, useEffect, useContext, useCallback } from 'react';
|
|
|
|
/**
|
|
* Theme selection, applied as `data-theme` on <html>.
|
|
*
|
|
* The attribute lives on documentElement rather than on a React root div so
|
|
* that anything rendered outside the tree — portals, overlays, the native
|
|
* scrollbar via color-scheme — picks the theme up too.
|
|
*
|
|
* `classic` is the default on purpose: an operator's screen must not change
|
|
* because a new theme shipped. Cockpit is opt-in, per browser.
|
|
*/
|
|
|
|
const ThemeContext = createContext(null);
|
|
|
|
export const THEMES = [
|
|
{ id: 'classic', label: 'Classic' },
|
|
{ id: 'cockpit', label: 'Cockpit' }
|
|
];
|
|
|
|
const STORAGE_KEY = 'scalesapp.theme';
|
|
const DEFAULT_THEME = 'classic';
|
|
|
|
const isValid = (id) => THEMES.some(t => t.id === id);
|
|
|
|
/** Read the stored choice. localStorage throws in some privacy modes, and can
|
|
* hold a theme id that no longer exists, so both are handled here once. */
|
|
const readStored = () => {
|
|
try {
|
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
return isValid(saved) ? saved : DEFAULT_THEME;
|
|
} catch {
|
|
return DEFAULT_THEME;
|
|
}
|
|
};
|
|
|
|
export const ThemeProvider = ({ children }) => {
|
|
const [theme, setThemeState] = useState(readStored);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
}, [theme]);
|
|
|
|
const setTheme = useCallback((id) => {
|
|
if (!isValid(id)) return;
|
|
setThemeState(id);
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, id);
|
|
} catch {
|
|
// A theme that cannot be remembered is still worth applying for this
|
|
// session, so a storage failure is not an error the user needs to see.
|
|
}
|
|
}, []);
|
|
|
|
const toggleTheme = useCallback(() => {
|
|
setThemeState(prev => {
|
|
const next = prev === 'classic' ? 'cockpit' : 'classic';
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, next);
|
|
} catch { /* see above */ }
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme, themes: THEMES }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useTheme = () => {
|
|
const ctx = useContext(ThemeContext);
|
|
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider');
|
|
return ctx;
|
|
};
|
|
|
|
export default ThemeContext;
|