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[]