Componentes React Native — Cultiva Hábitos

Código TypeScript listo para producción basado en los patrones de la skill diseno-react-native: StyleSheet tipado, animaciones Reanimated 3 en el hilo UI y navegación con tipos seguros.

HabitCard — Componente Animado Reanimated 3 StyleSheet

// components/HabitCard.tsx
import React from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  interpolateColor,
} from 'react-native-reanimated';

interface HabitCardProps {
  name: string;
  duration: string;
  icon: string;
  iconBg: string;
  status: 'done' | 'pending' | 'progress';
  progress?: number;  // 0-1 para status 'progress'
  onPress: () => void;
}

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

export function HabitCard({
  name, duration, icon, iconBg, status, progress = 0, onPress
}: HabitCardProps) {
  const scale = useSharedValue(1);
  const completed = useSharedValue(status === 'done' ? 1 : 0);

  const cardStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
    backgroundColor: interpolateColor(
      completed.value,
      [0, 1],
      ['#ffffff', '#f0fdf4']
    ),
  }));

  const handleComplete = () => {
    completed.value = withSpring(1, { damping: 15, stiffness: 150 });
    onPress();
  };

  return (
    <AnimatedPressable
      style={[styles.card, cardStyle]}
      onPressIn={() => { scale.value = withSpring(0.97); }}
      onPressOut={() => { scale.value = withSpring(1); }}
      onPress={handleComplete}
    >
      <View style={[styles.iconWrap, { backgroundColor: iconBg }]}>
        <Text style={styles.iconText}>{icon}</Text>
      </View>
      <View style={styles.info}>
        <Text style={styles.name}>{name}</Text>
        <Text style={styles.meta}>{duration}</Text>
        {status === 'progress' && (
          <View style={styles.progressBar}>
            <Animated.View
              style={[styles.progressFill, { width: `${progress * 100}%` }]}
            />
          </View>
        )}
      </View>
      <CheckCircle status={status} />
    </AnimatedPressable>
  );
}

const styles = StyleSheet.create({
  card: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
    padding: 14,
    borderRadius: 16,
    borderWidth: 1.5,
    borderColor: '#e2e8f0',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.06,
    shadowRadius: 4,
    elevation: 2,
  },
  iconWrap: { width: 40, height: 40, borderRadius: 12,
    alignItems: 'center', justifyContent: 'center' },
  iconText: { fontSize: 20 },
  info: { flex: 1, gap: 2 },
  name: { fontSize: 15, fontWeight: '600', color: '#1e293b' },
  meta: { fontSize: 12, color: '#64748b' },
  progressBar: { height: 4, backgroundColor: '#e2e8f0',
    borderRadius: 99, marginTop: 6, overflow: 'hidden' },
  progressFill: { height: '100%', backgroundColor: '#8b5cf6', borderRadius: 99 },
});

ProgressRing — Animación SVG con Reanimated 3 worklet

