← Volver al listado de tecnologías

Capítulo 3: Primeros Componentes y Estilos

Por: Artiko
react-nativeestilosflexboxcomponentesdesign-system

Capítulo 3: Primeros Componentes y Estilos

En este capítulo crearemos un sistema de diseño profesional con componentes reutilizables, estilos consistentes y soporte para temas.

🎯 Objetivos del Capítulo

Al finalizar este capítulo serás capaz de:

🎨 Sistema de Diseño

Paso 1: Tokens de Diseño

Crea src/styles/tokens.ts:

// src/styles/tokens.ts

// Colores base
export const colors = {
  // Brand colors
  primary: {
    50: '#E3F2FD',
    100: '#BBDEFB',
    200: '#90CAF9',
    300: '#64B5F6',
    400: '#42A5F5',
    500: '#2196F3', // Main
    600: '#1E88E5',
    700: '#1976D2',
    800: '#1565C0',
    900: '#0D47A1',
  },
  
  // Semantic colors
  success: {
    50: '#E8F5E8',
    100: '#C8E6C8',
    200: '#A5D6A5',
    300: '#81C784',
    400: '#66BB6A',
    500: '#4CAF50', // Main
    600: '#43A047',
    700: '#388E3C',
    800: '#2E7D32',
    900: '#1B5E20',
  },
  
  warning: {
    50: '#FFF8E1',
    100: '#FFECB3',
    200: '#FFE082',
    300: '#FFD54F',
    400: '#FFCA28',
    500: '#FFC107', // Main
    600: '#FFB300',
    700: '#FFA000',
    800: '#FF8F00',
    900: '#FF6F00',
  },
  
  error: {
    50: '#FFEBEE',
    100: '#FFCDD2',
    200: '#EF9A9A',
    300: '#E57373',
    400: '#EF5350',
    500: '#F44336', // Main
    600: '#E53935',
    700: '#D32F2F',
    800: '#C62828',
    900: '#B71C1C',
  },
  
  // Gray scale
  gray: {
    50: '#FAFAFA',
    100: '#F5F5F5',
    200: '#EEEEEE',
    300: '#E0E0E0',
    400: '#BDBDBD',
    500: '#9E9E9E',
    600: '#757575',
    700: '#616161',
    800: '#424242',
    900: '#212121',
  },
  
  // System colors
  white: '#FFFFFF',
  black: '#000000',
  transparent: 'transparent',
} as const;

// Espaciado
export const spacing = {
  xs: 4,
  sm: 8,
  md: 16,
  lg: 24,
  xl: 32,
  '2xl': 48,
  '3xl': 64,
} as const;

// Tipografía
export const typography = {
  fontSizes: {
    xs: 12,
    sm: 14,
    base: 16,
    lg: 18,
    xl: 20,
    '2xl': 24,
    '3xl': 30,
    '4xl': 36,
    '5xl': 48,
  },
  
  fontWeights: {
    normal: '400',
    medium: '500',
    semibold: '600',
    bold: '700',
  },
  
  lineHeights: {
    tight: 1.2,
    normal: 1.5,
    relaxed: 1.75,
  },
  
  letterSpacing: {
    tight: -0.5,
    normal: 0,
    wide: 0.5,
  },
} as const;

// Bordes y radios
export const borders = {
  width: {
    none: 0,
    thin: 1,
    medium: 2,
    thick: 4,
  },
  
  radius: {
    none: 0,
    sm: 4,
    base: 8,
    lg: 12,
    xl: 16,
    full: 9999,
  },
} as const;

// Sombras
export const shadows = {
  sm: {
    shadowColor: colors.black,
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.05,
    shadowRadius: 2,
    elevation: 1,
  },
  
  base: {
    shadowColor: colors.black,
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
  },
  
  lg: {
    shadowColor: colors.black,
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.15,
    shadowRadius: 8,
    elevation: 4,
  },
  
  xl: {
    shadowColor: colors.black,
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.2,
    shadowRadius: 16,
    elevation: 8,
  },
} as const;

// Duraciones de animación
export const durations = {
  fast: 150,
  normal: 300,
  slow: 500,
} as const;

Paso 2: Temas Claro y Oscuro

Crea src/styles/themes.ts:

// src/styles/themes.ts
import { colors, spacing, typography, borders, shadows } from './tokens';

