diff --git a/src/shared/components/CollectionCard.tsx b/src/shared/components/CollectionCard.tsx
deleted file mode 100644
index 48ef0a7..0000000
--- a/src/shared/components/CollectionCard.tsx
+++ /dev/null
@@ -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 (
-
- {/* Background Image or Solid Color */}
- {imageUrl ? (
-
-
-
- ) : (
-
- )}
-
- {/* Content */}
-
-
- {collection.icon}
-
-
-
- {collection.title}
-
-
- {variant === 'hero' && (
-
- {collection.description}
-
- )}
-
-
- {countLabel}
-
-
-
- )
-}
-
-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],
- },
- })
-}
diff --git a/src/shared/components/OnboardingStep.tsx b/src/shared/components/OnboardingStep.tsx
index 420a5f6..99d4bed 100644
--- a/src/shared/components/OnboardingStep.tsx
+++ b/src/shared/components/OnboardingStep.tsx
@@ -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 (
-
- {/* Progress bar */}
-
-
+
+ {/* Segmented progress bar */}
+
+ {progressAnims.map((anim, i) => (
+
+
+
+ ))}
{/* Back button — visible on steps 2+ */}
- {onBack && step > 1 && (
+ {onBack && step > 1 ? (
-
+
+ ) : (
+
)}
{/* 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],
},
})
}
diff --git a/src/shared/components/WorkoutCard.tsx b/src/shared/components/WorkoutCard.tsx
deleted file mode 100644
index 6f76aa6..0000000
--- a/src/shared/components/WorkoutCard.tsx
+++ /dev/null
@@ -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 = {
- 'full-body': BRAND.PRIMARY,
- 'core': BRAND.INFO,
- 'upper-body': TEXT.SECONDARY,
- 'lower-body': BRAND.PRIMARY,
- 'cardio': PHASE.PREP,
-}
-
-const CATEGORY_LABELS: Record = {
- '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 (
-
- {/* Background Image */}
-
- {/* Dark overlay for text readability */}
-
-
-
- {/* Category Badge — flat navy */}
-
-
- {CATEGORY_LABELS[workout.category]}
-
-
-
- {/* Play Button — flat navy circle */}
-
-
-
-
-
-
- {/* Content at Bottom */}
-
-
- {displayTitle}
-
-
- {displayMetadata}
-
-
-
- )
-}
-
-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],
- },
- })
-}
diff --git a/src/shared/components/native/NativeButton.tsx b/src/shared/components/native/NativeButton.tsx
index a4192fc..316059d 100644
--- a/src/shared/components/native/NativeButton.tsx
+++ b/src/shared/components/native/NativeButton.tsx
@@ -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 (
[
styles.base,
+ sizeStyle.container,
fullWidth && styles.fullWidth,
buttonStyle,
pressed && styles.pressed,
disabled && styles.disabled,
+ { borderCurve: 'continuous' } as ViewStyle,
style,
]}
testID={testID}
>
- {title ? {title} : null}
+
+ {systemImage ? (
+
+ ) : null}
+ {title ? (
+ {title}
+ ) : null}
+
)
}
-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 = {
+ 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,
},
})
diff --git a/src/shared/components/native/NativeLabeledRow.tsx b/src/shared/components/native/NativeLabeledRow.tsx
index 7e9268a..7661583 100644
--- a/src/shared/components/native/NativeLabeledRow.tsx
+++ b/src/shared/components/native/NativeLabeledRow.tsx
@@ -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 = (
- {label}
+ {icon ? (
+
+ ) : null}
+ {label}
{value ? {value} : null}
{children}
- {chevron ? › : null}
+ {(chevron || onPress) ? (
+
+ ) : null}
)
if (onPress) {
- return {content}
+ return (
+ pressed ? styles.pressed : undefined}
+ >
+ {content}
+
+ )
}
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,
},
})
diff --git a/src/shared/components/native/NativeList.tsx b/src/shared/components/native/NativeList.tsx
index 90c8b5c..da5f4ac 100644
--- a/src/shared/components/native/NativeList.tsx
+++ b/src/shared/components/native/NativeList.tsx
@@ -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 (
-
+
{children}
)
@@ -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 (
+
+
+
+ )
+}
+
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,
+ },
})
diff --git a/src/shared/components/native/NativeSection.tsx b/src/shared/components/native/NativeSection.tsx
index c3fbaf7..5c8a84a 100644
--- a/src/shared/components/native/NativeSection.tsx
+++ b/src/shared/components/native/NativeSection.tsx
@@ -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 (
{title ? (
@@ -19,6 +19,9 @@ export function NativeSection(props: NativeSectionProps) {
{children}
+ {footer ? (
+ {footer}
+ ) : null}
)
}
@@ -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,
+ },
})
diff --git a/src/shared/components/native/index.ts b/src/shared/components/native/index.ts
index f6ed387..233d45a 100644
--- a/src/shared/components/native/index.ts
+++ b/src/shared/components/native/index.ts
@@ -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'
diff --git a/src/shared/constants/borderRadius.ts b/src/shared/constants/borderRadius.ts
index 8371042..fb0e606 100644
--- a/src/shared/constants/borderRadius.ts
+++ b/src/shared/constants/borderRadius.ts
@@ -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
diff --git a/src/shared/constants/colors.ts b/src/shared/constants/colors.ts
index 9025f44..471254c 100644
--- a/src/shared/constants/colors.ts
+++ b/src/shared/constants/colors.ts
@@ -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
diff --git a/src/shared/constants/typography.ts b/src/shared/constants/typography.ts
index 8697644..724d249 100644
--- a/src/shared/constants/typography.ts
+++ b/src/shared/constants/typography.ts
@@ -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 = {
+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 = {
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,
diff --git a/src/shared/theme/colors.dark.ts b/src/shared/theme/colors.dark.ts
index 850a59b..46e78a8 100644
--- a/src/shared/theme/colors.dark.ts
+++ b/src/shared/theme/colors.dark.ts
@@ -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)'],
diff --git a/src/shared/theme/types.ts b/src/shared/theme/types.ts
index 8a03acf..fc8ba55 100644
--- a/src/shared/theme/types.ts
+++ b/src/shared/theme/types.ts
@@ -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[]