added print, download as pdf. no puppeteer, we'll see

This commit is contained in:
2026-09-22 22:06:06 +03:00
parent 122480cd46
commit 6f614270e7
37 changed files with 3955 additions and 473 deletions
+78
View File
@@ -0,0 +1,78 @@
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;