← Volver al listado de tecnologías
Capítulo 3: Primeros Componentes y Estilos
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:
- ✅ Crear un sistema de diseño escalable
- ✅ Dominar Flexbox para layouts responsivos
- ✅ Implementar componentes de UI reutilizables
- ✅ Configurar temas claro y oscuro
- ✅ Usar variables de diseño consistentes
🎨 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:
- Sistema de tokens de diseño con colores, espaciado, tipografía y sombras
- Temas claro y oscuro con context para cambiar entre ellos
- Componentes de layout (Container, Row, Column, Spacer)
- Componentes de UI (Card, Typography, Avatar, Badge)
- Sistema de estilos consistente y reutilizable
- Ejemplo completo mostrando todos los componentes
🎯 Siguiente Capítulo
En el Capítulo 4: Configuración de Zustand, aprenderemos:
- Instalación y configuración de Zustand
- Creación de stores tipados
- Patrones de estado inmutable
- DevTools y debugging
¡Continúa para dominar la gestión de estado en React Native! 🚀