270 lines
7.7 KiB
TypeScript
270 lines
7.7 KiB
TypeScript
import { createContext, useContext, useState, useEffect, useCallback } from "react";
|
|
import { useColorScheme } from "react-native";
|
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
|
|
type ThemeColors = {
|
|
primary: string;
|
|
primaryLight: string;
|
|
primaryDark: string;
|
|
primaryMuted: string;
|
|
secondary: string;
|
|
secondaryLight: string;
|
|
background: string;
|
|
surface: string;
|
|
surfaceAlt: string;
|
|
surfaceElevated: string;
|
|
card: string;
|
|
border: string;
|
|
borderLight: string;
|
|
text: string;
|
|
textSecondary: string;
|
|
textTertiary: string;
|
|
textInverse: string;
|
|
error: string;
|
|
errorLight: string;
|
|
success: string;
|
|
successLight: string;
|
|
star: string;
|
|
starLight: string;
|
|
tabActive: string;
|
|
tabInactive: string;
|
|
divider: string;
|
|
overlay: string;
|
|
shadow: string;
|
|
};
|
|
|
|
type Shadow = {
|
|
shadowColor: string;
|
|
shadowOffset: { width: number; height: number };
|
|
shadowOpacity: number;
|
|
shadowRadius: number;
|
|
elevation: number;
|
|
};
|
|
|
|
type ThemeShadows = {
|
|
xs: Shadow;
|
|
sm: Shadow;
|
|
md: Shadow;
|
|
lg: Shadow;
|
|
xl: Shadow;
|
|
};
|
|
|
|
type Typography = {
|
|
h1: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
h2: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
h3: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
h4: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
body: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
bodyBold: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
caption: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
captionBold: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
label: { fontSize: number; fontWeight: "700" | "600" | "500" | "400"; lineHeight: number };
|
|
};
|
|
|
|
type Theme = {
|
|
isDarkMode: boolean;
|
|
colors: ThemeColors;
|
|
spacing: {
|
|
xs: number;
|
|
sm: number;
|
|
md: number;
|
|
lg: number;
|
|
xl: number;
|
|
xxl: number;
|
|
};
|
|
radius: {
|
|
xs: number;
|
|
sm: number;
|
|
md: number;
|
|
lg: number;
|
|
xl: number;
|
|
full: number;
|
|
};
|
|
shadows: ThemeShadows;
|
|
typography: Typography;
|
|
};
|
|
|
|
const makeShadow = (elevation: number, color: string): Shadow => {
|
|
const shadows: Record<number, Shadow> = {
|
|
1: { shadowColor: color, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1 },
|
|
2: { shadowColor: color, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.06, shadowRadius: 6, elevation: 2 },
|
|
4: { shadowColor: color, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 4 },
|
|
8: { shadowColor: color, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.1, shadowRadius: 20, elevation: 8 },
|
|
16: { shadowColor: color, shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.12, shadowRadius: 32, elevation: 16 },
|
|
};
|
|
return shadows[elevation] || shadows[2];
|
|
};
|
|
|
|
const spacing = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48 };
|
|
const radius = { xs: 4, sm: 10, md: 14, lg: 20, xl: 28, full: 9999 };
|
|
|
|
const typography: Typography = {
|
|
h1: { fontSize: 34, fontWeight: "700", lineHeight: 42 },
|
|
h2: { fontSize: 26, fontWeight: "700", lineHeight: 34 },
|
|
h3: { fontSize: 20, fontWeight: "600", lineHeight: 28 },
|
|
h4: { fontSize: 17, fontWeight: "600", lineHeight: 24 },
|
|
body: { fontSize: 15, fontWeight: "400", lineHeight: 22 },
|
|
bodyBold: { fontSize: 15, fontWeight: "600", lineHeight: 22 },
|
|
caption: { fontSize: 13, fontWeight: "400", lineHeight: 18 },
|
|
captionBold: { fontSize: 13, fontWeight: "600", lineHeight: 18 },
|
|
label: { fontSize: 11, fontWeight: "500", lineHeight: 14 },
|
|
};
|
|
|
|
const shadowColorLight = "rgba(26, 42, 58, 0.12)";
|
|
const shadowColorDark = "rgba(0, 0, 0, 0.35)";
|
|
|
|
const LightTheme: Theme = {
|
|
isDarkMode: false,
|
|
colors: {
|
|
primary: "#D4764A",
|
|
primaryLight: "#FBF0EB",
|
|
primaryDark: "#B85E34",
|
|
primaryMuted: "#E8CFC3",
|
|
secondary: "#1A2A3A",
|
|
secondaryLight: "#E8ECF0",
|
|
background: "#FDF8F3",
|
|
surface: "#FFFFFF",
|
|
surfaceAlt: "#FAF6F2",
|
|
surfaceElevated: "#FFFFFF",
|
|
card: "#FFFFFF",
|
|
border: "#EDE8E3",
|
|
borderLight: "#F3EFEB",
|
|
text: "#1A1A1A",
|
|
textSecondary: "#6B6560",
|
|
textTertiary: "#9E9A95",
|
|
textInverse: "#FFFFFF",
|
|
error: "#C23A3A",
|
|
errorLight: "#FEECEC",
|
|
success: "#2E8B57",
|
|
successLight: "#E8F5EC",
|
|
star: "#D4953A",
|
|
starLight: "#FFF5E1",
|
|
tabActive: "#D4764A",
|
|
tabInactive: "#9E9A95",
|
|
divider: "#F0EBE6",
|
|
overlay: "rgba(26, 42, 58, 0.45)",
|
|
shadow: shadowColorLight,
|
|
},
|
|
spacing,
|
|
radius,
|
|
shadows: {
|
|
xs: makeShadow(1, shadowColorLight),
|
|
sm: makeShadow(2, shadowColorLight),
|
|
md: makeShadow(4, shadowColorLight),
|
|
lg: makeShadow(8, shadowColorLight),
|
|
xl: makeShadow(16, shadowColorLight),
|
|
},
|
|
typography,
|
|
};
|
|
|
|
const DarkTheme: Theme = {
|
|
isDarkMode: true,
|
|
colors: {
|
|
primary: "#E8956F",
|
|
primaryLight: "#2D2018",
|
|
primaryDark: "#E8956F",
|
|
primaryMuted: "#4A3A2E",
|
|
secondary: "#E8ECF0",
|
|
secondaryLight: "#2A3040",
|
|
background: "#121212",
|
|
surface: "#1E1E1E",
|
|
surfaceAlt: "#252525",
|
|
surfaceElevated: "#2A2A2A",
|
|
card: "#1E1E1E",
|
|
border: "#333333",
|
|
borderLight: "#2A2A2A",
|
|
text: "#F5F5F5",
|
|
textSecondary: "#AAAAAA",
|
|
textTertiary: "#777777",
|
|
textInverse: "#1A1A1A",
|
|
error: "#EF5350",
|
|
errorLight: "#2A1A1A",
|
|
success: "#66BB6A",
|
|
successLight: "#1A2A1E",
|
|
star: "#FFB74D",
|
|
starLight: "#2A2510",
|
|
tabActive: "#E8956F",
|
|
tabInactive: "#777777",
|
|
divider: "#2A2A2A",
|
|
overlay: "rgba(0,0,0,0.7)",
|
|
shadow: shadowColorDark,
|
|
},
|
|
spacing,
|
|
radius,
|
|
shadows: {
|
|
xs: makeShadow(1, shadowColorDark),
|
|
sm: makeShadow(2, shadowColorDark),
|
|
md: makeShadow(4, shadowColorDark),
|
|
lg: makeShadow(8, shadowColorDark),
|
|
xl: makeShadow(16, shadowColorDark),
|
|
},
|
|
typography,
|
|
};
|
|
|
|
export type { Theme, ThemeColors, ThemeShadows, Typography };
|
|
export { LightTheme, DarkTheme };
|
|
|
|
const THEME_KEY = "mojmajstor_theme_preference";
|
|
|
|
const ThemeContext = createContext<Theme>(LightTheme);
|
|
|
|
export type ThemeContextType = {
|
|
theme: Theme;
|
|
isDarkMode: boolean;
|
|
toggleTheme: () => void;
|
|
setDarkMode: (dark: boolean) => void;
|
|
};
|
|
|
|
const ThemeCtx = createContext<ThemeContextType>({
|
|
theme: LightTheme,
|
|
isDarkMode: false,
|
|
toggleTheme: () => {},
|
|
setDarkMode: () => {},
|
|
});
|
|
|
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
const systemScheme = useColorScheme();
|
|
const [preference, setPreference] = useState<"system" | "light" | "dark">("system");
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
useEffect(() => {
|
|
AsyncStorage.getItem(THEME_KEY).then((val) => {
|
|
if (val === "light" || val === "dark" || val === "system") {
|
|
setPreference(val);
|
|
}
|
|
setLoaded(true);
|
|
});
|
|
}, []);
|
|
|
|
const isDarkMode = preference === "dark" || (preference === "system" && systemScheme === "dark");
|
|
const theme = isDarkMode ? DarkTheme : LightTheme;
|
|
|
|
const setDarkMode = useCallback((dark: boolean) => {
|
|
const val = dark ? "dark" : "light";
|
|
setPreference(val);
|
|
AsyncStorage.setItem(THEME_KEY, val);
|
|
}, []);
|
|
|
|
const toggleTheme = useCallback(() => {
|
|
setDarkMode(!isDarkMode);
|
|
}, [isDarkMode, setDarkMode]);
|
|
|
|
if (!loaded) {
|
|
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
|
|
}
|
|
|
|
return (
|
|
<ThemeCtx.Provider value={{ theme, isDarkMode, toggleTheme, setDarkMode }}>
|
|
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
|
|
</ThemeCtx.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTheme() {
|
|
return useContext(ThemeContext);
|
|
}
|
|
|
|
export function useThemeToggle() {
|
|
return useContext(ThemeCtx);
|
|
}
|