export interface Theme {
  colors: {
    primary: string;
    secondary: string;
    background: string;
    surface: string;
    card: string;
    text: {
      primary: string;
      secondary: string;
      disabled: string;
      inverse: string;
    };
    border: string;
    divider: string;
    overlay: string;
    success: string;
    warning: string;
    error: string;
  };
  spacing: typeof spacing;
  typography: typeof typography;
  borders: typeof borders;
  shadows: typeof shadows;
}

export const lightTheme: Theme = {
  colors: {
    primary: colors.primary[500],
    secondary: colors.primary[100],
    background: colors.gray[50],
    surface: colors.white,
    card: colors.white,
    text: {
      primary: colors.gray[900],
      secondary: colors.gray[600],
      disabled: colors.gray[400],
      inverse: colors.white,
    },
    border: colors.gray[200],
    divider: colors.gray[100],
    overlay: 'rgba(0, 0, 0, 0.5)',
    success: colors.success[500],
    warning: colors.warning[500],
    error: colors.error[500],
  },
  spacing,
  typography,
  borders,
  shadows,
};

export const darkTheme: Theme = {
  colors: {
    primary: colors.primary[400],
    secondary: colors.primary[800],
    background: colors.gray[900],
    surface: colors.gray[800],
    card: colors.gray[800],
    text: {
      primary: colors.white,
      secondary: colors.gray[300],
      disabled: colors.gray[500],
      inverse: colors.gray[900],
    },
    border: colors.gray[700],
    divider: colors.gray[800],
    overlay: 'rgba(0, 0, 0, 0.7)',
    success: colors.success[400],
    warning: colors.warning[400],
    error: colors.error[400],
  },
  spacing,
  typography,
  borders,
  shadows,
};

export type ThemeType = 'light' | 'dark';

Paso 3: Context de Tema

Crea src/contexts/ThemeContext.tsx:

// src/contexts/ThemeContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { useColorScheme } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Theme, ThemeType, lightTheme, darkTheme } from '@/styles/themes';

interface ThemeContextType {
  theme: Theme;
  themeType: ThemeType;
  toggleTheme: () => void;
  setTheme: (themeType: ThemeType) => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
}

const THEME_STORAGE_KEY = '@todo_app_theme';

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const systemColorScheme = useColorScheme();
  const [themeType, setThemeType] = useState<ThemeType>('light');
  const [isLoaded, setIsLoaded] = useState(false);

  // Cargar tema guardado al iniciar
  useEffect(() => {
    loadTheme();
  }, []);

  // Aplicar tema del sistema si no hay preferencia guardada
  useEffect(() => {
    if (isLoaded && !themeType) {
      setThemeType(systemColorScheme === 'dark' ? 'dark' : 'light');
    }
  }, [systemColorScheme, isLoaded, themeType]);

  const loadTheme = async () => {
    try {
      const savedTheme = await AsyncStorage.getItem(THEME_STORAGE_KEY);
      if (savedTheme) {
        setThemeType(savedTheme as ThemeType);
      } else {
        // Si no hay tema guardado, usar el del sistema
        setThemeType(systemColorScheme === 'dark' ? 'dark' : 'light');
      }
    } catch (error) {
      console.warn('Error loading theme:', error);
      setThemeType('light');
    } finally {
      setIsLoaded(true);
    }
  };

  const saveTheme = async (newTheme: ThemeType) => {
    try {
      await AsyncStorage.setItem(THEME_STORAGE_KEY, newTheme);
    } catch (error) {
      console.warn('Error saving theme:', error);
    }
  };

  const setTheme = (newThemeType: ThemeType) => {
    setThemeType(newThemeType);
    saveTheme(newThemeType);
  };

  const toggleTheme = () => {
    const newTheme = themeType === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
  };

  const theme = themeType === 'dark' ? darkTheme : lightTheme;

  const value: ThemeContextType = {
    theme,
    themeType,
    toggleTheme,
    setTheme,
  };

  if (!isLoaded) {
    return null; // O un componente de loading
  }

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = (): ThemeContextType => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

📐 Flexbox y Layouts

Paso 1: Componente Container

Crea src/components/ui/Container.tsx:

// src/components/ui/Container.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { BaseComponentProps } from '@/types/components';

