- Backend NestJS con módulos de autenticación, inventario, créditos - Frontend React con dashboard y componentes UI - Base de datos PostgreSQL con migraciones - Tests E2E configurados - Configuración de Docker y deployment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
534 lines
16 KiB
Markdown
534 lines
16 KiB
Markdown
# ANÁLISIS DETALLADO - UX MOBILE: Animaciones + Modo Offline
|
|
|
|
---
|
|
id: ANALISIS-DET-UX-MOBILE-001
|
|
type: DetailedAnalysis
|
|
status: Approved
|
|
created_date: 2026-01-12
|
|
updated_date: 2026-01-12
|
|
phase: 2-detallado
|
|
simco_version: "4.0.0"
|
|
---
|
|
|
|
## FASE 2: ANÁLISIS DETALLADO DE CAMBIOS
|
|
|
|
### 2.1 Archivos Nuevos - Análisis Completo
|
|
|
|
---
|
|
|
|
#### 2.1.1 `src/hooks/useAnimations.ts`
|
|
|
|
**Propósito:** Colección de hooks reutilizables para animaciones con react-native-reanimated
|
|
|
|
**Líneas de Código:** 187
|
|
**Dependencias Externas:** react-native-reanimated
|
|
**Exports:**
|
|
|
|
| Export | Tipo | Parámetros | Retorno | Uso |
|
|
|--------|------|------------|---------|-----|
|
|
| `useFadeIn` | Hook | `delay?: number` | `{ animatedStyle, opacity }` | Fade in de 0 a 1 |
|
|
| `useSlideIn` | Hook | `delay?: number, distance?: number` | `{ animatedStyle, opacity, translateY }` | Slide desde abajo |
|
|
| `useSlideFromRight` | Hook | `delay?: number, distance?: number` | `{ animatedStyle, opacity, translateX }` | Slide desde derecha |
|
|
| `usePressScale` | Hook | `pressedScale?: number` | `{ animatedStyle, onPressIn, onPressOut, scale }` | Efecto press en botones |
|
|
| `useListItemAnimation` | Hook | `index: number, baseDelay?: number` | Return de useSlideIn | Animación stagger en listas |
|
|
| `useShimmer` | Hook | - | `{ animatedStyle, shimmerValue }` | Efecto shimmer para skeletons |
|
|
| `usePulse` | Hook | `minScale?: number, maxScale?: number` | `{ animatedStyle, scale }` | Animación de pulso |
|
|
| `useToggleAnimation` | Hook | `isVisible: boolean` | `{ animatedStyle }` | Entrada/salida de elementos |
|
|
| `Animated` | Re-export | - | - | Re-export de reanimated |
|
|
|
|
**Configuración por Defecto:**
|
|
|
|
```typescript
|
|
const DEFAULT_TIMING: WithTimingConfig = {
|
|
duration: 300,
|
|
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
|
|
};
|
|
|
|
const DEFAULT_SPRING: WithSpringConfig = {
|
|
damping: 15,
|
|
stiffness: 150,
|
|
};
|
|
```
|
|
|
|
**Consumidores:**
|
|
- `src/app/(tabs)/index.tsx` (HomeScreen)
|
|
- `src/app/(tabs)/inventory.tsx` (InventoryScreen)
|
|
- `src/components/ui/Skeleton.tsx`
|
|
|
|
---
|
|
|
|
#### 2.1.2 `src/hooks/useNetworkStatus.ts`
|
|
|
|
**Propósito:** Detectar y monitorear estado de conexión de red
|
|
|
|
**Líneas de Código:** 73
|
|
**Dependencias Externas:** @react-native-community/netinfo
|
|
**Exports:**
|
|
|
|
| Export | Tipo | Parámetros | Retorno | Uso |
|
|
|--------|------|------------|---------|-----|
|
|
| `useNetworkStatus` | Hook | - | `NetworkStatus` | Estado completo de red |
|
|
| `useIsOnline` | Hook | - | `boolean` | ¿Está online? |
|
|
| `useIsOffline` | Hook | - | `boolean` | ¿Está offline? |
|
|
|
|
**Interface NetworkStatus:**
|
|
|
|
```typescript
|
|
interface NetworkStatus {
|
|
isConnected: boolean;
|
|
isInternetReachable: boolean | null;
|
|
type: NetInfoStateType;
|
|
isWifi: boolean;
|
|
isCellular: boolean;
|
|
}
|
|
```
|
|
|
|
**Comportamiento:**
|
|
- Suscripción a eventos de NetInfo en useEffect
|
|
- Fetch inicial del estado de red
|
|
- Cleanup automático en unmount
|
|
- Manejo de `isInternetReachable` null (estado indeterminado)
|
|
|
|
**Consumidores:**
|
|
- `src/components/ui/OfflineBanner.tsx`
|
|
|
|
---
|
|
|
|
#### 2.1.3 `src/theme/ThemeContext.tsx`
|
|
|
|
**Propósito:** Sistema de temas con soporte light/dark mode
|
|
|
|
**Líneas de Código:** 77
|
|
**Dependencias Externas:** react (useColorScheme)
|
|
**Exports:**
|
|
|
|
| Export | Tipo | Descripción |
|
|
|--------|------|-------------|
|
|
| `ThemeColors` | Interface | Definición de colores del tema |
|
|
| `Theme` | Interface | `{ colors: ThemeColors, isDark: boolean }` |
|
|
| `ThemeProvider` | Component | Provider del contexto |
|
|
| `useTheme` | Hook | Acceso al tema completo |
|
|
| `useColors` | Hook | Acceso solo a colores |
|
|
|
|
**Paleta Light:**
|
|
|
|
```typescript
|
|
const lightColors: ThemeColors = {
|
|
primary: '#2563eb', // Azul principal
|
|
primaryLight: '#f0f9ff', // Azul claro
|
|
background: '#f5f5f5', // Gris fondo
|
|
card: '#ffffff', // Blanco tarjetas
|
|
text: '#1a1a1a', // Negro texto
|
|
textSecondary: '#666666',// Gris texto
|
|
border: '#e5e5e5', // Gris bordes
|
|
error: '#ef4444', // Rojo error
|
|
success: '#22c55e', // Verde éxito
|
|
warning: '#f59e0b', // Naranja warning
|
|
};
|
|
```
|
|
|
|
**Paleta Dark:**
|
|
|
|
```typescript
|
|
const darkColors: ThemeColors = {
|
|
primary: '#3b82f6',
|
|
primaryLight: '#1e3a5f',
|
|
background: '#0f0f0f',
|
|
card: '#1a1a1a',
|
|
text: '#ffffff',
|
|
textSecondary: '#a3a3a3',
|
|
border: '#2d2d2d',
|
|
error: '#f87171',
|
|
success: '#4ade80',
|
|
warning: '#fbbf24',
|
|
};
|
|
```
|
|
|
|
**Consumidores:**
|
|
- `src/app/_layout.tsx` (Provider)
|
|
- `src/components/ui/Skeleton.tsx`
|
|
- `src/components/ui/AnimatedList.tsx`
|
|
- `src/components/skeletons/*.tsx` (4 archivos)
|
|
|
|
---
|
|
|
|
#### 2.1.4 `src/components/ui/Skeleton.tsx`
|
|
|
|
**Propósito:** Componentes skeleton base con animación shimmer
|
|
|
|
**Líneas de Código:** 216
|
|
**Dependencias:**
|
|
- react-native-reanimated
|
|
- `../../theme/ThemeContext`
|
|
|
|
**Exports:**
|
|
|
|
| Export | Tipo | Props | Descripción |
|
|
|--------|------|-------|-------------|
|
|
| `Skeleton` | Component | `width?, height?, borderRadius?, style?` | Skeleton base |
|
|
| `SkeletonText` | Component | `width?, height?, style?` | Línea de texto |
|
|
| `SkeletonCircle` | Component | `size?, style?` | Avatar circular |
|
|
| `SkeletonImage` | Component | `width?, height?, borderRadius?, style?` | Imagen cuadrada |
|
|
| `SkeletonCard` | Component | `style?` | Tarjeta completa |
|
|
| `SkeletonListItem` | Component | `style?` | Item de lista |
|
|
| `SkeletonStat` | Component | `style?` | Estadística |
|
|
| `SkeletonList` | Component | `count?, style?` | Lista de skeletons |
|
|
|
|
**Animación Shimmer:**
|
|
|
|
```typescript
|
|
shimmerValue.value = withRepeat(
|
|
withTiming(1, { duration: 1200 }),
|
|
-1, // Infinito
|
|
false // Sin reverse
|
|
);
|
|
|
|
// Interpolación de opacidad
|
|
opacity: interpolate(shimmerValue.value, [0, 0.5, 1], [0.3, 0.6, 0.3])
|
|
```
|
|
|
|
**Consumidores:**
|
|
- `src/app/(tabs)/index.tsx` (HomeSkeleton)
|
|
- `src/components/skeletons/InventoryItemSkeleton.tsx`
|
|
- `src/components/skeletons/StoreCardSkeleton.tsx`
|
|
- `src/components/skeletons/CreditCardSkeleton.tsx`
|
|
- `src/components/skeletons/NotificationSkeleton.tsx`
|
|
|
|
---
|
|
|
|
#### 2.1.5 `src/components/ui/OfflineBanner.tsx`
|
|
|
|
**Propósito:** Banner visual que indica pérdida de conexión
|
|
|
|
**Líneas de Código:** 115
|
|
**Dependencias:**
|
|
- react-native-reanimated
|
|
- react-native-safe-area-context
|
|
- `../../hooks/useNetworkStatus`
|
|
- @expo/vector-icons
|
|
|
|
**Exports:**
|
|
|
|
| Export | Tipo | Props | Descripción |
|
|
|--------|------|-------|-------------|
|
|
| `OfflineBanner` | Component | `message?, showIcon?` | Banner offline |
|
|
| `WithOfflineBanner` | Component | `children` | Wrapper con banner |
|
|
|
|
**Comportamiento:**
|
|
- Posición absoluta top con z-index 9999
|
|
- Animación slide desde arriba con spring
|
|
- Respeta safe area insets
|
|
- Solo renderiza cuando `isOffline === true`
|
|
- Color rojo (#EF4444) para indicar problema
|
|
|
|
**Consumidores:**
|
|
- `src/app/_layout.tsx`
|
|
|
|
---
|
|
|
|
#### 2.1.6 `src/components/ui/AnimatedList.tsx`
|
|
|
|
**Propósito:** FlatList con animaciones de entrada staggered
|
|
|
|
**Líneas de Código:** 155
|
|
**Dependencias:**
|
|
- react-native-reanimated
|
|
- `../../theme/ThemeContext`
|
|
|
|
**Exports:**
|
|
|
|
| Export | Tipo | Descripción |
|
|
|--------|------|-------------|
|
|
| `AnimatedList<T>` | Generic Component | FlatList con animaciones |
|
|
| `useListItemEntering` | Hook | Crear entering animation |
|
|
| `AnimatedListItem` | Component | Item individual animado |
|
|
|
|
**Props AnimatedList:**
|
|
|
|
```typescript
|
|
interface AnimatedListProps<T> {
|
|
renderItem: (info: { item: T; index: number }) => ReactElement;
|
|
staggerDelay?: number; // Default: 50ms
|
|
animationType?: 'fade' | 'slide' | 'spring'; // Default: 'fade'
|
|
animateOnRefresh?: boolean; // Default: true
|
|
onRefresh?: () => Promise<void> | void;
|
|
isRefreshing?: boolean;
|
|
// ...FlatListProps
|
|
}
|
|
```
|
|
|
|
**Tipos de Animación:**
|
|
|
|
| Tipo | Animación |
|
|
|------|-----------|
|
|
| `fade` | FadeIn con delay |
|
|
| `slide` | SlideInRight con delay |
|
|
| `spring` | FadeIn springify |
|
|
|
|
**Consumidores:**
|
|
- Disponible para uso en cualquier pantalla con listas
|
|
|
|
---
|
|
|
|
#### 2.1.7-10 Skeletons Específicos
|
|
|
|
**Archivos:**
|
|
- `src/components/skeletons/InventoryItemSkeleton.tsx` (72 líneas)
|
|
- `src/components/skeletons/StoreCardSkeleton.tsx` (68 líneas)
|
|
- `src/components/skeletons/CreditCardSkeleton.tsx` (115 líneas)
|
|
- `src/components/skeletons/NotificationSkeleton.tsx` (62 líneas)
|
|
|
|
**Exports por Archivo:**
|
|
|
|
| Archivo | Exports |
|
|
|---------|---------|
|
|
| InventoryItemSkeleton | `InventoryItemSkeleton`, `InventoryListSkeleton`, `InventoryStatsSkeleton` |
|
|
| StoreCardSkeleton | `StoreCardSkeleton`, `StoreListSkeleton` |
|
|
| CreditCardSkeleton | `CreditBalanceSkeleton`, `TransactionSkeleton`, `TransactionListSkeleton`, `CreditPackageSkeleton`, `CreditPackageListSkeleton` |
|
|
| NotificationSkeleton | `NotificationItemSkeleton`, `NotificationListSkeleton`, `NotificationHeaderSkeleton` |
|
|
|
|
---
|
|
|
|
### 2.2 Archivos Modificados - Análisis Completo
|
|
|
|
---
|
|
|
|
#### 2.2.1 Stores (4 archivos)
|
|
|
|
**Patrón de Modificación Común:**
|
|
|
|
```typescript
|
|
// ANTES
|
|
export const useStore = create<State>()((set, get) => ({
|
|
// ...state
|
|
}));
|
|
|
|
// DESPUÉS
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
export const useStore = create<State>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// ...state
|
|
lastFetched: null, // NUEVO campo
|
|
}),
|
|
{
|
|
name: 'miinventario-{store-name}',
|
|
storage: createJSONStorage(() => AsyncStorage),
|
|
partialize: (state) => ({
|
|
// Solo datos, no funciones
|
|
}),
|
|
}
|
|
)
|
|
);
|
|
```
|
|
|
|
**Detalle por Store:**
|
|
|
|
| Store | Nombre Persistencia | Datos Persistidos | Límite |
|
|
|-------|---------------------|-------------------|--------|
|
|
| `stores.store.ts` | `miinventario-stores` | stores, currentStore, total, lastFetched | Sin límite |
|
|
| `inventory.store.ts` | `miinventario-inventory` | items, total, selectedStoreId, lastFetched | 100 items |
|
|
| `credits.store.ts` | `miinventario-credits` | balance, transactions, transactionsTotal, lastFetched | 50 tx |
|
|
| `notifications.store.ts` | `miinventario-notifications` | notifications, unreadCount, total, lastFetched | 50 notif |
|
|
|
|
**Campos Nuevos:**
|
|
- `lastFetched: number | null` - Timestamp de última carga (para futuro stale-while-revalidate)
|
|
|
|
---
|
|
|
|
#### 2.2.2 `src/app/_layout.tsx`
|
|
|
|
**Cambios Realizados:**
|
|
|
|
| # | Cambio | Tipo | Impacto |
|
|
|---|--------|------|---------|
|
|
| 1 | Import `OfflineBanner` | Nuevo import | - |
|
|
| 2 | Import `ThemeProvider` | Nuevo import | - |
|
|
| 3 | Wrap con `ThemeProvider` | Nuevo wrapper | Tema disponible globalmente |
|
|
| 4 | Agregar `<OfflineBanner />` | Nuevo componente | Banner visible globalmente |
|
|
| 5 | `animation: 'slide_from_right'` | Nueva opción | Transiciones mejoradas |
|
|
| 6 | `animationDuration: 250` | Nueva opción | Duración consistente |
|
|
|
|
**Jerarquía de Providers:**
|
|
|
|
```
|
|
GestureHandlerRootView
|
|
└── ThemeProvider ← NUEVO
|
|
└── SafeAreaProvider
|
|
└── QueryClientProvider
|
|
└── View
|
|
├── OfflineBanner ← NUEVO
|
|
├── StatusBar
|
|
└── Stack (con animación) ← MODIFICADO
|
|
```
|
|
|
|
---
|
|
|
|
#### 2.2.3 `src/app/(tabs)/index.tsx`
|
|
|
|
**Cambios Realizados:**
|
|
|
|
| # | Cambio | Descripción |
|
|
|---|--------|-------------|
|
|
| 1 | Imports reanimated | `FadeIn, FadeInDown, FadeInRight, Layout` |
|
|
| 2 | Import hooks | `useFadeIn, usePressScale` |
|
|
| 3 | Import skeletons | `Skeleton, SkeletonText, SkeletonStat` |
|
|
| 4 | `AnimatedTouchable` | `Animated.createAnimatedComponent(TouchableOpacity)` |
|
|
| 5 | `ActionCard` component | Nuevo componente con animación |
|
|
| 6 | `StatCard` component | Nuevo componente con animación |
|
|
| 7 | `HomeSkeleton` component | Skeleton para carga inicial |
|
|
| 8 | Estado `initialLoad` | Control de skeleton vs contenido |
|
|
| 9 | Animaciones en header | `FadeIn.duration(400)` |
|
|
| 10 | Animaciones en cards | `FadeInDown`, `FadeInRight` con delays |
|
|
|
|
**Componentes Nuevos Internos:**
|
|
|
|
```typescript
|
|
function ActionCard({ icon, title, description, onPress, index }) {
|
|
const { animatedStyle, onPressIn, onPressOut } = usePressScale(0.98);
|
|
return (
|
|
<Animated.View entering={FadeInRight.delay(200 + index * 100)}>
|
|
<AnimatedTouchable style={[styles.actionCard, animatedStyle]} ...>
|
|
...
|
|
</AnimatedTouchable>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
function StatCard({ value, label, index }) {
|
|
return (
|
|
<Animated.View entering={FadeInDown.delay(400 + index * 100)}>
|
|
...
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
function HomeSkeleton() {
|
|
return (
|
|
<View>
|
|
<SkeletonText /><Skeleton /><SkeletonStat />...
|
|
</View>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### 2.2.4 `src/app/(tabs)/inventory.tsx`
|
|
|
|
**Cambios Realizados:**
|
|
|
|
| # | Cambio | Descripción |
|
|
|---|--------|-------------|
|
|
| 1 | Imports reanimated | `FadeIn, FadeInDown, FadeInRight, FadeOut, Layout` |
|
|
| 2 | Import hooks | `usePressScale` |
|
|
| 3 | Import skeletons | `InventoryListSkeleton` |
|
|
| 4 | `AnimatedTouchable` | Para items de lista |
|
|
| 5 | `InventoryItemCard` component | Nuevo componente con animación |
|
|
| 6 | Estado `initialLoad` | Control de skeleton vs lista |
|
|
| 7 | Animaciones en header | `FadeIn`, `FadeInDown` |
|
|
| 8 | Animaciones en items | `FadeInRight` con delay |
|
|
| 9 | Animación de salida | `FadeOut` al eliminar |
|
|
| 10 | `Layout.springify()` | Animación de reordenamiento |
|
|
|
|
**Componente InventoryItemCard:**
|
|
|
|
```typescript
|
|
function InventoryItemCard({ item, index }) {
|
|
const { animatedStyle, onPressIn, onPressOut } = usePressScale(0.98);
|
|
|
|
return (
|
|
<Animated.View
|
|
entering={FadeInRight.delay(Math.min(index * 50, 300)).duration(300)}
|
|
exiting={FadeOut.duration(200)}
|
|
layout={Layout.springify()}
|
|
>
|
|
<AnimatedTouchable style={[styles.itemCard, animatedStyle]} ...>
|
|
{/* Item content */}
|
|
</AnimatedTouchable>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.3 Resumen de Líneas de Código
|
|
|
|
| Categoría | Archivos | Líneas Nuevas | Líneas Modificadas |
|
|
|-----------|----------|---------------|-------------------|
|
|
| Hooks | 2 | 260 | 0 |
|
|
| Theme | 1 | 77 | 0 |
|
|
| Components/UI | 3 | 486 | 0 |
|
|
| Components/Skeletons | 4 | 317 | 0 |
|
|
| Stores | 4 | 0 | ~80 (20 por archivo) |
|
|
| Screens | 3 | 0 | ~200 |
|
|
|
|
**Total:** ~1,140 líneas nuevas + ~280 líneas modificadas
|
|
|
|
---
|
|
|
|
### 2.4 Validación de Exports/Imports
|
|
|
|
#### Grafo de Dependencias Internas:
|
|
|
|
```
|
|
ThemeContext.tsx
|
|
↓
|
|
┌───┴────────────────────────────────────────┐
|
|
│ │
|
|
Skeleton.tsx ← AnimatedList.tsx │
|
|
↓ │
|
|
┌───┴───────────────────────┐ │
|
|
│ │ │ │ │ │
|
|
│ Inventory Store Credit Notification │
|
|
│ Skeleton Skeleton Skeleton Skeleton │
|
|
└───────────────────────────────────────────┘
|
|
|
|
useNetworkStatus.ts
|
|
↓
|
|
OfflineBanner.tsx
|
|
↓
|
|
_layout.tsx
|
|
|
|
useAnimations.ts
|
|
↓
|
|
┌───┴───────────────┐
|
|
│ │
|
|
(tabs)/index.tsx (tabs)/inventory.tsx
|
|
```
|
|
|
|
#### Validación de Imports:
|
|
|
|
| Archivo | Imports | ¿Válido? |
|
|
|---------|---------|----------|
|
|
| `useAnimations.ts` | react, react-native-reanimated | ✅ |
|
|
| `useNetworkStatus.ts` | react, @react-native-community/netinfo | ✅ |
|
|
| `ThemeContext.tsx` | react, react-native | ✅ |
|
|
| `Skeleton.tsx` | react, react-native, react-native-reanimated, ThemeContext | ✅ |
|
|
| `OfflineBanner.tsx` | react, react-native, reanimated, safe-area, useNetworkStatus, icons | ✅ |
|
|
| `AnimatedList.tsx` | react, react-native, reanimated, ThemeContext | ✅ |
|
|
| `_layout.tsx` | expo-router, react-query, etc, OfflineBanner, ThemeContext | ✅ |
|
|
|
|
---
|
|
|
|
### 2.5 Conclusión Fase 2
|
|
|
|
**Estado:** ✅ Análisis Detallado Completado
|
|
|
|
**Hallazgos:**
|
|
1. Todos los archivos nuevos siguen patrones consistentes
|
|
2. No hay dependencias circulares
|
|
3. Todos los imports son válidos
|
|
4. Los stores mantienen compatibilidad hacia atrás
|
|
5. Las pantallas mantienen funcionalidad existente + mejoras
|
|
|
|
**Siguiente Fase:** Planeación de documentación basada en este análisis
|
|
|
|
---
|
|
|
|
**Última Actualización:** 2026-01-12
|
|
**Responsable:** Claude Opus 4.5
|