feat: onboarding flow (6 screens) + audio engine + design system
Onboarding: - 6-screen flow: Problem → Empathy → Solution → Wow → Personalization → Paywall - useOnboarding hook with Zustand + AsyncStorage persistence - MiniTimerDemo with live 20s timer + haptics - Auto-redirect for first-time users - Mock RevenueCat for dev testing Audio: - useAudioEngine hook with expo-av - Phase sounds (count_3/2/1, beep, bell, fanfare) - Placeholder music tracks Design System: - Typography component + constants - GlassView component - Spacing, shadows, animations, borderRadius constants - Extended color palette (phase gradients, glass, surfaces) Timer: - Fix: handle 0-duration phases (immediate advance) - Enhanced TimerDisplay with phase gradients Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
32
src/shared/components/GlassView.tsx
Normal file
32
src/shared/components/GlassView.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { StyleSheet, View, type ViewProps } from 'react-native'
|
||||
import { BlurView } from 'expo-blur'
|
||||
import { GLASS } from '../constants/colors'
|
||||
|
||||
interface GlassViewProps extends ViewProps {
|
||||
intensity?: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function GlassView({
|
||||
intensity = GLASS.BLUR_MEDIUM,
|
||||
style,
|
||||
children,
|
||||
...rest
|
||||
}: GlassViewProps) {
|
||||
return (
|
||||
<View style={[styles.container, style]} {...rest}>
|
||||
<BlurView intensity={intensity} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
overflow: 'hidden',
|
||||
backgroundColor: GLASS.FILL,
|
||||
borderWidth: 0.5,
|
||||
borderColor: GLASS.BORDER,
|
||||
borderTopColor: GLASS.BORDER_TOP,
|
||||
},
|
||||
})
|
||||
25
src/shared/components/Typography.tsx
Normal file
25
src/shared/components/Typography.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Text, type TextProps } from 'react-native'
|
||||
import { TYPOGRAPHY } from '../constants/typography'
|
||||
import { TEXT } from '../constants/colors'
|
||||
|
||||
type TypographyVariant = keyof typeof TYPOGRAPHY
|
||||
|
||||
interface TypographyProps extends TextProps {
|
||||
variant: TypographyVariant
|
||||
color?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Typography({
|
||||
variant,
|
||||
color = TEXT.PRIMARY,
|
||||
style,
|
||||
children,
|
||||
...rest
|
||||
}: TypographyProps) {
|
||||
return (
|
||||
<Text style={[TYPOGRAPHY[variant], { color }, style]} {...rest}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
51
src/shared/constants/animations.ts
Normal file
51
src/shared/constants/animations.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Easing } from 'react-native'
|
||||
|
||||
export const DURATION = {
|
||||
SNAP: 60,
|
||||
QUICK: 120,
|
||||
FAST: 300,
|
||||
NORMAL: 400,
|
||||
SLOW: 600,
|
||||
XSLOW: 1000,
|
||||
BREATH: 1800,
|
||||
} as const
|
||||
|
||||
export const EASING = {
|
||||
STANDARD: Easing.inOut(Easing.ease),
|
||||
DECELERATE: Easing.out(Easing.ease),
|
||||
LINEAR: Easing.linear,
|
||||
} as const
|
||||
|
||||
export const SPRING = {
|
||||
BOUNCY: { tension: 50, friction: 7 },
|
||||
} as const
|
||||
|
||||
export const ANIMATION = {
|
||||
PULSE_UP: {
|
||||
toValue: 1.08,
|
||||
duration: DURATION.SNAP,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
PULSE_DOWN: {
|
||||
toValue: 1,
|
||||
duration: DURATION.QUICK,
|
||||
easing: EASING.DECELERATE,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
GRADIENT_CROSSFADE: {
|
||||
toValue: 1,
|
||||
duration: DURATION.SLOW,
|
||||
easing: EASING.STANDARD,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
FADE_IN: {
|
||||
toValue: 1,
|
||||
duration: DURATION.NORMAL,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
BREATH_HALF: {
|
||||
duration: DURATION.BREATH,
|
||||
easing: EASING.STANDARD,
|
||||
useNativeDriver: false,
|
||||
},
|
||||
} as const
|
||||
11
src/shared/constants/borderRadius.ts
Normal file
11
src/shared/constants/borderRadius.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const RADIUS = {
|
||||
XS: 4,
|
||||
SM: 6,
|
||||
MD: 12,
|
||||
LG: 16,
|
||||
XL: 20,
|
||||
'2XL': 28,
|
||||
'3XL': 32,
|
||||
'4XL': 36,
|
||||
FULL: 9999,
|
||||
} as const
|
||||
@@ -5,3 +5,72 @@ export const PHASE_COLORS = {
|
||||
REST: '#3B82F6',
|
||||
COMPLETE: '#22C55E',
|
||||
} as const
|
||||
|
||||
export const PHASE_GRADIENTS = {
|
||||
IDLE: ['#0A0A14', '#12101F', '#1E1E2E'] as const,
|
||||
GET_READY: ['#451A03', '#92400E', '#D97706'] as const,
|
||||
WORK: ['#450A0A', '#991B1B', '#EA580C'] as const,
|
||||
REST: ['#0C1929', '#1E3A5F', '#2563EB'] as const,
|
||||
COMPLETE: ['#052E16', '#166534', '#16A34A'] as const,
|
||||
} as const
|
||||
|
||||
export const ACCENT = {
|
||||
ORANGE: '#F97316',
|
||||
ORANGE_GLOW: '#FB923C',
|
||||
RED_HOT: '#EF4444',
|
||||
GOLD: '#FBBF24',
|
||||
WHITE: '#FFFFFF',
|
||||
WHITE_DIM: 'rgba(255, 255, 255, 0.6)',
|
||||
WHITE_FAINT: 'rgba(255, 255, 255, 0.15)',
|
||||
} as const
|
||||
|
||||
export const BRAND = {
|
||||
PRIMARY: '#F97316',
|
||||
PRIMARY_LIGHT: '#FB923C',
|
||||
SECONDARY: '#FBBF24',
|
||||
DANGER: '#EF4444',
|
||||
SUCCESS: '#22C55E',
|
||||
INFO: '#3B82F6',
|
||||
} as const
|
||||
|
||||
export const SURFACE = {
|
||||
BASE: '#0A0A14',
|
||||
RAISED: '#12101F',
|
||||
ELEVATED: '#1E1E2E',
|
||||
OVERLAY_LIGHT: 'rgba(255, 255, 255, 0.08)',
|
||||
OVERLAY_MEDIUM: 'rgba(255, 255, 255, 0.15)',
|
||||
OVERLAY_STRONG: 'rgba(255, 255, 255, 0.25)',
|
||||
SCRIM: 'rgba(0, 0, 0, 0.3)',
|
||||
} as const
|
||||
|
||||
export const TEXT = {
|
||||
PRIMARY: '#FFFFFF',
|
||||
SECONDARY: 'rgba(255, 255, 255, 0.85)',
|
||||
TERTIARY: 'rgba(255, 255, 255, 0.75)',
|
||||
MUTED: 'rgba(255, 255, 255, 0.6)',
|
||||
HINT: 'rgba(255, 255, 255, 0.45)',
|
||||
DISABLED: 'rgba(255, 255, 255, 0.15)',
|
||||
} as const
|
||||
|
||||
export const BORDER = {
|
||||
SUBTLE: 'rgba(255, 255, 255, 0.05)',
|
||||
LIGHT: 'rgba(255, 255, 255, 0.06)',
|
||||
MEDIUM: 'rgba(255, 255, 255, 0.15)',
|
||||
STRONG: 'rgba(255, 255, 255, 0.3)',
|
||||
} as const
|
||||
|
||||
export const APP_GRADIENTS = {
|
||||
HOME: ['#0A0A14', '#1A0E2E', '#2D1810'] as const,
|
||||
} as const
|
||||
|
||||
export const GLASS = {
|
||||
FILL: 'rgba(255, 255, 255, 0.03)',
|
||||
FILL_MEDIUM: 'rgba(255, 255, 255, 0.06)',
|
||||
FILL_STRONG: 'rgba(255, 255, 255, 0.10)',
|
||||
BORDER: 'rgba(255, 255, 255, 0.08)',
|
||||
BORDER_TOP: 'rgba(255, 255, 255, 0.15)',
|
||||
BLUR_LIGHT: 15,
|
||||
BLUR_MEDIUM: 25,
|
||||
BLUR_HEAVY: 40,
|
||||
BLUR_ATMOSPHERE: 60,
|
||||
} as const
|
||||
|
||||
16
src/shared/constants/index.ts
Normal file
16
src/shared/constants/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export {
|
||||
PHASE_COLORS,
|
||||
PHASE_GRADIENTS,
|
||||
ACCENT,
|
||||
BRAND,
|
||||
SURFACE,
|
||||
TEXT,
|
||||
BORDER,
|
||||
APP_GRADIENTS,
|
||||
GLASS,
|
||||
} from './colors'
|
||||
export { TYPOGRAPHY } from './typography'
|
||||
export { SPACING, LAYOUT } from './spacing'
|
||||
export { SHADOW, TEXT_SHADOW } from './shadows'
|
||||
export { RADIUS } from './borderRadius'
|
||||
export { DURATION, EASING, SPRING, ANIMATION } from './animations'
|
||||
44
src/shared/constants/shadows.ts
Normal file
44
src/shared/constants/shadows.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { TextStyle, ViewStyle } from 'react-native'
|
||||
|
||||
export const SHADOW = {
|
||||
BRAND_GLOW: {
|
||||
shadowColor: '#F97316',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
} as ViewStyle,
|
||||
|
||||
WHITE_GLOW: {
|
||||
shadowColor: '#FFFFFF',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.8,
|
||||
shadowRadius: 6,
|
||||
} as ViewStyle,
|
||||
} as const
|
||||
|
||||
export const TEXT_SHADOW = {
|
||||
BRAND: {
|
||||
textShadowColor: 'rgba(249, 115, 22, 0.5)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 30,
|
||||
} as TextStyle,
|
||||
|
||||
WHITE_SOFT: {
|
||||
textShadowColor: 'rgba(255, 255, 255, 0.25)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 30,
|
||||
} as TextStyle,
|
||||
|
||||
WHITE_MEDIUM: {
|
||||
textShadowColor: 'rgba(255, 255, 255, 0.3)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 20,
|
||||
} as TextStyle,
|
||||
|
||||
DANGER: {
|
||||
textShadowColor: 'rgba(239, 68, 68, 0.6)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 40,
|
||||
} as TextStyle,
|
||||
} as const
|
||||
25
src/shared/constants/spacing.ts
Normal file
25
src/shared/constants/spacing.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const SPACING = {
|
||||
0: 0,
|
||||
0.5: 2,
|
||||
1: 4,
|
||||
1.5: 6,
|
||||
2: 8,
|
||||
2.5: 10,
|
||||
3: 12,
|
||||
3.5: 14,
|
||||
4: 16,
|
||||
5: 20,
|
||||
6: 24,
|
||||
8: 32,
|
||||
10: 40,
|
||||
12: 48,
|
||||
14: 56,
|
||||
15: 60,
|
||||
} as const
|
||||
|
||||
export const LAYOUT = {
|
||||
PAGE_HORIZONTAL: 24,
|
||||
SECTION_GAP: 40,
|
||||
INLINE_GAP: 16,
|
||||
CONTROLS_GAP: 32,
|
||||
} as const
|
||||
73
src/shared/constants/typography.ts
Normal file
73
src/shared/constants/typography.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { TextStyle } from 'react-native'
|
||||
|
||||
export const TYPOGRAPHY = {
|
||||
countdown: {
|
||||
fontSize: 140,
|
||||
fontWeight: '900',
|
||||
fontVariant: ['tabular-nums'],
|
||||
} as TextStyle,
|
||||
|
||||
timeDisplay: {
|
||||
fontSize: 72,
|
||||
fontWeight: '900',
|
||||
fontVariant: ['tabular-nums'],
|
||||
} as TextStyle,
|
||||
|
||||
brandTitle: {
|
||||
fontSize: 56,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 12,
|
||||
} as TextStyle,
|
||||
|
||||
displayLarge: {
|
||||
fontSize: 42,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 4,
|
||||
} as TextStyle,
|
||||
|
||||
displaySmall: {
|
||||
fontSize: 32,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 20,
|
||||
} as TextStyle,
|
||||
|
||||
buttonHero: {
|
||||
fontSize: 30,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 5,
|
||||
} as TextStyle,
|
||||
|
||||
heading: {
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
} as TextStyle,
|
||||
|
||||
buttonMedium: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 2,
|
||||
} as TextStyle,
|
||||
|
||||
body: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
} as TextStyle,
|
||||
|
||||
caption: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
} as TextStyle,
|
||||
|
||||
label: {
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1,
|
||||
} as TextStyle,
|
||||
|
||||
overline: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
} as TextStyle,
|
||||
} as const
|
||||
Reference in New Issue
Block a user