interface ContainerProps extends BaseComponentProps {
  padding?: keyof typeof import('@/styles/tokens').spacing;
  margin?: keyof typeof import('@/styles/tokens').spacing;
  backgroundColor?: string;
  flex?: number;
  center?: boolean;
}

export const Container: React.FC<ContainerProps> = ({
  children,
  padding = 'md',
  margin,
  backgroundColor,
  flex,
  center = false,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const containerStyles = [
    styles.base,
    {
      padding: theme.spacing[padding],
      margin: margin ? theme.spacing[margin] : 0,
      backgroundColor: backgroundColor || 'transparent',
      flex: flex,
    },
    center && styles.center,
    style,
  ];

  return (
    <View style={containerStyles} testID={testID}>
      {children}
    </View>
  );
};

const styles = StyleSheet.create({
  base: {
    // Estilos base si son necesarios
  },
  center: {
    justifyContent: 'center',
    alignItems: 'center',
  },
});

Paso 2: Componente Row y Column

Crea src/components/ui/Layout.tsx:

// src/components/ui/Layout.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { BaseComponentProps } from '@/types/components';

interface LayoutProps extends BaseComponentProps {
  gap?: keyof typeof import('@/styles/tokens').spacing;
  padding?: keyof typeof import('@/styles/tokens').spacing;
  margin?: keyof typeof import('@/styles/tokens').spacing;
  justify?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
  align?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
  wrap?: boolean;
  flex?: number;
}

export const Row: React.FC<LayoutProps> = ({
  children,
  gap,
  padding,
  margin,
  justify = 'flex-start',
  align = 'center',
  wrap = false,
  flex,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const rowStyles = [
    styles.row,
    {
      gap: gap ? theme.spacing[gap] : 0,
      padding: padding ? theme.spacing[padding] : 0,
      margin: margin ? theme.spacing[margin] : 0,
      justifyContent: justify,
      alignItems: align,
      flexWrap: wrap ? 'wrap' : 'nowrap',
      flex: flex,
    },
    style,
  ];

  return (
    <View style={rowStyles} testID={testID}>
      {children}
    </View>
  );
};

export const Column: React.FC<LayoutProps> = ({
  children,
  gap,
  padding,
  margin,
  justify = 'flex-start',
  align = 'stretch',
  wrap = false,
  flex,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const columnStyles = [
    styles.column,
    {
      gap: gap ? theme.spacing[gap] : 0,
      padding: padding ? theme.spacing[padding] : 0,
      margin: margin ? theme.spacing[margin] : 0,
      justifyContent: justify,
      alignItems: align,
      flexWrap: wrap ? 'wrap' : 'nowrap',
      flex: flex,
    },
    style,
  ];

  return (
    <View style={columnStyles} testID={testID}>
      {children}
    </View>
  );
};

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',
  },
  column: {
    flexDirection: 'column',
  },
});

Paso 3: Componente Spacer

Crea src/components/ui/Spacer.tsx:

// src/components/ui/Spacer.tsx
import React from 'react';
import { View } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';

interface SpacerProps {
  size?: keyof typeof import('@/styles/tokens').spacing;
  horizontal?: boolean;
  flex?: number;
}

export const Spacer: React.FC<SpacerProps> = ({
  size = 'md',
  horizontal = false,
  flex,
}) => {
  const { theme } = useTheme();

  const spacerStyle = {
    [horizontal ? 'width' : 'height']: flex ? undefined : theme.spacing[size],
    flex: flex,
  };

  return <View style={spacerStyle} />;
};

🎨 Componentes de UI

Paso 1: Componente Card

Crea src/components/ui/Card.tsx:

// src/components/ui/Card.tsx
import React from 'react';
import { View, StyleSheet, Pressable } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { BaseComponentProps } from '@/types/components';

interface CardProps extends BaseComponentProps {
  padding?: keyof typeof import('@/styles/tokens').spacing;
  margin?: keyof typeof import('@/styles/tokens').spacing;
  shadow?: keyof typeof import('@/styles/tokens').shadows;
  radius?: keyof typeof import('@/styles/tokens').borders.radius;
  onPress?: () => void;
  disabled?: boolean;
}

