From f17125e2316e7ebc06d0cef9ffcf682769dde7b7 Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Sat, 21 Feb 2026 00:04:59 +0100 Subject: [PATCH] feat: system light/dark theme infrastructure Co-Authored-By: Claude Opus 4.6 --- src/shared/theme/ThemeContext.tsx | 27 +++++++ src/shared/theme/colors.dark.ts | 124 ++++++++++++++++++++++++++++++ src/shared/theme/colors.light.ts | 124 ++++++++++++++++++++++++++++++ src/shared/theme/index.ts | 5 ++ src/shared/theme/types.ts | 73 ++++++++++++++++++ 5 files changed, 353 insertions(+) create mode 100644 src/shared/theme/ThemeContext.tsx create mode 100644 src/shared/theme/colors.dark.ts create mode 100644 src/shared/theme/colors.light.ts create mode 100644 src/shared/theme/index.ts create mode 100644 src/shared/theme/types.ts diff --git a/src/shared/theme/ThemeContext.tsx b/src/shared/theme/ThemeContext.tsx new file mode 100644 index 0000000..bda8e83 --- /dev/null +++ b/src/shared/theme/ThemeContext.tsx @@ -0,0 +1,27 @@ +/** + * Theme context + provider + * Switches palette based on system color scheme + */ + +import { createContext, useContext, useMemo } from 'react' +import { useColorScheme } from 'react-native' +import { darkColors } from './colors.dark' +import { lightColors } from './colors.light' +import type { ThemeColors } from './types' + +const ThemeContext = createContext(darkColors) + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const scheme = useColorScheme() + const colors = useMemo( + () => (scheme === 'light' ? lightColors : darkColors), + [scheme], + ) + return ( + {children} + ) +} + +export function useThemeColors(): ThemeColors { + return useContext(ThemeContext) +} diff --git a/src/shared/theme/colors.dark.ts b/src/shared/theme/colors.dark.ts new file mode 100644 index 0000000..91cff01 --- /dev/null +++ b/src/shared/theme/colors.dark.ts @@ -0,0 +1,124 @@ +/** + * Dark theme palette + * Values extracted from constants/colors.ts — same values, ThemeColors shape + */ + +import { BRAND } from '../constants/colors' +import type { ThemeColors } from './types' + +export const darkColors: ThemeColors = { + bg: { + base: '#000000', + surface: '#1C1C1E', + elevated: '#2C2C2E', + overlay1: 'rgba(255, 255, 255, 0.05)', + overlay2: 'rgba(255, 255, 255, 0.08)', + overlay3: 'rgba(255, 255, 255, 0.12)', + scrim: 'rgba(0, 0, 0, 0.6)', + }, + text: { + primary: '#FFFFFF', + secondary: '#EBEBF5', + tertiary: 'rgba(235, 235, 245, 0.6)', + muted: 'rgba(235, 235, 245, 0.5)', + hint: 'rgba(235, 235, 245, 0.3)', + disabled: '#3A3A3C', + }, + glass: { + base: { + backgroundColor: 'rgba(255, 255, 255, 0.05)', + borderColor: 'rgba(255, 255, 255, 0.1)', + borderWidth: 1, + }, + elevated: { + backgroundColor: 'rgba(255, 255, 255, 0.08)', + borderColor: 'rgba(255, 255, 255, 0.15)', + borderWidth: 1, + }, + inset: { + backgroundColor: 'rgba(0, 0, 0, 0.25)', + borderColor: 'rgba(255, 255, 255, 0.05)', + borderWidth: 1, + }, + tinted: { + backgroundColor: 'rgba(255, 107, 53, 0.12)', + borderColor: 'rgba(255, 107, 53, 0.25)', + borderWidth: 1, + }, + successTinted: { + backgroundColor: 'rgba(48, 209, 88, 0.12)', + borderColor: 'rgba(48, 209, 88, 0.25)', + borderWidth: 1, + }, + blurTint: 'dark', + blurLight: 20, + blurMedium: 40, + blurHeavy: 60, + blurUltra: 80, + }, + shadow: { + sm: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 4, + }, + md: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.25, + shadowRadius: 24, + elevation: 8, + }, + lg: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 12 }, + shadowOpacity: 0.35, + shadowRadius: 40, + elevation: 12, + }, + xl: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 16 }, + shadowOpacity: 0.4, + shadowRadius: 48, + elevation: 16, + }, + BRAND_GLOW: { + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.5, + shadowRadius: 20, + elevation: 10, + }, + BRAND_GLOW_SUBTLE: { + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.3, + shadowRadius: 12, + elevation: 6, + }, + LIQUID_GLOW: { + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.4, + shadowRadius: 30, + elevation: 15, + }, + }, + border: { + glass: 'rgba(255, 255, 255, 0.1)', + glassLight: 'rgba(255, 255, 255, 0.05)', + glassStrong: 'rgba(255, 255, 255, 0.2)', + brand: 'rgba(255, 107, 53, 0.3)', + success: 'rgba(48, 209, 88, 0.3)', + }, + gradients: { + videoOverlay: ['transparent', 'rgba(0, 0, 0, 0.8)'], + videoTop: ['rgba(0, 0, 0, 0.5)', 'transparent'], + glassShimmer: ['rgba(255,255,255,0.1)', 'rgba(255,255,255,0.05)', 'rgba(255,255,255,0.1)'], + }, + colorScheme: 'dark', + statusBarStyle: 'light', +} diff --git a/src/shared/theme/colors.light.ts b/src/shared/theme/colors.light.ts new file mode 100644 index 0000000..c22d002 --- /dev/null +++ b/src/shared/theme/colors.light.ts @@ -0,0 +1,124 @@ +/** + * Light theme palette + * Apple HIG-aligned light mode values + */ + +import { BRAND } from '../constants/colors' +import type { ThemeColors } from './types' + +export const lightColors: ThemeColors = { + bg: { + base: '#FFFFFF', + surface: '#F2F2F7', + elevated: '#FFFFFF', + overlay1: 'rgba(0, 0, 0, 0.03)', + overlay2: 'rgba(0, 0, 0, 0.05)', + overlay3: 'rgba(0, 0, 0, 0.08)', + scrim: 'rgba(0, 0, 0, 0.4)', + }, + text: { + primary: '#000000', + secondary: '#3C3C43', + tertiary: 'rgba(60, 60, 67, 0.6)', + muted: 'rgba(60, 60, 67, 0.5)', + hint: 'rgba(60, 60, 67, 0.3)', + disabled: '#C7C7CC', + }, + glass: { + base: { + backgroundColor: 'rgba(0, 0, 0, 0.03)', + borderColor: 'rgba(0, 0, 0, 0.08)', + borderWidth: 1, + }, + elevated: { + backgroundColor: 'rgba(0, 0, 0, 0.05)', + borderColor: 'rgba(0, 0, 0, 0.1)', + borderWidth: 1, + }, + inset: { + backgroundColor: 'rgba(0, 0, 0, 0.06)', + borderColor: 'rgba(0, 0, 0, 0.04)', + borderWidth: 1, + }, + tinted: { + backgroundColor: 'rgba(255, 107, 53, 0.08)', + borderColor: 'rgba(255, 107, 53, 0.2)', + borderWidth: 1, + }, + successTinted: { + backgroundColor: 'rgba(48, 209, 88, 0.08)', + borderColor: 'rgba(48, 209, 88, 0.2)', + borderWidth: 1, + }, + blurTint: 'light', + blurLight: 20, + blurMedium: 40, + blurHeavy: 60, + blurUltra: 80, + }, + shadow: { + sm: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 2, + }, + md: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.12, + shadowRadius: 16, + elevation: 4, + }, + lg: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.15, + shadowRadius: 24, + elevation: 8, + }, + xl: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 12 }, + shadowOpacity: 0.18, + shadowRadius: 32, + elevation: 12, + }, + BRAND_GLOW: { + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.3, + shadowRadius: 16, + elevation: 8, + }, + BRAND_GLOW_SUBTLE: { + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.2, + shadowRadius: 10, + elevation: 4, + }, + LIQUID_GLOW: { + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.25, + shadowRadius: 24, + elevation: 12, + }, + }, + border: { + glass: 'rgba(0, 0, 0, 0.1)', + glassLight: 'rgba(0, 0, 0, 0.05)', + glassStrong: 'rgba(0, 0, 0, 0.15)', + brand: 'rgba(255, 107, 53, 0.3)', + success: 'rgba(48, 209, 88, 0.3)', + }, + gradients: { + videoOverlay: ['transparent', 'rgba(0, 0, 0, 0.6)'], + videoTop: ['rgba(0, 0, 0, 0.3)', 'transparent'], + glassShimmer: ['rgba(0,0,0,0.05)', 'rgba(0,0,0,0.02)', 'rgba(0,0,0,0.05)'], + }, + colorScheme: 'light', + statusBarStyle: 'dark', +} diff --git a/src/shared/theme/index.ts b/src/shared/theme/index.ts new file mode 100644 index 0000000..c1ebf31 --- /dev/null +++ b/src/shared/theme/index.ts @@ -0,0 +1,5 @@ +export { ThemeProvider, useThemeColors } from './ThemeContext' +export { darkColors } from './colors.dark' +export type { ThemeColors } from './types' +// Re-export invariant colors +export { BRAND, PHASE, PHASE_COLORS, GRADIENTS } from '../constants/colors' diff --git a/src/shared/theme/types.ts b/src/shared/theme/types.ts new file mode 100644 index 0000000..dae94f5 --- /dev/null +++ b/src/shared/theme/types.ts @@ -0,0 +1,73 @@ +/** + * Theme type definitions + * Same shape for light and dark palettes + */ + +export interface GlassStyle { + backgroundColor: string + borderColor: string + borderWidth: number +} + +export interface ShadowStyle { + shadowColor: string + shadowOffset: { width: number; height: number } + shadowOpacity: number + shadowRadius: number + elevation: number +} + +export interface ThemeColors { + bg: { + base: string + surface: string + elevated: string + overlay1: string + overlay2: string + overlay3: string + scrim: string + } + text: { + primary: string + secondary: string + tertiary: string + muted: string + hint: string + disabled: string + } + glass: { + base: GlassStyle + elevated: GlassStyle + inset: GlassStyle + tinted: GlassStyle + successTinted: GlassStyle + blurTint: 'dark' | 'light' + blurLight: number + blurMedium: number + blurHeavy: number + blurUltra: number + } + shadow: { + sm: ShadowStyle + md: ShadowStyle + lg: ShadowStyle + xl: ShadowStyle + BRAND_GLOW: ShadowStyle + BRAND_GLOW_SUBTLE: ShadowStyle + LIQUID_GLOW: ShadowStyle + } + border: { + glass: string + glassLight: string + glassStrong: string + brand: string + success: string + } + gradients: { + videoOverlay: readonly string[] + videoTop: readonly string[] + glassShimmer: readonly string[] + } + colorScheme: 'dark' | 'light' + statusBarStyle: 'light' | 'dark' +}