Live video on the AT Protocol
at eli/docker-deployment-docs 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 };