export const Card: React.FC<CardProps> = ({
  children,
  padding = 'md',
  margin,
  shadow = 'base',
  radius = 'base',
  onPress,
  disabled = false,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const cardStyles = [
    styles.base,
    {
      backgroundColor: theme.colors.card,
      padding: theme.spacing[padding],
      margin: margin ? theme.spacing[margin] : 0,
      borderRadius: theme.borders.radius[radius],
      borderWidth: theme.borders.width.thin,
      borderColor: theme.colors.border,
    },
    theme.shadows[shadow],
    disabled && styles.disabled,
    style,
  ];

  if (onPress) {
    return (
      <Pressable
        style={({ pressed }) => [
          ...cardStyles,
          pressed && !disabled && styles.pressed,
        ]}
        onPress={onPress}
        disabled={disabled}
        testID={testID}
      >
        {children}
      </Pressable>
    );
  }

  return (
    <View style={cardStyles} testID={testID}>
      {children}
    </View>
  );
};

const styles = StyleSheet.create({
  base: {
    // Estilos base
  },
  disabled: {
    opacity: 0.5,
  },
  pressed: {
    opacity: 0.8,
    transform: [{ scale: 0.98 }],
  },
});

Paso 2: Componente Typography

Crea src/components/ui/Typography.tsx:

// src/components/ui/Typography.tsx
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { BaseComponentProps } from '@/types/components';

interface TypographyProps extends BaseComponentProps {
  variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'body1' | 'body2' | 'caption' | 'overline';
  color?: 'primary' | 'secondary' | 'disabled' | 'inverse' | string;
  weight?: keyof typeof import('@/styles/tokens').typography.fontWeights;
  align?: 'left' | 'center' | 'right' | 'justify';
  numberOfLines?: number;
  selectable?: boolean;
}

export const Typography: React.FC<TypographyProps> = ({
  children,
  variant = 'body1',
  color = 'primary',
  weight,
  align = 'left',
  numberOfLines,
  selectable = false,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const getVariantStyles = () => {
    switch (variant) {
      case 'h1':
        return {
          fontSize: theme.typography.fontSizes['4xl'],
          fontWeight: theme.typography.fontWeights.bold,
          lineHeight: theme.typography.fontSizes['4xl'] * theme.typography.lineHeights.tight,
        };
      case 'h2':
        return {
          fontSize: theme.typography.fontSizes['3xl'],
          fontWeight: theme.typography.fontWeights.bold,
          lineHeight: theme.typography.fontSizes['3xl'] * theme.typography.lineHeights.tight,
        };
      case 'h3':
        return {
          fontSize: theme.typography.fontSizes['2xl'],
          fontWeight: theme.typography.fontWeights.semibold,
          lineHeight: theme.typography.fontSizes['2xl'] * theme.typography.lineHeights.normal,
        };
      case 'h4':
        return {
          fontSize: theme.typography.fontSizes.xl,
          fontWeight: theme.typography.fontWeights.semibold,
          lineHeight: theme.typography.fontSizes.xl * theme.typography.lineHeights.normal,
        };
      case 'body1':
        return {
          fontSize: theme.typography.fontSizes.base,
          fontWeight: theme.typography.fontWeights.normal,
          lineHeight: theme.typography.fontSizes.base * theme.typography.lineHeights.normal,
        };
      case 'body2':
        return {
          fontSize: theme.typography.fontSizes.sm,
          fontWeight: theme.typography.fontWeights.normal,
          lineHeight: theme.typography.fontSizes.sm * theme.typography.lineHeights.normal,
        };
      case 'caption':
        return {
          fontSize: theme.typography.fontSizes.xs,
          fontWeight: theme.typography.fontWeights.normal,
          lineHeight: theme.typography.fontSizes.xs * theme.typography.lineHeights.normal,
        };
      case 'overline':
        return {
          fontSize: theme.typography.fontSizes.xs,
          fontWeight: theme.typography.fontWeights.medium,
          lineHeight: theme.typography.fontSizes.xs * theme.typography.lineHeights.tight,
          letterSpacing: theme.typography.letterSpacing.wide,
          textTransform: 'uppercase' as const,
        };
      default:
        return {};
    }
  };

  const getTextColor = () => {
    switch (color) {
      case 'primary':
        return theme.colors.text.primary;
      case 'secondary':
        return theme.colors.text.secondary;
      case 'disabled':
        return theme.colors.text.disabled;
      case 'inverse':
        return theme.colors.text.inverse;
      default:
        return color;
    }
  };

  const textStyles = [
    styles.base,
    getVariantStyles(),
    {
      color: getTextColor(),
      fontWeight: weight ? theme.typography.fontWeights[weight] : undefined,
      textAlign: align,
    },
    style,
  ];

  return (
    <Text
      style={textStyles}
      numberOfLines={numberOfLines}
      selectable={selectable}
      testID={testID}
    >
      {children}
    </Text>
  );
};

const styles = StyleSheet.create({
  base: {
    // Estilos base si son necesarios
  },
});

Paso 3: Componente Avatar

Crea src/components/ui/Avatar.tsx:

// src/components/ui/Avatar.tsx
import React from 'react';
import { View, Image, StyleSheet } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { Typography } from './Typography';
import { BaseComponentProps } from '@/types/components';

interface AvatarProps extends BaseComponentProps {
  size?: 'sm' | 'md' | 'lg' | 'xl';
  source?: { uri: string } | number;
  name?: string;
  backgroundColor?: string;
  textColor?: string;
  onPress?: () => void;
}

export const Avatar: React.FC<AvatarProps> = ({
  size = 'md',
  source,
  name,
  backgroundColor,
  textColor,
  onPress,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const getSizeValue = () => {
    switch (size) {
      case 'sm': return 32;
      case 'md': return 40;
      case 'lg': return 56;
      case 'xl': return 80;
      default: return 40;
    }
  };

  const sizeValue = getSizeValue();

  const getInitials = () => {
    if (!name) return '?';
    const words = name.trim().split(' ');
    if (words.length === 1) {
      return words[0].charAt(0).toUpperCase();
    }
    return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase();
  };

  const avatarStyles = [
    styles.base,
    {
      width: sizeValue,
      height: sizeValue,
      borderRadius: sizeValue / 2,
      backgroundColor: backgroundColor || theme.colors.primary,
    },
    style,
  ];

  const getFontSize = () => {
    switch (size) {
      case 'sm': return theme.typography.fontSizes.xs;
      case 'md': return theme.typography.fontSizes.sm;
      case 'lg': return theme.typography.fontSizes.base;
      case 'xl': return theme.typography.fontSizes.lg;
      default: return theme.typography.fontSizes.sm;
    }
  };

  if (source) {
    return (
      <Image
        source={source}
        style={avatarStyles}
        testID={testID}
      />
    );
  }

  return (
    <View style={avatarStyles} testID={testID}>
      <Typography
        variant="body1"
        color={textColor || theme.colors.text.inverse}
        weight="medium"
        style={{ fontSize: getFontSize() }}
      >
        {getInitials()}
      </Typography>
    </View>
  );
};

const styles = StyleSheet.create({
  base: {
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  },
});

🔘 Componente Badge

Crea src/components/ui/Badge.tsx:

// src/components/ui/Badge.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { Typography } from './Typography';
import { BaseComponentProps } from '@/types/components';

interface BadgeProps extends BaseComponentProps {
  variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error';
  size?: 'sm' | 'md' | 'lg';
  text?: string;
  count?: number;
  maxCount?: number;
  dot?: boolean;
}

export const Badge: React.FC<BadgeProps> = ({
  variant = 'primary',
  size = 'md',
  text,
  count,
  maxCount = 99,
  dot = false,
  style,
  testID,
}) => {
  const { theme } = useTheme();

  const getVariantColors = () => {
    switch (variant) {
      case 'primary':
        return {
          backgroundColor: theme.colors.primary,
          textColor: theme.colors.text.inverse,
        };
      case 'secondary':
        return {
          backgroundColor: theme.colors.text.secondary,
          textColor: theme.colors.text.inverse,
        };
      case 'success':
        return {
          backgroundColor: theme.colors.success,
          textColor: theme.colors.text.inverse,
        };
      case 'warning':
        return {
          backgroundColor: theme.colors.warning,
          textColor: theme.colors.text.inverse,
        };
      case 'error':
        return {
          backgroundColor: theme.colors.error,
          textColor: theme.colors.text.inverse,
        };
      default:
        return {
          backgroundColor: theme.colors.primary,
          textColor: theme.colors.text.inverse,
        };
    }
  };

  const getSizeStyles = () => {
    if (dot) {
      switch (size) {
        case 'sm': return { width: 6, height: 6 };
        case 'md': return { width: 8, height: 8 };
        case 'lg': return { width: 10, height: 10 };
        default: return { width: 8, height: 8 };
      }
    }

    switch (size) {
      case 'sm':
        return {
          minWidth: 16,
          height: 16,
          paddingHorizontal: 4,
          fontSize: theme.typography.fontSizes.xs,
        };
      case 'md':
        return {
          minWidth: 20,
          height: 20,
          paddingHorizontal: 6,
          fontSize: theme.typography.fontSizes.xs,
        };
      case 'lg':
        return {
          minWidth: 24,
          height: 24,
          paddingHorizontal: 8,
          fontSize: theme.typography.fontSizes.sm,
        };
      default:
        return {
          minWidth: 20,
          height: 20,
          paddingHorizontal: 6,
          fontSize: theme.typography.fontSizes.xs,
        };
    }
  };

  const colors = getVariantColors();
  const sizeStyles = getSizeStyles();

  const badgeStyles = [
    styles.base,
    {
      backgroundColor: colors.backgroundColor,
      ...sizeStyles,
    },
    dot && styles.dot,
    style,
  ];

  const getDisplayText = () => {
    if (text) return text;
    if (count !== undefined) {
      return count > maxCount ? `${maxCount}+` : count.toString();
    }
    return '';
  };

  const displayText = getDisplayText();

  return (
    <View style={badgeStyles} testID={testID}>
      {!dot && displayText && (
        <Typography
          variant="caption"
          color={colors.textColor}
          weight="medium"
          style={{ fontSize: sizeStyles.fontSize }}
        >
          {displayText}
        </Typography>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  base: {
    borderRadius: 12,
    justifyContent: 'center',
    alignItems: 'center',
  },
  dot: {
    borderRadius: 5,
  },
});

🎯 Ejemplo de Uso Completo

Pantalla de Ejemplo

Crea src/screens/ComponentsShowcaseScreen.tsx:

// src/screens/ComponentsShowcaseScreen.tsx
import React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { Container } from '@/components/ui/Container';
import { Row, Column } from '@/components/ui/Layout';
import { Card } from '@/components/ui/Card';
import { Typography } from '@/components/ui/Typography';
import { Button } from '@/components/ui/Button';
import { Avatar } from '@/components/ui/Avatar';
import { Badge } from '@/components/ui/Badge';
import { Spacer } from '@/components/ui/Spacer';

export const ComponentsShowcaseScreen: React.FC = () => {
  const { theme, toggleTheme, themeType } = useTheme();

  return (
    <ScrollView style={[styles.container, { backgroundColor: theme.colors.background }]}>
      <Container padding="lg">
        <Column gap="lg">
          {/* Header */}
          <Row justify="space-between" align="center">
            <Typography variant="h2">Componentes UI</Typography>
            <Button
              title={`Tema ${themeType === 'light' ? 'Oscuro' : 'Claro'}`}
              variant="outline"
              size="small"
              onPress={toggleTheme}
            />
          </Row>

          {/* Typography Section */}
          <Card>
            <Column gap="md">
              <Typography variant="h3">Tipografía</Typography>
              <Typography variant="h1">Heading 1</Typography>
              <Typography variant="h2">Heading 2</Typography>
              <Typography variant="h3">Heading 3</Typography>
              <Typography variant="h4">Heading 4</Typography>
              <Typography variant="body1">
                Este es un texto de cuerpo normal que muestra cómo se ve el texto principal.
              </Typography>
              <Typography variant="body2" color="secondary">
                Este es un texto secundario más pequeño.
              </Typography>
              <Typography variant="caption" color="disabled">
                Este es un texto de caption.
              </Typography>
              <Typography variant="overline">Overline Text</Typography>
            </Column>
          </Card>

          {/* Buttons Section */}
          <Card>
            <Column gap="md">
              <Typography variant="h3">Botones</Typography>
              <Column gap="sm">
                <Button title="Primary Button" variant="primary" />
                <Button title="Secondary Button" variant="secondary" />
                <Button title="Outline Button" variant="outline" />
                <Button title="Ghost Button" variant="ghost" />
              </Column>
              
              <Row gap="sm">
                <Button title="Small" variant="primary" size="small" />
                <Button title="Medium" variant="primary" size="medium" />
                <Button title="Large" variant="primary" size="large" />
              </Row>

              <Button 
                title="Loading Button" 
                variant="primary" 
                loading 
              />
              <Button 
                title="Disabled Button" 
                variant="primary" 
                disabled 
              />
            </Column>
          </Card>

          {/* Avatars Section */}
          <Card>
            <Column gap="md">
              <Typography variant="h3">Avatars</Typography>
              <Row gap="md" align="center">
                <Avatar size="sm" name="John Doe" />
                <Avatar size="md" name="Jane Smith" />
                <Avatar size="lg" name="Bob Johnson" />
                <Avatar size="xl" name="Alice Brown" />
              </Row>
            </Column>
          </Card>

          {/* Badges Section */}
          <Card>
            <Column gap="md">
              <Typography variant="h3">Badges</Typography>
              <Row gap="md" align="center" wrap>
                <Badge text="Primary" variant="primary" />
                <Badge text="Success" variant="success" />
                <Badge text="Warning" variant="warning" />
                <Badge text="Error" variant="error" />
              </Row>
              
              <Row gap="md" align="center">
                <Badge count={5} variant="primary" />
                <Badge count={99} variant="error" />
                <Badge count={100} variant="success" maxCount={99} />
              </Row>

              <Row gap="md" align="center">
                <Badge dot variant="primary" size="sm" />
                <Badge dot variant="error" size="md" />
                <Badge dot variant="success" size="lg" />
              </Row>
            </Column>
          </Card>

          {/* Layout Section */}
          <Card>
            <Column gap="md">
              <Typography variant="h3">Layouts</Typography>
              
              <Typography variant="h4">Row Layout</Typography>
              <Row gap="sm" justify="space-between">
                <Card padding="sm" style={{ backgroundColor: theme.colors.primary }}>
                  <Typography color="inverse">Item 1</Typography>
                </Card>
                <Card padding="sm" style={{ backgroundColor: theme.colors.success }}>
                  <Typography color="inverse">Item 2</Typography>
                </Card>
                <Card padding="sm" style={{ backgroundColor: theme.colors.warning }}>
                  <Typography color="inverse">Item 3</Typography>
                </Card>
              </Row>

              <Typography variant="h4">Column Layout</Typography>
              <Column gap="sm">
                <Card padding="sm" style={{ backgroundColor: theme.colors.primary }}>
                  <Typography color="inverse">Item A</Typography>
                </Card>
                <Card padding="sm" style={{ backgroundColor: theme.colors.success }}>
                  <Typography color="inverse">Item B</Typography>
                </Card>
                <Card padding="sm" style={{ backgroundColor: theme.colors.warning }}>
                  <Typography color="inverse">Item C</Typography>
                </Card>
              </Column>
            </Column>
          </Card>

          {/* User Card Example */}
          <Card onPress={() => console.log('Card pressed!')}>
            <Row gap="md" align="center">
              <Avatar name="John Doe" size="lg" />
              <Column flex={1}>
                <Row justify="space-between" align="center">
                  <Typography variant="h4">John Doe</Typography>
                  <Badge count={3} variant="primary" />
                </Row>
                <Typography variant="body2" color="secondary">
                  Software Developer
                </Typography>
                <Spacer size="xs" />
                <Typography variant="caption" color="disabled">
                  Activo hace 2 horas
                </Typography>
              </Column>
            </Row>
          </Card>

        </Column>
      </Container>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

📱 Instalación de Dependencias

Instala las dependencias necesarias:

# AsyncStorage para persistir el tema
npm install @react-native-async-storage/async-storage

# Si usas Expo
npx expo install @react-native-async-storage/async-storage

✅ Resumen del Capítulo

En este capítulo hemos creado:

  1. Sistema de tokens de diseño con colores, espaciado, tipografía y sombras
  2. Temas claro y oscuro con context para cambiar entre ellos
  3. Componentes de layout (Container, Row, Column, Spacer)
  4. Componentes de UI (Card, Typography, Avatar, Badge)
  5. Sistema de estilos consistente y reutilizable
  6. Ejemplo completo mostrando todos los componentes

🎯 Siguiente Capítulo

En el Capítulo 4: Configuración de Zustand, aprenderemos:

¡Continúa para dominar la gestión de estado en React Native! 🚀