feat: system light/dark theme infrastructure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
src/shared/theme/ThemeContext.tsx
Normal file
27
src/shared/theme/ThemeContext.tsx
Normal 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)
|
||||
}
|
||||
124
src/shared/theme/colors.dark.ts
Normal file
124
src/shared/theme/colors.dark.ts
Normal 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',
|
||||
}
|
||||
124
src/shared/theme/colors.light.ts
Normal file
124
src/shared/theme/colors.light.ts
Normal 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',
|
||||
}
|
||||
5
src/shared/theme/index.ts
Normal file
5
src/shared/theme/index.ts
Normal 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
73
src/shared/theme/types.ts
Normal 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'
|
||||
}
|
||||
Reference in New Issue
Block a user