Live video on the AT Protocol
at eli/linking-fixes 436 lines 10 kB view raw
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 };