// components/ProgressRing.tsx
import Animated, {
  useSharedValue,
  useAnimatedProps,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import Svg, { Circle } from 'react-native-svg';
import { useEffect } from 'react';

const AnimatedCircle = Animated.createAnimatedComponent(Circle);

const RADIUS = 28;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS; // 175.93

interface ProgressRingProps {
  progress: number; // 0-1
  size?: number;
  color?: string;
  trackColor?: string;
  strokeWidth?: number;
}

export function ProgressRing({
  progress, size = 72,
  color = '#22c55e',
  trackColor = '#e2e8f0',
  strokeWidth = 8
}: ProgressRingProps) {
  const animProgress = useSharedValue(0);

  useEffect(() => {
    animProgress.value = withTiming(progress, {
      duration: 1200,
      easing: Easing.out(Easing.cubic),
    });
  }, [progress]);

  const animatedProps = useAnimatedProps(() => ({
    strokeDashoffset: CIRCUMFERENCE * (1 - animProgress.value),
  }));

  return (
    <Svg width={size} height={size}
      viewBox={`0 0 ${size} ${size}`}
      style={{ transform: [{ rotate: '-90deg' }] }}>
      <Circle
        cx={size / 2} cy={size / 2} r={RADIUS}
        fill="none"
        stroke={trackColor}
        strokeWidth={strokeWidth}
      />
      <AnimatedCircle
        cx={size / 2} cy={size / 2} r={RADIUS}
        fill="none"
        stroke={color}
        strokeWidth={strokeWidth}
        strokeDasharray={CIRCUMFERENCE}
        animatedProps={animatedProps}
        strokeLinecap="round"
      />
    </Svg>
  );
}

Navegación tipada — Bottom Tabs + Stack React Navigation 6 TypeScript

// navigation/types.ts
export type RootTabParamList = {
  Home: undefined;
  Explore: undefined;
  Stats: undefined;
  Profile: undefined;
};

export type HomeStackParamList = {
  HomeScreen: undefined;
  HabitDetail: { habitId: string; habitName: string };
  AddHabit: undefined;
};

// navigation/AppNavigator.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Tab = createBottomTabNavigator<RootTabParamList>();
const HomeStack = createNativeStackNavigator<HomeStackParamList>();

function HomeStackNavigator() {
  return (
    <HomeStack.Navigator
      screenOptions={{
        headerTintColor: '#22c55e',
        headerTitleStyle: { fontWeight: '700' },
        animation: 'ios_from_right',
      }}>
      <HomeStack.Screen name="HomeScreen" component={HomeScreen} />
      <HomeStack.Screen
        name="HabitDetail"
        component={HabitDetailScreen}
        options={({ route }) => ({ title: route.params.habitName })}
      />
      <HomeStack.Screen name="AddHabit" component={AddHabitScreen}
        options={{ presentation: 'modal' }} />
    </HomeStack.Navigator>
  );
}

export function AppNavigator() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color }) => {
            const icons: Record<keyof RootTabParamList, string> = {
              Home: focused ? '🏠' : '🏡',
              Explore: '🔍',
              Stats: '📊',
              Profile: '👤',
            };
            return <Text>{icons[route.name]}</Text>;
          },
          tabBarActiveTintColor: '#22c55e',
          tabBarInactiveTintColor: '#94a3b8',
          tabBarStyle: {
            borderTopColor: '#f1f5f9',
            paddingTop: 6, paddingBottom: 10,
          },
          headerShown: false,
        })}>>
        <Tab.Screen name="Home" component={HomeStackNavigator} />
        <Tab.Screen name="Explore" component={ExploreScreen} />
        <Tab.Screen name="Stats" component={StatsScreen} />
        <Tab.Screen name="Profile" component={ProfileScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Sistema de Tema — Light / Dark Context API

// theme/cultivaTheme.ts
export const cultivaLight = {
  colors: {
    primary:       '#22c55e',
    primaryLight:  '#dcfce7',
    secondary:     '#8b5cf6',
    background:    '#fafafa',
    surface:       '#ffffff',
    text:          '#1e293b',
    textSecondary: '#64748b',
    border:        '#e2e8f0',
    success:       '#22c55e',
    warning:       '#f59e0b',
    danger:        '#ef4444',
  },
  spacing: {
    xs: 4, sm: 8, md: 16,
    lg: 24, xl: 32, xxl: 48,
  },
  borderRadius: {
    sm: 8, md: 12, lg: 16,
    xl: 24, full: 9999,
  },
  typography: {
    h1: { fontSize: 32, fontWeight: '800', lineHeight: 40 },
    h2: { fontSize: 24, fontWeight: '700', lineHeight: 32 },
    h3: { fontSize: 20, fontWeight: '600', lineHeight: 28 },
    body: { fontSize: 16, fontWeight: '400', lineHeight: 24 },
    small: { fontSize: 13, fontWeight: '400', lineHeight: 18 },
    label: { fontSize: 12, fontWeight: '600', lineHeight: 16 },
  },
} as const;

export const cultivaDark = {
  ...cultivaLight,
  colors: {
    ...cultivaLight.colors,
    background:    '#0f172a',
    surface:       '#1e293b',
    text:          '#f1f5f9',
    textSecondary: '#94a3b8',
    border:        '#334155',
    primary:       '#4ade80',
    secondary:     '#a78bfa',
  },
} as const;

export type CultivaTheme = typeof cultivaLight;

Buenas Prácticas — Rendimiento y UX

Worklets en hilo UI

Usa useAnimatedStyle y runOnUI para mantener 60fps en animaciones sin bloquear el JS thread.

📋

FlatList siempre

Nunca ScrollView + .map() para listas largas. Usa FlatList con keyExtractor y getItemLayout.

🔷

TypeScript estricto

Define ParamList para cada navigator y usa NativeStackScreenProps en cada pantalla para evitar errores en runtime.

📱

Safe Area siempre

Wrap con useSafeAreaInsets() en pantallas con contenido cercano a notch o home indicator.

🎨

StyleSheet.create

Nunca inline styles en renders repetidos. StyleSheet.create optimiza la serialización al native layer.

🌙

Dark mode nativo

Usa useColorScheme() en el ThemeProvider para cambio automático. Prueba en simulador con Appearance.setColorScheme.

Componentes del sistema — Cultiva Hábitos

Componente Categoría Técnica Estado
HabitCard Lista Reanimated 3 Listo
ProgressRing Feedback SVG + worklet Listo
BottomTabNavigator Navegación React Navigation 6 Listo
HomeStackNavigator Navegación NativeStack Listo
ThemeProvider Sistema Context API Listo
Button Primitivo Spring press Listo
Input Formulario StyleSheet Listo
DraggableCard Gesture Pan Gesture Próximamente