feat: system light/dark theme infrastructure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-21 00:04:59 +01:00
parent b60083341e
commit f17125e231
5 changed files with 353 additions and 0 deletions

View File

@@ -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<ThemeColors>(darkColors)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const scheme = useColorScheme()
const colors = useMemo(
() => (scheme === 'light' ? lightColors : darkColors),
[scheme],
)
return (
<ThemeContext.Provider value={colors}>{children}</ThemeContext.Provider>
)
}
export function useThemeColors(): ThemeColors {
return useContext(ThemeContext)
}

View File

@@ -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',
}

View File

@@ -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',
}

View File

@@ -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'

73
src/shared/theme/types.ts Normal file
View File

@@ -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'
}