refresh design system: colors, typography, native components

Update color palette, dark theme tokens, typography scale, and border
radius constants. Refactor native UI primitives (NativeButton, NativeList,
NativeSection, NativeLabeledRow) for the new design language. Simplify
OnboardingStep. Remove legacy WorkoutCard and CollectionCard components.
This commit is contained in:
Millian Lamiaux
2026-04-21 21:50:31 +02:00
parent 13262305e5
commit 04b83fc419
13 changed files with 467 additions and 673 deletions

View File

@@ -1,186 +0,0 @@
/**
* CollectionCard - Premium collection card
* Used in Explore and Browse screens
* Supports 'default' and 'hero' variants
*/
import { useMemo, useRef, useCallback } from 'react'
import {
View,
StyleSheet,
Pressable,
Animated,
ImageBackground,
useWindowDimensions,
Text as RNText,
} from 'react-native'
import { useThemeColors, GRADIENTS } from '@/src/shared/theme'
import type { ThemeColors } from '@/src/shared/theme/types'
import { TEXT, NAVY, GREEN } from '@/src/shared/constants/colors'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
import { StyledText } from './StyledText'
import type { Collection } from '@/src/shared/types'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
export type CollectionCardVariant = 'default' | 'hero' | 'horizontal'
interface CollectionCardProps {
collection: Collection
variant?: CollectionCardVariant
onPress?: () => void
imageUrl?: string
workoutCountLabel?: string
}
export function CollectionCard({ collection, variant = 'default', onPress, imageUrl, workoutCountLabel }: CollectionCardProps) {
const colors = useThemeColors()
const { width: screenWidth } = useWindowDimensions()
const styles = useMemo(() => createStyles(colors, screenWidth, variant), [colors, screenWidth, variant])
// Press animation
const scaleValue = useRef(new Animated.Value(1)).current
const handlePressIn = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 0.97,
useNativeDriver: true,
speed: 50,
bounciness: 4,
}).start()
}, [scaleValue])
const handlePressOut = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 1,
useNativeDriver: true,
speed: 30,
bounciness: 6,
}).start()
}, [scaleValue])
const countLabel = workoutCountLabel ?? `${collection.workoutIds.length} workouts`
return (
<AnimatedPressable
style={[styles.container, { transform: [{ scale: scaleValue }] }]}
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
{/* Background Image or Solid Color */}
{imageUrl ? (
<ImageBackground
source={{ uri: imageUrl }}
style={StyleSheet.absoluteFill}
imageStyle={{ borderRadius: RADIUS.XL }}
resizeMode="cover"
>
<View
style={[
StyleSheet.absoluteFill,
{
backgroundColor: GRADIENTS.VIDEO_OVERLAY[1],
},
]}
/>
</ImageBackground>
) : (
<View
style={[
StyleSheet.absoluteFill,
{
borderRadius: RADIUS.XL,
backgroundColor: NAVY[800],
},
]}
/>
)}
{/* Content */}
<View style={styles.content}>
<View style={styles.iconContainer}>
<RNText style={styles.icon}>{collection.icon}</RNText>
</View>
<StyledText
preset={variant === 'hero' ? 'TITLE_2' : 'HEADLINE'}
color={TEXT.PRIMARY}
numberOfLines={2}
style={styles.title}
>
{collection.title}
</StyledText>
{variant === 'hero' && (
<StyledText
preset="CARD_SUBTITLE"
color={TEXT.SECONDARY}
numberOfLines={2}
style={{ marginBottom: SPACING[1] }}
>
{collection.description}
</StyledText>
)}
<StyledText
preset="CARD_METADATA"
color={TEXT.TERTIARY}
numberOfLines={1}
>
{countLabel}
</StyledText>
</View>
</AnimatedPressable>
)
}
function createStyles(colors: ThemeColors, screenWidth: number, variant: CollectionCardVariant) {
const defaultCardWidth = (screenWidth - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2
const horizontalCardWidth = screenWidth * 0.65
const containerByVariant = {
default: {
width: defaultCardWidth,
aspectRatio: 1 as number,
},
hero: {
width: screenWidth - LAYOUT.SCREEN_PADDING * 2,
height: 200,
},
horizontal: {
width: horizontalCardWidth,
height: 180,
},
}
return StyleSheet.create({
container: {
...containerByVariant[variant],
borderRadius: RADIUS.XL,
overflow: 'hidden',
},
content: {
flex: 1,
padding: SPACING[4],
justifyContent: 'flex-end',
},
iconContainer: {
width: 48,
height: 48,
borderRadius: RADIUS.LG,
backgroundColor: GREEN.DIM,
justifyContent: 'center',
alignItems: 'center',
marginBottom: SPACING[3],
borderWidth: 1,
borderColor: GREEN.BORDER,
},
icon: {
fontSize: 24,
},
title: {
marginBottom: SPACING[1],
},
})
}

View File

@@ -1,19 +1,20 @@
/**
* TabataFit OnboardingStep
* Reusable wrapper for each onboarding screen — progress bar, animation, layout
*
* Revamped: fade-up entrance, segmented progress, generous spacing
*/
import { useRef, useEffect, useMemo } from 'react'
import { View, StyleSheet, Animated, Dimensions, Pressable } from 'react-native'
import { View, StyleSheet, Animated, Pressable } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { Icon } from './Icon'
import { useThemeColors, BRAND } from '../theme'
import { useThemeColors } from '../theme'
import type { ThemeColors } from '../theme/types'
import { SPACING, LAYOUT } from '../constants/spacing'
import { RADIUS } from '../constants/borderRadius'
import { DURATION, EASE } from '../constants/animations'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
import { DURATION, EASE, SPRING } from '../constants/animations'
import { GREEN } from '../constants/colors'
interface OnboardingStepProps {
step: number
@@ -26,62 +27,74 @@ export function OnboardingStep({ step, totalSteps, children, onBack }: Onboardin
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
const insets = useSafeAreaInsets()
const slideAnim = useRef(new Animated.Value(SCREEN_WIDTH)).current
const fadeAnim = useRef(new Animated.Value(0)).current
const progressAnim = useRef(new Animated.Value(0)).current
const slideAnim = useRef(new Animated.Value(24)).current
const progressAnims = useRef(
Array.from({ length: totalSteps }, () => new Animated.Value(0))
).current
useEffect(() => {
// Reset position for new step
slideAnim.setValue(SCREEN_WIDTH)
// Reset for new step — fade up instead of slide right
fadeAnim.setValue(0)
slideAnim.setValue(24)
// Animate in
Animated.parallel([
Animated.timing(slideAnim, {
toValue: 0,
duration: DURATION.NORMAL,
easing: EASE.EASE_OUT,
useNativeDriver: true,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: DURATION.NORMAL,
easing: EASE.EASE_OUT,
useNativeDriver: true,
}),
Animated.spring(slideAnim, {
toValue: 0,
...SPRING.GENTLE,
useNativeDriver: true,
}),
]).start()
// Animate progress bar
Animated.timing(progressAnim, {
toValue: step / totalSteps,
duration: DURATION.SLOW,
easing: EASE.EASE_OUT,
useNativeDriver: false,
}).start()
// Animate progress segments
progressAnims.forEach((anim, i) => {
Animated.spring(anim, {
toValue: i < step ? 1 : 0,
...SPRING.SNAPPY,
useNativeDriver: false,
}).start()
})
}, [step])
const progressWidth = progressAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
})
return (
<View style={[styles.container, { paddingTop: insets.top + SPACING[3] }]}>
{/* Progress bar */}
<View style={styles.progressTrack}>
<Animated.View style={[styles.progressFill, { width: progressWidth }]} />
<View style={[styles.container, { paddingTop: insets.top + SPACING[4] }]}>
{/* Segmented progress bar */}
<View style={styles.progressRow}>
{progressAnims.map((anim, i) => (
<View key={i} style={styles.segmentTrack}>
<Animated.View
style={[
styles.segmentFill,
{
width: anim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
}),
},
]}
/>
</View>
))}
</View>
{/* Back button — visible on steps 2+ */}
{onBack && step > 1 && (
{onBack && step > 1 ? (
<Pressable
style={styles.backButton}
onPress={onBack}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
testID="onboarding-back-button"
>
<Icon name="chevron.left" size={24} tintColor={colors.text.secondary} />
<Icon name="chevron.left" size={22} tintColor={colors.text.secondary} />
</Pressable>
) : (
<View style={styles.backSpacer} />
)}
{/* Step content */}
@@ -89,10 +102,10 @@ export function OnboardingStep({ step, totalSteps, children, onBack }: Onboardin
style={[
styles.content,
{
transform: [{ translateX: slideAnim }],
transform: [{ translateY: slideAnim }],
opacity: fadeAnim,
},
{ paddingBottom: insets.bottom + SPACING[6] },
{ paddingBottom: insets.bottom + SPACING[8] },
]}
>
{children}
@@ -107,30 +120,37 @@ function createStyles(colors: ThemeColors) {
flex: 1,
backgroundColor: colors.bg.base,
},
progressTrack: {
height: 3,
backgroundColor: colors.bg.surface,
progressRow: {
flexDirection: 'row',
gap: SPACING[1],
marginHorizontal: LAYOUT.SCREEN_PADDING,
borderRadius: RADIUS.XS,
},
segmentTrack: {
flex: 1,
height: 4,
backgroundColor: colors.bg.overlay2,
borderRadius: RADIUS.PILL,
overflow: 'hidden',
},
progressFill: {
segmentFill: {
height: '100%',
backgroundColor: BRAND.PRIMARY,
borderRadius: RADIUS.XS,
backgroundColor: GREEN[500],
borderRadius: RADIUS.PILL,
},
backButton: {
marginTop: SPACING[3],
marginLeft: SPACING[3],
width: 40,
height: 40,
marginLeft: SPACING[2],
width: LAYOUT.TOUCH_TARGET,
height: LAYOUT.TOUCH_TARGET,
alignItems: 'center',
justifyContent: 'center',
},
backSpacer: {
height: SPACING[3] + LAYOUT.TOUCH_TARGET,
},
content: {
flex: 1,
paddingHorizontal: LAYOUT.SCREEN_PADDING,
paddingTop: SPACING[8],
},
})
}

View File

@@ -1,232 +0,0 @@
/**
* WorkoutCard - Dark Medical workout card
* Flat navy surface with border-dim, no glassmorphism
*/
import { useMemo, useRef, useCallback } from 'react'
import {
View,
StyleSheet,
Pressable,
Animated,
ImageBackground,
useWindowDimensions,
Text as RNText,
ViewStyle,
} from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
import { Icon } from './Icon'
import { useThemeColors } from '@/src/shared/theme'
import { BRAND, GRADIENTS, TEXT, NAVY, BORDER_COLORS, PHASE } from '@/src/shared/constants/colors'
import { FONT_FAMILY_SANS_SEMIBOLD } from '@/src/shared/constants/typography'
import type { ThemeColors } from '@/src/shared/theme/types'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import { SPACING } from '@/src/shared/constants/spacing'
import { StyledText } from './StyledText'
import type { Workout, WorkoutCategory } from '@/src/shared/types'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
export type WorkoutCardVariant = 'horizontal' | 'grid' | 'featured'
interface WorkoutCardProps {
workout: Workout
variant?: WorkoutCardVariant
onPress?: () => void
title?: string
metadata?: string
trainerName?: string
isLocked?: boolean
}
export const CATEGORY_COLORS: Record<WorkoutCategory, string> = {
'full-body': BRAND.PRIMARY,
'core': BRAND.INFO,
'upper-body': TEXT.SECONDARY,
'lower-body': BRAND.PRIMARY,
'cardio': PHASE.PREP,
}
const CATEGORY_LABELS: Record<WorkoutCategory, string> = {
'full-body': 'Full Body',
'core': 'Core',
'upper-body': 'Upper Body',
'lower-body': 'Lower Body',
'cardio': 'Cardio',
}
function getVariantDimensions(variant: WorkoutCardVariant, screenWidth: number): ViewStyle {
switch (variant) {
case 'featured':
return {
width: screenWidth - SPACING[6] * 2,
height: 320,
}
case 'horizontal':
return {
width: 200,
height: 280,
}
case 'grid':
default:
return {
flex: 1,
aspectRatio: 0.75,
}
}
}
export function WorkoutCard({
workout,
variant = 'horizontal',
onPress,
title,
metadata,
trainerName,
isLocked,
}: WorkoutCardProps) {
const colors = useThemeColors()
const { width: screenWidth } = useWindowDimensions()
const styles = useMemo(() => createStyles(colors), [colors])
const dimensions = useMemo(() => getVariantDimensions(variant, screenWidth), [variant, screenWidth])
// Press animation
const scaleValue = useRef(new Animated.Value(1)).current
const handlePressIn = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 0.97,
useNativeDriver: true,
speed: 50,
bounciness: 4,
}).start()
}, [scaleValue])
const handlePressOut = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 1,
useNativeDriver: true,
speed: 30,
bounciness: 6,
}).start()
}, [scaleValue])
const displayTitle = title ?? workout.title
const metaParts = [
`${workout.duration} min`,
`${workout.calories} cal`,
...(trainerName ? [trainerName] : []),
]
const displayMetadata = metadata ?? metaParts.join(' · ')
const categoryColor = CATEGORY_COLORS[workout.category]
return (
<AnimatedPressable
style={[styles.container, dimensions, { transform: [{ scale: scaleValue }] }]}
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
{/* Background Image */}
<ImageBackground
source={workout.thumbnailUrl ? { uri: workout.thumbnailUrl } : undefined}
style={StyleSheet.absoluteFill}
imageStyle={{ borderRadius: RADIUS.LG }}
resizeMode="cover"
>
{/* Dark overlay for text readability */}
<LinearGradient
colors={GRADIENTS.CARD_OVERLAY}
style={StyleSheet.absoluteFill}
/>
</ImageBackground>
{/* Category Badge — flat navy */}
<View style={[styles.categoryBadge, { backgroundColor: `${categoryColor}20`, borderColor: `${categoryColor}40` }]}>
<RNText style={[styles.categoryText, { color: categoryColor }]}>
{CATEGORY_LABELS[workout.category]}
</RNText>
</View>
{/* Play Button — flat navy circle */}
<View style={styles.playButtonContainer}>
<View style={styles.playButton}>
<Icon name={isLocked ? 'lock.fill' : 'play.fill'} size={24} tintColor={TEXT.PRIMARY} style={isLocked ? undefined : { marginLeft: 2 }} />
</View>
</View>
{/* Content at Bottom */}
<View style={styles.content}>
<StyledText
preset={variant === 'featured' ? 'TITLE_2' : 'HEADLINE'}
color={TEXT.PRIMARY}
numberOfLines={2}
style={styles.title}
>
{displayTitle}
</StyledText>
<StyledText
preset="CARD_METADATA"
color={TEXT.SECONDARY}
numberOfLines={1}
>
{displayMetadata}
</StyledText>
</View>
</AnimatedPressable>
)
}
function createStyles(_colors: ThemeColors) {
return StyleSheet.create({
container: {
borderRadius: RADIUS.LG,
borderCurve: 'continuous',
overflow: 'hidden',
backgroundColor: NAVY[800],
borderWidth: 1,
borderColor: BORDER_COLORS.DIM,
},
categoryBadge: {
position: 'absolute',
top: SPACING[3],
left: SPACING[3],
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: SPACING[3],
paddingVertical: SPACING[1],
borderRadius: RADIUS.SM,
borderCurve: 'continuous',
borderWidth: 1,
overflow: 'hidden',
},
categoryText: {
fontSize: 11,
fontFamily: FONT_FAMILY_SANS_SEMIBOLD,
},
playButtonContainer: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
},
playButton: {
width: 56,
height: 56,
borderRadius: RADIUS.FULL,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(17,34,64,0.7)', // Semi-transparent NAVY[800] overlay
borderWidth: 1,
borderColor: BORDER_COLORS.DIM,
},
content: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: SPACING[4],
},
title: {
marginBottom: SPACING[1],
},
})
}

