Live video on the AT Protocol
1import { PortalHost } from "@rn-primitives/portal";
2import {
3 createContext,
4 useContext,
5 useMemo,
6 useState,
7 type ReactNode,
8} from "react";
9import { Platform, useColorScheme } from "react-native";
10import {
11 animations,
12 borderRadius,
13 colors,
14 shadows,
15 spacing,
16 touchTargets,
17 typography,
18} from "./tokens";
19
20import { GestureHandlerRootView } from "react-native-gesture-handler";
21
22// Theme interfaces
23export interface Theme {
24 colors: {
25 // Core semantic colors
26 background: string;
27 foreground: string;
28
29 // Card/surface colors
30 card: string;
31 cardForeground: string;
32
33 // Popover colors
34 popover: string;
35 popoverForeground: string;
36
37 // Primary colors
38 primary: string;
39 primaryForeground: string;
40
41 // Secondary colors
42 secondary: string;
43 secondaryForeground: string;
44
45 // Muted colors
46 muted: string;
47 mutedForeground: string;
48
49 // Accent colors
50 accent: string;
51 accentForeground: string;
52
53 // Destructive colors
54 destructive: string;
55 destructiveForeground: string;
56
57 // Success colors
58 success: string;
59 successForeground: string;
60
61 // Warning colors
62 warning: string;
63 warningForeground: string;
64
65 // Border and input colors
66 border: string;
67 input: string;
68 ring: string;
69
70 // Text colors
71 text: string;
72 textMuted: string;
73 textDisabled: string;
74 };
75 spacing: typeof spacing;
76 borderRadius: typeof borderRadius;
77 typography: typeof typography;
78 shadows: typeof shadows;
79 touchTargets: typeof touchTargets;
80 animations: typeof animations;
81}
82
83// Utility styles interface
84export interface ThemeStyles {
85 shadow: {
86 sm: typeof shadows.sm;
87 md: typeof shadows.md;
88 lg: typeof shadows.lg;
89 xl: typeof shadows.xl;
90 };
91 button: {
92 primary: object;
93 secondary: object;
94 outline: object;
95 ghost: object;
96 };
97 text: {
98 primary: object;
99 muted: object;
100 disabled: object;
101 };
102 input: {
103 base: object;
104 focused: object;
105 error: object;
106 };
107 card: {
108 base: object;
109 };
110}
111
112// Icon utilities interface
113export interface ThemeIcons {
114 color: {
115 default: string;
116 muted: string;
117 primary: string;
118 secondary: string;
119 destructive: string;
120 success: string;
121 warning: string;
122 };
123 size: {
124 sm: number;
125 md: number;
126 lg: number;
127 xl: number;
128 };
129}
130
131// Create theme colors based on dark mode
132const createThemeColors = (isDark: boolean): Theme["colors"] => ({
133 background: isDark ? colors.gray[950] : colors.white,
134 foreground: isDark ? colors.gray[50] : colors.gray[950],
135
136 card: isDark ? colors.gray[900] : colors.white,
137 cardForeground: isDark ? colors.gray[50] : colors.gray[950],
138
139 popover: isDark ? colors.gray[900] : colors.white,
140 popoverForeground: isDark ? colors.gray[50] : colors.gray[950],
141
142 primary: Platform.OS === "ios" ? colors.ios.systemBlue : colors.primary[500],
143 primaryForeground: colors.white,
144
145 secondary: isDark ? colors.gray[800] : colors.gray[100],
146 secondaryForeground: isDark ? colors.gray[50] : colors.gray[900],
147
148 muted: isDark ? colors.gray[800] : colors.gray[100],
149 mutedForeground: isDark ? colors.gray[400] : colors.gray[500],
150
151 accent: isDark ? colors.gray[800] : colors.gray[100],
152 accentForeground: isDark ? colors.gray[50] : colors.gray[900],
153
154 destructive:
155 Platform.OS === "ios" ? colors.ios.systemRed : colors.destructive[500],
156 destructiveForeground: colors.white,
157
158 success: Platform.OS === "ios" ? colors.ios.systemGreen : colors.success[500],
159 successForeground: colors.white,
160
161 warning:
162 Platform.OS === "ios" ? colors.ios.systemOrange : colors.warning[500],
163 warningForeground: colors.white,
164
165 border: isDark ? colors.gray[500] + "30" : colors.gray[200] + "30",
166 input: isDark ? colors.gray[800] : colors.gray[200],
167 ring: Platform.OS === "ios" ? colors.ios.systemBlue : colors.primary[500],
168
169 text: isDark ? colors.gray[50] : colors.gray[950],
170 textMuted: isDark ? colors.gray[400] : colors.gray[500],
171 textDisabled: isDark ? colors.gray[600] : colors.gray[400],
172});
173
174// Create theme styles based on colors
175const createThemeStyles = (themeColors: Theme["colors"]): ThemeStyles => ({
176 shadow: {
177 sm: shadows.sm,
178 md: shadows.md,
179 lg: shadows.lg,
180 xl: shadows.xl,
181 },
182 button: {
183 primary: {
184 backgroundColor: themeColors.primary,
185 borderWidth: 0,
186 ...shadows.sm,
187 },
188 secondary: {
189 backgroundColor: themeColors.secondary,
190 borderWidth: 0,
191 },
192 outline: {
193 backgroundColor: "transparent",
194 borderWidth: 1,
195 borderColor: themeColors.border,
196 },
197 ghost: {
198 backgroundColor: "transparent",
199 borderWidth: 0,
200 },
201 },
202 text: {
203 primary: {
204 color: themeColors.text,
205 },
206 muted: {
207 color: themeColors.textMuted,
208 },
209 disabled: {
210 color: themeColors.textDisabled,
211 },
212 },
213 input: {
214 base: {
215 backgroundColor: themeColors.background,
216 borderWidth: 1,
217 borderColor: themeColors.border,
218 borderRadius: borderRadius.md,
219 paddingHorizontal: spacing[3],
220 paddingVertical: spacing[3],
221 minHeight: touchTargets.minimum,
222 },
223 focused: {
224 borderColor: themeColors.ring,
225 borderWidth: 2,
226 },
227 error: {
228 borderColor: themeColors.destructive,
229 borderWidth: 2,
230 },
231 },
232 card: {
233 base: {
234 backgroundColor: themeColors.card,
235 borderRadius: borderRadius.lg,
236 ...shadows.sm,
237 },
238 },
239});
240
241// Create theme icons based on colors
242const createThemeIcons = (themeColors: Theme["colors"]): ThemeIcons => ({
243 color: {
244 default: themeColors.text,
245 muted: themeColors.textMuted,
246 primary: themeColors.primary,
247 secondary: themeColors.secondary,
248 destructive: themeColors.destructive,
249 success: themeColors.success,
250 warning: themeColors.warning,
251 },
252 size: {
253 sm: 16,
254 md: 20,
255 lg: 24,
256 xl: 32,
257 },
258});
259
260// Theme context interface
261interface ThemeContextType {
262 theme: Theme;
263 styles: ThemeStyles;
264 icons: ThemeIcons;
265 isDark: boolean;
266 currentTheme: "light" | "dark" | "system";
267 systemTheme: "light" | "dark";
268 setTheme: (theme: "light" | "dark" | "system") => void;
269 toggleTheme: () => void;
270}
271
272// Create the theme context
273const ThemeContext = createContext<ThemeContextType | null>(null);
274
275// Theme provider props
276interface ThemeProviderProps {
277 children: ReactNode;
278 defaultTheme?: "light" | "dark" | "system";
279 forcedTheme?: "light" | "dark";
280}
281
282// Theme provider component
283export function ThemeProvider({
284 children,
285 defaultTheme = "system",
286 forcedTheme,
287}: ThemeProviderProps) {
288 const systemColorScheme = useColorScheme();
289 const [currentTheme, setCurrentTheme] = useState<"light" | "dark" | "system">(
290 defaultTheme,
291 );
292
293 // Determine if dark mode should be active
294 const isDark = useMemo(() => {
295 if (forcedTheme === "light") return false;
296 if (forcedTheme === "dark") return true;
297 if (currentTheme === "light") return false;
298 if (currentTheme === "dark") return true;
299 if (currentTheme === "system") return systemColorScheme === "dark";
300 return systemColorScheme === "dark";
301 }, [forcedTheme, currentTheme, systemColorScheme]);
302
303 // Create theme based on dark mode
304 const theme = useMemo<Theme>(() => {
305 const themeColors = createThemeColors(isDark);
306 return {
307 colors: themeColors,
308 spacing,
309 borderRadius,
310 typography,
311 shadows,
312 touchTargets,
313 animations,
314 };
315 }, [isDark]);
316
317 // Create utility styles
318 const styles = useMemo<ThemeStyles>(() => {
319 return createThemeStyles(theme.colors);
320 }, [theme.colors]);
321
322 // Create icon utilities
323 const icons = useMemo<ThemeIcons>(() => {
324 return createThemeIcons(theme.colors);
325 }, [theme.colors]);
326
327 // Theme controls
328 const setTheme = (newTheme: "light" | "dark" | "system") => {
329 if (!forcedTheme) {
330 setCurrentTheme(newTheme);
331 }
332 };
333
334 const toggleTheme = () => {
335 if (!forcedTheme) {
336 setCurrentTheme((prev) => {
337 if (prev === "light") return "dark";
338 if (prev === "dark") return "system";
339 return "light";
340 });
341 }
342 };
343
344 const value = useMemo<ThemeContextType>(
345 () => ({
346 theme,
347 styles,
348 icons,
349 isDark,
350 currentTheme: forcedTheme || currentTheme,
351 systemTheme: (systemColorScheme as "light" | "dark") || "light",
352 setTheme,
353 toggleTheme,
354 }),
355 [
356 theme,
357 styles,
358 icons,
359 isDark,
360 forcedTheme,
361 currentTheme,
362 systemColorScheme,
363 setTheme,
364 toggleTheme,
365 ],
366 );
367
368 return (
369 <ThemeContext.Provider value={value}>
370 <GestureHandlerRootView>
371 {children}
372 <PortalHost />
373 </GestureHandlerRootView>
374 </ThemeContext.Provider>
375 );
376}
377
378// Hook to use theme
379export function useTheme(): ThemeContextType {
380 const context = useContext(ThemeContext);
381 if (!context) {
382 throw new Error("useTheme must be used within a ThemeProvider");
383 }
384 return context;
385}
386
387// Hook to get current platform's typography
388export function usePlatformTypography() {
389 const { theme } = useTheme();
390
391 return useMemo(() => {
392 if (Platform.OS === "ios") {
393 return theme.typography.ios;
394 } else if (Platform.OS === "android") {
395 return theme.typography.android;
396 }
397 return theme.typography.universal;
398 }, [theme.typography]);
399}
400
401// Utility function to create theme-aware styles
402export function createThemedStyles<T extends Record<string, any>>(
403 styleCreator: (theme: Theme, styles: ThemeStyles, icons: ThemeIcons) => T,
404) {
405 return function useThemedStyles() {
406 const { theme, styles, icons } = useTheme();
407 return useMemo(
408 () => styleCreator(theme, styles, icons),
409 [theme, styles, icons],
410 );
411 };
412}
413
414// Create light and dark theme instances for external use
415export const lightTheme: Theme = {
416 colors: createThemeColors(false),
417 spacing,
418 borderRadius,
419 typography,
420 shadows,
421 touchTargets,
422 animations,
423};
424
425export const darkTheme: Theme = {
426 colors: createThemeColors(true),
427 spacing,
428 borderRadius,
429 typography,
430 shadows,
431 touchTargets,
432 animations,
433};
434
435// Export individual theme utilities for convenience
436export { createThemeColors, createThemeIcons, createThemeStyles };