Add beenvoice mobile companion app with full dark mode support.
Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useColorScheme as useSystemColorScheme, type ColorSchemeName } from "react-native";
|
||||
|
||||
import { getThemeColors, type ThemeColors } from "@/lib/theme-palette";
|
||||
|
||||
export type ColorMode = "system" | "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "beenvoice:color-mode";
|
||||
|
||||
type ThemeContextValue = {
|
||||
colorMode: ColorMode;
|
||||
setColorMode: (mode: ColorMode) => Promise<void>;
|
||||
colorScheme: NonNullable<ColorSchemeName>;
|
||||
colors: ThemeColors;
|
||||
isDark: boolean;
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const systemScheme = useSystemColorScheme();
|
||||
const [colorMode, setColorModeState] = useState<ColorMode>("system");
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AsyncStorage.getItem(STORAGE_KEY)
|
||||
.then((stored) => {
|
||||
if (stored === "light" || stored === "dark" || stored === "system") {
|
||||
setColorModeState(stored);
|
||||
}
|
||||
})
|
||||
.finally(() => setReady(true));
|
||||
}, []);
|
||||
|
||||
const colorScheme: NonNullable<ColorSchemeName> =
|
||||
colorMode === "system" ? (systemScheme ?? "light") : colorMode;
|
||||
|
||||
const colors = useMemo(() => getThemeColors(colorScheme), [colorScheme]);
|
||||
|
||||
const setColorMode = useCallback(async (mode: ColorMode) => {
|
||||
setColorModeState(mode);
|
||||
await AsyncStorage.setItem(STORAGE_KEY, mode);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
colorMode,
|
||||
setColorMode,
|
||||
colorScheme,
|
||||
colors,
|
||||
isDark: colorScheme === "dark",
|
||||
}),
|
||||
[colorMode, setColorMode, colorScheme, colors],
|
||||
);
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
colorMode: "system",
|
||||
setColorMode: async () => {},
|
||||
colorScheme: systemScheme ?? "light",
|
||||
colors: getThemeColors(systemScheme ?? "light"),
|
||||
isDark: systemScheme === "dark",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAppTheme() {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) throw new Error("useAppTheme must be used within ThemeProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user