View File

@@ -1,10 +1,22 @@
import { Pressable, StyleSheet, Text, type ViewStyle } from 'react-native'
import { Pressable, StyleSheet, Text, View, type ViewStyle, type TextStyle } from 'react-native'
import { Image } from 'expo-image'
import type { SFSymbol } from 'sf-symbols-typescript'
import { GREEN, NAVY, RED, TEXT } from '@/src/shared/constants/colors'
import { GREEN, NAVY, RED, TEXT, BRAND } from '@/src/shared/constants/colors'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import { LAYOUT } from '@/src/shared/constants/spacing'
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
/**
* SwiftUI-inspired button variants:
* - borderedProminent: filled green (primary CTA)
* - bordered: tinted background with colored text
* - borderless: text-only (inline actions)
* - destructive: red text action
* - plain: minimal, no chrome
*/
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive' | 'icon'
| 'borderedProminent' | 'bordered' | 'borderless' | 'plain'
type ControlSize = 'mini' | 'small' | 'regular' | 'large' | 'extraLarge'
export interface NativeButtonProps {
variant?: ButtonVariant
@@ -13,17 +25,29 @@ export interface NativeButtonProps {
onPress?: () => void
disabled?: boolean
color?: string
controlSize?: 'mini' | 'small' | 'regular' | 'large'
controlSize?: ControlSize
fullWidth?: boolean
style?: ViewStyle
testID?: string
}
export function NativeButton(props: NativeButtonProps) {
const { variant = 'primary', title, onPress, disabled, fullWidth, style, testID } = props
const {
variant = 'primary',
title,
systemImage,
onPress,
disabled,
color,
controlSize = 'regular',
fullWidth,
style,
testID,
} = props
const buttonStyle = getVariantStyle(variant)
const textStyle = getVariantTextStyle(variant)
const sizeStyle = SIZE_STYLES[controlSize]
const buttonStyle = getVariantStyle(variant, color)
const textStyle = getVariantTextStyle(variant, color)
return (
<Pressable
@@ -31,64 +55,122 @@ export function NativeButton(props: NativeButtonProps) {
disabled={disabled}
style={({ pressed }) => [
styles.base,
sizeStyle.container,
fullWidth && styles.fullWidth,
buttonStyle,
pressed && styles.pressed,
disabled && styles.disabled,
{ borderCurve: 'continuous' } as ViewStyle,
style,
]}
testID={testID}
>
{title ? <Text style={[styles.text, textStyle]}>{title}</Text> : null}
<View style={styles.content}>
{systemImage ? (
<Image
source={`sf:${systemImage}`}
style={[styles.icon, { tintColor: textStyle.color }]}
/>
) : null}
{title ? (
<Text style={[styles.text, sizeStyle.text, textStyle]}>{title}</Text>
) : null}
</View>
</Pressable>
)
}
function getVariantStyle(variant: ButtonVariant): ViewStyle {
function getVariantStyle(variant: ButtonVariant, color?: string): ViewStyle {
const brandColor = color ?? GREEN[500]
switch (variant) {
case 'primary':
return { backgroundColor: GREEN[500] }
case 'borderedProminent':
return { backgroundColor: brandColor }
case 'secondary':
return { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: GREEN[500] }
case 'bordered':
return { backgroundColor: `${brandColor}18` } // 10% opacity tint
case 'ghost':
case 'borderless':
case 'plain':
return { backgroundColor: 'transparent' }
case 'destructive':
return { backgroundColor: 'transparent' }
case 'icon':
return { backgroundColor: 'transparent' }
}
}
function getVariantTextStyle(variant: ButtonVariant) {
function getVariantTextStyle(variant: ButtonVariant, color?: string): TextStyle {
const brandColor = color ?? GREEN[500]
switch (variant) {
case 'primary':
case 'borderedProminent':
return { color: NAVY[900] }
case 'secondary':
case 'bordered':
return { color: brandColor }
case 'destructive':
return { color: RED[500] }
case 'ghost':
case 'borderless':
return { color: brandColor }
case 'plain':
return { color: TEXT.PRIMARY }
default:
return { color: TEXT.PRIMARY }
}
}
const SIZE_STYLES: Record<ControlSize, { container: ViewStyle; text: TextStyle }> = {
mini: {
container: { height: 28, paddingHorizontal: SPACING[2], borderRadius: RADIUS.SM },
text: { fontSize: 13 },
},
small: {
container: { height: 34, paddingHorizontal: SPACING[3], borderRadius: RADIUS.SM },
text: { fontSize: 14 },
},
regular: {
container: { height: 44, paddingHorizontal: SPACING[4], borderRadius: RADIUS.MD },
text: { fontSize: 17 },
},
large: {
container: { height: 50, paddingHorizontal: SPACING[5], borderRadius: RADIUS.MD },
text: { fontSize: 17 },
},
extraLarge: {
container: { height: LAYOUT.BUTTON_HEIGHT, paddingHorizontal: SPACING[6], borderRadius: RADIUS.MD },
text: { fontSize: 17 },
},
}
const styles = StyleSheet.create({
base: {
height: LAYOUT.BUTTON_HEIGHT,
borderRadius: RADIUS.MD,
alignItems: 'center',
justifyContent: 'center',
minHeight: LAYOUT.TOUCH_TARGET,
},
content: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[2],
},
fullWidth: {
width: '100%',
},
icon: {
width: 18,
height: 18,
},
text: {
fontSize: 15,
fontWeight: '600',
textAlign: 'center',
},
pressed: {
opacity: 0.8,
opacity: 0.7,
transform: [{ scale: 0.98 }],
},
disabled: {
opacity: 0.4,
opacity: 0.35,
},
})

View File

@@ -1,33 +1,68 @@
import { StyleSheet, Text, View, Pressable } from 'react-native'
import { TEXT } from '@/src/shared/constants/colors'
import { Image } from 'expo-image'
import { BRAND, TEXT } from '@/src/shared/constants/colors'
import { SPACING } from '@/src/shared/constants/spacing'
export interface NativeLabeledRowProps {
label: string
value?: string
/** SF Symbol name for expo-image source="sf:name" */
icon?: string
/** Icon tint color */
iconColor?: string
chevron?: boolean
onPress?: () => void
/** Destructive row — label turns red */
destructive?: boolean
children?: React.ReactNode
testID?: string
}
export function NativeLabeledRow(props: NativeLabeledRowProps) {
const { label, value, chevron, onPress, children } = props
const {
label,
value,
icon,
iconColor,
chevron,
onPress,
destructive,
children,
} = props
const labelColor = destructive ? BRAND.DANGER : TEXT.PRIMARY
const content = (
<View style={styles.row}>
<Text style={styles.label}>{label}</Text>
{icon ? (
<Image
source={`sf:${icon}`}
style={[styles.icon, { tintColor: iconColor ?? TEXT.SECONDARY }]}
/>
) : null}
<Text style={[styles.label, { color: labelColor }]}>{label}</Text>
<View style={styles.right}>
{value ? <Text style={styles.value}>{value}</Text> : null}
{children}
{chevron ? <Text style={styles.chevron}>&#8250;</Text> : null}
{(chevron || onPress) ? (
<Image
source="sf:chevron.right"
style={styles.chevronIcon}
/>
) : null}
</View>
</View>
)
if (onPress) {
return <Pressable onPress={onPress}>{content}</Pressable>
return (
<Pressable
onPress={onPress}
style={({ pressed }) => pressed ? styles.pressed : undefined}
>
{content}
</Pressable>
)
}
return content
@@ -42,9 +77,14 @@ const styles = StyleSheet.create({
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[3],
},
icon: {
width: 22,
height: 22,
marginRight: SPACING[3],
},
label: {
fontSize: 17,
color: TEXT.PRIMARY,
fontWeight: '400',
flex: 1,
},
right: {
@@ -54,11 +94,16 @@ const styles = StyleSheet.create({
},
value: {
fontSize: 17,
fontWeight: '400',
color: TEXT.TERTIARY,
},
chevron: {
fontSize: 20,
color: TEXT.TERTIARY,
marginLeft: SPACING[1],
chevronIcon: {
width: 13,
height: 13,
tintColor: TEXT.TERTIARY,
opacity: 0.6,
},
pressed: {
opacity: 0.7,
},
})

View File

@@ -1,5 +1,5 @@
import { type ViewStyle, View, StyleSheet } from 'react-native'
import { NAVY } from '@/src/shared/constants/colors'
import { NAVY, BORDER_COLORS } from '@/src/shared/constants/colors'
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
@@ -8,13 +8,20 @@ export interface NativeListProps {
style?: ViewStyle
scrollEnabled?: boolean
height?: number
/** Remove default margin (for embedding inside other containers) */
inset?: boolean
testID?: string
}
export function NativeList(props: NativeListProps) {
const { children, style, height } = props
const { children, style, height, inset = true } = props
return (
<View style={[styles.list, height ? { height } : undefined, style]}>
<View style={[
styles.list,
!inset && styles.noInset,
height ? { height } : undefined,
style,
]}>
{children}
</View>
)
@@ -24,13 +31,31 @@ export function calculateListHeight(rows: number, sections: number = 1): number
return rows * LAYOUT.LIST_ROW_HEIGHT + sections * LAYOUT.LIST_HEADER_HEIGHT + 20
}
/** Thin iOS-style separator for use between list rows */
export function ListSeparator() {
return (
<View style={styles.separatorOuter}>
<View style={styles.separator} />
</View>
)
}
const styles = StyleSheet.create({
list: {
marginHorizontal: LAYOUT.SCREEN_PADDING,
marginTop: LAYOUT.SECTION_GAP,
marginBottom: SPACING[3],
backgroundColor: NAVY[800],
borderRadius: RADIUS.XL,
borderRadius: RADIUS.LG,
borderCurve: 'continuous',
overflow: 'hidden',
},
noInset: {
marginHorizontal: 0,
},
separatorOuter: {
paddingLeft: SPACING[4],
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: BORDER_COLORS.SEPARATOR,
},
})

View File

@@ -2,15 +2,15 @@ import { StyleSheet, Text, View } from 'react-native'
import { NAVY, TEXT } from '@/src/shared/constants/colors'
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import { FONT_FAMILY } from '@/src/shared/constants/typography'
export interface NativeSectionProps {
title?: string
footer?: string
children: React.ReactNode
}
export function NativeSection(props: NativeSectionProps) {
const { title, children } = props
const { title, footer, children } = props
return (
<View style={styles.container}>
{title ? (
@@ -19,6 +19,9 @@ export function NativeSection(props: NativeSectionProps) {
<View style={styles.content}>
{children}
</View>
{footer ? (
<Text style={styles.footer}>{footer}</Text>
) : null}
</View>
)
}
@@ -29,16 +32,26 @@ const styles = StyleSheet.create({
},
header: {
fontSize: 13,
fontFamily: FONT_FAMILY.SANS_SEMIBOLD,
fontWeight: '400',
color: TEXT.TERTIARY,
letterSpacing: 0.06,
letterSpacing: -0.08,
textTransform: 'uppercase',
paddingHorizontal: LAYOUT.SCREEN_PADDING,
paddingHorizontal: LAYOUT.SCREEN_PADDING + LAYOUT.CARD_PADDING,
marginBottom: SPACING[2],
},
content: {
marginHorizontal: LAYOUT.SCREEN_PADDING,
backgroundColor: NAVY[800],
borderRadius: RADIUS.XL,
borderRadius: RADIUS.LG,
overflow: 'hidden',
},
footer: {
fontSize: 13,
fontWeight: '400',
color: TEXT.TERTIARY,
letterSpacing: -0.08,
paddingHorizontal: LAYOUT.SCREEN_PADDING + LAYOUT.CARD_PADDING,
marginTop: SPACING[2],
lineHeight: 18,
},
})

View File

@@ -1,7 +1,7 @@
export { NativeButton } from './NativeButton'
export type { NativeButtonProps } from './NativeButton'
export { NativeList, calculateListHeight } from './NativeList'
export { NativeList, calculateListHeight, ListSeparator } from './NativeList'
export type { NativeListProps } from './NativeList'
export { NativeSection } from './NativeSection'

View File

@@ -1,17 +1,17 @@
/**
* TabataFit Border Radius System
* Dark Medical — clean, minimal rounding
* iOS-native continuous corners (borderCurve: 'continuous')
*/
export const RADIUS = {
NONE: 0,
XS: 2, // Progress bar, hairline element
SM: 6, // Badge, chip, tag
MD: 10, // Button, input, tip card
LG: 14, // Standard program card
XL: 18, // Large card, modal
'2XL': 22, // Icon container, medium element
'3XL': 28, // Large element
XS: 4, // Progress bar, hairline element
SM: 8, // Badge, chip, tag
MD: 12, // Button, input, tip card — iOS standard
LG: 16, // Standard card — iOS grouped inset
XL: 20, // Large card, modal, bottom sheet
'2XL': 24, // Icon container, medium element
'3XL': 32, // Large element
// Special
PILL: 9999, // Pill, toggle, progress bar

View File

@@ -1,17 +1,18 @@
/**
* TabataFit Color System
* Dark Medical — navy backgrounds, green actions, no shadows
* Dark Premium — refined navy backgrounds, green actions, native iOS feel
*/
// ═══════════════════════════════════════════════════════════════════════════
// NAVY (Backgrounds)
// NAVY (Backgrounds) — warmer, closer to iOS dark mode tones
// ═══════════════════════════════════════════════════════════════════════════
export const NAVY = {
900: '#0D1B2A', // Main app background
800: '#112240', // Surface 1 — default cards
700: '#1A3050', // Surface 2 — elevated cards
600: '#243C5E', // Active borders
900: '#0A1628', // Main app background — deep, warm navy
800: '#111D2E', // Surface 1 — default cards (grouped inset bg)
700: '#192A3E', // Surface 2 — elevated cards
600: '#223750', // Active borders, selection
500: '#2C4566', // Tertiary surface / hover state
} as const
// ═══════════════════════════════════════════════════════════════════════════
@@ -22,8 +23,8 @@ export const GREEN = {
500: '#00C896', // Primary CTA, effort timer, progress
600: '#00A67C', // Hover/pressed state
700: '#00875F', // Deep active state
DIM: 'rgba(0,200,150,0.12)', // Badge/chip/card accent background
BORDER: 'rgba(0,200,150,0.35)', // Card accent border
DIM: 'rgba(0,200,150,0.10)', // Badge/chip/card accent background
BORDER: 'rgba(0,200,150,0.30)', // Card accent border
} as const
// ═══════════════════════════════════════════════════════════════════════════
@@ -31,18 +32,19 @@ export const GREEN = {
// ═══════════════════════════════════════════════════════════════════════════
export const TEXT = {
PRIMARY: '#E6F1FF', // white-100
SECONDARY: '#A8B2D8', // slate-300
TERTIARY: '#8892B0', // slate-400
MUTED: '#8892B0',
HINT: '#8892B0',
DISABLED: '#3A3A3C',
PRIMARY: '#ECEFF4', // High-contrast primary (Nord Snow Storm inspired)
SECONDARY: '#9BA4B5', // Secondary text
TERTIARY: '#6B7A8D', // Tertiary, placeholders — lower contrast than before
MUTED: '#6B7A8D',
HINT: '#6B7A8D',
DISABLED: '#3A4555',
} as const
export const BORDER_COLORS = {
DIM: 'rgba(168,178,216,0.15)', // Default border
HOVER: 'rgba(168,178,216,0.25)', // Hover border
BRAND: 'rgba(0,200,150,0.35)', // Green border (matches GREEN.BORDER)
DIM: 'rgba(150,164,190,0.12)', // Default border — softer
HOVER: 'rgba(150,164,190,0.20)', // Hover border
BRAND: 'rgba(0,200,150,0.30)', // Green border
SEPARATOR: 'rgba(150,164,190,0.08)', // iOS-style thin separator
} as const
// ═══════════════════════════════════════════════════════════════════════════
@@ -52,7 +54,7 @@ export const BORDER_COLORS = {
export const ORANGE = {
500: '#FF8A5C',
600: '#E06A3C',
DIM: 'rgba(255,138,92,0.12)',
DIM: 'rgba(255,138,92,0.10)',
} as const
// ═══════════════════════════════════════════════════════════════════════════
@@ -60,8 +62,8 @@ export const ORANGE = {
// ═══════════════════════════════════════════════════════════════════════════
export const RED = {
500: '#FF4444', // Timer emergency <10s ONLY
DIM: 'rgba(255,68,68,0.12)', // Danger background tint
500: '#FF453A', // iOS system red — timer emergency <10s
DIM: 'rgba(255,69,58,0.12)',
} as const
// ═══════════════════════════════════════════════════════════════════════════
@@ -73,7 +75,8 @@ export const BRAND = {
SECONDARY: GREEN['600'],
DANGER: RED['500'],
SUCCESS: GREEN['500'],
INFO: '#85C7F2',
INFO: '#64D2FF', // iOS system cyan
LINK: '#0A84FF', // iOS system blue — for tappable text
} as const
// ═══════════════════════════════════════════════════════════════════════════
@@ -84,9 +87,9 @@ export const DARK = {
BASE: NAVY['900'],
SURFACE: NAVY['800'],
ELEVATED: NAVY['700'],
OVERLAY_1: 'rgba(168,178,216,0.06)',
OVERLAY_2: 'rgba(168,178,216,0.10)',
OVERLAY_3: 'rgba(168,178,216,0.15)',
OVERLAY_1: 'rgba(150,164,190,0.05)',
OVERLAY_2: 'rgba(150,164,190,0.08)',
OVERLAY_3: 'rgba(150,164,190,0.12)',
SCRIM: 'rgba(0,0,0,0.6)',
} as const
@@ -102,16 +105,16 @@ export const PHASE = {
WORK_LIGHT: GREEN.DIM,
WORK_GLOW: 'rgba(0,200,150,0.5)',
REST: '#8892B0', // slate-400 — visual rest signal
REST_LIGHT: 'rgba(136,146,176,0.2)',
REST_GLOW: 'rgba(136,146,176,0.5)',
REST: '#6B7A8D', // Muted tertiary — visual rest signal
REST_LIGHT: 'rgba(107,122,141,0.2)',
REST_GLOW: 'rgba(107,122,141,0.5)',
COMPLETE: GREEN['500'],
COMPLETE_LIGHT: GREEN.DIM,
} as const
// ═══════════════════════════════════════════════════════════════════════════
// GRADIENTS (video overlays only — no glass shimmer)
// GRADIENTS (video overlays only)
// ═══════════════════════════════════════════════════════════════════════════
export const AMBER = {
@@ -121,7 +124,7 @@ export const AMBER = {
export const GRADIENTS = {
VIDEO_OVERLAY: ['transparent', 'rgba(0,0,0,0.8)'],
VIDEO_TOP: ['rgba(0,0,0,0.5)', 'transparent'],
CARD_OVERLAY: ['transparent', 'rgba(13,27,42,0.85)'] as const,
CARD_OVERLAY: ['transparent', 'rgba(10,22,40,0.85)'] as const,
ASSESSMENT_CTA: [ORANGE[500], RED[500]] as const,
} as const

View File

@@ -1,27 +1,42 @@
/**
* TabataFit Typography System
* Three families: Serif (emotional), Sans (interface), Mono (data)
* SF Pro (system font) — premium native iOS feel
*
* Uses Apple's Dynamic Type scale with system font.
* No custom fonts loaded — SF Pro is the default on iOS.
* fontWeight controls the visual weight; no fontFamily needed for SF Pro.
*
* For data/timer: system monospaced via fontVariant: ['tabular-nums']
*/
import { TextStyle } from 'react-native'
import { Platform, TextStyle } from 'react-native'
// ═══════════════════════════════════════════════════════════════════════════
// FONT FAMILIES
// FONT FAMILIES — System font (SF Pro on iOS, Roboto on Android)
// ═══════════════════════════════════════════════════════════════════════════
/**
* On iOS, omitting fontFamily uses SF Pro automatically.
* On Android, omitting fontFamily uses Roboto.
* We use 'System' as a semantic marker — React Native resolves it to the
* platform system font.
*/
export const FONT_FAMILY = {
SERIF: 'DMSerifDisplay_Regular',
SERIF_ITALIC: 'DMSerifDisplay_Italic',
SANS: 'Outfit_400Regular',
SANS_MEDIUM: 'Outfit_500Medium',
SANS_SEMIBOLD: 'Outfit_600SemiBold',
SANS_BOLD: 'Outfit_700Bold',
MONO: 'DMMono_400Regular',
MONO_MEDIUM: 'DMMono_500Medium',
// System font — no fontFamily needed (undefined = system font in RN)
SANS: undefined,
SANS_MEDIUM: undefined,
SANS_SEMIBOLD: undefined,
SANS_BOLD: undefined,
// Serif — iOS has New York via serif fontFamily
SERIF: Platform.OS === 'ios' ? 'Georgia' : 'serif',
SERIF_ITALIC: Platform.OS === 'ios' ? 'Georgia' : 'serif',
// Monospace — system mono (SF Mono on iOS, Roboto Mono on Android)
MONO: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
MONO_MEDIUM: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
} as const
// Font alias object — maps to FONT_FAMILY values
const FONT: Record<string, string> = {
const FONT = {
SANS: FONT_FAMILY.SANS,
SANS_MEDIUM: FONT_FAMILY.SANS_MEDIUM,
SANS_SEMIBOLD: FONT_FAMILY.SANS_SEMIBOLD,
@@ -36,212 +51,217 @@ const FONT: Record<string, string> = {
SEMIBOLD: FONT_FAMILY.SANS_SEMIBOLD,
BOLD: FONT_FAMILY.SANS_BOLD,
BLACK: FONT_FAMILY.SANS_BOLD,
}
} as const
// ═══════════════════════════════════════════════════════════════════════════
// TYPE SCALE
// TYPE SCALE — matches Apple HIG Dynamic Type defaults
// ═══════════════════════════════════════════════════════════════════════════
export const TYPOGRAPHY = {
// Display / Hero — Serif for emotional moments
// Display / Hero — bold system font for emotional moments
DISPLAY: {
fontFamily: FONT.SERIF,
fontSize: 28,
lineHeight: 34,
letterSpacing: 0,
} as TextStyle,
HERO: {
fontFamily: FONT.SERIF,
fontSize: 32,
lineHeight: 38,
letterSpacing: -0.5,
} as TextStyle,
// Large Title (like iOS)
LARGE_TITLE: {
fontFamily: FONT.SERIF,
fontSize: 34,
lineHeight: 41,
letterSpacing: 0.37,
} as TextStyle,
// Heading 1 — Serif for section titles
HEADING_1: {
fontFamily: FONT.SERIF,
fontSize: 22,
lineHeight: 28,
letterSpacing: 0.35,
} as TextStyle,
// Title 1 (backward compat)
TITLE_1: {
fontFamily: FONT.SERIF,
fontSize: 28,
fontWeight: '700' as const,
lineHeight: 34,
letterSpacing: 0.36,
} as TextStyle,
// Heading 2 / Title 2 — Sans for exercise titles, program cards
HERO: {
fontFamily: FONT.SERIF,
fontSize: 34,
fontWeight: '700' as const,
lineHeight: 41,
letterSpacing: 0.37,
} as TextStyle,
// Large Title — iOS native large title
LARGE_TITLE: {
fontSize: 34,
fontWeight: '700' as const,
lineHeight: 41,
letterSpacing: 0.37,
} as TextStyle,
// Heading 1
HEADING_1: {
fontSize: 28,
fontWeight: '700' as const,
lineHeight: 34,
letterSpacing: 0.36,
} as TextStyle,
// Title 1
TITLE_1: {
fontSize: 28,
fontWeight: '400' as const,
lineHeight: 34,
letterSpacing: 0.36,
} as TextStyle,
// Heading 2 — exercise titles, program cards
HEADING_2: {
fontFamily: FONT_FAMILY.SANS_SEMIBOLD,
fontSize: 18,
lineHeight: 24,
letterSpacing: 0,
fontSize: 22,
fontWeight: '700' as const,
lineHeight: 28,
letterSpacing: 0.35,
} as TextStyle,
TITLE_2: {
fontFamily: FONT_FAMILY.SANS_BOLD,
fontSize: 22,
fontWeight: '400' as const,
lineHeight: 28,
letterSpacing: 0.35,
} as TextStyle,
TITLE_3: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 20,
fontWeight: '400' as const,
lineHeight: 25,
letterSpacing: 0.38,
} as TextStyle,
// Headline
HEADLINE: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 17,
fontWeight: '600' as const,
lineHeight: 22,
letterSpacing: -0.41,
} as TextStyle,
// Body — Sans
// Body
BODY: {
fontFamily: FONT.SANS,
fontSize: 15,
lineHeight: 20,
letterSpacing: -0.24,
fontSize: 17,
fontWeight: '400' as const,
lineHeight: 22,
letterSpacing: -0.41,
} as TextStyle,
BODY_BOLD: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 15,
lineHeight: 20,
letterSpacing: -0.24,
fontSize: 17,
fontWeight: '600' as const,
lineHeight: 22,
letterSpacing: -0.41,
} as TextStyle,
// Callout
CALLOUT: {
fontFamily: FONT.SANS,
fontSize: 16,
fontWeight: '400' as const,
lineHeight: 21,
letterSpacing: -0.32,
} as TextStyle,
// Subheadline
SUBHEADLINE: {
fontFamily: FONT.SANS,
fontSize: 15,
fontWeight: '400' as const,
lineHeight: 20,
letterSpacing: -0.24,
} as TextStyle,
// Label — Mono for tags, metadata, uppercase
// Label — small uppercase for tags, metadata
LABEL: {
fontFamily: FONT.MONO_MEDIUM,
fontSize: 11,
fontSize: 12,
fontWeight: '500' as const,
lineHeight: 16,
letterSpacing: 0.08,
letterSpacing: 0.5,
textTransform: 'uppercase' as const,
} as TextStyle,
// Footnote
FOOTNOTE: {
fontFamily: FONT.SANS,
fontSize: 13,
fontWeight: '400' as const,
lineHeight: 18,
letterSpacing: -0.08,
} as TextStyle,
// Caption
CAPTION_1: {
fontFamily: FONT.SANS,
fontSize: 12,
fontWeight: '400' as const,
lineHeight: 16,
letterSpacing: 0,
} as TextStyle,
CAPTION_2: {
fontFamily: FONT.SANS,
fontSize: 11,
fontWeight: '400' as const,
lineHeight: 13,
letterSpacing: 0.07,
} as TextStyle,
// ─────────────────────────────────────────────────────────────────────────
// SPECIAL: TIMER — Mono, readable from 2 meters
// SPECIAL: TIMER — Monospaced, readable from 2 meters
// ─────────────────────────────────────────────────────────────────────────
TIMER_NUMBER: {
fontFamily: FONT.MONO_MEDIUM,
fontFamily: FONT.MONO,
fontSize: 88,
fontWeight: '300' as const,
lineHeight: 88,
letterSpacing: -2,
fontVariant: ['tabular-nums'],
} as TextStyle,
TIMER_NUMBER_COMPACT: {
fontFamily: FONT.MONO_MEDIUM,
fontFamily: FONT.MONO,
fontSize: 72,
fontWeight: '300' as const,
lineHeight: 72,
letterSpacing: -1.5,
fontVariant: ['tabular-nums'],
} as TextStyle,
TIMER_PHASE: {
fontFamily: FONT.MONO_MEDIUM,
fontSize: 13,
fontWeight: '600' as const,
lineHeight: 18,
letterSpacing: 0.15,
letterSpacing: 1.5,
textTransform: 'uppercase' as const,
} as TextStyle,
TIMER_ROUND: {
fontFamily: FONT.MONO_MEDIUM,
fontFamily: FONT.MONO,
fontSize: 17,
fontWeight: '500' as const,
lineHeight: 22,
letterSpacing: 0,
fontVariant: ['tabular-nums'],
} as TextStyle,
EXERCISE_NAME: {
fontFamily: FONT.SANS_BOLD,
fontSize: 28,
fontWeight: '700' as const,
lineHeight: 34,
letterSpacing: 0.36,
textAlign: 'center' as const,
} as TextStyle,
// ─────────────────────────────────────────────────────────────────────────
// SPECIAL: BUTTON — Sans
// SPECIAL: BUTTON
// ─────────────────────────────────────────────────────────────────────────
BUTTON_LARGE: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 15,
lineHeight: 20,
letterSpacing: 0.5,
fontSize: 17,
fontWeight: '600' as const,
lineHeight: 22,
letterSpacing: -0.41,
} as TextStyle,
BUTTON_MEDIUM: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 15,
fontWeight: '600' as const,
lineHeight: 20,
letterSpacing: 0.3,
letterSpacing: -0.24,
} as TextStyle,
BUTTON_SMALL: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 14,
fontWeight: '600' as const,
lineHeight: 18,
letterSpacing: 0.2,
letterSpacing: -0.15,
} as TextStyle,
// ─────────────────────────────────────────────────────────────────────────
@@ -249,55 +269,56 @@ export const TYPOGRAPHY = {
// ─────────────────────────────────────────────────────────────────────────
CARD_TITLE: {
fontFamily: FONT.SANS_SEMIBOLD,
fontSize: 17,
fontWeight: '600' as const,
lineHeight: 22,
letterSpacing: -0.41,
} as TextStyle,
CARD_SUBTITLE: {
fontFamily: FONT.SANS,
fontSize: 14,
lineHeight: 18,
letterSpacing: -0.15,
fontSize: 15,
fontWeight: '400' as const,
lineHeight: 20,
letterSpacing: -0.24,
} as TextStyle,
CARD_METADATA: {
fontFamily: FONT.SANS_MEDIUM,
fontSize: 13,
lineHeight: 16,
letterSpacing: 0,
fontWeight: '500' as const,
lineHeight: 18,
letterSpacing: -0.08,
} as TextStyle,
// ─────────────────────────────────────────────────────────────────────────
// SPECIAL: STATS — Mono
// SPECIAL: STATS — Monospaced numerals
// ─────────────────────────────────────────────────────────────────────────
STAT_VALUE: {
fontFamily: FONT.MONO_MEDIUM,
fontSize: 32,
lineHeight: 38,
fontFamily: FONT.MONO,
fontSize: 34,
fontWeight: '300' as const,
lineHeight: 41,
letterSpacing: -0.5,
fontVariant: ['tabular-nums'],
} as TextStyle,
STAT_LABEL: {
fontFamily: FONT.MONO_MEDIUM,
fontSize: 11,
fontSize: 12,
fontWeight: '500' as const,
lineHeight: 16,
letterSpacing: 0.08,
letterSpacing: 0.5,
textTransform: 'uppercase' as const,
} as TextStyle,
// ─────────────────────────────────────────────────────────────────────────
// SPECIAL: OVERLINE / SECTION HEADER — Mono
// SPECIAL: OVERLINE / SECTION HEADER
// ─────────────────────────────────────────────────────────────────────────
OVERLINE: {
fontFamily: FONT.MONO_MEDIUM,
fontSize: 11,
fontSize: 12,
fontWeight: '500' as const,
lineHeight: 16,
letterSpacing: 0.08,
letterSpacing: 0.5,
textTransform: 'uppercase' as const,
} as TextStyle,

View File

@@ -1,49 +1,51 @@
/**
* Dark Medical theme palette
* Navy backgrounds, green actions, no glass, no shadows
* Dark Premium theme palette
* Refined navy backgrounds, green actions, native iOS feel
*/
import type { ThemeColors } from './types'
import { NAVY, TEXT as TEXT_COLORS, BORDER_COLORS, GREEN, ORANGE } from '../constants/colors'
export const darkColors: ThemeColors = {
bg: {
base: '#0D1B2A', // navy-900
surface: '#112240', // navy-800
elevated: '#1A3050', // navy-700
overlay1: 'rgba(168,178,216,0.06)',
overlay2: 'rgba(168,178,216,0.10)',
overlay3: 'rgba(168,178,216,0.15)',
base: NAVY[900],
surface: NAVY[800],
elevated: NAVY[700],
overlay1: 'rgba(150,164,190,0.05)',
overlay2: 'rgba(150,164,190,0.08)',
overlay3: 'rgba(150,164,190,0.12)',
scrim: 'rgba(0,0,0,0.6)',
},
text: {
primary: '#E6F1FF', // white-100
secondary: '#A8B2D8', // slate-300
tertiary: '#8892B0', // slate-400
muted: '#8892B0',
hint: '#8892B0',
disabled: '#3A3A3C',
primary: TEXT_COLORS.PRIMARY,
secondary: TEXT_COLORS.SECONDARY,
tertiary: TEXT_COLORS.TERTIARY,
muted: TEXT_COLORS.MUTED,
hint: TEXT_COLORS.HINT,
disabled: TEXT_COLORS.DISABLED,
},
surface: {
default: {
backgroundColor: '#112240',
borderColor: 'rgba(168,178,216,0.15)',
backgroundColor: NAVY[800],
borderColor: BORDER_COLORS.DIM,
borderWidth: 1,
},
accent: {
backgroundColor: 'rgba(0,200,150,0.05)',
borderColor: 'rgba(0,200,150,0.35)',
backgroundColor: GREEN.DIM,
borderColor: GREEN.BORDER,
borderWidth: 1.5,
},
tip: {
backgroundColor: 'rgba(255,138,92,0.12)',
borderColor: '#FF8A5C',
backgroundColor: ORANGE.DIM,
borderColor: ORANGE[500],
borderWidth: 1,
},
},
border: {
dim: 'rgba(168,178,216,0.15)',
hover: 'rgba(168,178,216,0.25)',
brand: 'rgba(0,200,150,0.35)',
dim: BORDER_COLORS.DIM,
hover: BORDER_COLORS.HOVER,
brand: BORDER_COLORS.BRAND,
separator: BORDER_COLORS.SEPARATOR,
},
gradients: {
videoOverlay: ['transparent', 'rgba(0,0,0,0.8)'],

View File

@@ -1,6 +1,6 @@
/**
* Theme type definitions
* Dark Medical — no glass, no shadows
* Dark Premium — refined navy, native iOS feel
*/
export interface SurfaceStyle {
@@ -36,6 +36,7 @@ export interface ThemeColors {
dim: string
hover: string
brand: string
separator: string
}
gradients: {
videoOverlay: readonly string[]