Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v0.8.5 547 lines 13 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"; 21import { ToastProvider } from "../../components/ui"; 22 23// Import pairify function for generating theme tokens 24function pairify<T extends Record<string, any>>( 25 obj: T, 26 styleKeyPrefix: string, 27): Record<keyof T, any> { 28 const result: Record<string, any> = {}; 29 for (const [key, value] of Object.entries(obj)) { 30 if (typeof value === "object" && value !== null && !Array.isArray(value)) { 31 // For nested objects (like color scales), create another level 32 result[key] = {}; 33 for (const [nestedKey, nestedValue] of Object.entries(value)) { 34 result[key][nestedKey] = { [styleKeyPrefix]: nestedValue }; 35 } 36 } else { 37 // For simple values, create the style object directly 38 result[key] = { [styleKeyPrefix]: value }; 39 } 40 } 41 return result as Record<keyof T, any>; 42} 43 44// Theme interfaces 45export interface Theme { 46 colors: { 47 // Core semantic colors 48 background: string; 49 foreground: string; 50 51 // Card/surface colors 52 card: string; 53 cardForeground: string; 54 55 // Popover colors 56 popover: string; 57 popoverForeground: string; 58 59 // Primary colors 60 primary: string; 61 primaryForeground: string; 62 63 // Secondary colors 64 secondary: string; 65 secondaryForeground: string; 66 67 // Muted colors 68 muted: string; 69 mutedForeground: string; 70 71 // Accent colors 72 accent: string; 73 accentForeground: string; 74 75 // Destructive colors 76 destructive: string; 77 destructiveForeground: string; 78 79 // Success colors 80 success: string; 81 successForeground: string; 82 83 // Warning colors 84 warning: string; 85 warningForeground: string; 86 87 // Info colors 88 info: string; 89 infoForeground: string; 90 91 // Border and input colors 92 border: string; 93 input: string; 94 ring: string; 95 96 // Text colors 97 text: string; 98 textMuted: string; 99 textDisabled: string; 100 }; 101 spacing: typeof spacing; 102 borderRadius: typeof borderRadius; 103 typography: typeof typography; 104 shadows: typeof shadows; 105 touchTargets: typeof touchTargets; 106 animations: typeof animations; 107} 108 109// Theme-aware zero interface (like atoms but with theme colors) 110export interface ThemeZero { 111 // Colors using pairify 112 bg: Record<string, any>; 113 text: Record<string, any>; 114 border: Record<string, any>; 115 116 // Static design tokens (same as atoms) 117 shadow: { 118 sm: typeof shadows.sm; 119 md: typeof shadows.md; 120 lg: typeof shadows.lg; 121 xl: typeof shadows.xl; 122 }; 123 124 // Common button styles 125 button: { 126 primary: object; 127 secondary: object; 128 outline: object; 129 ghost: object; 130 }; 131 132 // Input styles 133 input: { 134 base: object; 135 focused: object; 136 error: object; 137 }; 138 139 // Card styles 140 card: { 141 base: object; 142 }; 143} 144 145// Icon utilities interface 146export interface ThemeIcons { 147 color: { 148 default: string; 149 muted: string; 150 primary: string; 151 secondary: string; 152 destructive: string; 153 success: string; 154 warning: string; 155 }; 156 size: { 157 sm: number; 158 md: number; 159 lg: number; 160 xl: number; 161 }; 162} 163 164// Create theme colors based on dark mode 165const createThemeColors = ( 166 isDark: boolean, 167 lightTheme?: ColorPalette | Theme["colors"], 168 darkTheme?: ColorPalette | Theme["colors"], 169 colorTheme?: Partial<Theme["colors"]>, 170): Theme["colors"] => { 171 let baseColors: Theme["colors"]; 172 173 if (isDark && darkTheme) { 174 // Use dark theme 175 baseColors = isColorPalette(darkTheme) 176 ? generateThemeColorsFromPalette(darkTheme, true) 177 : darkTheme; 178 } else if (!isDark && lightTheme) { 179 // Use light theme 180 baseColors = isColorPalette(lightTheme) 181 ? generateThemeColorsFromPalette(lightTheme, false) 182 : lightTheme; 183 } else { 184 // Fall back to default gray theme 185 const defaultPalette = colors.neutral; 186 baseColors = generateThemeColorsFromPalette(defaultPalette, isDark); 187 } 188 189 // Merge with custom color overrides if provided 190 return { 191 ...baseColors, 192 ...colorTheme, 193 }; 194}; 195 196// Create theme-aware zero tokens using pairify 197const createThemeZero = (themeColors: Theme["colors"]): ThemeZero => ({ 198 // Theme-aware colors using pairify 199 bg: pairify(themeColors, "backgroundColor"), 200 text: pairify(themeColors, "color"), 201 border: { 202 ...pairify(themeColors, "borderColor"), 203 default: { borderColor: themeColors.border }, 204 }, 205 206 // Static design tokens 207 shadow: { 208 sm: shadows.sm, 209 md: shadows.md, 210 lg: shadows.lg, 211 xl: shadows.xl, 212 }, 213 214 // Common button styles 215 button: { 216 primary: { 217 backgroundColor: themeColors.primary, 218 borderWidth: 0, 219 ...shadows.sm, 220 }, 221 secondary: { 222 backgroundColor: themeColors.secondary, 223 borderWidth: 0, 224 }, 225 outline: { 226 backgroundColor: "transparent", 227 borderWidth: 1, 228 borderColor: themeColors.border, 229 }, 230 ghost: { 231 backgroundColor: "transparent", 232 borderWidth: 0, 233 }, 234 }, 235 236 // Input styles 237 input: { 238 base: { 239 backgroundColor: themeColors.background, 240 borderWidth: 1, 241 borderColor: themeColors.border, 242 borderRadius: borderRadius.md, 243 paddingHorizontal: spacing[3], 244 paddingVertical: spacing[3], 245 minHeight: touchTargets.minimum, 246 }, 247 focused: { 248 borderColor: themeColors.ring, 249 borderWidth: 2, 250 }, 251 error: { 252 borderColor: themeColors.destructive, 253 borderWidth: 2, 254 }, 255 }, 256 257 // Card styles 258 card: { 259 base: { 260 backgroundColor: themeColors.card, 261 borderRadius: borderRadius.lg, 262 ...shadows.sm, 263 }, 264 }, 265}); 266 267// Create theme icons based on colors 268const createThemeIcons = (themeColors: Theme["colors"]): ThemeIcons => ({ 269 color: { 270 default: themeColors.text, 271 muted: themeColors.textMuted, 272 primary: themeColors.primary, 273 secondary: themeColors.secondary, 274 destructive: themeColors.destructive, 275 success: themeColors.success, 276 warning: themeColors.warning, 277 }, 278 size: { 279 sm: 16, 280 md: 20, 281 lg: 24, 282 xl: 32, 283 }, 284}); 285 286// Theme context interface 287interface ThemeContextType { 288 theme: Theme; 289 zero: ThemeZero; 290 icons: ThemeIcons; 291 isDark: boolean; 292 currentTheme: "light" | "dark" | "system"; 293 systemTheme: "light" | "dark"; 294 setTheme: (theme: "light" | "dark" | "system") => void; 295 toggleTheme: () => void; 296} 297 298// Create the theme context 299const ThemeContext = createContext<ThemeContextType | null>(null); 300 301// Color palette type 302type ColorPalette = { 303 50: string; 304 100: string; 305 200: string; 306 300: string; 307 400: string; 308 500: string; 309 600: string; 310 700: string; 311 800: string; 312 900: string; 313 950: string; 314}; 315 316// Helper function to check if input is a ColorPalette or Theme["colors"] 317function isColorPalette( 318 input: ColorPalette | Theme["colors"], 319): input is ColorPalette { 320 return "50" in input && "100" in input && "950" in input; 321} 322 323// Helper function to generate Theme["colors"] from ColorPalette 324function generateThemeColorsFromPalette( 325 palette: ColorPalette, 326 isDark: boolean, 327): Theme["colors"] { 328 return { 329 background: isDark ? palette[950] : colors.white, 330 foreground: isDark ? palette[50] : palette[950], 331 332 card: isDark ? palette[900] : colors.white, 333 cardForeground: isDark ? palette[50] : palette[950], 334 335 popover: isDark ? palette[900] : colors.white, 336 popoverForeground: isDark ? palette[50] : palette[950], 337 338 primary: 339 Platform.OS === "ios" ? colors.ios.systemBlue : colors.primary[500], 340 primaryForeground: colors.white, 341 342 secondary: isDark ? palette[800] : palette[100], 343 secondaryForeground: isDark ? palette[50] : palette[900], 344 345 muted: isDark ? palette[800] : palette[100], 346 mutedForeground: isDark ? palette[400] : palette[500], 347 348 accent: isDark ? palette[800] : palette[100], 349 accentForeground: isDark ? palette[50] : palette[900], 350 351 destructive: colors.destructive[700], 352 destructiveForeground: colors.white, 353 354 success: colors.success[700], 355 successForeground: colors.white, 356 357 warning: colors.warning[700], 358 warningForeground: colors.white, 359 360 info: colors.blue[700], 361 infoForeground: isDark ? palette[50] : palette[900], 362 363 border: isDark ? palette[500] + "30" : palette[200] + "30", 364 input: isDark ? palette[800] : palette[200], 365 ring: Platform.OS === "ios" ? colors.ios.systemBlue : colors.primary[500], 366 367 text: isDark ? palette[50] : palette[950], 368 textMuted: isDark ? palette[400] : palette[500], 369 textDisabled: isDark ? palette[600] : palette[400], 370 }; 371} 372 373// Theme provider props 374interface ThemeProviderProps { 375 children: ReactNode; 376 defaultTheme?: "light" | "dark" | "system"; 377 forcedTheme?: "light" | "dark"; 378 colorTheme?: Partial<Theme["colors"]>; 379 lightTheme?: ColorPalette | Theme["colors"]; 380 darkTheme?: ColorPalette | Theme["colors"]; 381} 382 383// Theme provider component 384// Should be surrounded by SafeAreaProvider at the root 385export function ThemeProvider({ 386 children, 387 defaultTheme = "system", 388 forcedTheme, 389 colorTheme, 390 lightTheme, 391 darkTheme, 392}: ThemeProviderProps) { 393 const systemColorScheme = useColorScheme(); 394 const [currentTheme, setCurrentTheme] = useState<"light" | "dark" | "system">( 395 defaultTheme, 396 ); 397 398 // Determine if dark mode should be active 399 const isDark = useMemo(() => { 400 if (forcedTheme === "light") return false; 401 if (forcedTheme === "dark") return true; 402 if (currentTheme === "light") return false; 403 if (currentTheme === "dark") return true; 404 if (currentTheme === "system") return systemColorScheme === "dark"; 405 return systemColorScheme === "dark"; 406 }, [forcedTheme, currentTheme, systemColorScheme]); 407 408 // Create theme based on dark mode 409 const theme = useMemo<Theme>(() => { 410 const themeColors = createThemeColors( 411 isDark, 412 lightTheme, 413 darkTheme, 414 colorTheme, 415 ); 416 return { 417 colors: themeColors, 418 spacing, 419 borderRadius, 420 typography, 421 shadows, 422 touchTargets, 423 animations, 424 }; 425 }, [isDark, lightTheme, darkTheme, colorTheme]); 426 427 // Create theme-aware zero tokens 428 const zero = useMemo<ThemeZero>(() => { 429 return createThemeZero(theme.colors); 430 }, [theme.colors]); 431 432 // Create icon utilities 433 const icons = useMemo<ThemeIcons>(() => { 434 return createThemeIcons(theme.colors); 435 }, [theme.colors]); 436 437 // Theme controls 438 const setTheme = (newTheme: "light" | "dark" | "system") => { 439 if (!forcedTheme) { 440 setCurrentTheme(newTheme); 441 } 442 }; 443 444 const toggleTheme = () => { 445 if (!forcedTheme) { 446 setCurrentTheme((prev) => { 447 if (prev === "light") return "dark"; 448 if (prev === "dark") return "system"; 449 return "light"; 450 }); 451 } 452 }; 453 454 const value = useMemo<ThemeContextType>( 455 () => ({ 456 theme, 457 zero, 458 icons, 459 isDark, 460 currentTheme: forcedTheme || currentTheme, 461 systemTheme: (systemColorScheme as "light" | "dark") || "light", 462 setTheme, 463 toggleTheme, 464 }), 465 [ 466 theme, 467 zero, 468 icons, 469 isDark, 470 forcedTheme, 471 currentTheme, 472 systemColorScheme, 473 setTheme, 474 toggleTheme, 475 ], 476 ); 477 478 return ( 479 <ThemeContext.Provider value={value}> 480 <GestureHandlerRootView> 481 {children} 482 <PortalHost /> 483 <ToastProvider /> 484 </GestureHandlerRootView> 485 </ThemeContext.Provider> 486 ); 487} 488 489// Hook to use theme 490export function useTheme(): ThemeContextType { 491 const context = useContext(ThemeContext); 492 if (!context) { 493 throw new Error("useTheme must be used within a ThemeProvider"); 494 } 495 return context; 496} 497 498// Hook to get current platform's typography 499export function usePlatformTypography() { 500 const { theme } = useTheme(); 501 502 return useMemo(() => { 503 if (Platform.OS === "ios") { 504 return theme.typography.ios; 505 } else if (Platform.OS === "android") { 506 return theme.typography.android; 507 } 508 return theme.typography.universal; 509 }, [theme.typography]); 510} 511 512// Utility function to create theme-aware styles 513export function createThemedStyles<T extends Record<string, any>>( 514 styleCreator: (theme: Theme, zero: ThemeZero, icons: ThemeIcons) => T, 515) { 516 return function useThemedStyles() { 517 const { theme, zero, icons } = useTheme(); 518 return useMemo( 519 () => styleCreator(theme, zero, icons), 520 [theme, zero, icons], 521 ); 522 }; 523} 524 525// Create light and dark theme instances for external use 526export const lightTheme: Theme = { 527 colors: createThemeColors(false), 528 spacing, 529 borderRadius, 530 typography, 531 shadows, 532 touchTargets, 533 animations, 534}; 535 536export const darkTheme: Theme = { 537 colors: createThemeColors(true), 538 spacing, 539 borderRadius, 540 typography, 541 shadows, 542 touchTargets, 543 animations, 544}; 545 546// Export individual theme utilities for convenience 547export { createThemeColors, createThemeIcons, createThemeZero };