feat: vylepšená podpora themes
CI / Generate TypeScript types (push) Successful in 1m3s
CI / Server unit tests (push) Successful in 30s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 36s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 2m46s
CI / Notify (push) Successful in 1s
CI / Generate TypeScript types (push) Successful in 1m3s
CI / Server unit tests (push) Successful in 30s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 36s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 2m46s
CI / Notify (push) Successful in 1s
This commit is contained in:
@@ -4,22 +4,23 @@ const BANK_ACCOUNT_NUMBER_KEY = 'bank_account_number';
|
||||
const BANK_ACCOUNT_HOLDER_KEY = 'bank_account_holder_name';
|
||||
const HIDE_SOUPS_KEY = 'hide_soups';
|
||||
const THEME_KEY = 'theme_preference';
|
||||
const COLOR_THEME_KEY = 'color_theme';
|
||||
const ACCENT_HUE_KEY = 'accent_hue';
|
||||
const LEGACY_COLOR_THEME_KEY = 'color_theme';
|
||||
|
||||
export type ThemePreference = 'system' | 'light' | 'dark';
|
||||
export type ColorTheme = 'green' | 'blue' | 'purple';
|
||||
|
||||
export type SettingsContextProps = {
|
||||
bankAccount?: string,
|
||||
holderName?: string,
|
||||
hideSoups?: boolean,
|
||||
themePreference: ThemePreference,
|
||||
colorTheme: ColorTheme,
|
||||
accentHue: number,
|
||||
effectiveDark: boolean,
|
||||
setBankAccountNumber: (accountNumber?: string) => void,
|
||||
setBankAccountHolderName: (holderName?: string) => void,
|
||||
setHideSoupsOption: (hideSoups?: boolean) => void,
|
||||
setThemePreference: (theme: ThemePreference) => void,
|
||||
setColorTheme: (color: ColorTheme) => void,
|
||||
setAccentHue: (hue: number) => void,
|
||||
}
|
||||
|
||||
type ContextProps = {
|
||||
@@ -49,16 +50,58 @@ function getInitialTheme(): ThemePreference {
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function getInitialColorTheme(): ColorTheme {
|
||||
function getInitialAccentHue(): number {
|
||||
try {
|
||||
const saved = localStorage.getItem(COLOR_THEME_KEY) as ColorTheme | null;
|
||||
if (saved && ['green', 'blue', 'purple'].includes(saved)) {
|
||||
return saved;
|
||||
const saved = localStorage.getItem(ACCENT_HUE_KEY);
|
||||
if (saved !== null) {
|
||||
const n = parseInt(saved, 10);
|
||||
if (!isNaN(n) && n >= 0 && n <= 360) return n;
|
||||
}
|
||||
} catch (e) {
|
||||
// Migrace ze starého string formátu (green/blue/purple)
|
||||
const old = localStorage.getItem(LEGACY_COLOR_THEME_KEY);
|
||||
if (old === 'blue') return 217;
|
||||
if (old === 'purple') return 263;
|
||||
} catch {
|
||||
// localStorage nedostupný
|
||||
}
|
||||
return 'green';
|
||||
return 142;
|
||||
}
|
||||
|
||||
// Převod HSL na relativní jas dle WCAG (pro výpočet kontrastu s bílým textem)
|
||||
function hslToRelativeLuminance(h: number, s: number, l: number): number {
|
||||
const sn = s / 100, ln = l / 100;
|
||||
const a = sn * Math.min(ln, 1 - ln);
|
||||
const ch = (n: number) => {
|
||||
const k = (n + h / 30) % 12;
|
||||
return ln - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
};
|
||||
const toLinear = (c: number) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
return 0.2126 * toLinear(ch(0)) + 0.7152 * toLinear(ch(8)) + 0.0722 * toLinear(ch(4));
|
||||
}
|
||||
|
||||
// Najde nejnižší světlost, při které má barva dostatečný kontrast s bílým textem (WCAG AA 4.5:1)
|
||||
function adjustedL(hue: number, sat: number, targetL: number): number {
|
||||
let l = targetL;
|
||||
while (l >= 5) {
|
||||
const lum = hslToRelativeLuminance(hue, sat, l);
|
||||
if (1.05 / (lum + 0.05) >= 4.5) return l;
|
||||
l -= 1;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
function applyAccentColors(hue: number, isDark: boolean): void {
|
||||
const sat = 70;
|
||||
const baseL = adjustedL(hue, sat, isDark ? 55 : 38);
|
||||
const hoverL = isDark ? Math.min(baseL + 10, 80) : Math.max(baseL - 10, 10);
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--luncher-primary', `hsl(${hue} ${sat}% ${baseL}%)`);
|
||||
root.style.setProperty('--luncher-primary-hover', `hsl(${hue} ${sat}% ${hoverL}%)`);
|
||||
root.style.setProperty('--luncher-primary-light', isDark
|
||||
? `hsl(${hue} 60% 12%)`
|
||||
: `hsl(${hue} 60% 92%)`);
|
||||
root.style.setProperty('--luncher-action-icon', `hsl(${hue} ${sat}% ${baseL}%)`);
|
||||
root.style.setProperty('--luncher-success', `hsl(${hue} ${sat}% ${baseL}%)`);
|
||||
}
|
||||
|
||||
function useProvideSettings(): SettingsContextProps {
|
||||
@@ -66,7 +109,15 @@ function useProvideSettings(): SettingsContextProps {
|
||||
const [holderName, setHolderName] = useState<string | undefined>();
|
||||
const [hideSoups, setHideSoups] = useState<boolean | undefined>();
|
||||
const [themePreference, setTheme] = useState<ThemePreference>(getInitialTheme);
|
||||
const [colorTheme, setColor] = useState<ColorTheme>(getInitialColorTheme);
|
||||
const [accentHue, setHue] = useState<number>(getInitialAccentHue);
|
||||
const [effectiveDark, setEffectiveDark] = useState<boolean>(() => {
|
||||
try {
|
||||
const pref = localStorage.getItem(THEME_KEY) as ThemePreference | null;
|
||||
if (pref === 'dark') return true;
|
||||
if (pref === 'light') return false;
|
||||
} catch { /* noop */ }
|
||||
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const accountNumber = localStorage.getItem(BANK_ACCOUNT_NUMBER_KEY);
|
||||
@@ -112,29 +163,27 @@ function useProvideSettings(): SettingsContextProps {
|
||||
}, [themePreference]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(COLOR_THEME_KEY, colorTheme);
|
||||
document.documentElement.setAttribute('data-luncher-color', colorTheme);
|
||||
}, [colorTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const applyTheme = (theme: 'light' | 'dark') => {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
const applyTheme = (dark: boolean) => {
|
||||
document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
|
||||
setEffectiveDark(dark);
|
||||
};
|
||||
|
||||
if (themePreference === 'system') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
applyTheme(mediaQuery.matches ? 'dark' : 'light');
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
applyTheme(e.matches ? 'dark' : 'light');
|
||||
};
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
applyTheme(mq.matches);
|
||||
const handler = (e: MediaQueryListEvent) => applyTheme(e.matches);
|
||||
mq.addEventListener('change', handler);
|
||||
return () => mq.removeEventListener('change', handler);
|
||||
} else {
|
||||
applyTheme(themePreference);
|
||||
applyTheme(themePreference === 'dark');
|
||||
}
|
||||
}, [themePreference]);
|
||||
|
||||
// Aplikuje accent barvy při změně hue nebo přepnutí světlý/tmavý
|
||||
useEffect(() => {
|
||||
localStorage.setItem(ACCENT_HUE_KEY, String(accentHue));
|
||||
applyAccentColors(accentHue, effectiveDark);
|
||||
}, [accentHue, effectiveDark]);
|
||||
|
||||
function setBankAccountNumber(bankAccount?: string) {
|
||||
setBankAccount(bankAccount);
|
||||
}
|
||||
@@ -151,8 +200,8 @@ function useProvideSettings(): SettingsContextProps {
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
function setColorTheme(color: ColorTheme) {
|
||||
setColor(color);
|
||||
function setAccentHue(hue: number) {
|
||||
setHue(hue);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -160,11 +209,12 @@ function useProvideSettings(): SettingsContextProps {
|
||||
holderName,
|
||||
hideSoups,
|
||||
themePreference,
|
||||
colorTheme,
|
||||
accentHue,
|
||||
effectiveDark,
|
||||
setBankAccountNumber,
|
||||
setBankAccountHolderName,
|
||||
setHideSoupsOption,
|
||||
setThemePreference,
|
||||
setColorTheme,
|
||||
setAccentHue,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user