import { androidTokens, darkTokens, iosTokens } from "@jobber/design"; import React, { createContext, useCallback, useContext, useState } from "react"; import mergeWith from "lodash/mergeWith"; import { Platform, useColorScheme } from "react-native"; import type { AtlantisThemeContextProviderProps, AtlantisThemeContextValue, EffectiveTheme, Theme, } from "./types"; const lightTokens = Platform.select({ ios: () => iosTokens, android: () => androidTokens, default: () => androidTokens, })(); const completeDarkTokens = mergeWith( {}, lightTokens, darkTokens, (platformValue, darkValue) => { const platformValueIsObject = typeof platformValue === "object" && platformValue !== null; const darkValueIsObject = typeof darkValue === "object" && darkValue !== null; if (platformValueIsObject && !darkValueIsObject) return platformValue; }, ); const defaultAtlantisThemeContextValue: AtlantisThemeContextValue = { theme: "light", effectiveTheme: "light", tokens: lightTokens, setTheme: () => { console.error( "useAtlantisTheme accessed outside of AtlantisThemeContextProvider", ); }, }; const AtlantisThemeContext = createContext< AtlantisThemeContextValue | undefined >(undefined); export function AtlantisThemeContextProvider({ children, theme, onThemeChange, dangerouslyOverrideTheme, }: AtlantisThemeContextProviderProps) { const parentThemeContext = useContext(AtlantisThemeContext); const [localTheme, setLocalTheme] = useState(undefined); const colorScheme = useColorScheme(); const setTheme = useCallback( (nextTheme: Theme) => { onThemeChange?.(nextTheme); if (theme !== undefined) return; // A forced visual override should preserve the parent subtree's selected // theme semantics while still rendering with the forced tokens. if (dangerouslyOverrideTheme !== undefined && parentThemeContext) { parentThemeContext.setTheme(nextTheme); return; } setLocalTheme(nextTheme); }, [dangerouslyOverrideTheme, onThemeChange, parentThemeContext, theme], ); const currentTheme = theme ?? localTheme ?? parentThemeContext?.theme ?? "light"; const effectiveTheme = dangerouslyOverrideTheme ?? resolveEffectiveTheme(currentTheme, colorScheme); const currentTokens = effectiveTheme === "dark" ? completeDarkTokens : lightTokens; return ( {children} ); } export function useAtlantisTheme() { return useContext(AtlantisThemeContext) ?? defaultAtlantisThemeContextValue; } function resolveEffectiveTheme( theme: Theme, colorScheme: ReturnType, ): EffectiveTheme { if (theme !== "system") return theme; return colorScheme === "dark" ? "dark" : "light"; }