Live video on the AT Protocol
79
fork

Configure Feed

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

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